diff --git a/.DS_Store b/.DS_Store
new file mode 100644
index 0000000..2eca365
Binary files /dev/null and b/.DS_Store differ
diff --git a/.gitignore b/.gitignore
index 90f8fbb..f8ee27a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -173,4 +173,10 @@ poetry.toml
# LSP config files
pyrightconfig.json
-# End of https://www.toptal.com/developers/gitignore/api/python
\ No newline at end of file
+# End of https://www.toptal.com/developers/gitignore/api/python
+
+/data/
+/.data/
+
+# LLM
+.claude/
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..61e83a8
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,86 @@
+# Agent Coding Style Guidelines
+
+This document outlines specific coding style conventions to be followed for all code generation and modification tasks.
+
+## 1. Docstrings
+
+All function and method docstrings should be written in the imperative mood.
+
+- **DO**: `"""Create a new user in the database."""`
+- **DON'T**: `"""Creates a new user in the database."""`
+- **DON'T**: `"""This function creates a new user..."""`
+
+## 2. Function and Method Ordering
+
+Code within a file should be organized in order of abstraction, from highest to lowest (top-down).
+
+- The main entry point or the highest-level orchestration function (e.g., `main`) should appear first.
+- Helper functions called by the main function should be defined below it.
+- Lower-level helpers called by other helpers should be defined even further down.
+
+### Example Structure:
+
+```python
+def main():
+ """Run the full process."""
+ # High-level orchestration
+ data = load_data()
+ processed_data = process_data(data)
+ save_data(processed_data)
+
+# --- Helper Functions ---
+
+def load_data():
+ """Load data from the source."""
+ # ... implementation ...
+
+def process_data(data):
+ """Process the raw data."""
+ # ... implementation ...
+
+def save_data(data):
+ """Save the processed data."""
+ # ... implementation ...
+```
+
+## 3. Trailing Commas
+
+Use trailing commas in multiline lists, tuples, dictionaries, function definitions, and function calls where each item is on a new line. This improves readability and simplifies diffs in version control.
+
+### Example:
+
+```python
+# List
+items = [
+ "item_a",
+ "item_b",
+ "item_c", # Trailing comma here
+]
+
+# Function definition
+def my_function(
+ param_a: int,
+ param_b: str, # Trailing comma here
+) -> bool:
+ pass
+
+# Function call
+result = another_function(
+ arg_a=1,
+ arg_b="hello", # Trailing comma here
+)
+```
+
+## 4. Configuration File Comments
+
+In configuration files (e.g., `.yaml`), add comments to document the possible values for fields that have a limited, explicit set of options (i.e., enums).
+
+### Example:
+
+```yaml
+training:
+ optimizer: "Adam" # Options: "Adam", "SGD", "RMSprop"
+ early_stopping:
+ metric: "val_loss" # Options: "val_loss", "val_accuracy"
+ mode: "min" # Options: "min", "max"
+```
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8d4674f..f6f7f41 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -47,6 +47,37 @@ git pull --rebase origin dev
---
+## 🆕 Working on Dependent Features
+
+Sometimes you may need to start working on a feature (B) that depends on another feature (A) that is not yet merged into `dev`. In this case, you can create a new branch for feature B from the branch of feature A.
+
+1. **Create a branch for feature B from branch A:**
+
+ ```bash
+ # Make sure you are on branch A
+ git checkout feat/branch-A
+
+ # Create branch B from branch A
+ git checkout -b feat/branch-B
+ ```
+
+2. **Work on feature B and once feature A is merged into `dev`, you need to rebase your branch B onto `dev`.**
+
+ ```bash
+ # Make sure you are on branch B
+ git checkout feat/branch-B
+
+ # Fetch the latest changes from the remote
+ git fetch origin
+
+ # Rebase your branch onto dev
+ git rebase origin/dev
+ ```
+
+ This will reapply your changes from branch B on top of the `dev` branch.
+
+---
+
## 4️⃣ Squashing Commits (Optional but Recommended)
Before pushing your branch, if you have multiple commits, it is recommended to **squash them into a single commit**:
diff --git a/README.md b/README.md
index ba8e4ca..ed654d2 100644
--- a/README.md
+++ b/README.md
@@ -3,3 +3,223 @@
Machine Learning Project for senior year Computer Science ML course.

+
+## Overview
+
+This project implements an AutoML pipeline for **SEM (Scanning Electron Microscopy) image segmentation**. It provides two main workflows:
+
+1. **AutoML Exploration** - Automatically explore combinations of augmentations and models to find optimal configurations
+2. **Swin Model Training** - Dedicated training pipeline for Swin Transformer segmentation with learning curve analysis
+
+## Installation
+
+This project uses [uv](https://docs.astral.sh/uv/) for dependency management.
+
+```bash
+# Clone the repository
+git clone https://github.com/CfM47/ML-Project.git
+cd ML-Project
+
+# Install dependencies
+uv sync
+
+# Install dev dependencies (for testing/linting)
+uv sync --group dev
+```
+
+## Data Setup
+
+### SEM Images
+
+Place the SEM Images dataset in the `data/sem_images/raw/` directory:
+
+```
+data/sem_images/raw/Brittle/{images.png...}
+data/sem_images/raw/Ductile/{images.png...}
+```
+
+For training/test splits:
+```
+data/train/unlabeled/{images.png...}
+data/train/labeled/{masks.png...}
+data/test/unlabeled/{images.png...}
+data/test/labeled/{masks.png...}
+```
+
+## Usage
+
+### Workflow 1: AutoML Exploration
+
+The AutoML system explores combinations of augmentation strategies and segmentation models using k-fold cross-validation.
+
+```python
+from main import _run_with_setup
+
+_run_with_setup(
+ unlabeled_dir="data/train/unlabeled",
+ labeled_dir="data/train/labeled",
+ classification_dataset_dir="data/classification",
+ auto_ml_cache_dir="cache/automl",
+ augmentator_indices=[0, 1], # Optional: filter augmentators
+ model_indices=[0, 1], # Optional: filter models
+)
+```
+
+**Available Models** (via `setup/models/setup.py`):
+- ViT Segmentation Model
+- Swin Segmentation Model
+- QuadTree + CNN/ViT classifiers
+- SlidingWindow + CNN/ViT classifiers
+
+**Available Augmentations** (via `setup/augmentators/setup.py`):
+- Identity (no augmentation)
+- Combined 2Geo + 2Photo + 1SEM
+- Combined 3Geo + 1Photo + 1SEM
+
+### Workflow 2: Swin Model Training
+
+Dedicated training pipeline with learning curve analysis and early stopping support.
+
+#### Learning Curve Validation
+
+Run k-fold cross-validation at varying training percentages:
+
+```python
+from model.swin.train import run_percentage_validation
+from model.swin.config import SwinTrainingConfig
+
+config = SwinTrainingConfig(
+ train_percentages=[10, 20, 30, 40, 50, 60, 70, 80],
+ n_folds=5,
+ epochs=40,
+ patience=5, # Early stopping (None to disable)
+)
+
+metrics, fig = run_percentage_validation(
+ train_unlabeled_dir="data/train/unlabeled",
+ train_labeled_dir="data/train/labeled",
+ config=config,
+)
+```
+
+#### Final Model Training
+
+Train on full dataset with 80/20 validation split:
+
+```python
+from model.swin.train import run_final_training
+from model.swin.config import SwinTrainingConfig
+
+config = SwinTrainingConfig(
+ epochs=40,
+ patience=5,
+ output_dir="results/swin",
+)
+
+model, test_metrics, mask_pairs, fig = run_final_training(
+ train_unlabeled_dir="data/train/unlabeled",
+ train_labeled_dir="data/train/labeled",
+ test_unlabeled_dir="data/test/unlabeled",
+ test_labeled_dir="data/test/labeled",
+ config=config,
+)
+```
+
+#### CLI Usage
+
+```bash
+# Learning curve validation
+python -m model.swin.train validate \
+ --train-unlabeled data/train/unlabeled \
+ --train-labeled data/train/labeled \
+ --output-dir results/swin
+
+# Final training with test evaluation
+python -m model.swin.train train \
+ --train-unlabeled data/train/unlabeled \
+ --train-labeled data/train/labeled \
+ --test-unlabeled data/test/unlabeled \
+ --test-labeled data/test/labeled \
+ --output-dir results/swin
+```
+
+### Kaggle Notebooks
+
+Pre-configured notebooks for running on Kaggle are available in `kaggle/`:
+
+- `run-automl.ipynb` - AutoML exploration
+- `run-training.ipynb` - Swin final model training
+- `run-validation.ipynb` - Swin learning curve validation
+
+## Configuration
+
+### SwinTrainingConfig
+
+| Parameter | Type | Default | Description |
+|-----------|------|---------|-------------|
+| `train_percentages` | `List[int]` | `[10, 20, ..., 80]` | Percentages for learning curve |
+| `n_folds` | `int` | `5` | Number of cross-validation folds |
+| `epochs` | `int` | `40` | Training epochs |
+| `batch_size` | `int` | `2` | Batch size |
+| `learning_rate` | `float` | `1e-4` | Learning rate |
+| `embed_dim` | `int` | `96` | Swin embedding dimension |
+| `depths` | `List[int]` | `[2, 2, 6, 2]` | Swin layer depths |
+| `num_heads` | `List[int]` | `[3, 6, 12, 24]` | Swin attention heads |
+| `patience` | `int \| None` | `None` | Early stopping patience (None = disabled) |
+| `augmentation_copies` | `int` | `2` | Augmentation copies per sample |
+| `num_test_visualizations` | `int` | `10` | Samples to visualize |
+| `output_dir` | `Path` | `results/swin` | Output directory |
+| `seed` | `int` | `42` | Random seed |
+| `device` | `str` | `"auto"` | Device: "auto", "cuda", "mps", "cpu" |
+
+## Project Structure
+
+```
+ML-Project/
+├── auto_ml/ # Core AutoML framework
+│ ├── implementations/ # Concrete implementations
+│ │ ├── augmentators/ # Data augmentation strategies
+│ │ ├── classifiers/ # CNN, ViT classifiers
+│ │ ├── evaluators/ # Metrics (Dice, IoU, Accuracy, etc.)
+│ │ ├── segmentators/ # Swin, ViT, QuadTree, SlidingWindow
+│ │ ├── datasets.py # Dataset loading utilities
+│ │ └── nodes.py # AutoML pipeline nodes
+│ ├── interfaces.py # Abstract interfaces
+│ └── automl.py # AutoML orchestration
+├── model/ # Swin training pipeline
+│ └── swin/
+│ ├── config.py # SwinTrainingConfig
+│ ├── train.py # Training entry points
+│ ├── data.py # Data utilities
+│ ├── evaluation.py # Evaluation helpers
+│ ├── metrics.py # Metrics dataclasses
+│ └── visualization.py # Plotting utilities
+├── setup/ # Pre-configured setups for AutoML
+│ ├── augmentators/ # Augmentation node configurations
+│ ├── evaluator/ # Evaluator configurations
+│ └── models/ # Model node configurations
+├── kaggle/ # Kaggle notebook templates
+├── tests/ # Unit tests
+├── main.py # AutoML entry point
+└── pyproject.toml # Project configuration
+```
+
+## Development
+
+```bash
+# Run tests
+make test
+
+# Type checking
+make typecheck
+
+# Linting
+make lint
+
+# Format code
+make format
+```
+
+## License
+
+This project is for educational purposes as part of a senior year ML course.
diff --git a/auto_ml/__init__.py b/auto_ml/__init__.py
new file mode 100644
index 0000000..93e9b34
--- /dev/null
+++ b/auto_ml/__init__.py
@@ -0,0 +1,3 @@
+"""AutoML package."""
+
+from auto_ml.automl import AutoML as AutoML
diff --git a/auto_ml/automl.py b/auto_ml/automl.py
new file mode 100644
index 0000000..b40b6f3
--- /dev/null
+++ b/auto_ml/automl.py
@@ -0,0 +1,310 @@
+import copy
+import json
+import time
+from pathlib import Path
+from typing import Any, Dict, List, Optional
+
+from auto_ml.implementations import DataAugmentatorNode, EvaluatorNode, ModelNode
+from auto_ml.interfaces import SegmentationDatasetInterface
+
+
+class AutoML:
+ """
+ AutoML Orchestrator.
+
+ Manages the execution of experiments across multiple data augmentation
+ strategies and models. Includes caching and execution time tracking.
+ """
+
+ def __init__(self, cache_dir: Path = Path("automl_cache")) -> None: # noqa: D107
+ self.results: Dict[str, Any] = {}
+ self.cache_dir = Path(cache_dir)
+ self.cache_dir.mkdir(exist_ok=True)
+
+ self.results_cache_path = self.cache_dir / "results_cache.json"
+ self.execution_times_cache_path = self.cache_dir / "execution_times_cache.json"
+
+ self.results_cache: Dict[str, Any] = {}
+ self.execution_times_cache: Dict[str, Any] = {}
+
+ self._load_caches()
+
+ def _load_caches(self) -> None:
+ """Load results and execution times caches from JSON files."""
+ if self.results_cache_path.exists():
+ try:
+ with open(self.results_cache_path, "r") as f:
+ self.results_cache = json.load(f)
+ print(f"Loaded results cache from {self.results_cache_path}")
+ except (json.JSONDecodeError, IOError) as e:
+ print(f"Warning: Failed to load results cache: {e}")
+ self.results_cache = {}
+
+ if self.execution_times_cache_path.exists():
+ try:
+ with open(self.execution_times_cache_path, "r") as f:
+ self.execution_times_cache = json.load(f)
+ print(
+ f"Loaded execution times cache from "
+ f"{self.execution_times_cache_path}",
+ )
+ except (json.JSONDecodeError, IOError) as e:
+ print(f"Warning: Failed to load execution times cache: {e}")
+ self.execution_times_cache = {}
+
+ def _save_results_cache(self) -> None:
+ """Save results cache to JSON file."""
+ try:
+ with open(self.results_cache_path, "w") as f:
+ json.dump(self.results_cache, f, indent=2, default=str)
+ print(f"Saved results cache to {self.results_cache_path}")
+ except IOError as e:
+ print(f"Warning: Failed to save results cache: {e}")
+
+ def _save_execution_times_cache(self) -> None:
+ """Save execution times cache to JSON file."""
+ try:
+ with open(self.execution_times_cache_path, "w") as f:
+ json.dump(self.execution_times_cache, f, indent=2)
+ print(
+ f"Saved execution times cache to {self.execution_times_cache_path}",
+ )
+ except IOError as e:
+ print(f"Warning: Failed to save execution times cache: {e}")
+
+ def _clear_cache_entry(self, augmentator_name: str, model_name: str) -> None:
+ """Clear cache entry for a specific augmentator+model pair."""
+ if augmentator_name in self.results_cache:
+ if model_name in self.results_cache[augmentator_name]:
+ del self.results_cache[augmentator_name][model_name]
+
+ if augmentator_name in self.execution_times_cache:
+ if model_name in self.execution_times_cache[augmentator_name]:
+ del self.execution_times_cache[augmentator_name][model_name]
+
+ self._save_results_cache()
+ self._save_execution_times_cache()
+ print(f"Cleared cache for {augmentator_name}:{model_name}")
+
+ def _is_cached(self, augmentator_name: str, model_name: str) -> bool:
+ """Check if a combination is in the results cache."""
+ return (
+ augmentator_name in self.results_cache
+ and model_name in self.results_cache[augmentator_name]
+ )
+
+ def _get_cached_result(
+ self,
+ augmentator_name: str,
+ model_name: str,
+ ) -> Optional[Dict[str, Any]]:
+ """Retrieve cached result for a specific combination."""
+ if self._is_cached(augmentator_name, model_name):
+ result: Dict[str, Any] = self.results_cache[augmentator_name][model_name]
+ return result
+ return None
+
+ def _cache_result(
+ self,
+ augmentator_name: str,
+ model_name: str,
+ result: Dict[str, Any],
+ execution_time: float,
+ ) -> None:
+ """Cache result and execution time for a specific combination."""
+ if augmentator_name not in self.results_cache:
+ self.results_cache[augmentator_name] = {}
+
+ self.results_cache[augmentator_name][model_name] = result
+
+ if augmentator_name not in self.execution_times_cache:
+ self.execution_times_cache[augmentator_name] = {}
+
+ self.execution_times_cache[augmentator_name][model_name] = execution_time
+
+ self._save_results_cache()
+ self._save_execution_times_cache()
+
+ def run_experiment(
+ self,
+ dataset: SegmentationDatasetInterface,
+ augmentator_nodes: List[DataAugmentatorNode],
+ model_nodes: List[ModelNode],
+ evaluator_node: Optional[EvaluatorNode] = None,
+ clear_cache: Optional[List[tuple]] = None,
+ ) -> Dict[str, Dict[str, Dict[str, Any]]]:
+ """
+ Run a full experiment.
+
+ Iterates over each augmentator node and each model node.
+
+ Args:
+ dataset: The base dataset.
+ augmentator_nodes: List of DataAugmentatorNode instances.
+ model_nodes: List of ModelNode instances.
+ evaluator_node: Optional single EvaluatorNode instance.
+ clear_cache: Optional list of (augmentator_name, model_name)
+ tuples to clear.
+
+ Returns:
+ Dictionary structure:
+ {
+ augmentator_name: {
+ model_name: {
+ "mask_pairs": List[List[MaskPair]],
+ "evaluation": Dict[str, Any] (if evaluator_node provided)
+ }
+ }
+ }
+
+ """
+ # Clear cache entries if specified
+ if clear_cache:
+ for aug_name, model_name in clear_cache:
+ self._clear_cache_entry(aug_name, model_name)
+
+ print(
+ f"Starting AutoML Experiment with {len(augmentator_nodes)} augmentators",
+ f"and {len(model_nodes)} models.",
+ )
+
+ experiment_results: Dict[str, Any] = {}
+
+ for aug_node in augmentator_nodes:
+ aug_name = aug_node.name
+ print(f"\nProcessing Data Augmentation Node: {aug_name}")
+ experiment_results[aug_name] = {}
+
+ # 1. Process Data (Split & Augment)
+ try:
+ dataset_pairs = aug_node.process(dataset)
+ print(f" Generated {len(dataset_pairs)} dataset pairs (folds).")
+
+ # 2. Iterate Models
+ for model_node_template in model_nodes:
+ model_name = model_node_template.name
+
+ # Check cache first
+ if self._is_cached(aug_name, model_name):
+ print(f" Training Model Node: {model_name} (cached)")
+ cached_result = self._get_cached_result(
+ aug_name,
+ model_name,
+ )
+ execution_time = self.execution_times_cache[aug_name][
+ model_name
+ ]
+ experiment_results[aug_name][model_name] = cached_result
+ print(
+ f" Loaded from cache "
+ f"(execution time: {execution_time:.2f}s)",
+ )
+ continue
+
+ print(f" Training Model Node: {model_name}")
+ cycle_start_time = time.time()
+
+ try:
+ # CRITICAL: Use a fresh copy of the model for this pipeline run
+ # to ensure we start training from scratch (0).
+ model_node = copy.deepcopy(model_node_template)
+
+ # Train Model - returns List[List[MaskPair]]
+ mask_pairs = model_node.train(dataset_pairs)
+
+ total_pairs = sum(len(fold) for fold in mask_pairs)
+ print(f" Finished. Collected {total_pairs} mask pairs.")
+
+ # Build result dict
+ result: Dict[str, Any] = {
+ "train_size": len(dataset_pairs[0][0]),
+ "test_size": len(dataset_pairs[0][1]),
+ }
+
+ # 3. Pass to Evaluator Node (if provided)
+ if evaluator_node:
+ evaluation_results = evaluator_node.evaluate(mask_pairs)
+ result["evaluations"] = evaluation_results
+
+ experiment_results[aug_name][model_name] = result
+
+ # Cache the result and execution time
+ cycle_end_time = time.time()
+ execution_time = cycle_end_time - cycle_start_time
+
+ # Extract training history if available
+ # Extract training history if available
+ if (
+ hasattr(model_node, "last_training_metrics")
+ and model_node.last_training_metrics
+ ):
+ # We might have multiple folds.
+ # Let's simple store the history of the last fold
+ # or list of all.
+ # Since result is per model/aug pair,
+ # maybe we want to store all?
+ # For simplicity, append the history to the result object
+ # We'll use the 'history' key which is a list of lists
+ # (one per fold)
+ result["training_history"] = [
+ m.history for m in model_node.last_training_metrics
+ ]
+
+ self._cache_result(
+ aug_name,
+ model_name,
+ result,
+ execution_time,
+ )
+ print(
+ f" Cached result "
+ f"(execution time: {execution_time:.2f}s)",
+ )
+
+ except Exception as e:
+ print(f" Error training {model_name} on {aug_name}: {e}")
+ experiment_results[aug_name][model_name] = {"error": str(e)}
+
+ except Exception as e:
+ print(f" Error processing augmentation {aug_name}: {e}")
+ experiment_results[aug_name] = {"error": str(e)}
+
+ self.results = experiment_results
+ return experiment_results
+
+ def get_summary(self) -> str:
+ """
+ Get a readable summary of the experiment results.
+
+ Returns:
+ String summary.
+
+ """
+ if not self.results:
+ return "No results available."
+
+ summary = "=== AutoML Experiment Summary ===\n"
+
+ for aug_name, models_data in self.results.items():
+ summary += f"\nData Augmentation: {aug_name}\n"
+
+ if "error" in models_data:
+ summary += f" Error: {models_data['error']}\n"
+ continue
+
+ for model_name, res in models_data.items():
+ if isinstance(res, dict) and "error" in res:
+ summary += f" Model: {model_name} -> Error: {res['error']}\n"
+ elif isinstance(res, dict):
+ mask_pairs = res.get("mask_pairs", [])
+ total_pairs = sum(len(fold) for fold in mask_pairs)
+ summary += f" Model: {model_name} -> {len(mask_pairs)} folds, "
+ summary += f"{total_pairs} mask pairs\n"
+
+ # Show evaluation results if available
+ if "evaluation" in res:
+ for eval_name, eval_result in res["evaluation"].items():
+ summary += f" {eval_name}: {eval_result}\n"
+
+ summary += "\n================================="
+ return summary
diff --git a/auto_ml/implementations/__init__.py b/auto_ml/implementations/__init__.py
new file mode 100644
index 0000000..3dc51c3
--- /dev/null
+++ b/auto_ml/implementations/__init__.py
@@ -0,0 +1,143 @@
+"""Concrete implementations of the AutoML interfaces."""
+
+# Augmentators
+from auto_ml.implementations.augmentators import (
+ AdaptiveHistogramEqualizationAugmentator,
+ BrightnessAugmentator,
+ ChargingArtifactAugmentator,
+ ContrastAugmentator,
+ ElasticDeformationAugmentator,
+ GammaAugmentator,
+ GaussianBlurAugmentator,
+ GaussianNoiseAugmentator,
+ HorizontalFlipAugmentator,
+ IdentityAugmentator,
+ MultiplyDatasetAugmentator,
+ OneOfAugmentator,
+ RandomApplyAugmentator,
+ RandomChoiceAugmentator,
+ RandomCropAugmentator,
+ RotationAugmentator,
+ ScaleAugmentator,
+ ScanLineNoiseAugmentator,
+ SequentialAugmentator,
+ TranslationAugmentator,
+ VerticalFlipAugmentator,
+)
+
+# Datasets
+from auto_ml.implementations.datasets import load_dataset_from_directories
+
+# Evaluators
+from auto_ml.implementations.evaluators.accuracy import AccuracyEvaluator
+from auto_ml.implementations.evaluators.autoencoder import AutoencoderMaskEvaluator
+
+# Dice metrics
+from auto_ml.implementations.evaluators.dice import (
+ DiceClass0Evaluator,
+ DiceClass1Evaluator,
+ DiceClass2Evaluator,
+ DiceMacroAverageEvaluator,
+ DiceWeightedAverageEvaluator,
+)
+
+# IoU metrics
+from auto_ml.implementations.evaluators.iou import (
+ IoUClass0Evaluator,
+ IoUClass1Evaluator,
+ IoUClass2Evaluator,
+ IoUMacroAverageEvaluator,
+ IoUWeightedAverageEvaluator,
+)
+
+# Precision metrics
+from auto_ml.implementations.evaluators.precision import (
+ PrecisionClass0Evaluator,
+ PrecisionClass1Evaluator,
+ PrecisionClass2Evaluator,
+ PrecisionMacroAverageEvaluator,
+)
+
+# Recall metrics
+from auto_ml.implementations.evaluators.recall import (
+ RecallClass0Evaluator,
+ RecallClass1Evaluator,
+ RecallClass2Evaluator,
+ RecallMacroAverageEvaluator,
+)
+
+# Nodes
+from auto_ml.implementations.nodes import (
+ DataAugmentatorNode,
+ EvaluatorNode,
+ ModelNode,
+)
+from auto_ml.implementations.segmentators import (
+ QuadtreeSegmentationModel,
+ SwinModel,
+ ViTModel,
+)
+
+# Models
+from auto_ml.implementations.segmentators.base import InMemoryPyTorchDataset
+
+__all__ = [
+ # Augmentators
+ "IdentityAugmentator",
+ "RotationAugmentator",
+ "HorizontalFlipAugmentator",
+ "VerticalFlipAugmentator",
+ "ScaleAugmentator",
+ "TranslationAugmentator",
+ "RandomCropAugmentator",
+ "BrightnessAugmentator",
+ "ContrastAugmentator",
+ "GaussianNoiseAugmentator",
+ "GaussianBlurAugmentator",
+ "GammaAugmentator",
+ "SequentialAugmentator",
+ "RandomChoiceAugmentator",
+ "RandomApplyAugmentator",
+ "OneOfAugmentator",
+ "MultiplyDatasetAugmentator",
+ "ElasticDeformationAugmentator",
+ "AdaptiveHistogramEqualizationAugmentator",
+ "ChargingArtifactAugmentator",
+ "ScanLineNoiseAugmentator",
+ # Datasets
+ "load_dataset_from_directories",
+ # Nodes
+ "DataAugmentatorNode",
+ "ModelNode",
+ "EvaluatorNode",
+ # Models
+ "InMemoryPyTorchDataset",
+ "ViTModel",
+ "SwinModel",
+ "QuadtreeSegmentationModel",
+ # Evaluators
+ "AccuracyEvaluator",
+ "AutoencoderMaskEvaluator",
+ # Dice Metrics
+ "DiceClass0Evaluator",
+ "DiceClass1Evaluator",
+ "DiceClass2Evaluator",
+ "DiceMacroAverageEvaluator",
+ "DiceWeightedAverageEvaluator",
+ # IoU Metrics
+ "IoUClass0Evaluator",
+ "IoUClass1Evaluator",
+ "IoUClass2Evaluator",
+ "IoUMacroAverageEvaluator",
+ "IoUWeightedAverageEvaluator",
+ # Precision Metrics
+ "PrecisionClass0Evaluator",
+ "PrecisionClass1Evaluator",
+ "PrecisionClass2Evaluator",
+ "PrecisionMacroAverageEvaluator",
+ # Recall Metrics
+ "RecallClass0Evaluator",
+ "RecallClass1Evaluator",
+ "RecallClass2Evaluator",
+ "RecallMacroAverageEvaluator",
+]
diff --git a/auto_ml/implementations/augmentators/__init__.py b/auto_ml/implementations/augmentators/__init__.py
new file mode 100644
index 0000000..5d976b1
--- /dev/null
+++ b/auto_ml/implementations/augmentators/__init__.py
@@ -0,0 +1,68 @@
+"""Data augmentation implementations."""
+
+# Identity augmentator (baseline)
+# Composite augmentations (combine multiple augmentations)
+from auto_ml.implementations.augmentators.composite import (
+ MultiplyDatasetAugmentator,
+ OneOfAugmentator,
+ RandomApplyAugmentator,
+ RandomChoiceAugmentator,
+ SequentialAugmentator,
+)
+
+# Geometric augmentations (affect both image and mask)
+from auto_ml.implementations.augmentators.geometric import (
+ HorizontalFlipAugmentator,
+ RandomCropAugmentator,
+ RotationAugmentator,
+ ScaleAugmentator,
+ TranslationAugmentator,
+ VerticalFlipAugmentator,
+)
+from auto_ml.implementations.augmentators.identity import IdentityAugmentator
+
+# Photometric augmentations (affect only image)
+from auto_ml.implementations.augmentators.photometric import (
+ BrightnessAugmentator,
+ ContrastAugmentator,
+ GammaAugmentator,
+ GaussianBlurAugmentator,
+ GaussianNoiseAugmentator,
+)
+
+# SEM-specific augmentations
+from auto_ml.implementations.augmentators.sem_specific import (
+ AdaptiveHistogramEqualizationAugmentator,
+ ChargingArtifactAugmentator,
+ ElasticDeformationAugmentator,
+ ScanLineNoiseAugmentator,
+)
+
+__all__ = [
+ # Identity
+ "IdentityAugmentator",
+ # Geometric
+ "RotationAugmentator",
+ "HorizontalFlipAugmentator",
+ "VerticalFlipAugmentator",
+ "ScaleAugmentator",
+ "TranslationAugmentator",
+ "RandomCropAugmentator",
+ # Photometric
+ "BrightnessAugmentator",
+ "ContrastAugmentator",
+ "GaussianNoiseAugmentator",
+ "GaussianBlurAugmentator",
+ "GammaAugmentator",
+ # Composite
+ "SequentialAugmentator",
+ "RandomChoiceAugmentator",
+ "RandomApplyAugmentator",
+ "OneOfAugmentator",
+ "MultiplyDatasetAugmentator",
+ # SEM-specific
+ "ElasticDeformationAugmentator",
+ "AdaptiveHistogramEqualizationAugmentator",
+ "ChargingArtifactAugmentator",
+ "ScanLineNoiseAugmentator",
+]
diff --git a/auto_ml/implementations/augmentators/base.py b/auto_ml/implementations/augmentators/base.py
new file mode 100644
index 0000000..58053c4
--- /dev/null
+++ b/auto_ml/implementations/augmentators/base.py
@@ -0,0 +1,167 @@
+"""Base classes and utilities for data augmentation."""
+
+from typing import Tuple
+
+import numpy as np
+from scipy import ndimage
+
+from auto_ml.interfaces import ImageArray, MaskArray
+
+
+def apply_affine_to_image(
+ image: ImageArray,
+ matrix: np.ndarray,
+ order: int = 1,
+ fill_value: float = 0.0,
+) -> ImageArray:
+ """
+ Apply affine transformation to an image.
+
+ Args:
+ image: Input image array (H, W) or (H, W, C).
+ matrix: 2x3 or 3x3 affine transformation matrix.
+ order: Interpolation order (0=nearest, 1=bilinear, 3=cubic).
+ fill_value: Value to use for pixels outside boundaries.
+
+ Returns:
+ Transformed image with same shape as input.
+
+ """
+ # Handle both 2D and 3D images
+ is_grayscale = image.ndim == 2
+
+ if is_grayscale:
+ result = ndimage.affine_transform(
+ image,
+ matrix[:2, :2],
+ offset=matrix[:2, 2] if matrix.shape[0] == 3 else np.zeros(2),
+ order=order,
+ mode="constant",
+ cval=fill_value,
+ )
+ else:
+ # Apply to each channel separately
+ channels = []
+ for c in range(image.shape[2]):
+ channel = ndimage.affine_transform(
+ image[:, :, c],
+ matrix[:2, :2],
+ offset=matrix[:2, 2] if matrix.shape[0] == 3 else np.zeros(2),
+ order=order,
+ mode="constant",
+ cval=fill_value,
+ )
+ channels.append(channel)
+ result = np.stack(channels, axis=2)
+
+ return result.astype(image.dtype) # type: ignore[no-any-return]
+
+
+def apply_affine_to_mask(
+ mask: MaskArray,
+ matrix: np.ndarray,
+ fill_value: int = 0,
+) -> MaskArray:
+ """
+ Apply affine transformation to a mask using nearest neighbor interpolation.
+
+ Args:
+ mask: Input mask array (H, W).
+ matrix: 2x3 or 3x3 affine transformation matrix.
+ fill_value: Value to use for pixels outside boundaries.
+
+ Returns:
+ Transformed mask with same shape as input.
+
+ """
+ result = ndimage.affine_transform(
+ mask,
+ matrix[:2, :2],
+ offset=matrix[:2, 2] if matrix.shape[0] == 3 else np.zeros(2),
+ order=0, # Always use nearest neighbor for masks
+ mode="constant",
+ cval=fill_value,
+ )
+
+ return result.astype(mask.dtype) # type: ignore[no-any-return]
+
+
+def rotation_matrix(angle_degrees: float, center: Tuple[float, float]) -> np.ndarray:
+ """
+ Create a 3x3 rotation matrix.
+
+ Args:
+ angle_degrees: Rotation angle in degrees (positive = counter-clockwise).
+ center: Center of rotation (x, y).
+
+ Returns:
+ 3x3 affine transformation matrix.
+
+ """
+ angle_rad = np.deg2rad(angle_degrees)
+ cos_theta = np.cos(angle_rad)
+ sin_theta = np.sin(angle_rad)
+
+ cx, cy = center
+
+ # Translation to origin -> Rotation -> Translation back
+ matrix = np.array(
+ [
+ [cos_theta, -sin_theta, cx - cx * cos_theta + cy * sin_theta],
+ [sin_theta, cos_theta, cy - cx * sin_theta - cy * cos_theta],
+ [0, 0, 1],
+ ],
+ )
+
+ return matrix
+
+
+def scale_matrix(
+ scale_x: float,
+ scale_y: float,
+ center: Tuple[float, float],
+) -> np.ndarray:
+ """
+ Create a 3x3 scale matrix.
+
+ Args:
+ scale_x: Scale factor along x-axis.
+ scale_y: Scale factor along y-axis.
+ center: Center of scaling (x, y).
+
+ Returns:
+ 3x3 affine transformation matrix.
+
+ """
+ cx, cy = center
+
+ matrix = np.array(
+ [
+ [scale_x, 0, cx * (1 - scale_x)],
+ [0, scale_y, cy * (1 - scale_y)],
+ [0, 0, 1],
+ ],
+ )
+
+ return matrix
+
+
+def translation_matrix(tx: float, ty: float) -> np.ndarray:
+ """
+ Create a 3x3 translation matrix.
+
+ Args:
+ tx: Translation along x-axis (positive = right).
+ ty: Translation along y-axis (positive = down).
+
+ Returns:
+ 3x3 affine transformation matrix.
+
+ """
+ return np.array(
+ [
+ [1, 0, tx],
+ [0, 1, ty],
+ [0, 0, 1],
+ ],
+ )
diff --git a/auto_ml/implementations/augmentators/composite.py b/auto_ml/implementations/augmentators/composite.py
new file mode 100644
index 0000000..9840ff9
--- /dev/null
+++ b/auto_ml/implementations/augmentators/composite.py
@@ -0,0 +1,269 @@
+"""Composite augmentation implementations for combining multiple augmentations."""
+
+import random
+from typing import List
+
+from auto_ml.interfaces import DataAugmentatorInterface, SegmentationDatasetInterface
+
+
+class SequentialAugmentator(DataAugmentatorInterface):
+ """
+ Apply multiple augmentations sequentially.
+
+ Each augmentation is applied to the output of the previous one.
+ """
+
+ def __init__(self, augmentators: List[DataAugmentatorInterface]) -> None:
+ """
+ Initialize the sequential augmentator.
+
+ Args:
+ augmentators: List of augmentators to apply in order.
+
+ """
+ self.augmentators = augmentators
+
+ def augment(
+ self, dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Apply all augmentations sequentially."""
+ result = dataset
+
+ for augmentator in self.augmentators:
+ result = augmentator.augment(result)
+
+ # Update metadata to reflect all augmentations
+ aug_names = [
+ aug.__class__.__name__.replace("Augmentator", "").lower()
+ for aug in self.augmentators
+ ]
+ result.metadata["augmentation"] = f"sequential_{'+'.join(aug_names)}"
+
+ return result
+
+
+class RandomChoiceAugmentator(DataAugmentatorInterface):
+ """
+ Randomly choose one augmentation from a list to apply.
+
+ Each sample gets one randomly selected augmentation.
+ """
+
+ def __init__(
+ self,
+ augmentators: List[DataAugmentatorInterface],
+ random_seed: int | None = None,
+ ) -> None:
+ """
+ Initialize the random choice augmentator.
+
+ Args:
+ augmentators: List of augmentators to choose from.
+ random_seed: Random seed for reproducibility.
+
+ """
+ self.augmentators = augmentators
+ self.random_seed = random_seed
+
+ def augment(
+ self, dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Apply one randomly selected augmentation to each sample."""
+ rng = random.Random(self.random_seed)
+ augmented_samples = []
+
+ for image, mask in dataset.samples:
+ # Choose random augmentator
+ augmentator = rng.choice(self.augmentators)
+
+ # Create single-sample dataset
+ temp_dataset = SegmentationDatasetInterface.from_pairs(
+ [(image, mask)],
+ metadata=dataset.metadata,
+ )
+
+ # Apply augmentation
+ aug_dataset = augmentator.augment(temp_dataset)
+
+ # Extract augmented sample
+ aug_image, aug_mask = aug_dataset.samples[0]
+ augmented_samples.append((aug_image, aug_mask))
+
+ return SegmentationDatasetInterface.from_pairs(
+ augmented_samples,
+ metadata={**dataset.metadata, "augmentation": "random_choice"},
+ )
+
+
+class RandomApplyAugmentator(DataAugmentatorInterface):
+ """
+ Apply an augmentation with a given probability.
+
+ Each sample has a chance of being augmented or left unchanged.
+ """
+
+ def __init__(
+ self,
+ augmentator: DataAugmentatorInterface,
+ probability: float = 0.5,
+ random_seed: int | None = None,
+ ) -> None:
+ """
+ Initialize the random apply augmentator.
+
+ Args:
+ augmentator: The augmentator to apply.
+ probability: Probability of applying the augmentation (0.0-1.0).
+ random_seed: Random seed for reproducibility.
+
+ """
+ self.augmentator = augmentator
+ self.probability = probability
+ self.random_seed = random_seed
+
+ def augment(
+ self, dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Apply augmentation to samples with given probability."""
+ rng = random.Random(self.random_seed)
+ augmented_samples = []
+
+ for image, mask in dataset.samples:
+ if rng.random() < self.probability:
+ # Apply augmentation
+ temp_dataset = SegmentationDatasetInterface.from_pairs(
+ [(image, mask)],
+ metadata=dataset.metadata,
+ )
+ aug_dataset = self.augmentator.augment(temp_dataset)
+ aug_image, aug_mask = aug_dataset.samples[0]
+ augmented_samples.append((aug_image, aug_mask))
+ else:
+ # Keep original
+ augmented_samples.append((image, mask))
+
+ return SegmentationDatasetInterface.from_pairs(
+ augmented_samples,
+ metadata={**dataset.metadata, "augmentation": "random_apply"},
+ )
+
+
+class OneOfAugmentator(DataAugmentatorInterface):
+ """
+ Apply one augmentation from a list with specified probabilities.
+
+ Similar to RandomChoiceAugmentator but allows weighted selection.
+ """
+
+ def __init__(
+ self,
+ augmentators: List[DataAugmentatorInterface],
+ probabilities: List[float] | None = None,
+ random_seed: int | None = None,
+ ) -> None:
+ """
+ Initialize the one-of augmentator.
+
+ Args:
+ augmentators: List of augmentators to choose from.
+ probabilities: Probability for each augmentator.
+ If None, uses uniform distribution.
+ random_seed: Random seed for reproducibility.
+
+ """
+ self.augmentators = augmentators
+
+ if probabilities is None:
+ self.probabilities = [1.0 / len(augmentators)] * len(augmentators)
+ else:
+ # Normalize probabilities
+ total = sum(probabilities)
+ self.probabilities = [p / total for p in probabilities]
+
+ self.random_seed = random_seed
+
+ def augment(
+ self, dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Apply one weighted-random augmentation to each sample."""
+ rng = random.Random(self.random_seed)
+ augmented_samples = []
+
+ for image, mask in dataset.samples:
+ # Choose random augmentator with weights
+ augmentator = rng.choices(
+ self.augmentators,
+ weights=self.probabilities,
+ k=1,
+ )[0]
+
+ # Create single-sample dataset
+ temp_dataset = SegmentationDatasetInterface.from_pairs(
+ [(image, mask)],
+ metadata=dataset.metadata,
+ )
+
+ # Apply augmentation
+ aug_dataset = augmentator.augment(temp_dataset)
+
+ # Extract augmented sample
+ aug_image, aug_mask = aug_dataset.samples[0]
+ augmented_samples.append((aug_image, aug_mask))
+
+ return SegmentationDatasetInterface.from_pairs(
+ augmented_samples,
+ metadata={**dataset.metadata, "augmentation": "one_of"},
+ )
+
+
+class MultiplyDatasetAugmentator(DataAugmentatorInterface):
+ """
+ Multiply dataset size by applying different augmentations.
+
+ Creates N copies of the dataset with different augmentations applied.
+ Useful for significant data expansion.
+ """
+
+ def __init__(
+ self,
+ augmentators: List[DataAugmentatorInterface],
+ include_original: bool = True,
+ num_copies: int = 1,
+ ) -> None:
+ """
+ Initialize the multiply dataset augmentator.
+
+ Args:
+ augmentators: List of augmentators to apply.
+ include_original: Whether to include original samples.
+ num_copies: Number of times to repeat the augmentators list.
+
+ """
+ self.augmentators = augmentators * num_copies
+ self.include_original = include_original
+
+ def augment(
+ self, dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Create multiple augmented copies of the dataset."""
+ all_samples = []
+
+ # Include original if requested
+ if self.include_original:
+ all_samples.extend(dataset.samples)
+
+ # Apply each augmentator
+ for augmentator in self.augmentators:
+ aug_dataset = augmentator.augment(dataset)
+ all_samples.extend(aug_dataset.samples)
+
+ total_multiplier = len(self.augmentators) + (1 if self.include_original else 0)
+
+ return SegmentationDatasetInterface.from_pairs(
+ all_samples,
+ metadata={
+ **dataset.metadata,
+ "augmentation": "multiply_dataset",
+ "multiplier": total_multiplier,
+ },
+ )
diff --git a/auto_ml/implementations/augmentators/doc/AUGMENTATORS.md b/auto_ml/implementations/augmentators/doc/AUGMENTATORS.md
new file mode 100644
index 0000000..beb6876
--- /dev/null
+++ b/auto_ml/implementations/augmentators/doc/AUGMENTATORS.md
@@ -0,0 +1,920 @@
+# Documentación de Augmentators
+
+Este documento describe todas las clases y funciones de aumentación de datos disponibles en el módulo `augmentators`. Estas herramientas permiten expandir y diversificar datasets de imágenes y máscaras para mejorar el entrenamiento de modelos de machine learning.
+
+---
+
+## Tabla de Contenidos
+
+1. [base.py - Funciones Base](#basepy---funciones-base)
+2. [identity.py - Aumentación Identidad](#identitypy---aumentación-identidad)
+3. [geometric.py - Aumentaciones Geométricas](#geometricpy---aumentaciones-geométricas)
+4. [photometric.py - Aumentaciones Fotométricas](#photometricpy---aumentaciones-fotométricas)
+5. [sem_specific.py - Aumentaciones Específicas para SEM](#sem_specificpy---aumentaciones-específicas-para-sem)
+6. [composite.py - Aumentaciones Compuestas](#compositepy---aumentaciones-compuestas)
+
+---
+
+## base.py - Funciones Base
+
+Este módulo contiene funciones utilitarias para aplicar transformaciones afines a imágenes y máscaras.
+
+### `apply_affine_to_image`
+
+Aplica una transformación afín a una imagen.
+
+**Parámetros:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `image` | `ImageArray` | Imagen de entrada (H, W) o (H, W, C) |
+| `matrix` | `np.ndarray` | Matriz de transformación afín 2x3 o 3x3 |
+| `order` | `int` | Orden de interpolación (0=nearest, 1=bilinear, 3=cubic). Default: 1 |
+| `fill_value` | `float` | Valor para píxeles fuera de límites. Default: 0.0 |
+
+**Retorna:** Imagen transformada con la misma forma que la entrada.
+
+**Ejemplo:**
+```python
+import numpy as np
+from auto_ml.implementations.augmentators.base import apply_affine_to_image, rotation_matrix
+
+# Crear imagen de ejemplo
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+
+# Crear matriz de rotación (30 grados, centro de la imagen)
+center = (128, 128)
+matrix = rotation_matrix(30, center)
+
+# Aplicar transformación
+rotated_image = apply_affine_to_image(image, matrix, order=1)
+```
+
+---
+
+### `apply_affine_to_mask`
+
+Aplica una transformación afín a una máscara usando interpolación de vecino más cercano.
+
+**Parámetros:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `mask` | `MaskArray` | Máscara de entrada (H, W) |
+| `matrix` | `np.ndarray` | Matriz de transformación afín 2x3 o 3x3 |
+| `fill_value` | `int` | Valor para píxeles fuera de límites. Default: 0 |
+
+**Retorna:** Máscara transformada con la misma forma que la entrada.
+
+**Ejemplo:**
+```python
+import numpy as np
+from auto_ml.implementations.augmentators.base import apply_affine_to_mask, rotation_matrix
+
+# Crear máscara de ejemplo
+mask = np.zeros((256, 256), dtype=np.uint8)
+mask[100:150, 100:150] = 1
+
+# Crear matriz de rotación
+center = (128, 128)
+matrix = rotation_matrix(45, center)
+
+# Aplicar transformación
+rotated_mask = apply_affine_to_mask(mask, matrix)
+```
+
+---
+
+### `rotation_matrix`
+
+Crea una matriz de rotación 3x3.
+
+**Parámetros:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `angle_degrees` | `float` | Ángulo de rotación en grados (positivo = sentido antihorario) |
+| `center` | `Tuple[float, float]` | Centro de rotación (x, y) |
+
+**Retorna:** Matriz de transformación afín 3x3.
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators.base import rotation_matrix
+
+# Matriz de rotación de 90 grados alrededor del centro (128, 128)
+matrix = rotation_matrix(90, (128, 128))
+print(matrix)
+```
+
+---
+
+### `scale_matrix`
+
+Crea una matriz de escala 3x3.
+
+**Parámetros:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `scale_x` | `float` | Factor de escala en el eje X |
+| `scale_y` | `float` | Factor de escala en el eje Y |
+| `center` | `Tuple[float, float]` | Centro de escalado (x, y) |
+
+**Retorna:** Matriz de transformación afín 3x3.
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators.base import scale_matrix
+
+# Matriz de escala (2x en X, 0.5x en Y) alrededor del centro
+matrix = scale_matrix(2.0, 0.5, (128, 128))
+```
+
+---
+
+### `translation_matrix`
+
+Crea una matriz de traslación 3x3.
+
+**Parámetros:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `tx` | `float` | Traslación en el eje X (positivo = derecha) |
+| `ty` | `float` | Traslación en el eje Y (positivo = abajo) |
+
+**Retorna:** Matriz de transformación afín 3x3.
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators.base import translation_matrix
+
+# Matriz de traslación (50 píxeles a la derecha, 30 hacia abajo)
+matrix = translation_matrix(50, 30)
+```
+
+---
+
+## identity.py - Aumentación Identidad
+
+### `IdentityAugmentator`
+
+Aumentador de identidad que retorna el dataset sin cambios. Útil como línea base o cuando no se desea aumentación.
+
+**Métodos:**
+- `augment(dataset: DatasetInterface) -> DatasetInterface`: Retorna el dataset sin modificaciones.
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import IdentityAugmentator
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+# Crear dataset de ejemplo
+images = [np.random.randint(0, 255, (256, 256), dtype=np.uint8)]
+masks = [np.zeros((256, 256), dtype=np.uint8)]
+dataset = DatasetInterface.from_pairs(list(zip(images, masks)))
+
+# Aplicar aumentación identidad
+augmentator = IdentityAugmentator()
+result = augmentator.augment(dataset)
+# result contiene los mismos datos que dataset
+```
+
+---
+
+## geometric.py - Aumentaciones Geométricas
+
+Estas aumentaciones afectan tanto la imagen como la máscara, manteniendo la alineación espacial.
+
+### `RotationAugmentator`
+
+Rota imágenes y máscaras por un ángulo especificado.
+
+**Parámetros del constructor:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `angle_range` | `Tuple[float, float]` | Rango de ángulos en grados (min, max). Default: (-15.0, 15.0) |
+| `random_seed` | `int \| None` | Semilla para reproducibilidad. Default: None |
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import RotationAugmentator
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+# Crear dataset
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+mask[100:150, 100:150] = 1
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+# Rotar entre -30 y 30 grados
+augmentator = RotationAugmentator(
+ angle_range=(-30.0, 30.0),
+ random_seed=42,
+)
+rotated_dataset = augmentator.augment(dataset)
+```
+
+---
+
+### `HorizontalFlipAugmentator`
+
+Voltea imágenes y máscaras horizontalmente.
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import HorizontalFlipAugmentator
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+augmentator = HorizontalFlipAugmentator()
+flipped_dataset = augmentator.augment(dataset)
+```
+
+---
+
+### `VerticalFlipAugmentator`
+
+Voltea imágenes y máscaras verticalmente.
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import VerticalFlipAugmentator
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+augmentator = VerticalFlipAugmentator()
+flipped_dataset = augmentator.augment(dataset)
+```
+
+---
+
+### `ScaleAugmentator`
+
+Escala imágenes y máscaras (zoom in/out) manteniendo el tamaño original.
+
+**Parámetros del constructor:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `scale_range` | `Tuple[float, float]` | Rango de factores de escala. <1.0 = zoom out, >1.0 = zoom in. Default: (0.8, 1.2) |
+| `random_seed` | `int \| None` | Semilla para reproducibilidad. Default: None |
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import ScaleAugmentator
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+# Escalar entre 70% y 130%
+augmentator = ScaleAugmentator(
+ scale_range=(0.7, 1.3),
+ random_seed=42,
+)
+scaled_dataset = augmentator.augment(dataset)
+```
+
+---
+
+### `TranslationAugmentator`
+
+Traslada (desplaza) imágenes y máscaras horizontal y/o verticalmente.
+
+**Parámetros del constructor:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `translate_range` | `Tuple[float, float]` | Rango de traslación como fracción del tamaño. Ej: (-0.1, 0.1) = ±10%. Default: (-0.1, 0.1) |
+| `random_seed` | `int \| None` | Semilla para reproducibilidad. Default: None |
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import TranslationAugmentator
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+# Trasladar hasta ±20% del tamaño de la imagen
+augmentator = TranslationAugmentator(
+ translate_range=(-0.2, 0.2),
+ random_seed=42,
+)
+translated_dataset = augmentator.augment(dataset)
+```
+
+---
+
+### `RandomCropAugmentator`
+
+Recorta una región aleatoria de las imágenes y máscaras, redimensionando al tamaño original.
+
+**Parámetros del constructor:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `crop_size_range` | `Tuple[float, float]` | Rango del tamaño de recorte como fracción. Ej: (0.8, 1.0) = 80-100%. Default: (0.8, 1.0) |
+| `random_seed` | `int \| None` | Semilla para reproducibilidad. Default: None |
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import RandomCropAugmentator
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+# Recortar entre 70% y 90% de la imagen
+augmentator = RandomCropAugmentator(
+ crop_size_range=(0.7, 0.9),
+ random_seed=42,
+)
+cropped_dataset = augmentator.augment(dataset)
+```
+
+---
+
+## photometric.py - Aumentaciones Fotométricas
+
+Estas aumentaciones solo afectan la imagen, dejando la máscara sin modificar.
+
+### `BrightnessAugmentator`
+
+Ajusta el brillo de las imágenes.
+
+**Parámetros del constructor:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `brightness_range` | `tuple[float, float]` | Rango de multiplicadores. 1.0 = sin cambio, <1.0 = oscuro, >1.0 = brillante. Default: (0.8, 1.2) |
+| `random_seed` | `int \| None` | Semilla para reproducibilidad. Default: None |
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import BrightnessAugmentator
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+# Variar brillo entre 60% y 140%
+augmentator = BrightnessAugmentator(
+ brightness_range=(0.6, 1.4),
+ random_seed=42,
+)
+bright_dataset = augmentator.augment(dataset)
+```
+
+---
+
+### `ContrastAugmentator`
+
+Ajusta el contraste de las imágenes.
+
+**Parámetros del constructor:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `contrast_range` | `tuple[float, float]` | Rango de multiplicadores. 1.0 = sin cambio, <1.0 = menos contraste, >1.0 = más contraste. Default: (0.8, 1.2) |
+| `random_seed` | `int \| None` | Semilla para reproducibilidad. Default: None |
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import ContrastAugmentator
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+# Variar contraste entre 70% y 150%
+augmentator = ContrastAugmentator(
+ contrast_range=(0.7, 1.5),
+ random_seed=42,
+)
+contrast_dataset = augmentator.augment(dataset)
+```
+
+---
+
+### `GaussianNoiseAugmentator`
+
+Añade ruido gaussiano a las imágenes.
+
+**Parámetros del constructor:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `noise_std_range` | `tuple[float, float]` | Rango de desviación estándar del ruido. Default: (0.0, 10.0) |
+| `random_seed` | `int \| None` | Semilla para reproducibilidad. Default: None |
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import GaussianNoiseAugmentator
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+# Añadir ruido con std entre 5 y 20
+augmentator = GaussianNoiseAugmentator(
+ noise_std_range=(5.0, 20.0),
+ random_seed=42,
+)
+noisy_dataset = augmentator.augment(dataset)
+```
+
+---
+
+### `GaussianBlurAugmentator`
+
+Aplica desenfoque gaussiano a las imágenes.
+
+**Parámetros del constructor:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `sigma_range` | `tuple[float, float]` | Rango de valores sigma. Mayor = más desenfoque. Default: (0.0, 2.0) |
+| `random_seed` | `int \| None` | Semilla para reproducibilidad. Default: None |
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import GaussianBlurAugmentator
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+# Aplicar desenfoque con sigma entre 0.5 y 3.0
+augmentator = GaussianBlurAugmentator(
+ sigma_range=(0.5, 3.0),
+ random_seed=42,
+)
+blurred_dataset = augmentator.augment(dataset)
+```
+
+---
+
+### `GammaAugmentator`
+
+Aplica corrección gamma a las imágenes.
+
+**Parámetros del constructor:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `gamma_range` | `tuple[float, float]` | Rango de valores gamma. 1.0 = sin cambio, <1.0 = más brillante, >1.0 = más oscuro. Default: (0.8, 1.2) |
+| `random_seed` | `int \| None` | Semilla para reproducibilidad. Default: None |
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import GammaAugmentator
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+# Aplicar corrección gamma entre 0.5 y 1.5
+augmentator = GammaAugmentator(
+ gamma_range=(0.5, 1.5),
+ random_seed=42,
+)
+gamma_dataset = augmentator.augment(dataset)
+```
+
+---
+
+## sem_specific.py - Aumentaciones Específicas para SEM
+
+Aumentaciones diseñadas específicamente para imágenes de microscopía electrónica de barrido (SEM).
+
+### `ElasticDeformationAugmentator`
+
+Aplica deformación elástica para simular variaciones naturales en texturas de roca.
+
+**Parámetros del constructor:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `alpha` | `float` | Fuerza de deformación (mayor = más distorsión). Default: 50.0 |
+| `sigma` | `float` | Suavidad del campo de deformación (mayor = más suave). Default: 5.0 |
+| `random_seed` | `int \| None` | Semilla para reproducibilidad. Default: None |
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import ElasticDeformationAugmentator
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+# Aplicar deformación elástica
+augmentator = ElasticDeformationAugmentator(
+ alpha=100.0,
+ sigma=10.0,
+ random_seed=42,
+)
+deformed_dataset = augmentator.augment(dataset)
+```
+
+---
+
+### `AdaptiveHistogramEqualizationAugmentator`
+
+Aplica CLAHE (Contrast Limited Adaptive Histogram Equalization) para mejorar el contraste local.
+
+**Parámetros del constructor:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `clip_limit` | `float` | Umbral para limitar el contraste (mayor = más contraste). Default: 2.0 |
+| `tile_grid_size` | `Tuple[int, int]` | Tamaño de la cuadrícula para ecualización local. Default: (8, 8) |
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import AdaptiveHistogramEqualizationAugmentator
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+# Aplicar CLAHE
+augmentator = AdaptiveHistogramEqualizationAugmentator(
+ clip_limit=3.0,
+ tile_grid_size=(16, 16),
+)
+clahe_dataset = augmentator.augment(dataset)
+```
+
+---
+
+### `ChargingArtifactAugmentator`
+
+Simula artefactos de carga en imágenes SEM de muestras no conductivas.
+
+**Parámetros del constructor:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `intensity_range` | `Tuple[float, float]` | Rango de multiplicadores de brillo para manchas de carga. Default: (0.7, 1.3) |
+| `num_spots` | `Tuple[int, int]` | Rango de número de manchas a añadir. Default: (1, 3) |
+| `spot_size_range` | `Tuple[int, int]` | Rango del radio de manchas en píxeles. Default: (30, 80) |
+| `random_seed` | `int \| None` | Semilla para reproducibilidad. Default: None |
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import ChargingArtifactAugmentator
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+# Simular artefactos de carga
+augmentator = ChargingArtifactAugmentator(
+ intensity_range=(0.5, 1.5),
+ num_spots=(2, 5),
+ spot_size_range=(20, 60),
+ random_seed=42,
+)
+charging_dataset = augmentator.augment(dataset)
+```
+
+---
+
+### `ScanLineNoiseAugmentator`
+
+Añade artefactos de líneas de escaneo típicos de imágenes SEM.
+
+**Parámetros del constructor:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `probability` | `float` | Probabilidad de añadir ruido a cada línea. Default: 0.3 |
+| `intensity_range` | `Tuple[float, float]` | Rango de intensidad del ruido (fracción del valor máximo). Default: (0.02, 0.08) |
+| `direction` | `str` | Dirección de las líneas ("horizontal" o "vertical"). Default: "horizontal" |
+| `random_seed` | `int \| None` | Semilla para reproducibilidad. Default: None |
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import ScanLineNoiseAugmentator
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+# Añadir ruido de líneas de escaneo
+augmentator = ScanLineNoiseAugmentator(
+ probability=0.5,
+ intensity_range=(0.05, 0.15),
+ direction="horizontal",
+ random_seed=42,
+)
+scanline_dataset = augmentator.augment(dataset)
+```
+
+---
+
+## composite.py - Aumentaciones Compuestas
+
+Estas clases permiten combinar múltiples aumentaciones de diferentes formas.
+
+### `SequentialAugmentator`
+
+Aplica múltiples aumentaciones secuencialmente. Cada aumentación se aplica sobre el resultado de la anterior.
+
+**Parámetros del constructor:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `augmentators` | `List[DataAugmentatorInterface]` | Lista de aumentadores a aplicar en orden |
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import (
+ SequentialAugmentator,
+ RotationAugmentator,
+ BrightnessAugmentator,
+ GaussianNoiseAugmentator,
+)
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+# Aplicar rotación -> brillo -> ruido en secuencia
+augmentator = SequentialAugmentator([
+ RotationAugmentator(angle_range=(-15, 15)),
+ BrightnessAugmentator(brightness_range=(0.9, 1.1)),
+ GaussianNoiseAugmentator(noise_std_range=(0, 5)),
+])
+augmented = augmentator.augment(dataset)
+```
+
+---
+
+### `RandomChoiceAugmentator`
+
+Elige aleatoriamente una aumentación de la lista para aplicar a cada muestra.
+
+**Parámetros del constructor:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `augmentators` | `List[DataAugmentatorInterface]` | Lista de aumentadores de donde elegir |
+| `random_seed` | `int \| None` | Semilla para reproducibilidad. Default: None |
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import (
+ RandomChoiceAugmentator,
+ HorizontalFlipAugmentator,
+ VerticalFlipAugmentator,
+ RotationAugmentator,
+)
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+# Elegir aleatoriamente una aumentación para cada muestra
+augmentator = RandomChoiceAugmentator(
+ augmentators=[
+ HorizontalFlipAugmentator(),
+ VerticalFlipAugmentator(),
+ RotationAugmentator(angle_range=(-45, 45)),
+ ],
+ random_seed=42,
+)
+augmented = augmentator.augment(dataset)
+```
+
+---
+
+### `RandomApplyAugmentator`
+
+Aplica una aumentación con una probabilidad dada.
+
+**Parámetros del constructor:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `augmentator` | `DataAugmentatorInterface` | El aumentador a aplicar |
+| `probability` | `float` | Probabilidad de aplicar la aumentación (0.0-1.0). Default: 0.5 |
+| `random_seed` | `int \| None` | Semilla para reproducibilidad. Default: None |
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import (
+ RandomApplyAugmentator,
+ RotationAugmentator,
+)
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+# Aplicar rotación con 70% de probabilidad
+augmentator = RandomApplyAugmentator(
+ augmentator=RotationAugmentator(angle_range=(-30, 30)),
+ probability=0.7,
+ random_seed=42,
+)
+augmented = augmentator.augment(dataset)
+```
+
+---
+
+### `OneOfAugmentator`
+
+Similar a `RandomChoiceAugmentator` pero permite especificar pesos/probabilidades para cada aumentación.
+
+**Parámetros del constructor:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `augmentators` | `List[DataAugmentatorInterface]` | Lista de aumentadores |
+| `probabilities` | `List[float] \| None` | Probabilidad para cada aumentador. Si es None, usa distribución uniforme. Default: None |
+| `random_seed` | `int \| None` | Semilla para reproducibilidad. Default: None |
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import (
+ OneOfAugmentator,
+ HorizontalFlipAugmentator,
+ VerticalFlipAugmentator,
+ RotationAugmentator,
+)
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+image = np.random.randint(0, 255, (256, 256), dtype=np.uint8)
+mask = np.zeros((256, 256), dtype=np.uint8)
+dataset = DatasetInterface.from_pairs([(image, mask)])
+
+# Elegir con probabilidades ponderadas (50% flip horizontal, 30% flip vertical, 20% rotación)
+augmentator = OneOfAugmentator(
+ augmentators=[
+ HorizontalFlipAugmentator(),
+ VerticalFlipAugmentator(),
+ RotationAugmentator(angle_range=(-45, 45)),
+ ],
+ probabilities=[0.5, 0.3, 0.2],
+ random_seed=42,
+)
+augmented = augmentator.augment(dataset)
+```
+
+---
+
+### `MultiplyDatasetAugmentator`
+
+Multiplica el tamaño del dataset aplicando diferentes aumentaciones. Crea N copias del dataset con diferentes aumentaciones.
+
+**Parámetros del constructor:**
+| Parámetro | Tipo | Descripción |
+|-----------|------|-------------|
+| `augmentators` | `List[DataAugmentatorInterface]` | Lista de aumentadores a aplicar |
+| `include_original` | `bool` | Si incluir las muestras originales. Default: True |
+
+**Ejemplo:**
+```python
+from auto_ml.implementations.augmentators import (
+ MultiplyDatasetAugmentator,
+ HorizontalFlipAugmentator,
+ VerticalFlipAugmentator,
+ RotationAugmentator,
+)
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+# Dataset con 2 imágenes
+images = [np.random.randint(0, 255, (256, 256), dtype=np.uint8) for _ in range(2)]
+masks = [np.zeros((256, 256), dtype=np.uint8) for _ in range(2)]
+dataset = DatasetInterface.from_pairs(list(zip(images, masks)))
+
+# Multiplicar dataset: original + 3 versiones aumentadas = 8 muestras totales
+augmentator = MultiplyDatasetAugmentator(
+ augmentators=[
+ HorizontalFlipAugmentator(),
+ VerticalFlipAugmentator(),
+ RotationAugmentator(angle_range=(-45, 45)),
+ ],
+ include_original=True,
+)
+expanded_dataset = augmentator.augment(dataset)
+# expanded_dataset tendrá 2 * 4 = 8 muestras
+```
+
+---
+
+## Ejemplo Completo: Pipeline de Aumentación
+
+Aquí hay un ejemplo completo que combina varias aumentaciones para crear un pipeline de entrenamiento robusto:
+
+```python
+from auto_ml.implementations.augmentators import (
+ SequentialAugmentator,
+ RandomApplyAugmentator,
+ OneOfAugmentator,
+ RotationAugmentator,
+ HorizontalFlipAugmentator,
+ VerticalFlipAugmentator,
+ ScaleAugmentator,
+ BrightnessAugmentator,
+ ContrastAugmentator,
+ GaussianNoiseAugmentator,
+ ElasticDeformationAugmentator,
+)
+from auto_ml.interfaces import DatasetInterface
+import numpy as np
+
+# Crear dataset de ejemplo
+images = [np.random.randint(0, 255, (256, 256), dtype=np.uint8) for _ in range(10)]
+masks = [np.zeros((256, 256), dtype=np.uint8) for _ in range(10)]
+dataset = DatasetInterface.from_pairs(list(zip(images, masks)))
+
+# Crear pipeline de aumentación complejo
+pipeline = SequentialAugmentator([
+ # Transformaciones geométricas (aplicar una de ellas)
+ OneOfAugmentator([
+ HorizontalFlipAugmentator(),
+ VerticalFlipAugmentator(),
+ RotationAugmentator(angle_range=(-30, 30)),
+ ScaleAugmentator(scale_range=(0.8, 1.2)),
+ ]),
+
+ # Transformaciones fotométricas (aplicar con probabilidad)
+ RandomApplyAugmentator(
+ BrightnessAugmentator(brightness_range=(0.8, 1.2)),
+ probability=0.5,
+ ),
+ RandomApplyAugmentator(
+ ContrastAugmentator(contrast_range=(0.8, 1.2)),
+ probability=0.5,
+ ),
+
+ # Ruido (aplicar con baja probabilidad)
+ RandomApplyAugmentator(
+ GaussianNoiseAugmentator(noise_std_range=(0, 10)),
+ probability=0.3,
+ ),
+
+ # Deformación elástica para SEM (aplicar con baja probabilidad)
+ RandomApplyAugmentator(
+ ElasticDeformationAugmentator(alpha=50, sigma=5),
+ probability=0.2,
+ ),
+])
+
+# Aplicar pipeline
+augmented_dataset = pipeline.augment(dataset)
+```
+
+---
+
+## Resumen de Todas las Clases
+
+| Módulo | Clase | Descripción |
+|--------|-------|-------------|
+| `identity` | `IdentityAugmentator` | Sin modificaciones (línea base) |
+| `geometric` | `RotationAugmentator` | Rotación aleatoria |
+| `geometric` | `HorizontalFlipAugmentator` | Volteo horizontal |
+| `geometric` | `VerticalFlipAugmentator` | Volteo vertical |
+| `geometric` | `ScaleAugmentator` | Zoom in/out |
+| `geometric` | `TranslationAugmentator` | Desplazamiento |
+| `geometric` | `RandomCropAugmentator` | Recorte aleatorio |
+| `photometric` | `BrightnessAugmentator` | Ajuste de brillo |
+| `photometric` | `ContrastAugmentator` | Ajuste de contraste |
+| `photometric` | `GaussianNoiseAugmentator` | Ruido gaussiano |
+| `photometric` | `GaussianBlurAugmentator` | Desenfoque gaussiano |
+| `photometric` | `GammaAugmentator` | Corrección gamma |
+| `sem_specific` | `ElasticDeformationAugmentator` | Deformación elástica |
+| `sem_specific` | `AdaptiveHistogramEqualizationAugmentator` | CLAHE |
+| `sem_specific` | `ChargingArtifactAugmentator` | Artefactos de carga SEM |
+| `sem_specific` | `ScanLineNoiseAugmentator` | Ruido de líneas de escaneo |
+| `composite` | `SequentialAugmentator` | Aplicar en secuencia |
+| `composite` | `RandomChoiceAugmentator` | Elegir una aleatoriamente |
+| `composite` | `RandomApplyAugmentator` | Aplicar con probabilidad |
+| `composite` | `OneOfAugmentator` | Elegir con pesos |
+| `composite` | `MultiplyDatasetAugmentator` | Expandir dataset |
diff --git a/auto_ml/implementations/augmentators/doc/USAGE_GUIDE.md b/auto_ml/implementations/augmentators/doc/USAGE_GUIDE.md
new file mode 100644
index 0000000..9550bb4
--- /dev/null
+++ b/auto_ml/implementations/augmentators/doc/USAGE_GUIDE.md
@@ -0,0 +1,564 @@
+# Guía de Uso: Augmentadores en el Proyecto
+
+Esta guía te muestra cómo usar los augmentadores de datos en tu proyecto AutoML, con ejemplos prácticos y diferentes combinaciones para distintos escenarios.
+
+---
+
+## Tabla de Contenidos
+
+1. [Introducción](#introducción)
+2. [Uso Básico](#uso-básico)
+3. [Combinaciones Comunes](#combinaciones-comunes)
+4. [Augmentadores Composite](#augmentadores-composite)
+5. [Integración con AutoML](#integración-con-automl)
+6. [Ejemplos por Escenario](#ejemplos-por-escenario)
+7. [Mejores Prácticas](#mejores-prácticas)
+
+---
+
+## Introducción
+
+El sistema de augmentación permite expandir tu dataset aplicando transformaciones a las imágenes y máscaras. Todos los augmentadores implementan la interfaz `DataAugmentatorInterface` y tienen un método `augment()` que recibe un `DatasetInterface` y retorna un nuevo dataset augmentado.
+
+### Importar Augmentadores
+
+```python
+from auto_ml.implementations import (
+ # Geometric
+ RotationAugmentator,
+ HorizontalFlipAugmentator,
+ VerticalFlipAugmentator,
+ ScaleAugmentator,
+ TranslationAugmentator,
+ RandomCropAugmentator,
+
+ # Photometric
+ BrightnessAugmentator,
+ ContrastAugmentator,
+ GaussianNoiseAugmentator,
+ GaussianBlurAugmentator,
+ GammaAugmentator,
+
+ # SEM-specific
+ ElasticDeformationAugmentator,
+ AdaptiveHistogramEqualizationAugmentator,
+ ChargingArtifactAugmentator,
+ ScanLineNoiseAugmentator,
+
+ # Composite
+ SequentialAugmentator,
+ RandomApplyAugmentator,
+ RandomChoiceAugmentator,
+ OneOfAugmentator,
+ MultiplyDatasetAugmentator,
+
+ # Identity (sin cambios)
+ IdentityAugmentator,
+)
+```
+
+---
+
+## Uso Básico
+
+### Ejemplo 1: Augmentador Simple
+
+```python
+from auto_ml.implementations import (
+ RotationAugmentator,
+ load_dataset_from_directories,
+)
+from pathlib import Path
+
+# Cargar dataset
+input_dir = Path("data/input")
+target_dir = Path("data/target")
+dataset = load_dataset_from_directories(input_dir, target_dir)
+
+# Crear augmentador
+augmentator = RotationAugmentator(
+ angle_range=(-30, 30), # Rotar entre -30 y +30 grados
+ random_seed=42, # Para reproducibilidad
+)
+
+# Aplicar augmentación
+augmented_dataset = augmentator.augment(dataset)
+
+print(f"Dataset original: {len(dataset)} muestras")
+print(f"Dataset augmentado: {len(augmented_dataset)} muestras")
+```
+
+### Ejemplo 2: Sin Augmentación (Identity)
+
+```python
+from auto_ml.implementations import IdentityAugmentator
+
+# No aplica ninguna transformación
+augmentator = IdentityAugmentator()
+augmented_dataset = augmentator.augment(dataset)
+
+# El dataset mantiene las mismas muestras
+assert len(augmented_dataset) == len(dataset)
+```
+
+---
+
+## Combinaciones Comunes
+
+### Ejemplo 3: Pipeline Secuencial Simple
+
+Aplica múltiples augmentaciones en orden:
+
+```python
+from auto_ml.implementations import (
+ SequentialAugmentator,
+ RotationAugmentator,
+ BrightnessAugmentator,
+)
+
+# Crear pipeline: primero rota, luego ajusta brillo
+pipeline = SequentialAugmentator([
+ RotationAugmentator(angle_range=(-15, 15), random_seed=42),
+ BrightnessAugmentator(brightness_range=(0.8, 1.2), random_seed=42),
+])
+
+augmented_dataset = pipeline.augment(dataset)
+```
+
+### Ejemplo 4: Augmentación Geométrica Completa
+
+```python
+from auto_ml.implementations import (
+ SequentialAugmentator,
+ RotationAugmentator,
+ HorizontalFlipAugmentator,
+ ScaleAugmentator,
+)
+
+# Combinar rotación, flip y escala
+geometric_pipeline = SequentialAugmentator([
+ RotationAugmentator(angle_range=(-20, 20), random_seed=42),
+ HorizontalFlipAugmentator(),
+ ScaleAugmentator(scale_range=(0.9, 1.1), random_seed=42),
+])
+
+augmented_dataset = geometric_pipeline.augment(dataset)
+```
+
+### Ejemplo 5: Augmentación Fotométrica
+
+```python
+from auto_ml.implementations import (
+ SequentialAugmentator,
+ BrightnessAugmentator,
+ ContrastAugmentator,
+ GaussianNoiseAugmentator,
+)
+
+# Pipeline para mejorar variabilidad fotométrica
+photometric_pipeline = SequentialAugmentator([
+ BrightnessAugmentator(brightness_range=(0.85, 1.15), random_seed=42),
+ ContrastAugmentator(contrast_range=(0.8, 1.2), random_seed=42),
+ GaussianNoiseAugmentator(std=0.01, random_seed=42),
+])
+
+augmented_dataset = photometric_pipeline.augment(dataset)
+```
+
+### Ejemplo 6: Augmentación SEM Completa
+
+Para imágenes de microscopía electrónica de barrido:
+
+```python
+from auto_ml.implementations import (
+ SequentialAugmentator,
+ ElasticDeformationAugmentator,
+ AdaptiveHistogramEqualizationAugmentator,
+ ChargingArtifactAugmentator,
+ ScanLineNoiseAugmentator,
+)
+
+# Pipeline específico para SEM
+sem_pipeline = SequentialAugmentator([
+ ElasticDeformationAugmentator(alpha=50.0, sigma=5.0, random_seed=42),
+ AdaptiveHistogramEqualizationAugmentator(clip_limit=2.0, tile_grid_size=(8, 8)),
+ ChargingArtifactAugmentator(num_spots=(1, 3), intensity_range=(0.7, 1.3), random_seed=42),
+ ScanLineNoiseAugmentator(probability=0.2, intensity_range=(0.02, 0.05), random_seed=42),
+])
+
+augmented_dataset = sem_pipeline.augment(dataset)
+```
+
+---
+
+## Augmentadores Composite
+
+### Ejemplo 7: Aplicación Aleatoria (RandomApplyAugmentator)
+
+Aplica una augmentación con cierta probabilidad:
+
+```python
+from auto_ml.implementations import (
+ RandomApplyAugmentator,
+ GaussianBlurAugmentator,
+)
+
+# Aplica blur solo al 50% de las muestras
+random_blur = RandomApplyAugmentator(
+ augmentator=GaussianBlurAugmentator(sigma_range=(0.5, 1.5), random_seed=42),
+ probability=0.5,
+ random_seed=42,
+)
+
+augmented_dataset = random_blur.augment(dataset)
+```
+
+### Ejemplo 8: Elegir Una Augmentación (OneOfAugmentator)
+
+Aplica solo UNA de varias augmentaciones posibles:
+
+```python
+from auto_ml.implementations import (
+ OneOfAugmentator,
+ RotationAugmentator,
+ HorizontalFlipAugmentator,
+ VerticalFlipAugmentator,
+)
+
+# Cada muestra recibe solo UNA de estas transformaciones
+one_of = OneOfAugmentator(
+ augmentators=[
+ RotationAugmentator(angle_range=(-30, 30), random_seed=42),
+ HorizontalFlipAugmentator(),
+ VerticalFlipAugmentator(),
+ ],
+ random_seed=42,
+)
+
+augmented_dataset = one_of.augment(dataset)
+```
+
+### Ejemplo 9: Selección Aleatoria (RandomChoiceAugmentator)
+
+Aplica N augmentaciones aleatorias de una lista:
+
+```python
+from auto_ml.implementations import (
+ RandomChoiceAugmentator,
+ BrightnessAugmentator,
+ ContrastAugmentator,
+ GaussianNoiseAugmentator,
+ GaussianBlurAugmentator,
+)
+
+# Aplica 2 augmentaciones aleatorias de la lista
+random_choice = RandomChoiceAugmentator(
+ augmentators=[
+ BrightnessAugmentator(brightness_range=(0.8, 1.2), random_seed=42),
+ ContrastAugmentator(contrast_range=(0.8, 1.2), random_seed=42),
+ GaussianNoiseAugmentator(std=0.02, random_seed=42),
+ GaussianBlurAugmentator(sigma_range=(0.5, 1.0), random_seed=42),
+ ],
+ num_choices=2,
+ random_seed=42,
+)
+
+augmented_dataset = random_choice.augment(dataset)
+```
+
+### Ejemplo 10: Multiplicar Dataset
+
+Crea múltiples versiones augmentadas del dataset:
+
+```python
+from auto_ml.implementations import (
+ MultiplyDatasetAugmentator,
+ RotationAugmentator,
+)
+
+# Crea 5 versiones augmentadas de cada muestra
+multiplier = MultiplyDatasetAugmentator(
+ augmentator=RotationAugmentator(angle_range=(-30, 30), random_seed=42),
+ num_copies=5,
+)
+
+augmented_dataset = multiplier.augment(dataset)
+
+# Si el dataset original tenía 100 muestras, ahora tiene 500
+print(f"Dataset multiplicado: {len(augmented_dataset)} muestras")
+```
+
+---
+
+## Integración con AutoML
+
+### Ejemplo 11: Uso con DataAugmentatorNode
+
+```python
+from auto_ml.implementations import (
+ DataAugmentatorNode,
+ SequentialAugmentator,
+ RotationAugmentator,
+ BrightnessAugmentator,
+ load_dataset_from_directories,
+)
+from auto_ml.automl import AutoML
+from pathlib import Path
+
+# Cargar dataset
+dataset = load_dataset_from_directories(
+ Path("data/input"),
+ Path("data/target"),
+)
+
+# Crear augmentador
+augmentator = SequentialAugmentator([
+ RotationAugmentator(angle_range=(-15, 15), random_seed=42),
+ BrightnessAugmentator(brightness_range=(0.9, 1.1), random_seed=42),
+])
+
+# Crear nodo con k-fold cross-validation
+aug_node = DataAugmentatorNode(
+ augmentator=augmentator,
+ name="GeometricPhotometric",
+ k_folds=5,
+ random_seed=42,
+)
+
+# Usar en AutoML
+augmentators = [aug_node]
+# ... definir models y evaluator_node ...
+# automl = AutoML()
+# automl.run_experiment(dataset, augmentators, models, evaluator_node)
+```
+
+### Ejemplo 12: Múltiples Estrategias de Augmentación
+
+```python
+from auto_ml.implementations import (
+ DataAugmentatorNode,
+ IdentityAugmentator,
+ SequentialAugmentator,
+ RotationAugmentator,
+ BrightnessAugmentator,
+ ElasticDeformationAugmentator,
+)
+
+# Estrategia 1: Sin augmentación (baseline)
+aug_node_identity = DataAugmentatorNode(
+ augmentator=IdentityAugmentator(),
+ name="Baseline_NoAug",
+ k_folds=5,
+ random_seed=42,
+)
+
+# Estrategia 2: Augmentación ligera
+aug_node_light = DataAugmentatorNode(
+ augmentator=SequentialAugmentator([
+ RotationAugmentator(angle_range=(-10, 10), random_seed=42),
+ BrightnessAugmentator(brightness_range=(0.95, 1.05), random_seed=42),
+ ]),
+ name="LightAugmentation",
+ k_folds=5,
+ random_seed=42,
+)
+
+# Estrategia 3: Augmentación agresiva
+aug_node_aggressive = DataAugmentatorNode(
+ augmentator=SequentialAugmentator([
+ RotationAugmentator(angle_range=(-45, 45), random_seed=42),
+ BrightnessAugmentator(brightness_range=(0.7, 1.3), random_seed=42),
+ ElasticDeformationAugmentator(alpha=70.0, sigma=7.0, random_seed=42),
+ ]),
+ name="AggressiveAugmentation",
+ k_folds=5,
+ random_seed=42,
+)
+
+# Comparar las tres estrategias
+augmentators = [aug_node_identity, aug_node_light, aug_node_aggressive]
+```
+
+---
+
+## Ejemplos por Escenario
+
+### Escenario 1: Dataset Pequeño (< 100 muestras)
+
+Maximiza la variabilidad con augmentación agresiva:
+
+```python
+from auto_ml.implementations import (
+ MultiplyDatasetAugmentator,
+ SequentialAugmentator,
+ RotationAugmentator,
+ HorizontalFlipAugmentator,
+ VerticalFlipAugmentator,
+ BrightnessAugmentator,
+ ContrastAugmentator,
+ GaussianNoiseAugmentator,
+ ElasticDeformationAugmentator,
+)
+
+# Pipeline agresivo para expandir dataset pequeño
+small_dataset_pipeline = MultiplyDatasetAugmentator(
+ augmentator=SequentialAugmentator([
+ RotationAugmentator(angle_range=(-45, 45), random_seed=42),
+ OneOfAugmentator([
+ HorizontalFlipAugmentator(),
+ VerticalFlipAugmentator(),
+ ], random_seed=42),
+ BrightnessAugmentator(brightness_range=(0.7, 1.3), random_seed=42),
+ ContrastAugmentator(contrast_range=(0.7, 1.3), random_seed=42),
+ GaussianNoiseAugmentator(std=0.03, random_seed=42),
+ ElasticDeformationAugmentator(alpha=60.0, sigma=6.0, random_seed=42),
+ ]),
+ num_copies=10, # Multiplica por 10
+)
+
+augmented_dataset = small_dataset_pipeline.augment(dataset)
+```
+
+### Escenario 2: Dataset Balanceado (Entrenamiento General)
+
+Augmentación moderada para mejorar generalización:
+
+```python
+from auto_ml.implementations import (
+ SequentialAugmentator,
+ RandomApplyAugmentator,
+ RotationAugmentator,
+ HorizontalFlipAugmentator,
+ BrightnessAugmentator,
+ GaussianNoiseAugmentator,
+)
+
+# Pipeline balanceado
+balanced_pipeline = SequentialAugmentator([
+ RotationAugmentator(angle_range=(-20, 20), random_seed=42),
+ HorizontalFlipAugmentator(),
+ RandomApplyAugmentator(
+ augmentator=BrightnessAugmentator(brightness_range=(0.85, 1.15), random_seed=42),
+ probability=0.5,
+ random_seed=42,
+ ),
+ RandomApplyAugmentator(
+ augmentator=GaussianNoiseAugmentator(std=0.015, random_seed=42),
+ probability=0.3,
+ random_seed=42,
+ ),
+])
+
+augmented_dataset = balanced_pipeline.augment(dataset)
+```
+
+### Escenario 3: Imágenes SEM de Rocas
+
+Augmentación específica para microscopía:
+
+```python
+from auto_ml.implementations import (
+ SequentialAugmentator,
+ RandomApplyAugmentator,
+ RotationAugmentator,
+ ElasticDeformationAugmentator,
+ AdaptiveHistogramEqualizationAugmentator,
+ ChargingArtifactAugmentator,
+ ScanLineNoiseAugmentator,
+)
+
+# Pipeline especializado para SEM
+sem_rocks_pipeline = SequentialAugmentator([
+ # Transformaciones geométricas sutiles
+ RotationAugmentator(angle_range=(-15, 15), random_seed=42),
+
+ # Deformación elástica (común en muestras naturales)
+ ElasticDeformationAugmentator(alpha=40.0, sigma=5.0, random_seed=42),
+
+ # Mejora de contraste local (siempre útil)
+ AdaptiveHistogramEqualizationAugmentator(clip_limit=2.0, tile_grid_size=(8, 8)),
+
+ # Artefactos de carga (aleatorio, 30% probabilidad)
+ RandomApplyAugmentator(
+ augmentator=ChargingArtifactAugmentator(
+ num_spots=(1, 3),
+ intensity_range=(0.7, 1.2),
+ random_seed=42,
+ ),
+ probability=0.3,
+ random_seed=42,
+ ),
+
+ # Ruido de línea de escaneo (aleatorio, 20% probabilidad)
+ RandomApplyAugmentator(
+ augmentator=ScanLineNoiseAugmentator(
+ probability=0.3,
+ intensity_range=(0.02, 0.05),
+ random_seed=42,
+ ),
+ probability=0.2,
+ random_seed=42,
+ ),
+])
+
+augmented_dataset = sem_rocks_pipeline.augment(dataset)
+```
+
+### Escenario 4: Testing y Validación
+
+Sin augmentación para evaluación justa:
+
+```python
+from auto_ml.implementations import IdentityAugmentator
+
+# Sin cambios para test/validation
+test_augmentator = IdentityAugmentator()
+test_dataset = test_augmentator.augment(dataset)
+```
+
+---
+
+## Mejores Prácticas
+
+### 1. Combinar Augmentaciones Complementarias
+
+Mezcla geométricas con fotométricas:
+
+```python
+# Buena combinación: geométrica + fotométrica
+good_combo = SequentialAugmentator([
+ RotationAugmentator(angle_range=(-20, 20), random_seed=42), # Geométrica
+ BrightnessAugmentator(brightness_range=(0.85, 1.15), random_seed=42), # Fotométrica
+])
+
+# Evitar: múltiples augmentaciones del mismo tipo
+# (puede ser redundante o excesivo)
+redundant = SequentialAugmentator([
+ BrightnessAugmentator(brightness_range=(0.8, 1.2), random_seed=42),
+ ContrastAugmentator(contrast_range=(0.8, 1.2), random_seed=42),
+ GammaAugmentator(gamma_range=(0.8, 1.2), random_seed=42),
+ # Todas fotométricas, puede ser demasiado
+])
+```
+
+### 2. Usar Composite Augmentators para Variabilidad
+
+```python
+# En lugar de aplicar siempre la misma augmentación:
+always_same = RotationAugmentator(angle_range=(-20, 20), random_seed=42)
+
+# Mejor: variar con OneOfAugmentator
+varied = OneOfAugmentator([
+ RotationAugmentator(angle_range=(-20, 20), random_seed=42),
+ HorizontalFlipAugmentator(),
+ ScaleAugmentator(scale_range=(0.9, 1.1), random_seed=42),
+], random_seed=42)
+```
+
+---
+
+## Recursos Adicionales
+
+- **Documentación de API**: Ver `AUGMENTATORS.md` para detalles de cada función
+
+---
\ No newline at end of file
diff --git a/auto_ml/implementations/augmentators/geometric.py b/auto_ml/implementations/augmentators/geometric.py
new file mode 100644
index 0000000..3507fec
--- /dev/null
+++ b/auto_ml/implementations/augmentators/geometric.py
@@ -0,0 +1,311 @@
+"""Geometric augmentation implementations."""
+
+import random
+from typing import Tuple
+
+import numpy as np
+
+from auto_ml.implementations.augmentators.base import (
+ apply_affine_to_image,
+ apply_affine_to_mask,
+ rotation_matrix,
+ scale_matrix,
+ translation_matrix,
+)
+from auto_ml.interfaces import DataAugmentatorInterface, SegmentationDatasetInterface
+
+
+class RotationAugmentator(DataAugmentatorInterface):
+ """
+ Rotate images and masks by a specified angle.
+
+ Applies the same rotation to both image and mask, preserving alignment.
+ """
+
+ def __init__(
+ self,
+ angle_range: Tuple[float, float] = (-15.0, 15.0),
+ random_seed: int | None = None,
+ ) -> None:
+ """
+ Initialize the rotation augmentator.
+
+ Args:
+ angle_range: Range of rotation angles in degrees (min, max).
+ Positive = counter-clockwise.
+ random_seed: Random seed for reproducibility.
+
+ """
+ self.angle_range = angle_range
+ self.random_seed = random_seed
+
+ def augment(
+ self, dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Apply random rotation to all samples in the dataset."""
+ rng = random.Random(self.random_seed)
+ augmented_samples = []
+
+ for image, mask in dataset.samples:
+ # Random angle within range
+ angle = rng.uniform(*self.angle_range)
+
+ # Get image center
+ h, w = image.shape[:2]
+ center = (w / 2, h / 2)
+
+ # Create rotation matrix
+ matrix = rotation_matrix(angle, center)
+
+ # Apply to image and mask
+ aug_image = apply_affine_to_image(image, matrix, order=1)
+ aug_mask = apply_affine_to_mask(mask, matrix)
+
+ augmented_samples.append((aug_image, aug_mask))
+
+ return SegmentationDatasetInterface.from_pairs(
+ augmented_samples,
+ metadata={**dataset.metadata, "augmentation": "rotation"},
+ )
+
+
+class HorizontalFlipAugmentator(DataAugmentatorInterface):
+ """Flip images and masks horizontally."""
+
+ def augment(
+ self, dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Apply horizontal flip to all samples."""
+ augmented_samples = []
+
+ for image, mask in dataset.samples:
+ aug_image = np.ascontiguousarray(np.fliplr(image))
+ aug_mask = np.ascontiguousarray(np.fliplr(mask))
+ augmented_samples.append((aug_image, aug_mask))
+
+ return SegmentationDatasetInterface.from_pairs(
+ augmented_samples,
+ metadata={**dataset.metadata, "augmentation": "horizontal_flip"},
+ )
+
+
+class VerticalFlipAugmentator(DataAugmentatorInterface):
+ """Flip images and masks vertically."""
+
+ def augment(
+ self,
+ dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Apply vertical flip to all samples."""
+ augmented_samples = []
+
+ for image, mask in dataset.samples:
+ aug_image = np.ascontiguousarray(np.flipud(image))
+ aug_mask = np.ascontiguousarray(np.flipud(mask))
+ augmented_samples.append((aug_image, aug_mask))
+
+ return SegmentationDatasetInterface.from_pairs(
+ augmented_samples,
+ metadata={**dataset.metadata, "augmentation": "vertical_flip"},
+ )
+
+
+class ScaleAugmentator(DataAugmentatorInterface):
+ """
+ Scale images and masks.
+
+ Applies zoom in/out while maintaining image size through cropping or padding.
+ """
+
+ def __init__(
+ self,
+ scale_range: Tuple[float, float] = (0.8, 1.2),
+ random_seed: int | None = None,
+ ) -> None:
+ """
+ Initialize the scale augmentator.
+
+ Args:
+ scale_range: Range of scale factors (min, max).
+ Values < 1.0 zoom out, > 1.0 zoom in.
+ random_seed: Random seed for reproducibility.
+
+ """
+ self.scale_range = scale_range
+ self.random_seed = random_seed
+
+ def augment(
+ self,
+ dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Apply random scaling to all samples."""
+ rng = random.Random(self.random_seed)
+ augmented_samples = []
+
+ for image, mask in dataset.samples:
+ # Random scale factor
+ scale = rng.uniform(*self.scale_range)
+
+ # Get image center
+ h, w = image.shape[:2]
+ center = (w / 2, h / 2)
+
+ # Create scale matrix
+ matrix = scale_matrix(scale, scale, center)
+
+ # Apply to image and mask
+ aug_image = apply_affine_to_image(image, matrix, order=1)
+ aug_mask = apply_affine_to_mask(mask, matrix)
+
+ augmented_samples.append((aug_image, aug_mask))
+
+ return SegmentationDatasetInterface.from_pairs(
+ augmented_samples,
+ metadata={**dataset.metadata, "augmentation": "scale"},
+ )
+
+
+class TranslationAugmentator(DataAugmentatorInterface):
+ """
+ Translate (shift) images and masks.
+
+ Shifts the image content horizontally and/or vertically.
+ """
+
+ def __init__(
+ self,
+ translate_range: Tuple[float, float] = (-0.1, 0.1),
+ random_seed: int | None = None,
+ ) -> None:
+ """
+ Initialize the translation augmentator.
+
+ Args:
+ translate_range: Range of translation as fraction of image size.
+ E.g., (-0.1, 0.1) means shift by up to ±10%.
+ random_seed: Random seed for reproducibility.
+
+ """
+ self.translate_range = translate_range
+ self.random_seed = random_seed
+
+ def augment(
+ self,
+ dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Apply random translation to all samples."""
+ rng = random.Random(self.random_seed)
+ augmented_samples = []
+
+ for image, mask in dataset.samples:
+ h, w = image.shape[:2]
+
+ # Random translation
+ tx_frac = rng.uniform(*self.translate_range)
+ ty_frac = rng.uniform(*self.translate_range)
+
+ tx = tx_frac * w
+ ty = ty_frac * h
+
+ # Create translation matrix
+ matrix = translation_matrix(tx, ty)
+
+ # Apply to image and mask
+ aug_image = apply_affine_to_image(image, matrix, order=1)
+ aug_mask = apply_affine_to_mask(mask, matrix)
+
+ augmented_samples.append((aug_image, aug_mask))
+
+ return SegmentationDatasetInterface.from_pairs(
+ augmented_samples,
+ metadata={**dataset.metadata, "augmentation": "translation"},
+ )
+
+
+class RandomCropAugmentator(DataAugmentatorInterface):
+ """
+ Random crop of images and masks.
+
+ Extracts a random region from the input and resizes back to original size.
+ """
+
+ def __init__(
+ self,
+ crop_size_range: Tuple[float, float] = (0.8, 1.0),
+ random_seed: int | None = None,
+ ) -> None:
+ """
+ Initialize the random crop augmentator.
+
+ Args:
+ crop_size_range: Range of crop size as fraction of original
+ (min, max). E.g., (0.8, 1.0) crops 80-100%.
+ random_seed: Random seed for reproducibility.
+
+ """
+ self.crop_size_range = crop_size_range
+ self.random_seed = random_seed
+
+ def augment(
+ self,
+ dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Apply random crop to all samples."""
+ rng = random.Random(self.random_seed)
+ augmented_samples = []
+
+ for image, mask in dataset.samples:
+ h, w = image.shape[:2]
+
+ # Random crop size
+ crop_frac = rng.uniform(*self.crop_size_range)
+ crop_h = int(h * crop_frac)
+ crop_w = int(w * crop_frac)
+
+ # Random crop position
+ top = rng.randint(0, h - crop_h)
+ left = rng.randint(0, w - crop_w)
+
+ # Crop
+ if image.ndim == 2:
+ cropped_image = image[top : top + crop_h, left : left + crop_w]
+ else:
+ cropped_image = image[top : top + crop_h, left : left + crop_w, :]
+
+ cropped_mask = mask[top : top + crop_h, left : left + crop_w]
+
+ # Resize back to original size
+ from scipy import ndimage
+
+ zoom_h = h / crop_h
+ zoom_w = w / crop_w
+
+ if image.ndim == 2:
+ aug_image = ndimage.zoom(
+ cropped_image,
+ (zoom_h, zoom_w),
+ order=1,
+ )
+ else:
+ aug_image = ndimage.zoom(
+ cropped_image,
+ (zoom_h, zoom_w, 1),
+ order=1,
+ )
+
+ aug_mask = ndimage.zoom(
+ cropped_mask,
+ (zoom_h, zoom_w),
+ order=0,
+ )
+
+ # Ensure correct types
+ aug_image = aug_image.astype(image.dtype)
+ aug_mask = aug_mask.astype(mask.dtype)
+
+ augmented_samples.append((aug_image, aug_mask))
+
+ return SegmentationDatasetInterface.from_pairs(
+ augmented_samples,
+ metadata={**dataset.metadata, "augmentation": "random_crop"},
+ )
diff --git a/auto_ml/implementations/augmentators/identity.py b/auto_ml/implementations/augmentators/identity.py
new file mode 100644
index 0000000..4bc64c9
--- /dev/null
+++ b/auto_ml/implementations/augmentators/identity.py
@@ -0,0 +1,21 @@
+"""Data augmentation implementations."""
+
+from auto_ml.interfaces import DataAugmentatorInterface, SegmentationDatasetInterface
+
+
+class IdentityAugmentator(DataAugmentatorInterface):
+ """
+ Identity augmentator that returns the dataset unchanged.
+
+ Useful as a baseline or when no augmentation is desired.
+ """
+
+ def augment(
+ self,
+ dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Return the dataset unchanged."""
+ return SegmentationDatasetInterface(
+ samples=list(dataset.samples),
+ metadata={**dataset.metadata, "augmentation": "identity"},
+ )
diff --git a/auto_ml/implementations/augmentators/photometric.py b/auto_ml/implementations/augmentators/photometric.py
new file mode 100644
index 0000000..f3202b7
--- /dev/null
+++ b/auto_ml/implementations/augmentators/photometric.py
@@ -0,0 +1,269 @@
+"""Photometric (pixel-level) augmentation implementations."""
+
+import random
+
+import numpy as np
+from scipy import ndimage
+
+from auto_ml.interfaces import DataAugmentatorInterface, SegmentationDatasetInterface
+
+
+class BrightnessAugmentator(DataAugmentatorInterface):
+ """
+ Adjust image brightness.
+
+ Only affects the image, mask remains unchanged.
+ """
+
+ def __init__(
+ self,
+ brightness_range: tuple[float, float] = (0.8, 1.2),
+ random_seed: int | None = None,
+ ) -> None:
+ """
+ Initialize the brightness augmentator.
+
+ Args:
+ brightness_range: Range of brightness multipliers (min, max).
+ 1.0 = no change, < 1.0 = darker, > 1.0 = brighter.
+ random_seed: Random seed for reproducibility.
+
+ """
+ self.brightness_range = brightness_range
+ self.random_seed = random_seed
+
+ def augment(
+ self, dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Apply random brightness adjustment to all images."""
+ rng = random.Random(self.random_seed)
+ augmented_samples = []
+
+ for image, mask in dataset.samples:
+ # Random brightness factor
+ factor = rng.uniform(*self.brightness_range)
+
+ # Apply brightness
+ aug_image = np.clip(image * factor, 0, 255).astype(image.dtype)
+
+ augmented_samples.append((aug_image, mask))
+
+ return SegmentationDatasetInterface.from_pairs(
+ augmented_samples,
+ metadata={**dataset.metadata, "augmentation": "brightness"},
+ )
+
+
+class ContrastAugmentator(DataAugmentatorInterface):
+ """
+ Adjust image contrast.
+
+ Only affects the image, mask remains unchanged.
+ """
+
+ def __init__(
+ self,
+ contrast_range: tuple[float, float] = (0.8, 1.2),
+ random_seed: int | None = None,
+ ) -> None:
+ """
+ Initialize the contrast augmentator.
+
+ Args:
+ contrast_range: Range of contrast multipliers (min, max).
+ 1.0 = no change, < 1.0 = less contrast,
+ > 1.0 = more contrast.
+ random_seed: Random seed for reproducibility.
+
+ """
+ self.contrast_range = contrast_range
+ self.random_seed = random_seed
+
+ def augment(
+ self, dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Apply random contrast adjustment to all images."""
+ rng = random.Random(self.random_seed)
+ augmented_samples = []
+
+ for image, mask in dataset.samples:
+ # Random contrast factor
+ factor = rng.uniform(*self.contrast_range)
+
+ # Calculate mean for contrast adjustment
+ mean = np.mean(image)
+
+ # Apply contrast
+ aug_image = np.clip(
+ (image - mean) * factor + mean,
+ 0,
+ 255,
+ ).astype(image.dtype)
+
+ augmented_samples.append((aug_image, mask))
+
+ return SegmentationDatasetInterface.from_pairs(
+ augmented_samples,
+ metadata={**dataset.metadata, "augmentation": "contrast"},
+ )
+
+
+class GaussianNoiseAugmentator(DataAugmentatorInterface):
+ """
+ Add Gaussian noise to images.
+
+ Only affects the image, mask remains unchanged.
+ """
+
+ def __init__(
+ self,
+ noise_std_range: tuple[float, float] = (0.0, 10.0),
+ random_seed: int | None = None,
+ ) -> None:
+ """
+ Initialize the Gaussian noise augmentator.
+
+ Args:
+ noise_std_range: Range of noise standard deviation (min, max).
+ random_seed: Random seed for reproducibility.
+
+ """
+ self.noise_std_range = noise_std_range
+ self.random_seed = random_seed
+
+ def augment(
+ self, dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Add random Gaussian noise to all images."""
+ rng = np.random.default_rng(self.random_seed)
+ augmented_samples = []
+
+ for image, mask in dataset.samples:
+ # Random noise std
+ noise_std = rng.uniform(*self.noise_std_range)
+
+ # Generate noise
+ noise = rng.normal(0, noise_std, image.shape)
+
+ # Add noise
+ aug_image = np.clip(
+ image.astype(np.float32) + noise,
+ 0,
+ 255,
+ ).astype(image.dtype)
+
+ augmented_samples.append((aug_image, mask))
+
+ return SegmentationDatasetInterface.from_pairs(
+ augmented_samples,
+ metadata={**dataset.metadata, "augmentation": "gaussian_noise"},
+ )
+
+
+class GaussianBlurAugmentator(DataAugmentatorInterface):
+ """
+ Apply Gaussian blur to images.
+
+ Only affects the image, mask remains unchanged.
+ """
+
+ def __init__(
+ self,
+ sigma_range: tuple[float, float] = (0.0, 2.0),
+ random_seed: int | None = None,
+ ) -> None:
+ """
+ Initialize the Gaussian blur augmentator.
+
+ Args:
+ sigma_range: Range of blur sigma values (min, max).
+ Larger values = more blur.
+ random_seed: Random seed for reproducibility.
+
+ """
+ self.sigma_range = sigma_range
+ self.random_seed = random_seed
+
+ def augment(
+ self, dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Apply random Gaussian blur to all images."""
+ rng = random.Random(self.random_seed)
+ augmented_samples = []
+
+ for image, mask in dataset.samples:
+ # Random sigma
+ sigma = rng.uniform(*self.sigma_range)
+
+ # Apply blur
+ if image.ndim == 2:
+ aug_image = ndimage.gaussian_filter(image, sigma=sigma)
+ else:
+ # Apply to each channel
+ channels = []
+ for c in range(image.shape[2]):
+ blurred = ndimage.gaussian_filter(image[:, :, c], sigma=sigma)
+ channels.append(blurred)
+ aug_image = np.stack(channels, axis=2)
+
+ aug_image = aug_image.astype(image.dtype)
+
+ augmented_samples.append((aug_image, mask))
+
+ return SegmentationDatasetInterface.from_pairs(
+ augmented_samples,
+ metadata={**dataset.metadata, "augmentation": "gaussian_blur"},
+ )
+
+
+class GammaAugmentator(DataAugmentatorInterface):
+ """
+ Apply gamma correction to images.
+
+ Only affects the image, mask remains unchanged.
+ """
+
+ def __init__(
+ self,
+ gamma_range: tuple[float, float] = (0.8, 1.2),
+ random_seed: int | None = None,
+ ) -> None:
+ """
+ Initialize the gamma augmentator.
+
+ Args:
+ gamma_range: Range of gamma values (min, max).
+ 1.0 = no change, < 1.0 = brighter,
+ > 1.0 = darker.
+ random_seed: Random seed for reproducibility.
+
+ """
+ self.gamma_range = gamma_range
+ self.random_seed = random_seed
+
+ def augment(
+ self, dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Apply random gamma correction to all images."""
+ rng = random.Random(self.random_seed)
+ augmented_samples = []
+
+ for image, mask in dataset.samples:
+ # Random gamma
+ gamma = rng.uniform(*self.gamma_range)
+
+ # Normalize to [0, 1]
+ normalized = image.astype(np.float32) / 255.0
+
+ # Apply gamma
+ corrected = np.power(normalized, gamma)
+
+ # Scale back to [0, 255]
+ aug_image = (corrected * 255).astype(image.dtype)
+
+ augmented_samples.append((aug_image, mask))
+
+ return SegmentationDatasetInterface.from_pairs(
+ augmented_samples,
+ metadata={**dataset.metadata, "augmentation": "gamma"},
+ )
diff --git a/auto_ml/implementations/augmentators/sem_specific.py b/auto_ml/implementations/augmentators/sem_specific.py
new file mode 100644
index 0000000..0d905d8
--- /dev/null
+++ b/auto_ml/implementations/augmentators/sem_specific.py
@@ -0,0 +1,308 @@
+"""SEM-specific augmentation implementations for microscopy images."""
+
+import random
+from typing import Tuple
+
+import cv2
+import numpy as np
+from scipy import ndimage
+
+from auto_ml.interfaces import DataAugmentatorInterface, SegmentationDatasetInterface
+
+
+class ElasticDeformationAugmentator(DataAugmentatorInterface):
+ """
+ Apply elastic deformation to simulate natural rock texture variations.
+
+ Useful for SEM images where structures can appear with slight warping
+ due to sample preparation or natural material heterogeneity.
+ """
+
+ def __init__(
+ self,
+ alpha: float = 50.0,
+ sigma: float = 5.0,
+ random_seed: int | None = None,
+ ) -> None:
+ """
+ Initialize the elastic deformation augmentator.
+
+ Args:
+ alpha: Strength of deformation (higher = more distortion).
+ sigma: Smoothness of deformation field (higher = smoother).
+ random_seed: Random seed for reproducibility.
+
+ """
+ self.alpha = alpha
+ self.sigma = sigma
+ self.random_seed = random_seed
+
+ def augment(
+ self, dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Apply elastic deformation to all samples."""
+ rng = np.random.default_rng(self.random_seed)
+ augmented_samples = []
+
+ for image, mask in dataset.samples:
+ h, w = image.shape[:2]
+
+ # Generate random displacement fields
+ dx = rng.uniform(-1, 1, (h, w)) * self.alpha
+ dy = rng.uniform(-1, 1, (h, w)) * self.alpha
+
+ # Smooth the displacement fields
+ dx = ndimage.gaussian_filter(dx, self.sigma, mode="constant", cval=0)
+ dy = ndimage.gaussian_filter(dy, self.sigma, mode="constant", cval=0)
+
+ # Create meshgrid for remapping
+ x, y = np.meshgrid(np.arange(w), np.arange(h))
+ map_x = (x + dx).astype(np.float32)
+ map_y = (y + dy).astype(np.float32)
+
+ # Apply deformation to image
+ if image.ndim == 2:
+ aug_image = cv2.remap(
+ image,
+ map_x,
+ map_y,
+ interpolation=cv2.INTER_LINEAR,
+ borderMode=cv2.BORDER_REFLECT,
+ )
+ else:
+ aug_image = cv2.remap(
+ image,
+ map_x,
+ map_y,
+ interpolation=cv2.INTER_LINEAR,
+ borderMode=cv2.BORDER_REFLECT,
+ )
+
+ # Apply same deformation to mask (nearest neighbor)
+ aug_mask = cv2.remap(
+ mask.astype(np.float32),
+ map_x,
+ map_y,
+ interpolation=cv2.INTER_NEAREST,
+ borderMode=cv2.BORDER_REFLECT,
+ ).astype(mask.dtype)
+
+ augmented_samples.append((aug_image.astype(np.uint8), aug_mask))
+
+ return SegmentationDatasetInterface.from_pairs(
+ augmented_samples,
+ metadata={**dataset.metadata, "augmentation": "elastic_deformation"},
+ )
+
+
+class AdaptiveHistogramEqualizationAugmentator(DataAugmentatorInterface):
+ """
+ Apply CLAHE (Contrast Limited Adaptive Histogram Equalization).
+
+ Enhances local contrast, revealing subtle features in SEM images
+ that may be important for segmentation. Only affects the image.
+ """
+
+ def __init__(
+ self,
+ clip_limit: float = 2.0,
+ tile_grid_size: Tuple[int, int] = (8, 8),
+ ) -> None:
+ """
+ Initialize the CLAHE augmentator.
+
+ Args:
+ clip_limit: Threshold for contrast limiting (higher = more contrast).
+ tile_grid_size: Size of grid for local histogram equalization.
+
+ """
+ self.clip_limit = clip_limit
+ self.tile_grid_size = tile_grid_size
+
+ def augment(
+ self, dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Apply CLAHE to all images."""
+ clahe = cv2.createCLAHE(
+ clipLimit=self.clip_limit,
+ tileGridSize=self.tile_grid_size,
+ )
+
+ augmented_samples = []
+
+ for image, mask in dataset.samples:
+ # Apply CLAHE
+ if image.ndim == 2:
+ aug_image = clahe.apply(image)
+ else:
+ # Apply to each channel separately
+ channels = []
+ for c in range(image.shape[2]):
+ channel = clahe.apply(image[:, :, c])
+ channels.append(channel)
+ aug_image = np.stack(channels, axis=2)
+
+ augmented_samples.append((aug_image.astype(np.uint8), mask))
+
+ return SegmentationDatasetInterface.from_pairs(
+ augmented_samples,
+ metadata={**dataset.metadata, "augmentation": "clahe"},
+ )
+
+
+class ChargingArtifactAugmentator(DataAugmentatorInterface):
+ """
+ Simulate charging artifacts in SEM imaging of non-conductive samples.
+
+ Adds localized brightness variations that mimic electron charging
+ effects. Only affects the image.
+ """
+
+ def __init__(
+ self,
+ intensity_range: Tuple[float, float] = (0.7, 1.3),
+ num_spots: Tuple[int, int] = (1, 3),
+ spot_size_range: Tuple[int, int] = (30, 80),
+ random_seed: int | None = None,
+ ) -> None:
+ """
+ Initialize the charging artifact augmentator.
+
+ Args:
+ intensity_range: Range of brightness multipliers for charged spots.
+ num_spots: Range of number of charging spots to add.
+ spot_size_range: Range of spot radius in pixels.
+ random_seed: Random seed for reproducibility.
+
+ """
+ self.intensity_range = intensity_range
+ self.num_spots = num_spots
+ self.spot_size_range = spot_size_range
+ self.random_seed = random_seed
+
+ def augment(
+ self, dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Add charging artifacts to all images."""
+ rng = random.Random(self.random_seed)
+
+ augmented_samples = []
+
+ for image, mask in dataset.samples:
+ h, w = image.shape[:2]
+ aug_image = image.astype(np.float32).copy()
+
+ # Random number of spots
+ n_spots = rng.randint(*self.num_spots)
+
+ for _ in range(n_spots):
+ # Random spot location
+ cx = rng.randint(0, w - 1)
+ cy = rng.randint(0, h - 1)
+
+ # Random spot size
+ radius = rng.randint(*self.spot_size_range)
+
+ # Random intensity
+ intensity = rng.uniform(*self.intensity_range)
+
+ # Create Gaussian spot mask
+ y, x = np.ogrid[-cy : h - cy, -cx : w - cx]
+ distance = np.sqrt(x * x + y * y)
+ spot_mask = np.exp(-(distance**2) / (2 * (radius / 2) ** 2))
+
+ # Apply charging effect
+ if image.ndim == 2:
+ aug_image = aug_image * (1 + (intensity - 1) * spot_mask)
+ else:
+ for c in range(image.shape[2]):
+ aug_image[:, :, c] = aug_image[:, :, c] * (
+ 1 + (intensity - 1) * spot_mask
+ )
+
+ # Clip and convert back
+ aug_image_uint8: np.ndarray = np.clip(aug_image, 0, 255).astype(np.uint8)
+
+ augmented_samples.append((aug_image_uint8, mask))
+
+ return SegmentationDatasetInterface.from_pairs(
+ augmented_samples,
+ metadata={**dataset.metadata, "augmentation": "charging_artifact"},
+ )
+
+
+class ScanLineNoiseAugmentator(DataAugmentatorInterface):
+ """
+ Add scan line artifacts typical of SEM imaging.
+
+ Simulates beam drift or electromagnetic interference during scanning.
+ Only affects the image.
+ """
+
+ def __init__(
+ self,
+ probability: float = 0.3,
+ intensity_range: Tuple[float, float] = (0.02, 0.08),
+ direction: str = "horizontal",
+ random_seed: int | None = None,
+ ) -> None:
+ """
+ Initialize the scan line noise augmentator.
+
+ Args:
+ probability: Probability of adding noise to each line.
+ intensity_range: Range of noise intensity (fraction of max value).
+ direction: Direction of scan lines ("horizontal" or "vertical").
+ random_seed: Random seed for reproducibility.
+
+ """
+ self.probability = probability
+ self.intensity_range = intensity_range
+ self.direction = direction
+ self.random_seed = random_seed
+
+ def augment(
+ self, dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Add scan line noise to all images."""
+ rng = random.Random(self.random_seed)
+ np_rng = np.random.default_rng(self.random_seed)
+
+ augmented_samples = []
+
+ for image, mask in dataset.samples:
+ aug_image = image.astype(np.float32).copy()
+
+ # Random intensity for this image
+ intensity = rng.uniform(*self.intensity_range) * 255
+
+ if self.direction == "horizontal":
+ # Add horizontal scan lines
+ for i in range(aug_image.shape[0]):
+ if rng.random() < self.probability:
+ noise = np_rng.normal(0, intensity, aug_image.shape[1])
+ if image.ndim == 2:
+ aug_image[i, :] += noise
+ else:
+ for c in range(image.shape[2]):
+ aug_image[i, :, c] += noise
+ else:
+ # Add vertical scan lines
+ for j in range(aug_image.shape[1]):
+ if rng.random() < self.probability:
+ noise = np_rng.normal(0, intensity, aug_image.shape[0])
+ if image.ndim == 2:
+ aug_image[:, j] += noise
+ else:
+ for c in range(image.shape[2]):
+ aug_image[:, j, c] += noise
+
+ # Clip values to valid range
+ aug_image_uint8: np.ndarray = np.clip(aug_image, 0, 255).astype(np.uint8)
+
+ augmented_samples.append((aug_image_uint8, mask))
+
+ return SegmentationDatasetInterface.from_pairs(
+ augmented_samples,
+ metadata={**dataset.metadata, "augmentation": "scan_line_noise"},
+ )
diff --git a/auto_ml/implementations/classifiers/__init__.py b/auto_ml/implementations/classifiers/__init__.py
new file mode 100644
index 0000000..79e5b6e
--- /dev/null
+++ b/auto_ml/implementations/classifiers/__init__.py
@@ -0,0 +1,5 @@
+from auto_ml.implementations.classifiers.cnn import CNNModel
+from auto_ml.implementations.classifiers.swin import SwinModel
+from auto_ml.implementations.classifiers.vit import ViTModel
+
+__all__ = ["CNNModel", "ViTModel", "SwinModel"]
diff --git a/auto_ml/implementations/classifiers/cnn.py b/auto_ml/implementations/classifiers/cnn.py
new file mode 100644
index 0000000..e332019
--- /dev/null
+++ b/auto_ml/implementations/classifiers/cnn.py
@@ -0,0 +1,275 @@
+from pathlib import Path
+from typing import Dict, List, Tuple
+
+import numpy as np
+import torch
+import torch.nn as nn
+
+from auto_ml.interfaces import (
+ ClassificationDatasetInterface,
+ ClassificationModelInterface,
+ ImageArray,
+ MetricsResultInterface,
+)
+from auto_ml.models.cnn.model import CNNClassifier
+
+
+class CNNModel(ClassificationModelInterface):
+ """
+ CNN Model implementation for AutoML.
+
+ Wrap the CNNClassifier model from cnn.model to implement
+ the ClassificationModelInterface for region-based classification.
+ """
+
+ def __init__(
+ self,
+ num_classes: int = 3,
+ channels: int = 1,
+ base_filters: int = 32,
+ dropout: float = 0.5,
+ num_blocks: int = 3,
+ device: str = "auto",
+ train_epochs: int = 10,
+ train_batch_size: int = 16,
+ train_learning_rate: float = 0.01,
+ ) -> None:
+ """
+ Initialize the CNN Model.
+
+ Args:
+ num_classes: Number of output classes.
+ channels: Number of input channels (1 for grayscale, 3 for RGB).
+ base_filters: Base number of filters (doubled at each block).
+ dropout: Dropout rate before final classification layer.
+ num_blocks: Number of convolutional blocks. Each block halves the
+ spatial dimensions via MaxPool2d, so the minimum supported
+ input size is 2^num_blocks × 2^num_blocks pixels.
+ Examples: num_blocks=3 → min 8×8, num_blocks=5 → min 32×32.
+ device: Device to run the model on ("auto", "cuda", "mps", "cpu").
+ train_epochs: Number of training epochs.
+ train_batch_size: Training batch size.
+ train_learning_rate: Learning rate for training.
+
+ """
+ self.num_classes = num_classes
+ self.channels = channels
+ self.num_blocks = num_blocks
+ self.train_epochs = train_epochs
+ self.train_batch_size = train_batch_size
+ self.train_learning_rate = train_learning_rate
+
+ if device == "auto":
+ self.device = (
+ "cuda"
+ if torch.cuda.is_available()
+ else "mps"
+ if torch.backends.mps.is_available()
+ else "cpu"
+ )
+ else:
+ self.device = device
+
+ self.model = CNNClassifier(
+ num_classes=num_classes,
+ channels=channels,
+ base_filters=base_filters,
+ dropout=dropout,
+ num_blocks=num_blocks,
+ ).to(self.device)
+
+ self.model.eval()
+
+ def classify(
+ self,
+ image: ImageArray,
+ x: int,
+ y: int,
+ width: int,
+ height: int,
+ ) -> Tuple[int, float]:
+ """
+ Classify an image region.
+
+ Args:
+ image: Image as numpy array.
+ x: x-coordinate of the region.
+ y: y-coordinate of the region.
+ width: Width of the region.
+ height: Height of the region.
+
+ Returns:
+ Tuple of (class_label, confidence),
+ where class_label ∈ {0,1,2} and confidence ∈ [0,1].
+
+ """
+ # Extract region from image
+ region = image[y : y + height, x : x + width]
+
+ # Convert to tensor
+ region_tensor = self._preprocess_region(region)
+ region_tensor = region_tensor.to(self.device)
+
+ # Run inference
+ with torch.no_grad():
+ probabilities = self.model(region_tensor, return_logits=False)
+
+ # Get class label and confidence
+ confidence, class_label = torch.max(probabilities, dim=1)
+
+ return int(class_label.item()), float(confidence.item())
+
+ def _preprocess_region(self, region: ImageArray) -> torch.Tensor:
+ """
+ Preprocess an image region for the CNN model.
+
+ Args:
+ region: Image region as numpy array.
+
+ Returns:
+ Preprocessed tensor of shape (1, C, H, W).
+
+ """
+ # Normalize to [0, 1]
+ region_float = region.astype(np.float32) / 255.0
+
+ # Handle grayscale vs RGB
+ if region_float.ndim == 2:
+ # Grayscale: (H, W) -> (1, 1, H, W)
+ tensor = torch.from_numpy(region_float).unsqueeze(0).unsqueeze(0)
+ else:
+ # RGB: (H, W, C) -> (1, C, H, W)
+ tensor = torch.from_numpy(region_float).permute(2, 0, 1).unsqueeze(0)
+
+ # Handle channel mismatch
+ if tensor.shape[1] != self.channels:
+ if self.channels == 1 and tensor.shape[1] == 3:
+ # Convert RGB to grayscale using luminosity method
+ tensor = (
+ tensor[:, 0:1, :, :] * 0.299
+ + tensor[:, 1:2, :, :] * 0.587
+ + tensor[:, 2:3, :, :] * 0.114
+ )
+ elif self.channels == 3 and tensor.shape[1] == 1:
+ # Convert grayscale to RGB by repeating channels
+ tensor = tensor.repeat(1, 3, 1, 1)
+
+ return tensor
+
+ def load_weights(self, path: Path) -> None:
+ """
+ Load model weights from a file.
+
+ Args:
+ path: Path to the weights file.
+
+ """
+ self.model.load_state_dict(torch.load(path, map_location=self.device))
+ self.model.eval()
+
+ def save_weights(self, path: Path) -> None:
+ """
+ Save model weights to a file.
+
+ Args:
+ path: Path to save the weights file.
+
+ """
+ torch.save(self.model.state_dict(), path)
+
+ def train(
+ self,
+ dataset: ClassificationDatasetInterface,
+ ) -> MetricsResultInterface:
+ """
+ Train the CNN model on provided data.
+
+ Train the CNN model on the provided dataset. Handles variable-sized images
+ by processing them individually.
+
+ Args:
+ dataset: The training dataset.
+
+ Returns:
+ MetricsResultInterface containing training metrics.
+
+ """
+ # Set model to training mode
+ self.model.train()
+
+ # Convert dataset to tensors (returns list of tensors + labels)
+ images_list, labels_tensor = dataset.to_tensors()
+
+ # Group images by size for batching
+ size_groups: Dict[Tuple[int, int], List[int]] = {}
+ for idx, img in enumerate(images_list):
+ size = (img.shape[1], img.shape[2]) # (H, W)
+ if size not in size_groups:
+ size_groups[size] = []
+ size_groups[size].append(idx)
+
+ # Setup training
+ criterion = nn.CrossEntropyLoss()
+ optimizer = torch.optim.Adam(
+ self.model.parameters(),
+ lr=self.train_learning_rate,
+ )
+
+ final_loss = 0.0
+ final_accuracy = 0.0
+
+ # Train on grouped batches
+ for epoch in range(self.train_epochs):
+ epoch_loss = 0.0
+ correct = 0
+ total = 0
+
+ # Process each size group in sorted order for determinism
+ for size in sorted(size_groups.keys()):
+ indices = size_groups[size]
+ # Shuffle indices within each size group using numpy for determinism
+ perm = np.random.permutation(len(indices))
+ shuffled_indices = [indices[i] for i in perm]
+
+ # Create batches
+ for batch_start in range(
+ 0,
+ len(shuffled_indices),
+ self.train_batch_size,
+ ):
+ batch_indices = shuffled_indices[
+ batch_start : batch_start + self.train_batch_size
+ ]
+
+ # Stack images of same size into batch
+ batch_images = torch.stack(
+ [images_list[i] for i in batch_indices],
+ ).to(self.device)
+ batch_labels = torch.tensor(
+ [labels_tensor[i] for i in batch_indices],
+ dtype=torch.long,
+ ).to(self.device)
+
+ optimizer.zero_grad()
+ outputs = self.model(batch_images, return_logits=True)
+ loss = criterion(outputs, batch_labels)
+ loss.backward()
+ optimizer.step()
+
+ epoch_loss += loss.item() * batch_images.size(0)
+ _, predicted = outputs.max(1)
+ correct += predicted.eq(batch_labels).sum().item()
+ total += batch_labels.size(0)
+
+ final_loss = epoch_loss / total if total > 0 else 0.0
+ final_accuracy = correct / total if total > 0 else 0.0
+
+ # Log progress
+ print(f"Epoch {epoch + 1}/{self.train_epochs}, Loss: {final_loss:.6f}")
+
+ # Set model back to eval mode
+ self.model.eval()
+ return MetricsResultInterface(
+ accuracy=final_accuracy,
+ loss=final_loss,
+ )
diff --git a/auto_ml/implementations/classifiers/swin.py b/auto_ml/implementations/classifiers/swin.py
new file mode 100644
index 0000000..30dcf9b
--- /dev/null
+++ b/auto_ml/implementations/classifiers/swin.py
@@ -0,0 +1,237 @@
+from pathlib import Path
+from typing import List, Tuple
+
+import numpy as np
+import torch
+import torch.nn as nn
+import torch.nn.functional as F # noqa: N812
+
+from auto_ml.interfaces import (
+ ClassificationDatasetInterface,
+ ClassificationModelInterface,
+ ImageArray,
+ MetricsResultInterface,
+)
+from auto_ml.models.swin.classification import SwinClassifier
+
+
+class SwinModel(ClassificationModelInterface):
+ """
+ Swin Transformer Model implementation for AutoML.
+
+ Wraps the SwinClassifier model to implement the
+ ClassificationModelInterface for region-based classification.
+ """
+
+ def __init__(
+ self,
+ image_size: int = 224,
+ patch_size: int = 4,
+ num_classes: int = 3,
+ embed_dim: int = 96,
+ depths: List[int] = [2, 2, 6, 2],
+ num_heads: List[int] = [3, 6, 12, 24],
+ window_size: int = 7,
+ channels: int = 1,
+ dropout: float = 0.0,
+ device: str = "auto",
+ train_epochs: int = 10,
+ train_batch_size: int = 0,
+ train_learning_rate: float = 0.001,
+ ) -> None:
+ """
+ Initialize the SwinModel.
+
+ Args:
+ image_size: Input image size for the transformer.
+ patch_size: Size of each patch.
+ num_classes: Number of output classes.
+ embed_dim: Embedding dimension.
+ depths: Number of transformer layers in each stage.
+ num_heads: Number of attention heads in each stage.
+ window_size: Attention window size.
+ channels: Number of input channels (1 for grayscale, 3 for RGB).
+ dropout: Dropout rate.
+ device: Device to run on ("auto", "cuda", "mps", "cpu").
+ train_epochs: Number of training epochs.
+ train_batch_size: Training batch size. Adaptive if 0.
+ train_learning_rate: Learning rate for training.
+
+ """
+ self.num_classes = num_classes
+ self.channels = channels
+ self.image_size = image_size
+ self.train_epochs = train_epochs
+ self.train_learning_rate = train_learning_rate
+
+ if device == "auto":
+ self.device = (
+ "cuda"
+ if torch.cuda.is_available()
+ else "mps"
+ if torch.backends.mps.is_available()
+ else "cpu"
+ )
+ else:
+ self.device = device
+
+ if train_batch_size == 0:
+ if self.device == "cuda":
+ self.train_batch_size = 32
+ elif self.device == "mps":
+ self.train_batch_size = 16
+ else:
+ self.train_batch_size = 8
+ else:
+ self.train_batch_size = train_batch_size
+
+ self.model = SwinClassifier(
+ image_size=image_size,
+ patch_size=(patch_size, patch_size),
+ num_classes=num_classes,
+ embed_dim=embed_dim,
+ depths=depths,
+ num_heads=num_heads,
+ window_size=[window_size, window_size],
+ channels=channels,
+ dropout=dropout,
+ ).to(self.device)
+
+ self.model.eval()
+
+ def classify(
+ self,
+ image: ImageArray,
+ x: int,
+ y: int,
+ width: int,
+ height: int,
+ ) -> Tuple[int, float]:
+ """Classify an image region."""
+ region = image[y : y + height, x : x + width]
+ region_tensor = self._preprocess_region(region)
+ region_tensor = region_tensor.to(self.device)
+
+ with torch.no_grad():
+ probabilities = self.model(region_tensor, return_logits=False)
+
+ confidence, class_label = torch.max(probabilities, dim=1)
+
+ return int(class_label.item()), float(confidence.item())
+
+ def train(
+ self,
+ dataset: ClassificationDatasetInterface,
+ ) -> MetricsResultInterface:
+ """Train the Swin model."""
+ self.model.train()
+
+ images_list, labels_tensor = dataset.to_tensors()
+
+ criterion = nn.CrossEntropyLoss()
+ optimizer = torch.optim.Adam(
+ self.model.parameters(),
+ lr=self.train_learning_rate,
+ )
+
+ final_loss = 0.0
+ final_accuracy = 0.0
+
+ for epoch in range(self.train_epochs):
+ epoch_loss = 0.0
+ correct = 0
+ total = 0
+
+ indices = np.arange(len(images_list))
+ perm = np.random.permutation(len(indices))
+ shuffled_indices = indices[perm]
+
+ for batch_start in range(
+ 0,
+ len(shuffled_indices),
+ self.train_batch_size,
+ ):
+ batch_indices = shuffled_indices[
+ batch_start : batch_start + self.train_batch_size
+ ]
+
+ batch_images = []
+ for idx in batch_indices:
+ img = images_list[int(idx)]
+ if (
+ img.shape[1] != self.image_size
+ or img.shape[2] != self.image_size
+ ):
+ img = F.interpolate(
+ img.unsqueeze(0),
+ size=(self.image_size, self.image_size),
+ mode="bilinear",
+ align_corners=False,
+ ).squeeze(0)
+ batch_images.append(img)
+
+ batch_images_tensor = torch.stack(batch_images).to(self.device)
+ batch_labels = torch.tensor(
+ [labels_tensor[int(i)] for i in batch_indices],
+ dtype=torch.long,
+ ).to(self.device)
+
+ optimizer.zero_grad()
+ outputs = self.model(batch_images_tensor, return_logits=True)
+ loss = criterion(outputs, batch_labels)
+ loss.backward()
+ optimizer.step()
+
+ epoch_loss += loss.item() * batch_images_tensor.size(0)
+ _, predicted = outputs.max(1)
+ correct += predicted.eq(batch_labels).sum().item()
+ total += batch_labels.size(0)
+
+ final_loss = epoch_loss / total if total > 0 else 0.0
+ final_accuracy = correct / total if total > 0 else 0.0
+
+ print(f"Epoch {epoch + 1}/{self.train_epochs}, Loss: {final_loss:.6f}")
+
+ self.model.eval()
+ return MetricsResultInterface(
+ accuracy=final_accuracy,
+ loss=final_loss,
+ )
+
+ def _preprocess_region(self, region: ImageArray) -> torch.Tensor:
+ """Preprocess a region for the Swin model."""
+ region_float = region.astype(np.float32) / 255.0
+
+ if region_float.ndim == 2:
+ tensor = torch.from_numpy(region_float).unsqueeze(0).unsqueeze(0)
+ else:
+ tensor = torch.from_numpy(region_float).permute(2, 0, 1).unsqueeze(0)
+
+ if tensor.shape[1] != self.channels:
+ if self.channels == 1 and tensor.shape[1] == 3:
+ tensor = (
+ tensor[:, 0:1, :, :] * 0.299
+ + tensor[:, 1:2, :, :] * 0.587
+ + tensor[:, 2:3, :, :] * 0.114
+ )
+ elif self.channels == 3 and tensor.shape[1] == 1:
+ tensor = tensor.repeat(1, 3, 1, 1)
+
+ if tensor.shape[2] != self.image_size or tensor.shape[3] != self.image_size:
+ tensor = F.interpolate(
+ tensor,
+ size=(self.image_size, self.image_size),
+ mode="bilinear",
+ align_corners=False,
+ )
+
+ return tensor
+
+ def load_weights(self, path: Path) -> None:
+ """Load model weights from a file."""
+ self.model.load_state_dict(torch.load(path, map_location=self.device))
+ self.model.eval()
+
+ def save_weights(self, path: Path) -> None:
+ """Save model weights to a file."""
+ torch.save(self.model.state_dict(), path)
diff --git a/auto_ml/implementations/classifiers/vit.py b/auto_ml/implementations/classifiers/vit.py
new file mode 100644
index 0000000..f17f007
--- /dev/null
+++ b/auto_ml/implementations/classifiers/vit.py
@@ -0,0 +1,310 @@
+from pathlib import Path
+from typing import Tuple
+
+import numpy as np
+import torch
+import torch.nn as nn
+import torch.nn.functional as F # noqa: N812
+
+from auto_ml.interfaces import (
+ ClassificationDatasetInterface,
+ ClassificationModelInterface,
+ ImageArray,
+ MetricsResultInterface,
+)
+from auto_ml.models.vit.classification import ViTClassification
+
+
+class ViTModel(ClassificationModelInterface):
+ """
+ ViT Model implementation for AutoML.
+
+ Wraps the ViTClassification model from vit.classification to implement
+ the ClassificationModelInterface for region-based classification.
+ """
+
+ def __init__(
+ self,
+ image_size: int = 512,
+ patch_size: int = 16,
+ num_classes: int = 3,
+ dim: int = 768,
+ depth: int = 12,
+ heads: int = 12,
+ mlp_dim: int = 3072,
+ channels: int = 1,
+ dropout: float = 0.1,
+ emb_dropout: float = 0.1,
+ device: str = "auto",
+ train_epochs: int = 10,
+ train_batch_size: int = 0,
+ train_learning_rate: float = 0.001,
+ ) -> None:
+ """
+ Initialize the ViT Model.
+
+ Args:
+ image_size: Input image size (must be divisible by patch_size).
+ patch_size: Size of each patch.
+ num_classes: Number of output classes.
+ dim: Transformer embedding dimension.
+ depth: Number of transformer encoder layers.
+ heads: Number of attention heads.
+ mlp_dim: Dimension of the MLP feedforward layer.
+ channels: Number of input channels (1 for grayscale, 3 for RGB).
+ dropout: Dropout rate in transformer and classification head.
+ emb_dropout: Dropout rate after positional embedding.
+ device: Device to run the model on ("auto", "cuda", "mps", "cpu").
+ train_epochs: Number of training epochs.
+ train_batch_size: Training batch size. If 0, uses adaptive sizing
+ (32 for CUDA, 16 for MPS, 8 for CPU).
+ train_learning_rate: Learning rate for training.
+
+ """
+ self.num_classes = num_classes
+ self.channels = channels
+ self.image_size = image_size
+ self.train_epochs = train_epochs
+ self.train_learning_rate = train_learning_rate
+
+ if device == "auto":
+ self.device = (
+ "cuda"
+ if torch.cuda.is_available()
+ else "mps"
+ if torch.backends.mps.is_available()
+ else "cpu"
+ )
+ else:
+ self.device = device
+
+ # Set adaptive batch size if not specified
+ if train_batch_size == 0:
+ if self.device == "cuda":
+ self.train_batch_size = 32
+ elif self.device == "mps":
+ self.train_batch_size = 16
+ else:
+ self.train_batch_size = 8
+ else:
+ self.train_batch_size = train_batch_size
+
+ self.model = ViTClassification(
+ image_size=image_size,
+ patch_size=patch_size,
+ num_classes=num_classes,
+ dim=dim,
+ depth=depth,
+ heads=heads,
+ mlp_dim=mlp_dim,
+ channels=channels,
+ dropout=dropout,
+ emb_dropout=emb_dropout,
+ ).to(self.device)
+
+ self.model.eval()
+
+ def classify(
+ self,
+ image: ImageArray,
+ x: int,
+ y: int,
+ width: int,
+ height: int,
+ ) -> Tuple[int, float]:
+ """
+ Classify an image region.
+
+ Args:
+ image: Image as numpy array.
+ x: x-coordinate of the region.
+ y: y-coordinate of the region.
+ width: Width of the region.
+ height: Height of the region.
+
+ Returns:
+ Tuple of (class_label, confidence),
+ where class_label ∈ {0,1,2} and confidence ∈ [0,1].
+
+ """
+ # Extract region from image
+ region = image[y : y + height, x : x + width]
+
+ # Convert to tensor and resize to expected image_size
+ region_tensor = self._preprocess_region(region)
+ region_tensor = region_tensor.to(self.device)
+
+ # Run inference
+ with torch.no_grad():
+ probabilities = self.model(region_tensor, return_logits=False)
+
+ # Get class label and confidence
+ confidence, class_label = torch.max(probabilities, dim=1)
+
+ return int(class_label.item()), float(confidence.item())
+
+ def train(
+ self,
+ dataset: ClassificationDatasetInterface,
+ ) -> MetricsResultInterface:
+ """
+ Train the ViT model on provided data.
+
+ Args:
+ dataset: The training dataset.
+
+ Returns:
+ MetricsResultInterface containing training metrics.
+
+ """
+ # Set model to training mode
+ self.model.train()
+
+ # Convert dataset to tensors (returns list of tensors + labels)
+ images_list, labels_tensor = dataset.to_tensors()
+
+ # Setup training
+ criterion = nn.CrossEntropyLoss()
+ optimizer = torch.optim.Adam(
+ self.model.parameters(),
+ lr=self.train_learning_rate,
+ )
+
+ final_loss = 0.0
+ final_accuracy = 0.0
+
+ # Train over epochs
+ for epoch in range(self.train_epochs):
+ epoch_loss = 0.0
+ correct = 0
+ total = 0
+
+ # Shuffle indices for this epoch
+ indices = np.arange(len(images_list))
+ perm = np.random.permutation(len(indices))
+ shuffled_indices = indices[perm]
+
+ # Create batches
+ for batch_start in range(
+ 0,
+ len(shuffled_indices),
+ self.train_batch_size,
+ ):
+ batch_indices = shuffled_indices[
+ batch_start : batch_start + self.train_batch_size
+ ]
+
+ # Get batch images and preprocess to fixed size
+ batch_images = []
+ for idx in batch_indices:
+ img = images_list[int(idx)]
+ # Ensure image is the correct size
+ if (
+ img.shape[1] != self.image_size
+ or img.shape[2] != self.image_size
+ ):
+ img = F.interpolate(
+ img.unsqueeze(0),
+ size=(self.image_size, self.image_size),
+ mode="bilinear",
+ align_corners=False,
+ ).squeeze(0)
+ batch_images.append(img)
+
+ # Stack batch
+ batch_images_tensor = torch.stack(batch_images).to(self.device)
+ batch_labels = torch.tensor(
+ [labels_tensor[int(i)] for i in batch_indices],
+ dtype=torch.long,
+ ).to(self.device)
+
+ optimizer.zero_grad()
+ outputs = self.model(batch_images_tensor, return_logits=True)
+ loss = criterion(outputs, batch_labels)
+ loss.backward()
+ optimizer.step()
+
+ epoch_loss += loss.item() * batch_images_tensor.size(0)
+ _, predicted = outputs.max(1)
+ correct += predicted.eq(batch_labels).sum().item()
+ total += batch_labels.size(0)
+
+ final_loss = epoch_loss / total if total > 0 else 0.0
+ final_accuracy = correct / total if total > 0 else 0.0
+
+ # Log progress
+ print(f"Epoch {epoch + 1}/{self.train_epochs}, Loss: {final_loss:.6f}")
+
+ # Set model back to eval mode
+ self.model.eval()
+ return MetricsResultInterface(
+ accuracy=final_accuracy,
+ loss=final_loss,
+ )
+
+ def _preprocess_region(self, region: ImageArray) -> torch.Tensor:
+ """
+ Preprocess an image region for the ViT model.
+
+ Args:
+ region: Image region as numpy array.
+
+ Returns:
+ Preprocessed tensor of shape (1, C, image_size, image_size).
+
+ """
+ # Normalize to [0, 1]
+ region_float = region.astype(np.float32) / 255.0
+
+ # Handle grayscale vs RGB
+ if region_float.ndim == 2:
+ # Grayscale: (H, W) -> (1, 1, H, W)
+ tensor = torch.from_numpy(region_float).unsqueeze(0).unsqueeze(0)
+ else:
+ # RGB: (H, W, C) -> (1, C, H, W)
+ tensor = torch.from_numpy(region_float).permute(2, 0, 1).unsqueeze(0)
+
+ # Handle channel mismatch
+ if tensor.shape[1] != self.channels:
+ if self.channels == 1 and tensor.shape[1] == 3:
+ # Convert RGB to grayscale using luminosity method
+ tensor = (
+ tensor[:, 0:1, :, :] * 0.299
+ + tensor[:, 1:2, :, :] * 0.587
+ + tensor[:, 2:3, :, :] * 0.114
+ )
+ elif self.channels == 3 and tensor.shape[1] == 1:
+ # Convert grayscale to RGB by repeating channels
+ tensor = tensor.repeat(1, 3, 1, 1)
+
+ # Resize to expected image_size (ViT requires fixed input size)
+ if tensor.shape[2] != self.image_size or tensor.shape[3] != self.image_size:
+ tensor = F.interpolate(
+ tensor,
+ size=(self.image_size, self.image_size),
+ mode="bilinear",
+ align_corners=False,
+ )
+
+ return tensor
+
+ def load_weights(self, path: Path) -> None:
+ """
+ Load model weights from a file.
+
+ Args:
+ path: Path to the weights file.
+
+ """
+ self.model.load_state_dict(torch.load(path, map_location=self.device))
+ self.model.eval()
+
+ def save_weights(self, path: Path) -> None:
+ """
+ Save model weights to a file.
+
+ Args:
+ path: Path to save the weights file.
+
+ """
+ torch.save(self.model.state_dict(), path)
diff --git a/auto_ml/implementations/datasets.py b/auto_ml/implementations/datasets.py
new file mode 100644
index 0000000..f306754
--- /dev/null
+++ b/auto_ml/implementations/datasets.py
@@ -0,0 +1,170 @@
+"""Dataset loading utilities."""
+
+from pathlib import Path
+from typing import List, Tuple
+
+import numpy as np
+from PIL import Image
+
+from auto_ml.interfaces import (
+ ClassificationDatasetInterface,
+ SegmentationDatasetInterface,
+)
+
+
+def load_dataset_from_directories(
+ input_dir: Path,
+ target_dir: Path,
+ target_size: Tuple[int, int] = (512, 512),
+) -> SegmentationDatasetInterface:
+ """
+ Load dataset from input and target directories.
+
+ Args:
+ input_dir: Directory containing input images.
+ target_dir: Directory containing target (labeled) images.
+ target_size: tuple (height, width) to resize images to.
+
+ Returns:
+ Populated DatasetInterface.
+
+ """
+ print(f"Loading from:\n Input: {input_dir}\n Target: {target_dir}")
+
+ input_path = Path(input_dir)
+ target_path = Path(target_dir)
+
+ # Get all input files
+ input_files = sorted(
+ [
+ f
+ for f in input_path.glob("*")
+ if f.suffix.lower() in [".jpg", ".jpeg", ".png", ".tif", ".tiff"]
+ ],
+ )
+
+ # Get all target files
+ target_files = sorted(
+ [
+ f
+ for f in target_path.glob("*")
+ if f.suffix.lower() in [".png"] and "_labeled" in f.name
+ ],
+ )
+
+ dataset = SegmentationDatasetInterface()
+
+ # Create valid pairs
+ # Heuristic: Target stem should start with Input stem
+ # Or more robust: Target name is Input stem + "_labeled"
+
+ # Let's map normalized stems to consistency
+ target_map = {}
+ for t in target_files:
+ # standard format: "name_labeled.png" -> key: "name"
+ # Handle " _labeled" or "_labeled"
+ stem = t.stem
+ if stem.endswith("_labeled"):
+ key = stem[:-8].strip() # remove _labeled and strip spaces
+ target_map[key] = t
+
+ matched_count = 0
+
+ for inp in input_files:
+ key = inp.stem.strip()
+
+ if key in target_map:
+ target_file = target_map[key]
+ try:
+ # 1. Load Input (Grayscale L)
+ input_img = Image.open(inp).convert("L")
+ input_img = input_img.resize((512, 512))
+ input_np = np.array(input_img)
+
+ # 2. Load Target (RGB)
+ target_img = Image.open(target_file).convert("RGB")
+ target_img = target_img.resize(
+ (512, 512),
+ resample=Image.Resampling.NEAREST,
+ )
+ target_np = np.array(target_img)
+
+ # 3. Process Mask (Red/Green logic from verify logic or implementations)
+ # Reusing logic from implementations.py load_dataset_from_directories
+ r = target_np[:, :, 0]
+ g = target_np[:, :, 1]
+ b = target_np[:, :, 2]
+
+ is_red = (r > 100) & (r > g + 20) & (r > b + 20)
+ is_green = (g > 100) & (g > r + 20) & (g > b + 20)
+
+ mask = np.full_like(r, 2, dtype=np.uint8) # Default 2 (background?)
+ mask[is_red] = 0
+ mask[is_green] = 1
+
+ dataset.add_sample(input_np, mask)
+ matched_count += 1
+
+ except Exception as e:
+ print(f"Error loading {inp.name}: {e}")
+ else:
+ pass
+
+ print(f"Loaded {len(dataset)} pairs out of {len(input_files)} input files.")
+ return dataset
+
+
+def load_classification_dataset_from_dir(
+ input_path: Path,
+ class_subdirs: List[str] = ["Brittle", "Ductile", "Mixed"],
+ target_size: Tuple[int, int] = (512, 512),
+) -> ClassificationDatasetInterface:
+ """
+ Load dataset from input directory.
+
+ This method assumes the input_path is a folder which contains one sub folder
+ for each class specified in class_subdirs.
+
+ Args:
+ input_path: Directory containing input images.
+ class_subdirs: List of subfolders to load.
+ target_size: tuple (height, width) to resize images to.
+
+ Returns:
+ Populated DatasetInterface.
+
+ """
+ print(f"Loading classification dataset from:\n Root: {input_path}")
+
+ dataset = ClassificationDatasetInterface()
+ total_count = 0
+
+ for label, folder in enumerate(class_subdirs):
+ class_dir = input_path / folder
+
+ if not class_dir.exists():
+ print(f"Warning: class directory not found: {class_dir}")
+ continue
+
+ input_files = sorted(
+ [
+ f
+ for f in class_dir.glob("*")
+ if f.suffix.lower() in [".jpg", ".jpeg", ".png", ".tif", ".tiff"]
+ ],
+ )
+
+ for inp in input_files:
+ try:
+ input_img: Image.Image | Image.ImageFile.ImageFile = Image.open(inp)
+ input_img = input_img.resize((target_size[1], target_size[0]))
+ input_np = np.array(input_img)
+
+ dataset.add_sample(input_np, label)
+ total_count += 1
+
+ except Exception as e:
+ print(f"Error loading {inp.name}: {e}")
+
+ print(f"Loaded {total_count} samples across {len(class_subdirs)} classes.")
+ return dataset
diff --git a/auto_ml/implementations/evaluators/__init__.py b/auto_ml/implementations/evaluators/__init__.py
new file mode 100644
index 0000000..bfc78d9
--- /dev/null
+++ b/auto_ml/implementations/evaluators/__init__.py
@@ -0,0 +1,65 @@
+"""Evaluator implementations subpackage."""
+
+from auto_ml.implementations.evaluators.accuracy import AccuracyEvaluator
+from auto_ml.implementations.evaluators.autoencoder import AutoencoderMaskEvaluator
+
+# Dice metrics
+from auto_ml.implementations.evaluators.dice import (
+ DiceClass0Evaluator,
+ DiceClass1Evaluator,
+ DiceClass2Evaluator,
+ DiceMacroAverageEvaluator,
+ DiceWeightedAverageEvaluator,
+)
+
+# IoU metrics
+from auto_ml.implementations.evaluators.iou import (
+ IoUClass0Evaluator,
+ IoUClass1Evaluator,
+ IoUClass2Evaluator,
+ IoUMacroAverageEvaluator,
+ IoUWeightedAverageEvaluator,
+)
+
+# Precision metrics
+from auto_ml.implementations.evaluators.precision import (
+ PrecisionClass0Evaluator,
+ PrecisionClass1Evaluator,
+ PrecisionClass2Evaluator,
+ PrecisionMacroAverageEvaluator,
+)
+
+# Recall metrics
+from auto_ml.implementations.evaluators.recall import (
+ RecallClass0Evaluator,
+ RecallClass1Evaluator,
+ RecallClass2Evaluator,
+ RecallMacroAverageEvaluator,
+)
+
+__all__ = [
+ "AccuracyEvaluator",
+ "AutoencoderMaskEvaluator",
+ # Dice Metrics
+ "DiceClass0Evaluator",
+ "DiceClass1Evaluator",
+ "DiceClass2Evaluator",
+ "DiceMacroAverageEvaluator",
+ "DiceWeightedAverageEvaluator",
+ # IoU Metrics
+ "IoUClass0Evaluator",
+ "IoUClass1Evaluator",
+ "IoUClass2Evaluator",
+ "IoUMacroAverageEvaluator",
+ "IoUWeightedAverageEvaluator",
+ # Precision Metrics
+ "PrecisionClass0Evaluator",
+ "PrecisionClass1Evaluator",
+ "PrecisionClass2Evaluator",
+ "PrecisionMacroAverageEvaluator",
+ # Recall Metrics
+ "RecallClass0Evaluator",
+ "RecallClass1Evaluator",
+ "RecallClass2Evaluator",
+ "RecallMacroAverageEvaluator",
+]
diff --git a/auto_ml/implementations/evaluators/accuracy.py b/auto_ml/implementations/evaluators/accuracy.py
new file mode 100644
index 0000000..e6a9916
--- /dev/null
+++ b/auto_ml/implementations/evaluators/accuracy.py
@@ -0,0 +1,23 @@
+"""Accuracy evaluator implementation."""
+
+from typing import List
+
+from auto_ml.interfaces import EvaluatorInterface, MaskPair
+
+
+class AccuracyEvaluator(EvaluatorInterface):
+ """Accuracy Evaluator: Calculates overall pixel-wise accuracy."""
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """Evaluate overall accuracy across all mask pairs."""
+ total_correct = 0
+ total_pixels = 0
+
+ for fold_pairs in mask_pairs:
+ for predicted_mask, real_mask in fold_pairs:
+ correct = (predicted_mask == real_mask).sum()
+ total_correct += correct
+ total_pixels += real_mask.size
+
+ accuracy = total_correct / total_pixels if total_pixels > 0 else 0.0
+ return float(accuracy)
diff --git a/auto_ml/implementations/evaluators/autoencoder.py b/auto_ml/implementations/evaluators/autoencoder.py
new file mode 100644
index 0000000..6d29785
--- /dev/null
+++ b/auto_ml/implementations/evaluators/autoencoder.py
@@ -0,0 +1,187 @@
+"""Autoencoder-based mask evaluator implementation."""
+
+from typing import Any, List, Optional
+
+import numpy as np
+import torch
+import torch.nn as nn
+
+from auto_ml.interfaces import EvaluatorInterface, MaskArray, MaskPair
+from auto_ml.models.maskautoencoder.maskautoencoder import MaskAutoencoder
+
+
+class AutoencoderMaskEvaluator(EvaluatorInterface):
+ """
+ Autoencoder-based Mask Evaluator.
+
+ Uses a convolutional autoencoder to embed masks into 3D space,
+ trains One-Class SVM on reference masks, then evaluates how many
+ predicted masks match the learned distribution.
+
+ Workflow:
+ 1. Initialize with reference masks → trains autoencoder + SVM
+ 2. evaluate() classifies predicted masks against learned distribution
+
+ Returns: float - ratio of predicted masks matching reference distribution (0.0-1.0)
+ """
+
+ def __init__(
+ self,
+ reference_masks: List[MaskArray],
+ latent_dim: int = 3,
+ epochs: int = 50,
+ batch_size: int = 4,
+ lr: float = 1e-3,
+ nu: float = 0.1,
+ kernel: str = "rbf",
+ device: str = "auto",
+ ) -> None:
+ """
+ Initialize and train the autoencoder evaluator.
+
+ Args:
+ reference_masks: List of 512x512 mask arrays defining the target class.
+ latent_dim: Dimension of the latent space (default: 3).
+ epochs: Number of training epochs for autoencoder.
+ batch_size: Batch size for training.
+ lr: Learning rate.
+ nu: One-Class SVM nu parameter (proportion of outliers).
+ kernel: One-Class SVM kernel type.
+ device: Device to use ('auto', 'cpu', 'cuda', 'mps').
+
+ """
+ self.reference_masks = reference_masks
+ self.latent_dim = latent_dim
+ self.epochs = epochs
+ self.batch_size = batch_size
+ self.lr = lr
+ self.nu = nu
+ self.kernel = kernel
+
+ # Validate reference masks
+ if len(reference_masks) < 2:
+ raise ValueError("Need at least 2 reference masks for training")
+
+ for i, mask in enumerate(reference_masks):
+ if mask.shape != (512, 512):
+ raise ValueError(
+ f"Reference mask {i} must be 512x512, got {mask.shape}",
+ )
+
+ # Setup device
+ if device == "auto":
+ self.device = (
+ "cuda"
+ if torch.cuda.is_available()
+ else "mps"
+ if torch.backends.mps.is_available()
+ else "cpu"
+ )
+ else:
+ self.device = device
+
+ # Initialize and train autoencoder
+ self.autoencoder = MaskAutoencoder(latent_dim=latent_dim).to(self.device)
+ self._classifier: Optional[Any] = None
+
+ # Train immediately
+ self._train(reference_masks)
+
+ def _mask_to_tensor(self, mask: MaskArray) -> torch.Tensor:
+ """Convert mask array to normalized tensor."""
+ mask_normalized = (
+ mask.astype(np.float32) / mask.max()
+ if mask.max() > 0
+ else mask.astype(np.float32)
+ )
+ tensor = torch.from_numpy(mask_normalized).unsqueeze(0).unsqueeze(0)
+ return tensor.to(self.device)
+
+ def _train(self, masks: List[MaskArray]) -> None:
+ """Train autoencoder and One-Class SVM on reference masks."""
+ from sklearn.svm import OneClassSVM
+
+ print(f"Training AutoencoderMaskEvaluator on {len(masks)} reference masks...")
+
+ tensors = [self._mask_to_tensor(m).squeeze(0) for m in masks]
+ dataset_tensor = torch.stack(tensors)
+
+ self.autoencoder.train()
+ optimizer = torch.optim.Adam(self.autoencoder.parameters(), lr=self.lr)
+ criterion = nn.MSELoss()
+
+ for epoch in range(self.epochs):
+ total_loss = 0.0
+ indices = torch.randperm(len(dataset_tensor))
+
+ for i in range(0, len(dataset_tensor), self.batch_size):
+ batch_idx = indices[i : i + self.batch_size]
+ batch = dataset_tensor[batch_idx].to(self.device)
+
+ optimizer.zero_grad()
+ recon, _ = self.autoencoder(batch)
+ loss = criterion(recon, batch)
+ loss.backward()
+ optimizer.step()
+ total_loss += loss.item()
+
+ if (epoch + 1) % 20 == 0 or epoch == 0:
+ avg_loss = total_loss / max(1, len(dataset_tensor) / self.batch_size)
+ print(f" Epoch {epoch + 1}/{self.epochs}, Loss: {avg_loss:.6f}")
+
+ # Extract embeddings and fit One-Class SVM
+ self.autoencoder.eval()
+ embeddings = []
+ with torch.no_grad():
+ for t in tensors:
+ z = self.autoencoder.encode(t.unsqueeze(0).to(self.device))
+ embeddings.append(z.cpu().numpy().squeeze())
+
+ self._classifier = OneClassSVM(nu=self.nu, kernel=self.kernel)
+ assert self._classifier is not None
+ self._classifier.fit(np.array(embeddings))
+ print(" Training complete.")
+
+ def encode(self, mask: MaskArray) -> np.ndarray:
+ """Encode a single mask to its 3D embedding."""
+ self.autoencoder.eval()
+ with torch.no_grad():
+ tensor = self._mask_to_tensor(mask)
+ z = self.autoencoder.encode(tensor)
+ return z.cpu().numpy().squeeze().astype(np.float32)
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate mask pairs against the reference distribution.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ Float ratio (0.0-1.0) of predicted masks matching the reference
+ distribution.
+
+ """
+ predicted_masks = []
+ for fold_pairs in mask_pairs:
+ for pred_mask, _ in fold_pairs:
+ predicted_masks.append(pred_mask)
+
+ if not predicted_masks:
+ return 0.0
+
+ # Classify predicted masks against reference distribution
+ assert self._classifier is not None
+ matches = 0
+ for pred_mask in predicted_masks:
+ embedding = self.encode(pred_mask)
+ prediction = self._classifier.predict(embedding.reshape(1, -1))
+ if prediction[0] == 1: # Inlier
+ matches += 1
+
+ return matches / len(predicted_masks)
+
+ def get_embeddings(self, masks: List[MaskArray]) -> np.ndarray:
+ """Get 3D embeddings for a list of masks."""
+ return np.array([self.encode(m) for m in masks])
diff --git a/auto_ml/implementations/evaluators/dice.py b/auto_ml/implementations/evaluators/dice.py
new file mode 100644
index 0000000..ae914a1
--- /dev/null
+++ b/auto_ml/implementations/evaluators/dice.py
@@ -0,0 +1,307 @@
+"""Dice coefficient evaluators for multi-class segmentation."""
+
+from typing import List, Tuple
+
+import numpy as np
+
+from auto_ml.interfaces import EvaluatorInterface, MaskArray, MaskPair
+
+
+class DiceClass0Evaluator(EvaluatorInterface):
+ """
+ Dice Coefficient (F1 Score) for class 0.
+
+ Measure the overlap between predicted and ground truth regions
+ for class 0. More sensitive to small regions than IoU.
+
+ The Dice coefficient is the harmonic mean of precision and recall,
+ and is equivalent to the F1 score. It emphasizes the intersection
+ more heavily than IoU.
+
+ Formula: Dice = 2*TP / (2*TP + FP + FN)
+ Range: [0.0, 1.0] where 1.0 is perfect overlap
+
+ Edge cases:
+ - Return 0.0 if class 0 never appears in predictions or ground truth
+
+ """
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate Dice coefficient for class 0 across all mask pairs.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ Dice coefficient for class 0 as float in [0.0, 1.0].
+
+ """
+ tp, fp, fn, tn = aggregate_confusion_matrices(mask_pairs, class_id=0)
+
+ denominator = 2 * tp + fp + fn
+ if denominator == 0:
+ return 0.0
+
+ dice = (2 * tp) / denominator
+ return float(dice)
+
+
+class DiceClass1Evaluator(EvaluatorInterface):
+ """
+ Dice Coefficient (F1 Score) for class 1.
+
+ Measure the overlap between predicted and ground truth regions
+ for class 1. More sensitive to small regions than IoU.
+
+ The Dice coefficient is the harmonic mean of precision and recall,
+ and is equivalent to the F1 score. It emphasizes the intersection
+ more heavily than IoU.
+
+ Formula: Dice = 2*TP / (2*TP + FP + FN)
+ Range: [0.0, 1.0] where 1.0 is perfect overlap
+
+ Edge cases:
+ - Return 0.0 if class 1 never appears in predictions or ground truth
+
+ """
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate Dice coefficient for class 1 across all mask pairs.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ Dice coefficient for class 1 as float in [0.0, 1.0].
+
+ """
+ tp, fp, fn, tn = aggregate_confusion_matrices(mask_pairs, class_id=1)
+
+ denominator = 2 * tp + fp + fn
+ if denominator == 0:
+ return 0.0
+
+ dice = (2 * tp) / denominator
+ return float(dice)
+
+
+class DiceClass2Evaluator(EvaluatorInterface):
+ """
+ Dice Coefficient (F1 Score) for class 2.
+
+ Measure the overlap between predicted and ground truth regions
+ for class 2. More sensitive to small regions than IoU.
+
+ The Dice coefficient is the harmonic mean of precision and recall,
+ and is equivalent to the F1 score. It emphasizes the intersection
+ more heavily than IoU.
+
+ Formula: Dice = 2*TP / (2*TP + FP + FN)
+ Range: [0.0, 1.0] where 1.0 is perfect overlap
+
+ Edge cases:
+ - Return 0.0 if class 2 never appears in predictions or ground truth
+
+ """
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate Dice coefficient for class 2 across all mask pairs.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ Dice coefficient for class 2 as float in [0.0, 1.0].
+
+ """
+ tp, fp, fn, tn = aggregate_confusion_matrices(mask_pairs, class_id=2)
+
+ denominator = 2 * tp + fp + fn
+ if denominator == 0:
+ return 0.0
+
+ dice = (2 * tp) / denominator
+ return float(dice)
+
+
+class DiceMacroAverageEvaluator(EvaluatorInterface):
+ """
+ Dice Coefficient Macro Average across all classes (unweighted mean).
+
+ Compute Dice for each class independently and return the unweighted mean.
+ Treat all classes equally regardless of their frequency in the dataset.
+
+ This metric is useful when all classes are equally important, particularly
+ for SEM segmentation where minority material phases should not be ignored.
+
+ Formula: Dice_macro = (Dice_0 + Dice_1 + Dice_2) / 3
+ Range: [0.0, 1.0] where 1.0 is perfect overlap for all classes
+
+ Edge cases:
+ - Classes that never appear contribute 0.0 to the average
+ - Always divide by 3 (all classes) for consistency
+
+ """
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate macro-averaged Dice across all classes.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ Macro-averaged Dice as float in [0.0, 1.0].
+
+ """
+ dices = []
+
+ for class_id in [0, 1, 2]:
+ tp, fp, fn, tn = aggregate_confusion_matrices(mask_pairs, class_id)
+ denominator = 2 * tp + fp + fn
+
+ if denominator == 0:
+ dice = 0.0
+ else:
+ dice = (2 * tp) / denominator
+
+ dices.append(dice)
+
+ macro_dice = sum(dices) / 3.0
+ return float(macro_dice)
+
+
+class DiceWeightedAverageEvaluator(EvaluatorInterface):
+ """
+ Dice Coefficient Weighted Average by class frequency.
+
+ Weight each class Dice by the number of ground truth pixels for that class.
+ Better reflect overall accuracy when classes are imbalanced.
+
+ This metric is useful for understanding overall performance in datasets
+ with class imbalance, as it weights classes by their actual prevalence.
+
+ Formula: Dice_weighted = sum(weight_c * Dice_c) / sum(weight_c)
+ where weight_c = TP_c + FN_c (ground truth pixels for class c)
+ Range: [0.0, 1.0] where 1.0 is perfect overlap
+
+ Edge cases:
+ - Return 0.0 if all classes have zero ground truth pixels
+ - Classes with more ground truth pixels contribute more to the average
+
+ """
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate weighted-averaged Dice across all classes.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ Weighted-averaged Dice as float in [0.0, 1.0].
+
+ """
+ weighted_sum = 0.0
+ total_weight = 0
+
+ for class_id in [0, 1, 2]:
+ tp, fp, fn, tn = aggregate_confusion_matrices(mask_pairs, class_id)
+
+ # Weight by ground truth pixel count
+ weight = tp + fn
+
+ if weight > 0:
+ denominator = 2 * tp + fp + fn
+ if denominator > 0:
+ dice = (2 * tp) / denominator
+ weighted_sum += dice * weight
+ total_weight += weight
+
+ if total_weight == 0:
+ return 0.0
+
+ weighted_dice = weighted_sum / total_weight
+ return float(weighted_dice)
+
+
+# --- Helper Functions ---
+
+
+def compute_confusion_matrix_per_class(
+ predicted_mask: MaskArray,
+ real_mask: MaskArray,
+ class_id: int,
+) -> Tuple[int, int, int, int]:
+ """
+ Compute confusion matrix components for a specific class.
+
+ Args:
+ predicted_mask: Predicted segmentation mask (512x512 uint8).
+ real_mask: Ground truth segmentation mask (512x512 uint8).
+ class_id: Class ID to compute metrics for (0, 1, or 2).
+
+ Returns:
+ Tuple of (tp, fp, fn, tn) as integers where:
+ - tp: True Positives (predicted class c AND real class c)
+ - fp: False Positives (predicted class c BUT real is not c)
+ - fn: False Negatives (real class c BUT predicted is not c)
+ - tn: True Negatives (predicted not c AND real not c)
+
+ """
+ mask_c = real_mask == class_id
+ pred_c = predicted_mask == class_id
+
+ tp = int(np.sum(mask_c & pred_c))
+ fp = int(np.sum(pred_c & ~mask_c))
+ fn = int(np.sum(mask_c & ~pred_c))
+ tn = int(np.sum(~mask_c & ~pred_c))
+
+ return tp, fp, fn, tn
+
+
+def aggregate_confusion_matrices(
+ mask_pairs: List[List[MaskPair]],
+ class_id: int,
+) -> Tuple[int, int, int, int]:
+ """
+ Aggregate confusion matrix values across all folds and samples.
+
+ Iterate through all folds and all samples within each fold,
+ summing up the confusion matrix components for the specified class.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+ class_id: Class ID to compute metrics for (0, 1, or 2).
+
+ Returns:
+ Aggregated tuple of (tp, fp, fn, tn) across all folds.
+
+ """
+ total_tp = 0
+ total_fp = 0
+ total_fn = 0
+ total_tn = 0
+
+ for fold_pairs in mask_pairs:
+ for predicted_mask, real_mask in fold_pairs:
+ tp, fp, fn, tn = compute_confusion_matrix_per_class(
+ predicted_mask,
+ real_mask,
+ class_id,
+ )
+ total_tp += tp
+ total_fp += fp
+ total_fn += fn
+ total_tn += tn
+
+ return total_tp, total_fp, total_fn, total_tn
diff --git a/auto_ml/implementations/evaluators/iou.py b/auto_ml/implementations/evaluators/iou.py
new file mode 100644
index 0000000..2bce30c
--- /dev/null
+++ b/auto_ml/implementations/evaluators/iou.py
@@ -0,0 +1,295 @@
+"""IoU evaluators for multi-class segmentation."""
+
+from typing import List, Tuple
+
+import numpy as np
+
+from auto_ml.interfaces import EvaluatorInterface, MaskArray, MaskPair
+
+
+class IoUClass0Evaluator(EvaluatorInterface):
+ """
+ IoU (Intersection over Union) for class 0.
+
+ Measure the overlap between predicted and ground truth regions
+ for class 0. Also known as the Jaccard Index.
+
+ Formula: IoU = TP / (TP + FP + FN)
+ Range: [0.0, 1.0] where 1.0 is perfect overlap
+
+ Edge cases:
+ - Return 0.0 if class 0 never appears in predictions or ground truth
+
+ """
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate IoU for class 0 across all mask pairs.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ IoU score for class 0 as float in [0.0, 1.0].
+
+ """
+ tp, fp, fn, tn = aggregate_confusion_matrices(mask_pairs, class_id=0)
+
+ denominator = tp + fp + fn
+ if denominator == 0:
+ return 0.0
+
+ iou = tp / denominator
+ return float(iou)
+
+
+class IoUClass1Evaluator(EvaluatorInterface):
+ """
+ IoU (Intersection over Union) for class 1.
+
+ Measure the overlap between predicted and ground truth regions
+ for class 1. Also known as the Jaccard Index.
+
+ Formula: IoU = TP / (TP + FP + FN)
+ Range: [0.0, 1.0] where 1.0 is perfect overlap
+
+ Edge cases:
+ - Return 0.0 if class 1 never appears in predictions or ground truth
+
+ """
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate IoU for class 1 across all mask pairs.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ IoU score for class 1 as float in [0.0, 1.0].
+
+ """
+ tp, fp, fn, tn = aggregate_confusion_matrices(mask_pairs, class_id=1)
+
+ denominator = tp + fp + fn
+ if denominator == 0:
+ return 0.0
+
+ iou = tp / denominator
+ return float(iou)
+
+
+class IoUClass2Evaluator(EvaluatorInterface):
+ """
+ IoU (Intersection over Union) for class 2.
+
+ Measure the overlap between predicted and ground truth regions
+ for class 2. Also known as the Jaccard Index.
+
+ Formula: IoU = TP / (TP + FP + FN)
+ Range: [0.0, 1.0] where 1.0 is perfect overlap
+
+ Edge cases:
+ - Return 0.0 if class 2 never appears in predictions or ground truth
+
+ """
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate IoU for class 2 across all mask pairs.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ IoU score for class 2 as float in [0.0, 1.0].
+
+ """
+ tp, fp, fn, tn = aggregate_confusion_matrices(mask_pairs, class_id=2)
+
+ denominator = tp + fp + fn
+ if denominator == 0:
+ return 0.0
+
+ iou = tp / denominator
+ return float(iou)
+
+
+class IoUMacroAverageEvaluator(EvaluatorInterface):
+ """
+ IoU Macro Average across all classes (unweighted mean).
+
+ Compute IoU for each class independently and return the unweighted mean.
+ Treat all classes equally regardless of their frequency in the dataset.
+
+ This metric is useful when all classes are equally important,
+ such as in SEM segmentation where minority phases should not be ignored.
+
+ Formula: IoU_macro = (IoU_0 + IoU_1 + IoU_2) / 3
+ Range: [0.0, 1.0] where 1.0 is perfect overlap for all classes
+
+ Edge cases:
+ - Classes that never appear contribute 0.0 to the average
+ - Always divide by 3 (all classes) for consistency
+
+ """
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate macro-averaged IoU across all classes.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ Macro-averaged IoU as float in [0.0, 1.0].
+
+ """
+ ious = []
+
+ for class_id in [0, 1, 2]:
+ tp, fp, fn, tn = aggregate_confusion_matrices(mask_pairs, class_id)
+ denominator = tp + fp + fn
+
+ if denominator == 0:
+ iou = 0.0
+ else:
+ iou = tp / denominator
+
+ ious.append(iou)
+
+ macro_iou = sum(ious) / 3.0
+ return float(macro_iou)
+
+
+class IoUWeightedAverageEvaluator(EvaluatorInterface):
+ """
+ IoU Weighted Average by class frequency.
+
+ Weight each class IoU by the number of ground truth pixels for that class.
+ Better reflect overall accuracy when classes are imbalanced.
+
+ This metric is useful for understanding overall performance in datasets
+ with class imbalance, as it weights classes by their actual prevalence.
+
+ Formula: IoU_weighted = sum(weight_c * IoU_c) / sum(weight_c)
+ where weight_c = TP_c + FN_c (ground truth pixels for class c)
+ Range: [0.0, 1.0] where 1.0 is perfect overlap
+
+ Edge cases:
+ - Return 0.0 if all classes have zero ground truth pixels
+ - Classes with more ground truth pixels contribute more to the average
+
+ """
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate weighted-averaged IoU across all classes.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ Weighted-averaged IoU as float in [0.0, 1.0].
+
+ """
+ weighted_sum = 0.0
+ total_weight = 0
+
+ for class_id in [0, 1, 2]:
+ tp, fp, fn, tn = aggregate_confusion_matrices(mask_pairs, class_id)
+
+ # Weight by ground truth pixel count
+ weight = tp + fn
+
+ if weight > 0:
+ denominator = tp + fp + fn
+ if denominator > 0:
+ iou = tp / denominator
+ weighted_sum += iou * weight
+ total_weight += weight
+
+ if total_weight == 0:
+ return 0.0
+
+ weighted_iou = weighted_sum / total_weight
+ return float(weighted_iou)
+
+
+# --- Helper Functions ---
+
+
+def compute_confusion_matrix_per_class(
+ predicted_mask: MaskArray,
+ real_mask: MaskArray,
+ class_id: int,
+) -> Tuple[int, int, int, int]:
+ """
+ Compute confusion matrix components for a specific class.
+
+ Args:
+ predicted_mask: Predicted segmentation mask (512x512 uint8).
+ real_mask: Ground truth segmentation mask (512x512 uint8).
+ class_id: Class ID to compute metrics for (0, 1, or 2).
+
+ Returns:
+ Tuple of (tp, fp, fn, tn) as integers where:
+ - tp: True Positives (predicted class c AND real class c)
+ - fp: False Positives (predicted class c BUT real is not c)
+ - fn: False Negatives (real class c BUT predicted is not c)
+ - tn: True Negatives (predicted not c AND real not c)
+
+ """
+ mask_c = real_mask == class_id
+ pred_c = predicted_mask == class_id
+
+ tp = int(np.sum(mask_c & pred_c))
+ fp = int(np.sum(pred_c & ~mask_c))
+ fn = int(np.sum(mask_c & ~pred_c))
+ tn = int(np.sum(~mask_c & ~pred_c))
+
+ return tp, fp, fn, tn
+
+
+def aggregate_confusion_matrices(
+ mask_pairs: List[List[MaskPair]],
+ class_id: int,
+) -> Tuple[int, int, int, int]:
+ """
+ Aggregate confusion matrix values across all folds and samples.
+
+ Iterate through all folds and all samples within each fold,
+ summing up the confusion matrix components for the specified class.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+ class_id: Class ID to compute metrics for (0, 1, or 2).
+
+ Returns:
+ Aggregated tuple of (tp, fp, fn, tn) across all folds.
+
+ """
+ total_tp = 0
+ total_fp = 0
+ total_fn = 0
+ total_tn = 0
+
+ for fold_pairs in mask_pairs:
+ for predicted_mask, real_mask in fold_pairs:
+ tp, fp, fn, tn = compute_confusion_matrix_per_class(
+ predicted_mask,
+ real_mask,
+ class_id,
+ )
+ total_tp += tp
+ total_fp += fp
+ total_fn += fn
+ total_tn += tn
+
+ return total_tp, total_fp, total_fn, total_tn
diff --git a/auto_ml/implementations/evaluators/precision.py b/auto_ml/implementations/evaluators/precision.py
new file mode 100644
index 0000000..30410a8
--- /dev/null
+++ b/auto_ml/implementations/evaluators/precision.py
@@ -0,0 +1,249 @@
+"""Precision evaluators for multi-class segmentation."""
+
+from typing import List, Tuple
+
+import numpy as np
+
+from auto_ml.interfaces import EvaluatorInterface, MaskArray, MaskPair
+
+
+class PrecisionClass0Evaluator(EvaluatorInterface):
+ """
+ Precision for class 0.
+
+ Measure the accuracy of positive predictions for class 0.
+ Precision answers: "Of all pixels predicted as class 0, how many were correct?"
+
+ High precision means few false positives (low false detection rate).
+ Important for SEM analysis to understand false detection of material phases.
+
+ Formula: Precision = TP / (TP + FP)
+ Range: [0.0, 1.0] where 1.0 is perfect precision
+
+ Edge cases:
+ - Return 0.0 if no pixels are predicted as class 0
+
+ """
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate precision for class 0 across all mask pairs.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ Precision for class 0 as float in [0.0, 1.0].
+
+ """
+ tp, fp, fn, tn = aggregate_confusion_matrices(mask_pairs, class_id=0)
+
+ denominator = tp + fp
+ if denominator == 0:
+ return 0.0
+
+ precision = tp / denominator
+ return float(precision)
+
+
+class PrecisionClass1Evaluator(EvaluatorInterface):
+ """
+ Precision for class 1.
+
+ Measure the accuracy of positive predictions for class 1.
+ Precision answers: "Of all pixels predicted as class 1, how many were correct?"
+
+ High precision means few false positives (low false detection rate).
+ Important for SEM analysis to understand false detection of material phases.
+
+ Formula: Precision = TP / (TP + FP)
+ Range: [0.0, 1.0] where 1.0 is perfect precision
+
+ Edge cases:
+ - Return 0.0 if no pixels are predicted as class 1
+
+ """
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate precision for class 1 across all mask pairs.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ Precision for class 1 as float in [0.0, 1.0].
+
+ """
+ tp, fp, fn, tn = aggregate_confusion_matrices(mask_pairs, class_id=1)
+
+ denominator = tp + fp
+ if denominator == 0:
+ return 0.0
+
+ precision = tp / denominator
+ return float(precision)
+
+
+class PrecisionClass2Evaluator(EvaluatorInterface):
+ """
+ Precision for class 2.
+
+ Measure the accuracy of positive predictions for class 2.
+ Precision answers: "Of all pixels predicted as class 2, how many were correct?"
+
+ High precision means few false positives (low false detection rate).
+ Important for SEM analysis to understand false detection of material phases.
+
+ Formula: Precision = TP / (TP + FP)
+ Range: [0.0, 1.0] where 1.0 is perfect precision
+
+ Edge cases:
+ - Return 0.0 if no pixels are predicted as class 2
+
+ """
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate precision for class 2 across all mask pairs.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ Precision for class 2 as float in [0.0, 1.0].
+
+ """
+ tp, fp, fn, tn = aggregate_confusion_matrices(mask_pairs, class_id=2)
+
+ denominator = tp + fp
+ if denominator == 0:
+ return 0.0
+
+ precision = tp / denominator
+ return float(precision)
+
+
+class PrecisionMacroAverageEvaluator(EvaluatorInterface):
+ """
+ Precision Macro Average across all classes (unweighted mean).
+
+ Compute precision for each class independently and return the unweighted mean.
+ Treat all classes equally regardless of their frequency in the dataset.
+
+ This metric is useful when all classes are equally important, such as
+ in SEM segmentation where all material phases matter.
+
+ Formula: Precision_macro = (Precision_0 + Precision_1 + Precision_2) / 3
+ Range: [0.0, 1.0] where 1.0 is perfect precision for all classes
+
+ Edge cases:
+ - Classes with no predictions contribute 0.0 to the average
+ - Always divide by 3 (all classes) for consistency
+
+ """
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate macro-averaged precision across all classes.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ Macro-averaged precision as float in [0.0, 1.0].
+
+ """
+ precisions = []
+
+ for class_id in [0, 1, 2]:
+ tp, fp, fn, tn = aggregate_confusion_matrices(mask_pairs, class_id)
+ denominator = tp + fp
+
+ if denominator == 0:
+ precision = 0.0
+ else:
+ precision = tp / denominator
+
+ precisions.append(precision)
+
+ macro_precision = sum(precisions) / 3.0
+ return float(macro_precision)
+
+
+# --- Helper Functions ---
+
+
+def compute_confusion_matrix_per_class(
+ predicted_mask: MaskArray,
+ real_mask: MaskArray,
+ class_id: int,
+) -> Tuple[int, int, int, int]:
+ """
+ Compute confusion matrix components for a specific class.
+
+ Args:
+ predicted_mask: Predicted segmentation mask (512x512 uint8).
+ real_mask: Ground truth segmentation mask (512x512 uint8).
+ class_id: Class ID to compute metrics for (0, 1, or 2).
+
+ Returns:
+ Tuple of (tp, fp, fn, tn) as integers where:
+ - tp: True Positives (predicted class c AND real class c)
+ - fp: False Positives (predicted class c BUT real is not c)
+ - fn: False Negatives (real class c BUT predicted is not c)
+ - tn: True Negatives (predicted not c AND real not c)
+
+ """
+ mask_c = real_mask == class_id
+ pred_c = predicted_mask == class_id
+
+ tp = int(np.sum(mask_c & pred_c))
+ fp = int(np.sum(pred_c & ~mask_c))
+ fn = int(np.sum(mask_c & ~pred_c))
+ tn = int(np.sum(~mask_c & ~pred_c))
+
+ return tp, fp, fn, tn
+
+
+def aggregate_confusion_matrices(
+ mask_pairs: List[List[MaskPair]],
+ class_id: int,
+) -> Tuple[int, int, int, int]:
+ """
+ Aggregate confusion matrix values across all folds and samples.
+
+ Iterate through all folds and all samples within each fold,
+ summing up the confusion matrix components for the specified class.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+ class_id: Class ID to compute metrics for (0, 1, or 2).
+
+ Returns:
+ Aggregated tuple of (tp, fp, fn, tn) across all folds.
+
+ """
+ total_tp = 0
+ total_fp = 0
+ total_fn = 0
+ total_tn = 0
+
+ for fold_pairs in mask_pairs:
+ for predicted_mask, real_mask in fold_pairs:
+ tp, fp, fn, tn = compute_confusion_matrix_per_class(
+ predicted_mask,
+ real_mask,
+ class_id,
+ )
+ total_tp += tp
+ total_fp += fp
+ total_fn += fn
+ total_tn += tn
+
+ return total_tp, total_fp, total_fn, total_tn
diff --git a/auto_ml/implementations/evaluators/recall.py b/auto_ml/implementations/evaluators/recall.py
new file mode 100644
index 0000000..8cc145f
--- /dev/null
+++ b/auto_ml/implementations/evaluators/recall.py
@@ -0,0 +1,249 @@
+"""Recall evaluators for multi-class segmentation."""
+
+from typing import List, Tuple
+
+import numpy as np
+
+from auto_ml.interfaces import EvaluatorInterface, MaskArray, MaskPair
+
+
+class RecallClass0Evaluator(EvaluatorInterface):
+ """
+ Recall (Sensitivity) for class 0.
+
+ Measure the completeness of detection for class 0.
+ Recall answers: "Of all pixels that should be class 0, how many did we find?"
+
+ High recall means few false negatives (low miss rate).
+ Critical for SEM to ensure all material phases are properly identified.
+
+ Formula: Recall = TP / (TP + FN)
+ Range: [0.0, 1.0] where 1.0 is perfect recall
+
+ Edge cases:
+ - Return 0.0 if no pixels in ground truth are class 0
+
+ """
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate recall for class 0 across all mask pairs.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ Recall for class 0 as float in [0.0, 1.0].
+
+ """
+ tp, fp, fn, tn = aggregate_confusion_matrices(mask_pairs, class_id=0)
+
+ denominator = tp + fn
+ if denominator == 0:
+ return 0.0
+
+ recall = tp / denominator
+ return float(recall)
+
+
+class RecallClass1Evaluator(EvaluatorInterface):
+ """
+ Recall (Sensitivity) for class 1.
+
+ Measure the completeness of detection for class 1.
+ Recall answers: "Of all pixels that should be class 1, how many did we find?"
+
+ High recall means few false negatives (low miss rate).
+ Critical for SEM to ensure all material phases are properly identified.
+
+ Formula: Recall = TP / (TP + FN)
+ Range: [0.0, 1.0] where 1.0 is perfect recall
+
+ Edge cases:
+ - Return 0.0 if no pixels in ground truth are class 1
+
+ """
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate recall for class 1 across all mask pairs.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ Recall for class 1 as float in [0.0, 1.0].
+
+ """
+ tp, fp, fn, tn = aggregate_confusion_matrices(mask_pairs, class_id=1)
+
+ denominator = tp + fn
+ if denominator == 0:
+ return 0.0
+
+ recall = tp / denominator
+ return float(recall)
+
+
+class RecallClass2Evaluator(EvaluatorInterface):
+ """
+ Recall (Sensitivity) for class 2.
+
+ Measure the completeness of detection for class 2.
+ Recall answers: "Of all pixels that should be class 2, how many did we find?"
+
+ High recall means few false negatives (low miss rate).
+ Critical for SEM to ensure all material phases are properly identified.
+
+ Formula: Recall = TP / (TP + FN)
+ Range: [0.0, 1.0] where 1.0 is perfect recall
+
+ Edge cases:
+ - Return 0.0 if no pixels in ground truth are class 2
+
+ """
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate recall for class 2 across all mask pairs.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ Recall for class 2 as float in [0.0, 1.0].
+
+ """
+ tp, fp, fn, tn = aggregate_confusion_matrices(mask_pairs, class_id=2)
+
+ denominator = tp + fn
+ if denominator == 0:
+ return 0.0
+
+ recall = tp / denominator
+ return float(recall)
+
+
+class RecallMacroAverageEvaluator(EvaluatorInterface):
+ """
+ Recall Macro Average across all classes (unweighted mean).
+
+ Compute recall for each class independently and return the unweighted mean.
+ Treat all classes equally regardless of their frequency in the dataset.
+
+ This metric is useful when all classes are equally important, particularly
+ for SEM segmentation where all material phases need proper detection.
+
+ Formula: Recall_macro = (Recall_0 + Recall_1 + Recall_2) / 3
+ Range: [0.0, 1.0] where 1.0 is perfect recall for all classes
+
+ Edge cases:
+ - Classes with no ground truth pixels contribute 0.0 to the average
+ - Always divide by 3 (all classes) for consistency
+
+ """
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate macro-averaged recall across all classes.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ Macro-averaged recall as float in [0.0, 1.0].
+
+ """
+ recalls = []
+
+ for class_id in [0, 1, 2]:
+ tp, fp, fn, tn = aggregate_confusion_matrices(mask_pairs, class_id)
+ denominator = tp + fn
+
+ if denominator == 0:
+ recall = 0.0
+ else:
+ recall = tp / denominator
+
+ recalls.append(recall)
+
+ macro_recall = sum(recalls) / 3.0
+ return float(macro_recall)
+
+
+# --- Helper Functions ---
+
+
+def compute_confusion_matrix_per_class(
+ predicted_mask: MaskArray,
+ real_mask: MaskArray,
+ class_id: int,
+) -> Tuple[int, int, int, int]:
+ """
+ Compute confusion matrix components for a specific class.
+
+ Args:
+ predicted_mask: Predicted segmentation mask (512x512 uint8).
+ real_mask: Ground truth segmentation mask (512x512 uint8).
+ class_id: Class ID to compute metrics for (0, 1, or 2).
+
+ Returns:
+ Tuple of (tp, fp, fn, tn) as integers where:
+ - tp: True Positives (predicted class c AND real class c)
+ - fp: False Positives (predicted class c BUT real is not c)
+ - fn: False Negatives (real class c BUT predicted is not c)
+ - tn: True Negatives (predicted not c AND real not c)
+
+ """
+ mask_c = real_mask == class_id
+ pred_c = predicted_mask == class_id
+
+ tp = int(np.sum(mask_c & pred_c))
+ fp = int(np.sum(pred_c & ~mask_c))
+ fn = int(np.sum(mask_c & ~pred_c))
+ tn = int(np.sum(~mask_c & ~pred_c))
+
+ return tp, fp, fn, tn
+
+
+def aggregate_confusion_matrices(
+ mask_pairs: List[List[MaskPair]],
+ class_id: int,
+) -> Tuple[int, int, int, int]:
+ """
+ Aggregate confusion matrix values across all folds and samples.
+
+ Iterate through all folds and all samples within each fold,
+ summing up the confusion matrix components for the specified class.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+ class_id: Class ID to compute metrics for (0, 1, or 2).
+
+ Returns:
+ Aggregated tuple of (tp, fp, fn, tn) across all folds.
+
+ """
+ total_tp = 0
+ total_fp = 0
+ total_fn = 0
+ total_tn = 0
+
+ for fold_pairs in mask_pairs:
+ for predicted_mask, real_mask in fold_pairs:
+ tp, fp, fn, tn = compute_confusion_matrix_per_class(
+ predicted_mask,
+ real_mask,
+ class_id,
+ )
+ total_tp += tp
+ total_fp += fp
+ total_fn += fn
+ total_tn += tn
+
+ return total_tp, total_fp, total_fn, total_tn
diff --git a/auto_ml/implementations/nodes.py b/auto_ml/implementations/nodes.py
new file mode 100644
index 0000000..6308319
--- /dev/null
+++ b/auto_ml/implementations/nodes.py
@@ -0,0 +1,247 @@
+"""Pipeline node implementations."""
+
+import copy
+from typing import Any, Dict, List, Tuple
+
+import numpy as np
+
+from auto_ml.interfaces import (
+ DataAugmentatorInterface,
+ DataAugmentatorNodeInterface,
+ EvaluatorInterface,
+ EvaluatorNodeInterface,
+ MaskPair,
+ MetricsResultInterface,
+ ModelNodeInterface,
+ SegmentationDatasetInterface,
+ SegmentationModelInterface,
+)
+
+
+class DataAugmentatorNode(DataAugmentatorNodeInterface):
+ """
+ Data Augmentator Node implementation.
+
+ Splits the dataset into K folds (or single split) and applies augmentation
+ to the training set of each fold.
+ """
+
+ def __init__(
+ self,
+ augmentator: DataAugmentatorInterface,
+ name: str = "DataAugmentatorNode",
+ k_folds: int = 5,
+ test_size: float = 0.2,
+ random_seed: int = 42,
+ ) -> None:
+ """
+ Initialize the node.
+
+ Args:
+ augmentator: The augmentator to apply to training sets.
+ name: Name of this node instance.
+ k_folds: Number of folds for Cross Validation.
+ If 1, performs a single random split based on test_size.
+ test_size: Fraction of data for validation if k_folds=1.
+ random_seed: Seed for reproducibility.
+
+ """
+ self.augmentator = augmentator
+ self.name = name
+ self.k_folds = k_folds
+ self.test_size = test_size
+ self.random_seed = random_seed
+
+ def process(
+ self,
+ dataset: SegmentationDatasetInterface,
+ ) -> List[Tuple[SegmentationDatasetInterface, SegmentationDatasetInterface]]:
+ """
+ Process the dataset.
+
+ Returns:
+ List of (augmented_train_dataset, val_dataset) tuples.
+
+ """
+ n_samples = len(dataset)
+ indices = np.arange(n_samples)
+ rng = np.random.default_rng(self.random_seed)
+
+ # Shuffle indices once
+ rng.shuffle(indices)
+
+ results = []
+
+ if self.k_folds <= 1:
+ # Single Split
+ split_idx = int(n_samples * (1 - self.test_size))
+ train_indices = indices[:split_idx]
+ val_indices = indices[split_idx:]
+
+ # Create Datasets
+ train_dataset = SegmentationDatasetInterface.from_pairs(
+ [dataset.samples[i] for i in train_indices],
+ metadata={**dataset.metadata, "split": "train", "fold": 0},
+ )
+ val_dataset = SegmentationDatasetInterface.from_pairs(
+ [dataset.samples[i] for i in val_indices],
+ metadata={**dataset.metadata, "split": "val", "fold": 0},
+ )
+
+ # Augment Train
+ aug_train = self.augmentator.augment(train_dataset)
+
+ results.append((aug_train, val_dataset))
+
+ else:
+ # K-Fold Split
+ fold_sizes = np.full(self.k_folds, n_samples // self.k_folds, dtype=int)
+ fold_sizes[: n_samples % self.k_folds] += 1
+ current = 0
+
+ for i in range(self.k_folds):
+ start, stop = current, current + fold_sizes[i]
+ val_mask = np.zeros(n_samples, dtype=bool)
+ val_mask[start:stop] = True
+
+ val_indices_fold = indices[val_mask]
+ train_indices_fold = indices[~val_mask]
+
+ # Create Datasets
+ train_dataset = SegmentationDatasetInterface.from_pairs(
+ [dataset.samples[j] for j in train_indices_fold],
+ metadata={**dataset.metadata, "split": "train", "fold": i},
+ )
+ val_dataset = SegmentationDatasetInterface.from_pairs(
+ [dataset.samples[j] for j in val_indices_fold],
+ metadata={**dataset.metadata, "split": "val", "fold": i},
+ )
+
+ # Augment Train
+ aug_train = self.augmentator.augment(train_dataset)
+
+ results.append((aug_train, val_dataset))
+
+ current = stop
+
+ return results
+
+
+class ModelNode(ModelNodeInterface):
+ """
+ Generic Model Node implementation.
+
+ Manages the training and evaluation of a model across multiple dataset pairs
+ (e.g., cross-validation folds).
+ """
+
+ def __init__(
+ self,
+ model: SegmentationModelInterface,
+ name: str = "ModelNode",
+ ) -> None:
+ """
+ Initialize the Model Node.
+
+ Args:
+ model: The model to train and evaluate.
+ name: Name of this node instance.
+
+ """
+ self.model = model
+ self.name = name
+
+ def train(
+ self,
+ dataset_pairs: List[
+ Tuple[SegmentationDatasetInterface, SegmentationDatasetInterface]
+ ],
+ ) -> List[List[MaskPair]]:
+ """
+ Train the model on the provided dataset pairs.
+
+ Args:
+ dataset_pairs: List of (train_dataset, val_dataset) tuples.
+
+ Returns:
+ List of mask pair lists, one per dataset pair/fold.
+
+ """
+ all_mask_pairs: List[List[MaskPair]] = []
+ self.last_training_metrics: List[MetricsResultInterface] = []
+
+ for i, (train_dataset, val_dataset) in enumerate(dataset_pairs):
+ print(f"Processing split {i + 1}/{len(dataset_pairs)}...")
+
+ # Train model
+ print(f" Training on {len(train_dataset)} samples...")
+
+ # Create a fresh copy of the model for this fold to ensure
+ # training starts from scratch
+ fold_model = copy.deepcopy(self.model)
+
+ # Pass validation dataset to allow tracking of validation metrics per epoch
+ metrics = fold_model.train(train_dataset, validation_dataset=val_dataset)
+ self.last_training_metrics.append(metrics)
+
+ # Evaluate on validation set
+ print(f" Evaluating on {len(val_dataset)} samples...")
+ mask_pairs = fold_model.evaluate(val_dataset)
+
+ all_mask_pairs.append(mask_pairs)
+ print(f" Split {i + 1}: Collected {len(mask_pairs)} mask pairs")
+
+ return all_mask_pairs
+
+
+class EvaluatorNode(EvaluatorNodeInterface):
+ """
+ Evaluator Node implementation.
+
+ Receives named evaluators and runs each on the mask pairs,
+ returning a dictionary of results.
+ """
+
+ def __init__(
+ self,
+ evaluators: Dict[str, EvaluatorInterface],
+ name: str = "EvaluatorNode",
+ ) -> None:
+ """
+ Initialize the Evaluator Node.
+
+ Args:
+ evaluators: Dictionary mapping evaluator names to
+ EvaluatorInterface instances.
+ name: Name of this node instance.
+
+ """
+ self.evaluators = evaluators
+ self.name = name
+
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> Dict[str, Any]:
+ """
+ Run all evaluators on the mask pairs.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode.
+
+ Returns:
+ Dictionary mapping evaluator names to their results.
+
+ """
+ print(f"\n{self.name}: Running evaluators...")
+
+ results: Dict[str, Any] = {}
+
+ for eval_name, evaluator in self.evaluators.items():
+ print(f" Running evaluator: {eval_name}")
+ results[eval_name] = []
+ for mask_pair_list in mask_pairs:
+ evaluation_list = [mask_pair_list]
+ result = evaluator.evaluate(evaluation_list)
+ results[eval_name].append(result)
+ print(f" Result: {result}")
+
+ print(f"{self.name}: Evaluation complete.")
+ return results
diff --git a/auto_ml/implementations/segmentators/__init__.py b/auto_ml/implementations/segmentators/__init__.py
new file mode 100644
index 0000000..37e4a6d
--- /dev/null
+++ b/auto_ml/implementations/segmentators/__init__.py
@@ -0,0 +1,19 @@
+"""Model implementations subpackage."""
+
+from auto_ml.implementations.segmentators.base import InMemoryPyTorchDataset
+from auto_ml.implementations.segmentators.quadtree import (
+ QuadtreeSegmentationModel,
+)
+from auto_ml.implementations.segmentators.sliding_window import (
+ SlidingWindowSegmentationModel,
+)
+from auto_ml.implementations.segmentators.swin import SwinModel
+from auto_ml.implementations.segmentators.vit import ViTModel
+
+__all__ = [
+ "InMemoryPyTorchDataset",
+ "ViTModel",
+ "SwinModel",
+ "QuadtreeSegmentationModel",
+ "SlidingWindowSegmentationModel",
+]
diff --git a/auto_ml/implementations/segmentators/base.py b/auto_ml/implementations/segmentators/base.py
new file mode 100644
index 0000000..ac6c0b3
--- /dev/null
+++ b/auto_ml/implementations/segmentators/base.py
@@ -0,0 +1,42 @@
+"""Base utilities for model implementations."""
+
+from typing import Tuple
+
+import torch
+from torch.utils.data import Dataset
+
+from auto_ml.interfaces import SegmentationDatasetInterface
+
+
+class InMemoryPyTorchDataset(Dataset):
+ """Bridge between AutoML DatasetInterface and PyTorch Dataset."""
+
+ def __init__(self, dataset: SegmentationDatasetInterface) -> None:
+ """Initialize the Dataset."""
+ self.dataset = dataset
+
+ def __len__(self) -> int:
+ """Get Amount of samples in the dataset."""
+ return len(self.dataset)
+
+ def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor]: # noqa: D105
+ # reuse logic from vit/dataset.py but adapted for in-memory data
+ input_interface, output_interface = self.dataset[idx]
+
+ # Prepare inputs
+ # Assuming images are numpy arrays (H, W, 3) or (H, W)
+ img = input_interface.image
+ if input_interface.is_grayscale:
+ # Convert to tensor (1, H, W)
+ img_tensor = torch.from_numpy(img).float() / 255.0
+ img_tensor = img_tensor.unsqueeze(0)
+ else:
+ # Convert RGB (H, W, 3) -> (3, H, W)
+ img_tensor = torch.from_numpy(img).float() / 255.0
+ img_tensor = img_tensor.permute(2, 0, 1)
+
+ # Prepare targets
+ mask = output_interface.mask
+ mask_tensor = torch.from_numpy(mask).long()
+
+ return img_tensor, mask_tensor
diff --git a/auto_ml/implementations/segmentators/quadtree.py b/auto_ml/implementations/segmentators/quadtree.py
new file mode 100644
index 0000000..eac3b9c
--- /dev/null
+++ b/auto_ml/implementations/segmentators/quadtree.py
@@ -0,0 +1,622 @@
+"""Quadtree-based segmentation model implementation."""
+
+import math
+from pathlib import Path
+from typing import Any, Dict, List, Optional, Tuple, TypedDict
+
+import numpy as np
+
+from auto_ml.implementations.datasets import load_classification_dataset_from_dir
+from auto_ml.interfaces import (
+ ClassificationModelInterface,
+ ImageArray,
+ MaskArray,
+ MaskPair,
+ MetricsResultInterface,
+ SegmentationDatasetInterface,
+ SegmentationModelInterface,
+)
+
+
+class _BestParams(TypedDict):
+ threshold: float
+ min_region_size: int
+ max_depth: Optional[int]
+
+
+class QuadtreeSegmentationModel(SegmentationModelInterface):
+ """
+ Quadtree-based image segmentation model.
+
+ The model recursively classifies image regions using an injected
+ ClassificationModelInterface. Regions with confidence below a
+ threshold are subdivided into four quadrants.
+ """
+
+ def __init__(
+ self,
+ classifier: ClassificationModelInterface,
+ classifier_dataset_dir: Optional[Path] = None,
+ threshold: float = 0.5,
+ min_region_size: int = 1,
+ max_depth: Optional[int] = None,
+ optimize_metric: Optional[str] = None,
+ search_space: Optional[Dict[str, Tuple[Any, Any]]] = None,
+ n_trials: int = 20,
+ ) -> None:
+ """
+ Initialize the quadtree segmentation model.
+
+ Args:
+ classifier: Region classifier implementing
+ ClassificationModelInterface.
+ classifier_dataset_dir: Directory containing dataset for training
+ the classifier. If None, assumes classifier
+ is already trained, will throw error if train()
+ is called.
+ threshold: Minimum confidence required to accept a region.
+ min_region_size: Minimum width or height to allow subdivision.
+ max_depth: Optional maximum recursion depth.
+ optimize_metric: Whether to optimize hyperparameters by maximizing a metric.
+ search_space: Optional dictionary defining the search ranges for
+ hyperparameters.
+ - 'threshold': (min, max) float
+ - 'min_region_size': (min, max) int
+ - 'max_depth': (min, max) int
+ If None, default ranges are used.
+ n_trials: Number of random trials to perform during hyperparameter
+ tuning. Defaults to 20.
+
+ """
+ self.classifier = classifier
+ self.threshold = threshold
+ self.min_region_size = min_region_size
+ self.max_depth = max_depth
+ self.classifier_dataset_dir = classifier_dataset_dir
+ self.optimize_metric = optimize_metric
+ self.search_space = search_space
+ self.n_trials = n_trials
+
+ def train(
+ self,
+ dataset: SegmentationDatasetInterface,
+ validation_dataset: SegmentationDatasetInterface | None = None,
+ ) -> MetricsResultInterface:
+ """
+ Train the quadtree segmenter. Optionally performs hyperparameter tuning.
+
+ Args:
+ dataset: Segmentation dataset for training and tuning.
+ validation_dataset: Optional validation dataset.
+
+ Returns:
+ MetricsResultInterface containing segmentation quality metrics.
+
+ """
+ print("[QuadtreeSegmentationModel] Starting training...")
+ print(f"[QuadtreeSegmentationModel] Dataset size: {len(dataset)}")
+
+ if self.classifier_dataset_dir is not None:
+ dir_path = self.classifier_dataset_dir
+ print(
+ "[QuadtreeSegmentationModel] Training classifier "
+ f"from: {dir_path}",
+ )
+ classifier_dataset = load_classification_dataset_from_dir(
+ self.classifier_dataset_dir,
+ )
+ ds_size = len(classifier_dataset)
+ print(
+ "[QuadtreeSegmentationModel] Classifier "
+ f"dataset size: {ds_size}",
+ )
+ self.classifier.train(classifier_dataset)
+ print("[QuadtreeSegmentationModel] Classifier training completed")
+ else:
+ print("[QuadtreeSegmentationModel] Using pre-trained classifier")
+ # else, assume classifier is already trained,
+ # this is needed for tests that check this method not to break
+
+ if not self.optimize_metric:
+ print(
+ "[QuadtreeSegmentationModel] No hyperparameter "
+ "optimization requested",
+ )
+ predicted_real_pairs = self.evaluate(dataset)
+ metrics = self._compute_metrics(predicted_real_pairs)
+ print(
+ "[QuadtreeSegmentationModel] Final metrics - "
+ f"Accuracy: {metrics.accuracy:.4f}, "
+ f"Loss: {metrics.loss:.4f}",
+ )
+ return metrics
+
+ # hyperparameter tunning
+ print("[QuadtreeSegmentationModel] Starting hyperparameter tuning...")
+ print(f"[QuadtreeSegmentationModel] Optimize metric: {self.optimize_metric}")
+ print(f"[QuadtreeSegmentationModel] Number of trials: {self.n_trials}")
+ if self.search_space:
+ threshold_range = self.search_space.get("threshold", (0.5, 0.9))
+ min_region_range = self.search_space.get("min_region_size", (8, 32))
+ max_depth_range = self.search_space.get("max_depth", (4, 8))
+ print("[QuadtreeSegmentationModel] Using custom search space")
+ else:
+ # simple default ranges
+ threshold_range = (0.5, 0.9)
+ min_region_range = (8, 32)
+ max_depth_range = (4, 8)
+ print("[QuadtreeSegmentationModel] Using default search space")
+
+ print(f"[QuadtreeSegmentationModel] Threshold range: {threshold_range}")
+ print(f"[QuadtreeSegmentationModel] Min region size range: {min_region_range}")
+ print(f"[QuadtreeSegmentationModel] Max depth range: {max_depth_range}")
+
+ # Split dataset in train/val just for tunning
+ n = len(dataset)
+ val_ratio = 0.2
+ val_size = int(n * val_ratio)
+ indices = np.arange(n)
+ np.random.shuffle(indices)
+ val_indices = indices[:val_size]
+ val_dataset = SegmentationDatasetInterface(
+ [dataset.samples[i] for i in val_indices],
+ )
+ print(
+ "[QuadtreeSegmentationModel] Split dataset: "
+ f"validation size = {val_size} samples",
+ )
+
+ # Simulated Annealing Configuration
+ ranges = {
+ "threshold": threshold_range,
+ "min_region_size": min_region_range,
+ "max_depth": max_depth_range,
+ }
+
+ # Initial Solution (Random Start)
+ current_threshold = np.random.uniform(threshold_range[0], threshold_range[1])
+ current_min_size = np.random.randint(
+ min_region_range[0],
+ min_region_range[1] + 1,
+ )
+ current_max_depth = np.random.randint(
+ max_depth_range[0],
+ max_depth_range[1] + 1,
+ )
+
+ current_params: _BestParams = {
+ "threshold": current_threshold,
+ "min_region_size": current_min_size,
+ "max_depth": current_max_depth,
+ }
+
+ print("[QuadtreeSegmentationModel] Initial random solution:")
+ print(f" - threshold: {current_threshold:.4f}")
+ print(f" - min_region_size: {current_min_size}")
+ print(f" - max_depth: {current_max_depth}")
+
+ # We need an initial metric for SA to compare against
+ self.threshold = current_threshold
+ self.min_region_size = current_min_size
+ self.max_depth = current_max_depth
+
+ print("[QuadtreeSegmentationModel] Evaluating initial solution...")
+ initial_pairs = self.evaluate(val_dataset)
+ initial_metrics = self._compute_metrics(initial_pairs)
+ current_metric_val = -1.0
+
+ if self.optimize_metric in initial_metrics.to_dict():
+ val = initial_metrics.to_dict()[self.optimize_metric]
+ if isinstance(val, (int, float)):
+ current_metric_val = val
+ print(
+ "[QuadtreeSegmentationModel] Initial "
+ f"{self.optimize_metric}: {current_metric_val:.4f}",
+ )
+
+ best_params = current_params.copy()
+ best_metric = current_metric_val
+
+ # SA Hyperparameters
+ temp = 1.0
+ alpha = 0.90 # Cooling rate
+
+ print(
+ "[QuadtreeSegmentationModel] Starting Simulated "
+ f"Annealing loop (alpha={alpha})",
+ )
+ print("-" * 80)
+
+ # SA Loop
+ for i in range(self.n_trials):
+ print(
+ "[QuadtreeSegmentationModel] Trial "
+ f"{i+1}/{self.n_trials} (temp={temp:.4f})",
+ )
+ # Generate Neighbor
+ neighbor_params = self._get_neighbor(current_params, ranges)
+ print(
+ " Generated neighbor: "
+ f"threshold={neighbor_params['threshold']:.4f}, "
+ f"min_region_size={neighbor_params['min_region_size']}, "
+ f"max_depth={neighbor_params['max_depth']}",
+ )
+
+ # Evaluate Neighbor
+ self.threshold = neighbor_params["threshold"]
+ self.min_region_size = neighbor_params["min_region_size"]
+ self.max_depth = neighbor_params["max_depth"]
+
+ predicted_real_pairs = self.evaluate(val_dataset)
+ metrics = self._compute_metrics(predicted_real_pairs)
+ metrics_dict = metrics.to_dict()
+
+ if self.optimize_metric not in metrics_dict:
+ print(
+ "[QuadtreeSegmentationModel] Metric "
+ f"'{self.optimize_metric}' not found. Stopping.",
+ )
+ break
+
+ neighbor_metric_val = metrics_dict[self.optimize_metric]
+ if not isinstance(neighbor_metric_val, (int, float)):
+ print("[QuadtreeSegmentationModel] Invalid metric type. Stopping.")
+ break
+
+ print(f" Neighbor {self.optimize_metric}: {neighbor_metric_val:.4f}")
+
+ # Acceptance Probability (Maximization)
+ # if neighbor is better, prob > 1, algorithm accepts
+ # if worse, prob < 1, algorithm accepts with probability
+ delta = neighbor_metric_val - current_metric_val
+ print(f" Delta: {delta:.4f}")
+
+ if delta > 0:
+ acceptance_prob = 2.0 # Always accept
+ print(" ✓ Better solution found! Accepting.")
+ else:
+ # Avoid overflow/underflow if temp is too low or delta too neg
+ try:
+ acceptance_prob = math.exp(delta / temp)
+ except OverflowError:
+ acceptance_prob = 0.0
+ print(f" Acceptance probability: {acceptance_prob:.4f}")
+
+ if delta > 0 or np.random.rand() < acceptance_prob:
+ current_params = neighbor_params
+ current_metric_val = neighbor_metric_val
+ print(
+ f" ✓ Solution accepted. Current "
+ f"{self.optimize_metric}: {current_metric_val:.4f}",
+ )
+ else:
+ print(
+ f" ✗ Solution rejected. Current "
+ f"{self.optimize_metric}: {current_metric_val:.4f}",
+ )
+
+ # Keep track of global best
+ if current_metric_val > best_metric:
+ best_metric = current_metric_val
+ best_params = current_params.copy()
+ print(f" ⭐ NEW BEST FOUND! {self.optimize_metric}: {best_metric:.4f}")
+ print(f" Best params: threshold={best_params['threshold']:.4f}, "
+ f"min_region_size={best_params['min_region_size']}, "
+ f"max_depth={best_params['max_depth']}")
+
+ # Cool down
+ temp *= alpha
+ print("-" * 80)
+
+ # update with best configuration
+ print("\n[QuadtreeSegmentationModel] Hyperparameter tuning completed!")
+ print(
+ "[QuadtreeSegmentationModel] Best "
+ f"{self.optimize_metric}: {best_metric:.4f}",
+ )
+ print("[QuadtreeSegmentationModel] Best parameters:")
+ print(f" - threshold: {best_params['threshold']:.4f}")
+ print(f" - min_region_size: {best_params['min_region_size']}")
+ print(f" - max_depth: {best_params['max_depth']}")
+
+ self.threshold = best_params["threshold"]
+ self.min_region_size = best_params["min_region_size"]
+ self.max_depth = best_params["max_depth"]
+
+ # evaluate over the whole dataset
+ print(
+ "[QuadtreeSegmentationModel] Evaluating best "
+ "configuration on full dataset...",
+ )
+ final_pairs = self.evaluate(dataset)
+ final_metric = self._compute_metrics(final_pairs)
+
+ print("[QuadtreeSegmentationModel] Final metrics on full dataset:")
+ print(f" - Accuracy: {final_metric.accuracy:.4f}")
+ print(f" - Loss: {final_metric.loss:.4f}")
+ print(f" - IoU: {final_metric.iou:.4f}")
+ print(f" - Precision: {final_metric.precision:.4f}")
+ print(f" - Recall: {final_metric.recall:.4f}")
+ print(f" - F1 Score: {final_metric.f1_score:.4f}")
+
+ return final_metric
+
+ def evaluate(self, dataset: SegmentationDatasetInterface) -> List[MaskPair]:
+ """
+ Evaluate the model on a dataset.
+
+ For each image, a segmentation mask is produced using recursive
+ quadtree decomposition.
+
+ Returns:
+ List of (predicted_mask, real_mask) tuples.
+
+ """
+ print(
+ "[QuadtreeSegmentationModel.evaluate] Evaluating "
+ f"{len(dataset)} images...",
+ )
+ results = []
+ for idx, (image, real_mask) in enumerate(dataset):
+ if (idx + 1) % max(1, len(dataset) // 10) == 0 or idx == 0:
+ print(f" Processing image {idx + 1}/{len(dataset)}")
+ results.append((self._segment_image(image), real_mask))
+ print("[QuadtreeSegmentationModel.evaluate] Evaluation completed")
+ return results
+
+ def _segment_image(
+ self,
+ image: ImageArray,
+ ) -> MaskArray:
+ """Segment a single image using recursive quadtree splitting."""
+ print(
+ "[QuadtreeSegmentationModel._segment_image] Starting "
+ f"segmentation (threshold={self.threshold:.4f}, "
+ f"min_region_size={self.min_region_size}, "
+ f"max_depth={self.max_depth})",
+ )
+ mask = np.zeros((512, 512), dtype=np.uint8)
+
+ self._segment_region(
+ image=image,
+ mask=mask,
+ x=0,
+ y=0,
+ width=512,
+ height=512,
+ depth=0,
+ )
+
+ print("[QuadtreeSegmentationModel._segment_image] Segmentation completed")
+ return mask
+
+ def _segment_region(
+ self,
+ image: ImageArray,
+ mask: MaskArray,
+ x: int,
+ y: int,
+ width: int,
+ height: int,
+ depth: int,
+ ) -> None:
+ """
+ Recursively segment a rectangular region of the image.
+
+ If the classifier confidence is sufficient, the region is
+ filled in the mask. Otherwise, the region is subdivided
+ into four quadrants and processed recursively.
+ """
+ label, confidence = self.classifier.classify(
+ image=image,
+ x=x,
+ y=y,
+ width=width,
+ height=height,
+ )
+
+ indent = " " * depth
+ print(
+ f"{indent}[Depth {depth}] Region ({x}, {y}, {width}x{height}): "
+ f"label={label}, confidence={confidence:.4f}",
+ )
+
+ if self._should_stop_recursion(confidence, width, height, depth):
+ reason = ""
+ if confidence >= self.threshold:
+ reason = "confidence threshold met"
+ elif width <= self.min_region_size or height <= self.min_region_size:
+ reason = "min region size reached"
+ elif self.max_depth is not None and depth >= self.max_depth:
+ reason = "max depth reached"
+ print(
+ f"{indent} → Stopping recursion ({reason}). "
+ f"Filling with label {label}",
+ )
+ mask[y : y + height, x : x + width] = label
+ return
+
+ # Subdivide region (integer division allowed)
+ w_half = width // 2
+ h_half = height // 2
+
+ # Ensure progress (should not happen if min_region_size >= 1)
+ if w_half == 0 or h_half == 0:
+ mask[y : y + height, x : x + width] = label
+ return
+
+ regions: List[Tuple[int, int, int, int]] = [
+ (x, y, w_half, h_half), # top left
+ (x + w_half, y, w_half, h_half), # top right
+ (x, y + h_half, w_half, h_half), # bottom left
+ (x + w_half, y + h_half, w_half, h_half), # bottom right
+ ]
+
+ for xr, yr, wr, hr in regions:
+ self._segment_region(
+ image=image,
+ mask=mask,
+ x=xr,
+ y=yr,
+ width=wr,
+ height=hr,
+ depth=depth + 1,
+ )
+
+ # ------------------------------------------------------------------
+ # Utility methods
+ # ------------------------------------------------------------------
+
+ def _get_neighbor(
+ self,
+ params: _BestParams,
+ ranges: Dict[str, Tuple[Any, Any]],
+ ) -> _BestParams:
+ """Generate a neighbor configuration by perturbing current parameters."""
+ new_params = params.copy()
+
+ # Perturb one parameter at a time or all? Let's perturb all slightly.
+
+ # Threshold: Perturb by normal noise stride 0.05
+ t_range = ranges["threshold"]
+ t_current = new_params["threshold"]
+ t_new = t_current + np.random.normal(0, 0.05)
+ new_params["threshold"] = np.clip(t_new, t_range[0], t_range[1])
+
+ # Min Region: Perturb by +/- 1 or Stay
+ mr_range = ranges["min_region_size"]
+ mr_current = new_params["min_region_size"]
+ mr_step = np.random.randint(-2, 3) # -2, -1, 0, 1, 2
+ mr_new = mr_current + mr_step
+ new_params["min_region_size"] = int(
+ np.clip(mr_new, mr_range[0], mr_range[1]),
+ )
+
+ # Max Depth: Perturb by +/- 1
+ md_range = ranges["max_depth"]
+ if new_params["max_depth"] is not None:
+ md_current = new_params["max_depth"]
+ md_step = np.random.randint(-1, 2)
+ md_new = md_current + md_step
+ new_params["max_depth"] = int(np.clip(md_new, md_range[0], md_range[1]))
+
+ return new_params
+
+ def _should_stop_recursion(
+ self,
+ confidence: float,
+ width: int,
+ height: int,
+ depth: int,
+ ) -> bool:
+ """
+ Determine whether recursion should stop.
+
+ Determine whether recursion should stop based on region size and maximum
+ depth constraints.
+ """
+ return (
+ confidence >= self.threshold
+ or width <= self.min_region_size
+ or height <= self.min_region_size
+ or (self.max_depth is not None and depth >= self.max_depth)
+ )
+
+ def _compute_metrics(
+ self,
+ predicted_real_pairs: List[MaskPair],
+ ) -> MetricsResultInterface:
+ """Compute segmentation metrics and return a MetricsResultInterface."""
+ num_classes = 3 # brittle, ductile, mixed
+
+ total_pixels = 0
+ correct_pixels = 0
+
+ class_correct = np.zeros(num_classes, dtype=int)
+ class_total = np.zeros(num_classes, dtype=int)
+ intersection = np.zeros(num_classes, dtype=int)
+ union = np.zeros(num_classes, dtype=int)
+ pred_counts = np.zeros(num_classes, dtype=int)
+
+ for pred, real in predicted_real_pairs:
+ pred_flat = pred.flatten()
+ real_flat = real.flatten()
+
+ total_pixels += pred_flat.size
+ correct_pixels += np.sum(pred_flat == real_flat)
+
+ for cls in range(num_classes):
+ pred_cls = pred_flat == cls
+ real_cls = real_flat == cls
+
+ class_correct[cls] += np.sum(pred_cls & real_cls)
+ class_total[cls] += np.sum(real_cls)
+ pred_counts[cls] += np.sum(pred_cls)
+
+ intersection[cls] += np.sum(pred_cls & real_cls)
+ union[cls] += np.sum(pred_cls | real_cls)
+
+ # Pixel-level accuracy
+ pixel_accuracy = (
+ float(correct_pixels) / float(total_pixels) if total_pixels > 0 else 0.0
+ )
+
+ # Per-class accuracy (avoid division by zero)
+ per_class_accuracy = [
+ float(class_correct[c]) / float(class_total[c])
+ if class_total[c] > 0
+ else 0.0
+ for c in range(num_classes)
+ ]
+
+ # Mean IoU
+ mean_iou = float(
+ np.mean(
+ [
+ float(intersection[c]) / float(union[c]) if union[c] > 0 else 0.0
+ for c in range(num_classes)
+ ],
+ ),
+ )
+
+ # Precision, recall, F1-score per class
+ precision_per_class = [
+ float(intersection[c]) / float(pred_counts[c])
+ if pred_counts[c] > 0
+ else 0.0
+ for c in range(num_classes)
+ ]
+ recall_per_class = [
+ float(intersection[c]) / float(class_total[c])
+ if class_total[c] > 0
+ else 0.0
+ for c in range(num_classes)
+ ]
+ f1_per_class = [
+ (2 * precision_per_class[c] * recall_per_class[c])
+ / (precision_per_class[c] + recall_per_class[c])
+ if (precision_per_class[c] + recall_per_class[c]) > 0
+ else 0.0
+ for c in range(num_classes)
+ ]
+ mean_f1 = float(np.mean(f1_per_class))
+ mean_precision = float(np.mean(precision_per_class))
+ mean_recall = float(np.mean(recall_per_class))
+
+ # Loss fallback
+ loss = 1.0 - pixel_accuracy
+
+ return MetricsResultInterface(
+ accuracy=pixel_accuracy,
+ loss=loss,
+ iou=mean_iou,
+ precision=mean_precision,
+ recall=mean_recall,
+ f1_score=mean_f1,
+ additional_metrics={
+ "per_class_accuracy": per_class_accuracy,
+ "precision_per_class": precision_per_class,
+ "recall_per_class": recall_per_class,
+ "f1_per_class": f1_per_class,
+ },
+ )
diff --git a/auto_ml/implementations/segmentators/sliding_window.py b/auto_ml/implementations/segmentators/sliding_window.py
new file mode 100644
index 0000000..1879648
--- /dev/null
+++ b/auto_ml/implementations/segmentators/sliding_window.py
@@ -0,0 +1,344 @@
+"""Sliding window-based segmentation model implementation."""
+
+from pathlib import Path
+from typing import List, Optional, cast
+
+import numpy as np
+
+from auto_ml.implementations.datasets import load_classification_dataset_from_dir
+from auto_ml.interfaces import (
+ ClassificationModelInterface,
+ ImageArray,
+ MaskArray,
+ MaskPair,
+ MetricsResultInterface,
+ SegmentationDatasetInterface,
+ SegmentationModelInterface,
+)
+
+
+class SlidingWindowSegmentationModel(SegmentationModelInterface):
+ """
+ Sliding window-based image segmentation model.
+
+ The model applies a fixed-size sliding window across the entire 512x512 image,
+ classifies each window position using an injected ClassificationModelInterface,
+ and aggregates overlapping classifications via majority voting to produce a
+ final per-pixel segmentation mask.
+ """
+
+ def __init__(
+ self,
+ classifier: ClassificationModelInterface,
+ classifier_dataset_dir: Optional[Path] = None,
+ window_size: int = 64,
+ stride: int = 32,
+ aggregation_method: str = "confidence_weighted",
+ ) -> None:
+ """
+ Initialize the sliding window segmentation model.
+
+ Args:
+ classifier: Region classifier implementing
+ ClassificationModelInterface.
+ classifier_dataset_dir: Directory containing dataset for training
+ the classifier. If None, assumes classifier
+ is already trained.
+ window_size: Size of the sliding window (e.g., 32, 64, 128, 256).
+ stride: Step size for moving the window across the image.
+ aggregation_method: Method for aggregating votes from overlapping
+ windows. Supported methods: "majority_vote" and
+ "confidence_weighted".
+
+ """
+ self.classifier = classifier
+ self.window_size = window_size
+ self.stride = stride
+ self.aggregation_method = aggregation_method
+ self.classifier_dataset_dir = classifier_dataset_dir
+
+ self._validate_parameters()
+
+ def train(
+ self,
+ dataset: SegmentationDatasetInterface,
+ validation_dataset: SegmentationDatasetInterface | None = None,
+ ) -> MetricsResultInterface:
+ """
+ Train the sliding window segmenter.
+
+ Args:
+ dataset: Segmentation dataset for training.
+ validation_dataset: Optional validation dataset (currently unused).
+
+ Returns:
+ MetricsResultInterface containing segmentation quality metrics.
+
+ """
+ print("[SlidingWindowSegmentationModel] Starting training...")
+ print(f"[SlidingWindowSegmentationModel] Dataset size: {len(dataset)}")
+ print(f"[SlidingWindowSegmentationModel] Window size: {self.window_size}")
+ print(f"[SlidingWindowSegmentationModel] Stride: {self.stride}")
+ print(
+ "[SlidingWindowSegmentationModel] "
+ f"Aggregation method: {self.aggregation_method}",
+ )
+
+ if self.classifier_dataset_dir is not None:
+ dir_path = self.classifier_dataset_dir
+ print(
+ "[SlidingWindowSegmentationModel] Training classifier "
+ f"from: {dir_path}",
+ )
+ classifier_dataset = load_classification_dataset_from_dir(
+ self.classifier_dataset_dir,
+ )
+ ds_size = len(classifier_dataset)
+ print(
+ "[SlidingWindowSegmentationModel] Classifier "
+ f"dataset size: {ds_size}",
+ )
+ self.classifier.train(classifier_dataset)
+ print("[SlidingWindowSegmentationModel] Classifier training completed")
+ else:
+ print("[SlidingWindowSegmentationModel] Using pre-trained classifier")
+
+ # Evaluate on training dataset to compute baseline metrics
+ print(
+ "[SlidingWindowSegmentationModel] "
+ "Evaluating on training dataset...",
+ )
+ predicted_real_pairs = self.evaluate(dataset)
+ metrics = self._compute_metrics(predicted_real_pairs)
+
+ print("[SlidingWindowSegmentationModel] Training metrics:")
+ print(f" - Accuracy: {metrics.accuracy:.4f}")
+ print(f" - Loss: {metrics.loss:.4f}")
+ print(f" - IoU: {metrics.iou:.4f}")
+ print(f" - Precision: {metrics.precision:.4f}")
+ print(f" - Recall: {metrics.recall:.4f}")
+ print(f" - F1 Score: {metrics.f1_score:.4f}")
+
+ return metrics
+
+ def evaluate(self, dataset: SegmentationDatasetInterface) -> List[MaskPair]:
+ """
+ Evaluate the model on a dataset.
+
+ For each image, a segmentation mask is produced using sliding window
+ classification and vote aggregation.
+
+ Returns:
+ List of (predicted_mask, real_mask) tuples.
+
+ """
+ print(
+ "[SlidingWindowSegmentationModel.evaluate] Evaluating "
+ f"{len(dataset)} images...",
+ )
+ results = []
+ for idx, (image, real_mask) in enumerate(dataset):
+ if (idx + 1) % max(1, len(dataset) // 10) == 0 or idx == 0:
+ print(f" Processing image {idx + 1}/{len(dataset)}")
+ results.append((self._segment_image(image), real_mask))
+ print("[SlidingWindowSegmentationModel.evaluate] Evaluation completed")
+ return results
+
+ def _segment_image(self, image: ImageArray) -> MaskArray:
+ """
+ Segment a single image using sliding window classification.
+
+ Args:
+ image: Input image (512x512).
+
+ Returns:
+ Predicted segmentation mask (512x512).
+
+ """
+ print(
+ "[SlidingWindowSegmentationModel._segment_image] "
+ f"Starting segmentation (window_size={self.window_size}, "
+ f"stride={self.stride})",
+ )
+
+ # Initialize vote accumulator: (512, 512, 3) for 3 classes
+ num_classes = 3
+ vote_map = np.zeros((512, 512, num_classes), dtype=np.float32)
+
+ # Slide window across image
+ window_count = 0
+ for y in range(0, 512, self.stride):
+ for x in range(0, 512, self.stride):
+ # Handle edge boundaries (windows may be smaller at edges)
+ actual_width = min(self.window_size, 512 - x)
+ actual_height = min(self.window_size, 512 - y)
+
+ # Classify this window region
+ label, confidence = self.classifier.classify(
+ image=image,
+ x=x,
+ y=y,
+ width=actual_width,
+ height=actual_height,
+ )
+
+ # Add vote for all pixels in this window
+ if self.aggregation_method == "majority_vote":
+ vote_map[y : y + actual_height, x : x + actual_width, label] += 1
+ elif self.aggregation_method == "confidence_weighted":
+ vote_map[
+ y : y + actual_height, x : x + actual_width, label,
+ ] += confidence
+
+ window_count += 1
+
+ print(
+ f"[SlidingWindowSegmentationModel._segment_image] "
+ f"Processed {window_count} windows",
+ )
+
+ # Final mask: argmax across class dimension
+ mask = cast(MaskArray, np.argmax(vote_map, axis=2).astype(np.uint8))
+
+ print("[SlidingWindowSegmentationModel._segment_image] Segmentation completed")
+ return mask
+
+ def _validate_parameters(self) -> None:
+ """
+ Validate constructor parameters.
+
+ Ensure stride <= window_size to avoid gaps in coverage.
+ Ensure window_size is reasonable (8 <= window_size <= 512).
+
+ Raises:
+ ValueError: If parameters are invalid.
+
+ """
+ if self.stride > self.window_size:
+ raise ValueError(
+ f"stride ({self.stride}) must be <= window_size "
+ f"({self.window_size}) to ensure full image coverage",
+ )
+
+ if not (8 <= self.window_size <= 512):
+ raise ValueError(
+ f"window_size ({self.window_size}) must be in range [8, 512]",
+ )
+
+ if self.stride < 1:
+ raise ValueError(f"stride ({self.stride}) must be >= 1")
+
+ if self.aggregation_method not in ["majority_vote", "confidence_weighted"]:
+ raise ValueError(
+ f"aggregation_method '{self.aggregation_method}' is not supported. "
+ "Supported methods: 'majority_vote', 'confidence_weighted'",
+ )
+
+ def _compute_metrics(
+ self,
+ predicted_real_pairs: List[MaskPair],
+ ) -> MetricsResultInterface:
+ """
+ Compute segmentation metrics and return a MetricsResultInterface.
+
+ Args:
+ predicted_real_pairs: List of (predicted_mask, real_mask) tuples.
+
+ Returns:
+ MetricsResultInterface containing accuracy, loss, IoU, precision,
+ recall, F1 score, and per-class metrics.
+
+ """
+ num_classes = 3 # brittle, ductile, mixed
+
+ total_pixels = 0
+ correct_pixels = 0
+
+ class_correct = np.zeros(num_classes, dtype=int)
+ class_total = np.zeros(num_classes, dtype=int)
+ intersection = np.zeros(num_classes, dtype=int)
+ union = np.zeros(num_classes, dtype=int)
+ pred_counts = np.zeros(num_classes, dtype=int)
+
+ for pred, real in predicted_real_pairs:
+ pred_flat = pred.flatten()
+ real_flat = real.flatten()
+
+ total_pixels += pred_flat.size
+ correct_pixels += np.sum(pred_flat == real_flat)
+
+ for cls in range(num_classes):
+ pred_cls = pred_flat == cls
+ real_cls = real_flat == cls
+
+ class_correct[cls] += np.sum(pred_cls & real_cls)
+ class_total[cls] += np.sum(real_cls)
+ pred_counts[cls] += np.sum(pred_cls)
+
+ intersection[cls] += np.sum(pred_cls & real_cls)
+ union[cls] += np.sum(pred_cls | real_cls)
+
+ # Pixel-level accuracy
+ pixel_accuracy = (
+ float(correct_pixels) / float(total_pixels) if total_pixels > 0 else 0.0
+ )
+
+ # Per-class accuracy (avoid division by zero)
+ per_class_accuracy = [
+ float(class_correct[c]) / float(class_total[c])
+ if class_total[c] > 0
+ else 0.0
+ for c in range(num_classes)
+ ]
+
+ # Mean IoU
+ mean_iou = float(
+ np.mean(
+ [
+ float(intersection[c]) / float(union[c]) if union[c] > 0 else 0.0
+ for c in range(num_classes)
+ ],
+ ),
+ )
+
+ # Precision, recall, F1-score per class
+ precision_per_class = [
+ float(intersection[c]) / float(pred_counts[c])
+ if pred_counts[c] > 0
+ else 0.0
+ for c in range(num_classes)
+ ]
+ recall_per_class = [
+ float(intersection[c]) / float(class_total[c])
+ if class_total[c] > 0
+ else 0.0
+ for c in range(num_classes)
+ ]
+ f1_per_class = [
+ (2 * precision_per_class[c] * recall_per_class[c])
+ / (precision_per_class[c] + recall_per_class[c])
+ if (precision_per_class[c] + recall_per_class[c]) > 0
+ else 0.0
+ for c in range(num_classes)
+ ]
+ mean_f1 = float(np.mean(f1_per_class))
+ mean_precision = float(np.mean(precision_per_class))
+ mean_recall = float(np.mean(recall_per_class))
+
+ # Loss fallback
+ loss = 1.0 - pixel_accuracy
+
+ return MetricsResultInterface(
+ accuracy=pixel_accuracy,
+ loss=loss,
+ iou=mean_iou,
+ precision=mean_precision,
+ recall=mean_recall,
+ f1_score=mean_f1,
+ additional_metrics={
+ "per_class_accuracy": per_class_accuracy,
+ "precision_per_class": precision_per_class,
+ "recall_per_class": recall_per_class,
+ "f1_per_class": f1_per_class,
+ },
+ )
diff --git a/auto_ml/implementations/segmentators/swin.py b/auto_ml/implementations/segmentators/swin.py
new file mode 100644
index 0000000..ad14966
--- /dev/null
+++ b/auto_ml/implementations/segmentators/swin.py
@@ -0,0 +1,262 @@
+"""Swin Transformer model implementation for segmentation."""
+
+import copy
+from typing import Dict, List
+
+import numpy as np
+import torch
+import torch.nn as nn
+import torch.optim as optim
+from torch.utils.data import DataLoader
+
+from auto_ml.implementations.segmentators.base import InMemoryPyTorchDataset
+from auto_ml.interfaces import (
+ MaskPair,
+ MetricsResultInterface,
+ SegmentationDatasetInterface,
+ SegmentationModelInterface,
+)
+from auto_ml.models.swin.segmentation import SwinSegmentation
+
+
+class SwinModel(SegmentationModelInterface):
+ """
+ Swin Transformer Model implementation for AutoML.
+
+ Wraps the SwinSegmentation model from swin.segmentation.
+ """
+
+ def __init__( # noqa: D107
+ self,
+ epochs: int = 10,
+ batch_size: int = 4,
+ lr: float = 1e-4,
+ embed_dim: int = 96,
+ depths: List[int] | None = None,
+ num_heads: List[int] | None = None,
+ window_size: List[int] | None = None,
+ patience: int | None = None,
+ device: str = "auto",
+ ) -> None:
+ self.epochs = epochs
+ self.batch_size = batch_size
+ self.lr = lr
+ self.patience = patience
+
+ # Defaults for Swin-T if not provided
+ self.embed_dim = embed_dim
+ self.depths = depths if depths else [2, 2, 6, 2]
+ self.num_heads = num_heads if num_heads else [3, 6, 12, 24]
+ self.window_size = window_size if window_size else [7, 7]
+
+ if device == "auto":
+ self.device = (
+ "cuda"
+ if torch.cuda.is_available()
+ else "mps"
+ if torch.backends.mps.is_available()
+ else "cpu"
+ )
+ else:
+ self.device = device
+
+ self.model = SwinSegmentation(
+ patch_size=[4, 4], # Default fixed
+ embed_dim=self.embed_dim,
+ depths=self.depths,
+ num_heads=self.num_heads,
+ window_size=self.window_size,
+ mlp_ratio=4.0,
+ dropout=0.1,
+ num_classes=3,
+ channels=1,
+ ).to(self.device)
+
+ def train(
+ self,
+ dataset: SegmentationDatasetInterface,
+ validation_dataset: SegmentationDatasetInterface | None = None,
+ ) -> MetricsResultInterface:
+ """Train the model."""
+ pytorch_dataset = InMemoryPyTorchDataset(dataset)
+ dataloader = DataLoader(
+ pytorch_dataset,
+ batch_size=self.batch_size,
+ shuffle=True,
+ )
+
+ criterion = nn.CrossEntropyLoss()
+ optimizer = optim.Adam(self.model.parameters(), lr=self.lr)
+
+ # Log training mode
+ self._log_training_mode(validation_dataset)
+
+ # Early stopping state
+ best_val_loss = float("inf")
+ best_model_state: dict | None = None
+ patience_counter = 0
+
+ total_loss: float = 0
+ history: List[Dict[str, float]] = []
+ epochs_trained = 0
+
+ for epoch in range(self.epochs):
+ epoch_loss = 0
+ self.model.train()
+
+ for inputs, masks in dataloader:
+ inputs = inputs.to(self.device)
+ masks = masks.to(self.device)
+
+ # Check for channel mismatch (similar logic to ViTModel)
+ if inputs.shape[1] != 1:
+ inputs = (
+ inputs[:, 0:1, :, :] * 0.299
+ + inputs[:, 1:2, :, :] * 0.587
+ + inputs[:, 2:3, :, :] * 0.114
+ )
+
+ optimizer.zero_grad()
+ outputs = self.model(inputs)
+ loss = criterion(outputs, masks)
+ loss.backward()
+ optimizer.step()
+
+ epoch_loss += loss.item()
+
+ avg_loss = epoch_loss / len(dataloader) if len(dataloader) > 0 else 0
+ total_loss = float(avg_loss)
+ epochs_trained = epoch + 1
+
+ epoch_metrics = {
+ "epoch": epoch + 1,
+ "train_loss": float(avg_loss),
+ }
+
+ # Validation step
+ if validation_dataset:
+ avg_val_loss = self._compute_validation_loss(
+ validation_dataset,
+ criterion,
+ )
+ epoch_metrics["val_loss"] = avg_val_loss
+
+ # Track best model (always when validation is provided)
+ if avg_val_loss < best_val_loss:
+ best_val_loss = avg_val_loss
+ best_model_state = copy.deepcopy(self.model.state_dict())
+ patience_counter = 0
+ status = "improved"
+ else:
+ patience_counter += 1
+ if self.patience is not None:
+ status = f"no improvement ({patience_counter}/{self.patience})"
+ else:
+ status = "no improvement"
+
+ print(
+ f"Epoch {epoch + 1}/{self.epochs}, "
+ f"Loss: {avg_loss:.6f}, Val Loss: {avg_val_loss:.6f}, "
+ f"Status: {status}",
+ )
+
+ # Early stopping check (only when patience is set)
+ if self.patience is not None and patience_counter >= self.patience:
+ print(f"Early stopping at epoch {epoch + 1}")
+ break
+ else:
+ print(f"Epoch {epoch + 1}/{self.epochs}, Loss: {avg_loss:.6f}")
+
+ history.append(epoch_metrics)
+
+ # Restore best model if early stopping was used
+ if best_model_state is not None:
+ self.model.load_state_dict(best_model_state)
+ print(f"Restored best model (val_loss: {best_val_loss:.6f})")
+ total_loss = best_val_loss
+
+ return MetricsResultInterface(
+ loss=total_loss,
+ accuracy=0.0,
+ additional_metrics={"epochs_trained": epochs_trained},
+ history=history,
+ )
+
+ def _log_training_mode(
+ self,
+ validation_dataset: SegmentationDatasetInterface | None,
+ ) -> None:
+ """Log the training mode based on configuration."""
+ if self.patience is not None and validation_dataset is not None:
+ print(
+ f"Training for up to {self.epochs} epochs "
+ f"with early stopping (patience={self.patience})",
+ )
+ elif self.patience is not None and validation_dataset is None:
+ print(
+ f"Training for {self.epochs} epochs "
+ f"(patience ignored: no validation dataset)",
+ )
+ else:
+ print(f"Training for {self.epochs} epochs (no early stopping)")
+
+ def _compute_validation_loss(
+ self,
+ validation_dataset: SegmentationDatasetInterface,
+ criterion: nn.Module,
+ ) -> float:
+ """Compute average validation loss."""
+ self.model.eval()
+ val_dataset_torch = InMemoryPyTorchDataset(validation_dataset)
+ val_loader = DataLoader(val_dataset_torch, batch_size=1, shuffle=False)
+ val_loss = 0.0
+
+ with torch.no_grad():
+ for v_inputs, v_masks in val_loader:
+ v_inputs = v_inputs.to(self.device)
+ v_masks = v_masks.to(self.device)
+
+ if v_inputs.shape[1] != 1:
+ v_inputs = (
+ v_inputs[:, 0:1, :, :] * 0.299
+ + v_inputs[:, 1:2, :, :] * 0.587
+ + v_inputs[:, 2:3, :, :] * 0.114
+ )
+
+ v_outputs = self.model(v_inputs)
+ v_loss = criterion(v_outputs, v_masks)
+ val_loss += v_loss.item()
+
+ avg_val_loss = val_loss / len(val_loader) if len(val_loader) > 0 else 0
+ return float(avg_val_loss)
+
+ def evaluate(self, dataset: SegmentationDatasetInterface) -> List[MaskPair]:
+ """Evaluate the model and return predicted/real mask pairs."""
+ pytorch_dataset = InMemoryPyTorchDataset(dataset)
+ dataloader = DataLoader(pytorch_dataset, batch_size=1, shuffle=False)
+
+ self.model.eval()
+ mask_pairs: List[MaskPair] = []
+
+ with torch.no_grad():
+ for inputs, masks in dataloader:
+ inputs = inputs.to(self.device)
+ masks = masks.to(self.device)
+
+ if inputs.shape[1] != 1:
+ inputs = (
+ inputs[:, 0:1, :, :] * 0.299
+ + inputs[:, 1:2, :, :] * 0.587
+ + inputs[:, 2:3, :, :] * 0.114
+ )
+
+ outputs = self.model(inputs)
+
+ # Get predicted mask
+ predictions = torch.argmax(outputs, dim=1)
+ predicted_mask = predictions.squeeze().cpu().numpy().astype(np.uint8)
+ real_mask = masks.squeeze().cpu().numpy().astype(np.uint8)
+
+ mask_pairs.append((predicted_mask, real_mask))
+
+ return mask_pairs
diff --git a/auto_ml/implementations/segmentators/vit.py b/auto_ml/implementations/segmentators/vit.py
new file mode 100644
index 0000000..25a8e8b
--- /dev/null
+++ b/auto_ml/implementations/segmentators/vit.py
@@ -0,0 +1,198 @@
+"""ViT model implementation for segmentation."""
+
+from typing import Dict, List
+
+import numpy as np
+import torch
+import torch.nn as nn
+import torch.optim as optim
+from torch.utils.data import DataLoader
+
+from auto_ml.implementations.segmentators.base import InMemoryPyTorchDataset
+from auto_ml.interfaces import (
+ MaskPair,
+ MetricsResultInterface,
+ SegmentationDatasetInterface,
+ SegmentationModelInterface,
+)
+from auto_ml.models.vit.segmentation import ViTSegmentation
+
+
+class ViTModel(SegmentationModelInterface):
+ """
+ ViT Model implementation for AutoML.
+
+ Wraps the ViTSegmentation model from vit.model.
+ """
+
+ def __init__( # noqa: D107
+ self,
+ epochs: int = 10,
+ batch_size: int = 4,
+ lr: float = 1e-4,
+ dim: int = 768,
+ depth: int = 12,
+ heads: int = 12,
+ mlp_dim: int = 1024,
+ device: str = "auto",
+ ) -> None:
+ self.epochs = epochs
+ self.batch_size = batch_size
+ self.lr = lr
+ self.dim = dim
+
+ if device == "auto":
+ self.device = (
+ "cuda"
+ if torch.cuda.is_available()
+ else "mps"
+ if torch.backends.mps.is_available()
+ else "cpu"
+ )
+ else:
+ self.device = device
+
+ self.model = ViTSegmentation(
+ image_size=512,
+ patch_size=16,
+ num_classes=3,
+ dim=self.dim,
+ depth=depth,
+ heads=heads,
+ mlp_dim=mlp_dim,
+ channels=1, # Warning: Hardcoded for grayscale, should be dynamic if needed
+ dropout=0.1,
+ emb_dropout=0.1,
+ ).to(self.device)
+
+ def train(
+ self,
+ dataset: SegmentationDatasetInterface,
+ validation_dataset: SegmentationDatasetInterface | None = None,
+ ) -> MetricsResultInterface:
+ """Train the model."""
+ pytorch_dataset = InMemoryPyTorchDataset(dataset)
+ dataloader = DataLoader(
+ pytorch_dataset,
+ batch_size=self.batch_size,
+ shuffle=True,
+ )
+
+ criterion = nn.CrossEntropyLoss()
+ optimizer = optim.Adam(self.model.parameters(), lr=self.lr)
+
+ self.model.train()
+
+ total_loss: float = 0
+ history: List[Dict[str, float]] = []
+
+ # Simplified training loop for AutoML context
+ for epoch in range(self.epochs):
+ epoch_loss = 0
+ self.model.train()
+
+ # iterate over batches
+ for inputs, masks in dataloader:
+ inputs = inputs.to(self.device)
+ masks = masks.to(self.device)
+
+ # Check for channel mismatch if we hardcoded channels=1
+ if inputs.shape[1] != 1 and self.model.patch_embed.in_channels == 1:
+ # Force grayscale conversion if model expects 1 channel
+ # (B, 3, H, W) -> (B, 1, H, W) using luminosity method
+ inputs = (
+ inputs[:, 0:1, :, :] * 0.299
+ + inputs[:, 1:2, :, :] * 0.587
+ + inputs[:, 2:3, :, :] * 0.114
+ )
+
+ optimizer.zero_grad()
+ outputs = self.model(inputs)
+ loss = criterion(outputs, masks)
+ loss.backward()
+ optimizer.step()
+
+ epoch_loss += loss.item()
+
+ avg_loss = epoch_loss / len(dataloader) if len(dataloader) > 0 else 0
+ total_loss = float(avg_loss) # Report the last epoch's loss
+
+ epoch_metrics = {
+ "epoch": epoch + 1,
+ "train_loss": float(avg_loss),
+ }
+
+ # Validation Step
+ if validation_dataset:
+ self.model.eval()
+ val_dataset_torch = InMemoryPyTorchDataset(validation_dataset)
+ val_loader = DataLoader(val_dataset_torch, batch_size=1, shuffle=False)
+ val_loss = 0.0
+
+ with torch.no_grad():
+ for v_inputs, v_masks in val_loader:
+ v_inputs = v_inputs.to(self.device)
+ v_masks = v_masks.to(self.device)
+
+ if (
+ v_inputs.shape[1] != 1
+ and self.model.patch_embed.in_channels == 1
+ ):
+ v_inputs = (
+ v_inputs[:, 0:1, :, :] * 0.299
+ + v_inputs[:, 1:2, :, :] * 0.587
+ + v_inputs[:, 2:3, :, :] * 0.114
+ )
+
+ v_outputs = self.model(v_inputs)
+ v_loss = criterion(v_outputs, v_masks)
+ val_loss += v_loss.item()
+
+ avg_val_loss = val_loss / len(val_loader) if len(val_loader) > 0 else 0
+ epoch_metrics["val_loss"] = float(avg_val_loss)
+ print(
+ f"Epoch {epoch + 1}/{self.epochs}, "
+ f"Loss: {avg_loss:.6f}, Val Loss: {avg_val_loss:.6f}",
+ )
+ else:
+ print(f"Epoch {epoch + 1}/{self.epochs}, Loss: {avg_loss:.6f}")
+
+ history.append(epoch_metrics)
+
+ return MetricsResultInterface(
+ loss=total_loss,
+ accuracy=0.0, # Placeholder
+ additional_metrics={"epochs_trained": self.epochs},
+ history=history,
+ )
+
+ def evaluate(self, dataset: SegmentationDatasetInterface) -> List[MaskPair]:
+ """Evaluate the model and return predicted/real mask pairs."""
+ pytorch_dataset = InMemoryPyTorchDataset(dataset)
+ dataloader = DataLoader(pytorch_dataset, batch_size=1, shuffle=False)
+
+ self.model.eval()
+ mask_pairs: List[MaskPair] = []
+
+ with torch.no_grad():
+ for inputs, masks in dataloader:
+ inputs = inputs.to(self.device)
+ masks = masks.to(self.device)
+
+ if inputs.shape[1] != 1 and self.model.patch_embed.in_channels == 1:
+ inputs = (
+ inputs[:, 0:1, :, :] * 0.299
+ + inputs[:, 1:2, :, :] * 0.587
+ + inputs[:, 2:3, :, :] * 0.114
+ )
+
+ outputs = self.model(inputs)
+
+ # Get predicted mask
+ predictions = torch.argmax(outputs, dim=1)
+ predicted_mask = predictions.squeeze().cpu().numpy().astype(np.uint8)
+ real_mask = masks.squeeze().cpu().numpy().astype(np.uint8)
+
+ mask_pairs.append((predicted_mask, real_mask))
+
+ return mask_pairs
diff --git a/auto_ml/interfaces.py b/auto_ml/interfaces.py
new file mode 100644
index 0000000..a442a6e
--- /dev/null
+++ b/auto_ml/interfaces.py
@@ -0,0 +1,911 @@
+"""
+AutoML Training Interfaces Module.
+
+This module defines all the interfaces for the AutoML training pipeline:
+- ModelInputInterface: 512x512 image input
+- ModelOutputInterface: 512x512 matrix with values 0, 1, or 2
+- DatasetInterface: Set of original images and corresponding masks
+- DataAugmentatorInterface: Dataset transformation (Dataset -> Dataset)
+- DataAugmentatorNodeInterface: Dataset -> List of (Dataset, Dataset) tuples
+- ModelNodeInterface: List of (Dataset, Dataset) tuples -> Dictionary
+"""
+
+from abc import ABC, abstractmethod
+from dataclasses import dataclass, field
+from typing import Any, Dict, Iterator, List, Optional, Tuple
+
+import numpy as np
+import torch
+from numpy.typing import NDArray
+
+# ==============================================================================
+# Type Aliases for clarity
+# ==============================================================================
+
+# 512x512 image as numpy array (height, width, channels)
+# Shape: (512, 512, 3) for RGB or (512, 512) for grayscale
+ImageArray = NDArray[np.uint8]
+
+# 512x512 segmentation mask with values 0, 1, or 2
+MaskArray = NDArray[np.uint8] # Shape: (512, 512), values in {0, 1, 2}
+
+# Mask pair: (predicted_mask, real_mask)
+MaskPair = Tuple[MaskArray, MaskArray]
+
+# Classification dataset sample: (image, label)
+ClassificationDatasetSample = Tuple[ImageArray, int]
+
+# A dataset sample is a pair of (image, mask)
+SegmentationDatasetSample = Tuple[ImageArray, MaskArray]
+
+# ==============================================================================
+# Model Interfaces
+# ==============================================================================
+
+
+@dataclass
+class ModelInputInterface:
+ """
+ Model Input Interface: Represents a 512x512 image input.
+
+ Attributes:
+ image: A numpy array of shape (512, 512, 3) for RGB images
+ or (512, 512) for grayscale images.
+
+ """
+
+ image: ImageArray
+
+ def __post_init__(self) -> None:
+ """Validate the image dimensions."""
+ if self.image.ndim == 2:
+ if self.image.shape != (512, 512):
+ raise ValueError(
+ f"Grayscale image must be 512x512, got {self.image.shape}",
+ )
+ elif self.image.ndim == 3:
+ if self.image.shape[:2] != (512, 512):
+ raise ValueError(
+ f"Image height and width must be 512x512, got {self.image.shape[:2]}", # noqa: E501
+ )
+ else:
+ raise ValueError(
+ f"Image must be 2D (grayscale) or 3D (color), got {self.image.ndim}D",
+ )
+
+ @property
+ def shape(self) -> Tuple[int, ...]:
+ """Return the shape of the image."""
+ return self.image.shape
+
+ @property
+ def is_grayscale(self) -> bool:
+ """Check if the image is grayscale."""
+ return self.image.ndim == 2
+
+ def to_tensor(self) -> NDArray[np.float32]:
+ """
+ Convert the image to a normalized float tensor.
+
+ Returns:
+ Normalized image array with values in [0, 1].
+
+ """
+ return self.image.astype(np.float32) / 255.0
+
+
+# ==============================================================================
+# Model Output Interface
+# ==============================================================================
+
+
+@dataclass
+class ModelOutputInterface:
+ """
+ Model Output Interface: Represents a 512x512 segmentation mask.
+
+ The mask contains integer values 0, 1, or 2 representing different
+ segmentation classes.
+
+ Attributes:
+ mask: A numpy array of shape (512, 512) with values in {0, 1, 2}.
+
+ """
+
+ mask: MaskArray
+
+ def __post_init__(self) -> None:
+ """Validate the mask dimensions and values."""
+ if self.mask.shape != (512, 512):
+ raise ValueError(
+ f"Mask must be 512x512, got {self.mask.shape}",
+ )
+
+ unique_values = np.unique(self.mask)
+ valid_values = {0, 1, 2}
+ if not set(unique_values).issubset(valid_values):
+ invalid = set(unique_values) - valid_values
+ raise ValueError(
+ f"Mask values must be 0, 1, or 2. Found invalid values: {invalid}",
+ )
+
+ @property
+ def shape(self) -> Tuple[int, int]:
+ """Return the shape of the mask."""
+ return self.mask.shape
+
+ def get_class_counts(self) -> Dict[int, int]:
+ """
+ Count pixels for each class.
+
+ Returns:
+ Dictionary mapping class index to pixel count.
+
+ """
+ unique, counts = np.unique(self.mask, return_counts=True)
+ return {int(k): int(v) for k, v in zip(unique, counts)}
+
+ def to_one_hot(self, num_classes: int = 3) -> NDArray[np.float32]:
+ """
+ Convert to one-hot encoded representation.
+
+ Args:
+ num_classes: Number of classes (default: 3).
+
+ Returns:
+ One-hot encoded array of shape (512, 512, num_classes).
+
+ """
+ one_hot = np.zeros((512, 512, num_classes), dtype=np.float32)
+ for c in range(num_classes):
+ one_hot[:, :, c] = (self.mask == c).astype(np.float32)
+ return one_hot
+
+
+# ==============================================================================
+# Metrics Result Interface
+# ==============================================================================
+
+
+@dataclass
+class MetricsResultInterface:
+ """
+ Metrics Result Interface: Represents the result of training or evaluation.
+
+ Attributes:
+ accuracy: Overall accuracy score (0.0 to 1.0).
+ loss: Loss value from training/evaluation.
+ iou: Intersection over Union score for segmentation tasks.
+ precision: Precision score (0.0 to 1.0).
+ recall: Recall score (0.0 to 1.0).
+ f1_score: F1 score (harmonic mean of precision and recall).
+ additional_metrics: Dictionary for any additional custom metrics.
+
+ """
+
+ accuracy: float = 0.0
+ loss: float = 0.0
+ iou: float = 0.0
+ precision: float = 0.0
+ recall: float = 0.0
+ f1_score: float = 0.0
+ additional_metrics: Dict[str, Any] = field(default_factory=dict)
+ history: List[Dict[str, float]] = field(default_factory=list)
+
+ def to_dict(self) -> Dict[str, Any]:
+ """Convert metrics to a dictionary."""
+ return {
+ "accuracy": self.accuracy,
+ "loss": self.loss,
+ "iou": self.iou,
+ "precision": self.precision,
+ "recall": self.recall,
+ "f1_score": self.f1_score,
+ "history": self.history,
+ **self.additional_metrics,
+ }
+
+ def __repr__(self) -> str:
+ """Return a string representation of the metrics."""
+ return (
+ f"MetricsResultInterface("
+ f"accuracy={self.accuracy:.4f}, "
+ f"loss={self.loss:.4f}, "
+ f"iou={self.iou:.4f}, "
+ f"f1={self.f1_score:.4f})"
+ )
+
+
+# ==============================================================================
+# Dataset Interface
+# ==============================================================================
+
+
+@dataclass
+class SegmentationDatasetInterface:
+ """
+ Dataset Interface: A collection of 512x512 original images and their masks.
+
+ The dataset is stored as a list of pairs, where each pair contains
+ an original image and its corresponding mask.
+
+ Attributes:
+ samples: List of (image, mask) pairs.
+ metadata: Optional dictionary containing dataset metadata.
+
+ """
+
+ samples: List[SegmentationDatasetSample] = field(default_factory=list)
+ metadata: Dict[str, Any] = field(default_factory=dict)
+
+ def __len__(self) -> int:
+ """Return the number of samples in the dataset."""
+ return len(self.samples)
+
+ def __getitem__(self, idx: int) -> Tuple[ModelInputInterface, ModelOutputInterface]:
+ """
+ Get a single sample from the dataset.
+
+ Args:
+ idx: Index of the sample.
+
+ Returns:
+ Tuple of (ModelInputInterface, ModelOutputInterface).
+
+ """
+ image, mask = self.samples[idx]
+ return (
+ ModelInputInterface(image=image),
+ ModelOutputInterface(mask=mask),
+ )
+
+ def __iter__(self) -> Iterator[SegmentationDatasetSample]:
+ """Iterate over all samples in the dataset."""
+ for sample in self.samples:
+ yield sample
+
+ def get_raw_sample(self, idx: int) -> SegmentationDatasetSample:
+ """
+ Get a raw sample (image, mask) pair without wrapping in interfaces.
+
+ Args:
+ idx: Index of the sample.
+
+ Returns:
+ Tuple of (image array, mask array).
+
+ """
+ return self.samples[idx]
+
+ @property
+ def images(self) -> List[ImageArray]:
+ """Get all images from the dataset."""
+ return [sample[0] for sample in self.samples]
+
+ @property
+ def masks(self) -> List[MaskArray]:
+ """Get all masks from the dataset."""
+ return [sample[1] for sample in self.samples]
+
+ def add_sample(self, image: ImageArray, mask: MaskArray) -> None:
+ """
+ Add a new sample to the dataset.
+
+ Args:
+ image: 512x512 image array.
+ mask: 512x512 mask array with values in {0, 1, 2}.
+
+ """
+ # Validate by creating the interface objects
+ _ = ModelInputInterface(image=image)
+ _ = ModelOutputInterface(mask=mask)
+
+ self.samples.append((image, mask))
+
+ def add_pair(self, pair: SegmentationDatasetSample) -> None:
+ """
+ Add a new (image, mask) pair to the dataset.
+
+ Args:
+ pair: Tuple of (image array, mask array).
+
+ """
+ image, mask = pair
+ self.add_sample(image, mask)
+
+ @classmethod
+ def from_pairs(
+ cls,
+ pairs: List[SegmentationDatasetSample],
+ metadata: Dict[str, Any] | None = None,
+ ) -> "SegmentationDatasetInterface":
+ """
+ Create a dataset from a list of (image, mask) pairs.
+
+ Args:
+ pairs: List of (image, mask) tuples.
+ metadata: Optional metadata dictionary.
+
+ Returns:
+ DatasetInterface instance.
+
+ """
+ dataset = cls(samples=[], metadata=metadata or {})
+ for image, mask in pairs:
+ dataset.add_sample(image, mask)
+ return dataset
+
+ def split(
+ self,
+ ratio: float = 0.8,
+ shuffle: bool = True,
+ random_seed: int | None = None,
+ ) -> Tuple["SegmentationDatasetInterface", "SegmentationDatasetInterface"]:
+ """
+ Split the dataset into two parts.
+
+ Args:
+ ratio: Ratio for the first split (default: 0.8 for 80/20 split).
+ shuffle: Whether to shuffle before splitting (default: True).
+ random_seed: Random seed for reproducibility.
+
+ Returns:
+ Tuple of two DatasetInterface objects.
+
+ """
+ n = len(self)
+ indices = np.arange(n)
+
+ if shuffle:
+ rng = np.random.default_rng(random_seed)
+ rng.shuffle(indices)
+
+ split_idx = int(n * ratio)
+
+ train_indices = indices[:split_idx]
+ val_indices = indices[split_idx:]
+
+ train_dataset = SegmentationDatasetInterface(
+ samples=[self.samples[i] for i in train_indices],
+ metadata={**self.metadata, "split": "train"},
+ )
+
+ val_dataset = SegmentationDatasetInterface(
+ samples=[self.samples[i] for i in val_indices],
+ metadata={**self.metadata, "split": "val"},
+ )
+
+ return train_dataset, val_dataset
+
+
+# ===============================================================================
+# Classification Dataset Interface
+# ===============================================================================
+
+
+@dataclass
+class ClassificationDatasetInterface:
+ """
+ Classification Dataset Interface: A collection of image regions and labels.
+
+ Store training data for classification models. Each sample consists of
+ an image array and its corresponding class label.
+
+ Attributes:
+ samples: List of (image, label) pairs.
+ metadata: Optional dictionary containing dataset metadata.
+
+ """
+
+ samples: List[ClassificationDatasetSample] = field(default_factory=list)
+ metadata: Dict[str, Any] = field(default_factory=dict)
+
+ def __len__(self) -> int:
+ """Return the number of samples in the dataset."""
+ return len(self.samples)
+
+ def __getitem__(self, idx: int) -> Tuple[ImageArray, int]:
+ """
+ Get a single sample from the dataset.
+
+ Args:
+ idx: Index of the sample.
+
+ Returns:
+ Tuple of (image array, label).
+
+ """
+ return self.samples[idx]
+
+ def __iter__(self) -> Iterator[ClassificationDatasetSample]:
+ """Iterate over all samples in the dataset."""
+ for sample in self.samples:
+ yield sample
+
+ def get_raw_sample(self, idx: int) -> ClassificationDatasetSample:
+ """
+ Get a raw sample (image, label) pair.
+
+ Args:
+ idx: Index of the sample.
+
+ Returns:
+ Tuple of (image array, label).
+
+ """
+ return self.samples[idx]
+
+ @property
+ def images(self) -> List[ImageArray]:
+ """Get all images from the dataset."""
+ return [sample[0] for sample in self.samples]
+
+ @property
+ def labels(self) -> List[int]:
+ """Get all labels from the dataset."""
+ return [sample[1] for sample in self.samples]
+
+ def add_sample(self, image: ImageArray, label: int) -> None:
+ """
+ Add a new sample to the dataset.
+
+ Args:
+ image: Image array.
+ label: Class label (integer).
+
+ """
+ self.samples.append((image, label))
+
+ def add_pair(self, pair: ClassificationDatasetSample) -> None:
+ """
+ Add a new (image, label) pair to the dataset.
+
+ Args:
+ pair: Tuple of (image array, label).
+
+ """
+ image, label = pair
+ self.add_sample(image, label)
+
+ @classmethod
+ def from_pairs(
+ cls,
+ pairs: List[ClassificationDatasetSample],
+ metadata: Dict[str, Any] | None = None,
+ ) -> "ClassificationDatasetInterface":
+ """
+ Create a dataset from a list of (image, label) pairs.
+
+ Args:
+ pairs: List of (image, label) tuples.
+ metadata: Optional metadata dictionary.
+
+ Returns:
+ ClassificationDatasetInterface instance.
+
+ """
+ dataset = cls(samples=[], metadata=metadata or {})
+ for image, label in pairs:
+ dataset.add_sample(image, label)
+ return dataset
+
+ @classmethod
+ def from_tensors(
+ cls,
+ images: "torch.Tensor",
+ labels: "torch.Tensor",
+ metadata: Dict[str, Any] | None = None,
+ ) -> "ClassificationDatasetInterface":
+ """
+ Create a dataset from image and label tensors.
+
+ Args:
+ images: Image tensor of shape (N, C, H, W).
+ labels: Label tensor of shape (N,).
+ metadata: Optional metadata dictionary.
+
+ Returns:
+ ClassificationDatasetInterface instance.
+
+ """
+ dataset = cls(samples=[], metadata=metadata or {})
+ # Convert tensors to numpy arrays and add samples
+ images_np = (images.cpu().numpy() * 255).astype(np.uint8)
+ labels_np = labels.cpu().numpy()
+
+ for i in range(len(labels)):
+ # Handle different tensor shapes
+ img = images_np[i]
+ if img.ndim == 3: # (C, H, W) -> (H, W, C) or (H, W)
+ if img.shape[0] == 1: # Grayscale
+ img = img[0] # (H, W)
+ else: # RGB
+ img = img.transpose(1, 2, 0) # (H, W, C)
+ dataset.add_sample(img, int(labels_np[i]))
+
+ return dataset
+
+ def to_tensors(self) -> Tuple[List["torch.Tensor"], "torch.Tensor"]:
+ """
+ Convert dataset to tensors.
+
+ Returns:
+ Tuple of (list of image tensors, labels tensor).
+ Each image tensor has shape (C, H, W).
+ Images may have different sizes.
+
+ """
+ import torch
+
+ images_list = []
+ labels_list = []
+
+ for image, label in self.samples:
+ # Normalize to [0, 1]
+ image_float = image.astype(np.float32) / 255.0
+
+ # Handle grayscale vs RGB
+ if image_float.ndim == 2:
+ # Grayscale: (H, W) -> (1, H, W)
+ tensor = torch.from_numpy(image_float).unsqueeze(0)
+ else:
+ # RGB: (H, W, C) -> (C, H, W)
+ tensor = torch.from_numpy(image_float).permute(2, 0, 1)
+
+ images_list.append(tensor)
+ labels_list.append(label)
+
+ labels_tensor = torch.tensor(labels_list, dtype=torch.long)
+
+ return images_list, labels_tensor
+
+ def split(
+ self,
+ ratio: float = 0.8,
+ shuffle: bool = True,
+ random_seed: int | None = None,
+ ) -> Tuple["ClassificationDatasetInterface", "ClassificationDatasetInterface"]:
+ """
+ Split the dataset into two parts.
+
+ Args:
+ ratio: Ratio for the first split (default: 0.8 for 80/20 split).
+ shuffle: Whether to shuffle before splitting (default: True).
+ random_seed: Random seed for reproducibility.
+
+ Returns:
+ Tuple of two ClassificationDatasetInterface objects.
+
+ """
+ n = len(self)
+ indices = np.arange(n)
+
+ if shuffle:
+ rng = np.random.default_rng(random_seed)
+ rng.shuffle(indices)
+
+ split_idx = int(n * ratio)
+
+ train_indices = indices[:split_idx]
+ val_indices = indices[split_idx:]
+
+ train_dataset = ClassificationDatasetInterface(
+ samples=[self.samples[i] for i in train_indices],
+ metadata={**self.metadata, "split": "train"},
+ )
+
+ val_dataset = ClassificationDatasetInterface(
+ samples=[self.samples[i] for i in val_indices],
+ metadata={**self.metadata, "split": "val"},
+ )
+
+ return train_dataset, val_dataset
+
+
+# ===============================================================================
+# Classification Model Interface
+# ===============================================================================
+
+
+class ClassificationModelInterface(ABC):
+ """
+ Minimum interface for an image classifier.
+
+ Receives an image region and return a tuple of
+ (class_label, confidence), where class_label ∈ Z and
+ confidence ∈ [0,1].
+ """
+
+ @abstractmethod
+ def classify(
+ self,
+ image: ImageArray,
+ x: int,
+ y: int,
+ width: int,
+ height: int,
+ ) -> Tuple[int, float]:
+ """
+ Classify an image region.
+
+ Args:
+ image: image as numpy array.
+ x: x-coordinate of the region.
+ y: y-coordinate of the region.
+ width: width of the region.
+ height: height of the region.
+
+ Returns:
+ Tuple of (class_label, confidence),
+ where class_label ∈ {0,1,2} and confidence ∈ [0,1].
+
+ """
+ pass
+
+ @abstractmethod
+ def train(
+ self,
+ dataset: ClassificationDatasetInterface,
+ ) -> MetricsResultInterface:
+ """
+ Train the classification model on provided data.
+
+ Args:
+ dataset: The training dataset.
+
+ Returns:
+ MetricsResultInterface containing training metrics.
+
+ """
+ pass
+
+
+# ==============================================================================
+# Segmentation Model Interface
+# ==============================================================================
+
+
+class SegmentationModelInterface(ABC):
+ """
+ Segmentation Model Interface: Abstract base class for machine learning models.
+
+ Provides a standard interface for training and evaluating models.
+ Both methods receive a DatasetInterface and return a MetricsResultInterface.
+ """
+
+ @abstractmethod
+ def train(
+ self,
+ dataset: "SegmentationDatasetInterface",
+ validation_dataset: Optional["SegmentationDatasetInterface"] = None,
+ ) -> MetricsResultInterface:
+ """
+ Train the model on the provided dataset.
+
+ Args:
+ dataset: The training dataset.
+ validation_dataset: Optional validation dataset for tracking progress.
+
+ Returns:
+ MetricsResultInterface containing training metrics.
+
+ """
+ pass
+
+ @abstractmethod
+ def evaluate(self, dataset: "SegmentationDatasetInterface") -> List[MaskPair]:
+ """
+ Evaluate the model on the provided dataset.
+
+ Args:
+ dataset: The evaluation dataset.
+
+ Returns:
+ List of (predicted_mask, real_mask) tuples for each sample.
+
+ """
+ pass
+
+
+# ==============================================================================
+# Data Augmentator Interface (Base Class)
+# ==============================================================================
+
+
+class DataAugmentatorInterface(ABC):
+ """
+ Data Augmentator Interface: Transforms a Dataset into another Dataset.
+
+ This is the base class for all data augmentation operations.
+ Input: DatasetInterface
+ Output: DatasetInterface
+ """
+
+ @abstractmethod
+ def augment(
+ self,
+ dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """
+ Apply augmentation to the dataset.
+
+ Args:
+ dataset: Input dataset to augment.
+
+ Returns:
+ Augmented dataset.
+
+ """
+ pass
+
+ def __call__(
+ self,
+ dataset: SegmentationDatasetInterface,
+ ) -> SegmentationDatasetInterface:
+ """Allow calling the augmentator as a function."""
+ return self.augment(dataset)
+
+
+# ==============================================================================
+# Data Augmentator Node Interface
+# ==============================================================================
+
+
+class DataAugmentatorNodeInterface(ABC):
+ """
+ Data Augmentator Node Interface.
+
+ Processes a Dataset and returns
+ a list of tuples, where each tuple contains two Datasets.
+
+ This can be used for creating train/validation splits, cross-validation
+ folds, or other dataset partitioning schemes.
+
+ Input: DatasetInterface
+ Output: List[Tuple[DatasetInterface, DatasetInterface]]
+ """
+
+ @abstractmethod
+ def process(
+ self,
+ dataset: SegmentationDatasetInterface,
+ ) -> List[Tuple[SegmentationDatasetInterface, SegmentationDatasetInterface]]:
+ """
+ Process the dataset and return a list of dataset pairs.
+
+ Args:
+ dataset: Input dataset to process.
+
+ Returns:
+ List of tuples, each containing two datasets
+ (e.g., train/validation pairs for k-fold cross-validation).
+
+ """
+ pass
+
+ def __call__(
+ self,
+ dataset: SegmentationDatasetInterface,
+ ) -> List[Tuple[SegmentationDatasetInterface, SegmentationDatasetInterface]]:
+ """Allow calling the node as a function."""
+ return self.process(dataset)
+
+
+# ==============================================================================
+# Model Node Interface
+# ==============================================================================
+
+
+class ModelNodeInterface(ABC):
+ """
+ Model Node Interface.
+
+ Receives a list of dataset tuples and returns
+ a list of mask pair lists from evaluation.
+
+ This interface represents the training/evaluation node in the pipeline.
+
+ Input: List[Tuple[DatasetInterface, DatasetInterface]]
+ (e.g., list of (train_dataset, val_dataset) pairs)
+ Output: List[List[MaskPair]] - list of mask pair lists (one per fold)
+ """
+
+ @abstractmethod
+ def train(
+ self,
+ dataset_pairs: List[
+ Tuple[SegmentationDatasetInterface, SegmentationDatasetInterface]
+ ],
+ ) -> List[List[MaskPair]]:
+ """
+ Train the model on the provided dataset pairs.
+
+ Args:
+ dataset_pairs: List of (train_dataset, val_dataset) tuples.
+
+ Returns:
+ List of mask pair lists, one list per dataset pair/fold.
+ Each inner list contains (predicted_mask, real_mask) tuples.
+
+ """
+ pass
+
+ def __call__(
+ self,
+ dataset_pairs: List[
+ Tuple[SegmentationDatasetInterface, SegmentationDatasetInterface]
+ ],
+ ) -> List[List[MaskPair]]:
+ """Allow calling the node as a function."""
+ return self.train(dataset_pairs)
+
+
+# ==============================================================================
+# Evaluator Interface
+# ==============================================================================
+
+
+class EvaluatorInterface(ABC):
+ """
+ Evaluator Interface: Base class for individual evaluation metrics.
+
+ Each evaluator computes a specific metric from mask pairs.
+
+ Input: List[List[MaskPair]] - list of mask pair lists from ModelNode
+ Output: float - the evaluation result as a single value
+ """
+
+ @abstractmethod
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """
+ Evaluate the mask pairs and return a metric value.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ Evaluation metric as a float value.
+
+ """
+ pass
+
+ def __call__(self, mask_pairs: List[List[MaskPair]]) -> float:
+ """Allow calling the evaluator as a function."""
+ return self.evaluate(mask_pairs)
+
+
+# ==============================================================================
+# Evaluator Node Interface
+# ==============================================================================
+
+
+class EvaluatorNodeInterface(ABC):
+ """
+ Evaluator Node Interface.
+
+ Receives a dictionary of named evaluators and mask pairs from ModelNode.
+ Calls each evaluator and returns a dictionary of results.
+
+ Input: List[List[MaskPair]] - list of mask pair lists from ModelNode
+ Output: Dict[str, Any] - dictionary mapping evaluator names to their results
+ """
+
+ @abstractmethod
+ def evaluate(self, mask_pairs: List[List[MaskPair]]) -> Dict[str, Any]:
+ """
+ Run all evaluators on the mask pairs.
+
+ Args:
+ mask_pairs: List of mask pair lists from ModelNode,
+ where each pair is (predicted_mask, real_mask).
+
+ Returns:
+ Dictionary mapping evaluator names to their results.
+
+ """
+ pass
+
+ def __call__(self, mask_pairs: List[List[MaskPair]]) -> Dict[str, Any]:
+ """Allow calling the node as a function."""
+ return self.evaluate(mask_pairs)
diff --git a/src/__init__.py b/auto_ml/models/__init__.py
similarity index 100%
rename from src/__init__.py
rename to auto_ml/models/__init__.py
diff --git a/auto_ml/models/cnn/__init__.py b/auto_ml/models/cnn/__init__.py
new file mode 100644
index 0000000..b71a916
--- /dev/null
+++ b/auto_ml/models/cnn/__init__.py
@@ -0,0 +1,5 @@
+"""CNN Classifier Model."""
+
+from .model import CNNClassifier
+
+__all__ = ["CNNClassifier"]
diff --git a/auto_ml/models/cnn/model.py b/auto_ml/models/cnn/model.py
new file mode 100644
index 0000000..4145e55
--- /dev/null
+++ b/auto_ml/models/cnn/model.py
@@ -0,0 +1,146 @@
+import torch
+import torch.nn as nn
+
+
+class CNNClassifier(nn.Module):
+ """CNN Classifier for variable-size image classification."""
+
+ def __init__(
+ self,
+ num_classes: int = 3,
+ channels: int = 1,
+ base_filters: int = 32,
+ dropout: float = 0.5,
+ num_blocks: int = 3,
+ ) -> None:
+ """
+ Initialize the CNN Classifier.
+
+ Args:
+ num_classes: Number of output classes.
+ channels: Number of input channels (1 for grayscale, 3 for RGB).
+ base_filters: Base number of filters (doubled at each block).
+ dropout: Dropout rate before final classification layer.
+ num_blocks: Number of convolutional blocks. Each block halves the
+ spatial dimensions via MaxPool2d, so the minimum supported
+ input size is 2^num_blocks × 2^num_blocks pixels.
+ Examples: num_blocks=3 → min 8×8, num_blocks=5 → min 32×32.
+
+ """
+ super().__init__()
+
+ self.num_classes = num_classes
+ self.num_blocks = num_blocks
+
+ # Feature extraction blocks
+ # Each block: Conv -> BatchNorm -> ReLU -> Conv -> BatchNorm -> ReLU -> MaxPool
+ # Receptive field grows while spatial dimensions shrink
+ self.features = self._build_feature_blocks(channels, base_filters, num_blocks)
+
+ # Calculate final channel count after num_blocks doublings:
+ # Block 1: base_filters, Block 2: base_filters*2, ...
+ # Block N: base_filters * 2^(N-1)
+ final_channels = base_filters * (2 ** (num_blocks - 1))
+
+ # Adaptive pooling: any spatial size -> 1x1
+ # This enables variable input sizes
+ self.global_pool = nn.AdaptiveAvgPool2d((1, 1))
+
+ # Classification head
+ self.classifier = nn.Sequential(
+ nn.Flatten(),
+ nn.Dropout(dropout),
+ nn.Linear(final_channels, max(final_channels // 4, num_classes)),
+ nn.ReLU(),
+ nn.Dropout(dropout * 0.5),
+ nn.Linear(max(final_channels // 4, num_classes), num_classes),
+ )
+
+ # Softmax for probability distribution output
+ self.softmax = nn.Softmax(dim=1)
+
+ def _build_feature_blocks(
+ self,
+ channels: int,
+ base_filters: int,
+ num_blocks: int,
+ ) -> nn.Sequential:
+ """
+ Build feature extraction blocks dynamically.
+
+ Args:
+ channels: Number of input channels.
+ base_filters: Base number of filters.
+ num_blocks: Number of convolutional blocks to create.
+
+ Returns:
+ Sequential container with all feature extraction blocks.
+
+ """
+ blocks = []
+ in_channels = channels
+
+ for i in range(num_blocks):
+ out_channels = base_filters * (2**i)
+ blocks.append(self._make_block(in_channels, out_channels))
+ in_channels = out_channels
+
+ return nn.Sequential(*blocks)
+
+ def _make_block(self, in_channels: int, out_channels: int) -> nn.Sequential:
+ """
+ Create a convolutional block.
+
+ Args:
+ in_channels: Number of input channels.
+ out_channels: Number of output channels.
+
+ Returns:
+ A sequential block with two conv layers and max pooling.
+
+ """
+ return nn.Sequential(
+ nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1),
+ nn.BatchNorm2d(out_channels),
+ nn.ReLU(inplace=True),
+ nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1),
+ nn.BatchNorm2d(out_channels),
+ nn.ReLU(inplace=True),
+ nn.MaxPool2d(kernel_size=2, stride=2),
+ )
+
+ def forward(
+ self,
+ x: torch.Tensor,
+ return_logits: bool = False,
+ ) -> torch.Tensor:
+ """
+ Perform forward pass of the CNN Classifier.
+
+ Args:
+ x: Input tensor of shape (B, C, H, W). H and W must be at least
+ 2^num_blocks pixels (e.g., 8×8 for num_blocks=3).
+ return_logits: If True, return raw logits instead of probabilities.
+ Use True when training with CrossEntropyLoss.
+
+ Returns:
+ Tensor of shape (B, num_classes) with probabilities (or logits if
+ return_logits=True). Probabilities sum to 1 along dim=1.
+
+ """
+ # Feature extraction
+ x = self.features(x)
+
+ # Global pooling: (B, C, H', W') -> (B, C, 1, 1)
+ x = self.global_pool(x)
+
+ # Classification: (B, C, 1, 1) -> (B, num_classes)
+ x = self.classifier(x)
+
+ # Return probabilities or logits
+ if return_logits:
+ return x
+
+ sm: torch.Tensor = self.softmax(x)
+
+ return sm
diff --git a/auto_ml/models/maskautoencoder/__init__.py b/auto_ml/models/maskautoencoder/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/auto_ml/models/maskautoencoder/maskautoencoder.py b/auto_ml/models/maskautoencoder/maskautoencoder.py
new file mode 100644
index 0000000..1560a36
--- /dev/null
+++ b/auto_ml/models/maskautoencoder/maskautoencoder.py
@@ -0,0 +1,103 @@
+from typing import Tuple
+
+import torch
+import torch.nn as nn
+
+
+class MaskAutoencoder(nn.Module):
+ """
+ Convolutional Autoencoder for mask dimensionality reduction.
+
+ Compresses 512x512 masks to a 3D latent representation while
+ preserving structural information through reconstruction.
+ """
+
+ def __init__(self, latent_dim: int = 3) -> None:
+ """
+ Initialize the autoencoder.
+
+ Args:
+ latent_dim: Dimension of the latent space (default: 3).
+
+ """
+ super().__init__()
+ self.latent_dim = latent_dim
+
+ # Encoder: 512x512 -> 3D
+ # 512 -> 256 -> 128 -> 64 -> 32 -> 16 -> 8 -> 4
+ self.encoder = nn.Sequential(
+ nn.Conv2d(1, 32, kernel_size=4, stride=2, padding=1), # 256x256
+ nn.BatchNorm2d(32),
+ nn.ReLU(inplace=True),
+ nn.Conv2d(32, 64, kernel_size=4, stride=2, padding=1), # 128x128
+ nn.BatchNorm2d(64),
+ nn.ReLU(inplace=True),
+ nn.Conv2d(64, 128, kernel_size=4, stride=2, padding=1), # 64x64
+ nn.BatchNorm2d(128),
+ nn.ReLU(inplace=True),
+ nn.Conv2d(128, 256, kernel_size=4, stride=2, padding=1), # 32x32
+ nn.BatchNorm2d(256),
+ nn.ReLU(inplace=True),
+ nn.Conv2d(256, 512, kernel_size=4, stride=2, padding=1), # 16x16
+ nn.BatchNorm2d(512),
+ nn.ReLU(inplace=True),
+ nn.Conv2d(512, 512, kernel_size=4, stride=2, padding=1), # 8x8
+ nn.BatchNorm2d(512),
+ nn.ReLU(inplace=True),
+ nn.Conv2d(512, 512, kernel_size=4, stride=2, padding=1), # 4x4
+ nn.BatchNorm2d(512),
+ nn.ReLU(inplace=True),
+ nn.Flatten(), # 512 * 4 * 4 = 8192
+ nn.Linear(512 * 4 * 4, 256),
+ nn.ReLU(inplace=True),
+ nn.Linear(256, latent_dim),
+ )
+
+ # Decoder: 3D -> 512x512
+ self.decoder_fc = nn.Sequential(
+ nn.Linear(latent_dim, 256),
+ nn.ReLU(inplace=True),
+ nn.Linear(256, 512 * 4 * 4),
+ nn.ReLU(inplace=True),
+ )
+
+ self.decoder_conv = nn.Sequential(
+ nn.ConvTranspose2d(512, 512, kernel_size=4, stride=2, padding=1), # 8x8
+ nn.BatchNorm2d(512),
+ nn.ReLU(inplace=True),
+ nn.ConvTranspose2d(512, 512, kernel_size=4, stride=2, padding=1), # 16x16
+ nn.BatchNorm2d(512),
+ nn.ReLU(inplace=True),
+ nn.ConvTranspose2d(512, 256, kernel_size=4, stride=2, padding=1), # 32x32
+ nn.BatchNorm2d(256),
+ nn.ReLU(inplace=True),
+ nn.ConvTranspose2d(256, 128, kernel_size=4, stride=2, padding=1), # 64x64
+ nn.BatchNorm2d(128),
+ nn.ReLU(inplace=True),
+ nn.ConvTranspose2d(128, 64, kernel_size=4, stride=2, padding=1), # 128x128
+ nn.BatchNorm2d(64),
+ nn.ReLU(inplace=True),
+ nn.ConvTranspose2d(64, 32, kernel_size=4, stride=2, padding=1), # 256x256
+ nn.BatchNorm2d(32),
+ nn.ReLU(inplace=True),
+ nn.ConvTranspose2d(32, 1, kernel_size=4, stride=2, padding=1), # 512x512
+ nn.Sigmoid(), # Output in [0, 1]
+ )
+
+ def encode(self, x: torch.Tensor) -> torch.Tensor:
+ """Encode input to latent space."""
+ result: torch.Tensor = self.encoder(x)
+ return result
+
+ def decode(self, z: torch.Tensor) -> torch.Tensor:
+ """Decode latent vector to reconstruction."""
+ x = self.decoder_fc(z)
+ x = x.view(-1, 512, 4, 4)
+ result: torch.Tensor = self.decoder_conv(x)
+ return result
+
+ def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
+ """Forward pass: returns (reconstruction, latent_vector)."""
+ z = self.encode(x)
+ recon = self.decode(z)
+ return recon, z
diff --git a/auto_ml/models/swin/__init__.py b/auto_ml/models/swin/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/auto_ml/models/swin/classification.py b/auto_ml/models/swin/classification.py
new file mode 100644
index 0000000..d78c040
--- /dev/null
+++ b/auto_ml/models/swin/classification.py
@@ -0,0 +1,96 @@
+"""Swin Transformer for classification."""
+
+from typing import List, Tuple
+
+import torch
+import torch.nn as nn
+from torchvision.models.swin_transformer import SwinTransformer
+
+
+class SwinClassifier(nn.Module):
+ """
+ Swin Transformer model for image classification.
+
+ This model uses the Swin Transformer as a feature extractor and adds a
+ classification head on top. It is designed to be a standalone
+ classifier that can be wrapped by an interface class.
+ """
+
+ def __init__(
+ self,
+ image_size: int = 224,
+ patch_size: Tuple[int, int] = (4, 4),
+ embed_dim: int = 96,
+ depths: List[int] = [2, 2, 6, 2],
+ num_heads: List[int] = [3, 6, 12, 24],
+ window_size: List[int] = [7, 7],
+ mlp_ratio: float = 4.0,
+ dropout: float = 0.0,
+ num_classes: int = 3,
+ channels: int = 1,
+ ) -> None:
+ """
+ Initialize the Swin Classifier.
+
+ Args:
+ image_size: Input image size. The Swin Transformer from torchvision
+ expects a fixed input size.
+ patch_size: Patch size for the transformer.
+ embed_dim: Embedding dimension.
+ depths: Number of layers in each stage.
+ num_heads: Number of attention heads in each stage.
+ window_size: Window size for self-attention.
+ mlp_ratio: Ratio of MLP hidden dim to embedding dim.
+ dropout: Dropout rate.
+ num_classes: Number of output classes.
+ channels: Number of input channels (1 for grayscale, 3 for RGB).
+
+ """
+ super().__init__()
+
+ # Swin Transformer Backbone
+ self.swin = SwinTransformer(
+ patch_size=list(patch_size),
+ embed_dim=embed_dim,
+ depths=depths,
+ num_heads=num_heads,
+ window_size=window_size,
+ mlp_ratio=mlp_ratio,
+ dropout=dropout,
+ num_classes=num_classes, # This will be replaced, but required
+ )
+
+ # Modify the first layer for the correct number of input channels
+ self.swin.features[0][0] = nn.Conv2d(
+ channels,
+ embed_dim,
+ kernel_size=patch_size,
+ stride=patch_size,
+ )
+
+ # Replace the final classification head
+ feature_dim = self.swin.head.in_features
+ self.swin.head = nn.Linear(feature_dim, num_classes)
+
+ def forward(self, x: torch.Tensor, return_logits: bool = True) -> torch.Tensor:
+ """
+ Forward pass for classification.
+
+ Args:
+ x: Input tensor of shape (B, C, H, W).
+ return_logits: If True, returns raw logits. If False, returns
+ probabilities after applying softmax.
+
+ Returns:
+ Tensor of shape (B, num_classes) containing logits or probabilities.
+
+ """
+ output = self.swin(x)
+
+ # this is added so that mypy does not complain :/
+ assert isinstance(output, torch.Tensor)
+
+ if return_logits:
+ return output
+
+ return torch.softmax(output, dim=1)
diff --git a/auto_ml/models/swin/segmentation.py b/auto_ml/models/swin/segmentation.py
new file mode 100644
index 0000000..fc420f8
--- /dev/null
+++ b/auto_ml/models/swin/segmentation.py
@@ -0,0 +1,108 @@
+from typing import List
+
+import torch.nn as nn
+from torchvision.models.swin_transformer import SwinTransformer
+
+
+class SwinSegmentation(nn.Module):
+ """Swin Segmentation Model."""
+
+ def __init__( # noqa: D107
+ self,
+ patch_size: List[int] = [4, 4],
+ embed_dim: int = 96,
+ depths: List[int] = [2, 2, 6, 2],
+ num_heads: List[int] = [3, 6, 12, 24],
+ window_size: List[int] = [7, 7],
+ mlp_ratio: float = 4.0,
+ dropout: float = 0.0,
+ num_classes: int = 3,
+ channels: int = 1,
+ ) -> None:
+ super().__init__()
+
+ # 1. Swin Transformer Backbone (Customizable)
+ self.swin = SwinTransformer(
+ patch_size=patch_size,
+ embed_dim=embed_dim,
+ depths=depths,
+ num_heads=num_heads,
+ window_size=window_size,
+ mlp_ratio=mlp_ratio,
+ dropout=dropout,
+ )
+
+ # Modify the first layer to accept 'channels' input (default 1 for grayscale)
+ # Original: Conv2d(3, embed_dim, kernel_size=(4, 4), stride=(4, 4)) # noqa: E501, ERA001
+ # Note: self.swin.features[0][0] is the first patch embedding conv
+
+ # We need to recreate it because we can't just change input channels if we want
+ # to be clean, but technically we can just replace the module.
+ original_first_layer = self.swin.features[0][0]
+ self.swin.features[0][0] = nn.Conv2d(
+ channels,
+ embed_dim,
+ kernel_size=original_first_layer.kernel_size,
+ stride=original_first_layer.stride,
+ )
+
+ # Determine feature dimension automatically
+ # Swin backbone final layer is 'head' (Linear), but we use features.
+ # The output of features() is (B, H/32, W/32, last_dim)
+ # last_dim is typically embed_dim * 2^(num_stages-1)
+ # For Swin-T: 96 * 8 = 768
+ self.dim = embed_dim * (2 ** (len(depths) - 1))
+
+ # 2. Adapter (1/32 -> 1/16) to match ViT decoder input expectation
+ # We need to upsample from H/32 to H/16
+ self.adapter = nn.Sequential(
+ nn.Conv2d(self.dim, self.dim, kernel_size=3, padding=1),
+ nn.BatchNorm2d(self.dim),
+ nn.ReLU(),
+ nn.Upsample(scale_factor=2, mode="bilinear", align_corners=False),
+ )
+
+ # 3. CNN Decoder (Same as ViT)
+ # Input: (B, Dim, H/16, W/16) -> (B, 32, 32) (if Image=512)
+
+ self.decoder = nn.Sequential(
+ # Block 1: 32x32 -> 64x64
+ nn.Conv2d(self.dim, 512, kernel_size=3, padding=1),
+ nn.BatchNorm2d(512),
+ nn.ReLU(),
+ nn.Upsample(scale_factor=2, mode="bilinear", align_corners=False),
+ # Block 2: 64x64 -> 128x128
+ nn.Conv2d(512, 256, kernel_size=3, padding=1),
+ nn.BatchNorm2d(256),
+ nn.ReLU(),
+ nn.Upsample(scale_factor=2, mode="bilinear", align_corners=False),
+ # Block 3: 128x128 -> 256x256
+ nn.Conv2d(256, 128, kernel_size=3, padding=1),
+ nn.BatchNorm2d(128),
+ nn.ReLU(),
+ nn.Upsample(scale_factor=2, mode="bilinear", align_corners=False),
+ # Block 4: 256x256 -> 512x512
+ nn.Conv2d(128, 64, kernel_size=3, padding=1),
+ nn.BatchNorm2d(64),
+ nn.ReLU(),
+ nn.Upsample(scale_factor=2, mode="bilinear", align_corners=False),
+ # Final Projection
+ nn.Conv2d(64, num_classes, kernel_size=1),
+ )
+
+ def forward(self, x): # noqa: ANN001, ANN201, D102
+ # Extract features
+ # Swin feature extractor returns (B, H/32, W/32, Dim)
+ features = self.swin.features(x)
+
+ # Permute to (B, Dim, H/32, W/32)
+ features = features.permute(0, 3, 1, 2).contiguous()
+
+ # Adapt to 1/16 resolution
+ # (B, Dim, H/32, W/32) -> (B, Dim, H/16, W/16)
+ x = self.adapter(features)
+
+ # Decode
+ x = self.decoder(x)
+
+ return x
diff --git a/auto_ml/models/vit/__init__.py b/auto_ml/models/vit/__init__.py
new file mode 100644
index 0000000..1d9c4df
--- /dev/null
+++ b/auto_ml/models/vit/__init__.py
@@ -0,0 +1,4 @@
+from auto_ml.models.vit.classification import ViTClassification
+from auto_ml.models.vit.segmentation import ViTSegmentation
+
+__all__ = ["ViTClassification", "ViTSegmentation"]
diff --git a/auto_ml/models/vit/classification.py b/auto_ml/models/vit/classification.py
new file mode 100644
index 0000000..3b70f64
--- /dev/null
+++ b/auto_ml/models/vit/classification.py
@@ -0,0 +1,148 @@
+import torch
+import torch.nn as nn
+
+
+class ViTClassification(nn.Module):
+ """
+ Vision Transformer Classifier for image classification.
+
+ Uses a CLS token approach where a learnable classification token is
+ prepended to the patch sequence. The transformer processes all tokens,
+ and the final CLS token representation is used for classification.
+ """
+
+ def __init__(
+ self,
+ image_size: int = 512,
+ patch_size: int = 16,
+ num_classes: int = 3,
+ dim: int = 768,
+ depth: int = 12,
+ heads: int = 12,
+ mlp_dim: int = 3072,
+ channels: int = 1,
+ dropout: float = 0.1,
+ emb_dropout: float = 0.1,
+ ) -> None:
+ """
+ Initialize the ViT Classifier.
+
+ Args:
+ image_size: Input image size (must be divisible by patch_size).
+ patch_size: Size of each patch.
+ num_classes: Number of output classes.
+ dim: Transformer embedding dimension.
+ depth: Number of transformer encoder layers.
+ heads: Number of attention heads.
+ mlp_dim: Dimension of the MLP feedforward layer.
+ channels: Number of input channels (1 for grayscale, 3 for RGB).
+ dropout: Dropout rate in transformer and classification head.
+ emb_dropout: Dropout rate after positional embedding.
+
+ """
+ super().__init__()
+
+ assert image_size % patch_size == 0, (
+ "Image dimensions must be divisible by the patch size."
+ )
+ num_patches = (image_size // patch_size) ** 2
+
+ self.patch_size = patch_size
+ self.image_size = image_size
+ self.num_classes = num_classes
+ self.dim = dim
+
+ # Patch Embedding using Conv2d
+ # This is more robust for MPS/CUDA backward passes than Rearrange+View
+ self.patch_embed = nn.Conv2d(
+ channels,
+ dim,
+ kernel_size=patch_size,
+ stride=patch_size,
+ )
+
+ # Learnable CLS token for classification
+ self.cls_token = nn.Parameter(torch.randn(1, 1, dim))
+
+ # Positional embedding: +1 for CLS token
+ self.pos_embedding = nn.Parameter(torch.randn(1, num_patches + 1, dim))
+ self.dropout = nn.Dropout(emb_dropout)
+
+ # Transformer Encoder
+ encoder_layer = nn.TransformerEncoderLayer(
+ d_model=dim,
+ nhead=heads,
+ dim_feedforward=mlp_dim,
+ dropout=dropout,
+ batch_first=True,
+ )
+ self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=depth)
+
+ # Classification head
+ self.classifier = nn.Sequential(
+ nn.LayerNorm(dim),
+ nn.Linear(dim, dim),
+ nn.GELU(),
+ nn.Dropout(dropout),
+ nn.Linear(dim, num_classes),
+ )
+
+ # Softmax for probability distribution output
+ self.softmax = nn.Softmax(dim=1)
+
+ def forward(
+ self,
+ x: torch.Tensor,
+ return_logits: bool = False,
+ ) -> torch.Tensor:
+ """
+ Forward pass of the ViT Classifier.
+
+ Args:
+ x: Input tensor of shape (B, C, H, W). H and W must equal image_size.
+ return_logits: If True, return raw logits instead of probabilities.
+ Use True when training with CrossEntropyLoss.
+
+ Returns:
+ Tensor of shape (B, num_classes) with probabilities (or logits if
+ return_logits=True). Probabilities sum to 1 along dim=1.
+
+ """
+ batch_size = x.shape[0]
+
+ # 1. Patch Embedding
+ # (B, C, H, W) -> (B, Dim, H/P, W/P)
+ x = self.patch_embed(x)
+
+ # Flatten: (B, Dim, H/P, W/P) -> (B, Dim, NumPatches)
+ x = x.flatten(2)
+
+ # Transpose: (B, Dim, NumPatches) -> (B, NumPatches, Dim)
+ x = x.transpose(1, 2).contiguous()
+
+ # 2. Prepend CLS token
+ # Expand cls_token for batch: (1, 1, Dim) -> (B, 1, Dim)
+ cls_tokens = self.cls_token.expand(batch_size, -1, -1)
+ # Concatenate: (B, 1, Dim) + (B, NumPatches, Dim) -> (B, NumPatches+1, Dim)
+ x = torch.cat((cls_tokens, x), dim=1)
+
+ # 3. Add Positional Embedding
+ x = x + self.pos_embedding
+ x = self.dropout(x)
+
+ # 4. Transformer Encoder
+ x = self.transformer(x) # (B, NumPatches+1, Dim)
+
+ # 5. Extract CLS token output
+ cls_output = x[:, 0] # (B, Dim)
+
+ # 6. Classification
+ x = self.classifier(cls_output) # (B, num_classes)
+
+ # Return probabilities or logits
+ if return_logits:
+ return x
+
+ sm: torch.Tensor = self.softmax(x)
+
+ return sm
diff --git a/auto_ml/models/vit/segmentation.py b/auto_ml/models/vit/segmentation.py
new file mode 100644
index 0000000..6f25c6a
--- /dev/null
+++ b/auto_ml/models/vit/segmentation.py
@@ -0,0 +1,114 @@
+import torch
+import torch.nn as nn
+
+
+class ViTSegmentation(nn.Module):
+ """Vision Transformer Segmentation Model."""
+
+ def __init__( # noqa: D107
+ self,
+ image_size: int = 512,
+ patch_size: int = 16,
+ num_classes: int = 3,
+ dim: int = 768,
+ depth: int = 12,
+ heads: int = 12,
+ mlp_dim: int = 3072,
+ channels: int = 1,
+ dropout: float = 0.1,
+ emb_dropout: float = 0.1,
+ ) -> None:
+ super().__init__()
+
+ assert image_size % patch_size == 0, (
+ "Image dimensions must be divisible by the patch size."
+ )
+ num_patches = (image_size // patch_size) ** 2
+
+ self.patch_size = patch_size
+ self.image_size = image_size
+ self.num_classes = num_classes
+ self.dim = dim
+
+ # Patch Embedding using Conv2d
+ # This is more robust for MPS/CUDA backward passes than Rearrange+View
+ self.patch_embed = nn.Conv2d(
+ channels,
+ dim,
+ kernel_size=patch_size,
+ stride=patch_size,
+ )
+
+ self.pos_embedding = nn.Parameter(torch.randn(1, num_patches, dim))
+ self.dropout = nn.Dropout(emb_dropout)
+
+ # Transformer Encoder
+ encoder_layer = nn.TransformerEncoderLayer(
+ d_model=dim,
+ nhead=heads,
+ dim_feedforward=mlp_dim,
+ dropout=dropout,
+ batch_first=True,
+ )
+ self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=depth)
+
+ # --- CNN Decoder (Progressive Upsampling) ---
+ # Feature map size: (H/P, W/P) -> (512/16, 512/16) = (32, 32)
+
+ self.decoder = nn.Sequential(
+ # Block 1: 32x32 -> 64x64
+ nn.Conv2d(dim, 512, kernel_size=3, padding=1),
+ nn.BatchNorm2d(512),
+ nn.ReLU(),
+ nn.Upsample(scale_factor=2, mode="bilinear", align_corners=False),
+ # Block 2: 64x64 -> 128x128
+ nn.Conv2d(512, 256, kernel_size=3, padding=1),
+ nn.BatchNorm2d(256),
+ nn.ReLU(),
+ nn.Upsample(scale_factor=2, mode="bilinear", align_corners=False),
+ # Block 3: 128x128 -> 256x256
+ nn.Conv2d(256, 128, kernel_size=3, padding=1),
+ nn.BatchNorm2d(128),
+ nn.ReLU(),
+ nn.Upsample(scale_factor=2, mode="bilinear", align_corners=False),
+ # Block 4: 256x256 -> 512x512
+ nn.Conv2d(128, 64, kernel_size=3, padding=1),
+ nn.BatchNorm2d(64),
+ nn.ReLU(),
+ nn.Upsample(scale_factor=2, mode="bilinear", align_corners=False),
+ # Final Projection
+ nn.Conv2d(64, num_classes, kernel_size=1),
+ )
+
+ def forward(self, img): # noqa: ANN001, ANN201, D102
+ # img: (B, C, H, W) # noqa: ERA001
+
+ # 1. Patch Embedding
+ # (B, C, H, W) -> (B, Dim, H/P, W/P)
+ x = self.patch_embed(img)
+
+ # Flatten: (B, Dim, H/P, W/P) -> (B, Dim, NumPatches)
+ x = x.flatten(2)
+
+ # Transpose: (B, Dim, NumPatches) -> (B, NumPatches, Dim)
+ x = x.transpose(1, 2).contiguous()
+
+ # 2. Add Positional Embedding
+ x += self.pos_embedding
+ x = self.dropout(x)
+
+ # 3. Transformer Encoder
+ x = self.transformer(x) # (B, NumPatches, Dim)
+
+ # 4. Reshape for CNN Decoder
+ # (B, NumPatches, Dim) -> (B, Dim, NumPatches)
+ x = x.transpose(1, 2).contiguous()
+
+ # (B, Dim, NumPatches) -> (B, Dim, H/P, W/P)
+ h = w = self.image_size // self.patch_size # 32
+ x = x.reshape(x.shape[0], self.dim, h, w)
+
+ # 5. Decode
+ x = self.decoder(x) # (B, NumClasses, ImageSize, ImageSize)
+
+ return x
diff --git a/docs/report/ML_report.pdf b/docs/report/ML_report.pdf
new file mode 100644
index 0000000..5b455c7
Binary files /dev/null and b/docs/report/ML_report.pdf differ
diff --git a/docs/report/heatmap.png b/docs/report/heatmap.png
new file mode 100644
index 0000000..298d34e
Binary files /dev/null and b/docs/report/heatmap.png differ
diff --git a/docs/report/loss_vs_data_size.png b/docs/report/loss_vs_data_size.png
new file mode 100644
index 0000000..31c477a
Binary files /dev/null and b/docs/report/loss_vs_data_size.png differ
diff --git a/docs/report/main.tex b/docs/report/main.tex
new file mode 100644
index 0000000..b0cecac
--- /dev/null
+++ b/docs/report/main.tex
@@ -0,0 +1,923 @@
+\documentclass[onecolumn, 12pt , journal]{IEEEtran}
+
+\ifCLASSINFOpdf
+\usepackage[pdftex]{graphicx}
+ % declare the path(s) where your graphic files are
+\graphicspath{{../pdf/}{../jpeg/}}
+ % and their extensions so you won't have to specify these with
+ % every instance of \includegraphics
+\DeclareGraphicsExtensions{.pdf,.jpeg,.png}
+\else
+ % or other class option (dvipsone, dvipdf, if not using dvips). graphicx
+ % will default to the driver specified in the system graphics.cfg if no
+ % driver is specified.
+ % \usepackage[dvips]{graphicx}
+ % declare the path(s) where your graphic files are
+ % \graphicspath{{../eps/}}
+ % and their extensions so you won't have to specify these with
+ % every instance of \includegraphics
+ % \DeclareGraphicsExtensions{.eps}
+\fi
+
+\usepackage[spanish]{babel} % activa idioma español
+\usepackage{titlesec}
+\setcounter{secnumdepth}{5}
+\addto\captionsspanish{%
+ \renewcommand{\tablename}{Tabla}%
+}
+
+% *** MATH PACKAGES ***
+\usepackage{amsmath}
+\usepackage{amssymb}
+\usepackage{amsthm}
+\interdisplaylinepenalty=2500
+% *** SPECIALIZED LIST PACKAGES ***
+\usepackage{algorithmic}
+% *** ALIGNMENT PACKAGES ***
+\usepackage{array}
+% *** SUBFIGURE PACKAGES ***
+\ifCLASSOPTIONcompsoc
+ \usepackage[caption=false,font=normalsize,labelfont=sf,textfont=sf]{subfig}
+\else
+ \usepackage[caption=false,font=footnotesize]{subfig}
+\fi
+% *** FLOAT PACKAGES ***
+%\usepackage{fixltx2e}
+%\usepackage{stfloats}
+%\usepackage{dblfloatfix}
+
+%\ifCLASSOPTIONcaptionsoff
+% \usepackage[nomarkers]{endfloat}
+% \let\MYoriglatexcaption\caption
+% \renewcommand{\caption}[2][\relax]{\MYoriglatexcaption[#2]{#2}}
+%\fi
+%\let\MYorigsubfloat\subfloat
+%\renewcommand{\subfloat}[2][\relax]{\MYorigsubfloat[]{#2}}
+
+% *** PDF, URL AND HYPERLINK PACKAGES ***
+\usepackage{url}
+\usepackage{float}
+
+\usepackage{booktabs}
+
+% correct bad hyphenation here
+\hyphenation{op-tical net-works semi-conduc-tor}
+\hyphenation{maxi-mizar REINFORCE}
+
+\newtheorem{definition}{Definición}
+\newtheorem{theorem}{Teorema}
+\newtheorem{lemma}{Lema}
+\newtheorem{proposition}{Proposición}
+
+% Corolarios
+\newtheorem{corollary}{Corolario}
+
+% Subparagraph command
+% \newcommand{\subparagraph}[1]{\vspace{1em}\noindent\textbf{#1.}}
+
+\begin{document}
+%
+% paper title
+% Titles are generally capitalized except for words such as a, an, and, as,
+% at, but, by, for, in, nor, of, on, or, the, to and up, which are usually
+% not capitalized unless they are the first or last word of the title.
+% Linebreaks \\ can be used within to get better formatting as desired.
+% Do not put math or special symbols in the title.
+\title{Segmentación de Zonas de Fractura en Imágenes SEM: \\
+ Análisis Comparativo mediante Experimentación Automatizada }
+%
+%
+% author names and IEEE memberships
+% note positions of commas and nonbreaking spaces ( ~ ) LaTeX will not break
+% a structure at a ~ so this keeps an author's name from being broken across
+% two lines.
+% use \thanks{} to gain access to the first footnote area
+% a separate \thanks must be used for each paragraph as LaTeX2e's \thanks
+% was not built to handle multiple paragraphs
+%
+
+\author{
+ Darío Hernández Cubilla,
+ Diego Manuel Viera Martínez,
+ Francisco Préstamo Bernárdez,
+ Jossué Arteche Muñoz,
+ Luis Alejandro Arteaga Morales,
+ Mauricio Sunde Jimenez,
+ Pablo Gómez Vidal
+ }
+
+
+% make the title area
+\maketitle
+% As a general rule, do not put math, special symbols or citations
+% in the abstract or keywords.
+% \begin{abstract}
+% The abstract goes here.
+% \end{abstract}
+
+% Note that keywords are not normally used for peerreview papers.
+% \begin{IEEEkeywords}
+% IEEE, IEEEtran, journal, \LaTeX, paper, template.
+% end{IEEEkeywords}
+
+% \begin{abstract}
+% Se presenta un enfoque sistemático para la detección de zonas frágiles y dúctiles en imágenes obtenidas mediante microscopía electrónica de barrido (SEM), un problema clave en la caracterización de fracturas. Para abordar la selección del modelo óptimo, se implementó un pipeline de AutoML ligero que permite la experimentación ágil con modelos basados en transformers y estrategias de segmentación jerárquica. La viabilidad de estas estrategias se evalúa bajo un marco experimental riguroso que considera explícitamente las limitaciones computacionales y la escasez de datos etiquetados. Los resultados evidencian cómo las técnicas de aumentación de datos y la transferencia de aprendizaje impactan en la calidad de la segmentación, ofreciendo una solución efectiva para el análisis automatizado de materiales.
+% \end{abstract}
+
+\section{Introducción}
+La caracterización de los mecanismos de fractura en materiales es un problema central en la ciencia e ingeniería de materiales, debido a su impacto directo en la evaluación del desempeño mecánico y la confiabilidad estructural. En particular, la identificación de zonas asociadas a comportamientos frágiles y dúctiles a partir de microestructuras proporciona información clave para el análisis del fallo y el diseño de materiales.
+
+Las imágenes obtenidas mediante microscopía electrónica de barrido (SEM) constituyen una de las principales herramientas para el estudio de superficies de fractura y microestructuras. No obstante, el análisis tradicional de este tipo de imágenes se basa en inspección visual experta, lo cual introduce subjetividad, limita la reproducibilidad de los resultados y dificulta su aplicación a grandes volúmenes de datos.
+
+En este contexto, las técnicas de aprendizaje automático y, en particular, los modelos de visión por computador han mostrado un potencial significativo para automatizar la identificación y delimitación de estas zonas en imágenes complejas. Recientemente, modelos basados en transformers, como Vision Transformer (ViT) y Swin Transformer, han alcanzado resultados destacados en tareas de segmentación semántica, posicionándose como herramientas prometedoras para la metalografía cuantitativa. Sin embargo, su aplicación efectiva en este dominio suele estar condicionada por la disponibilidad de datos anotados por expertos y recursos computacionales elevados.
+
+Estas limitaciones motivan la exploración de estrategias alternativas que permitan aprovechar modelos de alto desempeño bajo restricciones de cómputo y datos. En este trabajo se consideran tanto modelos del estado del arte como enfoques propuestos basados en la descomposición espacial de la imagen y el uso de clasificadores preentrenados, con el objetivo de transferir conocimiento desde tareas de clasificación hacia problemas de segmentación.
+
+Adicionalmente, se adopta un diseño de experimentación automatizada (un AutoML bastante simple) para facilitar la selección de modelos y el ajuste de hiperparámetros. Este enfoque simplificado permite evaluar múltiples configuraciones de pipelines de manera eficiente, considerando explícitamente las restricciones impuestas por un entorno de cómputo limitado. El objetivo de este estudio es determinar la estrategia más efectiva para la caracterización automática de zonas frágiles y dúctiles, utilizando esta herramienta experimental para comparar arquitecturas del estado del arte con enfoques alternativos.
+
+% \section{Estado del arte}
+% segmentacion
+% clasificacion
+% data augmentation
+% evaluation
+
+\section{Estado del Arte}
+
+\subsection{Modelos de Segmentación}
+La segmentación de imágenes ha estado dominada históricamente por las Redes Neuronales Convolucionales (CNN) \cite{sakib2019overview}. El modelo \emph{U-Net} es el estándar para arquitecturas de codificador-decodificador con conexiones de salto, permitiendo recuperar detalles espaciales finos \cite{ronneberger2015unet, cao2021swinunet}. En el ámbito de la microscopía electrónica (SEM), modelos como \emph{Mask R-CNN} se utilizan para la segmentación de instancias, permitiendo delimitar partículas individuales incluso en configuraciones de aglomerados \cite{monchot2021titanium, liu2021swin}.
+
+Recientemente, el \emph{Vision Transformer (ViT)} ha demostrado que es posible prescindir de las convoluciones al tratar parches de imagen como secuencias, capturando dependencias globales que las CNN suelen omitir \cite{dosovitskiy2021vit, cao2021swinunet}. No obstante, el ViT presenta una complejidad computacional cuadrática respecto al tamaño de la imagen, lo que dificulta su uso en tareas de predicción densa \cite{liu2021swin}. Para solucionar esto, el \emph{Swin Transformer} introduce mapas de características jerárquicos y un esquema de atención en ventanas desplazadas (\textit{shifted windows}), logrando una complejidad lineal \cite{liu2021swin}. Basado en esto, el modelo \emph{Swin-Unet} propone una arquitectura puramente basada en Transformers con estructura en forma de U para segmentación precisa, sustituyendo las operaciones de convolución por bloques de Swin Transformer \cite{cao2021swinunet}.
+
+\subsection{Clasificación de Imágenes}
+La clasificación de imágenes es fundamental cuando se emplean estrategias de segmentación mediante \textit{quadtrees} o ventanas deslizantes (\textit{sliding windows}). Modelos de CNN como \emph{ResNet} han sido la opción predeterminada debido a sus conexiones residuales que facilitan el entrenamiento profundo \cite{he2016resnet, sakib2019overview}. En microscopía electrónica, se han implementado bloques de \emph{EfficientNetB7} para clasificar morfologías de parches con alta precisión \cite{classification_report}.
+
+Aunque los mecanismos de atención temprana aplicados mediante \textit{sliding windows} presentaban una alta latencia por el acceso costoso a memoria, el enfoque de ventanas locales de \emph{Swin Transformer} optimiza este proceso, permitiendo conexiones entre parches vecinos mediante el desplazamiento de ventanas en capas consecutivas \cite{liu2021swin}.
+
+\subsection{Aumentación de Datos en Imágenes SEM}
+Debido a la escasez de datos etiquetados en SEM, la aumentación es crítica. Las técnicas estándar incluyen transformaciones geométricas como \emph{flipping (volteo), rotaciones de 90º y aleatorias, traslación y escalado} \cite{monchot2021titanium, gaox2024cyclegan}. El estado del arte actual también incluye métodos generativos avanzados, como el uso de \emph{StyleGAN2 con ADA} (Aumentación Adaptativa del Discriminador) para generar microestructuras realistas a partir de conjuntos de datos limitados \cite{lambard2023stylegan}. Asimismo, se han empleado modelos \emph{CycleGAN} para mejorar la calidad de imagen y eliminar el desenfoque en muestras de baja conductividad \cite{gaox2024cyclegan}.
+
+\subsection{Métricas de Evaluación}
+Para cuantificar el desempeño en segmentación, las fuentes destacan cuatro métricas principales \cite{classification_report, cao2021swinunet}:
+\begin{itemize}
+ \item mIoU (Mean Intersection over Union): Mide el solapamiento entre la predicción y el ground truth.
+ \item Dice Coefficient (DSC): Equivalente al F1-Score en segmentación binaria, evalúa la similitud entre conjuntos.
+ \item HD95 (Hausdorff Distance): Mide la distancia máxima entre los bordes de la predicción y la máscara real, indicando precisión en el contorno.
+ \item AP (Average Precision): Utilizada comúnmente en segmentación de instancias para evaluar la detección de objetos individuales.
+\end{itemize}
+
+
+\section{Metodología Experimental y Pipeline de Experimentación}
+La complejidad inherente a la segmentación de fracturas, sumada a la escasez de imágenes anotadas, exige un rigor experimental superior al convencional. Para abordar esto, se diseñó e implementó un framework de experimentación automatizada (\textit{AutoML custom y simple}) que permite la evaluación sistemática y reproducible de hipótesis de modelado.
+Este enfoque trasciende la simple búsqueda de hiperparámetros; constituye una metodología para aislar variables y cuantificar el impacto real de cada componente (arquitectura vs. estrategia de datos) en el desempeño final.
+
+\subsection{Arquitectura del Flujo Experimental}
+El proceso de investigación se estructuró mediante un pipeline lineal estandarizado, que garantiza que todas las estrategias compitan en igualdad de condiciones. El flujo se define formalmente como:
+
+\begin{equation}
+ \mathcal{P} : \text{Espacio de Datos} \xrightarrow{\phi} \text{Entrenamiento} \xrightarrow{\psi} \text{Evaluación}
+\end{equation}
+
+Donde el orquestador del sistema gestiona la ejecución iterativa de este pipeline, permutando las configuraciones de datos ($\phi$) y modelos ($\psi$) para cubrir exhaustivamente el espacio de búsqueda definido.
+
+\subsection{Estrategias de Aumentación y Control de Variabilidad}
+El primer componente del sistema, el \textit{Data Node}, tiene la responsabilidad crítica de definir el régimen de datos. Su función no es solo aplicar transformaciones, sino asegurar la integridad del protocolo experimental.
+El sistema genera particiones de datos (e.g., k-fold cross-validation) y aplica políticas de aumentación sintética de manera determinista. Esto permite desacoplar los efectos de la variabilidad del muestreo de las mejoras algorítmicas, permitiendo responder preguntas como: \textit{¿Es la mejora en precisión atribuible al modelo Swin Transformer o a la introducción de mas ejemplos entrenantes mediante data augmentation en el entrenamiento?}
+
+\subsection{Homogeneización del Entrenamiento de Modelos}
+Para garantizar la comparabilidad entre arquitecturas dispares (e.g., Transformers vs. Algoritmos de Partición Recursiva) Todos los modelos deben implementar métodos estandarizados \texttt{train()} y \texttt{evaluate()}, lo que desacopla la lógica interna del algoritmo del flujo experimental.
+
+El componente \texttt{ModelNode} orquesta la ejecución siguiendo un principio de aislamiento estricto. Para cada partición del dataset ($k$-fold). Esto asegura una inicialización "tabula rasa" para cada experimento, eliminando cualquier riesgo de fuga de pesos o sesgo residual de entrenamientos previos. Así, la evaluación refleja puramente la capacidad del modelo para aprender de los datos suministrados en esa iteración específica.
+
+
+\subsection{Evaluación Imparcial y Persistencia}
+La etapa final (\textit{Evaluator Node}) opera como un auditor independiente. Recibe las predicciones generadas y calcula métricas de desempeño sin acceso a la lógica que las produjo, asegurando una "evaluación ciega".
+Adicionalmente, el sistema implementa un mecanismo de persistencia granular que registra no solo las métricas finales, sino también los tiempos de cómputo y las configuraciones exactas. Esto habilita un análisis posterior profundo sobre el \textit{trade-off} entre costo computacional y precisión, fundamental para proponer soluciones viables en entornos de recursos limitados.
+
+En conclusión, esta metodología instrumentaliza el método científico: permite plantear hipótesis sobre arquitecturas y datos, y validarlas o refutarlas mediante evidencia empírica generada en un entorno controlado.
+
+
+
+\section{Descripción del dataset}
+% - División train/test
+% - Origen de las imágenes SEM
+% - Dimensiones (512x512)
+% - Clases de segmentación (frágil, dúctil, mixto)
+% - Distribución de clases
+A continuación se proporciona información sobre los datasets utilizados.
+\subsection{Dataset de segmentación}
+
+El dataset principal utilizado para la tarea de segmentación está compuesto por 94 imágenes obtenidas mediante microscopía electrónica de barrido (SEM) utilizando un microscopio Vega 3 Scan. Cada imagen cuenta con su correspondiente máscara de segmentación a nivel de píxel.
+
+Las máscaras de referencia delimitan dos morfologías de fractura fundamentales: zonas de comportamiento frágil y zonas de comportamiento dúctil. A nivel global, el conjunto presenta un desbalance significativo, con 73 imágenes donde predomina la fractura dúctil y 21 imágenes con mayor presencia de fractura frágil. Esta distribución refleja la naturaleza del material analizado y constituye un desafío adicional para la generalización del modelo.
+
+Las imágenes originales presentan tamaños variables, lo cual motivó la adopción de un preprocesamiento de redimensionamiento a una resolución fija de $512 \times 512$ píxeles para los modelos basados en transformers y para el análisis estadístico espacial.
+
+Con el objetivo de caracterizar la distribución espacial de las clases, se construyó el mapa de calor de la Figura~\ref{fig:heatmap_ductil} agregando las máscaras redimensionadas. Dado que las clases son excluyentes, el mapa de calor representa únicamente la frecuencia de la clase dúctil. Este análisis permitió identificar regiones con mayor recurrencia de comportamiento dúctil, así como zonas donde la presencia de material frágil es más probable, aportando una visión global de la estructura espacial del dataset.
+
+\begin{figure}[H]
+ \centering
+ \includegraphics{heatmap.png}
+ \caption{Mapa de calor de la distribución espacial de las clases dúctil y frágil, obtenido a partir de la superposición de las máscaras de segmentación redimensionadas a $512 \times 512$ píxeles.}
+ \label{fig:heatmap_ductil}
+\end{figure}
+
+
+\subsection{Dataset externo de clasificación}
+
+Para las tareas de clasificación, en particular aquellas asociadas a los enfoques basados en Quadtree y Sliding Window, se utilizó un dataset externo independiente del conjunto de segmentación. Este dataset fue empleado para entrenar los clasificadores auxiliares encargados de etiquetar regiones completas de imagen.
+
+El conjunto de datos corresponde al dataset de imágenes SEM publicado por Campari~\cite{campari_2025_15510590}, el cual contiene imágenes de microscopía electrónica anotadas a nivel de imagen para tareas de clasificación. El uso de este dataset permitió desacoplar el entrenamiento de los clasificadores del proceso de segmentación, habilitando estrategias de transferencia de aprendizaje desde clasificación hacia segmentación regional.
+
+Cabe destacar que este dataset externo no fue utilizado en ninguna etapa del entrenamiento de los modelos de segmentación directa, ni en la construcción del conjunto de prueba, garantizando así la independencia entre las fuentes de datos.
+
+
+\section{Estrategias de Segmentación}
+La segmentación de zonas frágiles y dúctiles se aborda mediante un conjunto de estrategias complementarias que responden a distintos niveles de complejidad y granularidad en la representación de la imagen. En primer lugar, se consideran arquitecturas basadas en Vision Transformers y Swin Transformers, seleccionadas por su capacidad para modelar dependencias espaciales de largo alcance y por su consolidación como enfoques de referencia en tareas de segmentación densa. Estos modelos permiten aprender representaciones globales y jerárquicas directamente a partir de la imagen completa, proporcionando una base sólida para la identificación precisa de regiones con comportamiento mecánico diferenciado.
+
+De forma complementaria, se exploran estrategias de segmentación indirecta que reutilizan conocimiento previamente adquirido en tareas de clasificación. En este enfoque, la información semántica extraída por modelos entrenados para discriminar regiones frágiles y dúctiles se emplea como guía para definir esquemas de segmentación basados en particiones espaciales adaptativas. En particular, se consideran métodos que descomponen la imagen en regiones locales cuya resolución y extensión se ajustan dinámicamente en función de la complejidad visual y la confianza de la clasificación, permitiendo una transición controlada entre análisis global y local.
+
+Este planteamiento unifica modelos de segmentación directa y estrategias guiadas por clasificación bajo un mismo marco conceptual, facilitando la comparación entre enfoques de distinta naturaleza y sentando las bases para la incorporación progresiva de métodos alternativos de particionado espacial en subsecciones posteriores.
+
+\subsection{Vision Transformer (ViT)}
+% - Arquitectura general
+% - División en patches
+% - Mecanismo de atención
+% - Adaptación para segmentación semántica
+% - Hiperparámetros utilizados
+
+La estrategia de segmentación basada en \emph{Vision Transformer} (ViT) se diseñó siguiendo un enfoque modular que separa explícitamente la definición arquitectónica del modelo del proceso de entrenamiento y evaluación. Esta decisión metodológica permite desacoplar los aspectos conceptuales del modelo de los detalles operativos, facilitando su integración en el framework de \emph{AutoML} y asegurando comparabilidad con otras estrategias de segmentación.
+
+\subsubsection{Arquitectura encoder--decoder}
+
+El modelo adopta una arquitectura de tipo \emph{encoder--decoder}, donde el encoder se basa en un Transformer y el decoder en una red neuronal convolucional. Esta combinación permite explotar la capacidad del mecanismo de autoatención para capturar dependencias globales, mientras que el decoder convolucional reconstruye la información espacial necesaria para una segmentación densa a nivel de píxel.
+
+El encoder transforma la imagen de entrada en una secuencia de representaciones latentes mediante un proceso de partición en parches no solapados. Cada parche es proyectado a un espacio de alta dimensionalidad, generando una secuencia de \emph{embeddings} que reemplaza la representación basada en píxeles. Dado que los Transformers carecen de noción explícita de orden espacial, se incorporan codificaciones posicionales aprendibles, las cuales preservan la información relativa a la localización de cada parche en la imagen original.
+
+Posteriormente, esta secuencia es procesada por múltiples capas de autoatención, donde cada parche puede intercambiar información con todos los demás. Este mecanismo permite construir representaciones con contexto global, capturando relaciones de largo alcance.
+
+\subsubsection{Decoder convolucional y reconstrucción espacial}
+
+La salida del encoder, expresada como una secuencia de vectores latentes, es reestructurada nuevamente en forma de mapa bidimensional de características. A partir de esta representación de baja resolución espacial, el decoder convolucional realiza una reconstrucción progresiva hasta alcanzar la resolución original de la imagen.
+
+Este proceso se lleva a cabo mediante una serie de etapas de refinamiento y reescalado, en las cuales se combinan convoluciones para la extracción local de características con operaciones de interpolación para el aumento gradual de la resolución. En la etapa final, una proyección lineal por píxel permite obtener, para cada clase, un mapa de activación que representa la pertenencia de cada píxel a las distintas regiones de interés.
+
+\subsubsection{Integración en el esquema experimental}
+
+La arquitectura ViT descrita se integra en el diseño experimental como un módulo autocontenido.
+Durante el entrenamiento, se emplea un esquema supervisado estándar para segmentación multiclase, utilizando funciones de pérdida adecuadas para este tipo de problema y validación sobre datos no vistos para monitorear el desempeño. En la fase de inferencia, las salidas continuas del modelo son convertidas en máscaras discretas mediante una asignación por máxima activación, produciendo mapas de segmentación directamente comparables con las máscaras de referencia.
+
+Esta estrategia combina la capacidad de los Vision Transformers para modelar contexto global con la precisión espacial de decodificadores convolucionales, resultando adecuada para problemas de segmentación.
+
+\subsection{Swin Transformer}
+
+El segmentador basado en \emph{Swin Transformer} se desarrolló siguiendo el mismo principio aplicado al modelo ViT: una separación estricta entre la arquitectura del modelo y la lógica de entrenamiento y evaluación. Esta coherencia de diseño garantiza modularidad, facilita la comparación experimental y permite integrar el modelo de manera transparente dentro del framework de AutoML.
+
+\subsubsection{Arquitectura encoder--decoder}
+
+Al igual que en el caso del ViT, el modelo adopta una arquitectura de tipo \emph{encoder--decoder}. Sin embargo, el encoder se basa en el Swin Transformer, una evolución de los Transformers para visión computacional que introduce propiedades jerárquicas y de localidad, tradicionalmente asociadas a las redes convolucionales.
+
+El encoder genera representaciones jerárquicas a múltiples escalas espaciales. A partir de la imagen de entrada, el modelo construye progresivamente un conjunto de mapas de características con resoluciones decrecientes, lo que permite capturar información tanto local como global. Este enfoque multiescala resulta especialmente adecuado para tareas de segmentación, donde coexisten detalles finos y estructuras de mayor tamaño.
+
+El mecanismo central del Swin Transformer es la autoatención restringida a ventanas locales, lo que reduce significativamente la complejidad computacional en comparación con la autoatención global. Para evitar la pérdida de información entre regiones disjuntas, estas ventanas se desplazan de manera alternada entre capas consecutivas, permitiendo que la información fluya entre distintas zonas de la imagen. De este modo, el modelo combina eficiencia computacional con una capacidad efectiva de modelar dependencias de largo alcance.
+
+Adicionalmente, la arquitectura fue adaptada para operar sobre imágenes en escala de grises, lo que asegura compatibilidad con el tipo de datos empleado en este trabajo sin alterar los principios fundamentales del modelo.
+
+\subsubsection{Adaptación de resolución y decoder compartido}
+
+El encoder basado en Swin Transformer produce su representación final a una resolución espacial inferior a la utilizada por el decoder convolucional. Para resolver esta discrepancia, se introduce un módulo de adaptación intermedio que reescala las características extraídas por el encoder hasta la resolución esperada por el decoder.
+
+Una vez ajustada la resolución, las características son procesadas por el mismo decoder convolucional utilizado en el modelo basado en ViT. Este decoder realiza una reconstrucción progresiva de la segmentación mediante etapas sucesivas de refinamiento y aumento de resolución, culminando en un mapa de segmentación denso a nivel de píxel. El uso de un decoder compartido evita la duplicación de componentes, mejora la mantenibilidad del sistema y asegura que las diferencias observadas en los resultados se deban principalmente al encoder y no a variaciones en la fase de reconstrucción.
+
+\subsubsection{Entrenamiento, regularización y evaluación}
+
+El proceso de entrenamiento del modelo Swin incorpora monitoreo constante del desempeño.
+Un aspecto distintivo de esta estrategia es la incorporación de un mecanismo de \emph{early stopping} durante el entrenamiento. Este mecanismo monitoriza el desempeño sobre el conjunto de validación y detiene el proceso de optimización cuando no se observan mejoras durante un número predefinido de iteraciones consecutivas. De este modo, se reduce el riesgo de sobreajuste y se selecciona automáticamente el estado del modelo con mejor capacidad de generalización.
+
+En la fase de inferencia, el modelo genera mapas de activación multiclase que son transformados en máscaras de segmentación discretas de forma análoga al modelo ViT, asegurando consistencia.
+
+El segmentador basado en Swin Transformer combina eficiencia computacional, representación jerárquica multiescala y mecanismos de regularización explícitos, constituyendo una alternativa de alto nivel dentro del estado del arte para segmentación semántica en imágenes complejas.
+
+
+\subsection{Descomposición quadtree}
+
+El enfoque de segmentación basado en quadtree difiere de manera fundamental de los modelos neuronales de tipo extremo a extremo. En lugar de aprender directamente una correspondencia píxel a píxel, este método se formula como un algoritmo recursivo de tipo \emph{divide y vencerás}, que combina un modelo de clasificación independiente con una estrategia adaptativa de partición espacial. Esta aproximación permite razonar sobre regiones completas de la imagen y ajustar dinámicamente el nivel de detalle de la segmentación en función de la complejidad local.
+
+\subsubsection{Principio de divide y vencerás mediante quadtree}
+
+El algoritmo opera de manera recursiva sobre regiones rectangulares de la imagen. Inicialmente, la imagen completa se considera como una única región. Para cada región analizada, se consulta a un clasificador externo con el fin de estimar la clase predominante y un nivel de confianza asociado a dicha predicción.
+
+Si la confianza supera un umbral predefinido, se asume que la región es suficientemente homogénea y se asigna la clase predicha a todos los píxeles que la componen. En caso contrario, el algoritmo interpreta que la región contiene información heterogénea o ambigua y procede a subdividirla en cuatro cuadrantes de igual tamaño. Este proceso se repite de forma independiente para cada subregión.
+
+La recursión se detiene cuando se cumple alguna de las siguientes condiciones: la confianza del clasificador es suficiente, la región alcanza un tamaño mínimo que impide una subdivisión significativa, o se llega a una profundidad máxima de recursión. Como resultado, el método genera regiones extensas y uniformes en zonas simples de la imagen, y particiones más finas en áreas con mayor complejidad estructural.
+
+\subsubsection{Separación entre segmentación y clasificación}
+
+Un rasgo central de este enfoque es la separación explícita entre la lógica de segmentación y el modelo de clasificación utilizado para tomar decisiones locales. El algoritmo quadtree se limita a gestionar la recursión y la asignación espacial de etiquetas, mientras que el clasificador actúa como un componente intercambiable encargado de evaluar regiones de la imagen.
+
+Esta separación permite desacoplar completamente la estrategia de partición del método concreto de clasificación empleado. En consecuencia, es posible evaluar distintos clasificadores bajo un mismo esquema de segmentación sin modificar el algoritmo quadtree, lo que aporta flexibilidad experimental y facilita el análisis comparativo de modelos.
+
+\subsubsection{Proceso de ajuste de parámetros mediante metaheurísticas}
+
+A diferencia de los modelos neuronales clásicos, el proceso de entrenamiento de este segmentador no se basa en retropropagación de gradientes. En su lugar, el ajuste se centra en la optimización de los hiperparámetros que controlan el comportamiento del algoritmo recursivo, tales como el umbral de confianza, el tamaño mínimo de región y la profundidad máxima de subdivisión.
+
+Este ajuste se plantea como un problema de optimización global y se resuelve mediante una metaheurística de recocido simulado (\emph{simulated annealing}). El procedimiento explora el espacio de hiperparámetros evaluando configuraciones candidatas sobre un conjunto de validación, aceptando tanto mejoras directas como, de manera probabilística, configuraciones subóptimas en etapas tempranas de la búsqueda. Este mecanismo permite escapar de óptimos locales y favorece una exploración más amplia del espacio de soluciones.
+
+Al finalizar el proceso, el algoritmo conserva la configuración de hiperparámetros que produjo el mejor desempeño global, la cual se fija para la generación de resultados finales.
+
+\subsubsection{Generación y evaluación de las segmentaciones}
+
+Durante la fase de evaluación, el algoritmo se aplica de manera independiente a cada imagen del conjunto de datos. A partir de una máscara inicialmente vacía, el proceso recursivo va asignando etiquetas a las distintas regiones según las decisiones tomadas en cada nivel de la jerarquía quadtree. El resultado final es una máscara de segmentación completa, construida de forma adaptativa en función de la complejidad local de la imagen.
+
+Las máscaras predichas se comparan posteriormente con las máscaras de referencia mediante las métricas definidas en el framework de evaluación, lo que permite analizar cuantitativamente el desempeño del método y contrastarlo con los enfoques basados en redes neuronales profundas.
+
+\subsection{Segmentación basada en ventanas deslizantes}
+
+El modelo de segmentación por \textit{Sliding Window} implementa una estrategia que divide la imagen de entrada en múltiples regiones más pequeñas, sobre las cuales se aplica un clasificador de imágenes previamente entrenado. Este enfoque permite realizar segmentación a nivel de píxel utilizando clasificadores más simples, sin necesidad de un modelo profundo para toda la imagen.
+
+\subsubsection{Estrategia general}
+
+La idea central consiste en deslizar una ventana rectangular de tamaño fijo sobre la imagen de entrada de $512 \times 512$ píxeles. En cada posición de la ventana, el clasificador determina la clase más probable de esa región. Las ventanas se superponen parcialmente, controladas por el parámetro de desplazamiento (\textit{stride}). Cada píxel puede ser evaluado varias veces por distintas ventanas superpuestas, y la clase final de cada píxel se determina agregando los resultados de todas las ventanas que lo cubren.
+
+\subsubsection{Parámetros principales}
+
+El comportamiento del modelo depende de dos parámetros fundamentales:
+
+\begin{itemize}
+ \item Tamaño de ventana (\textit{window\_size}): Define la altura y el ancho de la ventana deslizante en píxeles. Por ejemplo, un valor de 64 genera ventanas de 64x64 píxeles.
+ \item Desplazamiento (\textit{stride}): Controla cuántos píxeles se mueve la ventana en cada paso, tanto horizontal como verticalmente. Un desplazamiento menor implica mayor solapamiento entre ventanas, lo que aumenta el número de evaluaciones por píxel y puede producir segmentaciones más suaves, aunque con un mayor costo computacional. Un desplazamiento igual al tamaño de la ventana elimina la superposición.
+\end{itemize}
+
+\subsubsection{Proceso de segmentación}
+
+Para segmentar una imagen, se construye primero un mapa de votos tridimensional que acumula las predicciones del clasificador en cada ventana. Posteriormente, para cada píxel, se determina la clase final seleccionando aquella que haya recibido la mayor cantidad de votos o la mayor puntuación ponderada por la confianza del clasificador, dependiendo del método de agregación elegido. El resultado es una máscara de segmentación de la misma resolución que la imagen original.
+
+\subsubsection{Evaluación}
+
+El modelo se evalúa iterando sobre todas las imágenes del conjunto de datos de segmentación, generando máscaras predichas mediante el proceso descrito y comparándolas con las máscaras reales. Se calculan métricas estándar como precisión por píxel, IoU y F1-score, proporcionando una evaluación detallada del rendimiento del modelo.
+
+\section{Modelos de clasificación para segmentación}
+En esta sección se describen los modelos de clasificación empleados como componentes fundamentales de las estrategias de segmentación guiada basadas en particionado espacial. A diferencia de los enfoques de segmentación directa, estos métodos delegan la toma de decisiones semánticas a clasificadores entrenados para discriminar entre zonas frágiles y dúctiles, cuya salida se utiliza posteriormente para construir máscaras de segmentación a distintas escalas.
+
+El objetivo de esta sección es caracterizar los clasificadores utilizados, independientemente del esquema de particionado específico en el que se integran. De este modo, se separa explícitamente el análisis del modelo de clasificación del mecanismo geométrico de segmentación, permitiendo evaluar de forma aislada el impacto de la arquitectura del clasificador en el desempeño final del sistema. Las subsecciones siguientes presentan los distintos modelos considerados.
+
+\subsection{Clasificador CNN}
+
+El clasificador basado en Redes Neuronales Convolucionales (CNN) fue diseñado
+para clasificar regiones de tamaño variable extraídas por el algoritmo Quadtree.
+
+% \subparagraph{Arquitectura General}
+
+La arquitectura sigue un patrón de extracción de características seguido de
+clasificación:
+
+\begin{equation}
+ \begin{aligned}
+ \text{Entrada} &\rightarrow \text{Bloques Conv.} \rightarrow
+ \text{Pooling Global} \\
+ &\rightarrow \text{Clasificador} \rightarrow \text{Softmax}
+ \end{aligned}
+\end{equation}
+
+% \subparagraph{Bloques Convolucionales}
+
+Cada bloque convolucional aplica la siguiente secuencia:
+
+\begin{enumerate}
+ \item Convolución 3×3 con padding=1
+ \item Batch Normalization
+ \item Activación ReLU
+ \item Convolución 3×3 con padding=1
+ \item Batch Normalization
+ \item Activación ReLU
+ \item Max Pooling 2×2 con stride=2
+\end{enumerate}
+
+Se usa un kernel de $3 \times 3$ porque es el tamaño mínimo que captura información
+espacial (arriba, abajo, izquierda, derecha y diagonales). Al apilar múltiples
+capas con kernels pequeños se obtiene un campo receptivo grande con menos
+parámetros que usando un kernel grande directamente.
+
+El padding de $1$ píxel mantiene las dimensiones espaciales constantes después
+de cada convolución, lo que simplifica el diseño de la red.
+
+Batch Normalization se incluye para acelerar el entrenamiento y estabilizar
+los gradientes, permitiendo usar tasas de aprendizaje más altas.
+
+Se usan dos convoluciones por bloque antes del pooling para aumentar la
+capacidad de la red sin reducir las dimensiones espaciales prematuramente.
+
+% \subparagraph{Número Configurable de Bloques}
+
+El parámetro \texttt{num\_blocks} controla la profundidad de la red. Como cada
+bloque reduce las dimensiones a la mitad mediante Max Pooling, el tamaño mínimo
+de entrada es $2^{n} \times 2^{n}$ píxeles, donde $n$ es el número de bloques.
+
+\begin{table}[h]
+\centering
+\caption{Relación entre número de bloques y tamaño mínimo de entrada}
+\begin{tabular}{ccc}
+\toprule
+\texttt{num\_blocks} & Tamaño mínimo & Canales finales \\
+\midrule
+2 & 4×4 & 64 \\
+3 & 8×8 & 128 \\
+4 & 16×16 & 256 \\
+5 & 32×32 & 512 \\
+\bottomrule
+\end{tabular}
+\label{tab:cnn_blocks}
+\end{table}
+
+El valor por defecto es 3 bloques, lo que permite clasificar regiones de 8×8
+píxeles. Este tamaño fue elegido porque el Quadtree puede subdividir hasta
+regiones pequeñas, y 8×8 se consideró suficiente para contener información visual
+distinguible mientras permite segmentación detallada.
+
+% \subparagraph{Progresión de Filtros}
+
+El número de filtros se duplica en cada bloque, comenzando desde 32:
+
+\begin{equation}
+ \text{filtros en bloque } i = 32 \times 2^{i-1}
+\end{equation}
+
+Esta progresión compensa la reducción de dimensiones espaciales: a medida que
+la imagen se hace más pequeña, se aumentan los canales para mantener la
+capacidad de representación.
+
+% \subparagraph{Adaptive Average Pooling}
+
+Antes de la clasificación se aplica \texttt{AdaptiveAvgPool2d((1,1))}, que
+reduce cualquier tamaño espacial a 1×1 calculando el promedio de cada canal.
+
+Esta capa permite que la red acepte entradas de cualquier tamaño (siempre que
+cumplan el mínimo). Sin importar si la entrada es 8×8 o 256×256, la salida
+siempre es un vector de tamaño fijo que puede procesarse por las capas
+densas.
+
+% \subparagraph{Cabeza de Clasificación}
+
+La cabeza de clasificación consiste en:
+
+\begin{enumerate}
+ \item Aplanamiento del tensor
+ \item Dropout (50\%)
+ \item Capa densa con reducción a 1/4 de los canales
+ \item Activación ReLU
+ \item Dropout (25\%)
+ \item Capa densa final a 3 clases
+\end{enumerate}
+
+El Dropout previene sobreajuste, que es una preocupación con datasets pequeños
+de imágenes SEM. La capa intermedia reduce la dimensionalidad gradualmente
+antes de la clasificación final.
+
+% \subparagraph{Salida}
+
+La red produce probabilidades mediante Softmax, donde la probabilidad más alta
+indica la clase predicha y su valor representa la confianza. Esta confianza es
+usada por el Quadtree para decidir si subdividir la región o aceptar la
+clasificación.
+
+% \subparagraph{Entrenamiento}
+
+El entrenamiento usa Cross-Entropy Loss y el optimizador Adam. Las imágenes se
+agrupan por tamaño para formar batches, ya que las regiones del Quadtree tienen
+dimensiones variables y solo se pueden apilar en un tensor imágenes del mismo
+tamaño.
+
+\subsection{Clasificador ViT}
+
+El clasificador basado en Vision Transformer se utiliza como un componente auxiliar dentro del esquema de segmentación jerárquica, con el objetivo de asignar una única etiqueta a regiones completas de la imagen. A diferencia de los modelos de segmentación, este enfoque no opera a nivel de píxel, sino que produce una predicción global acompañada de una medida de confianza, la cual resulta fundamental para guiar las decisiones del algoritmo quadtree.
+
+\subsubsection{Arquitectura del clasificador}
+
+La arquitectura adoptada corresponde a un Vision Transformer estándar para tareas de clasificación de imágenes, cuyo elemento distintivo es el uso de un token de clasificación. La imagen de entrada se divide inicialmente en parches de tamaño fijo, los cuales son proyectados a un espacio de características de alta dimensión mediante un proceso de incrustación. A estas representaciones se les añade información posicional aprendible, permitiendo al modelo preservar las relaciones espaciales entre los distintos parches.
+
+Sobre esta secuencia de representaciones se introduce un token de clasificación, el cual no está asociado a ninguna región específica de la imagen. Este token se antepone a la secuencia de parches y participa activamente en los mecanismos de autoatención del transformer. A lo largo de las capas del encoder, el token de clasificación actúa como un agregador de información global, concentrando en una única representación los patrones más relevantes presentes en la imagen completa.
+
+Finalizado el procesamiento por el transformer, se descartan las representaciones asociadas a los parches y se conserva únicamente el vector correspondiente al token de clasificación. Este vector se proyecta mediante una cabeza de clasificación hacia el espacio de clases, generando puntuaciones que se transforman en probabilidades normalizadas. Estas probabilidades permiten obtener tanto la etiqueta predicha como una estimación explícita de la confianza del modelo.
+
+\subsubsection{Uso del clasificador en regiones de tamaño arbitrario}
+
+Dado que el clasificador se emplea para evaluar regiones de distinto tamaño dentro del esquema quadtree, resulta necesario un preprocesamiento que garantice la compatibilidad con la entrada de tamaño fijo requerida por el Vision Transformer. Cada región extraída de la imagen original se normaliza en formato y número de canales, y posteriormente se redimensiona mediante interpolación a la resolución esperada por el modelo.
+
+Este procedimiento permite aplicar un clasificador entrenado sobre imágenes completas a subregiones arbitrarias, manteniendo la coherencia de las predicciones y asegurando una integración transparente con el algoritmo de segmentación jerárquica.
+
+\subsubsection{Entrenamiento y rol en la segmentación}
+
+El clasificador puede entrenarse de manera independiente sobre conjuntos de datos externos de clasificación, utilizando un esquema supervisado estándar. Una vez finalizado el entrenamiento, el modelo se emplea principalmente en modo de inferencia, proporcionando predicciones rápidas y consistentes durante la ejecución del algoritmo quadtree.
+
+Dentro del diseño experimental, este clasificador actúa como un módulo intercambiable, lo que permite evaluar distintas arquitecturas de clasificación bajo el mismo esquema de segmentación. De este modo, el análisis se centra en estudiar cómo la capacidad discriminativa del clasificador influye en la calidad final de la segmentación basada en descomposición jerárquica.
+
+
+
+\section{Pipeline de Aumentación de Datos}
+Se implementó un conjunto completo de técnicas de aumentación de datos con el objetivo de incrementar la diversidad del dataset y mejorar la generalización de los modelos. La estrategia combinó transformaciones geométricas, fotométricas y específicas para SEM, aplicadas de manera secuencial y probabilística según un pipeline de aumentación.
+
+\subsubsection{Aumentaciones Geométricas}
+% - Rotación, volteo, escala, traslación, recorte
+% - Aplicación simultánea a imagen y máscara
+Afectan tanto a imágenes como a máscaras, manteniendo la alineación espacial:
+
+\begin{itemize}
+ \item Rotación: Rota la imagen y la máscara dentro de un rango definido, simulando diferentes orientaciones de la muestra.
+ \item Volteo: Invierte horizontal o verticalmente.
+ \item Escalado: Realiza zoom in/out manteniendo el tamaño original.
+ \item Traslación: Desplaza la imagen y máscara horizontal y/o verticalmente.
+ \item Recorte aleatorio: Extrae subregiones aleatorias, redimensionadas al tamaño original.
+\end{itemize}
+
+Estas transformaciones permiten al modelo generalizar a variaciones espaciales de la muestra.
+
+\subsubsection{Aumentaciones Fotométricas}
+% - Brillo, contraste, ruido gaussiano, desenfoque, gamma
+% - Aplicación solo a imagen (no a máscara)
+
+Modifican únicamente las imágenes, simulando variaciones en iluminación y ruido, sin afectar las máscaras:
+
+\begin{itemize}
+ \item Brillo.
+ \item Contraste.
+ \item Corrección gamma.
+ \item Ruido gaussiano.
+ \item Desenfoque gaussiano.
+\end{itemize}
+
+Estas técnicas permiten al modelo aprender invariancia frente a condiciones de adquisición o ruido instrumental.
+
+\subsubsection{Aumentaciones Específicas para SEM}
+Diseñadas para capturar artefactos y características típicas de imágenes SEM:
+
+\begin{itemize}
+ \item Deformación elástica (ElasticDeformationAugmentator): Simula variaciones naturales en texturas de roca o deformaciones durante la adquisición.
+ \item Ecualización adaptativa de histograma (AdaptiveHistogramEqualizationAugmentator): Mejora contraste local mediante CLAHE.
+ \item Artefactos de carga: Añade manchas típicas de muestras no conductivas.
+ \item Ruido de líneas de escaneo (ScanLineNoiseAugmentator): Simula líneas de escaneo o barrido que aparecen en SEM.
+\end{itemize}
+
+Estas técnicas aumentan la robustez del modelo frente a artefactos específicos de la microscopía electrónica.
+
+\subsubsection{Composición de Aumentaciones}
+% - Secuencial, selección aleatoria, aplicación probabilística
+% - Estrategias de combinación utilizadas
+Se implementaron métodos compuestos que permiten combinar varias aumentaciones para crear un pipeline complejo y controlado:
+
+\begin{itemize}
+ \item Secuencial: Aplica múltiples aumentaciones de manera secuencial.
+ \item Aplicación probabilistica: Aplica una aumentación con una probabilidad determinada, introduciendo variabilidad controlada.
+ \item Selección aleatoria: Elige una aumentación de un conjunto con probabilidades ponderadas.
+\end{itemize}
+
+Estas estrategias permiten generar un dataset enriquecido sin comprometer la coherencia entre imágenes y máscaras.
+
+\section{Métricas de Evaluación}
+La evaluación del desempeño de los modelos de segmentación se diseñó de forma modular, permitiendo calcular múltiples métricas a partir de una representación estándar de las predicciones y las máscaras de referencia.
+
+Para cada imagen del conjunto de validación, el modelo de segmentación genera una máscara predicha, la cual se empareja con su máscara real correspondiente, generando pares de comparación $(\hat M,M)$.
+
+Estos pares son procesados sistemáticamente para calcular métricas agregadas que resuman el desempeño del modelo. La mayoría de las métricas utilizadas se fundamentan en interpretar la segmentación como un problema de clasificación a nivel de píxel.
+
+La mayoría de las métricas utilizadas se fundamentan en interpretar la segmentación como un problema de clasificación a nivel de píxel. Para una clase dada, cada píxel de la máscara predicha se compara con la máscara real y se clasifica como verdadero positivo (TP), falso positivo (FP), falso negativo (FN) o verdadero negativo (TN). Los evaluadores acumulan estos conteos a lo largo de todas las imágenes antes de calcular la métrica final.
+
+\subsection{Intersección sobre Unión (IoU) y coeficiente Dice}
+% - Fórmula
+% - Variantes: por clase, promedio macro, promedio ponderado
+El \emph{Intersection over Union} (IoU) y el coeficiente \emph{Dice} son las métricas principales para cuantificar el solapamiento entre predicción y referencia.
+El IoU se define como la razón entre la intersección y la unión de ambas máscaras, penalizando tanto falsas detecciones como omisiones. El Dice, estrechamente relacionado, pondera el solapamiento relativo y puede interpretarse como una forma del F1-score a nivel de píxel, siendo ligeramente más sensible a errores de clasificación.
+
+Estas métricas se calculan de tres maneras complementarias:
+(i) por clase, evaluando individualmente cada etiqueta;
+(ii) promedio macro, donde se promedia el valor de cada clase sin ponderación, ofreciendo una visión balanceada incluso ante desbalances severos;
+(iii) promedio ponderado, donde cada clase contribuye según su frecuencia en el conjunto de datos, reflejando el desempeño global pero siendo sensible a clases dominantes como el fondo.
+
+\subsection{Precisión y Recall}
+% - Definiciones
+% - Interpretación en contexto de segmentación
+La precisión y el recall permiten un diagnóstico más fino de los errores del modelo. La precisión mide la confiabilidad de las predicciones positivas, mientras que el recall cuantifica la capacidad del modelo para detectar todos los píxeles relevantes de una clase. Al igual que en IoU y Dice, se reportan promedios macro para analizar el comportamiento medio del modelo en todas las clases, independientemente de su tamaño relativo.
+
+\subsection{Cohesión de la Máscara}
+
+La métrica \emph{Mask Cohesion} no es un indicador estándar como IoU o \emph{Accuracy}, sino un criterio personalizado diseñado para evaluar la plausibilidad geométrica de las máscaras segmentadas, que cuantifica cuánto de la máscara predicha se asemeja a la mascara real.
+
+En lugar de comparar píxel a píxel, esta métrica cuantifica la consistencia estructural global de las predicciones:
+
+\begin{enumerate}
+ \item Representación de forma: Se utiliza un Autoencoder que codifica cada máscara de $512\times512$ píxeles en un vector latente de tres dimensiones, capturando las características estructurales esenciales.
+
+ \item Detección de anomalías: A partir de las máscaras de referencia, se construye un ``cluster'' de formas válidas en el espacio latente. Un \emph{One-Class SVM} se entrena para delimitar este espacio, identificando inliers (formas plausibles) y outliers (formas anómalas).
+
+ \item Cálculo de la métrica: Cada máscara predicha se codifica y se clasifica mediante el SVM. La \emph{Mask Cohesion} se define como la fracción de máscaras predichas clasificadas como inliers:
+ \[
+ \text{Mask Cohesion} = \frac{\text{Número de máscaras inlier}}{\text{Número total de máscaras predichas}}.
+ \]
+\end{enumerate}
+
+Un valor alto de \emph{Mask Cohesion} indica que la mayoría de las máscaras predichas poseen una geometría coherente con las máscaras reales, incluso si no son exactas a nivel de píxel. Valores bajos reflejan máscaras fragmentadas, ruidosas o estructuralmente inconsistentes.
+
+
+Este conjunto de métricas proporciona una evaluación integral del desempeño, abarcando exactitud cuantitativa, balance entre clases y calidad estructural de las segmentaciones producidas.
+
+
+\section{Criterio de Selección de Modelo y Justificación del F1-score}
+
+La selección del modelo óptimo se fundamenta en una métrica que capture adecuadamente el balance entre precisión y exhaustividad en cada clase, particularmente ante el desbalance inherente del dataset. Para este propósito, se adopta el \emph{F1-score macro} como criterio principal de comparación entre modelos.
+
+\subsubsection{Equivalencia entre F1-score y coeficiente Dice}
+
+El coeficiente Dice y el F1-score son métricamente equivalentes cuando se interpretan en el contexto de segmentación binaria. Formalmente, dado un conjunto de verdaderos positivos $\text{TP}$, falsos positivos $\text{FP}$ y falsos negativos $\text{FN}$, ambas métricas se definen como:
+
+\begin{equation}
+ \text{Dice} = \frac{2 \cdot |A \cap B|}{|A| + |B|} = \frac{2 \cdot \text{TP}}{2 \cdot \text{TP} + \text{FP} + \text{FN}} = F_1
+\end{equation}
+
+donde $A$ representa el conjunto de píxeles predichos como positivos y $B$ el conjunto de píxeles realmente positivos (ground truth). Esta equivalencia permite interpretar el F1-score como una medida de solapamiento entre la predicción y la referencia, lo cual es precisamente el objetivo en tareas de segmentación semántica.
+
+\subsubsection{Justificación del promedio macro}
+
+Dado el desbalance significativo entre clases (73 imágenes con predominio dúctil frente a 21 con predominio frágil), el uso del promedio \emph{macro} resulta fundamental. A diferencia del promedio ponderado, el \emph{F1-score macro} calcula el F1 de forma independiente para cada clase y luego promedia sin considerar la frecuencia relativa:
+
+\begin{equation}
+ F_1^{\text{macro}} = \frac{1}{C} \sum_{c=1}^{C} F_1^{(c)}
+\end{equation}
+
+Este enfoque garantiza que el desempeño en la clase minoritaria (frágil) tenga igual peso que el de la clase mayoritaria (dúctil), evitando que un modelo que simplemente prediga la clase dominante obtenga puntuaciones artificialmente elevadas. Un modelo con alto $F_1^{\text{macro}}$ debe necesariamente desempeñarse bien en \emph{todas} las clases, lo cual es el comportamiento deseado para la caracterización de fracturas.
+
+\subsubsection{Proceso de selección basado en validación cruzada}
+
+El criterio de selección se aplica dentro del framework de experimentación automatizada mediante el siguiente protocolo:
+
+\begin{enumerate}
+ \item Para cada configuración (modelo + estrategia de aumentación), se ejecuta validación cruzada con $k=5$ folds.
+ \item En cada fold, se calcula el $F_1^{\text{macro}}$ sobre el conjunto de validación.
+ \item El desempeño final de cada configuración corresponde al promedio de los 5 valores obtenidos.
+ \item La configuración con mayor $F_1^{\text{macro}}$ promedio se selecciona como (modelo + estrategia de aumentación) óptimo.
+\end{enumerate}
+
+Este protocolo asegura que la selección no esté sesgada por particiones particulares del dataset y que el modelo elegido exhiba un desempeño robusto y generalizable. Las métricas complementarias (accuracy, mask cohesion) se reportan para proporcionar una visión holística, pero no participan directamente en el criterio de selección.
+
+
+% \subsection{Diseño del Framework AutoML}
+% - Arquitectura de nodos (DataAugmentatorNode, ModelNode, EvaluatorNode)
+% - Validación cruzada K-fold
+% - Sistema de caché para experimentos
+% - Flujo de ejecución
+
+\section{Resultados}
+
+En esta sección se presentan los resultados obtenidos a partir de las distintas combinaciones de estrategias de aumentación y modelos de segmentación evaluados. El desempeño se reporta mediante el \emph{F1-score} macro, exactitud (\emph{accuracy}), cohesion de la máscara (\emph{mask cohesion}) promedio en los $k=5$ folds y el tiempo total de ejecución, ambos agregados sobre las validaciones correspondientes.
+
+\subsection{Configuración de los modelos evaluados}
+
+Antes de analizar los resultados, se describen brevemente las configuraciones de los modelos utilizados:
+
+Se evaluaron dos configuraciones de segmentadores ViT:
+
+\begin{itemize}
+\item ViT estándar: dimensión del embedding de 256, profundidad de 6 capas, 8 cabezales de atención y una MLP interna de dimensión 512.
+\item ViT grande: dimensión del embedding de 512, profundidad de 12 capas, 16 cabezales de atención y una MLP de dimensión 2048.
+\end{itemize}
+
+Ambos modelos fueron entrenados durante 40 épocas con tamaño de batch fijo y bajo las mismas condiciones de optimización.
+
+De forma análoga, se consideraron dos variantes de Swin Transformer:
+
+\begin{itemize}
+\item Swin estándar: embedding inicial de 64 dimensiones, con una jerarquía de profundidades [2, 2, 6, 2] y número de cabezales [4, 8, 16, 32].
+\item Swin grande: embedding inicial de 128 dimensiones, una arquitectura más profunda [2, 2, 18, 2] y cabezales [8, 16, 32, 64].
+\end{itemize}
+
+Los enfoques basados en \emph{Quadtree} y \emph{Sliding Window} utilizan clasificadores auxiliares (CNN o ViT) entrenados sobre datasets de clasificación independientes. Dado que el proceso de entrenamiento de estos modelos no depende directamente del dataset de segmentación, se optó por evaluar únicamente una configuración sin aumentación de datos sobre el dataset de segmentación, con el fin de reducir el costo computacional y evitar redundancias experimentales.
+
+\subsection{Resultados sin aumentación de datos}
+
+La Tabla~\ref{tab:results_identity} muestra los resultados obtenidos utilizando el nodo de aumentación identidad (sin transformaciones adicionales).
+
+\begin{table}[ht]
+\centering
+\caption{Resultados con aumentación identidad}
+\label{tab:results_identity}
+\begin{tabular}{lcccc}
+\hline
+\textbf{Modelo} & \textbf{F1-score} & \textbf{Accuracy} & \textbf{Mask Cohesion} & \textbf{Tiempo (s)} \\
+\hline
+ViT estándar & 0.5977 & 0.6798 & 0.7339 & 1057.8780 \\
+Swin estándar & \textbf{0.7033} & 0.7384 & 0.5625 & \textbf{927.6742} \\
+ViT grande & 0.5408 & 0.6646 & 0.0 & 2407.9015 \\
+Swin grande & 0.5558 & 0.6645 & 0.0532 & 2715.1977 \\
+Quadtree + CNN & 0.4896 & 0.5523 & 0.0 & 6421.7091 \\
+Quadtree + ViT & 0.2500 & 0.3353 & 0.0 & 34886.8623 \\
+Sliding Window CNN (64/32) & 0.4739 & 0.6384 & 0.8947 & 7742.3934 \\
+Sliding Window CNN (128/64) & 0.4762 & 0.5923 & 0.7146 & 7605.4753 \\
+Sliding Window CNN (128/128) & 0.4881 & 0.5800 & 0.3836 & 7568.2322 \\
+Sliding Window CNN (256/128) & 0.4588 & 0.5543 & 0.4795 & 7573.1301 \\
+Sliding Window ViT (64/32) & 0.2500 & 0.3353 & 0.0 & 9141.4042 \\
+\hline
+\end{tabular}
+\end{table}
+
+Se observa que el modelo Swin estándar obtiene el mejor desempeño y costo computacional. Los enfoques jerárquicos y por ventanas presentan un rendimiento inferior, con un costo computacional considerablemente mayor, especialmente al emplear clasificadores basados en transformers.
+
+\subsection{Resultados con aumentación combinada (2 geométricas, 2 fotométricas, 1 SEM)}
+
+La Tabla~\ref{tab:results_combined_2geo} resume los resultados al aplicar una estrategia de aumentación combinada moderada.
+
+\begin{table}[ht]
+\centering
+\caption{Resultados con aumentación combinada (2G, 2F, 1SEM, $\times$2)}
+\label{tab:results_combined_2geo}
+\begin{tabular}{lcccc}
+\hline
+\textbf{Modelo} & \textbf{F1-score} & \textbf{Accuracy} & \textbf{Mask Cohesion} & \textbf{Tiempo (s)} \\
+\hline
+ViT estándar & 0.5645 & 0.6264 & 0.1368 & 2997.8260 \\
+Swin estándar & \textbf{0.7500} & 0.7771 & 0.4783 & \textbf{2564.0437} \\
+ViT grande & 0.3986 & 0.6646 & 0.0 & 6924.8986 \\
+Swin grande & 0.7269 & 0.7619 & 0.4766 & 7674.9682 \\
+\hline
+\end{tabular}
+\end{table}
+
+La aumentación beneficia de forma clara al Swin Transformer, especialmente en su variante estándar, mientras que los modelos ViT grandes muestran una degradación del desempeño, sugiriendo una relación desfavorable entre complejidad y tamaño efectivo del dataset.
+
+\subsection{Resultados con aumentación combinada (3 geométricas, 1 fotométrica, 1 SEM)}
+
+Finalmente, la Tabla~\ref{tab:results_combined_3geo} presenta los resultados con una estrategia de aumentación geométrica más agresiva.
+
+\begin{table}[ht]
+\centering
+\caption{Resultados con aumentación combinada (3G, 1F, 1SEM, $\times$2)}
+\label{tab:results_combined_3geo}
+\begin{tabular}{lcccc}
+\hline
+\textbf{Modelo} & \textbf{F1-score} & \textbf{Accuracy} & \textbf{Mask Cohesion} & \textbf{Tiempo (s)} \\
+\hline
+ViT estándar & 0.5742 & 0.6568 & 0.5771 & 3014.4913 \\
+Swin estándar & \textbf{0.7495} & 0.7738 & 0.5409 & \textbf{2575.2458} \\
+ViT grande & 0.4164 & 0.6643 & 0.0 & 6930.9403 \\
+Swin grande & 0.4593 & 0.6853 & 0.0736 & 7697.5314 \\
+\hline
+\end{tabular}
+\end{table}
+
+En este escenario, el Swin estándar mantiene un desempeño estable y elevado, mientras que el Swin grande sufre una degradación significativa, lo que refuerza la hipótesis de sobreajuste bajo restricciones de datos y cómputo.
+
+En conjunto, los resultados indican que los modelos Swin Transformer de complejidad moderada ofrecen el mejor equilibrio entre capacidad de representación, robustez frente a aumentación y eficiencia computacional dentro del contexto evaluado.
+
+
+
+\subsection{Análisis de Curvas de Aprendizaje}
+
+Para complementar los resultados cuantitativos, se analizó el comportamiento de las curvas de aprendizaje durante el entrenamiento y validación. Este análisis cualitativo es crucial para entender la estabilidad de la convergencia y detectar fenómenos de \emph{overfitting} que no se reflejan únicamente en la métrica final.
+
+
+\subsection{Impacto de la Aumentación en Swin Transformer}
+
+Para aislar el efecto de la aumentación de datos en el rendimiento del mejor modelo identificado (Swin Transformer Estándar), se consolidan los resultados obtenidos bajo las distintas estrategias evaluadas en la Tabla~\ref{tab:swin_augmentation_comparison}.
+
+\begin{table}[h]
+\centering
+\caption{Comparativa de Swin Transformer bajo distintas estrategias de aumentación}
+\label{tab:swin_augmentation_comparison}
+\begin{tabular}{lccc}
+\hline
+\textbf{Estrategia} & \textbf{F1-score} & \textbf{Accuracy} & \textbf{Mask Cohesion} \\
+\hline
+Identidad (Sin Aumentación) & 0.7033 & 0.7384 & 0.5625 \\
+Combinada 1 (2G, 2F, 1SEM) & \textbf{0.7500} & \textbf{0.7771} & 0.4783 \\
+Combinada 2 (3G, 1F, 1SEM) & 0.7495 & 0.7738 & 0.5409 \\
+\hline
+\end{tabular}
+\end{table}
+
+Se observa una mejora consistente y significativa al introducir técnicas de aumentación. El F1-score experimenta un incremento de aproximadamente 5 puntos porcentuales (de 0.7033 a 0.7500), lo que valida la hipótesis de que la diversidad sintética es crucial para compensar la escasez de datos etiquetados en este dominio. Además, el modelo mantiene un alto grado de cohesión en las máscaras, lo que sugiere que las transformaciones aplicadas no degradan la estructura topológica de las predicciones.
+
+
+\subsection{Análisis de Curvas de Aprendizaje}
+
+Para complementar los resultados cuantitativos, se analizó el comportamiento de las curvas de aprendizaje durante el entrenamiento y validación. Este análisis cualitativo es crucial para entender la estabilidad de la convergencia y detectar fenómenos de \emph{overfitting} que no se reflejan únicamente en la métrica final.
+
+\subsubsection{Comparativa de Validación por Aumentación}
+
+La Figura~\ref{fig:val_loss_combined} muestra la evolución del \emph{Validation Loss} para los distintos modelos evaluados utilizando la estrategia de aumentación combinada (2Geo+2Photo+1SEM).
+
+Se observa un comportamiento interesante en los modelos grandes. El Swin Large (línea roja), a pesar de su mayor capacidad, no logra reducir el \emph{Validation Loss} al mismo ritmo que su contraparte estándar, posiblemente debido a que la cantidad de datos, aun con aumentación, sigue siendo insuficiente para saturar la capacidad del modelo sin un ajuste más fino de hiperparámetros.
+
+\begin{figure}[H]
+ \centering
+ \includegraphics[width=0.9\linewidth]{val_loss_combined_2geo.png}
+ \caption{Comparativa del \emph{Validation Loss} a lo largo de las épocas con aumentación combinada (2Geo+2Photo+1SEM).}
+ \label{fig:val_loss_combined}
+\end{figure}
+
+\subsubsection{Análisis de Sobreajuste en Modelos Swin}
+
+Un fenómeno particular se observó en el escenario de aumentación combinada \path{Combined_2Geo_2Photo_1SEM_x2}, comparando el Swin Standard y el Swin Large. La Figura~\ref{fig:overfitting_analysis} ilustra las curvas de \emph{Training Loss} vs. \emph{Validation Loss} para ambos casos.
+
+\begin{itemize}
+ \item Swin Standard (Fig.~\ref{fig:swin_std_tv}): Este modelo muestra un comportamiento clásico de \emph{overfitting} a partir de cierta etapa. El \emph{Training Loss} (línea azul) disminuye drásticamente, convergiendo cada vez mas a cero, mientras que el \emph{Validation Loss} (línea naranja) se estabiliza y comienza a divergir ligeramente o estancarse. Esto indica que el modelo, con su capacidad moderada, es capaz de memorizar eficazmente el conjunto de entrenamiento aumentado, pero esta memorización deja de traducirse en mejoras de generalización.
+
+ \item Swin Large (Fig.~\ref{fig:swin_large_tv}): En contraste, el modelo grande muestra una dinámica diferente. Su \emph{Training Loss} no disminuye tan abruptamente como en el modelo estándar. Esto sugiere que el modelo más grande podría beneficiarse más de un entrenamiento más prolongado y de un volumen de datos aún mayor para desbloquear su potencial, mientras que el modelo estándar alcanza su techo de rendimiento (y comienza a sobreajustarse) más temprano.
+\end{itemize}
+
+\begin{figure}[H]
+ \centering
+ \subfloat[Swin Standard: Train vs Val]{
+ \includegraphics[width=0.7\linewidth]{train_val_swin_std.png}
+ \label{fig:swin_std_tv}}
+ \\
+ \subfloat[Swin Large: Train vs Val]{
+ \includegraphics[width=0.7\linewidth]{train_val_swin_large.png}
+ \label{fig:swin_large_tv}}
+ \caption{Análisis de curvas de entrenamiento y validación bajo aumentación combinada. Se observa un \emph{overfitting} más marcado en la caída del \emph{Training Loss} del modelo estándar en comparación con el modelo grande.}
+ \label{fig:overfitting_analysis}
+\end{figure}
+
+\subsection{Análisis de Escalabilidad de Datos}
+
+
+Adicionalmente, se realizó un estudio sobre la influencia del volumen de datos en el rendimiento del modelo. Este análisis se llevó a cabo utilizando el modelo \emph{Swin Transformer} en su configuración estándar, entrenado con subconjuntos del dataset original sin aplicar técnicas de aumentación. El objetivo fue evaluar la capacidad de aprendizaje intrínseca de la arquitectura frente a la estricta escasez de datos.
+
+La Figura~\ref{fig:loss_vs_data} ilustra la evolución de la pérdida (\emph{loss}) mínima alcanzada en validación en función del porcentaje de datos disponibles. Se observa una clara tendencia decreciente en la pérdida a medida que aumenta el tamaño del dataset, lo cual confirma que el modelo no ha saturado su capacidad de aprendizaje (no ha alcanzado un \emph{plateau} de rendimiento por falta de parámetros) y se beneficiaría significativamente de la incorporación de más muestras etiquetadas. Esto justifica empíricamente la necesidad crítica de las estrategias de aumentación implementadas en este trabajo para simular un régimen de datos más abundante y explotar el potencial de la arquitectura.
+
+
+\begin{figure}[h]
+ \centering
+ \includegraphics[width=0.9\linewidth]{loss_vs_data_size.png}
+ \caption{Relación entre el tamaño del dataset de entrenamiento y la pérdida mínima en validación. La tendencia indica que el aumento en la disponibilidad de datos contribuye consistentemente a la reducción del error del modelo.}
+ \label{fig:loss_vs_data}
+\end{figure}
+
+Para visualizar el impacto cualitativo de este incremento en los datos, la Figura~\ref{fig:progression} muestra la evolución de las segmentaciones en una misma muestra de validación conforme se entrena el modelo con porcentajes crecientes del dataset (e.g., 20\%, 50\%, 80\%). Se aprecia cómo la definición de las zonas y la reducción de ruidos espurios mejora notablemente con mayor volumen de ejemplos.
+
+\begin{figure}[H]
+ \centering
+ \includegraphics[width=0.9\linewidth]{progression.jpg}
+ \caption{Evolución cualitativa de la segmentación al aumentar el porcentaje de datos de entrenamiento. Se observa una mejora progresiva en la coherencia de las regiones detectadas.}
+ \label{fig:progression}
+\end{figure}
+
+
+\subsection{Evaluación Cualitativa en Test}
+
+Para complementar los resultados cuantitativos, se realizó una evaluación visual sobre el conjunto de prueba independiente. Para este análisis, se seleccionó el par modelo-estrategia que demostró el mayor rendimiento global según la métrica F1-score: el Swin Transformer Estándar entrenado con la estrategia de Aumentación Combinada (2 Geom, 2 Fotom, 1 SEM).
+
+La Figura~\ref{fig:test_predictions} presenta las segmentaciones generadas por esta configuración óptima. La visualización se estructura en tres columnas para facilitar el cotejo directo: la primera columna muestra la imagen SEM original, capturando la textura compleja del material; la columna central exhibe la máscara de referencia (\emph{Ground Truth}) generada por expertos; y la tercera columna presenta la segmentación inferida por el modelo.
+
+
+
+\begin{figure}[H]
+ \centering
+ \includegraphics[width=0.9\linewidth, height=0.95\textheight, keepaspectratio]{test_predictions.jpg}
+ \caption{Visualización de resultados en el conjunto de prueba. Izquierda: Imagen original. Centro: Máscara real. Derecha: Máscara predicha por el modelo Swin Transformer final.}
+ \label{fig:test_predictions}
+\end{figure}
+
+El análisis detallado de estas imágenes revela que el modelo ha logrado aprender no solo la textura local, sino la topología de las fracturas. Se observa una notable precisión en la delimitación de las zonas dúctiles frente a las áreas frágiles, respetando los bordes irregulares característicos de estas morfologías. Incluso en regiones donde el contraste es bajo o la transición es sutil, el modelo mantiene una coherencia estructural alta, evitando la fragmentación excesiva y demostrando una generalización robusta ante datos no vistos.
+
+\subsection{Posibles Sesgos}
+
+La presente sección analiza los posibles sesgos presentes en los datos y su impacto sobre los modelos.
+
+El dataset de segmentación cuenta con 94 imágenes, de las cuales 73 son mayormente dúctiles y 21 mayormente frágiles. Esta distribución desigual puede inducir un sesgo hacia la predicción de la clase dúctil, afectando la generalización de los modelos.
+
+Los modelos jerárquicos o basados en ventanas deslizantes utilizan clasificadores entrenados en datasets externos, lo que puede introducir sesgos adicionales en la segmentación final, condicionando los resultados a la naturaleza de esos datos auxiliares.
+
+Adicionalmente, las máscaras de \emph{ground truth} no fueron creadas por usuarios expertos, lo que puede introducir inconsistencias o errores de etiquetado. Esto limita la fidelidad de las métricas de evaluación y puede sesgar el entrenamiento hacia patrones no totalmente representativos de la realidad.
+
+Estos factores deben considerarse al interpretar los resultados y al extrapolar las conclusiones a otros conjuntos de imágenes o condiciones experimentales.
+
+
+\section{Limitaciones}
+
+El presente estudio está sujeto a varias limitaciones inherentes tanto a las características del dataset como a las restricciones computacionales del entorno de entrenamiento.
+
+El tamaño reducido del dataset de segmentación (94 imágenes SEM) constituye una limitación fundamental. Aun cuando se aplicaron estrategias de aumentación para incrementar la diversidad efectiva de los datos, estas transformaciones no sustituyen completamente la variabilidad morfológica y topológica que se obtendría con un mayor número de muestras reales. Adicionalmente, el desbalance entre clases —con una predominancia de regiones dúctiles frente a frágiles— introduce un sesgo estructural en el aprendizaje.
+
+La resolución variable de las imágenes SEM y la necesidad de redimensionamiento para ciertos análisis introducen una posible pérdida de información espacial, lo que puede afectar la delimitación precisa de bordes complejos en las máscaras de segmentación.
+
+En cuanto al aspecto computacional, los experimentos estuvieron condicionados por el entorno de Kaggle, con acceso a una GPU NVIDIA Tesla P100. Esta restricción impuso límites en la exploración de hiperparámetros, el tamaño de batch y la duración del entrenamiento. En consecuencia, no fue posible realizar búsquedas exhaustivas ni entrenamientos prolongados que podrían arrojar mejores resultados
+
+Asimismo, las limitaciones de memoria y tiempo de ejecución restringieron la evaluación sistemática de múltiples configuraciones de aumentación para los enfoques basados en Quadtree y Sliding Window, los cuales presentan un costo computacional significativamente superior debido a su naturaleza jerárquica o exhaustiva.
+
+Estas limitaciones deben considerarse al interpretar los resultados y motivan futuras extensiones del trabajo orientadas a la incorporación de datasets más extensos, recursos computacionales más robustos y estrategias de entrenamiento más exhaustivas.
+
+
+\section{Conclusiones}
+
+En este trabajo se evaluaron distintas arquitecturas y estrategias de segmentación aplicadas a imágenes SEM bajo un escenario de datos limitados, analizando tanto el desempeño cuantitativo como el comportamiento cualitativo de los modelos.
+
+Los resultados muestran de forma consistente que el \emph{Swin Transformer} en su configuración estándar ofrece el mejor equilibrio entre capacidad de representación, robustez y eficiencia computacional. Este modelo alcanza los valores más altos de \emph{F1-score} y \emph{accuracy} en todos los escenarios evaluados, manteniendo además una adecuada cohesión de las máscaras segmentadas.
+
+Las estrategias de aumentación de datos resultan determinantes para mejorar la generalización del modelo. En particular, las configuraciones combinadas permiten incrementar el \emph{F1-score} en aproximadamente cinco puntos porcentuales respecto al entrenamiento sin aumentación, sin comprometer la coherencia estructural de las predicciones. Este efecto confirma la importancia de la diversidad sintética para compensar la escasez de muestras etiquetadas en este dominio.
+
+Los enfoques alternativos basados en \emph{Quadtree} y \emph{Sliding Windows} muestran un rendimiento inferior y un costo computacional significativamente mayor, especialmente cuando emplean clasificadores basados en transformers, lo que limita su viabilidad práctica frente a modelos de segmentación end-to-end.
+
+El análisis de curvas de aprendizaje y de escalabilidad con respecto al volumen de datos indica que el Swin Transformer estándar no ha saturado su capacidad de aprendizaje y se beneficiaría de conjuntos de datos más extensos, reforzando la relevancia de la aumentación y la recolección de nuevas muestras.
+
+En conjunto, los resultados posicionan al Swin Transformer de complejidad moderada, entrenado con una estrategia de aumentación adecuada, como la opción más efectiva para la segmentación de zonas frágiles y dúctiles en imágenes SEM dentro del contexto evaluado. Como trabajo futuro, se plantea la incorporación de datasets más amplios y la exploración de esquemas híbridos que integren información multiescala de forma más eficiente.
+
+\bibliographystyle{ieeetr}
+\bibliography{references}
+
+\end{document}
diff --git a/docs/report/progression.jpg b/docs/report/progression.jpg
new file mode 100644
index 0000000..9b31e8a
Binary files /dev/null and b/docs/report/progression.jpg differ
diff --git a/docs/report/progression.png b/docs/report/progression.png
new file mode 100644
index 0000000..5cd3908
Binary files /dev/null and b/docs/report/progression.png differ
diff --git a/docs/report/references.bib b/docs/report/references.bib
new file mode 100644
index 0000000..7a5ea64
--- /dev/null
+++ b/docs/report/references.bib
@@ -0,0 +1,81 @@
+@dataset{campari_2025_15510590,
+ author = {Campari, Alessandro},
+ title = {SEM Image dataset},
+ month = may,
+ year = 2025,
+ publisher = {Zenodo},
+ version = {1.0},
+ doi = {10.5281/zenodo.15510590},
+ url = {https://doi.org/10.5281/zenodo.15510590},
+}
+
+
+@inproceedings{ronneberger2015unet,
+ author = {Ronneberger, Olaf and Fischer, Philipp and Brox, Thomas},
+ title = {U-Net: Convolutional Networks for Biomedical Image Segmentation},
+ booktitle = {MICCAI},
+ year = {2015}
+}
+
+@article{dosovitskiy2021vit,
+ author = {Dosovitskiy, Alexey and others},
+ title = {An Image is Worth 16x16 Words: Transformers for Image Recognition at Scale},
+ journal = {ICLR},
+ year = {2021}
+}
+
+@article{liu2021swin,
+ author = {Liu, Ze and others},
+ title = {Swin Transformer: Hierarchical Vision Transformer using Shifted Windows},
+ journal = {arXiv:2103.14030},
+ year = {2021}
+}
+
+@article{cao2021swinunet,
+ author = {Hu Cao and others},
+ title = {Swin-Unet: Unet-like Pure Transformer for Medical Image Segmentation},
+ journal = {arXiv:2105.05537},
+ year = {2021}
+}
+
+@article{sakib2019overview,
+ author = {Sakib, Shadman and others},
+ title = {An Overview of Convolutional Neural Network: Its Architecture and Applications},
+ journal = {Preprints},
+ year = {2019}
+}
+
+@article{he2016resnet,
+ author = {He, Kaiming and others},
+ title = {Deep Residual Learning for Image Recognition},
+ journal = {CVPR},
+ year = {2016}
+}
+
+@article{monchot2021titanium,
+ author = {Monchot, P. and others},
+ title = {Deep Learning Based Instance Segmentation of Titanium Dioxide Particles in SEM},
+ journal = {National Laboratory of Metrology and Testing (LNE)},
+ year = {2021}
+}
+
+@article{lambard2023stylegan,
+ author = {Lambard, G. and others},
+ title = {Generation of highly realistic microstructural images of alloys from limited data},
+ journal = {National Institute for Materials Science (NIMS)},
+ year = {2023}
+}
+
+@article{gaox2024cyclegan,
+ author = {Gao, X. and others},
+ title = {Enhancing SEM imaging quality of weakly conductive samples through unsupervised learning},
+ journal = {Guangdong University of Technology},
+ year = {2024}
+}
+
+@misc{classification_report,
+ title = {Machine Learning-Enabled Image Classification for Automated Electron Microscopy},
+ author = {Day, Alexandra L. and others},
+ year = {2024},
+ note = {Northwestern University}
+}
\ No newline at end of file
diff --git a/docs/report/test_predictions.jpg b/docs/report/test_predictions.jpg
new file mode 100644
index 0000000..f191e9c
Binary files /dev/null and b/docs/report/test_predictions.jpg differ
diff --git a/docs/report/test_predictions.png b/docs/report/test_predictions.png
new file mode 100644
index 0000000..5ae35b4
Binary files /dev/null and b/docs/report/test_predictions.png differ
diff --git a/docs/report/train_val_swin_large.png b/docs/report/train_val_swin_large.png
new file mode 100644
index 0000000..05bee8e
Binary files /dev/null and b/docs/report/train_val_swin_large.png differ
diff --git a/docs/report/train_val_swin_std.png b/docs/report/train_val_swin_std.png
new file mode 100644
index 0000000..3f65bcf
Binary files /dev/null and b/docs/report/train_val_swin_std.png differ
diff --git a/docs/report/val_loss_combined_2geo.png b/docs/report/val_loss_combined_2geo.png
new file mode 100644
index 0000000..dd8df1e
Binary files /dev/null and b/docs/report/val_loss_combined_2geo.png differ
diff --git a/docs/report/val_loss_identity.png b/docs/report/val_loss_identity.png
new file mode 100644
index 0000000..c60c0b2
Binary files /dev/null and b/docs/report/val_loss_identity.png differ
diff --git a/docs/soa/Classification.md b/docs/soa/Classification.md
new file mode 100644
index 0000000..ec29d3f
--- /dev/null
+++ b/docs/soa/Classification.md
@@ -0,0 +1,16 @@
+| Year | Article Name | Authors | Institution | Image Type / Modality | Resolution (Native / Input) | Key Features / Color / Modality | Used Datasets | Input is Output of Another Model? | Input Providing Model/Mechanism | Elaborated Explanation of Input Generation | CNN (General/Dropout CNN) | ResNet Models (18, 50, 101) | Vision Transformer (ViT) Models (Base, Huge, ViT) | DenseNet-121 | EfficientNet/B7 | MobileNet v2 | GoogLeNet | CNN-T (Hybrid) | VDSNet (Hybrid) | VGG-16 | ZF | AlexNet | CapsNet | SKAL | NSGA-II/MOEA/AR-MOEA/SMS-EMOA (Pruning/Optimization) | SVM (All Kernels) | ELM | Random Forest (RF) | Accuracy/OA | Precision / Micro_P | Recall / Sensitivity (Sens.) / Micro_R | F1 Score / Micro_F1 | F1/3 Score | Specificity (Spec.) | ROC Area / AUC | PRC Area / AUC-PR | Time/Efficiency (H/S/ms/G/M) | Loss/Error |
+|-------:|:------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------|:------------------------------------|:---------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------|:------------------------------|:----------------------------------------------------|:-------------------|:-------------------|:---------------|:------------|:-------------------|:------------------|:---------|:-----|:----------|:----------|:-------|:-------------------------------------------------------|:--------------------|:-------------------|:---------------------|:--------------------------------------------|:--------------------------|:----------------------------------------------|:-------------------------|:---------------------|:------------------------------------------------|:-----------------|:--------------------|:-----------------------------------------------|:-------------------------------|
+| 2021 | Deep Learning in Image Classification using Residual Network (ResNet) Variants for Detection of Colorectal Cancer | Devvi Sarwindaa, Radifa Hilya Paradisaa, Alhadi Bustamama, Pinkie Anggiab | Universitas Indonesia | Histopathological tissue specimens (Colorectal gland images) | Native range: 567x430 to 775x522 pixels. Input size: 224 x 224 pixels (resized) | Images were converted to grayscale. Features enhanced using Contrast-Limited Adaptive Histogram Equalization (CLAHE). Pixel distance is 0.6 µm. | Warwick-QU Dataset | No | N/A | N/A | No | Yes (50) (best results) | No | No | No | No | No | No | No | No | No | No | No | No | No | No | No | No | 88% (best results) (R50) | N/A | 96% (R18) / 93% (R50) | N/A | N/A | 92% (R50, 60%:40%) / 83% (R50) | N/A | N/A | Run Time R50: 2.89s/epoch | N/A |
+| 2024 | Machine Learning-Enabled Image Classification for Automated Electron Microscopy | Alexandra L Day, Carolin B Wahl, Vishu Gupta, Roberto dos Reis, Wei-keng Liao, Chad A Mirkin, Vinayak P Dravid, Alok Choudhary, and Ankit Agrawal | Northwestern University | High-Angle Annular Dark-Field (HAADF) images of nanoparticles, acquired using STEM. | Native resolutions: 512 × 512 pixels (Data1, Data2) or 1,024 × 1,024 pixels (Data3). Input size: Trained on images resized to 256 × 256 pixels (due to memory constraints) or 512 × 512 pixels (for Data3 testing consistency). | Grayscale images (single channel). Normalized using zero-mean unit-variance normalization. Some frames were deliberately acquired slightly out of focus. | Proprietary/Experimental HAADF Images | No | Preprocessing Layers + EfficientNetB7 Block | The final input features were generated by feeding normalized and augmented HAADF images through preprocessing layers (e.g., Normalization, RandomTranslation, RandomFlip), which produced a 3-channel input to the EfficientNetB7 block. | Yes | No | No | No | Yes (best results) | No | No | No | No | No | No | No | No | No | No | No | No | No | 75.3% | 96.2% | 70.2% | N/A | 92.8% (best results) | N/A | N/A | N/A | Inf Time: 71–122 ms | N/A |
+| 2021 | Dropout technique for image classification based on extreme learning machine | Gangi Siva Nandini, A.P. Siva Kumar, Chidananda K | N/A (Institution not explicitly named) | General computer vision images sourced from benchmark databases (MINIST, FACE, CIFAR, CIPHER). | Not explicitly specified, but input is highly dimensional . | Highly varied due to backgrounds, viewpoints, and lighting. Preprocessed using Dense SIFT operation and Histogram Oriented Gradients (HOG). | MINIST, FACE, CIFAR, CIPHER Databases | Yes | CNN Feature Mapping Stage + Dense SIFT/HOG | The low-level input features, initially generated using techniques like Dense SIFT and HOG, were processed hierarchically by the CNN Module (including convolution/pooling/dropout) to create the final feature maps input to the ELM classifier. | Yes | No | No | No | No | No | No | No | No | No | No | No | No | No | No | No | Yes (best results) | No | 1.0 (best results) (on MINIST) | N/A | N/A | N/A | N/A | N/A | N/A | N/A | Time: 1s (on MINIST) | 0.0 (best results) (on MINIST) |
+| 2022 | Hybrid Architecture Based on CNN and Transformer for Strip Steel Surface Defect Classification | Shunfeng Li, Chunxue Wu, and Naixue Xiong | University of Shanghai for Science and Technology; Sul Ross State University | Images of hot rolled strip steel surface defects. | Original size: 200 × 200 pixels. Input size: 224 × 224 pixels (scaled uniformly). | Original images are grayscale, but converted to pseudo-color images (3 channels) using the JET color mapping algorithm for enhanced contrast and feature extraction. | NEU-CLS Dataset | Yes | CNN Module (4 Convolutional Layers) | The CNN Module functioned as a feature extraction frontend, converting the pseudo-color input image into a compact feature map representation before it was patch embedded and sent to the Transformer encoder for global modeling. | Yes | Yes (18) | Yes | No | No | Yes | Yes | Yes (best results) | No | No | No | No | No | No | No | No | No | No | 99.17% (best results) (CNN-T) | 99.21% (CNN-T) | 99.17% (CNN-T) | 99.17% (CNN-T) | N/A | N/A | N/A | N/A | FLOPs: 0.12 G (CNN-T) / Params: 0.48 M | N/A |
+| 2023 | Multi-Branch Deep Learning Framework for Land Scene Classification in Satellite Imagery | Sultan Daud Khan and Saleh Basalamah | National University of Technology (Pakistan); Umm Al-Qura University (Saudi Arabia) | Remote sensing images (high-resolution satellite and aerial images). | Varies by dataset (64x64 to 256x256 native). Input patches re-sized to 224 × 224 pixels. | Images contain complex texture, cluttered background, extremely small objects, and large scale variations. Acquired from sources like the Sentinel-2A satellite and Google Earth. | UC-Merced, SIRI-WHU, EuroSAT Datasets | Yes | Fusion Module (Score Combination from Two Branches) | The final classification decision utilized the averaged prediction scores fused from two parallel branches: the Global Contextual Module (DenseNet+PPM) and the Local Feature Extraction Module (FCN+CNN), leveraging both holistic and regional information. | No | Yes (50, 101) | No | Yes (best results) | Yes | Yes | Yes | No | No | Yes | Yes | Yes | No | Yes | Yes | No | No | No | OA: 99.52% (best results) (D/D) | 100.00% (Max class-wise) | 100.00% (Max class-wise) | 100.00% (Max class-wise) | N/A | N/A | N/A | N/A | Train Time: 19.40 H (D/D) / 4.35 H (MobileNet) | N/A |
+| 2023 | Vision Transformer Outperforms Deep Convolutional Neural Network-based Model in Classifying X-ray Images | Om Uparkar, Jyoti Bharti, R. K. Pateriya, Rajeev Kumar Gupta, Ashutosh Sharma | Maulana Azad National Institute of Technology (Bhopal, India) | Chest X-ray images (frontal-view) used for lung disease detection. | Original resolution: 1024 × 1024. Input size (VGG16 component): 224 × 224 in RGB (three channels)]. | Images utilize transfer learning from ImageNet pre-trained weights. Classification influenced by concatenated auxiliary metadata (Age, Gender, X-ray view position PA/AP). | NIH chest X-rays dataset | Yes | ViT Encoder / VGG16 Features + Auxiliary Metadata | The input for the final classification layer was formed by concatenating image features extracted by either the ViT Encoder or the VGG16 component with non-image Auxiliary Features (Age, Gender, X-ray View Position). | No | No | Yes (Huge) (best results) | No | No | No | No | No | Yes | Yes | No | No | Yes | No | No | No | No | No | 70.24% (best results) (ViT-Huge) | 0.67 (ViT-Huge) | 0.63 (ViT-Huge) | 0.65 (ViT-Huge) | N/A | N/A | N/A | N/A | N/A | N/A |
+| 2024 | Fully Automated CTC Detection, Segmentation and Classification for Multi-Channel IF Imaging | Evan Schwab, Bharat Annaldas, Nisha Ramesh, Anna Lundberg, Vishal Shelke, Xinran Xu, Cole Gilbertson, Jiyun Byun, Ernest T. Lam | Epic Sciences (USA) | Multi-channel Immunofluorescence (IF) images captured via Widefield fluorescence microscopy. | FOV size: 2040 × 2040 pixels. Input patches (U-Net): 512 × 512. Final classification on 24x24 pixel thumbnails. | Utilizes three channels: DAPI (nucleus), CK (Cytokeratin), and CD45/31 (non-CTC indicator). Classification relies on 122 extracted features (morphology, intensity, texture). | Internal Data (DefineMBC clinical diagnostic test images) | Yes | Extracted Features (122 features) | The input consisted of 122 interpretable features (morphology, intensity, texture) quantitatively extracted from masks generated by the upstream detection and segmentation steps (LoG, Otsu’s method, 3-channel U-Net), which were fed into the RBF kernel SVM classifier. | No | No | No | No | No | No | No | No | No | No | No | No | No | No | No | Yes (best results) | No | No | 97.8% (RBF SVM) | N/A | Sens: 99.1% (best results) (RBF SVM, Val Set) | N/A | N/A | 99.8% (RBF SVM, Training Set) / 96.9% (Val Set) | N/A | N/A | Avg. Slide Time: 90 min | N/A |
+| 2021 | The Active Segmentation Platform for Microscopic Image Classification and Segmentation | Sumit K. Vohra and Dimiter Prodanov | Zuse Institute Berlin (ZIB); NERF (Neuroscience Research Flanders, Belgium) | Microscopic images of cells and subcellular structures, including ssTEM (Transmission Electron Microscopy) (ISBI 2012) and fluorescent images (HeLa/HEp-2). | Varies by dataset: 512 × 512 (EM ISBI), 382 × 382 (HeLa), Variable size (HEp-2) | Images are typically 16 bit precision. Classification depends on extracted regional features (e.g., moments) and scale space pixel-features (differential invariants). | HeLa, HEp-2 Data Sets | Yes | Feature Vector (Regional Moments + Scale Space Pixel-features) | The SVM classifier operates on a specialized feature vector combining Regional Features (Legendre and Zernike moments) and Scale Space Pixel-features derived from Differential Geometry Filters (e.g., LoG, ALoG, Curvature), selectively chosen after feature selection. | No | No | No | No | No | No | No | No | No | No | No | No | No | No | No | Yes (best results) | No | Yes | TP Rate: 0.93 (best results) (SMO/SVM) | 0.93 (SMO/SVM) | 0.93 (SMO/SVM) | 0.93 (SMO/SVM) | N/A | N/A | 0.99 (SMO/SVM) | 0.90 (SMO/SVM) | N/A | N/A |
+| 2022 | Multi-stream Cell Segmentation with Low-level Cues for Multi-modality Images | Wei Lou, Xinyi Yu, Chenyu Liu, Xiang Wan, Guanbin Li, Siqi Liu, Haofeng Li | The Chinese University of Hong Kong (Shenzhen); Shenzhen Research Institute of Big Data | Multi-modal microscopy images for cell segmentation. | Training used randomly sampled 512 × 512 image patches. | Includes four modalities: Brightfield (BF), Fluorescent (Fluo), Phase-contrast (PC), and Differential Interference Contrast (DIC). Images exhibit various textures, patterns, and cell sizes/shapes. | Competition Dataset + Public datasets | Yes | Unsupervised Classification Pipeline (Pseudo Labels) | The ResNet18 classifier was trained using pseudo labels derived from an upstream pipeline that automatically grouped images into categories (Classes 0-3) based on low-level visual cues (e.g., color saturation, cell area/shape characteristics). | No | Yes (18) | No | No | No | No | No | No | No | No | No | No | No | No | No | No | No | No | 97.91% (best results) (ResNet18 Classifier) | 99.48% (Class 0 accuracy) | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A |
+| 2023 | Multi-Task Learning of Scanning Electron Microscopy and Synthetic Thermal Tomography Images for Detection of Defects in Additively Manufactured Metals | Scott, S.; Chen, W.-Y.; Heifetz, A. | Nuclear Science and Engineering Division, Argonne National Laboratory; Dept. of Civil and Environmental Engineering, Duke University | Synthetic Thermal Tomography (TT); Experimental Scanning Electron Microscopy (SEM) | TT: 500 × 342 pixels. SEM Input: 224 × 224 pixels. SEM Native: 15 nm/pixel. | Multi-Task Learning (MTL) for regression/classification (TT) and segmentation (SEM). Shared U-Net encoder. TT images are pseudocolor (thermal effusivity). Dropout (0.2 probability) in TT decoder. | Synthetic TT images (329 elliptical defect images in SS316). Experimental SEM images (49 images with defects, from 212 total of SS316L). | Yes (for TT) | Thermal Tomography (TT) algorithm processing Pulsed Infrared Thermography (PIT) data. | PIT data simulated with MATLAB heat transfer simulations. The TT algorithm reconstructs thermal effusivity $e(x,y)$. | Yes (U-Net based CNN uses double convolutional layers and max-pooling). Dropout used. | No | No | No | No | No | No | No | No | No | No | No | No | No | No | No | No | N/A (Regression/Segmentation tasks) | N/A | N/A | N/A | N/A | N/A | N/A | N/A | Training occurred over 500 epochs. | Test MSE: 38.54 (MTL). Test AE: 0.47 (MTL). Testing BCE Loss: 0.03 (MTL). Testing IoU: 0.87 (MTL). |
+| 2023 | Semiconductor SEM Image Defect Classification Using Supervised and Semi-Supervised Learning with Vision Transformers | Huang, C.-F.; Sieg, K.; Karlinksy, L.; Flores, N.; Sheraw, R.; Zhang, X. | IBM Research, Albany, NY; IBM Research, Cambridge MA; IBM Research, Yorktown Heights, NY | SEM images of wafer defects (semiconductors) | Input: 224 × 224. Native/Cropped: 340 × 340 (from originals 680x680 or 480x480). | Automatic Defect Classification (ADC). Transfer Learning. Semi-Supervised Learning using Pseudo-Labels. | Semiconductor wafer defect data from IBM Albany fab (over 7400 total images, 11 defect types). | No | Fab system tools/Electron beam imaging. | Data downloaded from inspection layers, cropped to remove annotations, and manually labeled. | Mentioned as dominant model, but ViT is focus. | N/A | Yes (Vision Transformer/ViT approach). DinoV2 (self-supervised pre-trained ViT model) applied. | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | Yes (k-NN used for preliminary performance using Gaussian kernel). | N/A | N/A | Classification Accuracy: Over 90% (with < 15 images/defect for finetuning). Test accuracy (pseudo-labels): 93.3% to 100%. | N/A | N/A | N/A | N/A | N/A | N/A | N/A | Potential for faster turnaround time and efficient computation. ViTs are more computationally expensive than CNNs. | N/A |
+| 2020 | Deep Learning for Classification of the Chemical Composition of Particle Defects on Semiconductor Wafers | O’Leary, J.; Sawlani, K.; Mesbah, A. | Department of Chemical and Biomolecular Engineering, University of California, Berkeley, CA; Lam Research Corporation, Fremont, CA | SEM images of wafer defects; Energy-Dispersive X-ray (EDX) spectroscopy data. | Input: 140 × 140 pixels (cropped). Native: 480 × 480 pixels. | Classification of chemical composition. Hybrid CNN: fuses spectral EDX data with CNN's fully connected layer. Dropout (50%) used. Canny edge detection for outlier detection. | 5761 real semiconductor defects (8 classes) provided by Lam Research Corporation. | No | Review SEM/EDX tool. | Data obtained via optical scattering tool for location/size, then reviewed by SEM/EDX tool. Images cropped to 140x140. | Yes (Deep CNN, 5 convolutional blocks, architecture based on VGGNet). Dropout (50%) in FC layers. | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | Yes (VGGNet-based architecture used for convolutional/pooling layers). | No | Tested, performed poorly. | N/A | N/A | N/A | Referenced. | N/A | N/A | Combined CNN: Top-1 Accuracy: 82.1%. Top-3 Accuracy: 99.2%. | N/A | N/A | N/A | N/A | N/A | N/A | N/A | Training speed: 0.264 s/defect/epoch/GPU. CNN TLv2: Relative training time 0.15. | Categorical cross-entropy loss. Mean Validation Accuracy: 83.0%. |
+| 2020 | Identification and Classification of Atmospheric Particles Based on SEM Images Using Convolutional Neural Network with Attention Mechanism | Yin, C.; Cheng, X.; Liu, X.; Zhao, M. | College of Electrical Engineering and Automation, Shandong University of Science and Technology; Hangzhou Hikvision Digital Technology Co., Ltd. | SEM images of atmospheric particles (PM2.5). | Magnification 20,000 times. Processed/Cropped/Rotated (3469 images). | Classification of morphological characteristics (fibrous, flocculent, spherical, mineral). Attention Mechanism integrated into CNN (Attention-CNN). | Qingdao 2016–2018 database (3469 single-particle SEM images). | No | US FEI Nova Nano SEM 450 (high vacuum mode). | Samples collected on filter membranes, coated with platinum by ion sputtering, and SEM images obtained. Images processed via cropping and rotating. | Yes (Attention-CNN model uses convolution, pooling, and full connection layers). | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | Referenced as a feature extractor in related work. | N/A | N/A | N/A | N/A | N/A | Yes (used for comparison). | N/A | N/A | Classification Accuracy (Range): 94.33% to 98.56%. | Fibrous: 98.33%. Flocculent: 96.67%. Spherical: 94.17%. Mineral: 84.02%. | Fibrous: 96.71%. Flocculent: 98.31%. Spherical: 89.74%. Mineral: 88.46%. | Fibrous: 97.51%. Flocculent: 97.48%. Spherical: 91.90%. Mineral: 86.18%. | N/A | Fibrous: 99.32%. Flocculent: 98.66%. Spherical: 98.39%. Mineral: 95.80%. | N/A | N/A | Training time set to 200 iterations. | Cross-entropy cost function used as loss function. |
+| 2021 | Deep Learning Based Instance Segmentation of Titanium Dioxide Particles in the Form of Agglomerates in Scanning Electron Microscopy | Monchot, P.; Coquelin, L.; Guerroudj, K.; Feltin, N.; Delvallée, A.; Crouzier, L.; Fischer, N. | National Laboratory of Metrology and Testing (LNE), France | SEM images of Titanium Dioxide ($TiO_2$) particle agglomerates. Grayscale (C=1 channel). | W=2048, L=1536 pixels. Mini-mask size: 96 pixels. | Instance Segmentation (Mask R-CNN). Transfer Learning (from MS COCO). Application-specific Data Augmentation. | Function-specific database (77 real images manually segmented, 5947 particles). Test set: 19 images (3741 particles). | No | Scanning Electron Microscopy (SEM). | Images acquired with SEM. Agglomerates extracted, rotated/flipped, and randomly positioned on SEM backgrounds (data augmentation). | Yes (FCN used within Mask R-CNN structure). | Yes (ResNet is the standard backbone, coupled with FPN). | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | Referenced as a classic method. | N/A | N/A | mAP: 60.6. Detection rate: 83.80% (all particles). AP50: 84.6. | N/A | N/A | N/A | N/A | N/A | N/A | N/A | Total segmentation time: 110 s (for 3741 particles). Per particle: 0.035 s. | Mean DICE coefficient: 0.936 (All particles). DICE coefficient: 0.95 (over useful particles). |
\ No newline at end of file
diff --git a/docs/soa/DataAugmentationSoA.md b/docs/soa/DataAugmentationSoA.md
new file mode 100644
index 0000000..701a282
--- /dev/null
+++ b/docs/soa/DataAugmentationSoA.md
@@ -0,0 +1,23 @@
+## State-of-the-Art: Data Augmentation in Scanning Electron Microscopy (SEM) Imagery - Comprehensive Table
+
+| Article Title | Authors | Institution(s) | Year | Paper Focus | Base Model | Dataset(s) | Image Type | Application Domain | Image Resolution | Data Aug | Aug Techniques |
+| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |
+| **Enhancing Electron Microscopy Image Classification Using Data Augmentation** | Welsman J A, Weber G H, Amusat O O, Giannakou A, Ramakrishnan L | Lawrence Berkeley National Laboratory; Bournemouth University | N/A | Comparative Study | DenseNet169, MobileNetV2, ResNet101V2 | National Center for Electron Microscopy dataset (727 EM images) | Electron Microscopy (EM) | Image Classification (Metadata Generation) | Original: 1024x1024; Input: 224x224 | ✓ | Flipping, Random Masking, 90º Rotation, Random Rotation, Random Shifting, Random Zooming |
+| **Enhancing scanning electron microscopy imaging quality of weakly conductive samples through unsupervised learning** | Gao X, Huang T, Tang P, Di J, Zhong L, Zhang W | Guangdong University of Technology, China | 2024 | Novel Model | CycleGAN | Simulated (Gaussian, Hybrid blur) and Real SEM (WO3, CuS, SiO2) | SEM (Weakly conductive samples) | Image Quality Enhancement / Deblurring | 256×256 px | ✓ | Edge Loss ($\text{L}_{\text{edge}}$) using Sobel operator; SSIM Loss ($\text{L}_{\text{cycle}}$) |
+| **Physics-Based Synthetic Data Model for Automated Segmentation in Catalysis Microscopy** | Vuijk M, Ducci G, Sandoval L, Pietsch M, Reuter K, Lunkenbein T, Scheurer C | Fritz-Haber-Institut; Technische Universität München; Forschungszentrum Jülich | 2024 | Novel Model / Hybrid | U-NET | ESEM time-series (1,600 frames) of isopropanol oxidation on cobalt oxide catalyst | ESEM (time-series) | Semantic Segmentation (Evolving crack detection) | 512×512 px | ✓ | Physics-based crack trajectory generation (Distance Transform Map constraint: avoiding pores by 5 pixels); Geometric Aug. (Rotation, Translation, Scaling) |
+| **Generation of highly realistic microstructural images of alloys from limited data with a style‑based generative adversarial network** | Lambard G, Yamazaki K, Demura M | National Institute for Materials Science (NIMS); JFE Steel Corporation | 2023 | Novel Model | StyleGAN2 with ADA | Private dataset of 3000 SEM images of ferrite-martensite DP steel sheets | SEM (Dual-Phase steel microstructures) | Synthetic Data Generation (Materials Science/FEM Simulation) | 512×512 px | ✓ (ADA mechanism) | Pixel blitting, geometrical transformations (X-flip, rotation, translation, scaling); Target heuristic $r_t$ = 0.5 |
+| **Generation of highly realistic microstructural images of alloys from limited data with a style‑based generative adversarial network** | Ferreira I, Ochoa L, Koeshidayatullah A | King Fahd University of Petroleum and Minerals; Universidad Nacional de Colombia | N/A | Novel Model | StyleGAN2 with ADA | >10,000 thin section images (PPL and XPL) across four rock types | Petrographic Thin Section Images (PPL/XPL) | Synthetic Data Generation (Geosciences/Image Self-Labeling) | 512×512 px | ✓ | Image slicing; Truncation Trick (0.7 optimal) |
+
+**Legend:**
+* ✓ = Yes/Used
+* ❌ = No/Not used
+* N/A = Not applicable/Not reported
+* Acc = Accuracy
+* Aug = Augmentation
+* FID = Fréchet Inception Distance
+* Seg = Segmentation
+* Trans = Transformer
+* ADA = Adaptive Discriminator Augmentation
+* kimg = Thousand images processed by discriminator
+
+---
\ No newline at end of file
diff --git a/docs/soa/SegmentationSoA.md b/docs/soa/SegmentationSoA.md
new file mode 100644
index 0000000..c14431c
--- /dev/null
+++ b/docs/soa/SegmentationSoA.md
@@ -0,0 +1,21 @@
+# State-of-the-Art: Semantic Segmentation Models - Comprehensive Table
+
+| Article Title | Authors | Institution(s) | Year | Paper Focus | Base Model | Dataset(s) | Image Type | Application Domain | Image Resolution | U-Net | SegNet | FCN | DeepLab | Other CNN | ViT | SETR | Swin Trans | PVT | Twins | Other Pure Trans | Hybrid Models | Mask R-CNN | Other Instance | ResNet | VGG | MobileNet | Other Backbones | Data Aug | Aug Techniques | Transfer Learning | Few-Shot | Post-Process | End-to-End | Class-then-Seg | Training Strategy | mIoU (%) | Dice/DSC (%) | Global Acc (%) | Class Avg Acc (%) | Pixel Acc/MPA (%) | mAP (%) | AP50 (%) | AP75 (%) | AJI+ | Boundary F1 (%) | HD95 (px) | Jaccard (%) | Precision (%) | Recall (%) | F1-Score (%) |
+|---------------|---------|----------------|------|-------------|------------|-----------|-----------|-------------------|-----------------|-------|--------|-----|---------|-----------|-----|------|-----------|-----|-------|---------------------|---------------|-----------|----------------|--------|-----|-----------|----------------|----------|----------------|-------------------|----------|--------------|------------|----------------|-------------------|----------|--------------|----------------|-------------------|-------------------|---------|----------|----------|------|----------------|-----------|-------------|---------------|-----------|--------------|
+| SegNet: A Deep Convolutional Encoder-Decoder Architecture | Badrinarayanan V, Kendall A, Cipolla R | University of Cambridge, UK | 2015 | Novel Model / Comparative | VGG16 | CamVid, SUN RGB-D, ImageNet | RGB images | Road Scene / Autonomous Driving, Indoor Scene (AR) | 360×480 (CamVid), Various (SUN) | Yes | Yes | Yes | Yes | DeconvNet | No | No | No | No | No | No | No | No | No | No | Yes | No | No | No | N/A | ImageNet encoder | No | Dense CRF (DeepLab) | Yes | No | SGD; Cross-entropy; Median freq balance; LR:0.1/10⁻³; Mom:0.9; Batch:12/5/4 | 60.10 (CamVid 3.5K) | N/A | 90.40 | 71.20 | 90.40 | N/A | N/A | N/A | N/A | 46.84 | N/A | 60.10 | N/A | N/A | N/A |
+| SegFormer: Simple and Efficient Design | Xie E, Wang W, Yu Z, Anandkumar A, Alvarez JM, Luo P | HKU, Nanjing U., NVIDIA, Caltech | 2021 | Novel Model | N/A (Novel MiT) | ADE20K, Cityscapes, COCO-Stuff, ImageNet-1K | Natural images (high-res) | Semantic Seg, Scene Parsing, Autonomous Driving | Train:512×512 (ADE), 1024×1024 (City) | No | No | Yes | Yes | No | Yes | Yes | Yes | Yes | Yes | MiT (Mix Transformer) B0-B5 | Mix-FFN (3×3 Conv+MLP) | No | No | Yes | No | Yes | MiT B0-B5 | Yes | Resize(0.5-2.0), H-flip, random crop | ImageNet-1K, Mapillary Vistas | No | No | Yes | No | AdamW; LR:0.00006; Poly LR; 160K iter(ADE,City), 80K(COCO); Batch:16/8 | 51.8 (ADE-B5-MS), 84.0 (City-B5-MS) | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A |
+| Deep Learning Nanomaterial SEM | De Donato IML, Marques FD, Lunz JN, Archanjo BS, Lopes FJP, Giraldi GA | UFRJ, INMETRO, LNCC, Brasil | 2025 | Model Mod / Hybrid | U-Net | ZnO, GO nanoparticles (SEM) | SEM images | Nanomaterial analysis | Orig:ZnO 1770×2048, GO 1752×2016; Patches:256×256 | Yes | No | No | No | No | No | No | No | No | No | No | No | No | No | No | No | No | No | No | N/A | Fine-tuning (ZnO→GO) | FSL via fine-tuning | No | No | No | 2-phase: (1)ZnO K=4 CV (2)GO fine-tune LR:0.0001, 75:25 split | 95.69±3.62 (ZnO-M₁), 90.02±14.28 (GO-6M₂) | 97.76±2.05 (ZnO-M₁), 94.05±8.21 (GO-2M₁) | ≥98 (ZnO), 97.94±4.80 (GO-2M₁) | N/A | See Global | N/A | N/A | N/A | N/A | N/A | N/A | See IoU | N/A (GO:92.30±15.04) | 98.43±1.55 (ZnO-M₂), 99.75±0.29 (GO-6M₁) | See Dice |
+| Comparative Study Activated Carbon Pores | Pokharel B, Pandey DS, Sapkota A, Yadav B, Gurung V, Adhikari MP, Regmi LN, Adhikari NB | Tribhuvan U., RIT, UNT, Fetchly LLC | 2024 | Comparative Study | N/A (Multiple) | Activated Carbon SEM (128 images) | SEM (grayscale, 8-bit) | Materials characterization | Orig:1280×960; Cropped:572×572 | Yes | No | Yes | Yes | FPN, PSPNet | Yes | No | Yes | No | No | No | TransUNet, SwinUNet | No | No | Yes | No | No | No | No | N/A | ResNet50 pre-trained | No | No | Yes | No | Adam; Gen Dice Loss; ReduceLROnPlateau; LR:0.001; Early stop (p:20) | 62.07 (FPN-Best) | 73.62 (FPN-Best) | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | See IoU | N/A | N/A | See Dice |
+| Adaptive Template Transformer Mitochondria | Pan Y, Luo N, Sun R, Meng M, Zhang T, Xiong Z, Zhang Y | USTC, Hefei National Science Center, China | N/A | Novel Model | 3D U-Net (backbone) | MitoEM, Lucchi, NucMM-Z | EM 3D volumes | Medical/Biological (Cell physiology) | 30×8×8 nm | Yes | No | No | No | No | No | No | No | No | No | Transformer (MSA, Cross-attn, FFN) | ATFormer (3D U-Net+Trans: STLM+HALM) | No | Post-process instances | No | No | No | 3D U-Net Encoder | No | N/A | No | No | Efficient instance grouping | No | No | Adam; LR:0.0001; 100K iter; Loss:BCE(B,F,C)+OT(λ=0.5); Batch:12/5 | N/A | 94.8 (Lucchi) | N/A | N/A | N/A | 78.2 (MitoEM-R), 68.2 (H) | 96.2(R), 89.7(H), 98.2(NucMM) | 92.8(R), 84.1(H), 83.6(NucMM) | N/A | N/A | N/A | 90.2 (Lucchi) | N/A | N/A | N/A |
+| FFSwinNet: CNN-Transformer FFT Shale | Feng Y, Jia L, Zhang J, Chen J | Beijing Inst Tech, China U Geosciences | 2024 | Novel Model / Hybrid | TransUNet-inspired | Marine shale SEM, MCT shale SEM | Shale core SEM | Shale exploration, Geology | Raw MCT:1280×960; Patches:256×256 | Yes | Yes | Yes | Yes | PSPNet, Mask R-CNN | Yes | No | Yes | No | No | ScaleFormer | FFSwinNet (CNN-Trans), TransUNet | Yes | No | Yes | No | No | N/A | Yes | Random flip, Gaussian blur, contrast, pixel dropout | No | No | No | Yes | No | Adam; mom:0.999; decay:1e-8; Mixed loss (β*CE+(1-β)*Dice); Batch:8 | 86.37 (Marine), 81.56 (MCT) | 92.68 (Marine), 89.37 (MCT) | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | 5.76 (Marine), 5.94 (MCT) | See IoU | N/A | N/A | See Dice |
+| Identification Nanocomposites Agglomerates | Bai Y, Wang Y, Qiang D, Yuan X, Wu J, Chen W, Zhang S, Zhang Y, Chen G | UESTC China, U Southampton UK | 2022 | Comparative Study | N/A (Multiple) | Silica-polyethylene nanocomposites (28 SEM) | SEM | Dielectric Materials/Nanodielectrics | Orig:1280×960; Pixel blocks:25×25 | No | No | Yes | No | Pixel block CNN, Unsupervised self-encoding | No | No | No | No | No | No | No | No | No | Yes | Yes | No | SENet structure | Yes | Rotation(-25°-25°), H/V mirror, crop(0-20%), 90°rot | No | No | Threshold (Unsup) | Yes| Yes | Pixel/FCN:supervised; Unsup:iterative self-train w/Felzenszwalb superpixels | 84.3(Pixel), 77.7(FCN), 74.7(Unsup) | N/A | N/A | N/A | 91.7(Pixel), 87.1(FCN), 84.4(Unsup) | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A |
+| Segmentation Nano-Particles Transfer Learning mUNet | Sanan V S, Isaac R S R | Noorul Islam Centre Higher Ed, India | 2025 | Model Modification | U-Net | TiO2 particles (SEM), ImageNet | SEM (TSEM) | Materials Science, Nanotechnology | 256×256 (resized) | Yes | No | No | Yes | AlexNet, NSNet, Cascade Mask-RCNN (lit) | No | No | No | No | No | No | No | No | No | Yes | No | No | No | No | Scaling, norm, resize | ResNet50 ImageNet | No | Threshold+morphological | Yes | No | ADAM; Dice loss; LR:0.0001; 5-fold CV; 200 epochs; Batch:4; 85/15 split | 88.70 (mean) | 94.00 (mean) | 98.62 (mean) | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | See IoU | 91.42 | 96.76 | See Dice |
+| Uncertainty-aware Particle Segmentation EM | Rettenberger L, Szymanski NJ, Zeng Y, Schuetzke J, Wang S, Ceder G, Reischl M | KIT Germany, UC Berkeley, LBNL | 2024 | Model Mod / Comparative | Mask R-CNN | Inorganic powders (90 SEM), LiCoO2 (288 SEM) | Desktop SEM | Materials Science (powder morphology) | Resized:1920×1200; Orig High:7680×4800 | Yes | No | No | No | No | No | No | No | No | No | No | No | Yes | No | Yes | No | No | No | No | N/A | No | No | Filter, sort, IoU remove, binarize | Yes | Yes | Dual models (Low/High Mag); AdamW(LR:0.0001); Loss:MaskRCNN+ConfidenceLoss | N/A | N/A | N/A | N/A | N/A | N/A | N/A | N/A | 0.81(LowMag), 0.51(HighMag), 64.12±20.9(LiCoO2) | N/A | N/A | N/A | N/A | N/A | N/A |
+
+---
+
+**Notes:**
+- Metrics shown are best reported values for each paper
+- Some papers report multiple model variants - shown as separate values
+- IoU and Jaccard Index are equivalent metrics
+- Dice Coefficient and F1-Score are equivalent for binary segmentation
\ No newline at end of file
diff --git a/kaggle/run-automl.ipynb b/kaggle/run-automl.ipynb
new file mode 100644
index 0000000..2aec40f
--- /dev/null
+++ b/kaggle/run-automl.ipynb
@@ -0,0 +1,224 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "_cell_guid": "b1076dfc-b9ad-4769-8c92-a6c4dae69d19",
+ "_uuid": "8f2839f25d086af736a60e9eeb907d3b93b6e0e5",
+ "trusted": true
+ },
+ "outputs": [],
+ "source": [
+ "!ls /kaggle/input/sem-images"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-02T00:38:45.842537Z",
+ "iopub.status.busy": "2026-01-02T00:38:45.842232Z",
+ "iopub.status.idle": "2026-01-02T00:38:45.967823Z",
+ "shell.execute_reply": "2026-01-02T00:38:45.967027Z",
+ "shell.execute_reply.started": "2026-01-02T00:38:45.842504Z"
+ },
+ "trusted": true
+ },
+ "outputs": [],
+ "source": [
+ "!ls /kaggle/input/segmentations-images-automl/pictures"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-02T00:38:45.969456Z",
+ "iopub.status.busy": "2026-01-02T00:38:45.969175Z",
+ "iopub.status.idle": "2026-01-02T00:38:45.973876Z",
+ "shell.execute_reply": "2026-01-02T00:38:45.973179Z",
+ "shell.execute_reply.started": "2026-01-02T00:38:45.969425Z"
+ },
+ "trusted": true
+ },
+ "outputs": [],
+ "source": [
+ "segmentation_labeled_path = '/kaggle/input/segmentations-images-automl/pictures/vega_3_tescan_labeled_images' # noqa E501\n",
+ "segmentation_unlabeled_path = '/kaggle/input/segmentations-images-automl/pictures/vega_3_tescan_unlabeled_images' # noqa E501"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-02T00:38:45.976205Z",
+ "iopub.status.busy": "2026-01-02T00:38:45.975871Z",
+ "iopub.status.idle": "2026-01-02T00:38:57.593258Z",
+ "shell.execute_reply": "2026-01-02T00:38:57.592435Z",
+ "shell.execute_reply.started": "2026-01-02T00:38:45.976181Z"
+ },
+ "trusted": true
+ },
+ "outputs": [],
+ "source": [
+ "!cd /kaggle/input/sem-images && \\\n",
+ " mkdir -p /kaggle/working/sem-images && \\\n",
+ " tar -vJxf \\\n",
+ " /kaggle/input/sem-images/sem_images.tar.xz \\\n",
+ " -C /kaggle/working/"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-02T00:38:57.595038Z",
+ "iopub.status.busy": "2026-01-02T00:38:57.594588Z",
+ "iopub.status.idle": "2026-01-02T00:38:57.713607Z",
+ "shell.execute_reply": "2026-01-02T00:38:57.712666Z",
+ "shell.execute_reply.started": "2026-01-02T00:38:57.595003Z"
+ },
+ "trusted": true
+ },
+ "outputs": [],
+ "source": [
+ "!ls /kaggle/working/sem_images/raw"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-02T00:38:57.715767Z",
+ "iopub.status.busy": "2026-01-02T00:38:57.715155Z",
+ "iopub.status.idle": "2026-01-02T00:38:57.720441Z",
+ "shell.execute_reply": "2026-01-02T00:38:57.719771Z",
+ "shell.execute_reply.started": "2026-01-02T00:38:57.715717Z"
+ },
+ "trusted": true
+ },
+ "outputs": [],
+ "source": [
+ "classification_dataset_path ='/kaggle/working/sem_images/raw'"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-02T00:38:57.721854Z",
+ "iopub.status.busy": "2026-01-02T00:38:57.721506Z",
+ "iopub.status.idle": "2026-01-02T00:38:58.698180Z",
+ "shell.execute_reply": "2026-01-02T00:38:58.697235Z",
+ "shell.execute_reply.started": "2026-01-02T00:38:57.721819Z"
+ },
+ "trusted": true
+ },
+ "outputs": [],
+ "source": [
+ "!git clone https://github.com/CfM47/ML-Project.git"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-02T00:38:58.700399Z",
+ "iopub.status.busy": "2026-01-02T00:38:58.699688Z",
+ "iopub.status.idle": "2026-01-02T00:38:58.708627Z",
+ "shell.execute_reply": "2026-01-02T00:38:58.707674Z",
+ "shell.execute_reply.started": "2026-01-02T00:38:58.700256Z"
+ },
+ "trusted": true
+ },
+ "outputs": [],
+ "source": [
+ "import sys\n",
+ "\n",
+ "project_root = '/kaggle/working/ML-Project'\n",
+ "if project_root not in sys.path:\n",
+ " sys.path.insert(0, project_root)\n",
+ "\n",
+ "sys.path"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-02T00:38:58.710559Z",
+ "iopub.status.busy": "2026-01-02T00:38:58.710149Z"
+ },
+ "trusted": true
+ },
+ "outputs": [],
+ "source": [
+ "from main import _run_with_setup as run\n",
+ "\n",
+ "automl_cache_path = '/kaggle/working/automl_cache'\n",
+ "\n",
+ "run(\n",
+ " segmentation_unlabeled_path,\n",
+ " segmentation_labeled_path,\n",
+ " classification_dataset_path,\n",
+ " automl_cache_path,\n",
+ ")"
+ ]
+ }
+ ],
+ "metadata": {
+ "kaggle": {
+ "accelerator": "none",
+ "dataSources": [
+ {
+ "datasetId": 9039840,
+ "sourceId": 14180328,
+ "sourceType": "datasetVersion"
+ },
+ {
+ "datasetId": 9153261,
+ "sourceId": 14336332,
+ "sourceType": "datasetVersion"
+ },
+ {
+ "datasetId": 9173898,
+ "sourceId": 14366370,
+ "sourceType": "datasetVersion"
+ }
+ ],
+ "dockerImageVersionId": 31234,
+ "isGpuEnabled": false,
+ "isInternetEnabled": true,
+ "language": "python",
+ "sourceType": "notebook"
+ },
+ "kernelspec": {
+ "display_name": "Python 3",
+ "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.12"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 4
+}
diff --git a/kaggle/run-training.ipynb b/kaggle/run-training.ipynb
new file mode 100644
index 0000000..e17bca4
--- /dev/null
+++ b/kaggle/run-training.ipynb
@@ -0,0 +1,140 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "99852947",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "!git clone https://github.com/CfM47/ML-Project.git"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "89e0509f",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import sys\n",
+ "from pathlib import Path"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2ba6b9c0",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "!ls /kaggle/input/segmentations-images-automl/pictures"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "05fff357",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "segmentation_root = Path('/kaggle/input/segmentations-images-automl/pictures')\n",
+ "\n",
+ "TRAIN_LABELED = segmentation_root / 'vega_3_tescan_labeled_images'\n",
+ "TRAIN_UNLABELED = segmentation_root / 'vega_3_tescan_unlabeled_images'\n",
+ "TEST_UNLABELED = segmentation_root / 'sampled_unlabeled'\n",
+ "TEST_LABELED = segmentation_root / 'sampled_labeled'\n",
+ "\n",
+ "WORKING_DIR = Path('/kaggle/working')\n",
+ "PROJECT_ROOT = WORKING_DIR / 'ML-Project'\n",
+ "OUTPUT_DIR = WORKING_DIR / 'swin_results'"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "bbcc6efc",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "if str(PROJECT_ROOT) not in sys.path:\n",
+ " sys.path.insert(0, str(PROJECT_ROOT))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "bd42e98a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from model.swin.config import SwinTrainingConfig\n",
+ "\n",
+ "config = SwinTrainingConfig(\n",
+ " output_dir=OUTPUT_DIR,\n",
+ " device='auto',\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "39900be6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from model.swin.train import run_final_training\n",
+ "\n",
+ "model, test_metrics, mask_pairs, predictions_fig, loss_curves_fig = run_final_training(\n",
+ " TRAIN_UNLABELED,\n",
+ " TRAIN_LABELED,\n",
+ " TEST_UNLABELED,\n",
+ " TEST_LABELED,\n",
+ " config=config,\n",
+ ")"
+ ]
+ },
+{
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3d554aad",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "predictions_fig"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a1b2c3d4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "if loss_curves_fig is not None:\n",
+ " display(loss_curves_fig)\n",
+ "else:\n",
+ " print('Loss curves not available (training history validation failed)')"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3a5d834b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(\"Test Metrics:\")\n",
+ "for metric_name, value in test_metrics.items():\n",
+ " print(f\" {metric_name}: {value:.4f}\")"
+ ]
+ }
+ ],
+ "metadata": {
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/kaggle/run-validation.ipynb b/kaggle/run-validation.ipynb
new file mode 100644
index 0000000..c407995
--- /dev/null
+++ b/kaggle/run-validation.ipynb
@@ -0,0 +1,272 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "99852947",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-03T19:41:24.646553Z",
+ "iopub.status.busy": "2026-01-03T19:41:24.645539Z",
+ "iopub.status.idle": "2026-01-03T19:41:25.415575Z",
+ "shell.execute_reply": "2026-01-03T19:41:25.413923Z",
+ "shell.execute_reply.started": "2026-01-03T19:41:24.646481Z"
+ },
+ "trusted": true
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Cloning into 'ML-Project'...\n",
+ "remote: Enumerating objects: 1068, done.\u001b[K\n",
+ "remote: Counting objects: 100% (328/328), done.\u001b[K\n",
+ "remote: Compressing objects: 100% (213/213), done.\u001b[K\n",
+ "remote: Total 1068 (delta 169), reused 173 (delta 112), pack-reused 740 (from 2)\u001b[K\n",
+ "Receiving objects: 100% (1068/1068), 745.75 KiB | 15.22 MiB/s, done.\n",
+ "Resolving deltas: 100% (604/604), done.\n"
+ ]
+ }
+ ],
+ "source": [
+ "!git clone https://github.com/CfM47/ML-Project.git"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "89e0509f",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-03T19:41:25.418555Z",
+ "iopub.status.busy": "2026-01-03T19:41:25.418103Z",
+ "iopub.status.idle": "2026-01-03T19:41:25.424529Z",
+ "shell.execute_reply": "2026-01-03T19:41:25.423509Z",
+ "shell.execute_reply.started": "2026-01-03T19:41:25.418503Z"
+ },
+ "trusted": true
+ },
+ "outputs": [],
+ "source": [
+ "import sys\n",
+ "from pathlib import Path"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "2ba6b9c0",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-03T19:41:25.425924Z",
+ "iopub.status.busy": "2026-01-03T19:41:25.425596Z",
+ "iopub.status.idle": "2026-01-03T19:41:25.582225Z",
+ "shell.execute_reply": "2026-01-03T19:41:25.581044Z",
+ "shell.execute_reply.started": "2026-01-03T19:41:25.425896Z"
+ },
+ "trusted": true
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "sampled_labeled vega_3_tescan_labeled_images\n",
+ "sampled_unlabeled vega_3_tescan_unlabeled_images\n"
+ ]
+ }
+ ],
+ "source": [
+ "!ls /kaggle/input/segmentations-images-automl/pictures"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "05fff357",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-03T19:41:25.584103Z",
+ "iopub.status.busy": "2026-01-03T19:41:25.583755Z",
+ "iopub.status.idle": "2026-01-03T19:41:25.591229Z",
+ "shell.execute_reply": "2026-01-03T19:41:25.589894Z",
+ "shell.execute_reply.started": "2026-01-03T19:41:25.584068Z"
+ },
+ "trusted": true
+ },
+ "outputs": [],
+ "source": [
+ "segmentation_root = Path('/kaggle/input/segmentations-images-automl/pictures')\n",
+ "\n",
+ "TRAIN_LABELED = segmentation_root / 'vega_3_tescan_labeled_images'\n",
+ "TRAIN_UNLABELED = segmentation_root / 'vega_3_tescan_unlabeled_images'\n",
+ "\n",
+ "# Test set for progression visualization\n",
+ "TEST_LABELED = segmentation_root / 'sampled_labeled'\n",
+ "TEST_UNLABELED = segmentation_root / 'sampled_unlabeled'\n",
+ "\n",
+ "WORKING_DIR = Path('/kaggle/working')\n",
+ "PROJECT_ROOT = WORKING_DIR / 'ML-Project'\n",
+ "OUTPUT_DIR = WORKING_DIR / 'swin_results'"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "bbcc6efc",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-03T19:41:25.594595Z",
+ "iopub.status.busy": "2026-01-03T19:41:25.593801Z",
+ "iopub.status.idle": "2026-01-03T19:41:25.612574Z",
+ "shell.execute_reply": "2026-01-03T19:41:25.611106Z",
+ "shell.execute_reply.started": "2026-01-03T19:41:25.594549Z"
+ },
+ "trusted": true
+ },
+ "outputs": [],
+ "source": [
+ "if str(PROJECT_ROOT) not in sys.path:\n",
+ " sys.path.insert(0, str(PROJECT_ROOT))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "bd42e98a",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-03T19:41:25.613957Z",
+ "iopub.status.busy": "2026-01-03T19:41:25.613653Z",
+ "iopub.status.idle": "2026-01-03T19:41:36.441238Z",
+ "shell.execute_reply": "2026-01-03T19:41:36.439863Z",
+ "shell.execute_reply.started": "2026-01-03T19:41:25.613921Z"
+ },
+ "trusted": true
+ },
+ "outputs": [],
+ "source": [
+ "from model.swin.config import SwinTrainingConfig\n",
+ "\n",
+ "config = SwinTrainingConfig(\n",
+ " output_dir=OUTPUT_DIR,\n",
+ " device='auto',\n",
+ " patience=10,\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "39900be6",
+ "metadata": {
+ "execution": {
+ "execution_failed": "2026-01-03T19:46:49.262Z",
+ "iopub.execute_input": "2026-01-03T19:41:36.442700Z",
+ "iopub.status.busy": "2026-01-03T19:41:36.442316Z"
+ },
+ "trusted": true
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Loading training dataset...\n",
+ "Loading from:\n",
+ " Input: /kaggle/input/segmentations-images-automl/pictures/vega_3_tescan_unlabeled_images\n",
+ " Target: /kaggle/input/segmentations-images-automl/pictures/vega_3_tescan_labeled_images\n",
+ "Loaded 94 pairs out of 94 input files.\n",
+ "Loaded 94 training samples\n",
+ "\n",
+ "============================================================\n",
+ "Training with 10% of data\n",
+ "============================================================\n",
+ " Subsampled to 9 samples (10%)\n",
+ "\n",
+ " Fold 1/5\n",
+ " Training: 7 -> 21 samples (augmented)\n",
+ " Validation: 2 samples (no augmentation)\n",
+ "Training for up to 40 epochs with early stopping (patience=10)\n",
+ "Epoch 1/40, Loss: 0.960262, Val Loss: 0.735986, Status: improved\n"
+ ]
+ }
+ ],
+ "source": [
+ "from model.swin.train import run_percentage_validation\n",
+ "\n",
+ "validation_metrics, learning_curves_fig, progression_fig = run_percentage_validation(\n",
+ " TRAIN_UNLABELED,\n",
+ " TRAIN_LABELED,\n",
+ " TEST_UNLABELED,\n",
+ " TEST_LABELED,\n",
+ " config=config,\n",
+ ")\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "bdcc3da6-2656-45dc-bb64-b96eb9a29cc4",
+ "metadata": {
+ "execution": {
+ "execution_failed": "2026-01-03T19:46:49.263Z"
+ },
+ "trusted": true
+ },
+ "outputs": [],
+ "source": [
+ "learning_curves_fig"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "progression-fig",
+ "metadata": {
+ "trusted": true
+ },
+ "outputs": [],
+ "source": [
+ "progression_fig"
+ ]
+ }
+ ],
+ "metadata": {
+ "kaggle": {
+ "accelerator": "gpu",
+ "dataSources": [
+ {
+ "datasetId": 9173898,
+ "sourceId": 14366370,
+ "sourceType": "datasetVersion"
+ }
+ ],
+ "dockerImageVersionId": 31234,
+ "isGpuEnabled": true,
+ "isInternetEnabled": true,
+ "language": "python",
+ "sourceType": "notebook"
+ },
+ "kernelspec": {
+ "display_name": "Python 3",
+ "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.12"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/main.py b/main.py
index 5bdb54b..78cff6e 100644
--- a/main.py
+++ b/main.py
@@ -1,7 +1,139 @@
-def main() -> None:
- """Initialize the project."""
- print("Hello from ml-project!")
+from pathlib import Path
+
+from auto_ml.automl import AutoML
+from auto_ml.implementations import (
+ AccuracyEvaluator,
+ AutoencoderMaskEvaluator,
+ DataAugmentatorNode,
+ EvaluatorNode,
+ IdentityAugmentator,
+ ModelNode,
+ SwinModel,
+ ViTModel,
+ load_dataset_from_directories,
+)
+from setup.augmentators.setup import get_augmentator_nodes
+from setup.evaluator.setup import get_evaluator_node
+from setup.models.setup import get_model_nodes
+
+
+def _run_automl() -> None:
+ print("=== Starting AutoML Verification ===")
+
+ # Paths
+ base_dir = Path("pictures")
+ input_dir = base_dir / "vega_3_tescan_unlabeled_images"
+ target_dir = base_dir / "vega_3_tescan_labeled_images"
+
+ # 1. Load Dataset
+ print("\n--- Step 1: Loading Dataset ---")
+ dataset = load_dataset_from_directories(input_dir, target_dir)
+
+ if len(dataset) == 0:
+ print("Error: No data loaded.")
+ return
+
+ # 2. Setup Nodes
+ print("\n--- Step 2: Setting up Nodes ---")
+
+ # Augmentators
+ aug_node_1 = DataAugmentatorNode(
+ augmentator=IdentityAugmentator(),
+ name="Aug_Identity_K5",
+ k_folds=5,
+ random_seed=42,
+ )
+
+ aug_node_2 = DataAugmentatorNode( # noqa: F841
+ augmentator=IdentityAugmentator(), # reusing identity for now
+ name="Aug_Identity_K3",
+ k_folds=3,
+ random_seed=42,
+ )
+
+ augmentators = [aug_node_1, aug_node_2]
+
+ # Models
+ vit_model = ViTModel(epochs=2, batch_size=2, device="auto")
+ swin_model = SwinModel(epochs=2, batch_size=2, device="auto")
+
+ model_node_vit = ModelNode(model=vit_model, name="ViT_Model_Node")
+ model_node_swin = ModelNode(model=swin_model, name="Swin_Model_Node") # noqa: F841
+
+ models = [model_node_vit]
+
+ # Get reference masks from dataset for autoencoder training
+ reference_masks = dataset.masks
+
+ # Evaluator Node with named evaluators (including Autoencoder)
+ evaluator_node = EvaluatorNode(
+ evaluators={
+ "accuracy": AccuracyEvaluator(),
+ "mask_cohesion": AutoencoderMaskEvaluator(
+ reference_masks=reference_masks,
+ latent_dim=8,
+ epochs=20,
+ nu=0.5,
+ device="auto",
+ ),
+ },
+ name="MainEvaluator",
+ )
+
+ # 3. Run AutoML
+ print("\n--- Step 3: Running AutoML Experiment ---")
+ automl = AutoML()
+ automl.run_experiment(dataset, augmentators, models, evaluator_node=evaluator_node)
+
+ # 4. Results
+ print("\n--- Step 4: Summary ---")
+ print(automl.get_summary())
+
+ print("\n=== AUTOML VERIFICATION SUCCESSFUL! ===")
+
+
+def _run_with_setup(
+ unlabeled_dir: str | Path,
+ labeled_dir: str | Path,
+ classification_dataset_dir: str | Path,
+ auto_ml_cache_dir: str | Path,
+ augmentator_indices: list[int] | None = None,
+ model_indices: list[int] | None = None,
+) -> None:
+ """
+ Use setup to run Auto-ML.
+
+ Args:
+ unlabeled_dir: Directory containing unlabeled images.
+ labeled_dir: Directory containing labeled images.
+ classification_dataset_dir: Directory for classification dataset.
+ auto_ml_cache_dir: Directory for AutoML cache.
+ augmentator_indices: Optional list of indices to filter augmentator nodes.
+ If None, all augmentators are used.
+ model_indices: Optional list of indices to filter model nodes.
+ If None, all models are used.
+
+ """
+ unlabeled_dir = Path(unlabeled_dir)
+ labeled_dir = Path(labeled_dir)
+ classification_dataset_dir = Path(classification_dataset_dir)
+ auto_ml_cache_dir = Path(auto_ml_cache_dir)
+
+ dataset = load_dataset_from_directories(Path(unlabeled_dir), labeled_dir)
+
+ augmentators = get_augmentator_nodes()
+ if augmentator_indices is not None:
+ augmentators = [augmentators[i] for i in augmentator_indices]
+
+ models = get_model_nodes(classification_dataset_dir)
+ if model_indices is not None:
+ models = [models[i] for i in model_indices]
+
+ evaluator = get_evaluator_node(dataset)
+
+ automl = AutoML(cache_dir=auto_ml_cache_dir)
+ automl.run_experiment(dataset, augmentators, models, evaluator_node=evaluator)
if __name__ == "__main__":
- main()
+ _run_automl()
diff --git a/makefile b/makefile
index 455f7ec..462354c 100644
--- a/makefile
+++ b/makefile
@@ -28,7 +28,7 @@ lint-fix:
# Type Checking
typecheck:
@echo "Running Mypy..."
- $(UV) run mypy src/ tests/
+ $(UV) run mypy auto_ml/ notebooks/ tests/
@echo "---------------"
# ---------------------------------
diff --git a/model/__init__.py b/model/__init__.py
new file mode 100644
index 0000000..43269ec
--- /dev/null
+++ b/model/__init__.py
@@ -0,0 +1 @@
+"""Model training and validation module."""
diff --git a/model/swin/__init__.py b/model/swin/__init__.py
new file mode 100644
index 0000000..904030e
--- /dev/null
+++ b/model/swin/__init__.py
@@ -0,0 +1,10 @@
+"""Swin model training and validation submodule."""
+
+from model.swin.config import SwinTrainingConfig
+from model.swin.train import run_final_training, run_percentage_validation
+
+__all__ = [
+ "SwinTrainingConfig",
+ "run_percentage_validation",
+ "run_final_training",
+]
diff --git a/model/swin/config.py b/model/swin/config.py
new file mode 100644
index 0000000..69486ab
--- /dev/null
+++ b/model/swin/config.py
@@ -0,0 +1,45 @@
+"""Configuration for Swin model training and validation."""
+
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import List
+
+
+@dataclass
+class SwinTrainingConfig:
+ """Configuration for Swin training and validation experiments."""
+
+ # Training percentages for learning curve analysis
+ train_percentages: List[int] = field(
+ default_factory=lambda: [10, 20, 30, 40, 50, 60, 70, 80],
+ )
+
+ # K-fold settings (matches DataAugmentatorNode default)
+ n_folds: int = 5
+
+ # Swin model settings
+ epochs: int = 40
+ batch_size: int = 2
+ learning_rate: float = 1e-4
+ embed_dim: int = 96
+ depths: List[int] = field(default_factory=lambda: [2, 2, 6, 2])
+ num_heads: List[int] = field(default_factory=lambda: [3, 6, 12, 24])
+
+ # Early stopping (None = disabled)
+ patience: int | None = None
+
+ # Augmentation copies (for 2Geo2Photo1SEM)
+ augmentation_copies: int = 2
+
+ # Visualization
+ num_test_visualizations: int = 10
+ num_progression_samples: int = 10 # Test images to track across percentages
+
+ # Output directory
+ output_dir: Path = field(default_factory=lambda: Path("results/swin"))
+
+ # Reproducibility
+ seed: int = 42
+
+ # Device: "auto", "cuda", "mps", or "cpu"
+ device: str = "auto"
diff --git a/model/swin/data.py b/model/swin/data.py
new file mode 100644
index 0000000..475f987
--- /dev/null
+++ b/model/swin/data.py
@@ -0,0 +1,164 @@
+"""Dataset utilities for training and validation."""
+
+from typing import List, Tuple
+
+import numpy as np
+
+from auto_ml.implementations.augmentators.composite import (
+ MultiplyDatasetAugmentator,
+ RandomApplyAugmentator,
+ SequentialAugmentator,
+)
+from auto_ml.implementations.augmentators.geometric import (
+ HorizontalFlipAugmentator,
+ RotationAugmentator,
+)
+from auto_ml.implementations.augmentators.photometric import (
+ BrightnessAugmentator,
+ ContrastAugmentator,
+)
+from auto_ml.implementations.augmentators.sem_specific import (
+ ElasticDeformationAugmentator,
+)
+from auto_ml.interfaces import SegmentationDatasetInterface
+
+
+def subsample_dataset(
+ dataset: SegmentationDatasetInterface,
+ percentage: int,
+ seed: int,
+) -> SegmentationDatasetInterface:
+ """
+ Randomly subsample dataset to a given percentage.
+
+ Args:
+ dataset: The dataset to subsample.
+ percentage: Percentage of samples to keep (1-100).
+ seed: Random seed for reproducibility.
+
+ Returns:
+ Subsampled dataset.
+
+ """
+ if percentage >= 100:
+ return dataset
+
+ n_samples = len(dataset)
+ n_keep = max(1, int(n_samples * percentage / 100))
+
+ rng = np.random.default_rng(seed)
+ indices = rng.choice(n_samples, size=n_keep, replace=False)
+
+ return SegmentationDatasetInterface.from_pairs(
+ [dataset.samples[i] for i in indices],
+ metadata={**dataset.metadata, "subsampled_percentage": percentage},
+ )
+
+
+def create_kfold_splits(
+ dataset: SegmentationDatasetInterface,
+ n_folds: int,
+ seed: int,
+) -> List[Tuple[SegmentationDatasetInterface, SegmentationDatasetInterface]]:
+ """
+ Create k-fold train/val splits.
+
+ Replicate the logic from DataAugmentatorNode.process() but without
+ applying augmentation (augmentation is handled separately).
+
+ Args:
+ dataset: The dataset to split.
+ n_folds: Number of folds.
+ seed: Random seed for reproducibility.
+
+ Returns:
+ List of (train_dataset, val_dataset) tuples.
+
+ """
+ n_samples = len(dataset)
+ indices = np.arange(n_samples)
+ rng = np.random.default_rng(seed)
+ rng.shuffle(indices)
+
+ fold_sizes = np.full(n_folds, n_samples // n_folds, dtype=int)
+ fold_sizes[: n_samples % n_folds] += 1
+
+ splits = []
+ current = 0
+
+ for i in range(n_folds):
+ start, stop = current, current + fold_sizes[i]
+ val_mask = np.zeros(n_samples, dtype=bool)
+ val_mask[start:stop] = True
+
+ val_indices = indices[val_mask]
+ train_indices = indices[~val_mask]
+
+ train_dataset = SegmentationDatasetInterface.from_pairs(
+ [dataset.samples[j] for j in train_indices],
+ metadata={**dataset.metadata, "split": "train", "fold": i},
+ )
+ val_dataset = SegmentationDatasetInterface.from_pairs(
+ [dataset.samples[j] for j in val_indices],
+ metadata={**dataset.metadata, "split": "val", "fold": i},
+ )
+
+ splits.append((train_dataset, val_dataset))
+ current = stop
+
+ return splits
+
+
+def create_augmentator(num_copies: int = 2) -> MultiplyDatasetAugmentator:
+ """
+ Create the 2Geo2Photo1SEM augmentator.
+
+ Same pipeline as setup/augmentators/combined_2geo_2photo_1sem.py.
+
+ Args:
+ num_copies: Number of augmented copies to create.
+
+ Returns:
+ Configured augmentator.
+
+ """
+ return MultiplyDatasetAugmentator(
+ augmentators=[
+ SequentialAugmentator(
+ augmentators=[
+ # 2 Geometric augmentations
+ RandomApplyAugmentator(
+ augmentator=HorizontalFlipAugmentator(),
+ probability=0.5,
+ ),
+ RandomApplyAugmentator(
+ augmentator=RotationAugmentator(angle_range=(-15.0, 15.0)),
+ probability=0.5,
+ ),
+ # 2 Photometric augmentations
+ RandomApplyAugmentator(
+ augmentator=BrightnessAugmentator(
+ brightness_range=(0.85, 1.15),
+ ),
+ probability=0.3,
+ ),
+ RandomApplyAugmentator(
+ augmentator=ContrastAugmentator(
+ contrast_range=(0.85, 1.15),
+ ),
+ probability=0.4,
+ ),
+ # 1 SEM augmentation
+ RandomApplyAugmentator(
+ augmentator=ElasticDeformationAugmentator(
+ alpha=25.0,
+ sigma=3.5,
+ ),
+ probability=0.5,
+ ),
+ ],
+ ),
+ ],
+ num_copies=num_copies,
+ include_original=True,
+ )
diff --git a/model/swin/evaluation.py b/model/swin/evaluation.py
new file mode 100644
index 0000000..6e2ecc4
--- /dev/null
+++ b/model/swin/evaluation.py
@@ -0,0 +1,85 @@
+"""Evaluation utilities for training and validation."""
+
+from typing import Any, Dict, List, Tuple
+
+from auto_ml.implementations.evaluators import (
+ AccuracyEvaluator,
+ DiceMacroAverageEvaluator,
+)
+from auto_ml.implementations.nodes import EvaluatorNode
+from auto_ml.implementations.segmentators.swin import SwinModel
+from auto_ml.interfaces import MaskPair, SegmentationDatasetInterface
+
+
+def create_evaluator() -> EvaluatorNode:
+ """
+ Create evaluator with Dice (F1) and Accuracy metrics only.
+
+ Returns:
+ Configured EvaluatorNode.
+
+ """
+ return EvaluatorNode(
+ evaluators={
+ "Dice_Macro": DiceMacroAverageEvaluator(),
+ "Accuracy": AccuracyEvaluator(),
+ },
+ name="SwinValidationEvaluator",
+ )
+
+
+def evaluate_model(
+ model: SwinModel,
+ dataset: SegmentationDatasetInterface,
+ evaluator: EvaluatorNode,
+) -> Tuple[Dict[str, float], List[MaskPair]]:
+ """
+ Evaluate a trained model on a dataset.
+
+ Args:
+ model: Trained SwinModel.
+ dataset: Dataset to evaluate on.
+ evaluator: EvaluatorNode for computing metrics.
+
+ Returns:
+ Tuple of (metrics_dict, mask_pairs).
+
+ """
+ # Get mask pairs from model evaluation
+ mask_pairs = model.evaluate(dataset)
+
+ # Evaluator expects List[List[MaskPair]] (one list per fold)
+ # We wrap in a single list since this is a single evaluation
+ evaluation_results = evaluator.evaluate([mask_pairs])
+
+ # Extract scalar metrics (evaluator returns lists per fold, we take first)
+ metrics: Dict[str, float] = {}
+ for metric_name, values in evaluation_results.items():
+ if isinstance(values, list) and len(values) > 0:
+ metrics[metric_name] = values[0]
+ else:
+ metrics[metric_name] = float(values)
+
+ return metrics, mask_pairs
+
+
+def extract_metrics_from_evaluation(
+ evaluation_results: Dict[str, Any],
+) -> Dict[str, float]:
+ """
+ Extract scalar metrics from evaluator results.
+
+ Args:
+ evaluation_results: Results from EvaluatorNode.evaluate().
+
+ Returns:
+ Dictionary of metric name to scalar value.
+
+ """
+ metrics: Dict[str, float] = {}
+ for metric_name, values in evaluation_results.items():
+ if isinstance(values, list) and len(values) > 0:
+ metrics[metric_name] = values[0]
+ else:
+ metrics[metric_name] = float(values)
+ return metrics
diff --git a/model/swin/metrics.py b/model/swin/metrics.py
new file mode 100644
index 0000000..5b893fe
--- /dev/null
+++ b/model/swin/metrics.py
@@ -0,0 +1,169 @@
+"""Metrics dataclasses for tracking training and validation results."""
+
+from dataclasses import dataclass, field
+from typing import Dict, List
+
+import numpy as np
+
+
+@dataclass
+class TrainingHistory:
+ """Store validated training history with per-epoch losses."""
+
+ epochs: List[int]
+ train_losses: List[float]
+ val_losses: List[float]
+
+ @classmethod
+ def from_history_dicts(
+ cls,
+ history: List[Dict[str, float]],
+ ) -> "TrainingHistory | None":
+ """
+ Create TrainingHistory from list of epoch metric dicts.
+
+ Return None and log error if history is invalid (missing train_loss
+ or val_loss for any epoch).
+
+ Args:
+ history: List of dicts with 'epoch', 'train_loss', and 'val_loss' keys.
+
+ Returns:
+ TrainingHistory instance, or None if validation fails.
+
+ """
+ if not history:
+ print("Error: Training history is empty")
+ return None
+
+ epochs = []
+ train_losses = []
+ val_losses = []
+
+ for i, epoch_dict in enumerate(history):
+ epoch_num = int(epoch_dict.get("epoch", i + 1))
+
+ if "train_loss" not in epoch_dict:
+ print(f"Error: Missing train_loss for epoch {epoch_num}")
+ return None
+ if "val_loss" not in epoch_dict:
+ print(f"Error: Missing val_loss for epoch {epoch_num}")
+ return None
+
+ epochs.append(epoch_num)
+ train_losses.append(float(epoch_dict["train_loss"]))
+ val_losses.append(float(epoch_dict["val_loss"]))
+
+ return cls(
+ epochs=epochs,
+ train_losses=train_losses,
+ val_losses=val_losses,
+ )
+
+
+@dataclass
+class FoldMetrics:
+ """Store metrics for a single fold."""
+
+ fold: int
+ train_history: List[Dict[str, float]] = field(default_factory=list)
+
+ # Final metrics from evaluator
+ dice_macro: float = 0.0
+ accuracy: float = 0.0
+
+ @property
+ def final_train_loss(self) -> float:
+ """Return the final training loss."""
+ if not self.train_history:
+ return float("inf")
+ return self.train_history[-1].get("train_loss", float("inf"))
+
+ @property
+ def final_val_loss(self) -> float:
+ """Return the final validation loss."""
+ if not self.train_history:
+ return float("inf")
+ return self.train_history[-1].get("val_loss", float("inf"))
+
+ @property
+ def train_losses(self) -> List[float]:
+ """Return list of training losses per epoch."""
+ return [h.get("train_loss", float("inf")) for h in self.train_history]
+
+ @property
+ def val_losses(self) -> List[float]:
+ """Return list of validation losses per epoch."""
+ return [h.get("val_loss", float("inf")) for h in self.train_history]
+
+
+@dataclass
+class PercentageMetrics:
+ """Store aggregated metrics for a training percentage."""
+
+ percentage: int
+ fold_metrics: List[FoldMetrics] = field(default_factory=list)
+
+ # --- Dice Macro (F1) ---
+
+ @property
+ def mean_dice_macro(self) -> float:
+ """Calculate mean Dice macro across folds."""
+ if not self.fold_metrics:
+ return 0.0
+ return float(np.mean([fm.dice_macro for fm in self.fold_metrics]))
+
+ @property
+ def std_dice_macro(self) -> float:
+ """Calculate std of Dice macro across folds."""
+ if not self.fold_metrics:
+ return 0.0
+ return float(np.std([fm.dice_macro for fm in self.fold_metrics]))
+
+ # --- Accuracy ---
+
+ @property
+ def mean_accuracy(self) -> float:
+ """Calculate mean accuracy across folds."""
+ if not self.fold_metrics:
+ return 0.0
+ return float(np.mean([fm.accuracy for fm in self.fold_metrics]))
+
+ @property
+ def std_accuracy(self) -> float:
+ """Calculate std of accuracy across folds."""
+ if not self.fold_metrics:
+ return 0.0
+ return float(np.std([fm.accuracy for fm in self.fold_metrics]))
+
+ # --- Training Loss ---
+
+ @property
+ def mean_final_train_loss(self) -> float:
+ """Calculate mean final training loss across folds."""
+ if not self.fold_metrics:
+ return float("inf")
+ return float(np.mean([fm.final_train_loss for fm in self.fold_metrics]))
+
+ @property
+ def std_final_train_loss(self) -> float:
+ """Calculate std of final training loss across folds."""
+ if not self.fold_metrics:
+ return 0.0
+ return float(np.std([fm.final_train_loss for fm in self.fold_metrics]))
+
+ # --- Validation Loss ---
+
+ @property
+ def mean_final_val_loss(self) -> float:
+ """Calculate mean final validation loss across folds."""
+ if not self.fold_metrics:
+ return float("inf")
+ return float(np.mean([fm.final_val_loss for fm in self.fold_metrics]))
+
+ @property
+ def std_final_val_loss(self) -> float:
+ """Calculate std of final validation loss across folds."""
+ if not self.fold_metrics:
+ return 0.0
+ return float(np.std([fm.final_val_loss for fm in self.fold_metrics]))
diff --git a/model/swin/train.py b/model/swin/train.py
new file mode 100644
index 0000000..545eee3
--- /dev/null
+++ b/model/swin/train.py
@@ -0,0 +1,511 @@
+"""
+Swin Segmentation Training and Validation Module.
+
+Provide two entry points:
+1. run_percentage_validation: K-fold cross-validation with varying training percentages
+2. run_final_training: Train on 100% data and evaluate on test set
+"""
+
+from pathlib import Path
+from typing import Dict, List, Tuple
+
+import numpy as np
+import torch
+from matplotlib.figure import Figure
+
+from auto_ml.implementations import SwinModel, load_dataset_from_directories
+from auto_ml.interfaces import MaskPair, SegmentationDatasetInterface
+from model.swin.config import SwinTrainingConfig
+from model.swin.data import create_augmentator, create_kfold_splits, subsample_dataset
+from model.swin.evaluation import create_evaluator, evaluate_model
+from model.swin.metrics import FoldMetrics, PercentageMetrics, TrainingHistory
+from model.swin.visualization import (
+ plot_progression_grid,
+ plot_results,
+ plot_training_loss_curves,
+ visualize_predictions,
+)
+
+# ==============================================================================
+# Entry Points
+# ==============================================================================
+
+
+def run_percentage_validation(
+ train_unlabeled_dir: str | Path,
+ train_labeled_dir: str | Path,
+ test_unlabeled_dir: str | Path | None = None,
+ test_labeled_dir: str | Path | None = None,
+ config: SwinTrainingConfig | None = None,
+) -> Tuple[List[PercentageMetrics], Figure, Figure | None]:
+ """
+ Run learning curve analysis with k-fold cross-validation.
+
+ For each training percentage, run k-fold CV and aggregate metrics.
+ Save plots to output directory. Optionally generate progression visualization
+ if test set is provided.
+
+ Args:
+ train_unlabeled_dir: Directory with unlabeled training images.
+ train_labeled_dir: Directory with labeled training masks.
+ test_unlabeled_dir: Optional directory with unlabeled test images.
+ test_labeled_dir: Optional directory with labeled test masks.
+ config: Training configuration. Uses defaults if None.
+
+ Returns:
+ Tuple of (list of PercentageMetrics, learning curves Figure,
+ progression Figure or None if no test set).
+
+ """
+ if config is None:
+ config = SwinTrainingConfig()
+
+ # Setup output directory
+ config.output_dir.mkdir(parents=True, exist_ok=True)
+
+ # Load training dataset
+ print("Loading training dataset...")
+ train_dataset = load_dataset_from_directories(
+ Path(train_unlabeled_dir),
+ Path(train_labeled_dir),
+ )
+ print(f"Loaded {len(train_dataset)} training samples")
+
+ # Load test dataset if provided
+ test_dataset: SegmentationDatasetInterface | None = None
+ sample_indices: List[int] = []
+ if test_unlabeled_dir and test_labeled_dir:
+ print("Loading test dataset...")
+ test_dataset = load_dataset_from_directories(
+ Path(test_unlabeled_dir),
+ Path(test_labeled_dir),
+ )
+ print(f"Loaded {len(test_dataset)} test samples")
+
+ # Select random sample indices (fixed across percentages)
+ rng = np.random.default_rng(config.seed)
+ num_samples = min(config.num_progression_samples, len(test_dataset))
+ sample_indices = rng.choice(
+ len(test_dataset),
+ size=num_samples,
+ replace=False,
+ ).tolist()
+ print(f"Selected {num_samples} test samples for progression visualization")
+
+ # Run validation for each percentage
+ all_metrics, best_models = _run_validation_loop(train_dataset, config)
+
+ # Generate and save learning curves
+ plot_path = config.output_dir / "learning_curves.png"
+ learning_curves_fig = plot_results(all_metrics, output_path=plot_path)
+
+ # Generate progression visualization if test set is provided
+ progression_fig: Figure | None = None
+ if test_dataset and sample_indices:
+ predictions_by_percentage = _collect_progression_predictions(
+ best_models,
+ test_dataset,
+ sample_indices,
+ )
+ progression_path = config.output_dir / "progression.png"
+ progression_fig = plot_progression_grid(
+ test_dataset,
+ predictions_by_percentage,
+ sample_indices,
+ output_path=progression_path,
+ )
+
+ return all_metrics, learning_curves_fig, progression_fig
+
+
+def run_final_training(
+ train_unlabeled_dir: str | Path,
+ train_labeled_dir: str | Path,
+ test_unlabeled_dir: str | Path,
+ test_labeled_dir: str | Path,
+ config: SwinTrainingConfig | None = None,
+) -> Tuple[SwinModel, Dict[str, float], List[MaskPair], Figure, Figure | None]:
+ """
+ Train final model on 100% data and evaluate on test set.
+
+ Save model weights and prediction visualizations to output directory.
+
+ Args:
+ train_unlabeled_dir: Directory with unlabeled training images.
+ train_labeled_dir: Directory with labeled training masks.
+ test_unlabeled_dir: Directory with unlabeled test images.
+ test_labeled_dir: Directory with labeled test masks.
+ config: Training configuration. Uses defaults if None.
+
+ Returns:
+ Tuple of (trained_model, test_metrics, test_mask_pairs, predictions Figure,
+ loss_curves Figure or None if history validation failed).
+
+ """
+ if config is None:
+ config = SwinTrainingConfig()
+
+ # Setup output directory
+ config.output_dir.mkdir(parents=True, exist_ok=True)
+
+ # Load datasets
+ print("Loading training dataset...")
+ train_dataset = load_dataset_from_directories(
+ Path(train_unlabeled_dir),
+ Path(train_labeled_dir),
+ )
+ print(f"Loaded {len(train_dataset)} training samples")
+
+ print("Loading test dataset...")
+ test_dataset = load_dataset_from_directories(
+ Path(test_unlabeled_dir),
+ Path(test_labeled_dir),
+ )
+ print(f"Loaded {len(test_dataset)} test samples")
+
+ # Train final model
+ model, training_history = _train_final_model(train_dataset, config)
+
+ # Evaluate on test set
+ test_metrics, test_mask_pairs = _evaluate_on_test(model, test_dataset)
+
+ # Save model
+ model_path = config.output_dir / "model.pt"
+ _save_model(model, model_path)
+
+ # Visualize predictions
+ viz_path = config.output_dir / "test_predictions.png"
+ predictions_fig = visualize_predictions(
+ test_dataset,
+ test_mask_pairs,
+ num_samples=config.num_test_visualizations,
+ output_path=viz_path,
+ )
+
+ # Plot training loss curves if history is valid
+ loss_curves_fig: Figure | None = None
+ if training_history is not None:
+ loss_curves_path = config.output_dir / "training_loss_curves.png"
+ loss_curves_fig = plot_training_loss_curves(
+ training_history,
+ output_path=loss_curves_path,
+ )
+
+ return model, test_metrics, test_mask_pairs, predictions_fig, loss_curves_fig
+
+
+# ==============================================================================
+# Validation Loop
+# ==============================================================================
+
+
+def _run_validation_loop(
+ train_dataset: SegmentationDatasetInterface,
+ config: SwinTrainingConfig,
+) -> Tuple[List[PercentageMetrics], Dict[int, SwinModel]]:
+ """Run learning curve analysis for all training percentages."""
+ all_metrics: List[PercentageMetrics] = []
+ best_models_by_percentage: Dict[int, SwinModel] = {}
+
+ for percentage in config.train_percentages:
+ print(f"\n{'=' * 60}")
+ print(f"Training with {percentage}% of data")
+ print(f"{'=' * 60}")
+
+ pct_metrics, best_model = _run_percentage_experiment(
+ train_dataset,
+ percentage,
+ config,
+ )
+ all_metrics.append(pct_metrics)
+ best_models_by_percentage[percentage] = best_model
+
+ print(f"\n {percentage}% Summary:")
+ print(
+ f" Mean F1 (Dice): {pct_metrics.mean_dice_macro:.4f} "
+ f"± {pct_metrics.std_dice_macro:.4f}",
+ )
+ print(
+ f" Mean Accuracy: {pct_metrics.mean_accuracy:.4f} "
+ f"± {pct_metrics.std_accuracy:.4f}",
+ )
+
+ return all_metrics, best_models_by_percentage
+
+
+def _run_percentage_experiment(
+ dataset: SegmentationDatasetInterface,
+ percentage: int,
+ config: SwinTrainingConfig,
+) -> Tuple[PercentageMetrics, SwinModel]:
+ """Run k-fold cross-validation for a single training percentage."""
+ # Subsample dataset
+ subsampled = subsample_dataset(dataset, percentage, config.seed)
+ print(f" Subsampled to {len(subsampled)} samples ({percentage}%)")
+
+ # Create k-fold splits
+ splits = create_kfold_splits(subsampled, config.n_folds, config.seed)
+
+ pct_metrics = PercentageMetrics(percentage=percentage)
+
+ # Track best fold model by dice score
+ best_model: SwinModel | None = None
+ best_dice: float = -1.0
+
+ for fold, (train_split, val_split) in enumerate(splits):
+ print(f"\n Fold {fold + 1}/{config.n_folds}")
+ fold_metrics, model = _train_fold(train_split, val_split, fold, config)
+ pct_metrics.fold_metrics.append(fold_metrics)
+
+ # Track best model
+ if fold_metrics.dice_macro > best_dice:
+ best_dice = fold_metrics.dice_macro
+ best_model = model
+
+ print(
+ f" F1 (Dice): {fold_metrics.dice_macro:.4f}, "
+ f"Accuracy: {fold_metrics.accuracy:.4f}",
+ )
+
+ # best_model is guaranteed to be set since we have at least one fold
+ assert best_model is not None
+ return pct_metrics, best_model
+
+
+def _train_fold(
+ train_dataset: SegmentationDatasetInterface,
+ val_dataset: SegmentationDatasetInterface,
+ fold: int,
+ config: SwinTrainingConfig,
+) -> Tuple[FoldMetrics, SwinModel]:
+ """
+ Train and evaluate a single fold.
+
+ 1. Apply augmentation to training data (not validation)
+ 2. Create and train SwinModel
+ 3. Evaluate on validation set
+ 4. Return metrics and trained model
+
+ """
+ # Apply augmentation to training data only
+ augmentator = create_augmentator(num_copies=config.augmentation_copies)
+ aug_train = augmentator.augment(train_dataset)
+ print(f" Training: {len(train_dataset)} -> {len(aug_train)} samples (augmented)")
+ print(f" Validation: {len(val_dataset)} samples (no augmentation)")
+
+ # Create model
+ model = _create_swin_model(config)
+
+ # Train model
+ train_result = model.train(aug_train, validation_dataset=val_dataset)
+
+ # Evaluate on validation set
+ evaluator = create_evaluator()
+ metrics, _ = evaluate_model(model, val_dataset, evaluator)
+
+ fold_metrics = FoldMetrics(
+ fold=fold,
+ train_history=train_result.history,
+ dice_macro=metrics.get("Dice_Macro", 0.0),
+ accuracy=metrics.get("Accuracy", 0.0),
+ )
+
+ return fold_metrics, model
+
+
+# ==============================================================================
+# Final Training
+# ==============================================================================
+
+
+def _train_final_model(
+ train_dataset: SegmentationDatasetInterface,
+ config: SwinTrainingConfig,
+) -> Tuple[SwinModel, TrainingHistory | None]:
+ """Train final model with 80/20 train/val split and augmentation."""
+ print("\n" + "=" * 60)
+ print("Training final model")
+ print("=" * 60)
+
+ # Split into train/val (80/20)
+ train_split, val_split = train_dataset.split(
+ ratio=0.8,
+ shuffle=True,
+ random_seed=config.seed,
+ )
+ print(f"Split: {len(train_split)} train, {len(val_split)} validation")
+
+ # Apply augmentation to training data only
+ augmentator = create_augmentator(num_copies=config.augmentation_copies)
+ aug_train = augmentator.augment(train_split)
+ print(f"Training: {len(train_split)} -> {len(aug_train)} samples (augmented)")
+
+ # Create and train model with validation for early stopping
+ model = _create_swin_model(config)
+ train_result = model.train(aug_train, validation_dataset=val_split)
+
+ # Create validated training history
+ training_history = TrainingHistory.from_history_dicts(train_result.history)
+
+ return model, training_history
+
+
+def _evaluate_on_test(
+ model: SwinModel,
+ test_dataset: SegmentationDatasetInterface,
+) -> Tuple[Dict[str, float], List[MaskPair]]:
+ """Evaluate trained model on test dataset (no augmentation)."""
+ print("\n" + "=" * 60)
+ print("Evaluating on test set")
+ print("=" * 60)
+
+ evaluator = create_evaluator()
+ metrics, mask_pairs = evaluate_model(model, test_dataset, evaluator)
+
+ print(f"Test F1 (Dice): {metrics.get('Dice_Macro', 0.0):.4f}")
+ print(f"Test Accuracy: {metrics.get('Accuracy', 0.0):.4f}")
+
+ return metrics, mask_pairs
+
+
+# ==============================================================================
+# Model Utilities
+# ==============================================================================
+
+
+def _create_swin_model(config: SwinTrainingConfig) -> SwinModel:
+ """Create a fresh SwinModel instance from config."""
+ return SwinModel(
+ epochs=config.epochs,
+ batch_size=config.batch_size,
+ lr=config.learning_rate,
+ embed_dim=config.embed_dim,
+ depths=config.depths,
+ num_heads=config.num_heads,
+ patience=config.patience,
+ device=config.device,
+ )
+
+
+def _save_model(model: SwinModel, path: Path) -> None:
+ """Save model state dict to disk."""
+ torch.save(model.model.state_dict(), path)
+ print(f"Saved model to {path}")
+
+
+def _collect_progression_predictions(
+ best_models: Dict[int, SwinModel],
+ test_dataset: SegmentationDatasetInterface,
+ sample_indices: List[int],
+) -> Dict[int, List[np.ndarray]]:
+ """
+ Collect predictions from best models at each percentage for selected samples.
+
+ Args:
+ best_models: Dictionary mapping percentage to best fold's model.
+ test_dataset: Test dataset to predict on.
+ sample_indices: Indices of samples to predict.
+
+ Returns:
+ Dictionary mapping percentage to list of predicted masks.
+
+ """
+ predictions_by_percentage: Dict[int, List[np.ndarray]] = {}
+
+ for percentage, model in sorted(best_models.items()):
+ print(f" Collecting predictions for {percentage}%...")
+ predictions = _predict_samples(model, test_dataset, sample_indices)
+ predictions_by_percentage[percentage] = predictions
+
+ return predictions_by_percentage
+
+
+def _predict_samples(
+ model: SwinModel,
+ dataset: SegmentationDatasetInterface,
+ indices: List[int],
+) -> List[np.ndarray]:
+ """
+ Predict masks for specific sample indices.
+
+ Args:
+ model: Trained SwinModel.
+ dataset: Dataset containing samples.
+ indices: Indices of samples to predict.
+
+ Returns:
+ List of predicted masks as numpy arrays.
+
+ """
+ # Create subset dataset with only the selected samples
+ subset_samples = [dataset.samples[idx] for idx in indices]
+ subset_dataset = SegmentationDatasetInterface(samples=subset_samples)
+
+ # Predict only on the subset
+ mask_pairs = model.evaluate(subset_dataset)
+
+ # Extract predicted masks (first element of each pair)
+ predictions = [pair[0] for pair in mask_pairs]
+
+ return predictions
+
+
+# ==============================================================================
+# CLI Entry Point
+# ==============================================================================
+
+
+if __name__ == "__main__":
+ import argparse
+
+ parser = argparse.ArgumentParser(description="Swin model training and validation")
+ parser.add_argument(
+ "mode",
+ choices=["validate", "train"],
+ help="Mode: 'validate' for percentage validation, 'train' for final training",
+ )
+ parser.add_argument(
+ "--train-unlabeled",
+ required=True,
+ help="Path to unlabeled training images",
+ )
+ parser.add_argument(
+ "--train-labeled",
+ required=True,
+ help="Path to labeled training masks",
+ )
+ parser.add_argument(
+ "--test-unlabeled",
+ help="Path to unlabeled test images (required for 'train' mode)",
+ )
+ parser.add_argument(
+ "--test-labeled",
+ help="Path to labeled test masks (required for 'train' mode)",
+ )
+ parser.add_argument(
+ "--output-dir",
+ default="model/swin/results",
+ help="Output directory for results",
+ )
+
+ args = parser.parse_args()
+
+ config = SwinTrainingConfig(output_dir=Path(args.output_dir))
+
+ if args.mode == "validate":
+ run_percentage_validation(
+ args.train_unlabeled,
+ args.train_labeled,
+ config=config,
+ )
+ elif args.mode == "train":
+ if not args.test_unlabeled or not args.test_labeled:
+ parser.error("'train' mode requires --test-unlabeled and --test-labeled")
+
+ run_final_training(
+ args.train_unlabeled,
+ args.train_labeled,
+ args.test_unlabeled,
+ args.test_labeled,
+ config=config,
+ )
diff --git a/model/swin/visualization.py b/model/swin/visualization.py
new file mode 100644
index 0000000..5057b6e
--- /dev/null
+++ b/model/swin/visualization.py
@@ -0,0 +1,418 @@
+"""Visualization utilities for training and validation results."""
+
+from pathlib import Path
+from typing import Dict, List, Tuple
+
+import matplotlib.pyplot as plt
+import numpy as np
+from matplotlib.axes import Axes
+from matplotlib.figure import Figure
+
+from auto_ml.interfaces import MaskPair, SegmentationDatasetInterface
+from model.swin.metrics import PercentageMetrics, TrainingHistory
+
+# Default colors for 3-class segmentation (RGB, 0-1 range)
+DEFAULT_MASK_COLORS: List[Tuple[float, float, float]] = [
+ (1.0, 0.2, 0.2), # Class 0: Red
+ (0.2, 0.6, 1.0), # Class 1: Blue
+ (0.0, 0.0, 0.0), # Class 2: Black (background)
+]
+
+DEFAULT_MASK_ALPHA = 0.6 # Overlay opacity
+
+
+def _plot_mask(
+ ax: Axes,
+ mask: np.ndarray,
+ underlay: np.ndarray,
+ colors: List[Tuple[float, float, float]] | None = None,
+ alpha: float = DEFAULT_MASK_ALPHA,
+) -> None:
+ """
+ Plot a segmentation mask overlaid on an underlay image.
+
+ Args:
+ ax: Matplotlib axes to plot on.
+ mask: Segmentation mask with integer class labels (0, 1, 2).
+ underlay: Grayscale image to use as background.
+ colors: List of RGB tuples (0-1 range) for each class.
+ Defaults to DEFAULT_MASK_COLORS.
+ alpha: Opacity of the mask overlay (0-1). Defaults to DEFAULT_MASK_ALPHA.
+
+ """
+ if colors is None:
+ colors = DEFAULT_MASK_COLORS
+
+ # Normalize underlay to 0-1 range
+ if underlay.max() > 1:
+ underlay_norm = underlay.astype(np.float32) / 255.0
+ else:
+ underlay_norm = underlay.astype(np.float32)
+
+ # Convert grayscale to RGB if needed
+ if underlay_norm.ndim == 2:
+ underlay_rgb = np.stack([underlay_norm] * 3, axis=-1)
+ else:
+ underlay_rgb = underlay_norm
+
+ # Create colored mask overlay
+ h, w = mask.shape
+ mask_rgb = np.zeros((h, w, 3), dtype=np.float32)
+ for class_idx, color in enumerate(colors):
+ class_mask = mask == class_idx
+ for c in range(3):
+ mask_rgb[:, :, c] += class_mask * color[c]
+
+ # Blend underlay and mask
+ blended = (1 - alpha) * underlay_rgb + alpha * mask_rgb
+ blended = np.clip(blended, 0, 1)
+
+ ax.imshow(blended)
+ ax.axis("off")
+
+
+def plot_training_loss_curves(
+ history: TrainingHistory,
+ output_path: Path | None = None,
+) -> Figure:
+ """
+ Plot training and validation loss curves for a single training run.
+
+ Args:
+ history: Validated TrainingHistory dataclass.
+ output_path: Optional path to save the figure.
+
+ Returns:
+ Matplotlib Figure.
+
+ """
+ fig, ax = plt.subplots(figsize=(10, 6))
+
+ ax.plot(history.epochs, history.train_losses, color="red", label="Training")
+ ax.plot(history.epochs, history.val_losses, color="blue", label="Validation")
+
+ ax.set_xlabel("Epoch")
+ ax.set_ylabel("Loss")
+ ax.set_title("Training Loss Curves")
+ ax.legend()
+ ax.grid(True, alpha=0.3)
+
+ if output_path:
+ fig.savefig(output_path, dpi=150, bbox_inches="tight")
+ print(f"Saved training loss curves to {output_path}")
+
+ return fig
+
+
+def plot_results(
+ all_metrics: List[PercentageMetrics],
+ output_path: Path | None = None,
+) -> Figure:
+ """
+ Generate 4-panel learning curve figure.
+
+ Layout:
+ - Top-left: Loss vs training percentage (train & val)
+ - Top-right: F1 & Accuracy vs training percentage (val only)
+ - Bottom-left: Loss curves for lowest percentage (10%)
+ - Bottom-right: Loss curves for highest percentage (80%)
+
+ Args:
+ all_metrics: List of PercentageMetrics from validation run.
+ output_path: Optional path to save the figure.
+
+ Returns:
+ Matplotlib Figure.
+
+ """
+ fig, axes = plt.subplots(2, 2, figsize=(14, 10))
+
+ percentages = [pm.percentage for pm in all_metrics]
+
+ # --- Top-left: Loss vs training percentage ---
+ ax1 = axes[0, 0]
+ _plot_loss_vs_percentage(ax1, all_metrics, percentages)
+
+ # --- Top-right: F1 & Accuracy vs training percentage ---
+ ax2 = axes[0, 1]
+ _plot_metrics_vs_percentage(ax2, all_metrics, percentages)
+
+ # --- Bottom-left: Loss curves for lowest percentage ---
+ ax3 = axes[1, 0]
+ pm_low = all_metrics[0]
+ _plot_loss_curves(ax3, pm_low, f"Loss Curves ({pm_low.percentage}% Training Data)")
+
+ # --- Bottom-right: Loss curves for highest percentage ---
+ ax4 = axes[1, 1]
+ pm_high = all_metrics[-1]
+ _plot_loss_curves(
+ ax4,
+ pm_high,
+ f"Loss Curves ({pm_high.percentage}% Training Data)",
+ )
+
+ plt.tight_layout()
+
+ if output_path:
+ fig.savefig(output_path, dpi=150, bbox_inches="tight")
+ print(f"Saved learning curves to {output_path}")
+
+ return fig
+
+
+def visualize_predictions(
+ test_dataset: SegmentationDatasetInterface,
+ mask_pairs: List[MaskPair],
+ num_samples: int,
+ output_path: Path | None = None,
+) -> Figure:
+ """
+ Create side-by-side visualization grid.
+
+ Show Input | Ground Truth | Predicted for num_samples.
+
+ Args:
+ test_dataset: Test dataset with original images.
+ mask_pairs: List of (predicted_mask, real_mask) tuples.
+ num_samples: Number of samples to visualize.
+ output_path: Optional path to save the figure.
+
+ Returns:
+ Matplotlib Figure.
+
+ """
+ num_samples = min(num_samples, len(mask_pairs))
+
+ fig, axes = plt.subplots(num_samples, 3, figsize=(12, 4 * num_samples))
+
+ # Handle single sample case
+ if num_samples == 1:
+ axes = axes.reshape(1, -1)
+
+ for i in range(num_samples):
+ image = test_dataset.images[i]
+ predicted_mask, real_mask = mask_pairs[i]
+
+ # Input image
+ axes[i, 0].imshow(image, cmap="gray")
+ axes[i, 0].set_title("Input" if i == 0 else "")
+ axes[i, 0].axis("off")
+
+ # Ground truth mask (with underlay)
+ _plot_mask(axes[i, 1], real_mask, image)
+ axes[i, 1].set_title("Ground Truth" if i == 0 else "")
+
+ # Predicted mask (with underlay)
+ _plot_mask(axes[i, 2], predicted_mask, image)
+ axes[i, 2].set_title("Predicted" if i == 0 else "")
+
+ plt.tight_layout()
+
+ if output_path:
+ fig.savefig(output_path, dpi=150, bbox_inches="tight")
+ print(f"Saved predictions visualization to {output_path}")
+
+ return fig
+
+
+def plot_progression_grid(
+ test_dataset: SegmentationDatasetInterface,
+ predictions_by_percentage: Dict[int, List[np.ndarray]],
+ sample_indices: List[int],
+ output_path: Path | None = None,
+) -> Figure:
+ """
+ Create progression grid showing predictions across training percentages.
+
+ Layout:
+ - Columns: [10%, 20%, ..., 80%, Ground Truth, Original]
+ - Rows: One per sample
+
+ Args:
+ test_dataset: Test dataset with original images and masks.
+ predictions_by_percentage: Dict mapping percentage to list of predicted masks.
+ sample_indices: Indices of samples in test_dataset that were predicted.
+ output_path: Optional path to save the figure.
+
+ Returns:
+ Matplotlib Figure.
+
+ """
+ percentages = sorted(predictions_by_percentage.keys())
+ num_samples = len(sample_indices)
+ num_cols = len(percentages) + 2 # +2 for ground truth and original
+
+ fig, axes = plt.subplots(
+ num_samples,
+ num_cols,
+ figsize=(2 * num_cols, 2 * num_samples),
+ )
+
+ # Handle single sample case
+ if num_samples == 1:
+ axes = axes.reshape(1, -1)
+
+ for row, idx in enumerate(sample_indices):
+ image = test_dataset.images[idx]
+ ground_truth = test_dataset.masks[idx]
+
+ # Predictions for each percentage (with underlay)
+ for col, pct in enumerate(percentages):
+ pred = predictions_by_percentage[pct][row]
+ _plot_mask(axes[row, col], pred, image)
+ if row == 0:
+ axes[row, col].set_title(f"{pct}%", fontsize=10)
+
+ # Ground truth column (with underlay)
+ gt_col = len(percentages)
+ _plot_mask(axes[row, gt_col], ground_truth, image)
+ if row == 0:
+ axes[row, gt_col].set_title("Ground Truth", fontsize=10)
+
+ # Original image column
+ orig_col = len(percentages) + 1
+ axes[row, orig_col].imshow(image, cmap="gray")
+ if row == 0:
+ axes[row, orig_col].set_title("Original", fontsize=10)
+ axes[row, orig_col].axis("off")
+
+ plt.tight_layout()
+
+ if output_path:
+ fig.savefig(output_path, dpi=150, bbox_inches="tight")
+ print(f"Saved progression grid to {output_path}")
+
+ return fig
+
+
+# --- Helper Functions ---
+
+
+def _plot_loss_vs_percentage(
+ ax: Axes,
+ all_metrics: List[PercentageMetrics],
+ percentages: List[int],
+) -> None:
+ """Plot train and validation loss vs training percentage."""
+ # Validation loss (blue)
+ mean_val_losses = [pm.mean_final_val_loss for pm in all_metrics]
+ std_val_losses = [pm.std_final_val_loss for pm in all_metrics]
+ ax.errorbar(
+ percentages,
+ mean_val_losses,
+ yerr=std_val_losses,
+ marker="o",
+ color="blue",
+ capsize=5,
+ capthick=2,
+ label="Validation",
+ )
+
+ # Training loss (red)
+ mean_train_losses = [pm.mean_final_train_loss for pm in all_metrics]
+ std_train_losses = [pm.std_final_train_loss for pm in all_metrics]
+ ax.errorbar(
+ percentages,
+ mean_train_losses,
+ yerr=std_train_losses,
+ marker="s",
+ color="red",
+ capsize=5,
+ capthick=2,
+ label="Training",
+ )
+
+ ax.set_xlabel("Training Data Percentage (%)")
+ ax.set_ylabel("Loss")
+ ax.set_title("Loss vs Training Data Size")
+ ax.legend()
+ ax.grid(True, alpha=0.3)
+
+
+def _plot_metrics_vs_percentage(
+ ax: Axes,
+ all_metrics: List[PercentageMetrics],
+ percentages: List[int],
+) -> None:
+ """Plot F1 and Accuracy vs training percentage (validation only)."""
+ # F1 (Dice Macro) - blue
+ mean_f1 = [pm.mean_dice_macro * 100 for pm in all_metrics]
+ std_f1 = [pm.std_dice_macro * 100 for pm in all_metrics]
+ ax.errorbar(
+ percentages,
+ mean_f1,
+ yerr=std_f1,
+ marker="o",
+ color="blue",
+ capsize=5,
+ capthick=2,
+ label="F1 (Dice Macro)",
+ )
+
+ # Accuracy - green
+ mean_acc = [pm.mean_accuracy * 100 for pm in all_metrics]
+ std_acc = [pm.std_accuracy * 100 for pm in all_metrics]
+ ax.errorbar(
+ percentages,
+ mean_acc,
+ yerr=std_acc,
+ marker="s",
+ color="green",
+ capsize=5,
+ capthick=2,
+ label="Accuracy",
+ )
+
+ ax.set_xlabel("Training Data Percentage (%)")
+ ax.set_ylabel("Score (%)")
+ ax.set_title("F1 & Accuracy vs Training Data Size")
+ ax.legend()
+ ax.grid(True, alpha=0.3)
+
+
+def _plot_loss_curves(
+ ax: Axes,
+ pm: PercentageMetrics,
+ title: str,
+) -> None:
+ """Plot averaged loss curves over epochs for a percentage."""
+ if not pm.fold_metrics:
+ return
+
+ max_epochs = max(len(fm.train_history) for fm in pm.fold_metrics)
+
+ avg_train_losses = []
+ avg_val_losses = []
+
+ for epoch in range(max_epochs):
+ train_losses = [
+ fm.train_history[epoch].get("train_loss", float("nan"))
+ for fm in pm.fold_metrics
+ if epoch < len(fm.train_history)
+ ]
+ val_losses = [
+ fm.train_history[epoch].get("val_loss", float("nan"))
+ for fm in pm.fold_metrics
+ if epoch < len(fm.train_history)
+ ]
+
+ if train_losses:
+ avg_train_losses.append(np.nanmean(train_losses))
+ if val_losses:
+ avg_val_losses.append(np.nanmean(val_losses))
+
+ epochs = range(1, len(avg_train_losses) + 1)
+
+ ax.plot(epochs, avg_train_losses, color="red", label="Training")
+ ax.plot(
+ range(1, len(avg_val_losses) + 1),
+ avg_val_losses,
+ color="blue",
+ label="Validation",
+ )
+
+ ax.set_xlabel("Epoch")
+ ax.set_ylabel("Loss")
+ ax.set_title(title)
+ ax.legend()
+ ax.grid(True, alpha=0.3)
diff --git a/notebooks/__init__.py b/notebooks/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/notebooks/ductile_brittle_data_exploration.ipynb b/notebooks/ductile_brittle_data_exploration.ipynb
new file mode 100644
index 0000000..08bfb7d
--- /dev/null
+++ b/notebooks/ductile_brittle_data_exploration.ipynb
@@ -0,0 +1,1078 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "631cda20-9ff8-4936-b791-35edc2eae1f0",
+ "metadata": {},
+ "source": [
+ "# Ductile and Brittle Materials: Mask Dataset Exploration\n",
+ "\n",
+ "This notebook performs a basic analysis of a segmentation dataset where all masks are located in a single directory. The analysis focuses on key areas:\n",
+ "\n",
+ "2. **Pixel Distribution Analysis:** It calculates and visualizes the balance of pixels between the two categories, both across the entire dataset and within individual masks.\n",
+ "3. **Spatial Distribution Analysis:** It generates aggregate heatmaps to show if labels for each category tend to appear in specific regions of the image frame."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 28,
+ "id": "b8c79dde-fb74-4928-a2fc-ebe71b21fe25",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-09T17:35:49.798931Z",
+ "iopub.status.busy": "2026-01-09T17:35:49.798577Z",
+ "iopub.status.idle": "2026-01-09T17:35:49.804944Z",
+ "shell.execute_reply": "2026-01-09T17:35:49.803497Z",
+ "shell.execute_reply.started": "2026-01-09T17:35:49.798911Z"
+ },
+ "trusted": true
+ },
+ "outputs": [],
+ "source": [
+ "from pathlib import Path\n",
+ "\n",
+ "import matplotlib.pyplot as plt\n",
+ "import numpy as np\n",
+ "import pandas as pd\n",
+ "from PIL import Image\n",
+ "\n",
+ "%matplotlib inline"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ccf0b770",
+ "metadata": {},
+ "source": [
+ "Load data. Might need to onfigure the path to the directory containing all mask files."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "da949b39-bc15-45c6-9510-14d840565cae",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-09T17:35:49.813869Z",
+ "iopub.status.busy": "2026-01-09T17:35:49.813574Z",
+ "iopub.status.idle": "2026-01-09T17:35:49.830773Z",
+ "shell.execute_reply": "2026-01-09T17:35:49.829379Z",
+ "shell.execute_reply.started": "2026-01-09T17:35:49.813847Z"
+ },
+ "trusted": true
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "✅ Mask directory found: /kaggle/input/segmentations-images-automl/pictures/vega_3_tescan_labeled_images\n",
+ "Found 94 total mask files.\n"
+ ]
+ }
+ ],
+ "source": [
+ "plt.style.use(\"seaborn-v0_8-darkgrid\")\n",
+ "plt.rcParams[\"figure.figsize\"] = (12, 8)\n",
+ "plt.rcParams[\"figure.facecolor\"] = \"white\"\n",
+ "\n",
+ "# --- CONFIGURATION ---\n",
+ "# IMPORTANT: Set this to the path of the directory containing your mask images.\n",
+ "MASK_DIR = Path(\n",
+ " \"/kaggle/input/segmentations-images-automl/pictures/vega_3_tescan_labeled_images\",\n",
+ " )\n",
+ "# ---------------------\n",
+ "\n",
+ "\n",
+ "if not MASK_DIR.exists():\n",
+ " print(f\"❌ Directory not found: {MASK_DIR}\")\n",
+ " print(\"Please update the MASK_DIR variable.\")\n",
+ "else:\n",
+ " print(f\"✅ Mask directory found: {MASK_DIR}\")\n",
+ "\n",
+ "# Collect all image files from the directory\n",
+ "image_extensions = [\".png\", \".jpg\", \".jpeg\", \".tif\", \".tiff\", \".bmp\"]\n",
+ "mask_files = [p for p in MASK_DIR.iterdir() if p.suffix.lower() in image_extensions]\n",
+ "\n",
+ "df = pd.DataFrame(mask_files, columns=[\"filepath\"])\n",
+ "print(f\"Found {len(df)} total mask files.\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1764dd13",
+ "metadata": {},
+ "source": [
+ "Size analisis"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6aa25d7a",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-09T17:35:49.832686Z",
+ "iopub.status.busy": "2026-01-09T17:35:49.832471Z",
+ "iopub.status.idle": "2026-01-09T17:35:49.984117Z",
+ "shell.execute_reply": "2026-01-09T17:35:49.983157Z",
+ "shell.execute_reply.started": "2026-01-09T17:35:49.832669Z"
+ },
+ "trusted": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " filepath | \n",
+ " width | \n",
+ " height | \n",
+ " area | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 0 | \n",
+ " /kaggle/input/segmentations-images-automl/pict... | \n",
+ " 768 | \n",
+ " 768 | \n",
+ " 589824 | \n",
+ "
\n",
+ " \n",
+ " | 1 | \n",
+ " /kaggle/input/segmentations-images-automl/pict... | \n",
+ " 768 | \n",
+ " 768 | \n",
+ " 589824 | \n",
+ "
\n",
+ " \n",
+ " | 2 | \n",
+ " /kaggle/input/segmentations-images-automl/pict... | \n",
+ " 768 | \n",
+ " 768 | \n",
+ " 589824 | \n",
+ "
\n",
+ " \n",
+ " | 3 | \n",
+ " /kaggle/input/segmentations-images-automl/pict... | \n",
+ " 768 | \n",
+ " 768 | \n",
+ " 589824 | \n",
+ "
\n",
+ " \n",
+ " | 4 | \n",
+ " /kaggle/input/segmentations-images-automl/pict... | \n",
+ " 942 | \n",
+ " 604 | \n",
+ " 568968 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " filepath width height area\n",
+ "0 /kaggle/input/segmentations-images-automl/pict... 768 768 589824\n",
+ "1 /kaggle/input/segmentations-images-automl/pict... 768 768 589824\n",
+ "2 /kaggle/input/segmentations-images-automl/pict... 768 768 589824\n",
+ "3 /kaggle/input/segmentations-images-automl/pict... 768 768 589824\n",
+ "4 /kaggle/input/segmentations-images-automl/pict... 942 604 568968"
+ ]
+ },
+ "execution_count": 30,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "def get_image_size(path): # noqa: ANN001, ANN201, D103\n",
+ " with Image.open(path) as img:\n",
+ " width, height = img.size\n",
+ " return width, height\n",
+ "\n",
+ "\n",
+ "df[[\"width\", \"height\"]] = df[\"filepath\"].apply(\n",
+ " lambda p: pd.Series(get_image_size(p)),\n",
+ ")\n",
+ "\n",
+ "df[\"area\"] = df[\"width\"] * df[\"height\"]\n",
+ "\n",
+ "df.head()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 31,
+ "id": "ce635580",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-09T17:35:49.986070Z",
+ "iopub.status.busy": "2026-01-09T17:35:49.985745Z",
+ "iopub.status.idle": "2026-01-09T17:35:50.004494Z",
+ "shell.execute_reply": "2026-01-09T17:35:50.003155Z",
+ "shell.execute_reply.started": "2026-01-09T17:35:49.986040Z"
+ },
+ "trusted": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " width | \n",
+ " height | \n",
+ " area | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | count | \n",
+ " 94.000000 | \n",
+ " 94.000000 | \n",
+ " 9.400000e+01 | \n",
+ "
\n",
+ " \n",
+ " | mean | \n",
+ " 878.234043 | \n",
+ " 790.489362 | \n",
+ " 7.582223e+05 | \n",
+ "
\n",
+ " \n",
+ " | std | \n",
+ " 372.466450 | \n",
+ " 191.572931 | \n",
+ " 5.945341e+05 | \n",
+ "
\n",
+ " \n",
+ " | min | \n",
+ " 512.000000 | \n",
+ " 418.000000 | \n",
+ " 2.574880e+05 | \n",
+ "
\n",
+ " \n",
+ " | 25% | \n",
+ " 768.000000 | \n",
+ " 768.000000 | \n",
+ " 5.898240e+05 | \n",
+ "
\n",
+ " \n",
+ " | 50% | \n",
+ " 768.000000 | \n",
+ " 768.000000 | \n",
+ " 5.898240e+05 | \n",
+ "
\n",
+ " \n",
+ " | 75% | \n",
+ " 768.000000 | \n",
+ " 768.000000 | \n",
+ " 5.898240e+05 | \n",
+ "
\n",
+ " \n",
+ " | max | \n",
+ " 2048.000000 | \n",
+ " 1344.000000 | \n",
+ " 2.696080e+06 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " width height area\n",
+ "count 94.000000 94.000000 9.400000e+01\n",
+ "mean 878.234043 790.489362 7.582223e+05\n",
+ "std 372.466450 191.572931 5.945341e+05\n",
+ "min 512.000000 418.000000 2.574880e+05\n",
+ "25% 768.000000 768.000000 5.898240e+05\n",
+ "50% 768.000000 768.000000 5.898240e+05\n",
+ "75% 768.000000 768.000000 5.898240e+05\n",
+ "max 2048.000000 1344.000000 2.696080e+06"
+ ]
+ },
+ "execution_count": 31,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "df[[\"width\", \"height\", \"area\"]].describe()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 32,
+ "id": "46875e13",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-09T17:35:50.006306Z",
+ "iopub.status.busy": "2026-01-09T17:35:50.005961Z",
+ "iopub.status.idle": "2026-01-09T17:35:50.037506Z",
+ "shell.execute_reply": "2026-01-09T17:35:50.036246Z",
+ "shell.execute_reply.started": "2026-01-09T17:35:50.006278Z"
+ },
+ "trusted": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " width | \n",
+ " height | \n",
+ " count | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 2 | \n",
+ " 768 | \n",
+ " 768 | \n",
+ " 70 | \n",
+ "
\n",
+ " \n",
+ " | 0 | \n",
+ " 512 | \n",
+ " 512 | \n",
+ " 9 | \n",
+ "
\n",
+ " \n",
+ " | 1 | \n",
+ " 616 | \n",
+ " 418 | \n",
+ " 1 | \n",
+ "
\n",
+ " \n",
+ " | 3 | \n",
+ " 942 | \n",
+ " 604 | \n",
+ " 1 | \n",
+ "
\n",
+ " \n",
+ " | 4 | \n",
+ " 1058 | \n",
+ " 720 | \n",
+ " 1 | \n",
+ "
\n",
+ " \n",
+ " | 5 | \n",
+ " 1390 | \n",
+ " 834 | \n",
+ " 1 | \n",
+ "
\n",
+ " \n",
+ " | 6 | \n",
+ " 1452 | \n",
+ " 734 | \n",
+ " 1 | \n",
+ "
\n",
+ " \n",
+ " | 7 | \n",
+ " 1574 | \n",
+ " 780 | \n",
+ " 1 | \n",
+ "
\n",
+ " \n",
+ " | 8 | \n",
+ " 1714 | \n",
+ " 1344 | \n",
+ " 1 | \n",
+ "
\n",
+ " \n",
+ " | 9 | \n",
+ " 1732 | \n",
+ " 1296 | \n",
+ " 1 | \n",
+ "
\n",
+ " \n",
+ " | 10 | \n",
+ " 1736 | \n",
+ " 1302 | \n",
+ " 1 | \n",
+ "
\n",
+ " \n",
+ " | 11 | \n",
+ " 1896 | \n",
+ " 1288 | \n",
+ " 1 | \n",
+ "
\n",
+ " \n",
+ " | 12 | \n",
+ " 2002 | \n",
+ " 1332 | \n",
+ " 1 | \n",
+ "
\n",
+ " \n",
+ " | 13 | \n",
+ " 2004 | \n",
+ " 1340 | \n",
+ " 1 | \n",
+ "
\n",
+ " \n",
+ " | 14 | \n",
+ " 2010 | \n",
+ " 1338 | \n",
+ " 1 | \n",
+ "
\n",
+ " \n",
+ " | 15 | \n",
+ " 2012 | \n",
+ " 1340 | \n",
+ " 1 | \n",
+ "
\n",
+ " \n",
+ " | 16 | \n",
+ " 2048 | \n",
+ " 1268 | \n",
+ " 1 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " width height count\n",
+ "2 768 768 70\n",
+ "0 512 512 9\n",
+ "1 616 418 1\n",
+ "3 942 604 1\n",
+ "4 1058 720 1\n",
+ "5 1390 834 1\n",
+ "6 1452 734 1\n",
+ "7 1574 780 1\n",
+ "8 1714 1344 1\n",
+ "9 1732 1296 1\n",
+ "10 1736 1302 1\n",
+ "11 1896 1288 1\n",
+ "12 2002 1332 1\n",
+ "13 2004 1340 1\n",
+ "14 2010 1338 1\n",
+ "15 2012 1340 1\n",
+ "16 2048 1268 1"
+ ]
+ },
+ "execution_count": 32,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "resolution_counts = (\n",
+ " df.groupby([\"width\", \"height\"])\n",
+ " .size()\n",
+ " .reset_index(name=\"count\")\n",
+ " .sort_values(\"count\", ascending=False)\n",
+ ")\n",
+ "\n",
+ "resolution_counts"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9949ef10-2773-4525-adc8-e4eb959e3ad9",
+ "metadata": {},
+ "source": [
+ "Here, we automatically detect the two primary segmentation colors from a sample mask. We'll assume these correspond to your `brittle` and `ductile` categories. The notebook will refer to them as `CATEGORY_A` or `CATEGORY_B`."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 33,
+ "id": "3b9930ab-0188-4056-9a58-82a7e9adc1a3",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-09T17:35:50.039273Z",
+ "iopub.status.busy": "2026-01-09T17:35:50.039073Z",
+ "iopub.status.idle": "2026-01-09T17:35:50.075292Z",
+ "shell.execute_reply": "2026-01-09T17:35:50.073895Z",
+ "shell.execute_reply.started": "2026-01-09T17:35:50.039255Z"
+ },
+ "trusted": true
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "2 colors found in the sample image.\n",
+ "Discovered segmentation colors:\n",
+ " - DUCTILE_COLOR: (255, 0, 0)\n",
+ " - FRAGILE_COLOR: (0, 255, 0)\n"
+ ]
+ }
+ ],
+ "source": [
+ "DUCTILE_COLOR = None\n",
+ "FRAGILE_COLOR = None\n",
+ "\n",
+ "def find_segmentation_colors(sample_path): # noqa: ANN001, ANN201\n",
+ " \"\"\"Analyzes a sample mask to find the two non-background colors.\"\"\"\n",
+ " with Image.open(sample_path) as img:\n",
+ " # Using getcolors() is efficient for images with few colors\n",
+ " colors = img.convert(\"RGB\").getcolors(maxcolors=256)\n",
+ "\n",
+ " print(len(colors), \"colors found in the sample image.\")\n",
+ "\n",
+ " if not colors:\n",
+ " raise ValueError(\"Could not find any colors in the sample image.\")\n",
+ "\n",
+ " # Filter out black/dark colors, assuming they are the background\n",
+ " non_background_colors = [c[1] for c in colors if sum(c[1]) > 30]\n",
+ " non_background_colors.sort(\n",
+ " key=lambda x: x[0], reverse=True,\n",
+ " )\n",
+ "\n",
+ " if len(non_background_colors) < 2:\n",
+ " raise ValueError(\"Expected at least 2 colors\")\n",
+ "\n",
+ " return tuple(non_background_colors[0]), tuple(non_background_colors[1])\n",
+ "\n",
+ "if not df.empty:\n",
+ " try:\n",
+ " DUCTILE_COLOR, FRAGILE_COLOR = find_segmentation_colors(df[\"filepath\"].iloc[0])\n",
+ " print(\"Discovered segmentation colors:\")\n",
+ " print(f\" - DUCTILE_COLOR: {DUCTILE_COLOR}\")\n",
+ " print(f\" - FRAGILE_COLOR: {FRAGILE_COLOR}\")\n",
+ " except (ValueError, IndexError) as e:\n",
+ " print(f\"Could not automatically determine colors: {e}\")\n",
+ "else:\n",
+ " print(\"No mask files found to analyze.\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 34,
+ "id": "ff4b822c-aa38-44fc-9eb3-32cd96f7ed10",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-09T17:35:50.077069Z",
+ "iopub.status.busy": "2026-01-09T17:35:50.076670Z",
+ "iopub.status.idle": "2026-01-09T17:35:55.796777Z",
+ "shell.execute_reply": "2026-01-09T17:35:55.796138Z",
+ "shell.execute_reply.started": "2026-01-09T17:35:50.077045Z"
+ },
+ "trusted": true
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Category assignment complete.\n"
+ ]
+ }
+ ],
+ "source": [
+ "def get_category(mask_path, color_a, color_b) -> str: # noqa: ANN001\n",
+ " \"\"\"Assign a category to a mask based on which color is present.\"\"\"\n",
+ " with Image.open(mask_path) as img:\n",
+ " img_rgb = img.convert(\"RGB\")\n",
+ " np_img = np.array(img_rgb)\n",
+ "\n",
+ " count_a = np.sum(np.all(np_img == color_a, axis=-1))\n",
+ " count_b = np.sum(np.all(np_img == color_b, axis=-1))\n",
+ "\n",
+ " if count_a > count_b:\n",
+ " return \"Ductile\"\n",
+ " elif count_b > count_a:\n",
+ " return \"Fragile\"\n",
+ " return \"MIXED\"\n",
+ "\n",
+ "if DUCTILE_COLOR and FRAGILE_COLOR:\n",
+ " df[\"category\"] = df[\"filepath\"].map(\n",
+ " lambda p: get_category(p, DUCTILE_COLOR, FRAGILE_COLOR),\n",
+ " )\n",
+ " print(\"Category assignment complete.\")\n",
+ "else:\n",
+ " print(\"Skipping category assignment as colors were not identified.\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "30dfaed1-bae4-4252-966b-7dfc5eee7a7f",
+ "metadata": {},
+ "source": [
+ "Now we count the number of masks assigned to each category."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 35,
+ "id": "fe62c69a-39ba-4441-8a09-2eb3e0b83071",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-09T17:35:55.797812Z",
+ "iopub.status.busy": "2026-01-09T17:35:55.797534Z",
+ "iopub.status.idle": "2026-01-09T17:35:55.928532Z",
+ "shell.execute_reply": "2026-01-09T17:35:55.927556Z",
+ "shell.execute_reply.started": "2026-01-09T17:35:55.797785Z"
+ },
+ "trusted": true
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "=== Mask Counts by Category ===\n",
+ "category\n",
+ "Ductile 73\n",
+ "Fragile 21\n",
+ "Name: count, dtype: int64\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "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Q0V+XQDcOAgLAGXS5MmTlZSUpA8++MB4W6mpqQoNDdWdd96pZs2a6b777tN//vMfjRs3Tn5+fmratKnuu+8+JSQk2D0vLS1Nc+bMUdOmTeXt7a3p06crOztbmzdvliStWLFC5cqV04IFC9SkSRN5e3trxowZcnd314oVK+y2FRQUpHvvvVc1a9YscMZjx44pOjpaI0eOVK9evVS3bl0FBwdrypQp2rt3r3bu3Cl3d3fbN7Kenp6yWCz/+N579OihrVu32n4qn5GRoa+//lrdu3fPt26TJk20YcMGPf3007r11lt12223adiwYcrKyrIdhYmPj1fFihXVo0cP1a5dW40bN9b06dO1ZMkSOTs7220vNzdXEyZMUHp6ut59911byOzdu1ctWrSQv7+/atWqpQ4dOuj999/XE088UeR7yczM1HPPPacWLVqoUaNGmjp1qlxcXLRhwwZJF79ZP3HihObPn6+AgAA1bNhQL7zwgry9vbVkyRK7bd1xxx0KCQnRLbfconLl8v+vMj09XdnZ2apdu/Y/fo7zjB8/Xp9++qnatWunWrVqqVWrVurSpYu2bdsmq9WqChUq2D4HFotFVapUUU5Ojt577z317NlTTz75pOrWrWs70rF9+3bb533NmjVq0qSJxo8fr9tuu029evVSnz597F5/69at+uOPPzRr1iwFBQWpbt266t+/v/r166fVq1crOzvb7nP5wgsvqG7dunJzc1P//v21Y8cOu4v9P//8c9WuXVtBQUGX/TkAcH0jLACUSbVq1dLw4cO1YMECpaWlGW3Lzc1NDRo0sH2cd+rKnXfeabfs0lNibr31Vt188822j2vWrKkqVaro4MGDkqQ9e/aoWbNmqly5sm0dV1dXtWjRIt+pQb6+vkXOGB8fL6vVqoCAALvl/v7+kqRff/31H99nQbp166bz58/r888/l3Txm8UKFSronnvuybeum5ubdu/erccee0xBQUHy9/dX3759JV0MEklq166dcnNz9fDDD+ujjz7SoUOH5ObmpmbNmsnFxcVue6+++qp2796tZcuW2UVQp06d9Mknn+j555/Xl19+qVOnTqlu3bqqX79+ke/FxcVFfn5+dvM2aNDAdiH0nj17VLduXdWtW9fueW3atCn21yPvKMWl16cUpVy5clq5cqUeeOABBQQEyN/fXytWrFBWVpbdN/V/d+DAAZ0+fVrt2rXLN7P0v697YmKi3XuXpLvvvtvu47i4OLm6uuZbz9/fX2fOnLH9vZUu/t3/e1B16dJFnp6edkctNm7cqL59+xZ6xAbAjae8owcAgCs1bNgwRUVFKSIiIt9pSsVx6d2P8r5R+vvygr55+vtpLnkqVqyorKwsSdLp06e1f/9+2zf/ebKzs/MdTfh7fBTk9OnTBa6X9xPuzMzMIp9fGC8vL7Vr105RUVHq37+/1qxZo/vvvz9fBEgXj8DMnDlTAwYM0AsvvCBPT08dO3bMdi2AJPn4+Gj16tVatmyZ5s+fr2nTpqlRo0Z69tln1alTJ9t63377rbKysuTq6prvFKlnn31WDRs21GeffWY7f/+ee+5RWFhYkRekV6pUKd/RBTc3N1sQnj59WomJifm+Hjk5OcrJybH75v6fvh5Vq1ZVxYoVdfjw4SLXy2O1WjV06FAdOXJEU6ZMka+vr1xdXbVy5UqtXLmy0Oflfd3DwsI0derUfI+npKRIuhh2l56ydenfsdOnT8vd3T3f3+WC/g5d+nfb1dVVvXv31tq1azVmzBj98ssvOnr0qC0sAUAiLACUYa6urpoyZYrGjBmjhx9+uMB1Lv2Jct43/SWhoG/ms7KybN/geXh4qGbNmgoPD8+3XkGn1xQl7xu9S4+a5H1cUORcrp49e+rZZ5/Vd999p7i4OE2YMKHA9aKjo9W8eXNNmzbNtqygo0Xe3t6aPXu2rFar4uLitHTpUo0ZM0YbN260HXXw8PDQhx9+qOeff952ipCrq6ukixHXq1cv9erVS5mZmYqJidHcuXP17LPPFnnqW1ZWlqxWq903zpmZmbbTlTw8PHTrrbdq6dKlBT6/fPnL/1+is7OzWrVqpa+//lqhoaEFPvfkyZPavHmz+vTpowMHDmjfvn16+eWX7U5RKuxIRZ68o2cTJ05Uhw4d8j2eF0AuLi75Lm7PO4qUx8PDQ5mZmfk+R5f7d6hfv35avny5du7cqS1btuiuu+4q9NQ9ADcmToUCUKbdd999atu2rcLDw/NFhIeHR75vfPMu5C0Jhw8ftl20LF28o9DJkyd1++23S7p4a9xDhw6pVq1aqlevnu0/q9VqdwrV5fD19VW5cuW0Y8cOu+U7d+6UpHyntxRHp06d5O7urlmzZunmm29WYGBggevl5OSoatWqdsvyTo3J+9zv3LlTv/zyi6SLgdC0aVOFh4frwoUL+s9//mN7XvPmzeXt7a2IiAglJSVpxowZkqQzZ87o888/t93xyN3dXV27dtXgwYPzXeNyqbNnz2rPnj22j7OysnTo0CHddtttttc8cuSIKlWqZPf1cHZ2lpeXV7Fjb8iQITp69KgWLlyY7zGr1apXXnlFM2fOVEpKiu0i6r8fRTh9+rS2bNli9/n7+/MlqUGDBvLw8FBiYqLdzHXq1NH58+dt26tXr57de5dku9YnT9OmTXXu3Ll86+3cuVOVKlX6x1PNbrvtNgUGBio6OlobN27kom0A+RAWAMq80NBQ7d27N180NG3aVLt27dKXX36pP//8UytWrCjytqfFVaVKFb3wwgvau3ev9u3bp7CwMLm5ualz586SLt7yNTMzUxMmTFBcXJwSExP1ySefqFevXsX+xWnVq1dX7969tWTJEm3YsEGJiYn66quvNHPmTAUGBqpp06ZX/D4qVKigzp076/fff9eDDz5Y6DfYzZs31/bt2/X999/r8OHDmjt3rnJzc+Xs7Kw9e/YoLS1N//73vzV69Ght2bJFycnJOnjwoN555x1VqFChwPhp0KCBwsLC9PHHH2vLli0qX7685syZo0mTJmnPnj06cuSIdu3apejoaLVu3brI9+Hm5qY5c+Zo586d+v333zVt2jSdP39ePXv2lCT16dNHnp6eGjt2rHbu3KmkpCTbN8h/vyXr5Wrbtq3GjBmjt99+W5MnT9auXbuUnJys7du368knn9TWrVs1d+5c1apVS7fddps8PT31wQcf6NChQ9q9e7eGDRume++9V9LF2w6fOXPGdoQiJiZG+/fvV/ny5TVs2DB99NFHev/99/XHH38oISFBzz//vB566CFb2Hbp0kVxcXF69913dfjwYa1bt84WLXk6depku2D9xx9/1J9//qmVK1fq008/1RNPPKGbbrrpH99z//79bXcRu/QaDgDgVCgAZV6jRo306KOP6l//+pfd8rFjx+rYsWOaPHmynJ2d1blzZ40fP15jxowpkde9/fbb1bt3b40fP17//e9/Vb9+fb399tuqXr26pIs/RV65cqVef/11DRo0SDk5Oapfv74mT56sAQMGFPv1pk2bJovFonnz5iklJUVVq1bVfffdV+ipS8XRo0cPRUVFFXg3qDzjxo1TSkqKnn76abm6uqpHjx6aOnWq3Nzc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yszMtC1LT0/nQkAAMJCamqrk5GQNHDjQdhTh119/tT3u5eWlkydP2u1r4+Li/nG7devWVVZWlt1+/vTp00pPTy/B6YFrg7AArqJjx45p5syZ+uqrrzR9+nRZLBZVrlxZW7Zs0YULF7Rt2zbt3r3btv6DDz6oI0eO6JNPPlF2drY+//xzvfbaa3J3d1eFChUkSYcOHVJWVlahr9m+fXtZLBbNnj1bp0+fVkpKip555hnNmzfvar9dALhuWSwWubm5affu3Tp37pzWr1+vhIQEnT59WpmZmfL19VXFihW1dOlSZWdn69tvv9WPP/74j9u944475O/vr+nTpystLU1//fWXpk6dqkmTJl2DdwWULMICKGHh4eHy8/OTr6+vevTooWPHjikyMlJNmzaVs7Ozpk6dqjVr1iggIEBr167Vo48+antutWrV9N5772nFihVq1aqVlixZorffflsWi0V33nmn/P39FRISoo8++qjQ17/pppu0cOFCHTx4UO3atVOvXr1Uv359TZ48+Vq8fQC4LpUvX17Tpk3TkiVLFBQUpB07dmjBggWqWbOm7r//frm7u+uNN97Q2rVrFRgYqHXr1unxxx+/rNOhIiIiZLVa1alTJ9133326cOGCZs2adQ3eFVCy+M3bAAAAJSDvmrm8G3XMnz9fP/zwgz788ENHjgVcMxyxAAAAMGS1WvXAAw/o9ddfV05Ojg4fPqy1a9cqODjY0aMB1wxHLAAAAEpAfHy8pk+frn379qly5crq3LmzJk6cyO2+ccMgLAAAAAAY41QoAAAAAMYICwAAAADGCAsAAAAAxggLAAAAAMYICwAAAADGCAsAAAAAxggLAIDNwYMHNWHCBAUFBalZs2bq2LGjwsPDlZGRcVnPX758uc6fP391hwQAlEqEBQBAkpSQkKCQkBDVrFlT0dHR2rVrl95++23t379fAwYM0NmzZ4t8flpammbPnq0LFy5co4kBAKUJYQEAkCS98sorat++vSZOnKhq1arJ2dlZd955pxYtWqTmzZvr+PHjiouL0yOPPKKAgAAFBQVp6tSpysnJUWpqqjp06CCr1aqAgABFRUVJkjZu3KiePXuqefPm6tSpk1avXm17vTNnzuiZZ55R06ZN1blzZ8XGxqpJkybavn27JOnkyZOaNGmS2rdvL39/fz355JNKSkqSJCUlJcnb21sffvihWrdurTVr1qhx48bav3+/3Xu699577V4TAHD1lHf0AAAAxztx4oR27dqllStX5nusUqVKmjlzpqSL36j36NFDK1eu1LFjx9S/f381atRIAwcO1HvvvadBgwbpp59+kqurq+Li4hQaGqoFCxaobdu2+vnnnzV8+HDdfvvtatGihSIiIrR//35t2bJF5cqV05QpU+xOowoLC9Pp06cVHR0tFxcXvfDCCxo3bpw+/fRT2zo//vijvv76a7m7uysqKkrr16+Xt7e3pItHYI4ePaoHHnjgKn/2AAASRywAAJISExMlSQ0aNChyvbVr12rkyJFydnbWLbfcolatWik+Pr7AdaOionT33Xerffv2cnZ2VkBAgLp06aJ169ZJkr799lv1799fNWvW1M0336whQ4bYnpuRkaGtW7dq3LhxslgsqlSpksaOHau4uDjbrJLUq1cvVapUSU5OTurVq5c+//xzWa1WSdKWLVsUHBwsT09Po88NAODyEBYAADk5OUmScnNzi1zvhx9+0MMPPyx/f3/5+flp48aNys7OLnDdP//8U5s3b5afn5/tv+joaB07dkySlJKSojp16tjW9/Pzs/35v//9r6xWqxo2bGhbVrduXUlScnKybdktt9xi+3Pnzp2Vnp6un376SZK0detWde/e/bLePwDAHKdCAQBs37T/9ttvqlGjRoHrHDhwQM8884wmT56sfv36qUKFCpo4cWKhd4GqUKGCBgwYoBdffLHAx3Nzc1W+/P/+N1Su3P9+1lVYrEj/iyBJcnZ2tv25UqVK6tSpk9avX6/q1avr6NGjuueeewrdDgCgZHHEAgCgqlWrqnXr1lq+fHm+x86cOaM+ffpoy5YtcnFx0aBBg1ShQgVZrVYlJCQUus26devmu5j66NGjtrtGeXl52R19iIuLs/351ltvlXTx9rd58v6cF0EF6dWrl7Zu3aoNGzbo/vvvl6ura1FvGwBQgggLAIAkKTQ0VLt379azzz6ro0ePKjc3VwkJCRo2bJgqVKigVq1a6ezZs0pISNDJkyc1d+5cubi46Pjx47JarapQoYIk6dChQ8rKylJISIh27dqlzz77TNnZ2UpISNBDDz2kzZs3S5ICAwP18ccf6/jx4zp+/LhWrFhhm8XLy0vt27fXm2++qYyMDJ08eVJvvPGGAgMDVatWrULfQ1BQkJydnbV8+XJOgwKAa4ywAABIkho3bqxPPvlEubm56t27t/z9/TVu3Di1adNGy5YtU0BAgB599FE99thj6tatm2rXrq0XXnhB//nPfzR+/Hjdeeed8vf3V0hIiD766CM1bNhQERERevfddxUQEKAxY8Zo6NCh6tq1qyRp0qRJ8vLyUqdOnTR8+HANHTpU0v9OiZo9e7bc3NzUpUsXde3aVZUqVdKbb75Z5HtwdnZW9+7d5ebmpsDAwKv7CQMA2HGy5t0+AwCAayw7O1suLi6SLt6Z6t5779XWrVuLPN3pn0yePFm1atXSuHHjSmhKAMDl4IgFAMAh3n77bfXu3VvHjx/X2bNntWTJEjVs2NDuTlHF9dVXX+mbb77Ro48+WoKTAgAuB3eFAgA4xNChQ3X06FH17NlTOTk58vHx0fz58+3uDlUcDzzwgLKzszVnzhxVr169hKcFAPwTToUCAAAAYIxToQAAAAAYIywAAAAAGCMsAAAAABgjLAAAAAAYIywAAAAAGCMsAAAAABgjLAAAAAAYIywAAAAAGCMsAAAAABj7f59ML6EmTwl2AAAAAElFTkSuQmCC",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "if \"category\" in df.columns:\n",
+ " category_counts = df[\"category\"].value_counts()\n",
+ "\n",
+ " print(\"=== Mask Counts by Category ===\")\n",
+ " print(category_counts)\n",
+ "\n",
+ " fig, ax = plt.subplots(figsize=(8, 6))\n",
+ " category_counts.plot(kind=\"bar\", ax=ax, rot=0)\n",
+ " ax.set_title(\"Number of Masks per Category\")\n",
+ " ax.set_ylabel(\"Count\")\n",
+ " ax.set_xlabel(\"Category\")\n",
+ " plt.tight_layout()\n",
+ " plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "320d81ad-8f6e-471a-8c41-a613b1402dcc",
+ "metadata": {},
+ "source": [
+ "This is the main analysis, where we count the pixels for each category in every mask."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 36,
+ "id": "4aabce44-6224-41c8-8f78-92537fff4e30",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-09T17:35:55.929861Z",
+ "iopub.status.busy": "2026-01-09T17:35:55.929621Z",
+ "iopub.status.idle": "2026-01-09T17:36:01.707782Z",
+ "shell.execute_reply": "2026-01-09T17:36:01.706652Z",
+ "shell.execute_reply.started": "2026-01-09T17:35:55.929842Z"
+ },
+ "trusted": true
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Pixel counting and percentage calculation complete.\n"
+ ]
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " filepath | \n",
+ " width | \n",
+ " height | \n",
+ " area | \n",
+ " category | \n",
+ " pixels_a | \n",
+ " pixels_b | \n",
+ " pixels_bg | \n",
+ " total_pixels | \n",
+ " percent_a | \n",
+ " percent_b | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 0 | \n",
+ " /kaggle/input/segmentations-images-automl/pict... | \n",
+ " 768 | \n",
+ " 768 | \n",
+ " 589824 | \n",
+ " Ductile | \n",
+ " 495549 | \n",
+ " 94275 | \n",
+ " 0 | \n",
+ " 589824 | \n",
+ " 84.016418 | \n",
+ " 15.983582 | \n",
+ "
\n",
+ " \n",
+ " | 1 | \n",
+ " /kaggle/input/segmentations-images-automl/pict... | \n",
+ " 768 | \n",
+ " 768 | \n",
+ " 589824 | \n",
+ " Ductile | \n",
+ " 477795 | \n",
+ " 112029 | \n",
+ " 0 | \n",
+ " 589824 | \n",
+ " 81.006368 | \n",
+ " 18.993632 | \n",
+ "
\n",
+ " \n",
+ " | 2 | \n",
+ " /kaggle/input/segmentations-images-automl/pict... | \n",
+ " 768 | \n",
+ " 768 | \n",
+ " 589824 | \n",
+ " Fragile | \n",
+ " 210397 | \n",
+ " 379427 | \n",
+ " 0 | \n",
+ " 589824 | \n",
+ " 35.671149 | \n",
+ " 64.328851 | \n",
+ "
\n",
+ " \n",
+ " | 3 | \n",
+ " /kaggle/input/segmentations-images-automl/pict... | \n",
+ " 768 | \n",
+ " 768 | \n",
+ " 589824 | \n",
+ " Ductile | \n",
+ " 447846 | \n",
+ " 141978 | \n",
+ " 0 | \n",
+ " 589824 | \n",
+ " 75.928752 | \n",
+ " 24.071248 | \n",
+ "
\n",
+ " \n",
+ " | 4 | \n",
+ " /kaggle/input/segmentations-images-automl/pict... | \n",
+ " 942 | \n",
+ " 604 | \n",
+ " 568968 | \n",
+ " Fragile | \n",
+ " 81201 | \n",
+ " 487583 | \n",
+ " 184 | \n",
+ " 568968 | \n",
+ " 14.271629 | \n",
+ " 85.696032 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " filepath width height area \\\n",
+ "0 /kaggle/input/segmentations-images-automl/pict... 768 768 589824 \n",
+ "1 /kaggle/input/segmentations-images-automl/pict... 768 768 589824 \n",
+ "2 /kaggle/input/segmentations-images-automl/pict... 768 768 589824 \n",
+ "3 /kaggle/input/segmentations-images-automl/pict... 768 768 589824 \n",
+ "4 /kaggle/input/segmentations-images-automl/pict... 942 604 568968 \n",
+ "\n",
+ " category pixels_a pixels_b pixels_bg total_pixels percent_a percent_b \n",
+ "0 Ductile 495549 94275 0 589824 84.016418 15.983582 \n",
+ "1 Ductile 477795 112029 0 589824 81.006368 18.993632 \n",
+ "2 Fragile 210397 379427 0 589824 35.671149 64.328851 \n",
+ "3 Ductile 447846 141978 0 589824 75.928752 24.071248 \n",
+ "4 Fragile 81201 487583 184 568968 14.271629 85.696032 "
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "def count_pixels(mask_path, color_a, color_b): # noqa: ANN001, ANN201\n",
+ " \"\"\"Count the pixels for each category in a single mask.\"\"\"\n",
+ " with Image.open(mask_path) as img:\n",
+ " img_rgb = img.convert(\"RGB\")\n",
+ " np_img = np.array(img_rgb)\n",
+ "\n",
+ " total_pixels = np_img.shape[0] * np_img.shape[1]\n",
+ " pixels_a = np.sum(np.all(np_img == color_a, axis=-1))\n",
+ " pixels_b = np.sum(np.all(np_img == color_b, axis=-1))\n",
+ "\n",
+ " return {\n",
+ " \"pixels_a\": pixels_a,\n",
+ " \"pixels_b\": pixels_b,\n",
+ " \"pixels_bg\": total_pixels - pixels_a - pixels_b,\n",
+ " }\n",
+ "\n",
+ "if DUCTILE_COLOR and FRAGILE_COLOR:\n",
+ " pixel_counts = df[\"filepath\"].map(\n",
+ " lambda p: count_pixels(p, DUCTILE_COLOR, FRAGILE_COLOR),\n",
+ " )\n",
+ " df = pd.concat([df, pd.DataFrame(pixel_counts.to_list())], axis=1)\n",
+ "\n",
+ " # Calculate percentages\n",
+ " df[\"total_pixels\"] = df[\"pixels_a\"] + df[\"pixels_b\"] + df[\"pixels_bg\"]\n",
+ " df[\"percent_a\"] = df[\"pixels_a\"] / df[\"total_pixels\"] * 100\n",
+ " df[\"percent_b\"] = df[\"pixels_b\"] / df[\"total_pixels\"] * 100\n",
+ "\n",
+ " print(\"Pixel counting and percentage calculation complete.\")\n",
+ " display(df.head())\n",
+ "else:\n",
+ " print(\"Skipping pixel analysis as colors were not identified.\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 37,
+ "id": "158881af-bc9c-4116-95f6-50f38f38e924",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-09T17:36:01.709555Z",
+ "iopub.status.busy": "2026-01-09T17:36:01.709272Z",
+ "iopub.status.idle": "2026-01-09T17:36:02.001542Z",
+ "shell.execute_reply": "2026-01-09T17:36:02.000497Z",
+ "shell.execute_reply.started": "2026-01-09T17:36:01.709529Z"
+ },
+ "trusted": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAABKUAAAGGCAYAAACqvTJ0AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAQm5JREFUeJzt3XlcVIX+//E3oGAKorgWkpYLqaBirriluJvrN01NzSVTcw+3m0uWGnndMs29tLRyJbcoy8hyy0yz0MzKJZWLG0qIqajM7w9/TCJgMAxnhsPr+XjwuHK2+cx8mOZz33PmjIvFYrEIAAAAAAAAMJCrowsAAAAAAABA7kMoBQAAAAAAAMMRSgEAAAAAAMBwhFIAAAAAAAAwHKEUAAAAAAAADEcoBQAAAAAAAMMRSgEAAAAAAMBwhFIAAAAAAAAwHKEUAAAAAAAADEcoBZjYuHHj5O/vb/0JCAjQU089pREjRmjv3r2ptm/SpIlGjhyZLXXUq1cv229Hknr27KkuXbpky7Ft9eGHHyo4OFiBgYGKiYmx+TjZ8bjNmzdP/v7+unnzZpaPtXHjRvn7+6tz5852qAwAgIxh3nEOZp937v87q1ixourVq6f+/fun+XcGIGPyOLoAANnLx8dHmzdvliQlJibq9OnT2rp1q/r27avnn39e48aNs267fv165c2bN8PH3rBhgzZu3KiVK1c+cLvx48fr1q1btt2Bf9G4cWO9+eabql27tqS7Q4ezmTFjhp588km9/vrrKlasmM3HyWx/jLZu3Tr5+/vr559/1u+//67y5cs7uiQAQC7BvON4uWHeuffvLCkpSTExMXrnnXfUt29frVu3TgEBAQ6uEMh5CKUAk3N1dU0xGPj6+qpu3bqqW7euQkNDVa5cOT3zzDOS7r7QZsaPP/6Yoe28vLwyddyMOn/+vP73v/+lWFaoUKFsuS1bJSYm6vr166pWrZp8fX2zdKzM9sdIJ0+e1A8//KB3331X48eP14YNG1L8HwAAALIT845j5ZZ55/6/sxIlSigsLEz16tXTjh07CKUAG/DxPSCXevrpp1W3bl0tWbLEuuz+06XXrFmjtm3bqlq1aqpZs6b69u2rI0eOSLp72vi6dev0/fffy9/fX+Hh4dq3b5/8/f312WefqW3btqpbt66k1KezJ/vwww/VuHFjBQQEqFOnTjp06JB1XVr7nD17Vv7+/vr444+1b98+NWzYUJLUq1cvNWnSxFrXvaezJyYmatasWWrSpIkCAgIUHByscePGKTY2NsVttW/fXvv27VOnTp1UtWpVNWvWTJ988sm/Po7h4eFq27atAgMD9eSTT6pfv346fPiwJGnfvn0KDAyUJM2fP1/+/v46e/ZsqmMk368NGzZo0qRJqlWrlqpVq6bBgwenqPPe/rz22muqXbu2Ll++bF2f/Phv2rTJumzTpk3q3Lmzqlevrlq1amnkyJE6f/58uvfn119/Vf/+/VWnTh1VqVJFrVu3/td3hqW772qWLFlSwcHB6tChgzZv3pxt7xYDAJBRzDvMO2mxdd55EGcLCoGcglAKyMVCQkL0559/pnr3TZL27t2ryZMnq0+fPvr000+1cuVKeXt7q2/fvrp+/brmzZunypUrKygoSLt27VLr1q2t+y5atEjDhw9/4JBz8OBB7du3TwsXLtTHH38si8WiQYMG6e+//85Q7UFBQZo1a5aku6ewr1+/Ps3tJkyYoI8++kjDhg1TRESEwsLCtG/fPvXv318Wi8W63eXLlzV//nxNmDBBGzduVNmyZTVx4sQHXhNh/fr1+s9//qOmTZtq48aNWrFihW7duqVevXrp3LlzCgoKUmRkpCSpb9++2rVrlx5++OF0jzdv3jyVLl1aa9eu1ezZs7V//36NHTs2zW1Hjx4tb29vvfHGG5LuDqOTJ09WixYt1L59e0l3B7QxY8aoWrVqCg8P14IFC3TixAn17t1biYmJaR534MCB8vT01MqVKxUREaHevXtr+vTpioiISLfu27dva+PGjerYsaNcXV3VqVMnxcbGaseOHenuAwCAUZh3mHfuZ8u8k5ZLly4pLCxMJUqUSPG3ASDjCKWAXCx5YLh48WKqdYcPH9ZDDz2kdu3aydfXV0888YSmTZumJUuWyM3NTYUKFVKePHmUN29eFStWTPny5bPuGxwcrKZNm6pkyZLp3vbff/+tGTNm6IknnlBgYKAmTJigy5cva/fu3Rmq3d3dXQULFpQkeXt7p3mq9/nz57V582YNHDhQHTp00KOPPqpGjRpp3LhxOnLkiA4cOGDd9sKFC5o4caKqV6+uxx57TP369dOtW7f0yy+/pFvD0qVL1bBhQw0fPlxly5ZVYGCgZs+erRs3big8PFzu7u4qWrSoJCl//vwqVqyY3Nzc0j1euXLl1K9fP5UpU0ZNmjTRc889p127dunKlSupts2fP7/efPNNRUREaM+ePVq6dKni4+M1efJk6zaLFi1SzZo1NX78eJUpU0Y1atTQm2++qRMnTmjbtm2pjhkbG6uYmBg1a9ZM5cuXV6lSpdSlSxetXbtWNWvWTLfuHTt2KDY2Vv/3f/8nSSpdurRq1aqlDRs2pLsPAABGYd5h3rmXrfNO8r5BQUEKCgpSlSpVVK9ePf3444+aM2eOU3/sEHBmhFJALnb79m1JSnNwqFevnpKSkvTss8/q448/1smTJ5U/f35VrVpV7u7uDzxuRj5PHxAQIA8PD+vv/v7+kqQTJ05k5i480OHDh2WxWFSjRo0Uy4OCgiQpxQCWP39+VahQwfp78mARHx+f5rETEhJ06tSpVMcuWrSo/Pz8Hjjcpef+Y1WuXFkWiyXNd3YlqXr16urTp48mTJigxYsX6/XXX7fWnZCQoBMnTqT6SEDFihVVqFChNOvz8fFRUFCQJk+erNmzZ+v777/XrVu3VKlSpQdesHTdunWqXbu2/Pz8rMv+7//+Tzt37kzz/wAAAGAk5h3mnXvZOu9Idz+it3HjRm3cuFFbtmzRmjVr1KlTJ/Xt21cfffRRZh4GAP8foRSQi/35559ycXHRI488kmpdpUqVtGbNGpUrV05vv/22WrZsqTZt2uirr7761+Nm5EKfye/6JcufP78kZfh09oxISEhIsx5PT09J0rVr11Ld/v3uPeU9rWMnH+v+49977Iy6v87kmtIbFCWpe/fu+t///icfHx81atQoVX3vvPOO9R295J+//vpLFy5cSHUsFxcXvfvuu+rTp4927Nihnj17qm7dupo+fXq6p7+fP39eO3fu1HfffZfia5LHjh2r27dvZ+g6FQAAZCfmHeade9ky7yRzc3NT6dKlrT/VqlXTkCFD1Lt3b7355pv666+/Mvw4ALiLb98DcrFt27apcuXK6Z5u7O/vr+nTp8tisSgqKkpLly7V0KFDFRERoTJlymTptu8fYpKHswIFCki6OzDcPyBldoBLHgSvXr2aYnny7/cPipmRPJwlD0P3SkhIsOmbZ+5/TJJ/9/b2TnefKVOmqGrVqjp9+rQWLVqkIUOGSPpn4Ovdu7c6d+6car/0htICBQpo0KBBGjRokC5cuKAtW7Zo7ty5ypcvn4YPH55q+/DwcOXPn1/vv/++XF1Tvs+xbNkyhYeH68UXX3zAvQYAIHsx7zDv3C+z886/CQwM1M2bN/Xnn3+qSpUqmd4fyM04UwrIpVauXKkjR45o4MCBaa4/cOCAfvrpJ0l3B6YqVapo6tSpunPnjn777Tfrdum9s/Zvfv75Z924ccP6e/K33JQvX17S3SEjPj7eesq9JGs990uvhoCAALm6umr//v0plidfWyH5m2Js4enpqXLlyqU69oULF3TmzBmbjv3999+n+P3w4cPKmzdvio/F3Ss8PFy7d+9WWFiYXnnlFS1cuND6TTgFChRQhQoVdPLkyRTv6JUuXVqJiYkqUqRIquOdP38+xQU+ixcvrn79+qlevXo6evRoqu0tFos2bNigpk2bqnLlyqpYsWKKn2eeeUYnT55McS0LAACMxLzDvHO/zM47GZH8cczixYvbtD+QmxFKASaXlJSkixcv6uLFizp//rx+/PFHTZw4UdOmTdOAAQPUrFmzNPf7+uuv9dJLL+mLL75QdHS0Tpw4oUWLFilfvnzWAaRgwYI6deqUoqKiHvitLWnJly+fxo8fr99++00///yzpk2bphIlSig4OFiSVKVKFd26dUuLFi3SmTNntH37doWHh6c4RvI7art379Yvv/ySalgrVqyYOnbsqCVLlmjr1q06c+aMvvrqK4WFhal27dpZfierf//+2rlzp+bPn69Tp07p0KFDGj58uAoVKmS96Hdm/Pbbb1qyZIlOnTqlyMhIffTRR2ratGmaHw84d+6c3njjDQ0ePFiPP/642rZtq3r16mns2LHWU88HDBigr776SvPmzdPx48f1xx9/aPr06erYsWOa11iIj49XaGioZs2apT/++EMxMTHavn27Dh48qFq1aqXa/rvvvtOZM2fS/baZWrVqqWjRolzwHACQ7Zh3mHeya965171/ZxcvXtSpU6e0Zs0aLViwQM8999wDL3oPIG18fA8wucuXL6t+/fqS7r4D6O3trapVq2rZsmXW5WkZPny43NzcNH36dF24cEH58+dXxYoVtXTpUuu32PTp00djxoxR9+7d9fLLL6tSpUoZrqt+/fqqUKGC+vfvr9jYWFWsWFGLFi2yXgy0devWOnTokD766CMtW7ZMQUFBmjJlitq0aWM9RmBgoEJCQrR8+XJt2LBBO3fuTHU7kydPlo+Pj2bOnKmLFy+qcOHCatas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OCdCKQAAAAAAABiOUAoAAAAAAACGI5QCAAAAAACA4QilAAAAAAAAYDhCKQAAAAAAABiOUAoAAAAAAACGI5QCAAAAAACA4QilAOR4PXv21MyZMx12++fPn1enTp1UtWpVxcTEZGrfjRs3qkmTJlmuYd68eerSpUuWjwMgpZ07dyo4OFgjR47M9L4HDx5Up06dVKVKFTVv3lxbtmzJhgoB5CbMPMw8QHZx1MxDKAXArpo0aaKGDRvq77//TrF83759dhlEnNFnn32m2NhY7du3Tw8//HCm9u3QoYMiIyOzqTIAWbF06VJNnTpVpUuXzvS+Fy5c0MCBA9WrVy/t379f48eP1+LFixUXF2f/QgE4BDMPMw9gFo6ceQilANhdYmKiFixY4OgyDJOQkKASJUooX758ji4FgB15eHho/fr16Q5oERERat++vapVq6aQkBCtWbPGum7t2rWqXr26OnToIA8PDzVq1Ehbt25VoUKFDKoegBGYeQCYgSNnHkIpAHY3dOhQffjhhzp58mSa68+ePSt/f38dP37cumzmzJnq2bOnpLvvMFavXl1fffWVmjRpoqCgIL311luKiopSu3btFBQUpCFDhujWrVvW/W/cuKHQ0FAFBQWpWbNm+vzzz63r4uLiNGrUKNWvX19BQUEaNGiQzp8/n6KWjz76SLVq1dLWrVvTrHn16tVq1aqVqlatqpYtWyoiIkKS9NZbb2nBggX6+eefFRgYqOjo6DTv67Zt29SmTRtVqVJFPXr00MWLFyVJ4eHhqlevniRp9uzZ6ty5sywWiyTp999/V2BgoKKioiRJv/76q55//nnVqFFDderU0dSpU1M8BsmuX7+usWPHqm7dugoKClLXrl11+PDh9NoFIB29evWSl5dXmuuioqI0fvx4jR49WgcOHND06dP15ptv6uDBg5KkAwcOyM/PTy+99JKefPJJtW/fXrt37zayfAAGYOZJfV+ZeYCcx5EzD6EUALsrV66cunTpoqlTp9p8jOvXr2vv3r369NNP9eqrr2rRokVasGCBVqxYofDwcH3zzTcpTgHftGmTWrdurX379qlHjx4aNWqUdQgbN26cbty4oU8//VQ7d+5U/vz59Z///CfF7X3//feKjIxUmzZtUtUSGRmpGTNmaMqUKfrhhx80bNgwjR49WseOHdOIESM0aNAgValSRVFRUfL19U3z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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "fig, axes = plt.subplots(1, 2, figsize=(12, 4), sharey=True)\n",
+ "\n",
+ "# Histogram for pixels A\n",
+ "axes[0].hist(df[\"pixels_a\"], bins=50)\n",
+ "axes[0].set_title(\"Distribution of pixels A\")\n",
+ "axes[0].set_xlabel(\"Number of pixels\")\n",
+ "axes[0].set_ylabel(\"Frequency\")\n",
+ "\n",
+ "# Histogram for pixels B\n",
+ "axes[1].hist(df[\"pixels_b\"], bins=50)\n",
+ "axes[1].set_title(\"Distribution of pixels B\")\n",
+ "axes[1].set_xlabel(\"Number of pixels\")\n",
+ "\n",
+ "plt.tight_layout()\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "25e0a0ef-fa9c-4f5f-8870-db7f6e4d8ca7",
+ "metadata": {},
+ "source": [
+ "This section creates an aggregate heatmap to show if labels for each category tend to appear in specific regions. We assume that every image is squared."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 38,
+ "id": "6cb15e33-a7d4-4b8b-b226-cb541f996a3e",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-09T17:36:02.004162Z",
+ "iopub.status.busy": "2026-01-09T17:36:02.003877Z",
+ "iopub.status.idle": "2026-01-09T17:36:04.617883Z",
+ "shell.execute_reply": "2026-01-09T17:36:04.616855Z",
+ "shell.execute_reply.started": "2026-01-09T17:36:02.004142Z"
+ },
+ "trusted": true
+ },
+ "outputs": [],
+ "source": [
+ "H, W = 512, 512\n",
+ "num_images = len(df)\n",
+ "\n",
+ "heatmap_a = np.zeros((H, W), dtype=np.float64)\n",
+ "heatmap_b = np.zeros((H, W), dtype=np.float64)\n",
+ "\n",
+ "for path in df[\"filepath\"]:\n",
+ " with Image.open(path) as img:\n",
+ " img_rgb = (\n",
+ " img.convert(\"RGB\")\n",
+ " .resize((W, H), resample=Image.NEAREST)\n",
+ " )\n",
+ " np_img = np.array(img_rgb)\n",
+ "\n",
+ " mask_a = np.all(np_img == DUCTILE_COLOR, axis=-1)\n",
+ " mask_b = np.all(np_img == FRAGILE_COLOR, axis=-1)\n",
+ "\n",
+ " heatmap_a += mask_a\n",
+ " heatmap_b += mask_b\n",
+ "\n",
+ "# Normalización: frecuencia relativa por píxel\n",
+ "heatmap_a /= num_images\n",
+ "heatmap_b /= num_images\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 39,
+ "id": "788066bf",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-01-09T17:36:04.619209Z",
+ "iopub.status.busy": "2026-01-09T17:36:04.619032Z",
+ "iopub.status.idle": "2026-01-09T17:36:04.956864Z",
+ "shell.execute_reply": "2026-01-09T17:36:04.955783Z",
+ "shell.execute_reply.started": "2026-01-09T17:36:04.619191Z"
+ },
+ "trusted": true
+ },
+ "outputs": [
+ {
+ "data": {
+ "image/png": "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VVU9+OCDN/2d3/qt36rNzc161ateVffcc0998Rd/cf3dv/t362lPe1p94hOfqLW1tRofH6+ZmZm6du1aTU1NfcrrnZ6e1q/+6q/Ws571rPq+7/u++tzP/dzGxrzzne+s//Af/sN/1juO2qg9mtuoRnyb2tjYWP3QD/1QPf/5z6+3ve1t9Qd/8Af1B3/wB/W//C//S73uda+rl770pfW85z2vff7JT37ygPjonnvuqaqLTK+qanZ2tt72trfVi1/84vrEJz5Rx8fHTQ27tbV10881Pj5eb3zjG+ttb3tb3bhxo9Hnp6endXJyclMCqA996ENVVXXvvfcO/PypT31q+91we9rTnlZra2v1Xd/1XfXsZz+7vuZrvqa++Iu/+KYyqeH7PFz7nM/5nLrjjjva/++6665aWVmpP/uzP/u0373Z9md/9me1v79fT3/60wd+/qVf+qU1PT1df/Inf1LPetaz2s+/7Mu+rP17bW2tquq20LDKI53OzbuD97znPfX4xz++PXfVBc2dJZXLtD/90z+tvb29h1DiX/3VX11VVX/yJ39ST3va0/6Trj1qn9k2qhFfvo0C8W1ui4uL9S3f8i31Ld/yLVVV9b73va9e9KIX1ate9ar65m/+5rpy5Ur7XLa5ubmq6jvuv//3/379s3/2z+oHfuAH6hu/8RtrYWGh3v3ud9eLXvSim36WXq9Xf+tv/a26//7768UvfnHde++9NT09XW984xvrjW98401fBwXrGbVPJaC666676s1vfnP96q/+av3ar/1avfrVr67HPe5x9f3f//31Hd/xHZ/yfjcjmFpaWnrIz2ZnZ+vg4ODTfvdm297e3sM+z/j4eM3NzT1EmZz9wXn9RazD937v99b//X//35/y/vfdd1/97b/9ty/93OrXj3vc4276O7u7u4+oIE7fvfSlL62XvexlD/n99evXH7F7jdqtbaNAfPk2CsS3qREFDWeY99xzT/33//1/Xz/4gz9Yf/Znf9YC8bAT9//l5eWqqvrd3/3deuYzn1k//MM/3D7z3ve+91LP9KEPfag+8IEP1Mtf/vIBZfDJycmlriMAHx4e1uzsbPv5p6uDP/7xj6+Xvexl9bKXvaz+n//n/6k3vvGN9dKXvrQe//jH19d8zddc6hmG28Mtzzk4OGjB5OEC4WWX9Aj2w+95fn5e+/v7/1kK61e+8pV1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",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAAeIAAAHiCAYAAAA06c+jAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs/Xm07WtVJvbPtXbfN+ec2wBqlaKUcpHCoGgpopIRCCbBBiONDNBSsbRwiJZKYzOoojGKw4YUIqiAhBgbqhTiSKkkGovEsi0EKZEUEEW43NPsvm9/f+zf513PWvcCe1fdw7mS9Y5xxjln77W+37eZ75zPfOZ859s5PT09rWEbtmEbtmEbtmG7Ja17qzswbMM2bMM2bMP2/+U2NMTDNmzDNmzDNmy3sA0N8bAN27AN27AN2y1sQ0M8bMM2bMM2bMN2C9vQEA/bsA3bsA3bsN3CNjTEwzZswzZswzZst7ANDfGwDduwDduwDdstbENDPGzDNmzDNmzDdgvb0BAP27AN27AN27DdwjZ6qzswbMM2bMM2bJ88rdPp3LRnf7IWghx6xMM2bMM2bMM2bLewDT3iYRu2YRu2Ybvf2s30iD9Z29AjHrZhG7ZhG7Zhu4Vt6BEP27AN27AN2/3Whh7xxdvQEA/bsA3bsA3b/daGhvjibUhND9uwDduwDduw3cI29IiHbdiGbdiG7X5r3e7Qv7toG87YsA3bsA3bsA3bLWxDj3jYhm3Yhm3Y7rc2jBFfvA094mEbtmEbtmEbtlvYhh7xsA3bsA3bsN1vbegRX7wNPeJhG7ZhG7ZhG7Zb2IYe8bAN27AN27Ddb23oEV+8DQ3xsA3bsA3bsN1vbWiIL96G1PSwDduwDduwDdstbEOPeNiGbdiGbdjutzb0iC/ehh7xsA3bsA3bsA3bLWxDj3jYhm3Yhm3Y7rc2LHF58TacsWEbtmEbtmEbtlvYhh7xsA3bsA3bsN1vbRgjvngbesTDNmzDNmzDNmy3sA094mEbtmEbtmG739rQI754GxriYRu2YRu2Ybvf2tAQX7wNqelhG7ZhG7ZhG7Zb2IaG+BPc3vGOd9Tznve8+vIv//K666676lGPelQ95SlPqV/+5V/+hPXh+c9/fn3xF3/xhb7zyle+sh72sIfV/v7+hb73ile8oh72sIe1/z/zmc+s//6//+8v9IzztMH+3az3VP2nzd+tau9///vrBS94QX3Zl31Z3XXXXfWP/tE/qmc+85n1G7/xG32fe6CM6W//9m/rYQ97WN+fRz3qUfWEJzyhfuInfqK2trZudReH7eO0Tqdz0/58srahIf4Etj/8wz+spz/96dXtduunfuqn6nd+53fqjW98Yz3ykY+sH/qhH6rXve51N+W93/AN31D/6l/9q/b/F73oRfXWt771przr47VXvvKV9ZrXvObcn/93/+7f1Vd8xVd83M990zd9U7397W+viYmJ/5zu3Wd7IM3fRdrv/u7v1ld91VfVyspKvexlL6vf+q3fqp/5mZ+pf/AP/kE9//nPr+/93u+91V38qO2f/bN/Vm9/+9vr7W9/e/36r/96ffM3f3O9/vWvr+/7vu+71V0btmG739swRvwJbL/0S79Ut99+e73iFa9o6O7OO++su+66q/b29urd7373/f7Oo6Oj+ou/+Iv6mq/5mvazubm5+/09522Li4sX+vy///f//lyfm5mZqZmZmf+EHn3s9kCbv/O2a9eu1T/7Z/+svvzLv7x+8id/ssnbgx/84HrkIx9Zn/qpn1oveclL6mu+5mvqi77oi25xb+/dZmdn68qVK+3/n/Zpn1Z/8Rd/Ub/6q79aJycnw6IRD+D2yey53qw2lOZPYNvb26vj4+M6PDy81+9e+tKX1ite8Yr2/6/4iq+oH/zBH6xf/MVfrC/7si+rRzziEfW1X/u19c53vrPve69//evrSU96Ut111131mMc8pv7xP/7H9Z73vKeqzmi+hz/84bW7u1sveMELGkU8SENub2/XS17yknrsYx9bD3/4w+tLv/RL64UvfGGtrq5eaHzve9/76hu+4RvqEY94RH3Jl3xJ/dRP/VSdnp72fWaQMv6d3/md+tqv/dr6vM/7vPq8z/u8eupTn1r/9//9f7d+/uRP/mR96EMfqoc97GH1yle+slGXv/qrv1pPfepT66677qrNzc2PSp3/b//b/1ZPeMIT6q677qonPvGJ9bu/+7vtdx/tOw972MPqFa94xbnn7/T0tH7u536uvecLvuAL6rnPfW799V//dd+7Hv3oR9df/dVf1dOf/vT6h//wH9aXfdmXXYgduEj7lV/5ldrZ2annP//596kYn/GMZ9T/8X/8Hx/VCJ9HJtbX1+tFL3pRPfaxj6277rqrHve4x9VLXvKS2tvbq6qzeXn1q19dT3jCE+pzP/dz6wu/8Avrn/7Tf1of/OAH/5PHNTc3NzTCw/ZJ14YS/QlsX/qlX1r33HNPPeMZz6jf+q3fqs3NzY/5+d///d+vd77znfXa17623vSmN9XJyUk95znPqe3t7aqq+vVf//V6+ctfXs94xjPqt3/7t+sNb3hDdbvd+tZv/dba29urO++8s970pjdVVdULX/jCevvb336f73nJS15Sb33rW+tHfuRH6m1ve1v9+I//eP3hH/5h/dAP/dC5x3Z4eFjPec5z6vr16/W6172uXv/619f6+vq9YpHZPvCBD9R3fdd31ROe8IT6jd/4jfrVX/3Vuuuuu+pbv/Vb6+67764XvehF9fjHP77uuOOOevvb317f9E3f1L778z//8/WUpzylfvu3f/ujesIf+tCH6pd/+Zfrx37sx+rNb35zPfjBD67v/M7vrLvvvvtcYzrv/P30T/90/eRP/mQ9/elPr//1f/1f61WvelX99V//dT3rWc9qa1V15l2/5CUvqe/4ju+ot7zlLfXYxz62fvzHf7ze8Y53nKs/F2l/9Ed/VA972MPqzjvvvM/fd7vdevCDH/xRv38emXjJS15S73znO+unf/qn63d+53fqX/yLf1Fve9vb6uUvf3lVVf3ar/1a/ezP/mx97/d+b/2bf/Nv6jWveU1tbGzUc57znAuN5fDwsN7+9rfXW9/61j4ZGLYHZut2uzftzydrG1LTn8D2tKc9rVZWVuq1r31tfed3fmd1u9367M/+7PqSL/mS+uqv/ur6+3//7/d9fmdnp1760pe2uOf3fd/31bOf/ez6v/6v/6v+q//qv6qv+IqvqLe+9a31WZ/1WVVV9aAHPaie+cxn1rd8y7fUe9/73vrcz/3cWlpaqqozTyKpvmzPe97z6tu//dvrUz7lU6rqzAD91//1f11vetOb6vT09FxU0x//8R/XBz/4wXrNa15Tj370o6uq6od+6Ifqj//4j+vatWv3+Z2//Mu/rKOjo/qar/maunz5clVVveAFL6iv/MqvrPn5+ZqZmamJiYkaGRlpfeeRfdZnfVY95SlP+Zh9WllZqR/90R+t2267rarOWIfHPe5x9Vu/9Vv17Gc/++OOaWRk5OPO38HBQb3hDW+opzzlKfWsZz2rqqr+3t/7e/Wyl72svvZrv7be9ra31ZOf/OSqqtrd3a1v+qZvat70P/kn/6R+5Vd+pd75znfWP/yH//Dj9uci7Z577qnP+IzP+E/+/nlk4t3vfnd9/ud/fj3qUY9qn/nFX/zFOjk5qaqqd7/73XXnnXfWf/lf/pdVdSafP/mTP1kf/vCHPy69/LKXvax+9Ed/tKrOmKTT09N6+tOfXt/4jd/4nzymYRu2B2obGuJPYOt0OvVP/+k/rWc961n1+7//+/VHf/RH9Ud/9Ef1sz/7s/Xa1762fuAHfqCe8YxntM8/4hGP6Es+evjDH15VZ55eVdXU1FT9/u//fj3/+c+vD3/4w7W/v1/Hx8dVVbW2tnbufnW73XrjG99Yv//7v1/Xr19v9Pnh4WEdHBycKwHqve99b1VV3XXXXX0/f9SjHtV+N9g+7/M+r5aXl+sbvuEb6uu//uvri77oi+of/IN/0BT7x2qD77mv9imf8inNCFdV3XHHHbW4uFjvf//7P+53z9ve//731/b2dgMf2ud8zufUxMRE/Yf/8B+aIa6qeuQjH9n+vby8XFVVGxsb91t/tE6nc6+wwEXaeWTi8Y9/fP3cz/1cHRwc1OMf//h6zGMeU5/6qZ/anvHlX/7l9Su/8iv17Gc/u5785CfXF37hF9add97Zxv2x2rd927fVf/Pf/DdVdeYR/7//7/9br3nNa+opT3lKveENb2gAadgeeG0YI754GxriW9Dm5ubqK7/yK+srv/Irq+rMc/je7/3eevnLX15PfOIT69KlS+1z2aanp6uqp7j/h//hf6j/6X/6n+rbv/3b6/GPf3zNzs7Wn//5n18oG/b09LT+8T/+x3X33XfX85///LrrrrtqYmKi3vjGN9Yb3/jGcz8HBauP2sdKoLrjjjvqV3/1V+vnf/7n6/Wvf339yI/8SD34wQ+uf/JP/kl93dd93cd833kSpubn5+/1s6mpqdrZ2fm43z1vc5xmsD/dbremp6f7qOmq/vmgsD6awfzmb/7m+tM//dOP+f7nPOc59W3f9m33+vmDHvSgvhj1Rdp5ZeK7v/u76zM+4zPqzW9+c33Xd31XVZ0Z3x/4gR+o22+/vR73uMfVL/7iL9Yv/uIv1ktf+tLa3NysRz7ykfX93//99V/8F//Fx+zD8vJyfdqnfVr7/0Mf+tB6zGMeU1/6pV9aP/dzP/eAzvj+/3obGuKLt6Eh/gQ2SUGDHubDH/7w+u7v/u76ju/4jnr/+9/fDPGgEvf/hYWFqqp661vfWk960pPqO7/zO9tn3vWud12oT+9973vrPe95T734xS/uyww+ODi40HMY4N3d3Zqammo//3hx8Ic85CH1wz/8w/XDP/zD9f/8P/9PvfGNb6wf+IEfqIc85CH/2dm8g/NXdUb3M4b3ZQjv6zsfqzH2g+M8OTmp7e3t/6wM65e+9KUt8emjNbIw2L7wC7+wXvGKV9T73ve+j0pR/8//8/9cT3rSk+6VyX5emeh0OvVVX/VV9VVf9VW1vb1d/+f/+X/Wj/3Yj9V3f/d3t9j6ox/96Hr0ox9dR0dH9ad/+qf1P/6P/2N9y7d8S/3e7/3efQKlj9Xm5ubqUz/1Uz8qwzJsw/Z3tX3yRr8fYO3q1av16Ec/un7mZ37mPn//t3/7t1VVdfvtt7efvfOd7+xTxI43iSUfHBzci+b71//6X1fVvb2sj+Z1yeDO52xtbdVv//Zvf8zvDbZP//RPb33Od34sj+4v//Iv6w/+4A/a/z/zMz+z/vk//+c1OzvbMr8v0ofB9td//dd1zz33tP//7d/+ba2vr9dnfuZnVlXPi11ZWWmf+fM///P7fNZH68Pf//t/v+bm5uqP//iP+37+F3/xF3VwcFCPeMQj/pP6XnUmC5/2aZ/2Mf98tONgX/u1X1uLi4v1kpe85D6z9P+X/+V/qRe/+MX1J3/yJ/f63XlkYnd3t37zN3+zsTMzMzP1pCc9qZ71rGfVX/7lX1ZV1b/9t/+2/uN//I9VVTU6OlqPecxj6gUveEFtb2//J2VO7+7u1kc+8pG+PTJsD7w2LOhx8TY0xJ+gdtttt9UznvGMevWrX10vf/nL6x3veEd96EMfqve85z312te+tn7iJ36invzkJ/fF2MbHx+tFL3pRvfe97613vvOdLfFIss+jHvWo+u3f/u368z//83rf+95Xz3/+8+shD3lIVVX92Z/9WW1sbDSP6Y/+6I/qPe95z708rE//9E+vhYWFetOb3lQf+MAH6h3veEd98zd/c0uw+cM//MPa3d39uOP7wi/8wrr99ttbFvB//I//sX74h3/4Y9LA73jHO+rbv/3b681vfnN98IMfrA9+8IP1C7/wC7Wzs9Ooy/n5+bp27Vr9yZ/8yYWV9+LiYr3whS+sd7/73fWe97ynfuAHfqCmp6frCU94QlVVfe7nfm5VVb361a+uv/mbv6k/+IM/qFe+8pU1OzvbnvHx5m9sbKy+8Ru/sd785jfXm970pvrgBz9Yf/AHf1DPf/7z69M//dPbPH6i2/Lycr3iFa+oP/uzP6tnPvOZ9Xu/93v1oQ99qP7iL/6iXvayl9WLX/zi+tZv/db77N95ZOLo6Kh+9Ed/tL7v+76v3vnOd9bdd99df/Znf1Zvectb6gu+4Auqqupf/at/Vd/xHd9Rb3/72+vDH/5wvfe9763Xve51denSpY+bSLa1tVXXrl2ra9eu1T333FN/9md/Vs997nMbbT5sw/bJ1IbU9CewPf/5z6+HP/zh9Wu/9mv1m7/5m7W6ulqTk5P1mZ/5mfX93//99fVf//V9n//8z//8esQjHlHPec5z6tq1a/Wwhz2sfuZnfqZGR8+W7Yd/+IfrB37gB+pZz3pWLSws1NOe9rR6znOeU6urq/XzP//zNTo6Ws997nPr6U9/er35zW+u3/u936tf//Vf73vH9PR0veIVr6iXv/zl9eQnP7k+7dM+rb7ru76rHvWoR9W///f/vr7zO7+zXvWqV33csU1MTNSrX/3qevGLX1zf8A3fUAsLC/V1X/d19bSnPa1+4id+4j6/87SnPa12d3fr537u5+qf//N/XmNjY/XQhz60fuqnfqoZyac97Wn19re/vZ797GfX0572tJaZfJ72mZ/5mfXVX/3V9bznPa8+/OEP19/7e3+v/uW//Jct+/lRj3pUPe95z6s3velN9eu//uv12Z/92fWDP/iDfcdrLl++/DHnr6rq27/922tiYqLe8IY31Mte9rKam5urxz72sfW93/u9NT4+fu7+3t/tsY99bP3Gb/xGveY1r6kXv/jFde3atVpcXKzP/uzPrp/92Z+tL/3SL73P751XJl7/+tfXj/7oj9a3fMu31Pb2dl25cqUe+9jH1vOe97yqqvoX/+Jf1Cte8Yp60YteVDdu3Kj5+fl65CMfWb/wC79Qk5OTH7Pvr3jFK9q5+pGRkbp8+XJ9/ud/fr3pTW+61+mCYXtgtU9mz/Vmtc7pf05q5bDdtPYVX/EV9chHPvKjGrFhG7ZhG7YHYrvjjjtu2rM/8pGP3LRn38o2pKaHbdiGbdiG7X5rtypG/KEPfai+9Vu/tR7zmMfUl3/5l9eP/diPtTPtg+1973tfPfOZz6xHPvKR9bjHPa5e//rX34SZOH8bGuJhG7ZhG7Zh+zvfnvvc59btt99eb3vb2+p1r3tdve1tb6s3vOEN9/rc3t5effM3f3M97nGPq3/37/5dvfKVr6xf+7Vfq/e97323oNdnbUhND9uwDduwDdv91j5W6dT/3KaY0WB717veVV//9V9ff/AHf9ASLH/pl36p3vCGN9S/+Tf/pu+z//pf/+v6hV/4hQfUDWpDj3jYhm3Yhm3Y/k63d7/73fXgBz+471z9wx/+8PrABz5wrzus//RP/7Q+67M+q17wghfUox/96HriE59Yb3nLWz7RXe5rQ0M8bMM2bMM2bPdbuxUx4rW1tXsViGGUB2+R+8hHPlL/+//+v9c/+kf/qP7tv/239ZznPKe+//u/v/7Df/gP9/9knLMNDfGwDduwDduw3W/tViVrnTfKenp6Wg9/+MPrv/1v/9uampqqr/7qr67P/dzPvReF/YlsQ0M8bMM2bMM2bH+n2/Ly8r0uullbW6tOp3Ov6oNXrly5V+nZBz/4wR/1lrhPRDt3QY/HP/7xNTIyUmNjYzU2NlZVVZcuXarp6ek6Pj6uycnJOjo6qpOTk1pZWamxsbFWf3hkZKTm5uZqbm6uRkdH6/j4uE5OTlphiv39/ZqYmKjZ2dlaX1+vbrdb8/PzNTo6Wqenp3V0dFR7e3t17dq1GhkZqampqVpaWqrDw8M6OTmpo6Oj2t3draOjo9rf32/fm56errm5uRofH6/T09M6ODiovb29vnrCp6endXh4WGNjYzU7O1uLi4vV6XRqfX29Dg4OWn+N7+joqDqdTu3s7NT4+HgtLS21uzJ3dnbq+Pi4RkdHWx885+TkpPVje3u7xsbGamZmpra3t2t7e7v1rdPp1MLCQnW73RofH6+JiYl2XVyn06mTk5Pa2NiolZWVmpycrKmpqXYjztTUVI2NjdX+/n6dnp7W5ORkjYyM1MjISOvj1NRUe/bh4WF1Op3a39+vo6OjOj09rampqdrf36/Nzc3a2tpq7xgdHa1ut1ujo6NtzvXv8PCw1tbW6vj4uLa2tur09LS63W5NTk7W7Oxszc3N1cTERC0vL9f8/Hwb++joaFuz09PTOjk5qRs3btTIyEhtbm7W6OhobW1ttVulZmdna3x8vMmP6/GmpqbarVM7OzvV6XTa5Q67u7s1MjJSVWdVsLa3t6vT6dTo6GiNj4/X2NhYdTqdOjw8bGu3tLRUc3NzdXh4WEdHRzUyMtLW7PDwsEZHR9vvR0ZGamNjow4ODtocj4yM1MnJSU1PT9fMzExtbm7W3t5ebW5utrnf3NxsKN8Ri5GRkTo6Oqrj4+OG7jc3N2t6erouX77cxjgxMdHmc3JyshYWFmpycrKt58zMTNsTJycnbXxV1db9ypUrdXp62uZrb2+vNjY2qtvt1sTERBsrmd/c3Gyy6rnXr1+v/f39tq/HxsZqcXGx5ubmqtvt1tHRUa2urtbExESNjo5Wp9OpkZGROjg4qOPj49rY2Ghzvra2Vjs7O7W+vl6dTqft3dPT0/bH/iLXY2NjNTc311fbnPyMjIw0D+r4+LhGRkZqb2+vxsfHW5+73W5bg4ODgzZO3zs6OqrZ2dm2Lva8daY7jo+P25xvb2/XyclJTU1N1eLiYuvn5uZm7e7uNp1F/83OztbY2FhNTEw0eTw9Pa2RkZFaWVlpckreNzc3m76amZmppaWlOjk5qbW1tdrd3W26Z3d3t/b39+vk5KQODg5qf3+/6Ym9vb26ceNG/eEf/uH5LcUF2q0o6HHXXXfV3XffXSsrK83wvutd76qHPvSh97p45jM+4zPql37pl/queP3Qhz5Uj33sYz/h/dbObYhnZ2dramqqKeXDw8MmkOPj4zU+Pt4UYrfbbYI2NjbWPkPIxsfHa2RkpEZHR9vGPD09rdnZ2ZqcnGzCROl2u90m8IeHhzUxMVGdTqfGxsbq6Oioz1BtbW21jefP8fFxU76jo6M1OTlZ3W63Zmdna3R0tBmixcXFmpqaqsnJyZqfn6/j4+Pa3Nxsxr3qrOrQ2NhYTU1NNQPDoHQ6nfZ31Vlt3ImJiWYsj4+P23vMC2W3s7PTjJ13UGY5DlfQLS0t1enpaVNwFPfp6WlTYJ1Op5aWllp1J/PGkNnQVdUMzMLCQu3u7rZ3GevExESdnp42w2tOrO3i4mLt7OzU3t5eM8KUJPkw18fHxzU2NtZkoupMWVKyjJjvACpLS0s1OTlZW1tbtbW1VScnJ02JMV7dbrcODg6afFj3kZGRZqgmJyfbulRVU877+/t1+fLlZqS2trbauptTc0jBdjqdGh8fr/X19WYstre3a2RkpCmA2dnZ9rzBPVNVDfABCsfHxzU9Pd2MujFay52dnaasp6amWmxMX2ZmZvrWqKqacbBnJiYm6ujoqEZHR/v2HzlgyOzPqrN60YDU4eFhbW1tNQA4NTVV09PTtbCw0Pb37Oxszc7O1s7OTgOBnk8/jIyMNHmzR+gSzzRHh4eHNT09XdPT0zUyMlKTk5N1enpaY2Njfd87Pj6uxcXF2tvba3NgrzO8jDvgSo9MT0+33+/v7zddQRfZD/rlIhdlUe++++62flNTUw0E0xNbW1tNNmdmZpqsM8SXL1+uw8PDJkeM9v7+fnMWgAD9YIAnJycbUJiYmKiVlZU2rvn5+drd3a3Z2dm655577nVL2t/19jmf8zn1iEc8on78x3+8XvCCF9Q999xTr3vd6+qbvumbqqrqiU98Yr3kJS+pRz/60fXf/Xf/Xb3qVa+qV7/61fWN3/iN9ba3va3e/e5314/92I/dsv6f2xCnR8RYVZ1d90Zgx8bG6uTkpCnTqh5vPzc316cModG5ubmanJysnZ2dGhsbq0uXLjUEenp62hDzyMhILS8v18rKSo2MjDSUyJgtLS01b31vb68pEoqPQctNC1wwDhQa5XVwcNCHrhnysbGxGh0dbZuNQTw6Oqqpqalm5IxRH/f29trcHB4eNsPBa0kPjRL2+/SSut1uMwxV1Qy8+bBRKRpGjRHTxsbGmrc4PT3dPF1rfHR01N6X/aFkvSfXnJGbn59vCtN8joyMNICQ4CFBCebB3I2Pj/cZPkqefJgvHmkq2cnJybbWIyMjDeyMj4/X/Px8+ywAuLOzUwsLC21NeRBHR0c1OTlZnU6ndnd3+9iGk5OTxr5sb283YIXJARbILMBhzfTF/JO/mZmZNu8LCwu1sLBQy8vLbe4ODw8bKDaH5tgzpqenmyecwAHDRKb1A+g1f7w3z7MeyRycnJzUzMxMTUxM1PT0dGNByMHIyEibG/uYzjAvJycnNTs7WxsbG20+yTXjq8Z37imyNT4+3uqh00G86tRDWDHMlH0FmFq3kZGRmpiYaAZ6Zmamfcd6mG/6wFwDQePj403nAOaArnm2hwDvqmr7iMzrO0bN+D13b2+vAZ0E8lXVvGKMnLnw2ZvVboVHXFX10z/90/WDP/iD9cVf/MU1OztbT33qU+vpT396VVV94AMfaHXvb7/99vrZn/3ZeulLX1qvetWr6kEPelD9y3/5L/vq/H+i27lXA5UIETKojBNlaCPZ8FAaFEzR8lpt4lR46O6Dg4M+tAzpoY+rqr2PECattr+/31Ag5Wrzoo4mJiZqa2ureXK8OXQiD1zrdrt9Xrp5QSG7mQblx/igsbEJe3t7jSYyLxQAr8jcUWTGScF1u93WR3+sRQIGVKa539raap9NpYhq1WcIHuOQhriqmiLgZXU6nZqfn6/T09OmrE5OTmpiYqJ2d3fbu9B4p6enNTMzU8fHx31gjlHlqfEmkgHh+TO2JycnzYNloBM4mqv5+fnWfx6/tbT+FJY15fXwpu0HMsqz2dnZ6TN4xgAgTUxMtHXf2tpqynp8fLwxHQsLC4269Y6FhYVGMZJfhhLVnAyKtbPe+pIGZ3V1tWZmZhrTAvTyUHmEqbzHxsZqcnKy9vb2miEHPO2nmZmZBmjNOx3BsJFPBsY8pQcvpJAMG3p+YmKiyZBxel5VNYbJz4+OjpqXaZ58ji4aBLD+fXBwUPPz8w348nYxQwCufvseea6qBiLSWSFvIyMjtbS01OQa0LGv6JDDw8MW2piZmWkOB71gPNaQ/gBEzZW/b6YhvlXtjjvuqNe+9rX3+bu/+qu/6vv/F3zBF9Rv/MZvfCK6da527tVAFRHijD+KJ6IvbbaZmZnmPUC/ExMTfUqLkPkuJZlo9caNG40C4/n6rI0zMzPTFAilARAQPEY6vVBKimCKuS0sLNTm5maj0Pb39+v69ettfAy2OCklLbYJXECwPAWbYnZ2tra3t2tvb6+Wl5eb0qMctre3G/OQscTFxcUaHx+vvb29PkUk1kqpppdug4vxUTzmL70M80IJUJ68O4YAgED78xoODw9rY2OjxsbGan5+vlZWVpoyYZSTIqU4zKE1ENPSd7F177x8+XKbE547ChdQqeqFEg4ODmphYaF2dnZqa2urfYehJIM88KozxXb9+vW2Dik7jJ6xJeAyVxTn5ORkzczMNA8I8Njf369ut1vb29vNo+K9o/Rvu+22PibG2hmvuCkQyxsFmnZ3d/tAkPCNvQzoomjJPPBR1WO17KnJycn6yEc+0rzRpaWlWlxcbPkE+k92eGMM+f7+fm1sbLS+M8K+ryUwNnbeJz1Cl5CfZMq63W5dunSpDg8Pa3t7u8kFL319fb3tO3MKHOzt7fWBhZz/iYmJmpuba2AkPU57Sbw9wwuLi4ttLwlbzc7ONiAHcI6Ojjb9CNR7HgqcDNN5GEcsj/ccHR3V9PR0ra6uNuofeL9Z7VZ5xH+X27kNcVJNFD3hIxC8LJ4YhSPugqr0fcjcxqg6Q48Uy97eXq2vrzeDA01WVZ/ym5ubq+np6eaNZqwWQkVrUrRoG5tFvGV2drYJ82BMCW10cHDQFDy6bX19vdbW1hrlLKElY3KUjrFjBCg6Gz5j4gxrKuikdzPunZvTu6rO4r+7u7vN88ciZGIbD55XjMUQU7I2PGssRRqJBFuobVRc1ZnRk9hD8R0fH9fU1FRjRKw/Q8TDoxh5deZVoh/vtuoswQmzwsBgV6yLuQEk9HFjY6OBCslggx6j2C+lv7+/3+Y+vSwUrndYNyyHfSVGLe4JyJFzQMCaZbKafpND64Jt4R0y4EAeo1pVbd4TcCWIFM/MfAisFNDKSwMmGFdjt148U0BEkiUGy7qkQd3d3W1UPbBBTgFNQJ6OIIfkjIdtb0rMwkx4X+oX661f5kxymTml53if2I37AvxpzBPwCUvZm8Zo39rHwltYEzogWYaDg4OanJystbW1ljyrj5izwSIXw3Zr27kNcSbvpBdSdab4KCsxo4yRVPXirpDs1tZWyyScnZ1tn2UcEmmm953xUr9jCHhDDDcj7LuSeJL2lLGbSTSUNeMHONj4ScfbpDdu3GhghEKgHG0uz4Bi9SFjUpkoBrVSmGLSEpUoShTh8fFxbW9v91Fl6eGmJ8e4JX2K7ux2u7W8vNznWWbGrliL+TfPFFFVf9wXnYc9OD4+bmBhYWGhyVL2iyJMipayTMQNAPDGeXKUmH5XVVP+1s2ae8/+/n5LugIO9KeqF3/FbKALGUQJRdYuvSQeDq9PQpdGeQNtCSYZXt7Q4eFhLSwstGQpYxYC2N7ebjT9jRs3WnJXGm3gi6HW1+Pj42a4jZl8ZsLg5cuXmwwDV4CGuWXcAWt/ZEinl0b27E8JggwwuUqvfWpqqi+xilxlLJpsmUMMhrnFqtgDdAn9AjCtr683+TUGeztDFgy5LPbMVck5JbcZYkuZnp+fb/sg8x0y5uwdVT32INd2UBYzXgy03owG/Azb+du5DXEmd1RVQ74WPz0Ayk72YioQaPTg4KCPVpubm2ubZjBBh8La2NjoM05XrlxpyREoU5RyepkSdfRBRirvGe1tk4vFQbCZCGIeJJ9UVUOglG0aXoqYx0PR8copH4rOnNqElJLNTilBtJSJMZkvXnci77m5udrd3W2eC0UlbskLS2pvY2Oj0cJiwt7HS6uq1v9MWNM3mbY5XvOmP1XVpzgpbbQ71qGq+hLiJN9MT0+3WDdgyLvLzHPvSHn1mZmZmfYn388gi++vr6/X3Nxc2w9ra2vtaAy54tWK1ZEfMpRxWAo6E3TIeGaVb25utv0BVF66dKntl+PjsyNBFO7BwUHzhsyB43w8T8Y3Y7/2KJBIds2zzHoeGkbCfgbChQSEYABw7/RMMXMetn6KNaO9ydTa2loDSBlDZvDMXfaRIWaoGCj/FntNps0+raoGLpLmtU+Oj8+O7c3OzvaNf3V1te0PoSFrKSQnD2NxcbG9h/GfmppqspjGk8NBR5Ah3q49T9aN057375vVhtT0xdu5DbGjOmgVsSgCwotEC+Zi2PS8DP9Hf8n6E09ZX1+vmZmZ5sn5LBqKMWIM8mfoF4qAVwQRTk5ONiVYVX1JOjZxxjRtLEdzUH42rE3FeAMhhJ0HzcNmKH2vqkfHpzfNU0VbMc7GUtVTorJnoWYAwBjQcrJcGZlU2BQ1KpfyZUwzNkl56j9vHNoGCqqqUe85XkAMNciz1mcxQM8URgCcMBhV1bx/wLCqRysy6rw9RoYC5BV5TlW1s6LCAfm+TAwkT8DB0tJSkz2/0x/jASoAO4acIVxeXm6GUjIQWbDu6W14/qAHnnFCwMRaZuw6P8s7BrTMGyYF+MgcAZS8NbQnGDsylUlwANvu7m5fopN+JPhMVodM597AYngG/WHPJbDMkA0ZsRclL8o1weSQK/rHGCVoYhJ40BnuQC8n8LNedE8aRICGgTZmMrO3t9d0orh2Jona52Qp5zTDYJ59Mz3iYbt4O7chRjtlMkzGYcRo8jMQf9LBvEnCozHKNkBV9VE3p6enNT8/3zxbXiK0SHlCszymPCKR1Bo6s6q3KW1SQCKVDmWVZ1QZl+np6Xb0IjOsKQs/pyizn1W9Igs5J4wXD9pnGduq3pGS0dHRpigydswYWYcs3rC2ttZX3IKSz3kwZ0IHgEBmcpIDxj0Tnoyb8jAu1CnDmgk4DKq+53EggIdxlvSVa0hegCL9Pjw8rJWVlT6lngbdWioog342ZnHE3d3dltdAIcrEJucUIVByfHxca2trdfny5WYkMDDmOfcYb3B7e/te9LTPoOuPj4/r2rVrDcjs7e2189bAgKx77I8cBwaDnOubGLJwT4KwnDvsB68zPcpkzzIxMI2ZPARgcm9vr52xN4dAxv7+fsvfSJbCn6pqjIh9al0kgAGAzlpnuIU36XPCPjx57xsfH2/hlFwXffW5zAmwr/O5KafkPUMD1gSoBU5mZmYaA0NuU1fRoZlkSRZPT09bZn6GFu/vNvSIL97ObYgJHuOVST8UsczApJ8gWJtxf3+/LwbY6XSat8ljclyDUZbklQhXwgF6ETrNCkfiSLu7uy3ObGOraJOxo729vSbsms2UNBQvwQbsdDpNSfDuGQhKVnx3ZWWlPYditNl4VxnPY8B5EWhwVCIvGx0qMzNBUMZbk3pU5o2hBxwoUn30t9jm2tpaX0Wx/f392t7ebsaKgc7kncnJybp69WpTyEnrf+QjH2nf5XXxjA4PD1ulnNXV1TYvkm3Q4/rsfbxKxzsof8q4qge4sDxJYYtvOpJD8aGmHdOR4HdyclKrq6s1OjrayuUdHBzU7Oxs2ysZz6RYAUnyubCw0ArLJI3Ni8GOyBXgFYohkzfzwSCLJ4+OjjZDjzpOY8szdgY346rdbrdWV1eblycZT3/sLeGq3IcbGxt9yZ5AdDJSxiw27GSBfiwtLTVDCvxjVTAsKGDJTj7ntAWQkUcwxevz1Af9Yh70S/IUYLKzs9NYHzS8pDZyNzc31/RhhoHQ+Woa8H6resVZLl261PZ8Ju3lmWLUtPwNRtha8tgBIezNsD1w2oWypqt6hT0ICsVIMCQQJJVEMZ+entba2lorg0jp2RCSPqqqodOsfpRUGYH1LsZ4fX29xWIyYzZjJtlXhpihhYyhOgqDwqRIoUwMAAOUNGEme9lsjAAjzJDqoxiPeUxPJGn3pLUpETFTGaq7u7vt7Ozp6WmtrKw0yjkTgjLjFpjJjU6Z2exVZwpIXJaxm5+fb99xtAX6duRDlSzKaGJiosWhq6pR8ABfVY+CB6ysIZCWhhJtDqhktS9/JGORH0d5UhlXVd+REAaOIWOYACRrBJgtLy+3PeD76MqqasZDvB342t7ebsYtQz72gyRA1HDmM6BpkyUCoBimhYWFWlpa6gNEwIj9xnMCRqyLEIa9mwVKGFweHRpVyCSBqXnz7gy1kB+MiD2bZVZ534wLGR0fH28lZYGpubm5vmpu5hsD4LwuecW6yO7mmdsP9lzqB2NndMW87Xt6wTiz1oHx0o2YiGSzxsfHa3l5uX3fnsX4bW9v950MAKjpAfKf+vpmUtNDj/ji7dyGOOOeSTei8SgDAruzs9MQWdVZDVib2QbnBRCs3d3dvtqxKeC+m/ENG5MCy+McvuPfaCubfXz8rCwjw0+YxfdScJNunJqaaslA5oUhppQogaQ+8xgJuhzqNs7MtGYYzQP0DN2mUshMb54RT0k8PBPNkh4DHGx8Rlg2OaqQwaRgzSvDnf3K+N3Ozk4tLS215DafYTjQrGlcfc8ap1K2FhLyGBOAhmxtbW01byoTusTLrRdvD7AxNsaiqhrdmElPvCohkSy20e12W6yXgsxMeGCGhySut7Cw0MCNLF1HrhhOsrK5udlXV9t+5DGljIhpbm9vt38DmxmvZDSTXk1Klqwbx8zMTJ+RpRfELhk1c58eNsAEGAmvWIOkaKuq5YLQEwr6Z1iG0baHzTHwtr6+XlNTU7WwsNCO9gA7DLQ+Ap17e3t91dYyb0CCX7JSyVRYH+NggK2lfYkxmJ6ebme8MU4MeSaZzc7O1urqasuM397ebkxcUuCenQYfwL2Z1PSwXbxdyBBX9ajSjNMxqqenZ/V0GZTl5eUW5xobG2uH0hkqiLOqF0+10TPRiKLNhJLBDZHJV2mEPQvK531UVfPUqnoePVoqKVVG02bT/+Xl5UZx89gpAqhYXyhwm/zKlSttTjNzmDKiSI+Pj5sydsSJ4km6a7AC2cjISFP+Gcfi3fOGKQnvtfkVZlBRaG1trS/pIz2ZwWQVLAXlkGDFfPi8s77W9u67767T0965Ye9RQIHiMUfWxvPJWlK55JenyQhRTBS6dRJO8WylK601Q0yGyCFZsUZAl36JIVZVo/kzL+Lk5KRlDxuf/jGI+sUA8/Jkb1tTVdzEuTPRCQAZGxurra2tFlOtqia75EtSXx4TY1x8lrG0hzJ7PPMkeGinp6dtX/u88WX+CTlKHQHkG5O1VfhC6VKhho2Njb6iOJcvX251l70nGSvMBQABiJv3ubm55s3u7OzU4uJiX2lOYR97HXgGqjO0tLCw0ABUJugxumRga2urNjY2moNycnLS6haQlYwDG699kTS5eH7m59zfbegRX7yd2xCrlsTgoESdqa2q5kUxcIoqoKPRrZQD4ZYMg3pEnxJmaLWqJ5hic95HYTAi6T2nIEpSgVh5YeJpjA4vm/eQ5/V8hxL2/KTkUyn5DCWbBswtMAwu5c2j9lyfF9PKzEnImkfCsDMcVdWM8vr6eqPOKYw0+oyM4zneQ/HyAo2fkpmammrHy4wpsz8PDg6alzc+Pt4qblHwi4uLtbq62vd548oEoPQgeZ7AEyVsnqwbA1lVTbGiiM21YyOZ/JWGQFgBpZ6JSAwE5c+bv3TpUp+BJ7+ZmCiZCJjZ399v8psGP5OS7DvfMWbUbFWPznc1XDIzvMetra1aX19v4Giw2InvDFbVOzk5adSwvWo+AGn9tP+yuIb5pyPUj/dZMsygavpIHyQrAPBhhISlEiTRFarWMdQ8Vbka2C/fyfO6GDr9tMf1L0NmeUqCPGKCsGvkKS+I8KwEuBKw6C8V4uhfACdZQPOIdQSMUpaG7YHRzm2IKW7xQIKZMSkbeGRkpK+ualWvTi96z0bgHck+Feut6sWHUVUMr9hgVY9CRI/m9Yi5URgZisKm9D0UZVW1NH/ePGUrRiYx5eTkpFHiYovpFVb1KvowmhI+0GQ+m5uVcWTooHkGyebjIRufDZseOcXh54x1xvCretSkuV1eXm7GYn19vUZHR1tsEbgwX8IJvHNeIeVYVS2RxXzzxFGlVT3vUoLR2tpaLS8vt/KPs7OzzVhkqCA9/qqzxDiemAIXGSOWMZtntxkzc8y4j472LmBQSIKhsQ8ALYmBPJs8McCrRbUzIM52kyXGXKWqjEF6H6CX3r61pbytu38rPAJYoSyBHTLB26OoAYms7GUs9pQ5Nw+Absoy4CAjenZ2tiUV5dFE3wVIqnoxWeVZASwhDCyd9aWDeNr2JN2VwMeZdCEhhUTsXe8GqiYnJ+vSpUsNcHW73QYKGGFzw3u3f7vdbrvUhvxIjLOvzbHfmztyJ9Rkv0swva+Y++Fh78KMnIebbYStwbCdv53bEPPwUinwSnijqDCZuzxnG3tkZKRdl5eUFcGiACksipmy29nZuZdHZlNnEg4BrKq+zG3P15dE+Hn8wNnIGzdu1OFh72Yn8UC1cqF4Hhell3RmHgWx6WdmZvrK+1EQmRwzGJ+jJDLhIj2409PTBhxQnFmIwTMpJ2Akk0gcIXJJhs0vQYmXkZmYmejDy5EfQOHzABcWFpo8YEAYFwoWvQdUiJkZX7d7dqWbM9ESlzyD8rI2jBqvPZP70mNB15JzoRfgijLNGJ+xU+LmfmpqqhnYzc3Nmp+fr4mJiVYtiYIHTMlWZkDziJQlzdyDTqfTjpRpwF4CG/IvpioUBBwZsz+8LSyHeTo9PW1grKqaN21+AQCf5YkBMn4HHDMEDCt5zdACkCYzWjJeerliq9YaOPEu/RFHBh6xZpwB56qreuU+sS0zMzMtMZEhltegP4uLi01WhAyEWswN9kk4hu6SH6PvZJB+y/wFIQ/erdBdhljUrbb/7S/5ARlHv1ltSE1fvJ3bEDsCRDi63W67xJvHRUGJQficje3/nuPv09PTFjNFq1L6jHnGLW0CMV+Ks6pabIdXVlXNu7EBGKDMLib4PDcoEpVkMzBO6RFkVqbqRIeHh63YviQTCF/2c3oPkLVNKOGDsc5KVgCH39uAzpJmCcqqXma1P6qAAVIuHaDMKC6MhrnxPP0dDDNkHsHY2Fhdu3atZXmTB54BudBXBt56S9zLeKU4WnpFGbdH7QMeJycn7ewwBiGT6qp6NZp53RKerDeD6T3pcRlDJr6ZAzJs3AkGKUMeoHGQB8aUYs9QBkMCEKOwxfyzwE2CGZnlmQCVXjMwkvJ85cqVBrg2Njb6quJlXNj7q3pMEO9YVi+DKxnN+IDQTqfTkgExM2TbHDBCGAUMBZnMWLw5cORHGEpCnJAGIIfFSNmS+S6mS+bJjzHneXxMy6VLlxr4y/Pg9Ji54sQA2TLwM6acMV33FOc8p56w1xUp8Xv992z6ctgeGO3chhiiZFQzCSqz+3g5lPfGxkbbbIkM5+fnG3VqAyWSlT1bVQ1J8j4ynjKIOlNAUT6yZweVBLod7Xl42F872OcZbzGdwZJ5ufkdIWLEKU1ghdcBjVNqDDljoeaxNkhRUcQaD2Rk5OzeZrE3fU8am9eGGqMYePcZi0uqVh+M3ZzKBpcIJ+4lKUbdWwpFPNWzeZ/idKno0b1V1TJ6GRDKmWwCBfm7jNnJUPc8soqqzCzmZGl4go5iAYmUP3nUD54tuja9QEYLZZihmZGRkbp8+XIDHJnFnLSysZIFTAHlak+YG89n/PTT3k1QkN6ZeQFunRZwHEqWcWZZJxuBus18j/Hx8ZbcZP4l0gGyGDGg3l5icMi+i07oJTQ6Qy8UlSUfZbP7flWvKtra2lpfdnIyIXlEESPGEANkCwsLdePGjaajsBdYGM+1T62l3+c65z7b3NxseSuOCwLKmZAJRKrhTy+Qp9R7N5OeHnrEF28X8ohtfDfGUNQZC2aMknbNWLDNSigYK8a4qnc5tu/wlGxMHifDQbGlBw2xegfBr+r36Pb39xudykAACOnFSVxCOdnIPMn09sRZ0bsUFWNJOYjRZpLbwcFBK3rCMOv/2tpabW5utu8ALLxRSR+pNCRQodvNlxurKIKqM2PvOAYDlLFrm7fb7bb+UUJV1bwLR5EYWzcTQfnWtqpXHtO5Wp6M7zJI1l/xkaSV81mAD2VlHNaaYUma3/wZBy+dkUZNAnyo8xyHcIt9QZELZwAiPCNnkdGLPOFkEDLb2Lj98e7x8fEGMCQXMlhCBPpCtuyXrP2d68UDJF/izzxcV/el0cuQkn0LqGcGuSSjPHNOxu1ZoFqz9qqx8bIzt8I6MFBkmD7gYdNdnAbeMi+YPOlbJgymcaSLkmYGGrA53p1skvmam5u7Vz6HDGxyS3cCgvJrqnrV+OxHzwGOGcPMNQGUzfWwPXDauQ0xujYTqFDGqSQy5kjQc5P5N0VCaUHLs7OzfbcxUYpV1Tyzvb29mp2drc3NzaYQeC+Mizs8BynOPOeX/fR8Ch4CpQhR6gQ+Fdf4+HjL+OU5pjJFRTMw5kkGc54x9X2xHoos73uuqj5qn7KnpMTw9E8G9uzsbEtu0ShdCk12MBCAOvNOc2sMjIIa3mTDfO3u7tbS0lJtbGzU6Ohoy1Y2t+Yts6exB1nPPL0a3pq5AeSAOfNMPng5lBDPPmnco6OjdkEChaeyEznN6m/CCBS0OUpDx7vkrZJRoICMLC0tNQAF2GTcG+CdmZlpawOQedb29nabf+sEmFZVu05UCETWM2Opn97tM0CfKmZYA0YN22LdGQpj1S9sjFwDAAh1rG/2N8AIpCg5OjguRsi6zs/Pt3O/09PTzciijxP0HB8f1/Xr12tjY6P1mT4ACicnJxuYtZ4ZC84EPCDixo0b7RY0rEd67JloSG7EouWgpBMiDk6PoNI5C5wKoEE4gUFW9Q/F7xk3qw2TtS7eLlRrOqkrwigZCaqu6sWeKE4eIfSYCDzjFTaPDZTl7XhHPJlOp9OUtH8zOEl5OtpECXuW+GbSyvqrLOHo6GhdvXq1GVSbCXKGXPVHP5SZk1yWnkJVryh8GlDj5GlShv5/cnLS5wUn9XpwcFbsw9pA8jy6rC42OzvbklAkufF4GbmqanFxa55xKPFl70hlMRgPBTDMAfkAWpxFv3TpUjPo+gNA8fQox6reHa5kAOAha9adVyRGDMCgk3lfnm1MYoYZx2VweObez/A7ooXxsOb+TdEDn5ShO5tXV1dreXm5yaP50p8McywuLjZw6TNkIuOLgJjkKp6SY0v+KO3a6XRaERd7fGJiopaXlxsYqepdlsKgpQH2dyZIAkWALnmmAxjpPJXBmFf1kkVPT08bRZ5smz4lC5J7374lh1iog4ODlqWPAUpDNTIy0tYHo8CYVlVbX5T9bbfdVltbW7W6utpX0SxZIgV39BsrkicLANX0cvf29mpzc7Mv7ksvAVZk/eTkpMXm6d7M4s8yvsN269u5DTHDNTEx0QTTphYPg85yE1OMhJyhdNY0qz1BdlCx+CUliV6hVHklfpcbhaJgMKt6SC2PNQAGro/jkYlV6ndV9SFjCVW+jwZP1J7xUwXxszay98tIzRhzZsrqPwMITDAQGbPmaaHYRkdH68qVK3X16tWmtKylvvAuPRdjwNvi0YihD4ICHk5VNaMjFo7y07/MtrXueS53YWGh0X5V/YoduANOeIe5RmQVdUkBJhXtb+yCDO8MeWhTU1M1Pz/fjJREIQY+KT7ybG3sD2Ax/zCu5t+1loNsUVKRCTwyzEL2PNs8pZEGxPxfyVHylSDT+koCtN8GLx/h1a2srDTqmFGTYwDoZtzVOxjkBB1V1WjzlZWVlqQ4Pz/fR/sChINH94whKVgGn5yurq62/BE6hufLcGOGyHvmUzB0VdU3BrpnZmamPYucJ2AHbk9OTlpVOXPE0z0+Pm7vJpubm5t17dq1tl9nZmaaPlR4x5xjR+hBeQOpf25WG8aIL94udHwpK/kwVoNB/729vb5zm7xYAkdBZFINoYTOV1ZW2mZCS/GUCL24R3q1+/v7zRCJO0pS8vn0opUGXF9fb8eg0pvPzGaJRhAtwWYIgQ81ZkdHR+tTPuVTmhfp/fqARtvd3a2/+Zu/6as6RjkCOJnAYQ6ywhljOD4+3r4D9Fg7Fyeg/fIc79HRUVN0YuAZj81Y5NHR2aXoqEnPFRLIWCBPiSeUnqmjKQkIGGbrI26fsWfViMRFL1++3McQUGKO/Yhp8h7E9N0Vy2AbK+o+s6zJRFU1I0I+KDpUt3KvPDtULA/F/gEKeX/KKZKfZG+EGAA88sU7VO4zM+szwUkuAwWMGcnzt2TUe81nVa9IR9LZ9i5ZQZPzoIWO7BtHutDDmZiHobDXAT7g0HoB9aenZ0V31tbW6uDgoD339PS0JVGpaCU2nKDbegPynlvVu1Am18jRKgbZMTjx7mQDGeLLly+30xjmqaoaUKmqPuBu3/gbK6Yf6HhH2njUSWGTTcwP5i7jyWQ4/z9st75diJqG0lGufpZGgGDwYCYnJ9tGqao+hJh03+DZOYLrb+8mtD5D2MWi/D89B4aKEjw97R1JEDvKLGwb+uTkpBYWFpqyFe/htVBGNhqjIhlncnKyGev19fVGnVNw5sD4GBJzUdV/ibhNCmjI1Ex6nWEVf6uqPi+MZ24c6WVL7skbi/RNzJhCt7kpLTQvuZAZn8dmjIs3NTJyduRoY2OjVUPixaqaRAGKiwFAKNfNzc3mWXpmshSyjtfW1lrWeibkJXhLRWbMaPYMX2SxGiDN8adMLEx5tkd4Uj7nvXlEhpfn5wyCUA2anhF1Zj49dkr6+Pi4j8WyjuaCYWeAzTFP3zgAPnM9Pz/fZNVngFH7ZHR0tObn55uMMzRJS2fCUjJjDFYWcTFfMtZR6lXV5xGTW8Y1aWl7yvyJzUriWlxcbJS3/WofexbwjonIhCqylDkz3pnJYoMhNmPI8IPrNs2dmL19aj9lUibHRC3tdHLoZmDpZrVhjPji7dyGGLq1gIwoxJWoy4bkQVMIlBEqjFKA/igwwlJVTdFk6TwIFrL02TyqIN4i7ohuzOSjpLvzOIH4LlSd2ZfpSacinZmZ6TvWYVwTExO1trbWh64zPqz/lK/n6q+xJc0PnKDJ3F7kvTY0j0BcdHV1tc/I8FgpWcY+E1N4iowMlO37mcxk7cnF6OhZVaoER4BK0r/CAmjno6OjlkVtDYAk1acwBrwzlFzSsZQQxc1DNteMthKLmaznb+Mi7+SWHOfdseSnqprMMRaZ8EXG9ZOHyCu03kmBMlBV1XeEJ4EQJiXnXHayiwGqqsmo9cQSZDlEbJb4pd8BOIytnAp7yl7Q76TKM1wj+cjYebJzc3NtnmdnZ5sM0Q1OFKCvB4/jDCbm2a9ODdBfmZSXRVQWFxebLmNcAUT7PjOok6lL6j/BKn2nL5IjAR/ryzGgKyULZmEeQJQxzUQtMrqxsdEq9wGC3mmdc//d321ITV+8XajE5cnJSas2xePzO8jb4XToPBMmGISFhYWWRZleqo1mc/HqoGrI00KjNzPLcRAFSspJL8bduTbJwcFB32XZafihSMKubjahZsQYVgaA58jIMoi8AGOGgM0Xw6H5uXFU9XtTo6NndxA7mmODbW9v1+bmZi0sLLR7h1FsULn1mZiY6LsrOGsLz8zM1Ic+9KG6evVqW9eq6jtignb0bGCDMoHGUbfOGaNuJY4wALmWqlABdwwLZWI9eWCLi4st453x5Xl7dnoHmXAmHsoDylADI0kBkiklNHPtrb+4MoCjGT+vS3zY+yXeOQNqnlJmq85i2yo6UdIMgpvFhCIWFhbq6tWrLTbME8MkkVMGkkwqDCF2Pj4+3u7vNefuYSar4pMZQsojcuSRvDNGwloYFT/PWHyyHVn9zXd45n7OONE1QkMYkapqxYQYeZ7/7u7uvY4hCnGIAwMs5h44lmnuSsiFhYUGlvJGNMDWuWOAX75Cgj3yBHQk1Y55BMbuvPPOxlKSycxdGFLTD6x2bkNcVU2QMikgk4eqqhmmRMRJlVEmScEweJnAYcOI1+Xh+jREaE5UEwOuvwSe0vAeXhcvF6jg6VDIKFCUGwMsfkWwgQZHBTY3N6uqR+lX9RKPIH59ZlApkqoexSWDexDFZnassZkXHmDVWXZs0n2JVilvyUpoTWNTxEGcm4I1txQDL4HxFn/0N6+PFzA5OdmXtckjQuXx0lGkPKpBZQK4peefiXlJl4r9MjZVvcsL0KNLS0vNOBh/MiQMHmXKw0vPPOP8SafqCwMDdGS1t+3t7VbPm+zoJ4MKEKJiZ2ZmWpzS+O2h09PTWlhYaPQ2BZx7E0BFlzIujgWad59P79/P8xwwzy1Bse86ZqV/fg5ojo+fFbLJHAhMjTlKTzBl3Rgy/JFGU/+ASfkgvuP7QJZ1AjhdegMgZVJeeqnkWF/pJzot2RTeLAO8vb3dLkNhUB23ynrUvHPXJiYg73Q6NT8/38dQWMfNzc3meNxMQzz0iC/eLpSsRZDyHlJCywhQIhQRbyM3BGVl08hk3d3dbYZHbMZxpkFqVlYhoeWdZSH5g4ODdkQnPWWefd55W9WjCXkBNh6vgVHnnVKOUDMlLWs3vSYAQZJbVX/hCB6Fzc/LoOhRwZnAQkmi6FLBWQP0I0DjPUqDKlafSi0NiCIFSb8lZSvTk4LTj/n5+Zqbm2t3vwI1Ozs7rYpSKm9rvr6+3hLvkqZNg29eJZaRJVf6VfVKTHr+4uJinyxQlmNjZxeFoPaTQiX3ZG59fb3RunILeEEUJVnOkohpwHlAwBmgkAYcfWh/UPp5PE+frUPmbABF3gkAi7tihMiE34lzZ/GOzOHIBLL9/f1WMz2NjRCDpCuAaWRkpN0RnSGkhYWFJqvWMZMrsRrmC30NqGFLzD85kUls7JnHMBj75cXmPhwdHa2VlZW+Yh/Hx2e3WSW9C0Ch1GVaJ1UMANJlwJMkMtnRAFBVryztIP1s37h9jt4EboFajBcmCDOZmeXD9sBpFypxKZ6U8Zik2RjIjMkStqpe5SZKAsplnGV9MgIzMzMtZkMJiJnY4OKv0C1vhdGxoScmJpry5K2KpfEKx8Z6V6dRCoR/fn6+T6FOT0+3rGeKm4Ji3MbHz0oubmxsNANXVS0mxcuihGTYpifGw0GLZUm/hYWFWlhYaBmbo6OjLUnD94ALRnxxcbFtWPOErpVxS3nt7e21pJQrV660PAHzw2A7FqGIRtVZ2UDnfwGWpDOtKdAi5ouKy3KODCFFS+Z4k5kxTzHz2Cmhzc3NpuRHRkaaUU6vJpOb0oj4OYB3ctKrmUy5Z8Ifo4FlkCthjRiB9LLJo7Wr6t1sxggDUNPT0y17fGdnpy5dutT2JsZBMlXGo2dmZlqxicESnFXVjsGhZlG75DMLfBgHWWS4hT0GE4TyOck22Je8e6EF66aZC7qDoeHBSjrLC0voG7IiYXJubq6PRVAVLcdjboU4GK6jo6Omk/LYlOsmM6fEHnbMCBXtudbaccVM4GL8M5eAfNgbfjZYaCXzIzqd3q1VGY65mQlVw2Sti7cLxYhHR0drc3OzLwZMkUCDEFfGxlBHeVsQJQj5iXPwYGye/CxjiHpaXl7uiyOmx7C5udmeQyFQrpA2mo6xhKp9j7eOSqcEKRLKhvLhnYrxSZCx6SgjxiSRKfqUUnM+EJCAls0lL8zG7XbPjjjMz8+3NbPpjT2LA1Dk6V2Mjo62ZBKJWBqvmEeRlLA5BCKqepeE5LlmcpI1inMdqqodDRMjMz/WA8Ax176f1cvMsf5nbWnyuLy83OdRu2JzbW2tMQSMAdDIECtekmc0O51OLS0ttWQzzFHGoQE7c5LZ8S4xmJ+fb89DX2IS0lvLOK49hY0S45dNm1SnDGp7xVzn+XvgBPPCw/YcldLsLfMAwAEu5pzMjY+fFe2QCJkZ9vbQ7u5uCxfIJ2DYNAb08uXLdXR0VHNzc7W6utrAlN8D1Z1Opxko4RPGGhhIWTPHDFaWhqyqtjezPC72CWgGmqqq6Q6gk3zl9Z6AOZalqtqcpIzlepN5R98A0iw/Sl9aC2srFDdsD4x2bkOcNBb6lHCmx5JJTkkJz87OtsQhxq7T6ZUN5ClB5DZ4GjzKC8pdXFxsHtnKykrt7++3m2YoelRPokRx6ryjNS+gcK6R10PRo+nEryD09OwpW5SW8Sfo8DdjYbPpI4DDG6/qVSrKTHRG33ow1pB0HrMSVxXrVVKRctrc3Gwead4Aw3hl7B+goTD8Pss7Zs6ARjEAT+QGXUpmlPzc3Nxs8+E95GxnZ6fF1jLpLuP9FNnExEQDKJgS+QIARcbPGamqXo5DUnorKyst4cj68uQzKcv3k6GwDkI9MsFVQLt69Woz0BQ4WeNh6aP5IjsUbB7pkYilkAqD0+12W5IYetm+YaTME28b7W/fVvVyKtIDJm+8+KzsllcEMjwpI/msqmpGRSyV58xIZ3gAKDPXi4uLjTXJvQNkWXsedpYIFV/3/MzzyPwQfc3KdowxloZRp9vM4erqal9yG8NtT5CPzGC3pvQNJkIiImBFXvQvae2c35vRhjHii7dzG+LLly+3rFfesDgY+pPSo/DFhat6183x+qp6HgpvQTKBTcP4ZIb1YNJDxmkJJ0M0MjLSKtyg1WzoLP6QHplnedfBwUGj8LKoR2Yreg7jCGGngeZx2ggSo2wWxo3XxzPx/4whUo4QMGPl/ZlUYwOjvlGyaFxghPfks9bL+uY5SiicB5DJQI6cGGuyANYcWDk8PGxshrEAJZRoVbWzwmjupMh5/WJ1qTTF7cjg+vp6K7VYVc0wkC3eMtqxqprSS2/b2h0fH7c60X6Gqkbh+nx67WhegA0F6zupwPf392t9fb3F1jNTFoNR1UsEBAr0He25sbHR6F1AkOGVD5HMRWZgA5tOCzA44viYjQyzZCKdcXtXVTWv2V70Xntub2+vXSWYuRXkgA46OTm7nUjhG5nX9IO/nbzIhK8MS8juF1rRx0ziFHIit9Za3HvQEclwlfmuqsasJE0NFKPIGVXAC6Nh7bFtCwsLtby83LxiwGhzc7OvlKbvetbQWD6w2rkN8dLSUkNUNicFRBEzdDZKVe+ojY3OG7UBGHAKNL1h5wwT5cuGpJRt3ET6qEbZjlW94hmU0mAMJo2PvlM0ymxmYYHBywsy9pXZvxsbG331opMilqlqvignxpqyRkfn/PDcM4M9E3zQy9YlmYZMkqJUecniY4BUxnQZgcXFxbbmaMukhMkET50X4N5jijINE0rbeBPUMUqMZmaaSpxT+pDMMRS33357mxfZz8Ikvi9WroAEYGLt8ygJ2ZDPcHh4WDdu3GgAUpKQ+TYfDDUPBujC7GBoeE+AHCOcsmo+M+mLB2tPUuSDGbxra2sNHGZIIYFjZuhnzNucnZ6eFbyhA+w13icjBaTas0IJ3sdTZhAxZwC7vZRHD50vB+DEd+UZKM4xPz9f6+vrVVXtKA+ZPj09bbcjTU5ONg+SfMvVQAPrL8bLGmTS2ebmZvsMb9hFOaOjZ0e85ubmWmY6YGNMKPnUmxnfN3dkjFcOqOfY9EOOhvWzj8nHzWrDGPHF27kNsQ2exzAIZp7ptPA+A9VBeShtgoCenZycrMXFxSZUvDAItOpsgefm5poXzAhCwoQ0SzxSulX9HrgYTSZKOatJ0fEuGHqeqeSfqp6xnpmZaYlNYrj6ZzxV1Udzp/fOoOTnZT8yclXV51lTYjmf+if2xEjxuhgwa+l3+i3DMytsOYoktpnJdTa3+BXjlTJQ1X+Npnmw/hQOg+jzfs9bNn50fMb6ut1uY2tcs8c7Txl7yEMe0oxoXmDAE6H4yYt1V1zBnM/Pz7dx5/GyQaBkTTOBMClDgMNFBjL2yQ8jenp62rJrGa5BI0ueqqp5aFXVkphQ3TxI8m2tEiDo4+npaSvkM7h37aNMwuIVkh2GizEzRwwIxml+fr4vUXNxcbGtMQ9eGIfRSTCdJxPsjb29vXa/MyPPcKe8y4Ym62R68IiYEIJxo9Ud/8OEYZuc8wbYMryhHgGjOhja4nVbC+9K2QGo3eK2tbXVTkOgx4FjZ8bl8txMQzz0ti/eLnTpg7hpVgrKwuQMHqXJEFK+jISkEYiQsYCis6yf+E2n02nZh1nasKparFR8saqXuHL58uU+r0dVHpcL8AovXbrUdx5SdmIKflWvTB/PDfrc3NxsGyvPlVI6FEzGH30/E1YoukzwMpeMhTlWPCUTa2zaPB8qK9X8S8zhqSWFrok3V1VfzJcyzViovmXCCko848DmVCwcbZeUGQXnvbzn+fn5tn7pfVJUlC1QRblX9UoHekYmo/m551ZVAyPWDNjhBclRyNKEVb1jVoyB+eCpZiJcZufaJ4wU2dJvHnF6buZfyVLzgI5PAJVgbGRkpGX9Gqtn7ezs1NzcXDMWZMpeJ8eoVPNBhpOxGR0dbcfJyCODZY0ZCYYHMOJpSmYCpBhk8XQGUfiEt4fyN5dkKyl3AIOB8zzJcj5DpwEyDK1Qlqpj5gRAz7i9z1jPZDyShUv2CgtnbsmkDGgOkbXLwit5IiHnxt407mF74LRzG2JKu6pXCAAFmdmJNlnGjMSWZRiqRoQiTsXHqyaYNmVVNQ+uqmcQbG5Zr5AzpQ5p2+Qa4XeTC+rQcQAUVx5hyESg1dXVhrydp2Ts0c1VvYShk5OTdqUij5aXCqW6yCCT38z3yclJO5qk4k/GsJJqMyfoMUo4UX6n06lr1671JYh5H2pfv9P7SiqbMgFAeI76ZHzmLbPUZYoauxhXJiZJ2BLft17GRVlSKpTu2NhY67+kG9/XZwUaBs9ZyjtgaHjCnqtfYuOydvO4Xh5XogzT0wJGGZLNzc1m7CTzMTCpsBkP48z9ZX+gpIGcBD36VNU7H21cmTjmPD8jlYlZ+sQIG4t1JivAl9j09evXmxHlWSfwzVrJnutnLncA9pSsFN4ZGxtrZ3f1W3/yiJZcAvORa27fYKsyA1pSZlWPJUmPXsgDgLZGCcxWVlbaPHt3hqWqquke9RGAJWOxj4BAOpgRxsD4m9yQDTc0OWp1s9qQmr54O7chlgSkPB7jld5iZjknTSwWauMR+IwtMpwMbH5/UJGgXilySSFiRZlERCFVVdtUBDSP+sgI5c3wRAYTO/IcKeTsnHDGeSk5Cli/E5lnxiZll3Npw1dVQ+B5DZvkFX+LR6OoAANzm3FqgAYoSG9HvE9cVSKUeC6KjDdj7JR2Jh+Jzad3AHglxS4uzEDxINLbrqp2DrrTOSu8LweAQeWJ81zc8GMtxJyTsbAuV65cafJCwTpyc3Jy0uKWKG/rlet0++2398m19RVftQ76md7V0dFZsX5K2HwBCsnWGGvul6pqoIhM+R15ZNB2d3drfn6+yTaD4PgWD5e8M0RVPWBlTYwVtexsvfDF4NqQFXJirEk9m7s0vsbPEwSm1Sagf3wmAVECRoDGuMSv9YcxJjfpaWe8evC402COh1waACkLxeRcmEugYm5urnnZycgZoxwZgFyortvttuTBql4Nd/ub3KOph+2B085tiC2ieJqkjOPj43tdnE24CC6lLU7HANkwjDeFxvhWVTOovpcFQlA4mRzFc+NZ8f4YI81n04uoqkZ7+j4l5rLvPJdMEfCGUZg2fVW1yj2uAuTtMZ459oz/Aiw2oDFSmDYlA0458zYoFt9l7LwDBQm8aIxrHtfiRXvG6Ohoo0STOuQtiUFm7BLdSbk6AsRooBsZu6SK0/hQWJiHvMyA4aZsfQ79Sg47nV7lJ5/NOD4PE8vCMDOo6mNbswQk1rmqd1LA+MkzD0js2/89jwHhmWFlgLOqs+RJz0wPNCtNkWGfyVABFgLoZFx4yuRKyMfcmFNjzuxq+0s/cj9mVrc9w5gycih18m/M1qKq2nPtU3uMB+j7aH5Mgz2PkWK8fDe9V3F/gAFFTl5lSgMBWWCIPJhzdeAxI+b05OTsZjdA2hxgHQ4ODmp+fr7F2n1/fn6+MTvi8Xn0LuV5cnKynYQAtPUrQ0v3dxvS3hdv5zbEGcuQtEMJJ31Z1UvPRy3mzS1KxNnQvFXCx2NFD1JqBMezKV0Vcngag8hfxi3azHEUNLDzkzZDVY+2TrpxY2OjZWoqbrCwsNBiqZQRilUsxoa2CSS9TE5OtuxaBqqqGo3uWAvj6ZmUCyRMaVIgGWd2pEucEJ1NiTmqJQGOp6HMpyxn3gVPWjgCpS0rmUfEaFlXMkPp87KyDCL5yWSojY2NVsTk8PCwLl261JQVBW+Mjpa47EB4AggDGsyxvgEBPDIKndIkZ+RBics00uYDQ0OZpodENil34IJs2AeykNH/nlFVLa/BmgIXVT16WXyUIkZfZ5zQ89IooWjJ7+7ubpNvMpsUr+Sr69ev18HBQdtHy8vLDXzIjGccVLay9/Xt8PDs6ByKX3GTbrfbvD7niUdGRmp9fb0POF2/fr3V1JYQah+TNzJtjChszsLly5cbDW7MQCFDl+dwsT0KpuQ6+R3aWLU9YQd9X15eruPj41pdXe3bL/qUzBzQkNR/gveq6gONgJd1FsoBaIDsYXtgtHMb4qTT0H7b29vNM+BdMCRoOFmPPJ9BVIbmJmiJ6Ai0DVfVO7JBUYrJSNypqr6zmX5X1bvU3c8lgSXVdHp62hJebDqKjxJ0jAaQQCWhbTPLmtJXTB/FPj09XUtLSy1mvrS01DwnSjGTkChN8Tu/z6zpvBOXR2SOeEyMIGPDI9Cn9CwYFYlhqF3ermd6B6OaSSZij1W9ZD0Zztk3BoLC5336nvXmdVM0gAmFqz+a+aO4qnpnoVGggJpCIt1ut8m3tZfglFXlkjb0fjSl+bEHMkZpDsiWvVJVDciMjIy0ozr6C1j4Hu8/vTDvkzzIiLocIOex6sx7AWLIPMOSHiaQSV5WV1f7MtuTys74qtARMJmJUDxiaw8EeBawsr293c6/W4M8qeH2JIl3mLqqaoyP9c6Mb7KWQChj9ePjvQtCkuJnZI+Ojmp1dbUZWePOEE9V76RDZkjbJ+aVwbcHtKTvecD2mvHQieYe02K+sA9kZXR0tLFIN6MNPeKLt3Mb4qR/CRpjpiAD5b20tNQSslDJR0dHrS6yDScTcnJyslZWVlqsB4Ll8RBWSi8zXylFwpcGVSw6UWNSQOkFZSwL/ZSJU5cuXWqIOymv2dnZdnm5PmQCESBCaVLUvFGGNOln3o7jS+Pj463KD6XrjGDGdnkmCWAY3UzIYqR9l+FkDPw8FdHo6GgrKZrea7fbrdXV1eb5izvmmVvrwwMkA/lexthzASTnaMlGVbUzvqenp+2s9cHBWenJnAOUXlXv5qCk6Cl8rEKeb2UYKFexu4xPGpt1dgQts3UpzqRUfT4Vcxqfo6OjdvmFdyXwyKN6vCwgz15Df6bCTQpfPDwzep0oEDP1LM9O8CxRrdPptDAMfcBYVZ15q1tbWy2Zinc56LUZ6/r6est6z9j76upq38kGtcNHR0drYWGhyas5Fd8X7kjgTY+Ir/ub/rDH6R7znuBtZ2ennf8FaszPYFiMPsj1ZsTTE0ZHk6P19fXGLqGwhazokmTAjNv7MTPmN08C2BfD9sBoFzLEKMiqnmJOlEj5UE4MHZq2qpdsROGJp2a2dKfTqTvvvLNR2BQh433jxo0W5ySMaFN9EBMWn8mYYcYeoURIksdljJK/lOY0jtPTs6MO3nP77be3OHqO08aQ0QzxLi8vN8/aRgVyMo7O6I+NjdUdd9zRqGDVjhzpMOaqXqZ20ngYDR47oKFUoDFD2DxCSVuLi4vNM6VQut1u3y0yVWdFOxT8oKwz85Znk8lG1kKlIP8+OTm7mlDskDdEmcgR0L+rV6/2GSCxPorW/ykpgANQwmqQA5cQmLvFxcU27qpqiUmMSRpH68nwZ4KSUIT3MWjkkMJM2SB/+/v7faCEAbP+y8vL9eEPf7jFEXP/mjNyYezJGGXcFyAWZ5cZjJofGRmp69ev15133tnkIIGP9/I283eMXVU1+TQHGDbnxZNFGBsba8fqGC5MzdLSUlWdGW1AnAefFayymlcabdnHCpQAmt3uWdazhDF5CQwiwIYJZBiFAhhrutOaMtSZk8KDJwOjo2fXG6qzD4Cmxwtcb21ttVoG6UEnI8Xg38xa0/bHsJ2/ndsQ81BkP6MlbRLeGcMnYUMmn/gWby6pMsJrQ1BYFFwmktioQIFD9o6ooKwkmSQKZuQhzvQcCWinc5aN67x0xiQh5Kr+4iDG7x2oIsYgMyId5ZL5SDHy8qp6sa3x8fG6fPlyM85u2VlZWWkx3oxBiudmrD4pe3Nro4jrjY+PtypEGIaMw2MPvAtgyixgc4KKzjOOEL3/k5GMF3svD3hnZ6fFeyF7SreqWswrvUKVtcgdLz0VEkPNW84+Us5AkKsceWYJFhlQx7DSK2FweLgJADAj6MWs1mZNeU3JDPCErUUyDunF+bxxpOda1cv1yMx9SUEpi8ZS1bvqElMgLg9gARviwBLp9MG47KOtra02TjI3uEcxAxLgfA4IFNrxTCBCH/UnvUNyODo62pKYAAA/t5bktKqf1cgkMmCy0zmrG53hBaBKXgm5nZg4u3TD2h8eHrZCG/YXg+tiDPv04OCgxa2xdhlrz1AIGRIXzmRO++FmtSE1ffF2YUNsczG0fk7wKYuqahl9PISqHkLOjFPGeDCWlrE3SSxilBKhCC+Fh6pEkUOvlEXVmaGTKFVVTdjFjYANHhs6ihEi/LKNJXBBnOJ7q6urLdmIUXWeOjPBxel4tzKlHZWhJNBoxohapPhQ3owUZZHJSMY4aDQYu8F1sGYUatLMwEvSzhlb1DJpSvbp4NEPx9muXbt2r+zXpDIz+SqT8MRQUYoZOkn6lBEbpIXzrHtVrxwgL9w86nNVtXdW9WLXgA7mYzCL3f5xtETym+fpL285Y6UZ3zRH5D8VOc9Q2AV7wovKRm7sNbH7rJNt/HkM6ejoqGUOM+BpBMSmgR3jZFwBWWtiTr0T9WtuyBAdQ9Z83vvX1tb6wk3dbrf9zdslR87sSihzkgFTd+PGjarqL2gDCPGwHW2jF4A2Hr34uzFiEelAnnHGq5MpEW4ACsbHz4qKLC8vtz22tLRUh4eHrdiM89fklV5SiAaQGbYHTju3Ib799tvbsZU80M+wogxlN1b1jmpI9qFQ85yljWpT2iQnJyctXpwxjUx0qDpLgsqNl9WUqnpJRJByVbWMRIJeVX0KnHJigDxDPFN/bTjel78Z4pOTXh3ZS5cuNeStoZ94szYklC8ulArIhkLjMcq8/zwSZZNT3DLFjcfB/ow9o2FnZmYa3ZZ0vndl8lfS71B5eq8MZnoQQg28K2uMFaGYKT4K17h8n8EkK5n1q050XoSRcVpriLLH3ACVg0fKMubte6hKwCcT7ewNc5YJNQDAzMxM88QAJM8HUhg9LIV4NuPOw8oQUIY1rAnDjuLO2uKS0YASAMJ8U96OpwF2o6OjfdXsxIDNw+npWXbw1atXW3/tQ3tMoYlMkgMqyAUgRk71LT1yOSe86JmZmRabtm8xZXRUzokzuvZKJjlZX5nxwBwDKe9C3yRf0kVAXRpq7wVg7Jece4WDOBnyUKqqhcywE4AoNiHlHgNSVQ0A3Kw2pKYv3s5tiBUxyKMu4+Pjtba21qjPqh4dl0kbKysrfbRmKu6qXqGN5eXlWlpa6stQTUPtHDKFfOPGjaYoKQl1hqHjPJZDaWTZvzTANgQDCo0zaOKtlI0xDmYlOt97cHBQc3Nztbi42FcQIYsh8O4YVTQ8o+3/wAxF5EgH4wW8ACrpkTiPbDPmrU+Ms/WkIChv6LzqjOFgdHg5CXIoKXNPsYml88x4UZlhCsTw/NG5bvzBgCQ7wLgMUm3poQzS5sZDmfEG9Tuzfqt6Fdwy49Qak3teSxYzyWM0xkeWfMc6U+Li2bxKnzUXeUxNAtLo6Gi7LCI9adXLJFMCwFmTGj1q/XmL1j/jsvafOcrwgrUUbknPmyebsuwEAfAuEx1z4HlofMCFB540dnrd9lEmZiY7k2eG7WEZ1YeHhy1hE2hUtCcZDeuReQLKl2LI7ENMVX4nwx8ZxkpmJkNxwC1nQ/8dcfTd9fX1xrB4Lhn0ebJunobtgdPObYizIDtqlSK8ceNG2zyUbKfTaYqAkkpvkiKE4hjI+fn5luxDKSQ1CZkfHBw0j46RFEvKTFNGFkJGrye1pS8Znzo4OCs5yavKzEuNweUZqCjEm4JOxW55ooxExlptnqreEQrK2P95k+Z7YWGhqqrPOB0fH7fNbHxoxapqfTs5OWn9khHMU2EQdnZ2Gog6OTlpnlAm31hP1CglIsmNYZmenu6j+KwtTzcZkZ2dnbZuAEGOH1uAvUhvmfEjp4PyeHBwVkxkMOu+qlpIhGHgOZF7a1XVO1/MaGUdZXKOjciMf33zPN7W0dFRCyFkicR8nj9TU1N19erVptDJPIqVMWJsyJpwy2BMMauszc3N1fr6evP2ARxGWUITYMDQWw8MhzEwgvrgnYwCel1yEbAu45mcra2t1cLCQmOIeHXAMxYDQAYcjCEZDYyX89Bi5Ds7O3XPPfe0vqbnbI4kGwIKAIhxek/uaZ8jN7mnGW0e96CnCiB3u926ceNGAzTGCxw5srm3t9fi3xkeOj4+bndS5367GW0YI754O7chFmMTP4V8XRiecR4K1uaam5trtVxRLRI4xGQlMEm0onh5K6gjG4/iUmQ9s4Rl+9rUFBwKLxU6JZdof319vSmq9PgU4aiqVj/XMS3GjtflueLpmeVtrlBKso7n5+f7vKFUWuY8qX1Jb2hp42mL+/8HB37vhqi5ublGlxlTJjCZM4icccjaxYNZovPz8+1SBUdg1PHmBaTSTA+QQVOt69q1a21NgSjHvZL2ogAxLBkrzBgzZeasuozltbW1BsQoJzF4XhXwkAY5DSk6WR6C5LOq3nEWWbBAZx6BOzk5aQlODKiwQIYUqnpZ8YBweqFyDrIKWOY2SJZCb2Ia9vf36+67727elH1oTqxzVS8p7PDwsO8qPzFpzAEAeu3atQaS9Me+ZkjEZO3p2dnZGhsb66v2xqDTN+ZNHQOGm9eXlLp5cjkNIA3gMJQMM0rXezJfgfFSLU9hEKc29NU8Cct5DoeEZ++CCQ5EzjGnQJjDHt7Z2WlX0lpHcpZJZoqXpDNDn6WuHrYHRju3Ic4zpYRYXI6A2PAMBYWDDknjJlaUcU3eZ3pLEmIIFSMpHsVL47FVVUPkVT3llVmiCQQoHEr76OioKcaqaueECbONnkofZVjVo6A8d3x8vBlvSjk3DqPtmE4WI0iP1hVu5tpRBYBoeXm5j0bm+Ru3MZvr9EJ8hmeTGeOZEWv86FSeICbE+LMqU9ZlZmz0DT2WgCTjyOm963fGk8VEjYXC80yGyJow+jwVBmxhYaFGRkba2qyvrzfDRYbFXNMLM9eoTVQtg+33mWCWNDq6MwEmJY+BIQeABiNn7zEuEhn39vbaRScybQFZAAFAxRglxc7b5XlubW31JVWZQ4V7RkZG+sJWZEs4Ao1tnXh9BwcHjaFSb8DRJAY0S0oqK0qf8GrNh/PQPHJ7LI2/RDrJZjmu3LuD4QcGLo/hAcaMZVW1ddPnrAB2ctI7vkh/ZpZ2esypp+zlTO4kl9bSWek8aWItUeMAm302WPjm/mxDI3/xdqH7iG1aXlsqSYoxk2YGKUUU5ujoaCt1WdWrAZtK1uczaUpcamJioubn5+uDH/xgoz7zqI1YF+SnDGFVf6KCfjkC5dk20Ojo2YXet912W1PEm5ubTVFTAOJxlERV9XkAPOM8N2hDOxea8STjHGQBxIiNIz2t69ev1+TkZCu6AVUzCp5pTsWp/Z2Jdow9On9//+wWnbzJhqFkAFXeynyBrDNdVW0eMu6H7mTwu91u3XbbbW2MGcsSMhDbZmB49OhSHs7m5maLQ5JH60Ch8WrE4Sl0uQDkL/+ftCkqO71kVGEm3VT1EoMmJiZaZiujCviI1VLEjIqL58WT5+fn++4n1hcgCkDDCDDkjLUz9/oG6ADULg5hsBgHYyfrwhvWU1+AKtm5IyMjrWwl8AAgAkuSshK0zMzM1Pz8fCtDm94pmhYLRu+Yi/SaGXwJW2TLXtF34+Otk3Nzl4wTYApoYC7Id1UvYRXQMIeZl4LBI0fWxlwkDU9f2iuzs7OtzCaAfPny5TYfnjs9Pd0XuriZlbWG7eLtQrWm0ZW58QhHxjQpOoqI0e50On3352Ys1LOrenFMdw5DhpJhbDZezaCnTmlJyKqqprgzixX9xJhkZST9QinlJjg6OmrHHngPe3t7tbS01OJym5ubDclTrOKf6EzGGIBhdLXt7e1aXV1txjI9jNXV1VawQNKWbG0G0dwzyopzZKbr3NxcYwd4O6Ojo402HgQYYqGZyW6uzF0mUvm3/kgsI1NoXYoF7ZqGPhN9rBWAk56uuZPUl5m229vbNT8/3+Y4C0rwWPPGqewfWeVhJMgkR97DIzafmWAHBJC5pG1vv/32vrgjlkV5xzQcuY8YToAjwVfmSthH5JfsS7Y0B3lWH7uVx4VQ+LzWZDYU7zk6OrtFSqKeSnEMDeCWHjqvVLlIhVSsTzIQ5EIex9raWhsfOUxvV1Jmyh/QRo8A3hniQN07ypXJVeb6+Pi4VlZW+pLrDg/Pzvrzbu0vf9tj5EQzD9irzI+RcOdndCH9RCaAtYmJiRZ+SN2RFdNuVsvw0bCdr13IIx40xDxISiKPnxDYTMJBJaHnbApKRaYyZSh+YgNSThIbKPiq3rlPQuu6vapqin11dbUvWSqpPcbZ7zLOKEZL+Le3t2tlZaUp4MzO5RnazIO3FKHtjcX/GS7fHYw5MQI8JYbUuFGFGxsbdeXKlT5PCV14eHhYCwsL9zIwVdWMps/l5mUcKHvZ6OhX/aYIGSNlCc0/BWJswgCAGa+HMZBcxKtKzzJj8NgBxhK9y7NLKraql00rhi22aM2AADKUlK9cggR/5ji9bjIkaY085XEs68/TTg/YfFP2Pm+PMZ6Om4kJDhp4hriq+oDY8fFxXb16tZ1zte8YcGyD+WYMM+HHGB0RWlhYaPOZoRyUPblj9DJeyajLDxkdHa2lpaUGLAbP6svKtwfsczoqS+w6ObGystL2EeYEeLQXslKdlqEQ+RL229bWVl/SV7fbbRfdOALFwcja3PqdyXT2kjwBnrRTD8Jn3W63NjY26vr16325D3Rx5pnQd5wTe+1mtiE1ffF2bkMsMUPciQFgRKt6ySmEhiGzYap6h8vFEG2IpE/Rb7wNv0vDIQZL8PLM52CmaMa+0NQMZ1WvtF9VL2OY8oFYk/akKB0PgC55KI5fVVVTBKjsql5Si3ldX19vBj1RcaJyMa48i5kxMhvMc/WRwtNHWbKZFQyIZEUzSSbmATASe/RcKF/8m3EUx3OOmmHBSAg18GoykcoaMggZ80slKc5IDpJSlFAkwYdhBYY80/N5F+aRAb9y5UofHcqbJj9AVDIPvGbPRQcCKxkeMS8MZnoTDLH1u3z5cisyAaQOZgRndjUWgAzz4mZnZ/uofXJg3at64C0L95hzzAVl77n2CXCbMmYOZmZm2nWiwJl97fQBGQD6M2EwcyAYaA3gG6TrB+OxbmwDyJKez2Iz5DT3uHCOtU1vnV4ERpzxz/h+HiHDKgpJmc8M/Vg3rJEQQQLadJCM08+Nz/uTZRi2B0670O1LjiwR0Ex4QSenZ5WbUGUdRk/sgkKmsFGfNvr169ebseFt+LekLIaXQDIGvAWKrNvtXVlIOetnGlWbhPLNspBjY2PNq+SFQJoUhdhOt9vLpEZrpZft+RsbG/e5wc2N//NIJWcAGuaiqlqCi02Ldks2IROv9FN81HxU9S6AT5RtjrEO6e3JqK2qNu+o9apeBrL5z7nlzWQSkDlJD9Z7GIPMHXBdnJiqm63MnXkUV86jOZQXRUq2q3pnvbU8P+uZPG6fRaFTmEnD8mr03zwz5IMev7XAhOS5dXOlP96H8sfI2J+np6ftTlv9zzwFYAToMzbzJP5MdpNBsDZk0ecylIUVIN/YE3uC12vvZp4BoJXAdnx8vHm+5hSQytBPVbW7ga9fv94y+7Eh9gLdktWtjCW9UvqJJzo+Pt5yHYwNIyb/QT6C+TB3ABE9CUjRuePj43X9+vVW8pJsKE8rGz5j8va4+griyOmc3Kw2pKYv3s5tiCk5xriql23IW4O0x8bGWnJAxpcYAnWiR0ZGGr0DyULkPK88fqNaUjbPtmnT8EKWjINjVGLJlAnFmPE9gjyY4ZsUMU8768kyOmnMeEhASlVP+eu7eUjPiUF3nAXNRFmbM1Sb9WHMUzkxtliBrEsNDKDwOp2zetvovcwSVliADFjPZBkYJQYBbau/ynlSYow/z9yaUspCGJ1O72IERoFSBN5k5/KAgarj4+NWyKWq59lkrDipa0exKL48M42l8O/MyOeVeC/Dpf/2SyZuATDGhwo3lxik69evt0tP3GS2tLTUwgyO3mVmrmNGMoXzsnnyeMcdd7Rz3nfeeWetr6/XxsZGO2po7fL4mlCAozdAGpZjamqq7xYlspDMFmCd5+CNuSmo///Pcz+aW3sPeyMMAKSYR+MEIOVwmG/x8mRcMvEO+0Nm8zie/ccYkguMirkmR+LU6bU61ug55s+8osVTdy4uLra9pR8MLWOcjJX1yuS0YXvgtAvFiCHaqt7BdUp0ZmamxQ3RI5SXzSMelEaPUA/Gjng6mXhEmUmsgbJ3dnZa0lHGQTqd3q01IyMj7RgSz7uqV2PZdxwDsHlccegqN8knmbwC5Wccz7Pn5ub6zl6Pjp6dV6QwO53ePbg2lrnGMEjISCPiM7u7u42eNRd5nhQViTKUyLO1tdXWyfwfH5+dld7Y2Ojzjs15Jq4ltWasSeHa9FA72lA8GZVMOfF0qnqVsXh5GJSkeiF785bendgaoJQZ0pmN7rO+m2xAAgK/05LGz32RynAwwxWNSkYYCntLHwZDOZnYAzBtbm62o0JOMgCweeaYAfQcoAsITDbLOW2emkRDx2QAFoYPqBVKynwP8V4AxXWkZDDXcGRkpN0sJFFrdna2Ll261PcscXHsQq5ZjoucotUHE5zyjmfyKLRhXat69w8bU1UvqS9j9/qTgB1DsLq62va4fVpVzZuuqtYn8m+dAAQgLb1962mMSevnuWzgeWJiotW4tq9vpiEe0t4Xb+c2xLIYk+ZKSlqCQVX1KWtnA21UirrqzMvOW3Rs0PSYeXM2rWo+Mn9t7MuXL9fu7m5du3atJU9UnQGGxcXFquqnGHlwNiuDkHRkVTUKNjNzk9KGsDPbNJOYxJLzHDZPyxlixiH77R2MrdrAgMbk5GQrSGHjigcysihqysX/UY4SoYQaKG6ZlUmn8phu3LjRd147j0jwVK0nRYKK5MVT+K7i45lsbm72eSdV1RRMVU8R6qv1ED/TH/IxOzvbjoagaMlPHk2hQPWzqnehw2BsFOhCT1PICcTSMJr/BIgy7p219dyqXkGHZDoYFn1CQ9ofVdUHWrBSYsSO82E7KHvr7aw8YFNVfcbHGKamplr9ZjFJHmLKGMBm3zCc9gyQBVhfunSpVcnj/fOmgRDPtxckRpE5dK8MZ3tXX1RNy6NX5iLpc3s/2Rr9BwqwT1U9ZouBc2wIaAA25QGYU/UPeNL0ALCG3fMcwA+7wTsXljGXGbZKo53JhSnXw/bAaBcq6MFbSCr44OCsZCCvDgXjXsys/NPp9G706Xa77fymTc/zlchFSAmhzWwzMZwo0zz/ib6Zn59vtM5gRuj09HQ7z+kz8/Pz7UgIBLqwsFALCwt9CS76IflLzCrpyzTGnU6nNjY2qqp3XGJiYqJRvXnYX+Yo45Co2jzrC3DDg/BsStu4Bz02iWU29e7ubl2/fr2tN5QPrIyMjNTS0lJNTU3VRz7ykbbJncEGSMTdKP6lpaXWB3FqhhV48SzA5OjoqN0CJEu9qleUIJN/eC3AEDqRkltcXOzLus6jUHlcK5Nt/JvMU9RZxYgnnAYGMGNYM8kQwCI7xpaJeHlMxRgANBcaOK6WHqlsZQyQC+U7nU5dvny5xsbGmvFMoIllmZ2dbUBKdjJjLXeAIbRujDgGh3xb98nJyWaI82je0tJSnzfHuJlHiZr6kgmaSW9vbGzU5ORkLS4u1srKStsrWALG215KHSIpi5xiuIyn2+02PaVIB11kThg7dLmkQxXb1tbWGsDJGtnAorFxPOwzsmn9HY1Mtsl3gDv73DOAGeAxk9c4UzczRjw08hdv5zbElFSe963qZQATkKrqS6by/0R2jFMm5YgzMjoZAyU0GQsTD8qkpoyhZozFRvb5NJqEmCKt6sXlMqlKgk/GrACMubm5mp+fb3QzA7O6ulpVvfq0qbyrqubn5/sKMFT17jI19u3t7abMKSwIN5OE0E7pXfE8fDcTxTyD8tB3c51AaXZ2thXNwFowRsAKVG6uGEmJQcZBMRofhStZRZ/T6O7u7tbq6mpNT0/3lQnl3Vtn6w8UULqZ+c6DNNcUk1rDLoUfTDgBpiSA8Yy9gzeV5+RzfaqqHZcyV+h8Hn1mxjLuEhl5YRm3tI+sR1U1Zd7pdOpBD3pQ69vgOjnK1Ol0GoVKafPasSzpuRlntgTXqROSguVRkjFzSO4VArJWWfEOw2TNeKUA3ORk785l8o6Sl9ypWR/r5hhgVbWCJsaSoHEQ4AGQ9JE1Tw84Eyn9znu9j5wC4/QZcF/Vu5/c2hqrRicANXnkMFmfTAYcGssHVju3Ia7q3UkMFSZVmEdXNjY2mlK1+VwLaHPxnDJGKEMyY8OOTDHQkL3/V1X7DAWWdCIatqqaogYMxIPbZIz2zjgznL4DbVMMYmjj4+PN0Bq/m1Dy/C6UbqyqVKWXggK0EaHnwQQpcyg8oO+UAaOW3l+ibP8eHx+v9fX1RuHJXHZ+FngS56aEGBjex+bmZi0uLjZPIgHSxsZG8ww6nU5dvXq1KRTPtx7GrZISilfMG+VKsSXtXlXNc6B8sDMUPKNNsaWXYW4pUtSeRLyM6eUxqZmZmb4kMErRsxn79Ij0gYEANoRJknIfHR1t1LaLOTAx5hA7kDkX1pdhT3kCVMkfEDY6OtpXd3x09KwCHlmVsY2RSsOTSZzAJg+TfIqTCwvlfnQOFxO0sbHRLjUBLp3Prar2LMVCzDf2xzgZzdHR0brzzjtrd3e3VSQTPmEIzaM4O300MzPTF0IbHx9vzJm1tJ4px9PT000vZiyb3sucBIaY8RcKpEeA1gTDg6c5ABxzDoxkUiA5zOOb93cbZk1fvF3oHDHFRmlQLOoeEywCmfSxJAkF/Blen72v847iy+I8GYNN6pLxFqceHx+v5eXle8XEEp1SDOIuSUcOXpqNXsujFsfHx3Xbbbe188UMOAoZnWkcPNKM121tbbU/g6X5Mp5uo/OeABmVogAQYCGp3qpq1JyQgWMtvDp0lrhYVbXEKWtVVe2zaUx4qOkdJM0JZCRYUViBUqDoM9ElM455HJ4lRn18fNzKWJpTc8CbzmS/ZHJSsTGIYmzGmCDt5OSskIniCjwZ680oqlSV/SFrua55/lW//BzQ817jqDrz6BYXF5tM8GxHRkb65sLckr9MosMa+B5AzYB3Omf3EzMeWUIxjx7RAcCQrOmUEXHW9P4STGVi2vHxcT3oQQ9q+1kowTMPDw/r+vXrDRSQ8Ux+I1dZVAdIwkDZe2Lc9q7Qkedg6ciBdwCdksjyiJ8wgPlwZMkcnZyctBwOR6jIinej/smjoiH2pd/Nz883ee90Os3ZGXRqUOGD55dvVht62xdv5zbEFFF6nJBnIkOUDsWfMcn0AJyPTW+Bl4KigebFyDL2dv369XYtGgoXJbSwsFCXLl1qqHkwQStjzegi5fkI997e2dWHly5daoqHMWCw0URQMqWxvr7e4s6uguQVQ6boUxs36SaxWQkg5pFSsIlyA1dVXyIKxcDA5bnI4+PjBpxmZ2ebQsgkKUrF2idVqB8UjT5mFjFqjZEVJ2N0rLV3AQ9Ai5+ZF3OrL/qV2fdZC52RQXEDX8blHUlFJmiyfoqSVPXuJmawyEJmNaPIrVnKtvVM6ptizCNYxig8kPE9fUxApz/OaQ96ajKWyY6wAmOiL/qfzAnwlyELht5RsapqDIx1k+AHlGQiVKfTaf231wAVhs13PMt8M/zGLEaev8+EMX0zFrLhmBBgYO4zvEQPifsvLy83mR48nWGcwhfex8ibV+yOkJQ5Bdisp6N59jXQTebMHVmiiyXMZvIoGbCHsUXD9sBp5zbELvOuOlMIhM3iUjiZjWjzUZw8HPEi1C9lZDPZ1JRwUpKMieejcmWTzs/Pt2Qv/UnPBp2T9BC07cjAYLbkYKyHQpdIwZt2TCNjfGko9VkNWEjenPCWKBDjozAZCHcvy7rN2Lj3UTKSalBv0Ht6pbfddlujzEZGRmptba3NXcY4gRGlB3mo1pbBSSNDgadhw4Sgd81dsgIMBuXGCGW+AY+FgcNa5HphOIQ1zLG/s0hNlrOkcLMkKXoey5NxfWBAdjag5mx4MkjkwP+NTX/E7lOGfVa4gXEB2jAm6MmkQ5POTFqbEc24OQ9VSOf09LTdagQcJK3vM/5NDjOrmrHLEwXmAYCS6AX0YKrsXV4qeeKlX758uQ/Eodbdm8yo+QOUqj89eKY2jZfxYIpklkuEquqVWyXvALeMaZR16kKAKZ0PY0w20L/Jy8jI2VEvoNoaZv+tNeCbOQn2UlX1MTL3dxtS0xdv5zbEH/rQh2p+fr7FEMXNKK+qagUGVHKpquZ1qN5jY/JwKXrUM/Qm1om6zOQclBwBI6AuWKCwKRWGm5EVkxsbG6u1tbU2Hh4I4c0YJsU5MnJWkN7482zo9vZ2X9KTIzqZuc1bzKSn8fHxlpFLucoOpfTFih03ofhsPMAAsDGP4qgUCqTPU8841MTERK2urlan02mVp3jY6TUx/undZFIbNA7VZ5yaZ7+ystIXqxZvTaMikUziG8YjM2MxF4ytsZpnSszxK2EVgCGNvP+vra31JQjxru+4444+YyAHIatBmRMylHKbFLl1EscdnF8hFs+o6mWNCxGhnJMhkQmO/jTf2T81kIHNpaWltn6odoDH3wAiWUDpYr5GR8+Oi6knwLsTVz48PGxAyJoAuZLUJiYm6saNG80DtMbGxqj6mTUwfvKxvr5eW1tbfR6nNdLE2wFxeS8oY3Npvzk5wduW6CX/paqaLjBneS85YGkMCbzcE25N5QZ4Bp1DRwIGGRcHcjlE4uz0Bx0jbyNDAsN269u5DfG1a9fapfKMVVU1uiWVCsTJ07GxKYWMIWRyDeNDmfi8IwAov6reUaft7e12UTbUzHPx+TRAjIiWiTposkyAyeo1KFwGhDE+Pj5uRTDSEFKSxsI4p5eYaFZyCxozj8JU9WIvKNe8CxiFZ12yuo6zuQzi7u5ui+1KBFpYWGhrYM5zTSnP09PTVpA+6cqkuhg+oYAEXFlwQ8sMafODIdnb22tH03g1DAxGRSGIPA7GW+CpSgCkvNJbSvoQ8+CcJ9ll/BnGDAvIbSBvDD1jBMjls6xRJtgJ6+hPKuW8SQdATK8aiDNWIR1zOpgDgMIGJgEvMkHOgVP7OI/xJCuS8dLMYs4ExLm5uZYBzmM0BxmnTtCACTPnxgd48aaBAsmHQKZ9k6EC82ye0MnGB7QBLGNjY82LZaAZdUCVTkCXW0Ofob+qqjFKqYMGPWMJr/YJOQV0jcH8zs7O9uVz8Nh9zjoz7MMY8QOrXShZK2N9NhkByUPnsvN4cj5f1aOpIU+KBTKt6hmoPNdHmHhPWSYR0oMMMybNEEHS6cnLgNW/zODMjSamkp6YuQAG1J2GgG2uVC42atJ4km263W4rM0nJVvViaumdMZJoa4oPFZheHqOQ9LKCGgoIULzWz9wDTUAJRJ/JXei1pJVTUamPm0oUpSy5hNeMOoX6nU/O/jME5tj4Dw8P25WB+mINeE2ZSWsd0ptKqpg8ZWydvCZ4830KkTyZM3FSnru9AiyibBlT3hgvU98AEJQ0JTwYjiDjh4dndb6BNkVtkrrXf/sUIABIjAvIshZVvRMU1mYQeNrrwIE9DCzbc8no5LjIAPYKQDcf5sR+sy6YJQlM2SffEz7ImLu1Pjw8bJX6MGhCKeZD/70bFVxV7TijPcAJwHJlEZ1kIUZHRxuw5hDkzVF0U2ZO0wtJvVt/tHjG/DMJLlmFYbv17dyGWMIMJccz4F1UVd9xoDzCwlin14oOovQybmtD5lEZQkWRUJCZ+ZkJDloiWZsI2mZYNzY2+mJmbgOyYXl8+s8oVVUrxMCrY/xsMGNIapbCs+lSiVAcYuhKbDIovPpOp9OoXHFSSjITQzJblUKkiPP4hgQzmxy9lokulLo5oQR4MFmhDPUu2UfMH0NBRjAJfs+jGjQ0SferMlbVMw4MOmNOVjNbdXd3t52NVm2LwrQ+CR6BxKpe/WPrJUciKcRkO6w1UGVNeI1JX2IMchzmJ+OklKzPJiOR3rBcgKreeVyAMBMhKfeJiYlaW1trCY+UPwWOXTHWvFqSN8g7raq23wAQf+zBo6OjVno2jaY1ALKcJpDvwEtEs2KMsngNTxQTQH7JQcZJGcKqXsZ3Uv7OFeexOHsqk9bIiTmWYGd9efDCcsaaz8SeGA+ZEUKydlXVx/okm5cgyr7Wfwmip6en7drKm9WGMeKLt3Mb4vRICBOkmZ6szUQgGFpKwmaB3jIhiKfgZ5Q2RcQrGBkZqcuXL98r5ibxw78JYCp+ikezacSpKEF9dlYYmmRkJTvMzc01r6bbPTtzeM8997Txn56eNuoMADg+Pm7Z2VoeS1BKcGxsrK84RVKpSWFSvN6BbgYM0ts7ODiozc3NFpPN5BhKwPdt3PTGecHp0TDGlAd6kzIGTk5OeuUdUXXGQVlmfM5aO6qVFdV49ICWrPdMnhHTZADQhoyh8SW4yQxo/fN9f6whkImdMN5B7x6jUtUrGQkMJQ2dWbDmFnPQ6fQugtAn+00SEqrZPsHYMNS8s2737M5cAArIts/NLw9N/HdlZaUB6I2Njbp8+XL7nn2TyYcUcsZgT0/PKucBWEI/8gHMn7EBBVXVYucLCwu1sbFRMzMzbexADmNnHNbTfJLjTIoko7Ozsw2op8dJtzmSZ52qeiEg3nc6DQm+M+wBCKiOlqyhM8qZTa6/MvjJEN1HHzkiOuh4yFVZX19v6zFsD6x2bkOs/msmmohBUQwMXQoJBUuwbQbKKCmejOMSFs9M2pnAX7lypRkOG9/m9b2kxrzb2TrJFFB3JlEk7UNR2QA8PBvA920yRiOPBTkv7Bq0ql7Vrzy7LJ5GmUL74ngUiGSepN9UK0rlq08Mus9at6pqCTMSvsQ3fd76pHeYFLt5Rh8mfac/ElkGC2ZkElXOPY84LxIxlqQxAYCUI0Yyk8TEthMcDHpHaUCS5k9qPpkOsojupDQHjziZf0aPjOf3cz3MNRAi9plxfkCEDAAYYqqD3vrW1lbL9mUUrbUqdfoN2MhX8MwHP/jBje5OZidj5FXVjAp5sH+SSUoQClTIhHYaYPCSDAlSvFLyqOZ8FkMhBynHgIxxAWb2+vT0dAsf2N/2MJ0GKGROhrnOUyGpsySpSpLS/6yoxwnxLMeQ9JkOxcQACPIigL6MhaO3ExBYg5uZNT2MEV+8ndsQZ0k8ikRLRZJZlpQRhScJSUwTEkSbZryH0Sf4lLjYEdSX8RB9YHRsJkpKiUreXl7zJ+EBoBgZGanbbrut0eDiRxCmG430Mc/tMqq8Z2ebp6am2jxWVVOIjkaIH46OjrZyiBnDFuvWj6T6KT6UPeVhroAYKNzcZba6DQTtY0EoIOtKMVNyaNnNzc1W5Uwsnccta957eETdbrdlvPMe0/gndWgMPKZUvNadh8t7kaiGZvQM35Pkk9Qo8DM3N9e+c3h42DJxM+RgrsxLVfWtl58bE4/GPAAM9hGP2Pd4RbLQUe48xjzP6nuUboZU0MMJSuwPRs73eLkJSIVA9EVop6rapS55yUKGpuQkpNEwJ/pZ1V8KN5mBpOLTCFb1hwysExlmmAAksiPMY97Rxxl2Md5cTwAegwFUuPFM3F+fyS3wIPluUCaA00H2xbqoTWCdrREZxNCQOcbXfh6kihOA3ow2NMQXb+c2xGIV4niUgEUlWJQjo0LAx8fH27VqFGF6HjMzM63mLWoIopycnGwJURBqJtCoU0v4nSXM5BKb0DMpRRtXnCbjdOk9Kl6wvb3djLP+82BzPlJ5Znzchk16Gf2IBXCzTGbcMngUDiOY8b+qXi3ePLKVHiAvf2dnp69EngQ2SpiiYsyT7kPpVZ1tOrR+VliScayqEFCSXjrqNZmONCrG6zOUcRosn52bm2uswPb2dl26dKkBP4DCWWByksqY8U0FW9Wrm8zgolePj48boBmkQcUy08iKzWeIwc8BQ+9iPKwp44/REQJhjBkK48izo/affxtHxqGrqsl9jtlzjfPk5KTtYUCUV2g+rR+mSDZ29gfYpReAZMfnGHVndsXt9/f369q1a82rW15ebmDIXk1v2L9lxEtoympZwk1kwj4d9NwBX3qCXvB8RhtjmImgDPHS0lJfhblkSjwPgAaS06iTY2yKsJR9r98J5Fz56p1bW1vthqhhe+C0CyVrZUICVAiNqalqY6J1KNsrV660OGtSOjY7Q2aTU1QQPIou48KMDaMN3aeRT4PE0GVSS1X1JVhkTLqqd60bz0MW8t7eXjvyU9U7wsLT5uHLwDROnpqL3dNj5Zmn54fmTSTuWeoA67vSeuZDxisFy/tjyDIumyxGMhlJs6XXRHmkfGTyCqPvu5QpsJZeCnDk/ycnJy0hJWPMPss4YwYy8U75TnFzYQQGOc/2ZjwUiDJHxueYTFU1I0SJpocC/FRVy7x1cxjampwADGRYbJcSZhTJ68jISC0sLLQTC0Cwd5EvF2rkESt7CDC2RwdBKrlBjUqk9A5jzGIp+gCcbW1t9ekAz86KdgnaycXY2FgDDHfccUetra1Vt9ttuRXAdgJahVIyuS4Tu3jHZDFl2XygZwEe42XgjNNekkjIi2ecsXcjIyNt79OXmBQhKjkFABODr/9Aat6utbW11cJdGAcgjJzaswlqhRhyj8qdSUbz/m5Dj/ji7UKG2EJThJQvz0eGprNv3e7ZdVxiO5QpzyALDRC68fHxunTpUjOi0Fu32231bwkxRZUInlLheWxsbDSFB9FX9ah2lKmxzc/Pt0xo3iKv3TykB0Lh5NEC1FxVNXpU33jd/jA2slMlpFA43W63zQGFklQUQyDJ6eDgoNbW1pqS4bVQ7nmEBv21sLDQ5j8rBqUizr8pVu8bGxtryVeyNfV1e3u7VWXL413p0XufucEkMEy8HS1jpcksJBORniY53NjYaHIjJJBlGnnXDB6FnM/3u6peMlgm9xwfH7cr9k5OTprBZSAzeUh4YHNzs2/NyRnASXGmUcm8CsbFHCV4A5ZTVhhynpKLE6wxQMXYJrD2fNcyGiNGhIzot31jDq5cudLAVFLoOf/CUFgRspk0OWCagEV4KE9KJFMEzGeCJ+BtHzC8jKhnAPTHx8ftHD2Z9w7gwveMD+ABcjJZdGpqqm7cuNHYgfRoybnv2wfJLnCAMAzJBs7MzNSlS5dagqR9ZW6H7YHTzm2IMz6IHnb0Z/A8Lu8TGnbYfHR0tG6//faGYmWW2jhp5CHbrIRkAxBChnVsbKwdqbApk36ioPI8qiQhP0+ECV16r0xG6Pzo6KzS0z333FOjo6ONcrIZ0HmMD4XOa0svigIc9BIZSwrA+43r+vXrbQ0yqUamLO/AfcEyUo1zc3OzIffbb7+9GQpKaHp6umW0VvXqiFvTpKutRV7oQTmYX+tCwfBmGdM828pQUMoUbxrMqnt7LmTMOvBGjUtSj/Hoo5ilsWbmfBqKjY2N5nUCbdbL34xGVfUVTmEceWsUtCziTBRLcEcWkqo0j5gMnp7wRZ5LRolT4uZTZSkt6fAEouaL8bX3rJ95ygQg785rPz03z6Bbf31PA2E/OUueNQPIEO9SHNZneI0ZViC/6gEw5OmlY6wSIFqnZI3IL9nwnt3d3bp69Wo7qmQusDHebz6N33qSYyc/qs6AvkRGzzRuOSH6ub+/X2tray0WLYGs2+22MBzDnwV17u829Igv3i50H/Hx8XGLd0BwGSuywdJToeyc1WU8Tk/PzpcmjSbmC1Xa2DYID4TXRElV9YqcE/BMhKCAlLUcG+tdxkARMIJJpdo4SZFSUOPj4+2OXJ5fVTWarapXWpPyE0fMzcuLoEx50z6fFD6AQNEad8aKbVhIHQI+PDxsmbCUJBDkna6YPDnpXSDAyFdVUyJJ/2fc1/jMn35QyAyCeeJRoc/TW+WVWWN9yLVJL9t3xdmqqnmZPDqemOcyrABGVTVviZLlDUl6yuM6kon8ybOcg0lYCc4823x4D1n3LJQ1D2dvb6+BWmPyfN8lF5mYRD6BQKyH96a36Xme71gdYJP70l7RT/Kj/5IfB6lQRh2IY/wAtpQt+x5YdqGFUprOg0sErOpV+SIrfpbJXwnSEzj6fwKwfA5dBVCZn2SHMskqi6zQL4eHh61kq+9KdJuZmWnJVn6uhKsxZTKiEE7m5GRceHFxsYHEzCfJrPVhu/XtQpW1eKsSjmweyjApS5mUSZdlPHBkZKQd2OfN5VEVx33Ss4L6ZCBnYksmnzBYUK3npjKgZICFbCgkij3pU3GXk5OTvsSL1dXV5kn6GUXDQDEK0DWlnd5femhoxDyCUVV9G5yyM8dJe/L6eEPmNtG1BA5AwDsoX3NMiTH2aSwZMlSp+R2kr9PYkQHMSM6HZwJWeU2i9fEnk6/IVsbNrKP+AiiMikQhY8oYfGa3ZyIfJTzYX2sDUOWcHB8fNyPCYAMSSUcCGTnvjHFWqKLAUfQAm8+enPRuckJ/p5xliVP9YSTIE/lzPjUpecbcuMy54ixARoZB8nib0ADPLGuf5+1FQLmEKFTs4eFhOyHgbmAeMxkBiHMvmKvMebB2mXTHU04AmXHmweTIjKEnYNN/gCjPwh8cHLSwjQtkqqqxAMaXbBGA7LlJ5QNP+pchpUFq3+9uRht6xBdvFzq+JJaa8Unxyyx36I+jGpRCovSqqsXFxSakFMzs7GxtbGzUjRs32vWAfu485MHBQcssTsSXG8pGZEwY9vX19brtttv6FEIaOv0l+I5GVdW9Kn9JPFKXGEjhBRJ+lFyn02nlGdFVNjIvDogQg5ufn+87rmW+bexEzmnAZEwqI6ngBSV7cHBQV65caeACdT06OtrKRPICGfeqXhyOkqSs/MFyoOdGR0dbHDbjkwkGKLj0gvVRhn4mH1FYPpeUOmOg36OjvSNb5pQhw+qQ3cy+ZcSrqskJr7aqx3ZkQhXAp08MrLUzjozZZi6Cn3k+2UO7k0+XqKRnqr/oTsAhjxRhghKoZk1yhkq/rGdVtUskGLLR0dHWD/I/NTXVDLxiG9Y/i4solGMN8xgacOXGJ564e4QZHFejWu/R0dFaXl5uNbmtD7n1b+vEYB0eHrbMfvJmT3gf0GCdExSOjo42cGBP6k+enfb9POstbJTrnqE6/QREZ2ZmGiU9NTVVly5dqvX19RYaSiBF7oQMsmgIQDpsD5x2oRhxJorY4GIa6T0eHZ2ds71y5UrzHtMIDMb4kk7OjGZZsEdHR301ZHkyg4lBNgdaiNBlrC8TVlBAg94nwzDohZmHpNB4Bja5WDYQQnkyDFU9zzWp3VToOzs7fUXou91uO2fLiDkv6HmUq/7qy8LCQu3v7zdPgafKEFG24t8MewKmVGYag5/xT33xO0aN4ctrBPPZmXhnLf0OSCJrFIzxkTn0KGUojJFxZoo1s3uVMuXFohApXMaNsciEMIYYqABAFYuhCHnJnid2l9R1hiCqeobRfjAnPLY8T24+rLs4KJnws0wISprS98gXjxG9LC47Pj5eV69e7ZvfZICqeket6AEykN68PArr0u12mywD5jKl7R3hjbm5uQZiGblBat98MnQMobnktepbslUMqrVKzzz1gvHbp0I79hkgkayX/iVwEe7BcBnv0dFRLS8vt8xsjJKxML70HQ/Y+6p6uS/p5TtqeDOp6aFHfPF2oRixoxIUSyqajJPIPJa8wzhU9c5hpsK0IRgrHnBVtVgHb0Z5PAgwE1UygxHyRHVJvMqkskz8knCSSUFJfdu8WdOZZ6ufzudSDJJBKKuqXmyRB58ggkKgPFNB26RACuNs42byS1WPFdDPkZGRdiwMQKEwGToU4Ph4r+qPz9jM/h7sW9JnaewpeIbN7xnRpPZ4ouQC6EigketmvOlxM9aobvOZldEoOmADkJCFmsbCWIRW/M3zH6SUrWWeMTZu/SFLud7k0l5K+jKNQ4ZRGLM0ysZtjVCejJnEt1TaeTYaYE6DenJy0gCxvSIJyBiBLSAcdT9o+LBmLmoBcDKuubq6WrfffnvbE7zinLusBGcuc76sE4+2qur69eutz4CoPW29cg8AU5mLkkVwknWRgwFAaZmcid3Z3t5usV2ynF64fZXH+xh+uk1J0Mw/yUQ9njO5Aw7zFMCwPXDauQ3x9evXWwUoGbw2r7NzFldShU0GTaKhbCj02unpabvTNqvZ+A5qS7ay+DPBZhQoa8pZ3JpiHKRrKHNGg8Lk4SgpSXgpO14wWpwCYzAywSW98oybZaJGZsnakJlAhKITX2Y4lcHj6VGaQEDGF5XY01BwLhHI9+bxivTSqnrVlrrds4pYlG/Gov0safz0YLPiVFKp1g2i9x3gx/isH3lKjzFDFBSQ9TEO6wJo8Hqqqq90KWCRoQPGiLwNJgNhgACQTCxMo47yZfDMYVLMjAklzltMo0y5kttMaNzfP6vIJCxx6dKl9jvzNsjS6KeQjssgNjY22riwVScnZ9n3u7u7jTZlaIGV8fHx2tjYaJ5iVbUsYCEVa4xhITs8czrFs61nVgBjiKt6iZs3btxo8itmb9zmQbU7P+dFAmbyYoB6JxMwdGSZEZZAZq6Xlpaa/hsEXcZrLpIZm56ebpXdyOLBwVkZXmsBeFjP3MvJWpIDOu+TNUb8oQ99qF784hfXn//5n9f09HQ96UlPqu/5nu9p+kB75StfWa961avavtF+93d/ty5fvvyJ7HJrF7r0gaDk2UhGktIVl0rqEqpDg4oNV1XfZspsaVRNJq1QhpnRnF5sUtzr6+stDkrobSQKzBhyk6V3kJmpFg3SFBvSR96w6xCBEt4f1FvVK7jhWenpJZ3IaCV6zTgoEJEGwzpl3FtmN6/ZZ9NDRXnpe2Ymp/dc1UtQGaSqeUcZg9R3hlpCjnkFptCTzqQmi6KfwgnpUYsT8lbJiYzozHbPbNmsIw04+W7S3+bCvKdhNb/WI4tdJOgzBvHYzA9INoERzWIlY2NjrUBJPt88VlUfSzLoMTNeg0k9Yr72lPVIqttaurGKou90elXe5BLknFhvTFRS7CMjvbue7SlGfGtrq3m6GxsbtbS01AdaHFMcGRlpWdLJbADrMoo5AklfC3/Z62LEjLs9iV3If3uOcBY2kK7CHPHSrVWWEj04OGg1FzAgGboz3wsLC1VVDSQIvWxsbLT7tzMkZP4xIPQovZqGmizdrHarDPFzn/vcevjDH15ve9vb6saNG/Wc5zynLl++XN/4jd94r88++clPrh/5kR+5Bb2873ZuQ4wGFdcgtGguiiGTFRz7sEFdv4WORqsxKhImeKSZCZoUY8a5UDmZNJRZnmhLipbySGOTHgajLUNWXylBG9fZ6UyggVi73W6r5ZueYJv00V6x//uKRVGYMknRhzxoBs48GCtFkZ43BUAJogwpVoaFgYKWjY3xqOop64zDVfXiiwxqGoCkyD1jkGo2typhUVAZU045q+rF7JPaJxsZy7VmmXSG6s354ZHy6NPz928eNhlKBW0NBp9548aNNhfmp6r6Ehmtvz2AJeCJkQ1xQOtsLckXWffZzE9w7MmeRcEbA8Ygs+F54rOzs20Mzu1XVV/SoDFYS7kSvOajo6MGshRWmZubq6mpqVpcXOwzRuQRwLaOi4uLfePW18zux/KsrKz0gYPMFbFO1hftbl/SW8bpO5gYYQkySHZyPU5OTvpi3Fn8hUdb1WPBEvjnSQD6DQt3enpWU+D4+LjVAkjAaE6AQfM1OJZBL/HvenvXu95V73nPe+p1r3tdzc3N1dzcXD372c+uN7zhDfdpiB9o7ULJWjxWKDeL0FPgNo4i5em9oSxlJyb6JCAnJyetcIeMTMqZYGV8hpHN/9tobtsRQ1L8AqiwgcTA0lDpy/r6eqOfMlOSlymRJdG2TZiXdeu7d/AqPDfnt6pHy+rH9vZ2bW5u1tjYWF/t5aR+9Y0SB1bQvTwXgAGllexEziNQkHQgDxDNZS0AiqrqmyfKJY2rOBwllRW0lpeX21pkPCuVJ2qTAk0QQjYZ2/w+5VRVbS7Iy9zcXEviquqBjYynZvILYJO35DCAVb3EtjQqGQP3bnMEkFgvIKSqP+aewCSNVILRTBATpkDDZnw06d/B0I5wTn4PY6DYBnqaF48NE//EEmXcmvG2H8RWq848++Xl5drY2GhMR8ZWGV1UNo/emXjevnPLGYdN7zGPD2Iy6CB1AxKMJBu0vb3dbsICeOk6egJDBuyl1wq0qZdwfHzcwnJJHWdy68jI2RGtdDKsVVXvOBzGEoDP+Htew5kFQ25GuxVG/t3vfnc9+MEPbkxCVdXDH/7w+sAHPtBuvcv2V3/1V/XUpz613vve99add95ZL3jBC+pLvuRLPtHdbu3chlicSaUr5QFTuchqREGLm0xMTDThF1OiLChkdBsjnHGl2dnZPpox47a8aIgRUhXLZjQSSefGzAxS37cJbEzeSsaPKAgKZW9vr8VdMzGMMc2EDIaJwbJZCHAi60xAY3gZ2vTWq6op/Ky5m7Wv3eGc8bT0DHgBsskzXo9usuEnJyfb+DI+zMhkaIJhN1ZzkIAn0TslTi4YqapedikjNjU11c7IAnq8CN8FxGRH84yxEvpNeVVVn3xV9Tx580qufc/8k6kMyZAXcpgeGtkyF+kJugwEoDQm/aF4vQMgyrwDRjIVO+qbcZWlT7HLRyCz9gAlPjY21pT5YLEO8eEEFKenZ9XDrl69Wp1Op4VvzKkM4/n5+aqqloPCYxbntP7mVv+TBSB74rMYGUY/Y7L0iHEy4mRwcJ/aC0IiwPD8/Hx7JiBMr/D0MXxCe2SMTHW7Z2VYzWWOkceMuVDb+vT0tFZWVpohVvjD+Ww6JfeiMX+ytbW1tSY/GqO8urraZ4jvuOOO+pRP+ZT6nu/5nrrtttvql3/5l+vbvu3b6i1veUt9+qd/+ie039qFViSFGnWZhm2wfBslC6Wh4Rg+myjpF2fysgYw9J3Zrb6ftKE4JyWeyThV1TwviSHp5VKqsjmT6jK+lZWVPmROCaMyzQ/PMbO8tYwLTk5OtniOMaKxkiZNele/0VBZ2CRjktA25c1gUkT6nAkj6GUxJArJ2qfnTQ7S6FVVqwCVWceUgnPW1p0CYQjJTlJsifZRt4CG56Qnbn55s1oeSWHEjMO85flkSVHkUqYs48yDFG/M+L9nD8aQzX9V7zamDDWYZ+AR6Bj8Q0b0zfoZ72BYx9WDIyMjjckxp5S7MQIICXarqsmmEJQYKSNjvtH2Vb2cBd4fQ3Xp0qW+PdvpdGp2drZmZmZqbW3tXkl0jJwz8YxRgg57kr7hrfod4ADgYrWsIbDhz/7+fgME1tKzrL347+zsbMsXmZycbDeAra2ttUqC1lYCVV7GAgDoT97sRr9V9U6uYBaEwBh9hY44MEl57+7u9tUayFyC+7vdqhhx5qx8rPZ1X/d19XVf93Xt/89+9rPrN3/zN+stb3lLfdd3fddN6t3Hbuc2xBnLyESUpHfvS7Genp5V2aI0Tk5OGqJTQYYXmcYHTSdeC9FKdNne3m5JHD6XFb/QzZn0hYKCQqF1CiRr5aZBTQDAo8ws6fyMGDIazzvNmWfbVDazZAq0ICNEAWRGaSZoGb/55vmYL8AiY6dVvcIcvDnzLWyQz/LHe9KLBBoAjyyYYW7MPwOXXloq46qeJ0CmjI/hYUQyUzgBYVUvoc27eDt5LMjz/Z2enzh/Ag3PYKwo85xLa2eekoqUdIROJVvJCpCLwbrSeaQu8zD8fH5+vsmH/uljzmHKkszsZGvIs3gwoMBLTFYHeCRr/maEPQMA8GwyIfsfUPMdRuvg4OzyEmEI+8/auI40wT/ZYAyxNuRTrBsI5OWTmQwF5JpnCCzZGUY6jX5Vjxmxv32GA+AzdJv+A5+ANZBHv+Ua20/e62ii+Zidna0bN240HYnBzFyKT6a2vLxca2trfT9bW1urTqdTy8vLH/f7D37wg+vq1as3qXcfv53bEPNkxBYYLYiYQJ+cnLS7dfOcHWRnA8rgQ+e6PEIskZKR8OTfrvrjmaTgeg/KnBdFWK9evdqXsbu5udliTpubm02QKXAJDpROGhceRDIE+uj7jJ+xM2A2HKqKgre5BjOpJXc4l81A62uet7VxM9nDu21EsfKMfyWdSbFiPfwsmQQGZlDhMK7mRkIYKt+cJljjASXY85mMn1Jc3ul9ZNN6A0AydTN+aj0S8Hg2I510NBBjzfWFzKendHBw0JKw0uOwPgnEyBdv1LPSw7yvYy5io55D8aKbJfAwPP49PT3d5D37laURGbwsJGNsYrZHR2eXZ2CNyLCEsMEKUpk8ZL+nJ+6zAFxVNVmZnp7u85AVAAJEjCWBIpkYBIBV1S4Aoacy3JXVsOxxn0OT81wzriumnMwe6t4+qDqjR/3OdZpkV2wZC+Pd9tf+/n7TAeTYuldVS8SzfuTHvievwBfDfrParfCI77rrrrr77rtrZWWlGd53vetd9dCHPrTlcWivetWr6lGPelR90Rd9UfvZ+973vnrSk570Ce1ztnNH1SmuqmqFD2zS9MDW1tZqfX29CcTS0lL7e3FxsQkM4XeQP5OTKFreSipUm8tGkr0Mseal1wROIkdmOqbChn6dKczM7PRwMsHk6OioxbH9Qa2vra015ZteL2Uh8cqz8z5aSsMB/E6n0+aN0afwkh5mvDMkAP0y3taPMqLMj4/PMjApcQqNEvI5Bka/UXDm12d4rNZZYozGG0nasar6QIF54SFubGw0o6R/6c1WVYsDyzVA209MTLTLSoCXwcxyawVAAF7i5RKzKEBjPDk56bt/OtkYVZcYcbJH0WZiWFL/+ik8QNkzjJkDoekvAMXQJSPgBh6/A1rJjTnnnTN+AGAaMZ6zP8mS+Jn9Qn4mJydrYWGh7rjjjpqbm2uGIfMwzE+e9UZbj4+PtzP1c3NzfXdx+35eFJPeun6Te962PeHzyazYW9mXqp6hx05kvN3Yq87o/MuXL/eN9fr163XPPff0Vf7DvtnXqorRe6urqy2uvrCw0ADW1tZWS360b+x3OtfRN0xC5gp8MrXP+ZzPqUc84hH14z/+47W1tVXve9/76nWve1097WlPq6qqJz7xifUnf/InVXXmKb/4xS+u97///bW/v1+/8Au/UH/zN39TX/3VX33L+n9uj5gigGIhdlmZFACj4pxfxvqqeolJFCwhTw8tPz862juv6/uyBnmiUCMFRfE4OuKmFt52bsxM1kCvM06np2c3ykDhklh4nIrdZwJUKsPBpA+ImfGnuLLkHy+fN5nHKIwhE0LMh5/xnihcaD29p6r+MpvZgCB9ypbxOIqX4qKIUP6yfHnhPPP0eozB/5OOFqPUf3dem2OfScWfsd1MlGHcMvud0UsPwTqhKZNGZJSwBOk1+655Tc89E2OSCjfPGae3l8g+pkZ/xATzGjx7wR7l0VHGFD6Qh43KTPcEqNZI/xjyLCSDwUlWIkM3Cc4zGTPzKpKFQfPnmjC+2ILx8fFmqDK8wrO0lx2BMy8uVUi2QUzVXGGXrKH1tO8yMSyTBqt6xWFQ/0l320fWRpzWGW5edj7H+9MJ8Zz5+fmamZlpNzetr6/3ldK1RuYj8xYuX77c3k9336x2q2LEP/3TP10/+IM/WF/8xV9cs7Oz9dSnPrWe/vSnV1XVBz7wgUbTf8/3fE9VncWG19bW6qEPfWi9/vWvrzvuuOOW9LvqAoY4jQpqxmLu7++384Xj42eVdCiCqp4HxBDlsRnPo2gysYHSqOrdvMQYpgGyAdJgups0z5byfAk9jymPN6EMT056FyUwCKurqy1JpNvt1u233942vSQWhp+CY4TQgpRSnhe1STPulnEsXr9nZwKZmHnGzjL+iYZm0FDKGRsEmGziNLiDxygyZsYgJ2hCQYpbJT1X1TvalIYLePE7xja968zGlSHqZ96fiXvdbrfPk7RmlByjlWET3n8a9arqK38IfOirdQIKyEteBJJlT4GlzNCmHClS7BCZtm6MkXPsYsn2GIBrXLxZRiSpeEZM7F0GPFAgKz+93zx6RI7NKyPNAFLGxoJ6FQ/2uYzV+j8DwtvjWedngd29vb22J4SWMn+F3rDPPV8iFllOD5WMYE/oCiAAWLYHkqnJkJT+JdjXD3R/AhWyn9Xl8sSHo5yMLyB26dKlBsSTuVQdkNNgntbX1z8pC3rccccd9drXvvY+f/dXf/VX7d8TExP1whe+sF74whd+orr2cduFs6YpfSiYMcy7fpWGZAhsTAYHpZvJBpRYUpOZJUhgFQDJ2ByDUNWjDEdGRlrKOoqPgDPWjlplJqXneh4KOGORjBgaSH94Y0n7pSJJWjeTOzKJJZU8o0Yhmkv0NiNvo6ZiM1bzZpPyYhjdrJCkb4NZwBnv9HN5AAyKd1f1KECKSRzU7xJI5OUhxpY1cskDReNzSTlmol7KQcZheTfGnbHvZFisU3qSVb1YOxnJ+CrAR6k7m+od5C8BDYXpfYOANDOY0yPOJD0G1lwnPZxeHNkU30SXG0969NabAXDmNWUUQE5jq//m2P7FFtERkimVanQJAS/YXuJ9SkTzOXMtZ0FhEiA/gdhgxr0x+BkZcrQSkAWI9YO8DO6zqmqsmO/wVIUUMBo8YXuCJz89Pd3en+EvMud34tP2MZn2WVnXOX6609gk9pGXYXvgtAt7xBAp9EsBEA4bYDBjdGVlpSYmJlrcjEFAdWUSj+8eHh62mFAeJXFmMlEyD4rQqdUK9TvkrgAH5cRDzmQPiiRpVxuLJ5ZUEKWpn0lrpeLL7FGKmBdWVQ1AjI6eXVoPPUvUyivrqnq0qCshjSk3dNK9yUIwaOKZaD+UMIPJACStTDFmiUDzPmgsGBfGlSG01qnQrePgWV6gJ38+Nzd3r5AFA6SPmRSUSji9uIx9Z8w8E7woPQrUzwEdwMf4rT/jOz4+3jxMl5cYW8ZVzQ8jkWfIedH2RyZ1JUDwXUbZO9IQDYIA4yIfClTYZ+TOM9PrIk/2X3p2CVgHmRceHaPP2EtWrKoWA1aRL/Mu8ghPJlFmEhfj7B2Hh4eNrSIbVb2CGMfHx7W+vt5HbWdhHnvOZ803IJm5AmTAzXRXr16tzc3NdpRT/kEWK0kwaJ9k8ZKMUx8cHNTCwkILM1y6dKl58OaQnOSNYqkPbla7VR7x3+V2oYIe6Dxx4LGxsYZU5+fn2+H7pCMhVpl/Nnen02leXcayMrYB3VHkDBUhT1pHJSKeUWbg2ng8dWjbhmFgGOOkhTPBgQLvdns1iY2X1zOY1VvVyzje2tpqCDevOazq0boUpeczLhQJg8BTSk+KgsqM1wRKqPDDw7NbtCgj/Ts9PW23a1E4GTszH3nOWRIQYERRpBcOqFiPzc3NRp9SqACJz5k3R1CSOqcEGbgMm/i3ak2Ue1V/cpS+AZAUfMY+88wsRby1tdWMWBpMxoOMGndVtfAAsKa5S3ZpaanJDhmUpJTy6J0SIdOYqzJmDtHozs4nmNQ3Y1MOcmdnp65evVoLCwttXgCgTBCUJOn/d999d1X1LhJJhiPzFoBpn5mbm6uHPOQh9eEPf7itW7JhAID9KkFpfHy8rl271ld33N7IDHx5GgsLC7W8vNzkqqp3DIwsWUfU+ezsbC0vL7cxmVcyleelMxFOKC3r4m9sbNTq6mpjJHxfElz2Ie8iFsLiuCQYJu8zMzPt/HCyjnJhrDXAZk4yd2HYbn0792pAwBmHkKwlW9XiopIziQhKswl5ohQJVEhIvGPQg/OZjOOksnIueWRkpBWTPzo6ulfsmSJDOWdSiDHqFwMiXpSb2ObN84+MXyb4MAJQPCWYtGWCkEFKWzzQPGdyETo5KTKb21ym58kTrapGx6XizDhxfk9/fM45UeueXgZFIos9aUVzZ2yp5CibwVAD45Seb8aGyZ3583cyHb5LHtJDzJhrxs8zOQgASoAD/DiTbg702fsHk9DEZk9OThpAq6p7KUnzghmxrtYFO0XJVlWfPKaBIq8AFjbH3AID5nnQ+Hvv9vZ2y9UwhpRFf+dZa/sw96u9dPvtt7e585wEjRgUnrV9Yh0zYUrfrJnnJLuS+9D/OQryUNIDNgfkiJwBToeHZ+f1V1ZW2poADDL+sx/GSUflmMhLhp4AR3uUbGd2vd9lSVF6bGpqqtbW1voyxG+m1zr0iC/ezm2IH/SgBzWFkkUbkoJJWsXGrurF4dKDQBNloQleWWZy8s5sBpmRGXuamJhoG5CCz2xmRsDfqE1KwsalvHj86SUlRZnKRrZ2xlb9XlIMD0t/KHQeCwM6SE9V9cpWpqJDr6I4MQE+Rzmm4s6EEgrFfOU6AhBV1fpUVW18SaNX9Uoo8g7QocaUVZ5QkNYy6bNE+umt6is5MWbvYjCrqnkc6ZFRboOZr5gUv8+8gIzdm/MEoubHfDCuR0f9lwykzGeopupMWTnfaA4GwYXvk708Hw/Mjo31jq6RldXV1bp06VLbL6piSe7JohvmJmUymZ+M12O6rKW5w/Iw3CnDPNZcW+Dq5OSk79wugCIZjccr10OugSNl6+vrzSB3u/2XXgBI9ry8llw7cyjEIcEQOAZqs++5H+0dRxcTVK6urvaFYNTlTqp5bm6u77l0XjJc6Q17ftUZ2Jqbm+urs0A3AAkqkQmJWLs8yjhsD4x2oduXqvrveFWpJj1icTObOw+SE37e2MnJSYuXqJPKIMo4rOpdoUehUcpimhQ7g2Mz83AZGkKov2kIUaAE3iZUEaeq7iXA6G4ZmBQcQJE0tr4x1owyT2Z9fb3vuIZ3U5yZlJUx8sHs64w1Mp5JUwIXqfh4N2nsGAZGzXwwmPpY1avK1e12+0qEko3Z2dl2jETfGY/0IK1vJk+lhw+MUDxkJ5WvPvobUACiGJ9MiBukcM1hxtsHs4H9XJYuTzNjcfcVC9V/ST4oWIYMpUnZJzhIo89AOpqnhKZQ0ebmZssrcDrA99KQb21tNbmYnZ2t6enpBkQB76OjszPzSZPOzc01Rsv53sE4d3rrxpc5IHSEOGlVz9iSR3sGsOW1T0xM9F2AkPQr+QVwjo6OanNzs53scBTOPFtXFQDJM5kdDM9Yf8ZTroR3Of/rPYwjGcwz0FmvHXgzXrc8TU9P1/r6el9oLj38PD0yOPc7Ozu1vr7e5H1vb6+uX79+U6npoUd88Xbu1WCMJicna2ZmpiVR5KZOapBRhgQ7nU67JFsyRN6qRJlB8M7NocAZbZtM3C2pYRV/eHZJd/l3xgeTUs0knUzooTAysUpfq3pHZpI69G8xH6gckk+vxsalfLyPccx4nQ04eH7S5kyPmafjj/XzDPMw6K1A38ZPcalf6yINsSZxVOPwDj+3PuZa7IoholyT/jYuc+jnjCQjp2UcHAhKT43yzJyADH+Y+/x5hg4yNsj46JP3W9MEFhR1UtSMelLZsu9TOeb5e2tkL6XH5DPWy9glJ+aaJlil/I2T93hyctIHBhgDgCFj+J1O72YkHrI5NH7vA4AYZfIKQJEX4yOz5FKhC9n629vbfe/XGPBMLpUvwPOVQImRYyjtV4aVMWN06SL72LynHgDWAaU8R2+eqqrvAhJH3TLHQa6GG9fm5ub6jkMy8ECDPShXR7zafAi9pCwP2wOnndsQ55lfxpghVI2HV8AIo43FXSYmJtq1go4P2SjpwSWN43eUu9uZqnrx2Exg4lVQSlX9sUEKSV8p4owDUhAUHgWSyjQ9sxTujGGmEhKXOzo6apQbBefsL9qbsjO/+QwbiIKQKemdniNhg7LmJeZzk7pOT1d1McoHtSdhDB23sLDQB2aS1hykeTOxxRzo26AxSFlIJZaerPX27vRUGDjzSybTIGhpvHIc5okRzKx8/wc8KV3UMxnKfAEGByADQBxfkXHvGRSpuuWMIPnliWvO+GJ6jDer1pFbspQgzpqlgcGW8KiwUJKMBuOmALX5ThBiDhjoLIWZ7BDdkefI9VmoR7lHYKPT6bQ7etHV6geQ10yetG76DsBlnNY82KeSoBi0DEE438vAAzzJIKRcCgHQVyrxAR6ZHc44p1OhjxhBLJ/n6Zd3ZLgKQ3UzDfHQI754O7chtoiSC9LjScqLMPu31Pmpqam+TFbf56n5vExFGZ4TExN148aNpoQGKVm0IgEUdxPfQgNW9byM0dHRvoPyELm+QbTqWhsP+o/yzmQqfavqxd4yHjiYaMYo5vONmaKamZnpq9mdxzQoJUbDRqOw/D+9vVSSvH8KtKpa/yg+MXDrRYmK7QJYnpUUeWaVm4Oq/tgc7zPjrgluyBwjkEaeIfBOa5FGldeZBjJDHMZobVGKZEk8O9+bHuUgTZ1x4IzxG8v09HST96reOWuFHRi2pMpPTs5i6ZgVc6RP8hfsofHx8bpy5UpT1Mo4ks2UR0Y9+yiWmolC6FexZqCawTPngzLmTLBn2wNpWD1ffxi7fJZ5ts94rWNjYzUzM9OSDat6me4Zox4ZGWlHhzqdTt15551NDwCV+pcGyrlnFD6PlNGV0KWcqXkxJuV7Nfs+gVV6tycnJ7W6utr0ZTIfHAbMw+HhYbvZzViqqi+fZWpqqq5fv15V1cB1xthvVhsa4ou3cxviiYmJWlhYqIWFhT4jUFVN2NIAp9eVHm96jc7v2rhzc3M1OzvbNpINq1i7z1LyNiA0CIUCABJcjo6O2rETyp1Hnt6AvzNxJ4/WMCp5V25S1L5jg7pEHO1oo6PcNzc3m4HLS8wp/K2trb5YFJDBQBJ4fUjFnrFsRsvzkwLNakGUtvGiCK2d9R6MBSb1i5HIzFVghZertGnSh1XVp0DJTjIPlGTWRzYuCiv7wrABXNnEEil+ys5zsRPkYTAGl55N/knGgkfIazk8PGzjHmRbMBjm3rolk2COKFqybg+aC7H/7e3tvqNTGW+lsBVk8Zyk/q1h5iowImLC1igZnTREKXvmJoFD6o1cGzJjnKk7rCdWbmpqqoFDRsp6ZpKdUEPS40AI9g5IBW4VDcn8j/39/QacePMLCwsNwOiL/ar/4upoZkCKY6ACm/1prZLNEhbKOaWLFFVKR4GelU0tVj80lg+sdm5DfOnSparqUX+820wAQr8yIIQaPWPjUAKdTu8GIfeFUlw8IW3w3C/vIxWjuCMjJcbF6MnazUpbqDDJXRlTyyxhf6NpB70xXmtW86HE8yhSsgFV1UdrMkyO0ZgD3lQiZJu2qto8MhZ58XgqU0Y9PWebm+esUhgjPBhntPkpGTGvpD7TkPuZZ1CwVb3ayL6TtFuOg/eaMcQ0JOJyPLCq6lsfbEfGVY1XfXR0J5k1vgRm+uXn/p/rXdU7qpMKPyn4XAdGQ56DeU9jbC+RswR9ZMRRvZWVldrc3KyFhYW+qnYJJnzf+vCcjQVzpD739vZ2n+GydpgQYYWk/OkJ+xBDZX7y+9bC3NsrDIZnmD/H+JKK7XR6x7dGRkb6Qj7mAAgWAmBQAWf9Byjsrzy1gG2SvHp8fFZDIYG8cfOS01m4dOlSy5WxDwG4jJvTAWQFSLbeLoVwJp/+Ozg4qNtvv72xiZcuXepLkiPzxnYz2tDIX7yd2xBT9Fk8nrGYnZ1tBk+GZVKHp6dnl0BAZZkhqHbz8fFxKwEnK5PQpBdgIxDWbrdbCwsLjVqUqABVSjbJ7M7FxcXqds8yfJ1tzZgZj7fb7dbc3FwfRcjbE9tiqNCsH42mTKOpKEpmBOeZS7SejNc0YEl1MWiphAZjoBlbrKo+ZSiGbE0HY8bWisL0fHPJO9DnpLyTZdAwFZSqMafHbFx5ZAObIPkqk+pk0KYSNKeeBxRQfgmS0KY+x4uT5Wztcw2NdxCk+QxvksdDPvLz1vro6KhWVlb6Cuakd2iu01jYO9iizELPDHR7NungpHnJp8RABjtBYdKvvG3JXEAEo52ApNvttvi2/bC5uVmdTqcvY1mISRhndna2jyEZZGH0CXVrTObJux1pVC/e2noeFunk5KSB1ASgCZ5HR0fbueA8Wy3/JSuPmRvrmfWmJZwlu2KNTk7Oqudlzfn0gnnbvHZj7nQ6jVnjaJDtDJ2J9Q8mOg7bA6Od2xAr7SieYeOJl7mU+fDw7E7U69evN++JB0mYxF08S6yYYqUInY3NmBgllvE+mwZylGlMaQ0W7xD7g3yh8zSWlJMNVNXLjNQHRiiVD7TNqC8tLbXn6gNKmxEAHMbGxhrFmMxDekG+5/2UH/SfZ6m1jFdXVVMovO+Mq4rpA0AyVfUVE2IOM7TgmeLuxqXfgJs+AC2Z8AVU+Lnx8qpPT0/76o2nIs5MXQqS4a7qPxKUFHC32+0DV91ut1U+I9+MCiVmXChMio8BAzZ58XkmWF/N3dLSUt24caMZKglXxsb4JdVqPfRhbGyshTymp6dbf8xrshXWxvM9E3AwZmPLbGp0qUx4Y8a8DHqFOUf+vbKy0uQF4EmWzHNR7PYMapsBNO7cw97JQXB2+vDwsBYXF/u8buBA3oufC1mkfsNWABj2pCOHVdWOPx0dHbWrYIEBzM+1a9daHD69dSwBdgRARmNvbGxU1VllN3tU7P7k5KSuXLlS8/PzfWAeI5hsTR7zulltaOgv3i50mOzk5KSvrFwql5OTk1pZWamqagos45KU/87OThPERPDQbBrck5OTRvtU9ZIdIMOlpaUmuKheG5sxZZjyzHFSnahxiis3R1V/2UstL7uvqr7N7x36lNmkVf3HHChr1GlmqwIQmQSXRhiDYFyJ+judTlPGWWhg8Dxy0pK5xjb47OxsXb16tR1d4aEKMwAovpe0dBotc5M0PY/C3DAawhPmIT19StV4rGN630AOz2VQfjNxiLxk0mGuvbg571afGa9BarGq/7ahTKQaGxvru8M7maGJiYlWFx1oYvgSJCUTldSuPlhP/+fxonOTFue5iZUnSOCZpwFlvDMZKwHj0VHv3G/uMcyLtSE/+eyMlQOec3NzDQxkYZxkNvTBeHnJKWenp6fNiRDHJV/eSd6zyljGuBlgesD+sc4rKyt9OQrb29u1sbHR9igA6pkYQ0lkebED1s+70zsHQP2frgX+EgzlvgL67OHt7e2mc4btgdHObYgl2EBqSftB1EmzZmIQ7wkNNrjpKT7CTTkmzZt03fHxcbuQnkFHs3p2UnQ2j8842qD/+cemScG2uT2HsXGGj3JPBZieIfo06WEKLZNAAAznpfXJxmMQgCGbfmRkpB0F0ydUYnqtAFTSa2mUr1271qfIq3pJQDz2tbW12tvba0eX0thSquaVEsjYblKMg8g844VJL1tTnkcmcmEt8v3GlAlamQ1LhlDP2sLCQl8/AMQ0qpkckwo5Ke+khTOmytMe/L5x+ny3273X9ZXox0GqVjiGJ5Q5CJubm81D1nJdvNMZcexPUvw84Lxi0GfFWZP6Jwv2DyaEHFovCUpJbTOa8g6soWdgGPIu3aSPs2rYxsZG3bhxo9bX1xvQRBHTCwC7+Or+/n5f/kfGrtHpZI9XTFZyr6hiBcgJaVnvpMXtJfpI6C1PSpjP7PtgEprQXNZeAA4SRGUuzM1qQ4/44u3chtgmyqM+FN/q6mptbm7WjRs3mnFhQObm5hrNY+MzEhpBTgqKx0p4E7kSyKpeolJV76LwLDKQ9CAqCZpkGFZWVvoSqCiWPL+XXsmgUmfc80iVJDVKmmG2oVCDWTSBB8OgUiybm5vtYP/CwkLrC4PlvPXs7Gw74iCuqG951Zvv2oxQNpZDvB84oByBAH0zpmQSknXwTICC0s4MZRT7oIcy6ClTStaIgkwjn6Awixc4ew5U8QKtNe9zMLSQ8WF9oVQTvKEJGXVGJz1QsioJjJwY88TERMvwHzQuk5OT7Shdeu28SjeKWQ9zBCgx6BmbXFtba8/IPALskHAEDytrnCe1b8xZTc5appHibWI5Eszz5FMGXRbjGF/KjTH4GdnwvMPDszPYCg5lYZetra2WyMRAMcpJzScLBVQki3F0dNTqN/s8AJyhECAkWb/19fU6PT3LmxkMa5F1GfN+PgjqOBuLi4stcUsCKkp8dHS0VUSzLtY8Aeiw3fp2bkOcVZOqzoyY4H8mEaHeksphkDIdn+AyzlX9Bf/T2FKI6fFKSPAz/2f0IdukalBFqSxkU1PsFLFNN0j1MDxVvVrHeUSDsvZZz8wSgDakZyT9nONjlMy/M64MLMPOu+Q1eH+2ZAUyFpdJKsfHxy0WlZnj5oSXCkikwk8KUp8ADQoESqc0zF16c+SCwUnFl7/PGGRWgkqQksaMwUylnbFpXoSjMAwCA824JmgkU4BPynXGZZNWNedJ3xqfPiYbkIUvUgEDEtYuWQxMDppTfNl8KmGZMVf9BqL1A+tEJnwXCEz5wZplDkd6w7kvxJjJu9yNDGnpY663f2eCVBqVg4ODVn3LXO/t7bUylejzpNwnJydreXm56QIggQzLD0BxY+DIsnnPJCuMHZ3JScgwHaNID8j2zsS5PI0wNjZWi4uLTT46nU4tLi62eSH3/q8P1ikLHdmTN6MNPeKLtwslayk0LhvQppbcYFMdHh7W1NRUW3ibixFI9E2Ik1JMA2hRGQJ04urqaq2urjZvgleVFYr87Xk2TtLfKfjewcOB+MVzqnol9GzU0dHRprwpfkqYx5ZnQSmOVLaMFyqJ12ljUoyodUkhPAhKmPJJI+9vyl/iR164gerWHzfRTE1NtVideDQjy1vxb4AIgFFMgwGiQDMWZ07F6Y1x0Kuuqka75blucpbMBW+XpywhDZDxXfM2SKevra31ZRoz/ta9qv+8vH/7DkCUnkcCiQSqGV/VD96PozXkxPeTHQI8Gf4Ek4Ao0KQfjJH+p9GUQ4Aarep592Rzc3OzJicnm8dV1aOpfZ4s8cTR/Gl4yUxVz4DxTgE7RhIA0g/Jl+Z7EPTrP3kRGsoEtsnJyVpYWGggDuDc2tqqnZ2dxgCsr6/3AYNMErSeVb3a6+vr67WystKKeZjPnZ2dFhIjW9aLjKaOoy+EC9JpmZmZqatXr9bMzExbZ7qN7khglgl+yTDerDY0xBdvFzq+RCkwXjxPi09Y5ufn+yhgmzGPCRHEpOL8ScQ5eFQC0oO2q6qPKqXIkpar6nlIFLR4VGYwEnxK3vckV9hIEkqMX/yW90UBJR1EOZuj3HgUI4NxdHTUgAzUfnR01GJJm5ubtbu72444MWLpORgrRZbX5nU6nVY/Gqja29urtbW1vqIemTilNq75ZWR4hkIEjKLPAAcZ0khllHFr3i3DRHFUVYtlAl5pINJDYGDF4hLI+XzG2LwDu5PGltfPYIrv5hEjxgiFmXFvgKOqWrw0KVdj0N+sGHXlypX2jkH6k3xZHxdtGJO9lpeNeAcF7Vl5HLGq2ucSHCRTkzHyjGsnY5ExUODLnPnDa01mwX4EfIFFxjdrJ2Ng9BlQAMgB4OPj47ZHMtSV4E2/p6am6kEPelDziq33+vp6395MkHdyclJLS0u1vb3dV5RFbow+o7Kt5+HhYS0tLTXZtHbkJVkz82xuEkQOPtdVjMnOdTqdFn6gf6zRsD0w2rkNcSrg09PT5uUxCuJBibYoKcYgz1byghIZJtXMe+ZhHB4e9t3sk1cx3nPPPX3JXUklQ8+ZnMDbzfPCg9SzbEeewc7OTm1vb7cqXwwzKo8yoyB5G0mlUrpV/VfsJbWYxinjk5mkQklC4N1ut8WKgAOomLJLbySpd7VzeaqZ/AP0zM/Pt2NMlJw/joRkzJWMUFYMLyWFucj4FwOZnmRm26ZSSmRfVU0WGMGMeTJ6qHjzjHodpLCz9Ol9xdHIdOYTZGjB76wPuWTQrEOGFrwnM2fNGzBhj/h31Zn3KVOY3DIYyjMClenF87Lsa+uTRjfH4zkoeDKmD+YBILKvyTqdkPthdHS0Zfw6L69ZR6A4E8T0mTFhwHm0ua5VvRrndBDAMAiM7Sn7WWPwxMzFm60rhyOZJcU+JFAZC4Bj/ni3ycrMzMw0b5zTox8cgTw3T47oMPLgWtgct3U6Pj4LK96sNvSIL97ObYgTqSVdqHSkODDvLy+szmM3Vb2YXNInqMq8go3x2traarVel5aW+pRExoU9J71i3ipEfXJy0mgjCprgoMaSwqQYCTQFQLnoN4Vc1TMMVdVn6LOijc3H0PIoIdj0XAZjpJQpivL09LQ2NjYaEs+xUAAZ49Rvmxntx0s3HxT70dHZrS76PT8/35e8IgbOA+ThMLjmgFftqJDP5BpkMs5g83PeC0/7vuY0vbykUM1Drg258DxGEFVrjilU4/OOQU9TCCHHl3LOcOqT7yQ74D3ACUAh1mh9BlmeDAOYD0yBeWLo9c18pUdpfpIOBwzTOCTLkFnqSVerj8x4JdiUjJVhgAwzJMtRVY1Vq+qxFvQE4JfraqwAtH4nlZseZGadZx2Dzc3NNk8ZMkpGLCuZYbjIUOZyTE9P19LSUh8Fn8yP9bdWeTyJnjEnrhhl3Onbo6OjvlMfwAZdcjMN8bBdvJ3bEGdMhMFU61kxeEkNhMlGSmXjeI7SeYQuE6uyLiqUXXUmlG5XYegzS5QBTY8P2oYyeQw2cVbwgiQdTUBP8d5lRGZWLwWX9LY5ouBlfWZMGHKnHHwW2s3KW9CtAgWM5uzsbKvjnTHlvb29drjfOKuqeczmKL0s57KtCWNjTa0zhZUAQWgiY8UULo+EUfB+mdk8UXKSACypaQlqaVQ8k2E3dp4GQOgZVdXendSuv5O9GB3tVZEzNorMfsjypAAEwMXj1xJUGS8Pyb4gV+Rj8PsSn7yTkTaPVdWuAkWjoqjHx8fb/c33xcboO7bA8SdUvZCLfcYrNedjY2Ntrhn0wRwF7IP5FcNNby0TAvf29lpOSgIRSVMMW4ZXBhP2sGLp7UrKYzCBMPSvO5nJvlAUw6mP1sTeZwhTH2QoD9hKPUBeq3p1BPR5ZGSkJaUpAUxPJVh0HzodK8nQfqaT6NUMmdyMNvSIL97ObYgJ4ebmZh9ChHSTWrG5Cb7YF+Wc52rTs8xEF/+neFC1UHhmBp+e9ur4UjyuSIMms1A6xYr6yphmJkp0Op3mJSa1iEZL5Z1JPZSu/mdSSdLdmWCTMSkJGFW97GXUP4NuThgkc0cJUBaUex4jQsN1u73CI5T14uJiUx7mkhJKGjXj3z7ndwBEAix/9Ccz6MX/UXqUXXrG+QwKtqqXic0wUjjmJz0w7AAjYu40bElVNXYhPew0cLmuedUdgEWJGmd6lMY3GAbQT+9Mo5/Z30md2puOqKFyGTKfBbjsv6Q+MyRgHtKIMiLGMOhlJ82dxh0zYP+R++wfBs1+Mt6UH3JsjlP+MnaciVRoXP/PRDlH+jqdTrvBiD7zeXFlawIsWDdM2WDi2OLiYrvAwT7mWSfIMX/AjXHTp8mMMO6OFmatg06n0+jtbrdbq6urfXoqQa3+5PoO2wOjndsQr6+vtw2fcUACRbihPAtNyVf14qJV1XfpN2F0cbeNymsZHx+vjY2NtnEzUYgxm52dbTFbnyGIPu/d4+PjjbZRus5GrqqWuUgZUNiSSCgMZ355qObDO7NylM0hLmuT+1kq6aSYMw6EPqRkKAtggeec3iLlWtXz3Cg0a5gxPJ7nyclJi9/xYhnuNBp5VCbj0P4GJDL+mJmmFDEgYezpVTPWQBqARLkKkWQyj0S34+PjWlpa6qNjeT+YAhm1CVDIufwEHlgqxvSkGTT/3t/fb/RjVb9XSHYyJu9IDyMEYOT+AMJS+aL/yQ1ZyNMHZFgM07NyXRnZqmqAZjB8kZfZ22PGRYay0I71T4PKiBweHrbjSuRHv6y5/Z15I7kf7bWM9TPCOT/m0MUePmdtrCuwQ89l4l7e+JWhAg6Az/o/RsapksytmZubayydedzc3GzzpD/psQMiuZ5k2hwkaKI7B5Oycv/drDb0iC/ezm2I19bWamJioiXvyALNzVbVKxkI2ftceoeUr/gi5YHyQu9l2T+eDkNy6dKl9s6sejM/P18LCwt9QuiOz6zBmxs1PfukDhnTqp5Xgh7LDVJVfcJvY6bXDfUfHh72XSaQiFUcqNPptDtSx8fH+xAv5UURZLINQ8MoZqaoPm5vb7c4vDXa29urmZmZVnBf0YnBzNtMGoOwbTpeAkXCM2Rc/S6ZAorU39vb2zU3N9c8Wh60uQIq0iOWNbqystIyn++8884+T9b4E5CZq0GmwbwxPNZ9MI6YMpIZrqmYt7a2+jxRCT0oQj+TpVvVixmLX9pTFD1Ax4hm6AfNKWTkmWlAGDRrxwCai8H4OWYCCCXvWCTjJt/GLE9CImDmQgyuC88vc0n83hyhgq0ho44STrpVP4FJQJBM5rzYAwB86oSMKQPl2CbHn4BhBk44DNhGKWceQDIlKH1HMBcXF9uaMKrOQZM180A2UPLmCDMCzJnHzc3NVvUrq5MN261v5zbEFj6ptsxqhMB5EBlnS08jvdmq6qvgg4JCKxNahnl7e7sZOmdIp6enG/WTKLGqV3mqqkd38uIyuYQCktSRCgKVmolVeUhefDSNUGZVopSyshAPTix4YmKiL5aZyUFVvbAAryxpRJQyJUDhZCwYas6kkKre3ao2LzraWWVzA4Ck4UTLAVcZA/c9XoTPu7oxDQhlzbDxWqp64MfcKkGJTk9qlfHLWPHc3Fxf0lBe0mEsGdMDBMm1z6YsmQfKLVkBnj2P0HsZeAxDnp81FoBFXw4ODlod4gRNuZ4ZI2bcsm60PceYYmFQ6YxIGtSckwwNAR/p0QIt2CCABnt0dHR2PaOwkPWWr2GOEvAkC5XhIHJTVS3UlWGTpGwHa3TnHLgG9fj4uIWCMFb0m2eSaTS0OXTjVQILgFGfJF5a/8Ewiz7kaRRXNBpvnjKgn9Kpse4ZKlHww3x4tz7pTzJl93cbesQXb+c2xJIHIDCCa1FtJDQMFL6xsdE2rgXqdrsNkefz0IpVvdq8hI8gDlJLNoRkEvRuJgZVVfMyxNKgWHEbtwplfG6QBvJejXFTfjKNu3FSzuPj47W6utqyFSn2TNzIq+54+zIv3RCTHhAvlOFN9sHYsmoSoDMxMdGy3SnUzOw09/pnzBSTdcm5hcatj/6JiafiYQgZTYpbbI1na52qqnkxAIl1znOwAJY5oMita1LifmauM3HNZ8lwAkcy4LvmIilSv0+WKPvICyfzOWdVPXYBA+SWMnOfR6KMJ8ease28JSvpcbJrPfb39/uASoKrPBrl+elxGYvvNuUS9CoGZXJyshneZLoSeDL+JycnTX7JpHlnXNDXjJlY9sHBQd/9zr6H4fIMcwBIeI//555PfWCtNzc3+84Oc0oG9RR9Z62sc4a3Rkd7158m65P16DPD37t46e7VJkuZh0PmEgDdrDY0xBdv5zbElCkUSdENJh/Y1BRHov/V1dVmUCjtTqd3bCdjiBnrPT09O683NzfXBC3Ltfk76c+qniGcm5urbrfbbkRBOafHog8STJKSTEWqz3mkArXIOPgcrzCVNNCQ9wpTCBlP9idLY2ZBeBvXGKuqoXEG1BpVVd+xFEra2s3OzjZvE+Ln6ZjTrKGbSW4JfJIR0P+kpdO75fUxOrOzs3V8fNwulcikN4qDXGXckLJktHnWvPVUojnfGefkTXq2THRyT47TK2VUPS+zhK1LxnQZHvMNkPmZfqQXam2BxIxT+64/ci7sU/NQ1YuByqhFrZo/MsyDspaAnXUHitKIos/NAcN2dHTUThvYc0JawAiAnSDJnOt7eup0gWfmHsOIMLDit9PT040KJns848xeZ7zt/cHENWsra5ts5XWp5pMsYauqqu9qVnT96enZ0UzHK615hmHM/cnJSTuXvLW11U6e6Cs2grFmoMmSMQOKN9MjHraLtwsdX3I5Nu+AwCXipnR4eARLJm2eQ04KNOlbQm7zVFUzwKkwxUwodUrUpkBf2bAUUhpOAglETE1N1cLCQlNM4+PjLT4DnWfGbyrsnCuIWjF885IbJO8apih5KhTO4uJiX/wXSMniKN5BqZqL9MjSmKPvEmSgcRmGBDkZs+M1UcTmgYLzu263W8vLy005icM5qpOemZKdwI/PQfFAk/Glh0SeKDkeI+RPFngplDnD5RnpnS8vL9/rqJDyrgwDWUhAYk4ZBzJjzYVUzGfmErQNGdmw+/v7rViOJDmhA/NuvXNN5A3Yd5mzkZ444GlOvF/ClTG7bN54yU2n02leO3lJ4yiHwZjs+fRshQRSRhhz7/FZQITcAKO53knF2ov2ORnL/vpbvyYmJlq5XnolAWGeJAB8rA09QGfJiMceWbcEaEdHR+2OYs8Vj/csR5Cs38jISKvbsLu7W9euXWs5MYeHh63wDpBMf+mzdbxZbegRX7yd2xATtlR8FhatyPtLqi5L2xF6RofA23yUo+Srqt6Vh2KHmUgk6UVM2bvVxabkM56d/RjcrPoqbs0YjI2d3Y+a5xFd4s4A8WrNjY2fHhSPAT1nY4ob2sw2YtJamXxUVe3fvBtepBBCxpczAzi9+MwM9fs895vZrpQ+RZTeb1V/ta/0AhmjBCE8TGswOjraruxL+pyyTfaBEjNG888zoaDSy7IWPDXe/WA8mCElJ2Tc2DMTGIgio0lRp+eYsfT0QnjxAELOXZ7DTYXMK0qPmyeXdHVmn1P2Ys6DWbRkP0M8ZMEphYyrAmJAsGN35MTnKH9xZH3OsJE9Y/w8vqOjswIyGKDMPdBPckfGeaV+Z10T4GW4Ql+TuchrMYHcDMEABIuLi20PYSzEw3ngQiY5Zn3RL/oiM7LHxsaal+wu9qw5sL+/32doJyYm2m1O9hxmiCPhNEWyYwn+hu3WtwudI84MXZuPIrERqqpdAkHx2hyENoULVZNeW1X/BQmrq6u1vb3darPu7++3LG5eqfOEp6endePGjVbiL7MX04BU9ZJ2EqGPjIy05/ASGaeqnteRnivkTIlm0lMi5FS409PTdenSpT5aWQKXI0Oel4oFHSr2l95+Gr6M8WEHjB14YgT1zTMy5icUQelW9c47ZiyPAh2k9XzePPI40uuw3mQg8wNmZ2eb98946uf29va9FHQaLesDgGT4g9HNGHAmvfFUk+moqrYmFGt61Gnws5Y4Q5YerL3k+Wp/81asU4Yg7Ls0OD7DsJi/3JNCC5m8yGPPs6bpMes7r9H+EOJgaIV9kpoH4gABeiABpoQrcWfP5w0DrNY2Qz7WKcMO5oFhr6rGUjBiQLU9Yi4kgfoO2cuKfYwm2Rq8mtEY1YNPal1/zCkmAmgDJHI8ZDcdhKOjo1pdXW3yao663W5zPuz3DHHYK8bCwbhZbegRX7yd2xCrQDU6OtqyBm0uRjBrm1b1sgMJH1QuJljV8wwyPrq2ttYqykCdEnkoWskfFAhkvr+/3yoCOVuc3rBNm7cl+T3DNjs72zxNMVNKQRzpvrKpbXz99R7KdG5urm1Gytwz/J+xYkwphqpeFncqFMqTRzSoPBl6aBkiR5tlPDsvfDAeZzcpdEk76ZmnkqR400j7Ga9nZ2enVlZWWgUwXlR6hSMjI+14iGdlXWVKzDowDGQNMzJI/1qrzGrPeKWxJx2v8cwobR4iD5AhBJ7Sm8qQDcqVwTM/MzMz7eiRzzPa5q+q2nMpY8/EcngPAJIeK8A1GF4xDz4nTyCPr6m/Tu7I2/z8/P+PvT+PsWy9ysP/dU5VdVfXXNV3sq8dI0AEYTsCjCIS4wShJDIIEAEkCCgSQ8IYQ4gzCLABGydkwJHiQEQgxBBFSGFIiEgIJMYSJFL+AEUBQ2IEkRXAwB26hnNOVXcNp873j/p93v3sfS/QdXHjm/vrLbW6u+qcvd9hDc961nrXbkfgsoo92SLUsfUUpXNA8tG5L3TVPehSgjbj8/IZnxWRJgOWQFEulbxUVdMLn0u2Yjy+qvYGII1/Mpk02V9ZWWlBCNDvs1JRVdWCErIEfF1cXNSdO3dqbW2ttre36+joqDlqzCDgk87dWqke9zv6Rh+A2GTfHl4vjutaOWKVf8vLy633MMGX4+D4GFcKwxGikv0fYmVIUtEgzjyb61lQvJwI1M9xZFFYVRedpfGC/hklCiGvx5lnFJa5ME6B8fOcpJUzt+XzaPyTk5PeO1jn83mjpZLGTJqxqnvRgIg287TWz5/s0sW5DnNqnG9WG/useVT13/Gbhs16JPXPCANJVR1T4o8uQI8++miNx+NG1Q+7ImW0AnjZQ+DQ2KqqsRE5rhx/rofxMnSa+qfcZj4NO4AeBLay9aAOYb6XlG7eT3SZrI4I3mcwLBwDejKBWkZmjiUBatYjj4hx9MaSDEJV96Y1OszBkxMgTvSfzAFZUQHM4Ce9zBGRUetIVulKyhL61t4Z+2QyaS1p7XGuVzJgZMJ+ZRONYb6dPAIGqWvYC5H9/v5+A18HBwfNfmiAkuCL3Obb0OxbgsW0Mxitqiswbi98BrCaTCZt7iLeTO+oj7G2D+p6GBFf/7pvRzyZTHrVhxl1if5c6+vrDT0TtqpqtBJairFTcEQJRIYQNOdOKfKgut7IaXj025WTRXdlpJfKmUZJjpVxUtgkgs68rYgp6dRhtWoW/KQz92wKBWAwtL7v+Zyf5yeNi/aEdLOoRqRhTNZJiiEBDWNsrNmlK2nDodNlUI3x5OSkgQlgSBOBk5OT1m2IgVG0t1gsWg40jyXlkZGM7Kq6HDhZ8bykoRlJ+zwsFiJHmXdMuc08pjH4uZx5dn0DMBMI5D4zyAwmmTO2qurpWLZzVZWbzID9yry5vTaXlZWVBmCBx/w8ObAGOed0hIw++czUTo6JXgODnItncSJAkzPc5p45fvNOUGYe5GFIi5sD3Rs6O3Yh87+Xl5f17LPPNpmyBz6DvROZY67u3LnTgoBbt261Pu+AviYdQBv9I49+ThYd+xSFT6fTdtzQ3icwVyWfrF/KWp5ysDdprx9eH/7rvh1x0pGa5jMA8jx5lAECZ6RQOtlMYDab1e/+7u/WaDRqFI+CB8ZY9Ax1J1JPx0go0bQ+xwDK5aUjHkaRhBM1DWQkfUyJoOU8X01BOWSGhNHJfKP5WUM5nNXV1ZpMJu1729vbreCMUWR4KC1nLKrlwCk2Z5dRvH3K6ISzZPAYlnyNZVUHKtCvefQqG99zXAyCatzRaFRbW1vNQPudoheR38XFRW1ubrbIPdMAGSHYq4zgqroK8arufcEi9DwzmgVSvseRkCmywdj5blbtDn+2snL14oRhFJ85TY7KXDi8pPvzu8ZvDXLfk0o9ODho+2XcDHaOT+RKT1QBc3yOvFR1HbqkAqyvz1oXDl0zj5TPTAcp0gNiOHFzSHbs9PS0vY4zzzzbW8cBc6+G1D2ZvHv3buswN51Oe29WWyyualYEHhy3JiQcPgC2WCzqiSeeqKOjozo6Oqq7d+82mwVcAL0ZkWdzDSCfbSSLGekfHh42G5dHoQCMy8vLlj68vLzstQv2f0wTkPugrocR8fWv+94NHaBUpEKQw3Ob6SA5xMxTVnWvLlTZl1FjttAk6Olwk2rKiBOKhPjSIELuicQz/yeny1iIaJaWlnoUmj/pVOW6oV9MQFLD7uW4gecYdyL/LHBSST6dTtt3GK0EIxn9Mowow7y3CEw+P9MFjgxZN5/LYx2oRfvIiNgbtBljP8yDMYx+rmjEcyH2LFjiYKu6c6xVXdOKLEbLc8nkRxSfa42qkydMkKO4CpgU9WZukdFO6pM8yFlal8Vi0arwM09n/sk8GAdAZJ4chu9be3PlHJLKF8WSC2uTBjhrCDh8e2O82Iw8/cCp5JjtHZ3ixH0vWSRz4DTlU1dWrt6uBmAkCDEmestpAcUAeMqbwCBTMO4jDWStyThwcnJy0mpDgD8AlDzQid3d3UY3AyuAjkLOe/fuNWeY4830HH3O45VkRLoAABMBZ6qFHuspTm+kEIAnz3lQ10NHfP3rWi0uldpXdS8U5yA4MBeDg7JJg1FVLSqmyCKQrO6cz+et04zG+MaSRUCec/fu3VZIVtUd8fF5HXyS5jWmLK5h8LKiOAtEGGU07bA6NB3W6elpO4eZb+nBEFxeXtZkMqnZbNaaDEC1qbQiS8Viq6urtbOz08aYVDWDl8YoP5dHWDxHhCL3mPOjWOn0M9+XgCjz6Ekx5nlvhhVtZjzplKRBqqp3NpwRdO/pdNozuIxe5rGN0T4waPYu2RBrZL2zQjmdQlbMkwlpl6quc1rmcDNizeYXfmZdAYDM32uOkUdZhkBX6ocMJiDLGgX76z70gJNKCpNOD8GYsWVKwO+z4tee+puMktdkbozf8+07UCiSzLy/tZa7pfPkQL6eg8RasTPz+bwmk0k7IfHII480OQJsE8hjwbAWwMjGxkbt7u7WdDpt9g2Ic1zOmtrTjN7R0Pv7+81Wnp+f1/7+fi+P79iktacj6YyBmgTFGKjLy8vWi//h9eK57tsRozQo1c2bNxvC0smFoDIk2UGmqsupZR4sS/0J1sXFVeu4pOHSQSmIySpAdCdjmHRXgoSs+vT/qq4SVVTvmEhV14mJcee0fD/zZ+vr6209xuNxe+HE6upqj3qazWZtjoxp5jHPzs5acZwqTPPPgpWqLr+FfmfsgJ1swJ+KymhUdUVmSVVziNYv85wcR0Y61iaNtIb8nisC8Sx7Z04KaIYFR+aTQALdigKUX1ZgJyobj8ftBSLuz2BbC5Fg5o9FhD2FWe76q5MXlay+n3Q55sEJA3s4Ho9bcWPmtTOqdC/AK4sC00H5rr0gP5nD55DtjTkkYEHLZiWvz2QzDdFgFlBycqLYLMSyRimvyfj4jj3NyHII2jhsskQHEyD73NLSUs1ms6Y/gJvUTwJnVPFkMmnndw8PD1v1uPFZA9FxpsHQ+MmuWEN2cj6fN8o+I1qON58DrC0vL9dsNms/l/bL9AAZyje70UG2ylg0LHpQ18OI+PrXfTti9BRnw1gsFlcl/w6NE4jj4+NWTciREjSRAURZ1TmTzH9VVa/ABLUpakiqKp2PyC6NfFYYUlqRCcqRM6Bk+W7ljD4gdr/jHPL8bha6OJKSlLXKacZMIVMW0Gill8UujCDDgeJicEVm6UyAozxacnp69RIN62++1oEBzT3Nz+bZavccRgnGBaRlm8ykCpNWrLpyMpgEc8nowdwYOoYGBZuVpVVd0dJ4PG5vmLL3nmNtyEDWEpDhBBq5tpkG8Rx/GG4ONedNdskPmU5gZZ7ui14f5pnJIWc4pGU5cU6HnCb44xiBhqouHSACz4Ip80uWaVhpPMxzk2/yaq5+r5gvzzNj1MgCoEPfrFVVv/8AcOBnZ2dnLdeaTE/2v85xkTkgNVlBQKiqWsABWI9Go6afvovKt+6O4uWeO06VNLfnJ5Vs7GzJ1tZWY9+AboVkmC2A2holA/Tw+vBf1+o1nYUHScWNx+M6OjpqZwnRUHItGnkwzqkESfdmZATVnZ2dNcfPAIqCFotFy0NBm44EJcV2dnbWaMl0wFmJKCcoAk1BRQ1yxuhiTj/RO2PpWdvb280oaEDByAMXo9GoRWwqjnd2dno0b1KDme9O6i/pfwVjWXlsjVHjnFbmjLJyNWlZhsh400AkZV3VVekyklUdNciJZxqDY2TME6BZS+Oxhpwv52rdE+xUdXkzYye/iuOMPSldwGljY6M5NuNaWVlphUHkK7tgpeH2N1YgIz8/J9fD9zxzauadVdTk114kAJOTFvkz+PncjG4zIiMHZD3rMYbMEV20dwnWyCAZMMfc55QrewY4Z945c7bqCYzb/vk3eeBMyQOdzncl08P5fF63b9+ura2tHlj3fcAJA+DnCX7M15xWVlaa7DjmZH2rruptyKx50g82bTQaNTlk856vWt6esx30C1BeXV1t90vbm/r3ob4eZP75pXpdq2raERO5nKQXRRqiocz7XV5etlwNIUCxZGFVCja06x5+l5QYR5Z52uy6s7Gx0YoufFakhIK7uLioyWTS7schizRUQFZ1ZyEzIoKQM7LPc8dJOa2vr7eokCFORL+xsVGPP/54K1hbLBZNkRS0DCnFjDCybSbnm0rOoVV1jUkYOs4VTYt+zypoz2JszNW6iV5ms1kzkvZalMoYiVDsqc+LTNHIKuh/r25AAAp5U3U/BC8iQQ7b8RD0d67nsKAnWYphNAIsotLl4egBJ53ghLElu4pvzIF8c872JZkABWAoZ4Y6C9BU+noGelt0nEApDbhUA/YHiKjq3h3MISfIAzTsX64r6hwItL7abiqsk5YgG9ZIYx3OUsQq/ZIgzrys0cnJSR0eHjYZk5vf29tr88YaYcGMI9ugonPNuaoaa0XGpCns43w+r/39/SZndJm+Jbsk9ZcpGfclD5hCepSpDTZTKmHIeHimivGH14vnum9HzAAwqpl7yAiAou3v79d8Pq+tra3mdCFaHavSGOS5PULmGNFkMqmVlZWWF878X1VXLer8Hhq2qlrOkLPI83ZV1ZSQsTMHzlcBGOeUkY5I13gYAc/ynCz4UDGeNDpqfEjlMgLGl4YXpZ73SIoQqh82jE+6LffR91HfIgsRMqea0WrueQIRUQdAYvwoePPgtAAVn/dMn0vwkHnbpJKXl7t3X3uTlnVIY2qOWAQgEGCSghmPx72z8OY7POON9kxHQybtB5kjpycnJ40dqKpeHYU1AAYTeJKT8XhcjzzySN27d68d56nqv1e7qho7JZKiE9ZLfjyj+WSnMgJLyjf3JyPQvLefGy+AKDJO1gLY46CwaSLC+XxeGxsbvbFxwhcXF626mU5yPlkX8dhjj9XS0lI7w24OZJPeJfVtD8wbIMHw0X1rtL293YoNXRn1AmzWwRqSUeP1Pc/PanD3cTIg7RHbd3l5VTH/6KOPNv0DGrMQ9kFdD3PE17+uVTXNWCQiTufA0BGGpLuyyCiNAwGp6s79Ef7M3WaUltW8npOOj5CKEtw/izwcWXC8yJUVjQybeaRSVlW7D0Sea5TGjZG+vOz3qE2aCTUG3DAqJycnrX1jOv+M2kRf1sDYrG1GxCoxPcezoGnMAafCeKdR03hEMRlltw8A0jDnbi3RrVkgk9RpFtuI2rUwTTbCfbMA0P09f5ivtX/pkMmdqJsTStm0toy2QhqRnHm6Txb0JTsAWHG+xphMRlW1VIaXCHAOCW7MW3WsPQd2jZuzMAd7ap04boAYnU+XM31jbnQ+jx+urq72aP2MTjOVYT2sv0Iq9iDBnje+DVkR3dmGtH2yMCLozI9bEwyF/1uHzL8P0zSXl5dNVtmdo6Oj2t3dbS9ucR9Aa319vRVwqssAVDE5CU5SvjLX7n6OQQHJQ1m3dxsbG7WxsdErkuSEnRt/eL14rvt2xFBqFq5klMxwK7ff2tpqFCAlyPwTA5qRA8cLfVd1OeQ85jE8f5w0jIhoa2ur9Xbm+Kq63tWENgEFFM1BimYoS0YwBDxf+M3IZnW3qAbVVtW9yB3dZmxpEDLnpn8sA5FnPTN/lkYsAQpDtbKy0iKUpNekEoYR740bN1o0lzkze7+0tNSLVKwdqo2zAVhUjbsgfWmNjAwz52mdFSGROXvk3KV5mJvqafdh6NUTcOa+Z65y/Wg8QIAj5RAwG6q2OaqkNpO+zbmJIjkUDAewoxo/o3fG3lwY6Kpq90sZShkZUsfuA0yQF454WIglBcNZDZu7pJPNiIuMAd6iNZcTBVlolvI5ZIiwbTkX4CMB/tLSUjveZ/3u3btXt27dqul02mox5vN5r8FF1nwkqOMgvTebDaq6An8KtYDU1dXVZhuqrti9w8PDOjs7q4ODg9ra2urtMUeM3h6+xEEPBzKQe5qgBgO5trbWvnt8fFx37txp9jF19EFcDyPi61/37YiHSo5+zZeoq9pbWVmp27dvN8G9uLio/f39piwZWacDVFDF0cl7QpbyfZolQMRV1Wg2lCvDyOFlNM44iigIcxZ4JJ0kKsjiE28F4pDyPDWHVFXN8TEOjH1GKFmERUkyR+aogoKwBEAiiewOxJEmNe1++To0wAiCH0bYWayT9DA2oaqa08kinWHOnGFzLpSx8xwywYjbp6wdAJqqqs3B75NxQU9X9VscJu3ns1lQlhG176h0FmVY66WlpV7EKXIkw1XVzr4nGAJsssgm2R3HajY3N9u5Vg7dOWJrgDLntNH2XixwdnZW29vbba3pGL2wbvZZWoSB9uyspxDxiybtG/Dgj/m6DwCY+schoJftN930HLUF2Wt6yL6wBZeXly095QLG7KE5SndZNzpsrlVdtb37aXXL9g3rScgHHRBRJ/VtbX7zN3+zMR3mS88ODg56rAoKHPiQ2ss8N7CdaSWySc7y6KSxPYjroSO+/nXfjhi6UwjDqaThS5TGgGjZyEGLbkQW6YhFq5xS0s/PVy2ZEbXiF4qflFUWynDAVdWEtaqjkzJnKJ9X1UXBFA0qzvOZmScW7eT3RGSiDIYonROD4XMZzVV1L1b3bwYkC7k8J49vQczywjp8UUzrmpF/phs4karqOWiRNICRNCfjkUYhC1Eyirbuim/sWVLDjJBIGr3HGYnUOJVct2RQkmK2z9be3hnP7du3m6HO8dgfXaCGgHAov8afdRBZXc8gy53b2wQ4jD9ZpW/2VeVsVTVdSwbC2NMB23cpCWCYTqBoMyWQdRTWTJoBIBTpGbvoHTCzVgnAzI9z9r08bsPerK+v1+XlZaPAk0qmr+4pmrR2Gxsb7SzvdDpt65F52DxqRU7H43GPCVgsFi1QwITkPdJh2rPT09Pa3t7usUrJRCVgtWcrKyvtaKHAB5vGXmDFpHKSHcPQJTuScvHw+vBf1zq+xOBkxbINR/e5GO8bN67656Iwvd92mAuCLjMPpCKREOcxkKRes/jLxbBmnipp7qpqxqKq2j3SeKKaROEZWTP2Q8cn/5e5rFTEjLg8O/NbFIuj5pzSGKSRo7wKenJeSWOLtNOwivKOj4/bOUqtPtN52p+s3M1cuD1JinBYH8Com3s6GPdJZ5c0PkM9PIbESKLl3T+jNTLi/xxV9gP2TPPmkJaWlhpjsLGx0fYh5573r6rWxhA4yDwjenc06oqNOP5sNJOMEbqVrDDQQGg28DB2kRxw4t5ZS2BPAFogJ1Mb5uU7uU/mUNWlhkSFPpPpJLaCU8jmNpy5z2K6rEfKtd7jwKuxcsiev7m5WYeHh01fjYXe5lEhegzU5usUE/QZk30E/DB0quYdcRtS28Zy+/btWl5erjt37tRsNqvt7e2WgtKJ8ObNm72UAXvEFrHHCvWGR8V8J4stpXfc40FdDyPi61/XKtZSwIHiUuqvmcf+/n6r4kxDKF9BAPy+6qqBOUSZKFQU7J3ChByaS8XNApnxuKvUVGRRVS3PqnKXweHsRGsi3TR8IlT5yfX19V4eOKM+Ap5zz8IpClNVzQiJEhhcY8moPhs1SAFkH9xci/l83mMuRO3Z2clRnps3b7Z9BBhEuElLJ5DJiJPBTkrS3hlb0sqZ4kiUbo3sReYe3cNeZGTomfkdc7aWyQwM78GQuxhBEaV1QDXa5/weZ2xPRS2YF/M8PDzsFekkSMt7KsYh6+aUR8k2Nzfb8zlgfzPmo9GoUdU6RGWjD9G3tUgDbtyea/38nIMBQtOxJxVuD/ws1yv7gWexkbxuHk2cz+ctklRXkXqTgIHuZGtd4+WEE6AALvYpmbXMk2cEm8fNVNqbp88BFNiCpP7v3r1bW1tbzaYAb2nLsAhnZ2et50GyXil/0nf2cFiQJ3cMDD10li+u674dMfSaFGxVtcIAUe/y8nJ98IMfrPl8XoeHh+1cXdIhDGsWZVVVc7qi6KpOaTNaYbA5XgKcdJbnuCBZkTjnwSmmkUmjwPBxqO6VSumihPJqxu+gvUP+eT/Pkt/RoERuOgEMWhq6z+czgIwPhRdZMmoU2NowtNIESV3lMzi5jKby/2lkM5fNMCZ9nqkHY7QvGRGrSB+OOWlV309qXNQJhFmbTG3knue4RZQZ7WbO35X0bkYa9s4eGDvqMEFMVRe1ZiFQzjHBRFL41nvY3jGP9mVaBfh1P8yQtc91T5DDsHuu6FDeNSnUjN44C8/jANIR+L48q3tubW01sEImVAhb49ls1gOGCeoyzcNGYAwyRSRv7z5sUlLlCbJ9P49AZrplGP3mumaBqHti+LI4UBSc9sRaApGCIlXhmWJzuuTGjRvtPe3Wn+0BZh7U9dDJX/+6b0fM0KA6FBBAlHItmQNOBV5ZWanj4+MmfGmUqq4KVGwgx5uFQz6bhTPeesKhy99kTpZTdqVypCH374yukj4XfYokRchJwWYU5/nmnhG26lPUEmBy69atRhEzgPn6ybOzs9rZ2akbN260qCtzfIyeqD+p5gQHnKK12draamdbM686pLlEPIyRyCE/mxERA5CRnag98+o+bz72Mx0n8GOfhpFsrgWjaIxkKAuVMg/KUGZbQnNmuPL3jlFlFIq1YMwZTPPLyC6dPBaF8/O9PAeNCblx40Z7BZ71SEdCfgEDn0sHy2lpcpMAOaM+8yb/wBQnxA7kfZPZIte5T0ld24McJz0gr/nz4b1UyecxQ/rncycnJ22+1o+NsMfpIM0961FEutmljlyIrofpIfPL6Pv8/LwFJQlE9Iu+vLzsgWfrmnuXkb+UCeeKJchK/dT74emPB1k1/fC6/nWthh6Zz0IJioIpo4ppjdMTzaFGM/fFCOzv77czd0nz3rp1q9GxGemqCEYT+zuNW1JSiYAZc0rCQGa+mlPKoyscchpbhkfUW9U1l0gakGNJOo1Snp11bTyrutx4GiTjUiEr/86BHh0dtYgquygx2Fparqys9PrRMkCoQA4iEX8CI3n8TB94RRsjnc3/ny/vnmsiN29/8viPgr+hIRoWmqTsZdRjPgmQOBeGHF2r2ClfFWceLp+z36hVDj7l2xjtex5RUydRVb39SqrRODMfj0FJB0S+85JvzOI1kaT9ACzdIxtikM/UEXL9fCwQ2U0AbN4ZxQJj9JE85fru7u42p05HMurLSJMtuHfvXtMZaQ7OJhk0lDK9Nj/7ZV0y30rGPQsjxe7ZU7qUzVowOmyDrmez2az29vbq6OiojdNpBPUJ0+m0Dg8P2zOsT1U1oG2dBETmxTnTi9FoVLPZrE5OTnog/UFdDyPi61/37YgpSNLEVf0G4pBZ5lQIMkcGlTIwBI3Sok1SKThWAADiTuOZlHL+G0KF6hnyYY65qqtKfb6oaxhZJ33J8HgGwycarupHWeacEaRWf2msjKFtVuRPjUtExIFxolXV1olzTKOYhiy7kC0WiwZ+zCnz/Z5vTdOxLC0ttd66jBZjmga6qntZSDp0Tn55ebm2t7dbIU0qtjly6JkzTdozZcAcAAHRS+b35dg4ROO5vOyOw3GMCnT8fkhtiqZQyv7OiDhTCo7NWF85bDog6socMblImto4VE9nfpnjBVCSzk6g5P6ip0yf0KeMbF3JICRzkuPyvHSeQGuyZ+l06VM6UXKUMml82Ro3+2wPgSAdzFTByspKA7MZZVd1VfcAguhWYRcblvpC75eXlxt7JmpWtwAMZhSfYEEFvT0DmJJByJaZHC1gYQ5sTbJND68Xz3XfjhiVgQZNWnmIJGezWUO6mVvOvB5DqbdtIu/MI3peGv7MOVF6nyFoFAEaTTp6WN2pYMK8CLR7+R4lS8ooi5KMo6qjoKuqZygzSszIiRHxeX9E3RnFi2oZaECFQpuD5zGS5sNx2RNFIVC9QpekPocOIKPO7AJkbznRlZWVFsVngQtaO6OOg4ODunfvXu3s7LRiJJGf7/h8FoB5JtnI3C4KNQvfhusif+9zIg9vc8poXKTjeWTQz/OtWqKwzLky3nTJ8zjQpIbJsQjd/P0u55/OnSHOM7+cRF55/3QenA9jbg7pJF3OdAMZ9jcZtLxXpgnkhLMoKmUtUx/WeOgkAemMohPwAqjkEzjBCtkX37d+1hjTkyyM9Upa3dqrqHZWFxtCfkTnxkVGx+OuP7d1BZhTx63ryspKr4UpmadnPicQYU8UrSaI+lBfDyPi61/XOr6USrO0tFTT6bShTqhsNOredOKYU1WXt03FQMM4mH50dNRDoSI5VGIW2ezs7DR0WdWPzPP9yPIvjDSj4fKM+bx7BzHF4JCrOvpzPO7OOzMs6UwvLy9bHpywpwFDt3I+mWcUCTHESasvFosW5TDMqEYKnhSa8ZnjkB1IA4JCM1/5/4wuM7LLSBtFm80yvJdVtJZFVuQIOBOBMkqA0WQy6Rk764oR2d7ebnvLATCc5pHrydAnQyBKBESskQpjcxappWPNhh6aeFgv56DlJo3ZmmcR0urqam1ubjanmewOVsK7fIe9osmxaIv+LS8vt1QNvazqHGwWs+VRtSzUc790rEDb5eVle3tVRuSZesnoM4+auS/d9wzOSipCxXECDkAmG2/o+ez59ipTEanvGRmPx+N2ukDDFHJLj6y1vUgd8D02YBh9Gp+xkckEpY8++mjTY2Pd2NjodXlzbW1ttdw7u5SpNqDdGLLhEFCpW9eDfOnDQ0d8/eu+HXFV1880qSYKzLBqZShaUFLv59BcFjElsvQcCo42kkthaNFmVZ2joXxJ3TCyVf3eusNIw3jdN4+/OPMo8qcg6fwh6zQ0zgJWVaO9krIzV4ql4EK+y9yHlBujQBmTptZndmhw8rmqaBlzkQuQlce3RARJRWYFdFalLhZdYxXPSmeQ89JzNxv+V1VjR+S8GVx7BkTYo4uLiwZQsuKZkwBSjNf8OAzshLkAOK5kYJJazoKmjGAzNUK+8hyveWxubjYGIulXBtzYkm0Alow3c6a5zr4LICRFnGmCPP6WIGMYLSUjlLqU54SzriCdsbXLo3FV1fYgC5CqqlH0ZNALW+wbh2fM7EQCRuB/+M7yqj4zkjYi7UgCPLKT9QaAHL3OtJMrAa1WqCmL0+m0PTcBy/r6ek2n06qqVnzl75RntSpJ+Y9GowbqsoGNdU6g8JCafnFd9+2IM7+beSVOmSOuqlbpOxpdte1zFCcdrrN3iWK1w5SDGzrWPDbA2Ym6KZTiGUaDQxVxZP5oSN9W9V9UkEZWpTM0LJIEQkQVItmlpe59s8aS0a77U8w8W+0ctAIXxRW3bt1qEUBSnXn4/8aNG+18Ys4HGkZ3p1FkTOxdUlecfn4mj5dlBag1sFbARxqQNPIMBWMCiOjJi5bOKvOkKK2p+3DsnAqZHNLVHIMCGUWC5mMfMkfnHoxYgq6sEM7n2DvrJxrXcS71CrVtrz0nUzDWeZj78/Oq7ty7vc/0SoIR1fT0mSMc5n0zHWQuCQBzDPaLzvo+0Ars5nMyH51z8rukotG+ybQAxsZjn4xtyDCxNRy0wEC6J+cPyHBsZJUdA86yKI1tyRqB3BOpC3povJnyw4bQtWQUtre3WzMRaY60d+PxuB2TpBNZm8EWDNMUH8rrYUR8/eu+HXFGWIyuwix5EJRH5kY5W3kqAiIqSgciSsrqU5/NPImK0Cw6Mr6q6iknY52VlOkcs9pXxEtB0+mLNvMFDAxoNgkBBpKqNReKiR2wFkAJap4hz3edZn4tGQV5No5VRAYsKLyyX1XdG4oovXzZ4eFhL/pxH0aFYavqKjfz+EZG4pmrq+r3HEabk4mqKwe4s7PT1ofBzCMX8rgMa66FXKX1YNzSeTK60hr2IfPdCf4ATzLHkZoPGck9w67okiSyyzGTt9yTrFvIegRGOIvFGH6ybI7JMDHUKmWTMqWX9jj/nWDJc+mR59BRdOrdu3dbkR5QkGkArIA1IpdAlRccAARa51Z1Lw7JVEmuJzbGeMkUIEyGh3aBznB05q31K9kB9A8PD3tOju6in+fzrgsfcCx4UCfBcV5cXDRGxFwy7YKVsVc7OzttPNIiXtGZoG5YYzKZTOr8/Lym02mT/XzBxcPrxXNd66UPKbgMK+NMITizzNkxyOgaxnNYNZsUbB6FqOryrOgoeWAOhWNIOraqmoIo/kljWdVFZZPJpBkHjjRROoeYEQljz1gypvk2JIZVpJ95x6QZRZ6pmOkgAJUs0KnqiueGR0usKUeXBst5WM/0XOucUTojJneX1N/Z2VlNp9Pa2NjoUWfmlvT6cMwMQ+at7W8WoXDG2Xno4uKi5fZEy2QvjbucWTpKwE9ufhhhJtuTOfxkOjJnTobJA0fjykiMQ+TcqqpF02SFA2NMHZGS6vB8e+QyJvMReZG7rN4HlNwj6UprkPOwfxmxZnooWQeOnVPMoihXghfnc4fgkcMYgovU7SGNLqLMIq+hfclUAEdr3EB5phUyVZV7av3UMkgLDNMy9pCt0nlQR0HMTBaKKsQiL1X9s/+enQFC6go9Zm98d2lpqfb29npFYQ/iehgRX/+6b0eMhmUoMuJB12bkK5rb3NzsVftx5PJdIlaH75eXlxt6ZjRcaRCyOQNjrxoxvytnmZGesVd19Fr22fXZjESHxU8UWj4r87CEX8W2z1d1ubH5fN4MJWVT5IQKT+q+qho9zWgntZbPYRAuL7t2ie4h6kjjmZFNUo8ZjVJ8xl50jzrOPtBJw1uvjIyHjicNqmiNMU9HnUYz9yLBR1X3xqdc2+G5avfkoLIi2z2NM1MXeYzOGHNNsoNVOvusRwDgjC3ZDv/PNRyNRk2OjZXcJW2N0hShciwiMOAmqc7Ly8uazWa9vUmgYU8yRSMKy/oD61XVMVjWJXtSc/gKI43fs1ZXVxuwy720ZlkcliBScJBMARnm1HNPkzlJ4JygRA48U27AXNLu5IiMTiaTpnvWzxgV5zm1cHJy0nuz3NJS97pLKSD7ah/sYepB2gh7eeNG18J0aWmp0d1pyx5eL47rWseXRAmMDJqMcSJIjEHmhDhZwph5UoKStFhSR5QInZhOh2ImLUt5OAb/JpjoNc9BPyXNnoY6I4eM9jJP6VnpwEV2fsdAoPHkC92XsWGUk8qzvnKpHK3P2SNjYmz39vbqzp07vWhx6Mg4lYw2cj1R+hracwJJwyUdXNXvTZ5nJqu6dAEjxoChbDM9gA5MitbxEHMWZTCgabDdK9+aAyhlAZzoLA1aMhYMsDEnO5Kykd2nhnspgiZn1oi8YG+wOwCDNU+Zt//2xwvjMUmcKblHd2YEhe7WpU01fwIS8zCuZAiyTsEF2AHZQGKuzfHxcaOfzUNleD5b5JZ5efNA92ZETJatiwg+1y7B9fBUAlvmu/LnHHXS91IWWfHMkQ/lJFmQpIVXVlZqc3Ozbt261TvCmSmRZNp8/+bNm+2c/bAQlez6rE5sai4wfQ/SET/I/PNL9br2+4hRgTYz0RUheeyxx+rs7KwODw/r5OSk5WoouyKkqi7K5cApWpbtcyBJLSqgUMBAAUWNSZkTYs9LBEtRRK/ZnjPBwfr6enu3b1XX5SYNLmPDuGbhCrqQQYDcRfSexUgkTerfnApAkVXgin0mk0n7DOXf2dmpi4uugv38/OpNVuj9/Ky9ZnA4EWs9n89bwRinkIwBJ1jVRTOZBuBURqNRq6L3J6Ofqi6SsFfWO/OEVd0rF5N2NDbjA8iyuElUkVW0+Znnm0tGlC4A1d5l9CZKzvSB5yc4qqr2udXV1SbD1gOLZP5V1TpaqUEQ/RgzcJPpmCFVTx9QsHLWdMx307GikIELIMS9ybM50gvPy8K/dORsBRlMNkIdhX02z+wBn+ANG5DrbE3ys0NmxjPlgfMcfYJp8yfPeTyNvdE6ls0EJLMAi0yRDeAp53337t1WNzOfz2tra6sxEgIHdiDTQfYckwKIWNuH14vnum9HnO3YCGcWHiU17c/NmzdbDqWqam9vr0VzDIT8TVb0ilhED4xRnl1lrBlNz8/8mQiO4bq4uGhFIVVdjomxeb6omrLkEQd/O5uXFKwx5cWAZQ5qPB43Y5vHJYZ0vLWElDnhRJ0U+Pj4uOVGOQMOMx0u2jKdHkevoMT35IkZP2ud0ct0Om3fl3Kw3jkvRpBhr+qqZuXlGD8GLh0wateV7ER2o7q87HdlAyDseTpX1PGQrlssrvLP5NXzGUkORKTEyTGwxpY1EcNcaUbK9sAc1SnQKQAv9xtYyPPv3vA0rLZlnDmZ7J2cR7iq+j2ncz3Ie+aX5fTTcSdjRX5yvTiJIeXrucDQEHQlBZ/ReOb5c22tXaaiAAn3d1//xxBI02xvb7e9A3w4S/orYmav8k+yTQKVW7duNSYMS5L5XTKa4ME+ccbmkPIDSJkHvd/e3m6pvzx3/aCuhzni61/X6jWt2MBZV3kQzTvm86uD+ATS8ZV0IJQCnUOIOEMKyrAyRuloq6rRaNl5qaqaQFNEgsqBUCJ0cVYvD/NuFB5FlHlg90TDZTV1zkfzj6zMzjwuI8SAJa1W1Sll0tQZjYgsHcVZWrpqtAIwZc6Sg15dXW3nGDOqZOQyL81wOobEYXJis9msPc9+Zcs9e2CPMkqtqkZTkpHMF1Z1vYCtqXXKox/DqBlwysKmYeojqWfRbEY5AATAmPlcztI9rHEa9BxD5n+T0vZzhjMr+kW4IkGgcqiPSfcDSPSIgyY3+UYwgJdjuXnzZpMj7/zN4qFkacwB5crJJzPmMr98eUnqCIft55zMEPySTZ/LtIw9Ma9MadjvdN65/znW6XTa9p8+SZuYh+i3qmv4kacHhpFmVmpjLowBSOA8j4+Pe2DIcb7pdNroamxW5s2TBSRvWDxylsFHtuV8eL04rvt2xLdv366qam8HyhzP0tJSy0/leWJONY8uyKFwghQ5u8lwVtCr8vshpSYH66LEokdG8vkoVtTw3bt3e72SFUf4PAro5OSk95q6i4uLllvLCMU4zCObEDC2eVwiz5kywmmgGFyFWxx+Ri3JFkwmk5rNZg1ROyaB1sw+thnZ6bSTBSjj8bgV+VD4pG0di9rY2Gj7kLnjjGQAtaRinaccj8eNJs81EGVlpJXOq6oDKvZYQRannJGF6DTz4yIw/0/AY13cI/ckn6vq1dp5jqMw1gP4BAbT0WT05zPZXSnvw/F4FnlM1iH3DAg1Ts/muFIWdnZ22vdTD8iYewKvCSxzH3KcGSG6d76YIEFJgiDgyXN8LoGZ8flMslN0QhWyMQGN7kGPzS/TLY5YZnEUeV4sFo02tgbHx8etcc/29naT0ZSv09PTOj4+bmk69Q4ZbADR0mG+KyWVaS5zw5hkAOIzupFVVdPpB3U9dPLXv+7bEUOXHGdVv6Ix87PLy8s9hzaZTFrbwtu3b/foXMba5h0cHDS6M6s+05FmlTAqFN0mIqfQotGkBo+Pj5uyJ7XNUaE2b9y46opzdHRU5+fn9YpXvKIZ1My3UNKqaq+DpKju7TloaEY7C9dE0T5P6Tj6pFKTpqOojISUQb65qKorkmLA8rV/foYyz3yn9c7zrxmNMUIKehhlTo7cWCP5RM+0P5y4IpksnMuIt6p647FG5kM2M3KyZqhZxly+LKk8sjbMlee6k117xnCnTtjjpFOrqnesLCP3jNrIreekI8x8pJ+JrkRzmc8GbAApEblLFJkFYrnOuY4pZ3lv0TG5GTpJV6ZE7G1WxpMDDjEdPnCRe+7vlF9jNqeq6jUisqbWDiDn+BQD6n2+vLzcAhFOzb3IHJ3gJNlBY0lgNJlMnpMaytw9XfUSFGwGPcBIDtMy5GE+n7f2oePxuAU+q6ur9ZEf+ZGtYc6Duh464utf12rowfj5N4eU9M7x8XE99thjVzcPyun8/Lyefvrp5+TAKB/jpGDr7OysNYgQORCszJUlzcNZoRO3traeg6jlzxysd19GP5sJMDoi0YODg1pfX29NElDTFCqjNAbPPaFcVKi1dOX5P0qaVDdnm4VlHAoHDVik0a2qZth83jrKxVof+5rUO4oyqc3ni04YPg7EOmfRSUbaKrvtQ54xH+bYjMueZHOHpJ7dH1NgbRgi7xG27zmufCMYA2+NhiyL+2ZldkbvWSCUFDUnn/tk7bJCGDPjc5kOIB8ZXWaBo3FnMWSyGRw0w58OeTQaNZbCOqeDSNnK4rikO82VXCQrUNVFsfTVz/xJnUwmLZ1apgvYH8Da/qfNItv5HPdJsHXv3r165plnGmPBaT711FMtikya2Tx9nyxbZ30Jcmwco++nvRjWIEhNCBxQ4HLf9pdOHh0dVVX13lqWwEMRYFZhP7yuf3n97ofyum9H/MEPfrBWViDbQnsAAQAASURBVFZqa2urhyAJ2Wg0qqOjo1pbW6vf/d3frdPT09YneDabtShNpSNUS1CcueNooO3d3d0e4mXo5/N5o4rT0TDQWVhydnb1WjhnEzlGEbdzl5R6b2+vCXjSvwcHB80IG7cCCRFRFjXl59w783SimKruHcscd1VH3TKGmX+6vLxs7zflLOVrM3cMjGAKjCsji3xJBSdrT+2DCIFhNebLy8texy7KzzFlDpazM+7M8adzn81mvUKUzMlmjk7u3s8yGpMCEMn6DuOdKRBGOvfceIyT0eXokoUwRp9XDZ/PygrgrJh1H+MDRPJ39owRrqqefDH4nDeAm1Xhw3UyL3pS1afufTYb+OTzk+7PPDQHwbGkw5DKoCfsgM9ub283cEQ3MAocngj13r17dePGjVZMl+uZspGMG5p2eXm55cPNYTab1fHxcT399NN19+7dXv721q1bNZvNmlx5NhbG/G/cuHp5DbmyDvYrx0DnAHTFlmRg2II3K9fJ0mg0anltcyYvFxcX7dWg1s7+DFN6H+rrpR4R/+k//afr0z7t0+qzP/uz6w1veEMvEH2h133fgcEkNJm3Y7hFF5wJOlqkkZHa0dHRcyZAWNPITCaT3ssSGMukNznv6XRak8mkOVLCSknzDSp5LleOMyOPzL3Ki3OiaCaFa2loMxI1hixEGhaQDSOSjJI5Yp+xzlkMx1gDPZeXly1fz2HaswQJQwqvqsvp+06u9Xz+3GNLVdUqQDlBbEdWslrLIS1q7Jy9KMC45O6GVLwIJxkDY863SKUcSQt4ZgKgdCRyrOha30/Al0Anoxj5PP9nkBOEYVsywkka35WMClmwbqhwa5NRmPFZ99Q5jooxd380prFrLAN0AAbkoKqLdO25OQApADNHmgAEtZv1EVXVsynYLfuDXUG9Glema7K+gs1KAF9VvVRQrkuCIfru3iJSQQgdEmGmw/R8bFMCoazJSPDn80dHRz3am23JfQTU6HK+SGUIiH0/6e4HSUn//8v1/d///fWf//N/rm//9m+v4+PjeuMb31if/dmfXa973ete8D3v2xETDIhfBJWCydFwkAQ8kXkWl6iWzSrapJ6Wl5cbnZiRwtraWnsxt0jByyUo1JAGlRvOimCCqzJ1sbiqDmbw3HtlZaX1e6V0KysrDdFnfjMjSg6Ag6jq6PTM96WCei7nKl87pJyzAOXs7Kzl4Rlsxuvy8qpzEiebHYUACA59mIcWgeTRLnNICg3tyiB7blKPfpcgxHfJiH+n8R8WPqXRJCfr6+u1urra/s5oLcFYykTSpTkPkaxxoPLQjhnZKvLTjEEkx0ECi5kSAYqwN0kzJ1jNauQhFZ0AytxyjH6XtGymBjI3aQ3ITB61Snn1OY7f3mqwkk54WCxkvTWyGTJiaGpA33gSNHLWZICtoN9AtDlmjcYQ4CcIHzIOKv4FEMCXPugJUHwHKOYorZu1fr4r50TX3MO+Hx4etmdub2/30i6+px5myOiQRcFEVdf7IFMAD+J6qUfEn/RJn1Sf9EmfVN/0Td9U73vf++o973lPfdM3fVOdn5/XZ33WZ9Xnf/7n1ytf+cpr3fO+HfFTTz1V4/G4HXvhDEWjaMaqLoJaLLpiKsgfFarIgfA55rO8vFyPPvpoU27Up8/Jx11cdP2GU0C9USY7GGnAT7kfeeSRdhQgqa/T09M6OjpqjunWrVv16KOPthZ0kDGBTyXM4ovZbFaTyaRu3rzZjhA888wzvfeDMnoKMkRx7mUdKX5V12+bAmcFNFrv+Pi4tra2mjKIwjh0tBpnlk5XRMVoYxE2NzdreXm5RbqipoxmjMdzEqmr+mb4hgVRnJ+xDaM8VCHDkkwBQ8PBZeMITs1crAkQkcVADDLnaW2q+kfI0InpeGazWS/yEO0CPEDT8vJyT4aspfH5v3sni+J3IsY08uQ40yDGImpEM9sjukfOgL2cO1lk+MmK/XF/cwWc6Kk9NJfs/Gb+Co/0Kvcc+5n0/mKx6L2/OmXGXnCkWeeQbAMnmk7e+gFIt27dqtu3bzfqNzvZpewdHR3VwcFBa1uZBYGYKSDNe9eNnVwAOnREsEPWshlIgk/7kF3Sbt26VdPptM0Fg1dVDYBkQePD6w9/vfa1r23A50d+5Efqx37sx+qHfuiH6g1veEO99a1vrd3d3fu6z7Veg2jDky7lVHRJYlQhZsab8ub7ebNwQt5CrpZi+Z7CLfQiZWZE0pkkTb60tNQKSYCCrJ6s6ijSNEZyv+Ynp+XiBH2/qlpkfHBw0IxGNvCQt/L2GpGttQIw0sgldSbaA34Yp8zn5nlta2c9M4/GGNivNFyeLdrzf/fkCDh0ACJp1mQwnIG0BilPGaUwzBkNZ7OTrHbPfbRmyURYc2Ot6vKW1iqpT2sJUKbxVmCTqY2k5vPtQXnlGmIuMu2RKQERecqm+2WTEHqTfcqTJSGDdMvz8nWO5jOkdjOaBRQToPl56huwy+H5t+jSZ+xNynbKZVL5yZYsLS01EGNuqbvmk04tbU0W6SWA8rf13djYqL29vdrY2GhyxnEBBWxXVTWgidYHdNwX45GNi6zD8vJyS7ctFotmnzhf8ikS9x37Iwq21mRFRA5M+7zvkCfv735Q10s9Iq6q+sAHPlA/8RM/Uf/hP/yHeuaZZ+rTPu3T6h//439cn/Ipn1InJyf19re/vf723/7b9X3f9333db/7dsToRxFeVfVeSUhhq7oqSMqWjceHVDbDzpnLAblPOpQsBEmjlig7aZiVlZVWNMaJqULMN5YYv/+fn5/X9vZ2raystM8RdGtR1T+KUdXR92lsMj9pHdwjnUTmxDK6SQCUuaak69G38/m8GVgRCKTNGGS+1tiGa8vg+F3mudKBpZM3Rj/nLJIey+p3P+eoGG0GbLiOcoroVwUtl5eXLfrSHATgyjU0LvfLvzNKEg1aY/vK8YpCrMHw/hyldb5582aLlvQmVliXlHgaZeCF3BirNfP5Yaoiu4vlGDKqTebI+ttTgMT3M5eZ+Uyfycg00wHunc5HwWSmCfIYmUIjTAsZQWsn6LInyXJUdYVu0hX2jx4kKHavPE+9s7PTbINz9XSHQ5brPz09ra2trQZkjQkrmCmwrAHw/Ky0dw1ZKjaLjrlH5vcBAM8EiG7evNm6IZ6fXzUBkVKk5w+vF3Z97ud+br3//e+v173udfUVX/EV9cY3vrG9orXq6qjvt3/7t9ef/JN/8r7ved+O+LHHHmsKnNV5ufGZE0qh8HmRAaVVha3B+WOPPdZymvpHV1Vr76dYKBVfpSinMZ1O2+sCOQNRFaeULwwAMBjtNDSoJAYxjXOeJZaXZVw8z3yh0UTtFIpxMBaRbkYGjECuLaVcLBaNOs+1SNo3KT9gBBBgvDl/kbd1SCo2c5/WS2GLqE5OtKraPiWtx7i7XxpeTkJEwgmdnZ3Vs88+2wMxioFOT09bikShmMiccwJSMvJDoQJx9tt6JKvDQQIpKHnjAFwUl+V7ulGlHAoZyRyqvbKuyV5wwABhRlnWlaGvqrZu9I5TQpUCTOaFqcAe2TcyZJ4uY0p5IEvDF6nYT3ul4hnlm0VnxsPpk9mLi+5cLlCdQC7BkDU1TvqRkbA1yVx7Rtgi8Fu3brUaEqkfuoAts7f2gywkq5IA0L6vra21RkJ0hA25fft2DxRmuiP7S3s/8ng8brKpcNJ9Nzc3e3umf/YQwHyor5d6RPxpn/Zp9U//6T+tJ598svfz+XxezzzzTD3xxBN18+bN+pf/8l/e9z3v2xGLOCFnhhdlI1ohfJwCg5EGXO502BiDUck8DzSbrzesqh5thn5RjIU2U8Ci2lHBjCNR6WDkzxiepG8yAqjqt1pEUYk20bBpREWD8j3uB60CMsPzpmnE0thlNeRisWh5Ms4419q/k/ak/NbYmJL+wjok/ZxAJAvtrInxWvukPjMKYWiBj9yvIQgBCsx7mIdMCnN5ebkZTw4iqfgsFuM0XNZFLQH5TbZAhJMRrHGm4xAVkiky5POZe3b5N31hdFPXRFTWNKNXe2htpA5Ew+bOkQEuZDCbeXBmihiND7jw3MwBZ/Tns5kqAMyTLrV39Cr12/dz7sO0CfnDpAwZkNwfsgu0+AwH6GciR2uGYcl5cazYmePj49rc3OzlxRWe0b88UqYmhMNOtixrI6xJzp9jTyC/WFwVmbJvWELfPz8/7wEfNvJBXS91R/x93/d99df+2l97zs9PTk7qsz7rs+rnf/7nq6quVUV937uRL8CuuvL+cqiE2d/r6+u1vr7eM2YUemdnp6coaOOTk5MmoLp4jcfjdn4vc3uEFo2t37U8YzqBjY2N2tjYaFElqonxTMTJWXCunocCquqMsDXIvB2jx3hUdVQgpzEajXr3T/qO8iqy4ARFBe45zIfO5/Neu8mMBKqe+3IBBSlZkZ0Vm+nwkqZkxLKSOiM/9KP14kTdNwuRACuAIKObrJ7N/J/11kYwjU7S5NadsRft5pEsUbPncWKZb/ZzDRKSrgUgk1UQjZtjUt3ZdYocZUSZUXqCtXTaKZuZEiGL6VA4HoAJWMHScCZkF7vgeXTC/gGiZAo1muyUNcg15Wzcm15mqiedOD1LMJbzSkeSANpzhutlb7KuJMGcz+U5dTqRbWET+FvXqmrBR0bqFxcX9fTTT9fNmzdbhEsGEwAB354rbZZ6CtwDSAl+zs7O2gtX8j3lWXvCUVu/8/Pz561neHj9wddP//RP10//9E/XxcVFvfnNb37O73/7t3/7Ba/rfTtiQqQIIA2nqtWkhdKJZf6RAUhKjwMTGabRXVlZqf39/XbvRLNJC6UhytxYvomIQULHZX5MtypRpBx05sDyaEWicAac4GcVJ4NhjJkTpnxQcSqccWIEnBNOZVpfX28vjB8aOFRkVfXGbj8AEmuaRki0bm+2trbaGLPBxWh01cRFh6x79+71mBCORvFYghP7IxoD7LJQKsFWggGO0RjOz7tmCBrkD0GSyIlDSlm1Rvfu3evJQdYqADcMO+AI1KysrNTu7m6TB04qm8okWLA+6YyGEV86MvIkzzqfz59zNj1pY3LFeWcnII4H0+E76HhRuMgcI0U26CD95mSSKcjI1j4AOMliJJuUXZ+yXsJaiGitbVV39CdZAqDHWiuAkiPNdecMjZe8oe3tYTo2Y8c+5TG55eXl9vrX8Xhck8mkR9MDB2yEfaXbeVxuNpv1ZFlkm7njra2t2tnZaccu01nTuUxp3Lt3r4GHB3W9VCPij/u4j6vf+q3fqp/6qZ9q9jSvP/7H//jzOuj7ue7bERP8VCBCzpFBs9kZh1MlgImqE00zFIwvQYU8FR7IhzLCkOVsNuvluJImp+hZ9U1pfUbehgJmHpOxSDouKb2lpaX26kDzZNT8XgVnFkkxeoBBRsX5zOw+lblBe7GxsdFQLiPrXgxPUnvWkKHx2cwhUeL8rNxTNr3IvLQ8IyOQ9DPFT5r33r17rZ+vuQJXaP3Dw8NexIB+zrcRZacj62WuGAqG3PpUdQU1wOF43K8mJo8MrfUi4/bF2pOlu3fvNmAASGUeMt9kZZ+BOeueVfkiOsbZ2Le2tlqEa88Y9XyF4nze9demLwy8Z2QhYIIA9CvGS4V8RnfJYNjjZKQ8Y3l5uUV8+eo/48yGOfaCntknJxqGbIB1MnbrKGjwuyzoSlZiCAAFCcm6kEHOOtkFAMDFhlg3Z/6zZiILzzIFRU6tP9kkA6PRqLXc1BM/KWvBAH26vLxs6bKUp5fa9cEPfrDe9ra31S/+4i/W2tpafcZnfEa9+c1v7rE8w+upp56qN77xjfVlX/Zl9aY3ven3vf8rX/nK+vIv//IajUb1ZV/2ZR/Ssd+3I/Yyg+3t7XakJRE85JqGIHO9GZ3KVaUwOpes8MB9fIZht6iAgAiFY93a2mr34AwpzfCYSKJYgry0tNSMhcggq0mrutaOabREhfkWo2FUs76+3oz2jRs3WmGWZ1jT5wMAQEM6YFdSmEnNZSTGEKlQTpCUAIGxcw85LQbMM+xFFpCga5M2Hu6b+WWxkyIcQIijXFtba8dJ0N8cju5PjLpnJXMiR+fnWV3OKFfVc6Kwqo7qNT9yzHEkbcsRyAPeu3evvRp0ZWWlAYOMAIEWcrK2ttZAjXytdXN/eXKgLg0th8UBq0ew5uabADmZEycWjNO6kXkgO6P6BLK+57nJwOSxO1E6QJDUK1vguFI62ywQTao1wYv5ALfWOYFB6knqp++zA2Qo55q91pOZydSKnPL29nYrHsWkAIHuz176Ox01RwpAAVlV1as3cQwKqKcTPitAEr2zUw+Smv79HN+DvN70pjfVq1/96nrPe95Td+7cqa/8yq+sRx55pL70S7/09/zOO97xjvtaix/90R+tz//8z6+qK/n8N//m3/yen/2CL/iCa4/9WueIOV4omTJ5EUJSPiIJBiOjVHm4Rx555Dk00MbGRusokznGquo506Q+nffjdFdWVpoRHxZdPB81Zkzop/H4qtJ0e3u75bYZIsY26SjKJEpI5c8cKwdn4xXTVPVfpGC+IkufTUouaejsq5tKnUBFdTMqjjFjRPJYEGfsyA0jJlrONeN8rVkWUmXFbYIdc3CtrKzU3t5ea7wyjOI4BGyGMTI+/nC8WfTGOIrakpGo6roQmRcnm442QVJGRBmBZbMFkU0eZfI7tHU2HkknYT2rumg687vAUtKbgNDdu3dbD+XLy8va2Nho4HdYRJUG27+NiWzRNcAtAYj1Mt6UC4aYLmALMiLNaDWLk0TQi8XiOUcZ0wFX9ftJpyyRsywcZGc4fhFwFsUl3Q0wGadUmrROAhlzz3VYWVlpvQ84bvuYueq0l9bMONS0mI+5kzdglI1yP+PPo4BqbbAQQOhL5Xrf+95X73//++vd7353bW5u1ubmZn3Jl3xJ/eAP/uDv6Yh/9md/tn7913+9PvVTP/UPvP+/+Bf/ojnif/7P//nv+bnRaPRgHTHhODk56b36ixHc2tpqLew4JShQcw2Rqcv7iG/evNlye85cikYYCJ22kmoy8ePj4+aAq7oGCFXVFOf8/Lzu3LlTVVfKrkUmIa7qcpcadzBEWSABpVZ1rypLo6hKmvEHBChY5ssoFAcIxQMxFxcXLbpMepPCe7ZCmOernM4IMY+VpFO1j6noCQKSyrIG8o4cjXy26C4jmiFQcY+kmodRRhYlkQX7yLlk1O33w0ruNP72jLM2zzTwSXNmhJGgL2lnxh07cHFx0Zu/fTGOZIjsS1X/vb5DoJRRM+BlLq6MsoCura2tXt0DJ+Vn5Mma06EhxeskgJ4BnFmmEsj1kKHisDge8pq5V47x9PS0ptNpVVVzeumwM/XlnpnSwTpxfj6fYEKtBbYij7YBe8Cndw2LZEWidFnHrbOzs/ZiG6DLGm1vb9fl5WWrf7DniqzIsLEmyGXD5HYXi0V7haLOgewUmfd3On3dtpaWluro6KiB3Qd1JTD6o7p+5Vd+pZ588sn2Duiqqle/+tX1gQ98oGazWe+cb9VVQPX2t7+9/u7f/bv14z/+43/g/X/qp36q/fu9733vh2zcrmv1mh4WP3CuKlIpab5uj2BkjuLs7Kxu377dUPf6+nodHBy0KkAGwHM4C5SWezCIHBKjl7ko0a9D7V7GrfI6c3cJIoZ5p5WVlRZxJFLO/NX+/n5DsOYMfHBWKC40tghbzieRO4VkNBlQb27Jau5bt261KG19fb0Zu8xd5jz9jIICP4wbp5LRNefiZ1mYJoK1xqNR91IEzjpZDFWmWUQFqaOhM/LMNU1KNKvR7b99Z4zzuengqvpUflU1SjVpSH8z9hwhmbdmWZhH3oE9n6vqutHZm2QIMg0hWscOAS10D9NkHtYio7ak2jnDTBXk8STrn9FeVRch5vutk94nA55FV5Pdsc+pX/Ynz6pzeulggUL1KCLZvGemcpLZSfreM6wHgJQpl+G88/7AI/YPYFc8OcwxZ3SMHTAOHQTtBx1I+Uw62lotFov2djpRd75rGfOTuXVnsIc246V0HR4e1tbWVu9nnPLBwcFzHPF3f/d318d//MfXJ3/yJ9+XI/5v/+2/3dc4RqNRvf71r7+/Qcd13444jV/SMYyB/FWiy3y3KWOqeX8qmA5d2QpQsYSoaW1trRnm2WzW8oqcNqMlIiGwea4SvZdvV3GlA0zDkIUTolQOTiTOKcuLJyBAmzvqla+HVBw1n89bjpACim6h88yjpZE7OTlpz8x5UEpzRsvLYTO2HJ75cr7WnoEXKSjY2tvba/Te8nLXPW1oVAhnRnJV/c5I5qZpxenpac1msxap5TGqIW3M+DPgaTxzbz2DcRtS7bn/1occiBQZMo43Wyfmu2cZRs6QswPcMhKqqucUFiZ9mzLKwfgdufNMzAAHwyhjjVKm7Yu5Z9448+q5h2Qugdww/z8ETlk4B6zYE/I4mUwaeBjS4/ZFZbyoNYsu8wwxGU661tqZn2jT2JK9GY1GLdo012x647P2imMTvWcxGxvI2Rqbo4Y6dGUR6/DcvO9tbm62N8wBtZmfTvZkyO54IYp0BeD0oK4PR0Rc1bFLf9D167/+6/UjP/Ij9RM/8RP3fe+/8lf+yn19bjQa1f/+3//7vu/rulaO2AYSzNxwygwlV/Wrp0WHSVfOZrP2DmM5XApZVb0cB8FWOZoRmvObDObm5mbrG2s8cpiMu+fIh87n89752HRaqOU7d+60uWmhSbAnk0nt7u42Z1LV76ct8uUkTk5O2jtQ0ck6M+3u7rZxmz/DxSilIuYZRs9Op7m5udkcTkZzIibOGC0o0l1ZWWnHedIoZyMFjoURMdeMlMlGOrrcPyxBRuZVHfU6NPSOUgEzWloCack+ZNU4sJAUOdDi/pw0I4kGBKIyysjjYejmpPSlOfL/WT2fOUX5PpffZ3ToGfbcHDI3no4xawhEk4x+FrBlRG+cHGoatzTyCbSGUai19Dv3JDvT6bSxQVnYZi+2t7d7ziYdnnFJv9gXMsjZnZyctCI/AJ++Yz2SNUl6GFuVNSOiYWu4trZWq6urvfeBY9qAWaDBPl1cXNTR0VGv3/poNKrf+Z3f6b1JbWmpaxREH/1NhxS0ZRGjNU8wjXUE7s2Prj+I68PhiPf29urw8LD3Mycu9vb22s8Wi0V927d9W73pTW+qRx999L7v//73v/9DNdTnve7bEUN8UBq6Y1jIQPEzN+yzLgpH8BT9cCiEnzFiUCmbiwEUNXKwoqssbKjqIpLMGTIifpc5VX+jWZPuyQiiqnr9tNMBmx8HxcFnpDAajdq7SEXz0HRV1+iAclkXaN2/3ZsCA0bZ6CTzo+avkC4jGflPR4UYAFQYWjbzztY7QZpCE20Ks7jLHjImokfzqurAGGfKwNlTcgYgZaFNRrmZa8x/czrJNmQBk/2x32h/TjVBymLRf7NUplM4y6RZ7S+ZV3xU1b1RyR4aV0alCVD8e8gaJIBzf+tt/Y3RfVReJ6XvMr4EEuQzaVw6a12SJjYGDi/p16pqDi9BP5kgcxnppiz4t/qNbIRDB6yVNU5nn7UcZDsZMrUcef6a3cn9tRfAsDmfn583ahlgpEtDucSEOaNPLrP+gF5YWykqzyQ/2K8sTHwpXa95zWvqd37nd2p/f7853ve973310R/90b0z9L/9279dP//zP1+/9mu/Vu9617uqqnuBx3vf+976d//u3z3v/f/v//2/9apXvaqqrl748Htdo9GoPuIjPuLa479Wi0toUiTg50mnLS8vt7NvBIYwMuiMY+Y15vN5i8wIT1V3DCVzMrdu3WpKnHkRY1SkkgYL/ccooF1FklB1VTXKO/+fY0JFmh8ajbE23qwuz4hJrnll5eo9x+4hMsqjHxTTPVzDPKe1TCeYtK49QFFlpMp5qUpPdkNEkQYsKVdjrqoWLWSOkaNlbBmjvB9ZoTB3797t9efGWDhi4z6Ai/W3Lu49jM4wDkl5Wq9MQdjLqo7VACjTmWQkYn/t3TA9k8/LI1CTyaQ2NzfbuBnlpBaHhXPpePP5fs4xupJWzuNsxgg8+Kw5u38CAmALwM11qeq/Kco4zIMcTqfT5+SssWFpU1KHk+Ww9savUQc9GKYQRJQZMdIpDJixcLYcJTCW83Avup0MGyeHffK3Yi4dANnKy8vLJveTyaQODg56IB4wz/PEOZ6lpaVWI0MO5vN5AzjDBibqcDKg+VBfH46I+OM+7uPqta99bb3zne+sb/zGb6ynnnqq3v3ud7fzvm984xvrHe94R33CJ3xC/ezP/mzvu9/xHd9RTzzxxO9LP3/WZ31W/dIv/VJVVX36p3/6c9gi1wOnpvf39xt9gpbjNBjJGzdu9N4rPMwJ6sI0Ho/r8PCwCVlGZ4RU04OdnZ3W8ejOnTvNweUB/IyAGAH0auaqMm/KcUOxnGRGHxQp3yuqsQgWgFHPqEHkyyi7r7XKMan03dzc7PXDzvtQTMaPQ2UcGbYEATl/BTHo3zTEVV3aYRg5MMpJbxE2zILv+ZNv1FGgklWyHIJ6AeuSMmB/0ePj8bgODg5qMpk0A767u9sKMIyR87WHjB0n93xgyxqgorPyF+XI8FdVj31hgK0NJshecKgcDFk9PDysxWLRjLK5SllkNfbQCWfxWTpQn8nokKHn3DMaAjwSgJg3ORWZDmXQ/mU0nXpsfFXdSxs4CLSo3vQAvLV78sknG93MKXKSnpN6zclh6ehzzsPe5nGnLLK0n1l1nzl198JkZDHoYrHo1QeQBY7avsiF2w/ymnqe1fDWTwe9lFvrCAgC12tra3VwcNBjEc2DLAhe7jef+v/S9a53vave+ta31utf//ra2NioL/zCL6wv+qIvqqqrKFaa7Yknnuh979atW7WxsfH7UtVZNf0zP/MzH/KxX4uaduXRHNFD5vkYXIKbCC/pLHTvkE7kSBKVc7wMypBGSpQrT6hIyfiHNHZVtcIpeaXMjxlvRtMMpftUVaPZ9vb2Wp7YM8xpeXm5USA3btxoeU09sNPpMRIuEVnOOSkq9NXm5uZzomQON2niLCji6LK1KEU1z6quUxCjCCyoIpYbH4/H7ShaInFGNOlv90ItZw1BzoFDy/lkD+Gs3E2kKprjnMmP5/h/Ro9p+M0n6VZrqWDIuERs3nBErpMyT8qRTOX9yEp2FssiPUDBs0Ro+cIGoAe7lNFt0swZAYuk8pSC+9kvzpuTyMg389f00f/tgzVN2lczCgBJOiLnlWuPKk6n6t4Z/aaTs/bA0eXlZWNG6EKOy5uzRJr37t3rMT4ZRQPSxijVYF0zNQJ8PF8ltPFy4AC3F+OYw9HRUc/mTKfTnh0GtIzn8PCwrZNxkvEMkj7U14cjIq6qeuKJJ37P9//+6q/+6u/5vb//9//+H3jvl7/85e3fTz75ZJ2dndUv/dIv1VNPPVU3b96sxx9/vF7zmte84Llf633EFJSDZXSgQAZPI408uM7JJTWIysnzfklRofYoF6cpQqYQOlYxgKlgCr3SgBgTZExpqqohaP+mNFkcw3CcnJzU5uZmnZ6e1sHBQQMDL3/5y9saMU4iDuPNM9YUyTMzMjcnyuMVj4wABRYxZL4IJbe2ttY6jqWhZtgyRZD3de+q/vlG+2wtk+plONLBDfNR7pU0bBpcRpVjIX9JQfpdUny5Z9mjW9GT37lXFmZl7i8NbjpSUXT26U4mxBpyMNaPo62qtt/Gkc4elZvd19Kp5TrRI04koyiAk0wDC+6f+efU6Sy4oiN0ydzJ5dCB0U1jyjw123F8fNyeBwwq5LT+9o6sGmdVtR4ECeqqqqfTIkPrRlcTxIrw0+ll/2Vj0k/8/Py8nQc+Pz/vHfnKKnYsh+9nHjdtGiB5cXH15ibgyv3ztIK1ywr+rLI2bnIFNNy9e7cODw97RWzkJFM8D6/rX7/wC79QX/u1X1uTyaS2t7drsbg63/3EE0/Uu971rnrta1977XteKyImFKmgSY35XdLShJPhZoQgaoKH5qbolCsjcfm2NA6UlDKLHkVPKkYtmEjy+Pi4GQL0Kedwenpa+/v7dXFxUZubm00xOFGO+uzsrJ555plmlOfzef3O7/xOj4La3NxsBhUgYKA9UyP3NPzZbCCZgqpqTlcB1Gg0ameLLy4uelRWVbXoO/PtmjRwLGlEzCULsjg7UUICDZFz0pX+ADJyie6dFe9Jm2Y+M9F/gpLV1dXeka2UERQfliQLscgv6tPnOU2ys7y83It0/Fxk4aiZZ4i4jCUrVI0tI9CMmJOyTQo0942zY/SB0awLMM90NFXVSxVwivbNvejKMNrlzJNhsH/k0ToCwoqbMkpkG9bX13vsBBaAY0tddAadc8rKV+NN0Akgsw/0Il8/mGxR5kyBS/KdYDLXw9qRXx0FjTFTbtbKmt6+fbuOjo5ahC3oWF5ermeeeabZvawLmc1mLWUluPG8BGUnJye9FqXYEkDCZR4afDyo68MVEf9RXW9729vqcz/3c+urv/qr29nlo6Oj+t7v/d56y1veUv/+3//7a9/zWm9fYsQoomhTkwxCpEgpjV0aJo5IuX/mlhRqyLuIkNwj0WVSU1nFyPhDwhwOo8NYcuxJdVZdHQDnJJ1FzkISeSHosqo78kAxgI75fN4UTqSkAtL3qrp2hlmBal0SQGSTf2NHK2a0J/LIqEzVZBbPuL/15CQoelZM+zudLYMo+mBsGBrzrKpmIPOIWObhRK7pEI1DtCoPB7iI7jIyt0/Whnxksw9OlMMyTkZb9G2dyH5GzZyR+QwdqTW1X+Y6Ho97DVgAK2tsPtiJjJw9177ZdxGR/RvuV1L/5BlQGlK8GT15VgLtXBcAjHPCQABO5G1jY6PHmOW6ZsEguVWQ6WUqQDO9zrSGs7WOAdIVwIne5zw4JEDUGHRoG4/HDYQDQOwaOc6mOcAyJyjSNc7xeFxbW1vNSadNI8fAAD0Q4Zrvzs5Okz0NgDARWJMEtVkUeXJyUsvLy7W9vd32/uH1wq7f+q3fqm/4hm9oPq3qqnnIX//rf71+6Id+6AXd874dMQVnMPO4QhY8iJyg3iwsIqioFlFZ5u9QqpPJpLa2tpojpBAaf6DTMjKT32GsRaDb29u9XFseR+AwKbHiGeg3j89wknkG2UWZNH4Y5tUyos3mECg7a5FRRzY/8YdhNZ+8b1W171B+lHa+KIDSMkIirqSYoW2NVOw3xmAYFZvndDqtyWRSZ2dX3dM2NzebQeBkRdGibuuQNQHAjDz/0tJSTSaT5vjstYiDcc38r3VMMJeRX0bKnGhV58ST1mPIM5oTeRtTRlrm4l5JpVZ1DUOMLRmMqq7IyWftY0a9Irgh6Mrcs88x5vadDKZj5jjspd+TGXsDJGWELTq2XiJLTi3BknXiINNJVl0BjOwgloxT5qSNlS1JUKKoKoFQ1iaQ/Xy+4rc8++u7VV3BZzo60S07mHKewYNxZgMdsmR96Jjv6XUv189BA3t5JE6knPlx404GMvfgQV0v9Yj4da97Xf3Kr/xKfcInfELv57/2a79WH//xH/+C7nmt40sEtKpqa2ur0TWZ69Xkoqrqscceay0hHX5fXV1tqJAgE0SOjVFIala+pKparimrHNEzOcbpdNorTslCEnQzVLyxsdF+LiIXKaiQZPyrqilIovB0jKPR1UHyxeKqKw5En7lfyD1zXpmnykioqlouU0TFmYv2krbPIjnVgsMiKZ+H6hnlYZSSHa8gcONPNgMDMR6PW5OW3/3d3+05W0UoKmbTaBgT5wJwAHdZoGQuq6urNZvNGqNhbc0d8MNoDA1QOuHsUyzaM173zKp2cpU5w6RxRYmZUgDm0nhzANbTnqBnM1cMRDG8nGC+YABIy2YmGfmaRxZIuThobVg5XM6D/ooGjS/Bnbmenp4+B9xkS1JOgR1x6iKjcrJHHxNIkQV94wFkNiPPz6cTBwTIDHYCuFTFTFfG43GP8XKE02etZQIk+kRXARHA4Ply/+jlGzdu1N7eXu3s7DRgk3pqnwBFNndra6sBEWALqPK9zEM/qOul6IjzbUuve93r6hu+4RvqUz/1U+ujP/qjazQa1Qc+8IF673vfW3/5L//lF3T/a730QU4yKT4KkxWOS0tLtbm5WRsbG61SmKKgFeVbh05Dzkh+khPJAhkRCEoc/UVJGYukURNZo4N15OJszYHz3NzcrOl02hx8VVd9fXR0VLPZrCmTaCKP+ZycnNTe3l7LjSa9mFE5B8PYZQTN6S0vL7cOWZRZFM7oJpVtzzjqzIUOc/2J4PM8omhHxEWB5TP93/gVspgLYOXcovPdyUSg6quq5yBEzsOoMY2dNU15SWdjPzjzLPzipDwrmzRktMvoPx8o8W+Ghy5wLvYpwQoZSkqacVcNrCOUe+TL4rE02Af1FZgnTjsjOXMwV4Yf0BtWD2fEmWyKiBxozvaNaFSyba8UnnlWMzwBkDlweul7nmeNkjXJKnTODGWcqTPBQYIU48n6Aak0903AQheHleI5Ps+x34KOPGLGMdMBqYeklDE0CZbpVBbwsWvmbZz2k61O8OFsMXl+eN3/NXzj0ng8rp/7uZ+rn/u5n+v9/F//63/9+7528fe6rv32JRW4hJyByLcqJWrb2dlp1Fq+gzirIynj6enVSwM0OMjzlVm4oVAC2qTkqDoOQM4nczgcF2OatCqjwUlmv1rzz2M6HCEqFeXudYCLxaJe9apX9YqwgIg8/qLCOJE0B59sgOguadgsyng+9JuRLxCQ0S8nlZFpGnJ9ca0tZ2HdADSUGyXP4jfrsrq6+py3SWUFb1JmHJzP+RkDaD9yT1SQMpaMjf2T/yVzVV1jlMyrZk58a2ur7UeuEaOXkQ8AlXlIz6jqHPEwP0gmqqpX5Jh5PPKRjtpxlr29vVpevjrekq/uJG8Kczg0+5rncTOnnCBFlGhO9j9zxuSWw0iAmPsKEBrX0lL3ej9AmUzQvwRi1o8jksbifDK6ThCbLI8xZXc7e2Vd2Ka8j/VJsJznzNNJ07OM/rM25vz8vAUY2CIpHDJ8dHTUe6MZmQAaF4tF7y1K9DZtCCaOrma++0HmiF+KEfH9vnHpf/2v//WC7n/fjnhnZ6dV7zniIOJlcDkHxUlyjBTIOVfIvqrflg6Ckx+U/8jCrzxm4v4KMvSGzlwLgcs+yhy3sV5eXrboQqFR9rM+OztruUpOU4TMmDFWKysrrYvOxsZGzzEeHh72jtEkTZaOvaozNo6JMa5ZOQtFZx50Pp/XwcFB7e3ttSYUid4ZUHk0e5MAwP+ruq5iUH6en+Z8MpKdzWYtygfAGKeNjY2WU2OoDw4OqqoatcrZigJzLH7n2Mew8ppBtCcZVWRaIh2z7yV96zlJd5MddGne13w44lwfRimfm5/JyFD1ep5Tzeg1ozfAjP4w7MCYOWTeECAlb/n8jOwStKhiJvdkDg0MaBk7lmd9fb3dB4AUMWdqxhjItHFgtKSFMhq2HxnR2z9NPW7evNkYL3bBOqZ+nZ+f12QyaeO7uLhojs53yLniMTLHqdL1TMFwwvTMeiQlrNbFmpDHLAiczWZVVe150lFk4PLysnZ2dpodtjeOMWVtg/XIWpSH1wu7FotF/fZv/3ZvP5966qn6mq/5mvof/+N/XPt+9+2Ib9++/Zw8WlU1p0xxM/eETpZbofTjcffavzw+wOERMn2N79y501B4VjWKLNGlaOGkC7PoRERu7EkVchBQf6Jw0T6A4LscNaVfWVlpHceSis08GWVMOtW8RqNR69yV+UDOeHt7u0dPyyEyeBRf/ng+nzcw4MgPRwbRpyGr6noPi9gUzqHsGQJABiWazqjqiv7f2tpqiD3pSc9QhJJr6JiGvcjIk3O0LuQknRyajlNN2jzpyqouikgqP52zfWYA05FmZXoWNSWwyYKfBDk+U9W94UqhIFpxPp/3Wo5yGNk+dnt7u+0l9mjIHMm3W4dMTQzziVl8Zy5qIYDhNODuVdXR8kCwcWdaiqwBp4AOuXA0DmgDRnwngcpwHvSVDQKKb9261eTez+mAqBijI3q11xw1EERG6FYe2VNsmQ2EfL+qq1fJQrGM3oGVnZ2dXgHWvXv32tHLg4OD3hyeT5YSkBvj2tpazWazXnc4+/YgrhzTS/H6hV/4hfq6r/u6FkRg1aqq/tyf+3Mv6J737YhFQ1V9lLW8fHUshwNF31BiKBea50TQQhwROvPpp5+u3d3dquresSqH6LgTWholmRFwGh3OgyFQCMFo5rGTpHs5hel02juOYe7ZM5tDEJXduHGjdnZ2qqp7bR0DLtrOVzwa+3g8bjnnIfJPqifbZwIcDMPx8XGPbr53715tbW3VxsZGbW5utjW2J1VdVXEax6rumFkW4XAoznwr3LIOzrZWXRXzZS1B1g8wkiJmEcba2lrdunWrFymgX7MAJ1uccj4Z1fo5hiXnCMFm9JbgA9Xo9+5tj+x/5hmBEGOxVsPoM/OwWVTnc4y2/Wa47969W8fHx41SJ/eebc1Fp+kUfSYZEU7Sz7LC2b+TIeEs7LOoz57mMR4OgK7QvWxwwSEP6wGskzlJy5iLcZuTNaJP5qcpUH7e+pPXtBXuk+A2KWlrmumgtGHASsqP8Zi/qByTwL6trKy0ZiGXl5d1cHBQR0dHDRRVVUsPJcsjB5xr7jvSDsmksJ+ZB394vbDr7/29v1df/MVfXJ/xGZ9Rn/3Zn10/+ZM/Wb/8y79cP/mTP1lvfetbX9A9r5WxpzhVXRWkf4siKNXS0lJrsUZZ0phdXl69HDsLdWazWRO6qmq0FMfDEJyenjaUCI2iJbPhgrNzHGVV9xKJqitnwZlU1XNyMqInxsqxGdRaVUftidYyTyPiB07SQGs0ksDk6Oio3VsRh3tytr4/m83q8vKyVYzKjSvmMVfrI3eXRlokwOAx5D6Xz3R5ftKFeZZxc3OzgQU0GVotu6Yl/ZuVscbAyDv7nE4jnX5GaBzmdDpt8sHxZZ6frCW1zYgCeKKmdEaMqL2xPtl1LUFdOkkyUtWdWc5zqZ4BOGA2Mvd/7969euSRR9p8OIGsP2CksQLmTHewO+QvayJ8hq6nXnNidCNfjAJMOi0BWJPftBvyzglQgA3PyZQCUKD+QETIGfpZgnHPTZm17tihqmq1HEP7lmDP2X8vTcFMoNk9f1jVzCkCZktLVwWs5mufcx5YOQ4YMCDnmdsFXNgnKQRj39jYqN3d3ZrP5+0s9rBm4kFdL8UccV4f+MAH6mu+5mvavr3yla+sV77ylfWyl72s/s7f+Tv17ne/+9r3vNbxparuBQYEg9FImnY0GrU3NHFC0KMXPRCybEFIQM7Pz2t/f7+qqlFLHABHfHR01MtRZ/7Fu0IZXMqS1O1oNOpVbRvrI4880hwJFO8z5il/ZUwo68yLUTK9qbEHvkdhvDlFbjMj8KyoNAdOTfP8p59+ujkOAIMjRpMBQEkVcmb2rao7HpWGDtOR0WPmbnd3d5sR4BzltRg20U/m2zKqQv8bP4Pq+YwNQzeMUoA5e55UuXFngVlWppqHl4xUdRQfWfOsXBuGkHyQg6Tg3SPzsKI23dcSaAGUnB5nR3+85QizwchzbFmnkcVdHDawgYkxT3oBNKceZmTPeKvwNn6yZ01Sd5Jml7/1/arOSZKTjPqqqqUiVEMDRMkscDIo33RQxj50ZNi4rI63Fnn8K4EMGbRe9tY92SA0N6eOVqf/1jaLBo0JUMBcpf7RDZF+MkXsQwYy+/v7DTzu7OzU0dFRYxbo8MPr+tf29nY988wz9dhjj9XW1lb95m/+Zr3yla+sV7/61fU//+f/fEH3vG9HPJvNmiOo6l5WzdlQNKibccloMCur07AzXBRCJMWoE2QREiE/OTnp5T0pghwsOi0pZIo7Go1aOT8Dli0Y5VEY9o2NjTo6OmoV4unE/W2OGfkbn7ma43w+7xWcWQPGkzFmkC8vr3royrcP2QkGIudE8TkCBp0SZ9RgjFUdxQcgJX3O0FF4EbTva0RgPAx0Rj2eZ0wicL+zl5mvtW7oRHPJ/sXLy8ut4t76m4eoKnPHPpdyklfS8ehZtQhJP6YzzkgvKe+kjIfRYFX16FkMUh7JygYmQHE6j9xPzo9Ts2bpxLA2xgE4pqNKyp/jynFmVT+nljLjpAQHP2SM0rmoxhZlDNcsC7uMFbhMNsMch1X3ZF7ltmdk34FMWan34Bzz9YVkwX2AUAxD0vpyxwq52CD9FdhSOeuDg4Nee09yxoaKmm/evFknJye1vr7eA1Ge42c+f3h4WJPJpOnN8Pz4h/J6qUfEn/mZn1mf93mfV//pP/2nesMb3lBvetOb6rM/+7Prfe97X73iFa94Qfe8b0c8RLAoOYo1pBghyix8yErYrIzOStyq7n2/2uI5SsAIQY0QtyiBsZ5MJs1wra+v12w2a8ZdRDAajdoxLEZLhTelylxjVYdY0/mi2iiXyFfRyHjcvZeUk6as8oqrq6vt+EJVtaML6HWv/7OG9kDE7NVnVR2dzyFyNPasqtprLAGSPKJS1RW4Zd4umYSlpaXa3t7u5Wo5t7W1tWZwGGZG0c/lKRWDZeMH62O8jBEwZU3NCeOQDtVa5ZySWk7jzZECluQ5jfPQ6BtHVUczWytRn2fmPczJGjLoeSUTYH8Z3vPz88Y2JB2uiIlzyPqIqu7su73MXLZ1lc7Jym+AkNPNCBzwUUw0ZAlSp3Nf2BH3SpYiHT+5qepy5elQM+3CodE76yHyzOI7TtKapp4bc+5P1dVpBw1Ucp6emwwXIGWdAKCzs7M6PDxsOn90dNRAh3Wih6enp62jYJ46SRCXAQSdMWd/pNIAZz/PCPtBXC91R/w3/+bfrI/+6I+u9fX1+uZv/uZ629veVj/8wz9cTz75ZP3Df/gPX9A979sR5/tvoWpRJ0qJYqA+5AQJ+vLycg9ZckLyWiLINHyiT4VHosaMnhhRjkzRyXzetdRkPKHBrOoWsUHCerrKA3Eq6GMAQR4sz2OmgAMLjEZSk6IcqBuQcCbU3OTerIn7oDI5HKkAc1LEZm84P3lJuaP9/f2m7NkaE81a1b0AIis8Mwee+VYG0/EioCmBCadrzmSIAwUQ/J7xldMTOWVkLzLJ3F1272Jgk6K3LwBGFtclpZx0uN9hCeyRdQNOjSkNrbkzrPmOa7JJboA/e673cgJSka48rOgodWFjY6MZ45yHgkSUus9ngROwZB2q6jkOi/6kE6YHfpZAiuwnS3V6etpas1Z1r9as6o48AlD5rmrrk21y8xoWv3FUZAaAdrKA/EsVVVV7ny9mzl7Y/3v37rUxZl5f9CpCtXbkLIsKpZoyxQMECQqsAdbL+uYarq+vN5sD+F1cXNTt27ebTqHGre/D64Vdn/M5n1NVV/r1j/7RP/pD3+++HfGjjz7aK3yx+egdCom6Zig1TldENZ/P29s/OAjGIQ0mJUZ7ZURC4SaTSVVdgYSqatQdQ3t+ft7e2ckpV10p+vb2dlNAjp0zQRdTEo4F7asRSR554Giquvcnm1tVdzSG8QJoODjKmg7R95eWllrfbWspMjaejIbQsIqWRKZ+nzlwldIMQ9LeifjtbYKEbNYh6mdEco4ccdJ2+X8OJpsNMCYZzZkbh5gpCHubeXD3YYAYwqxITYPOmPs+40WWh7SuNc8c8MXFVaMN6QaRrPF4TuY8qzqnwVklBWw/qzpHSfbInDV1L9+zNsOcYN5T9KaL1pAWVtch2qyqXmTrjD7GK/dgWKwmIhxS/sO+AZ6beu3PMD/KubNL5MrnPCtBmD33XM/IN7plJK4RjD1YLBbtqJ2+CXfv3m3Bx/OlYIxPoaW5n5+fN+cKBJAl46qqdpLj8PCwMQGOuJFdkW/eG0DIgrYhaPlQXtb3pXpdXFzUv/pX/6p+9md/tvc+4j//5/98fd7nfd4Lmv+1ji9ljgkyy/cMM16ZC7m8vKrshdAYIgqv/SFEmQonCk/6cj6f9+iWpaWl2tnZqclk0sYmF4giq+peB+dNLY4Yob1FOQQ3o7W1tbVef93Foqu0zcj/4uKivV6R4c0mHZA8RcuoMunWjCqqqilbGiYRfOYhswDKHrzsZS9rRiDpVsqrmtPeZX55SGllhJEFKkDIYrFoXZ+yP7PnMGhpfN2LEeIwkmbLHC5Zk1Pc2tpqDIDPmRvHRBbci7xlFJwgoarL77qf9cloA1Bk6DM6ns1mLdLmxOXTE3BWVQ+MiqiTgs0cabIzzsAOwRwHkjRkFgw5yqfOAiUtrzvMbdMRe5xFVeo4kiqVEsjPZnokHQxHx2Fnrth9gBLyaX3cLxkH6536ZD/JIfCY6TKRqGe4v9z2EGwpzrReyXRh6YAB4BeTxY4AUtYdg8SWZL1JnjfHakkBipZPT6/eiw7osYfqezCRWWD28Lr+9fa3v71+5md+pj7zMz+z/sJf+Au1WCzqN37jN+qf/JN/Ur/6q79ab3nLW659z2sfX0KpVnVFNfKjFCTPO+q1zEERSJ2sqrrmGNr3idZUljIMcj2MCUpV3kxBE3SKgoEy0xglWq7qcm7+QJyEOCMbFNAHP/jBlpMVQWeRhu8xmHI+olmVkfkO1DTUupgxGBoe5AXVWyOGBdhB0TLIKMnFoissS1pvSDumwc32hj7vbwBG9C5ySopZbr2qM3AMin9n/++qrl+2sWexGTnQHEYEmnle/+Zw8gxzdoUyF52TUnYY9ay8XlpaqvX19V43uGQLGHIvTqAPHELKPllj5BndLO7zMyBRlXlGh7PZrIFC7I37DSu3OSxvy2KoOVzrsr6+3juDDFwCBgCN5+3s7DSZM192gZMBTDJHzblloSZH6hn22D3JJmdl3By39SWDw0jQ/juFAVDQZ/K6sbHRWAO1Eyrz2YWsd6jqd83iPNMOKJpik3T1kj6in+aJQVwsrl6HSAa3trZaeuvOnTut7WU2X7L2GxsbjUVyrvlBXC/1HPFP//RP1w/90A/VR33UR/V+/oVf+IX1RV/0RQ/WETOW8qJV1c7Yaf2W0bECJAqVFGYWLHEEhCyPIihckEuGGlFClIZxyEIVqJ/SUNJsLD+bzRqokIdJgwNkiKIZFKh8Z2enKVOCBEZh6LCACcqJ0vO8dHoUUSFMVfemIdFMjndvb6/3yki0M7ABnUPFOWcRGQPheaPRqJ19rOqKj/xNDoCJLIhKpC/3P6SHs9jGnjGkAJvnWTNjTqotK8SBxcxDMp6iYREXuc7csDG7B9ownU9GiQksyJVxZ94UwMvvWGNOicOzDu6V50oT7KHnPSsdbo6JbAGbdJRM+kzmeumkwqdcsxw7QEK2kuEwxqSPk4nAnkyn0+Yw5vOuhShADywnrcpJJWsAOA+j8azKz+eQMdE2gORn7BinRc40mVGTwDYZj8ItBZGAYLJ1SfEDxFmMl2PKXHzWH+glXdW9kY6tI6dkMlkkNuLh9cKu1dXVevnLX/6cnz/55JNt7a973bcjFlmITCguRKhaNp0h4RmNRr23GDFGUHhWUHK8IgVCjoJUyHJ0dNTyaRStqotG0qFVVWsNyfkxrFn5zUBwdJmjy0iA0C8vL9ft27db7lbk6viJ75qnuTgOcXh42KLW7JRlPJRcNJs5URF+7klGp/YlK18BhLwoOyTOOGE+ct6Zp1ehmtF/VbVxcZbWOh0Q2cl8bFXn4I03GQqGk9O8cePqVXEMZ9KHQIqoljwMi8DIR+Zl09ExgpyCn1VVr2Ato/d8haPPJj1rLs+nX/Y+ozeRUDojRYWZ2pnP581JLC8v93p6Z9onwYRILoswMxXgO1X9TmTZMIROZ/tE0TsdtKYZ/dO9LBriVMmBvXFfY8G6Yc7oLqCRKQpzzcjZvemGfTo/P29AXVRqTSaTSY8RANbYg+3t7V7B2/7+ftP5s7OzBuboBSrZnEXUFxcXNZlM2r6zR/TNetozAFi0i8FL+wpQiMzp+YO6XuoR8dd//dfXd3zHd9TXf/3X1+3bt6vqqrL+u77ru+rrv/7rX9A9r5UjZvDQNRyWn6WQXF527/F9+umnmxA4G0kQGD0CQgkdX9rc3GxFXgwn458NQTTDQDUZs444hHVjY6NFo8PIgNF2Vjgda+a95/N5Q7EMi6INDpCRNib9p5M2ZSjToQEb6+vrvRxydkJKRcxX46VBFq0cHR3Vo48+2p6VgCWPfHE6VZ0jZ2x9rqo7b8yoZDN665hVvVVdTjkNRDIc5MD9M/9nvROwmaNnAQYi58wBWy/1DENammyQO6xJsjRV1ZggoCjzgkmRcsqeM6RIM+fHwfpeHuFKELC5udl72UBWEIvWOH+ykNEuJ5ZUcDrfzc3NOjo6aikSxt360wNjyrO3yfoAItIMp6enrdMa/cjo1dopHvQ8KYGsfcg9Xyyu3k5UdWUArRHdodOZM7bWZBew9JISQEvaydySKQC6yIr7LC1dvZzCz+lTHl0yfqDVc6RjAHbppXT60gOedffu3day1iVSRu2jn81rc3Ozl3tPiv7h9Qdfn/Ipn9L7/2QyqR/5kR9pAaKTD4899lh9/ud//rXvf61zxKgPhtn7eiEv3YnkZDmJdACoIxejrViCseEI0zgx/pxvGkU0UBpSERHDlM6fslF2hhrFt1hcvQKvqotoGDIGbmiM8+B+VTVHI0JISpli+t3KykorsGDoMseZRj6j4KqrhvKMc1KukDuHKNrKqCsLZkQj2djE/PJMaxa+MHL2CXoX6TK+uX8ZoQ+LbjKSZEwBm9XV1XbMI49a2WNsAUPvGUlJKgAa5pkZUfdNx2qMHHY6nmSIgJ0sFBrmt40p554AL5ka650XIHvr1q3WSIeTTjpWtCkVwAFmdOiNYtPptAcoMy+/tHR1Zryq68dMB62BSE6EDHxUVYsK7WPmcpO2xpAkmEswhfGxTuyLngFV1Xrep+Pl3ICirE5fWlpq5/etNZmwVvP5vJ2YEJHeu3ev1adgMZIFBDRHo6sOg+h2uWD1LYALQD2ZTFqFMzsyHndn05O9c0/OPI/qJZtiTfMVoVnD8CCul2JE/OY3v/mB3v++HTEjBOllRWrVlZLKCVdVi6jkrRgj510zh+T/HJBoCd24sbHRKziqqmacIT4GhlJRWnlT0XxVR1VlXvjZZ5/tVQXL8zCwS0vdK9eMM41Cfi6VKM+NurJYg+FDTyU9zSilMcpIgkMAgHyW8llHNJy3WTG0kLr1r+qffeak8ogMoICyM/40SujCqs5ZkZ2sHs6xMhIiiHSEoqR0JtbM77NbkHslgElgxbglpQ74+FlSsoxbMg1SBRmBi0rJBYOUjt/96FRW3dpzz8HQ0DVGdz6ft+jeUTYRHtnL0wuOHaVjTnrengIryWhYO7JmXdNJ0q9MfViPzFOSGYAo9dYY0NjZ+tP9Mr9N57a3txvVC4hldfeQibDeuScKpZJ9AeazUQdHeH5+dfZ7c3OzyTGdlE4yXmM1vwQHKUvZ0jLBZ6YhEpxZ16ypcXbfvdnYpOXVczzI66XoiP/iX/yLz/nZ+fl5Pf300zUajerxxx9vvu+FXPftiAlAVr7euHGjtra2WhER5yQ/RRiz/aUeyRmtMD4omp2dnfZ7kTHqhaJUdf1iZ7NZPfLII+3nhE/LOAKttF9zekjenJaXr14Yrhisqm/UdcrJPK1nMlgcG1r++Pi40XqiGeiU8ctmF9ZQkwDjwwLkvCkVp5t5PkyE6FeEMaRys6DF2qG3srJVNDA0tNk2kkHKCDGdb/4sAYAxkoWq7rzs6elpK1Bz/EuDF4wD4+P+AGCCAc7I/uczEygl2Msoyf3N335XdWdqM7oTscizP1/Bke9yPPY6KWAswdnZWQMoAGdVNd3ScIJOKTLLc9KiygQAnKLxyJMqRMrGIsCHfeDcXc5Osw103xiSySL7WDbP0JSEMU/jlnT5+vp60yeylBG3+2WOPHPt2ChyzTF77snJSQ/wccSYQbp37969Vj3vvkAnm7i+vt5rNGPdsulL6gJQYA7WESgAZgQn29vbTc6wFSJv9SyapjiVkqzkw+t612QyqW/91m+t97znPW2fbt68WZ/5mZ9Zb33rW19Q+9D73g35DQa/qnN4DB0jf3Bw0D5HsCA9zTAojGpCeVF9nSkgw7i+vt6cmOeL+Kq6d2wuFova29ur6XRa+/v7DRVqKcdJoHO3t7dbHub5Cn8Yx6pqoABaTaUDJhaLq/eGnpyc1P7+fjtqoEc0h15VzZhQUtSlavGqas9D8ydiPjw87EWiijUYPlGDbmYUX7Qgh4fSy44+aYRS2AATaL6qes4pi/ESPFVVzxhaq4wcrGVW5luDjCS8H5kBzOMuWSwlsiWDGYlnE5Sq6hnDjNatn0uKw73R5MZpzZKxAWgyejcGMsZBGqsx+b+o0/qo5Pf9tbW1ZmRzL7a3t3vn3BOcGLNiyHSA29vbrbsaZ0/Wsk7B+LxJLdMgZNB+Ok5Dxjw/9x74wWZ5ljxzMgaA+vHxcZ2enjbwaP1Q3PSN/bLO1sy6pVyjg3Oso9GovbFNxbc3HLkcJUJpc/iZJgKeAeg8CcFxXl5e1u7ubo81ItdyvcAatuzs7KwBf418FGECzIDgg6yaHjKAL7Xr277t2+qZZ56p7/qu76pXvepVVVX1f/7P/6nv+Z7vqe/8zu+sb/7mb772Pa9VNU0h5es4VcjQ8aWq7kyvqJERglCfj870eY0r0Kcigxs3brQiDXQr4WSgkraFuhWnQK9yMyJVEQVlEGHl3JP6phyM8LB4Aw0n8mVcFI4lRef8qWIuF+Ocr1/jvKxL0mtDOnuxWPQofQUFSYcNaWcpB3vlMyIy87a+CUYyzZBVwtYvHfewYMfnMgpKyvf5xsQgJ7NiPVHhzmGj/MjLMG+cjs86pnOQD8z9TLmT5jDenAs55liq+rnWITWeZ5Xd1zOtm4gfYAAwvBSk6grkZVSX6Qt7YK2ruvaLnJBoenNzs8l47kvmu8mIymJzyrmZA/p5KOvSHCL5bAxjrMabUWwCNPucKRrFUPk8LEDaoaWlpdbbgB3JHKtctxy0/Z3NZvXH/tgfa+uUfecPDw8bu0c+5Ji3trZ6BY9YMfZKKm+xuOq5YGwARJ7vJo8YPuvueJNoXe2B+Ty8Xtj1X//rf62f/umfrr29vfazj/iIj6jXvOY19YVf+IUP1hGjOpPmS6U4PT2tyWTSoqDMJ6+srLQD8yr5UoFFB7du3WrVgJk7zDwkY7K8vNyQbBaCVFWvcEsxhoIRwry5udmajYxGo/YuYEVTngedukRDacgoPWOqk5hnoinlgsydk7h3717t7Ow0tG7+s9msVcGKihnvrGR2vvb5HBslzGINClvV0al59IfTUtDDICT9ngaQcWZgARoAytncYR4yK7PT8WZ0ioKVM6zqcq6epRrfWlhz42Dgss7BfbE41itp2nQAmZtDifq8S952KNdV1QNdCYIS3HFEwAHDmfNhROlE5jzlIDNdkpG98XIICdzSmdkf+uiznAPKk+z4Por5xo0bvReEYB6yiUlS7KI9son9yXPR2Rwk89s+k7pqz4E16z/Md9t/+rS6ulpHR0ftGWzS+vp63b17t+moiuQEilgJfeirqjERuZ/2yvcEH9PptI6OjnprJ/AAoMmJ1J+CU/pdVa2wbjqd1u7ubqP7pdzOzs5qOp0+LNb6Q1xs5PDShviFXPftiBeLRR0eHjYhY+TkOjIvkRQbB5fOJw1SotuknkSAJu48HmVmwJOPz0IvhkTuhHKenp7W9vZ2j2JP+jVzT/5QdGiV42AIM8JStCZqpUDGkG/n8XkOzLpwOihI1Fvmd3d2dnqUZ76Nxxw4lWEnLU7E+jCWCbYYfLlqv8/qUGuW+4Dd8B0Oz/wZXQ4jC6AAqozyyQIqGdWXERRHkHvC4QMKxpW5WDJjnzAFCe6GkT+aNcECB4ktyq5i1o0jyiKd7NcsqnFPXeU4Ds9U6EcmMwdsjo7koJON3b2BZc8DzDiyBBm5znSRjPo3GQIaMxeb9oP+ZrOZmzdvtiYWiiOlAvLoDWBnL82bLuX+JEOSwNHeZepExC24ePbZZ+vw8LClPObzee3u7tbe3l5jtR555JHentn3pO2z8vn4+LiOjo5aYVnKOxldXV3tpUCAF6c5FOotFotm0wQMaRezRmA6ndbW1lZz+BjB6XT6MEf8h7g+8RM/sd7+9rfX3/pbf6tFxfv7+/Wd3/md9drXvvYF3fO+d4NiEjhl+dkS0efSkK6srLTiqKpqZxUp8vb2dkOoDIHjFInmCZbzv4nS5ZCqqjl+DjRpMhFKOms/15FGftg9GEsIHMU4HJ+/rUFVvyd05oMYZZQ0p8ZYyStWXRlOBsqfzE16xygjkxFZGiZGg0NDcVmjdKb2EJ3P+QMXoqfM8aIA0Y8Z6dqvzGkyjgrgMkqp6r8uMN+gJVrLyELDhIyKRqNRO9NKZkVixsJJp7N1vzTgos+MxJISRH1yUMMiL8Yy6w7yOVkVay0uLy/r8PCwOTkymblmzs8zs2pcu8us4AUEMiUk6gYyGfTMAVu3dCAAGlnN/Di5BbZGo6u88J07d5psYFyMHZ2bxw05KPfDxnHEnkfmyVzS0oDEsDBPROj+5g5YcILqVczP8c0bN260I5f0W2TtfpgkR8SSyZJDzvEIFppxjnsNjy5l2sqRUPqkOMxaLC1dtcK0xp7/oK6XekT8rd/6rfXVX/3V9frXv76dxJhMJvVRH/VR9c/+2T97Qfe8FixivDIyOzk5qcPDw2a0sugI8vWmIkq4sbHRopiqroKRgRhWwoqiGDwRUVJzaNCMOiBTDoQCcwgunWycf06jzWFnZTCFlQdOyjPp0yxkoygcZt7PXCgSZ8bxLy8v12Qy6eXqRGKZ1wQqGLB0hAyYuQNJqTRpyI3dczMlkFR8Ai5K7+d+Zz/SiSW1715V1fssCtB9V1ZWWo4rq2FR9mj0ZFaMwZoyeOaZdHhSpRlVa7pinpn/JptJr+baYBk8Iyl8AAtTIZoU4eiNjY4kh1Vd/YQ1pDfkRc1F7qexAcPmn46K7uW/06G6R7YYNZ6qruOYaNI++2zmwau6gkLrnuAYsMzxZ/429c2aZ2pjmDOt6ncwy1SAhkPpZOmttBD7oPe0OhhFUtayqpoeq4tJIMUhJwuW6a+0S+xYgg52l84O00p0YGVlpTXcWVlZqclk0svDP7xe2PX444/Xv/23/7be//7312/91m/V2dlZvfKVr3zB0XDVNY8vLRaLdrZR0cHR0VGrQEYf60x1fn7eazwuKnRBrWdnZy0yFBlUVYve3Ov09LQ13aiqXn6JEqiEZBwZtaSRGITMMVMyRws4xqp+/2ACLjKgCKIhxjXz25mLky82N0YetexoVxqZpJk3Njbad7OaNh2MyCDz9Qx8Oh9RVVWXd/Vvx83ko5JOz4Ip0RnDmMdxGGPO2XetC4PLuFvfpIRV86LmVb4yoADW0tJS7xhJ0sgcmvlmHjQdp/1JR54AJvN0rqRh/TyjDVSr+1k71Gqe1wV85EitmyN/7j+k5+/du9ecw/n5eavVSIfLkQ0dUJ6lNh/66vkcCh3TDyBl1Ng8jw7L39+8ebMeffTRHkNEroyJM+I88rhQ7hHHNixuGr4hDnOUOpgMCOBjn4wHu5JpsKTy034Bp7ne1jTBP6YDS2N/PbuqerKMjbNuHHvVVS0C/co9c64Zy0in1S4AQ05IPKjrpR4Rf9VXfVV9z/d8T33sx35sfezHfuyH5J7XcsQiVBcBd/wmEfJ8Pu+dQaTck8mkGWqGu6pai0t07bCz0Pn5ed25c6ehOehPLgZF5vgTClikmPncjMpE64Ra9GW+6exRUai8pPkYDwrAeWRUIloxJwbOHKyFSJcBqKpWfSniQDPNZrO2pjs7O61K3Tgyl2neUDZn4HeZO1OF61kcgggmaWm/y0YQnBajy7BxRGQno0tO2R6QJ4bTmue/of6qagaQAcqzt4xYgoPMCWeqg3NmaOX1Mu9vTu5HzsmNdbWXSXcPX15BtgBdl2fag4y8FUyRi+wmpwOZqmN1CRkVp7E0xzzSlX296Z/5kQFr5o1C1o9jUkCkXiPTAYoyrY35KnQE/Okg++JeSZcbl/FKp2QtBz3OOXGs5jEajdo7yjMd5LNARh4tTNlPwE1mfW5jY6OtW9YxiHaBIkeOMCbsHHtBL7yadXV1tQ4PD5sD9pIWAC37B9ARgc2DuoaA5aV2/fZv/3b98i//cr3mNa/5kN3z2u8jzoIO17DIRhRBQVwqppOaZDwS3TF4iomm02mLUinoeDxuVYxZkar5OkfIsHMsSvqzAIVjqOoKRlJxoe6Tk5OaTCYNeKQjFilUdS81SKeTtCdFNZ/szzvMAy0vLzejurW11SI9uTR9sxkMOS0KmHn98XjcqjoZ5oyIGZbT09MWxdlbVGkyEElvMqj+zTllDpSs2MMELhgIxkwOMdMg+X5WMpOU+DCtkNGfdpDJHmAmOEHjzTSLz47HV686xFgAAKIY35U6yTRJNhdJ5wI4JB1vT+yjMThn74jg5WX3er6MrOwxsJvHYjBS9iYdUDpQ+2MO9slnkm1I5mdlZaVV+icITF1PWpljXl9fb68g3NraahRqdtOTmwUGU15dCRqsZ+bVc8+NnX1QxzIej2t7e7tGo6uGHEntZmU+J6rQLFMn+k4n4+PK5iWiVE4cZc8Bi7ABKTJJ/1Hjt2/f7qWn2CFRt2LaLIxMFuThdb3rz/yZP1Nf93VfV3/iT/yJevnLX/6cwre/8Tf+xrXvea3jS6KIdD6MbkYRS0tLLZ+rmKGqO1pycXFRh4eHLVrkdH2GI3dxVopZCLheuaqIgYDl5eXWzpHzE7VqYCFiItAKSOSGIH/ULlp8Pp/Xs88+217wkEUgqEeGmJOEdLXbZIST/uQwIPvxeNyoc4Y8148Bevrpp1vBFsDh85yI9bQGxmrcgAbHfnR01Mu3+Z0IgcP0J5+XxrCqy6F6TlVHX+YRlXSweRwraf5hBH1xcdEMYVbUoharug5YnEmmVtJA+m5GztZZNetsNqvDw8Mm38ZUdQUys1rdWmRREpmVnkh9sr9ediINwRlxWtIYt2/f7hXdkOFca3Rq5i4Zcn8SeAxrMjKawsZgUjglaSUyr0rYZzi9qn7xUe5B1i2cnp72qvxns1kvigdWzBl17f5sAFDgc+4vOs46DLLL4S4Wi9rd3a3d3d1evUlG5eaSxzfJlnU8Pz+vra2tpgtZx5G9ujnLjY2NBqqxGcNU3s2bN+vo6KixaCcnJ/Xkk0/W0tJVMxAOd3V1tXWQIz/sgIKtB3W91KnpX/zFX6wnn3yy7ty5U3fu3On97oXO/b4dceZIKK0HZ/s2uavMzcjRokoWi0WrumQECLuLAGWvaDS4qICyU1rRycrKSj3yyCN1cXHROx+ciD8Nrvuo2mScGDlzUwXJ2FB2ypKRaL6YXU5Ini9RMuSfx7AY2CHNm45usVi0XDiUzyikYc9uSxwSytnZ5qrOYWXzFUprj2azWRsb8JS0vPXPqJNMDHPMPpc0J4bg7OysVwBoznJmXulmjfO8cha2eI4ri5oY3qrqUaloUmxIvgmoqisuGjoue3Tz5lVjf9GNeWJGkhkACEWqCVbI0erqai9PmiyH/DlZVbluLNY36wHMmWyJbK1DVjUn4MMmuJL9MKcEvUm7W296YQ8zZ00es0PeeDyura2tRnHnS1FEnwlA1ZHkUR9rbmxV1ZxV7kXqnsI4dQlSA/4MmQX2LVlB+2F8xkFeAUafRW9nrtp+ZdFiAmh2K4ENUJH5Zmft0d95yuThdb3rt37rt+rTP/3Ta3l5uf7sn/2z9fjjj39I7nut40sMhUjU2TeFRpShqlpkmf2hk/KsqmZ8RH0MOppqaWmpVfcxxo5rbGxstGjY79E7Ozs77bOi4rxEWIQ9nRJkiRLLZhaQuQP4s9msReLmXNU1b3BfNJe+1xBrGgD51MzvcS6cFIcjary8vGwdfTKvyxCnk6iq1nJPlCcC8B15QYCCc3KffHsVg+B9nIyvaBZA8LssTiEPnu0z5svAYUcYGg5peXm5te1M2fA5a+lZGUHbT7m4dC4Am/dLZxqG8U06NB1MUrrD3J/f+wynylGIhEWpjDj5ECFyuqJTed0sTqzq3iCVgITc2D8yyikYJ7k3h2FOXFoji+98h25kVCoSE71my9OqfnOPYTohjyNl/QnAQeaTvTk/P29snP21D+7HhiTw8dxkz9iQjKTJtfW1VtI3aUOsjy5Y1l4eOkFwOmPAzT6zF3rlD5kbTjbPydsTkTpQgWF4kNdLNSL++Z//+fqKr/iKeuyxx2o+n9c/+Af/oH7gB37gD1Ut7bpWr+ksctDBRbOJ4+PjpmSMFQSXaBJCZIQIPSPu/Nu9e/faO0YpLgqsqhqa1M3G2ORAfYcjpkAo8Oyb7PJv0TMDwpFkAVhVtbZz8tIaD1iDtbW12t3dbd9L5ZXfOTs7axQSJ2Nuqs6fr2Csqv/CBMUwVdWK3hStDaMa987IAKJHMaZjRmUBVlkExcDJ/WFAAByRBUOXFLT55PEghjKNlIgCus9q2nTmeR45WQYG1v3S6TB65pP/H9KSZOPy8rJms1krwJETzFzexcVFO6ZHZ4YROCNf1UWo9MM4ydrS0lLrxY6doT9ZwV7VByTWN50kx4Em970E0+SN88oe5JxAOnFOI2sIgJ35fN6iWXqWhZPuiQVA3QNrvqMgLan/rPUwjqxn8b1MowBZolOg0t5m8xhrrFuf/+cau8d0Oq27d+/W9vZ2bW1tNar62WefbeAVI0AmAA6ABCOQJ0Wy2t5aoqnpPMYKYJxMJu3/JycnzUZrEGTPH173f/2Tf/JP6uu+7uvqS7/0S6uq6vu///vrne98Z/3AD/zAH/re9+2I19fXmzFhNLPCj6FSjECQ9a2lCBRyc3OzvcihqnvPKseeRhvNSslXV1cbItY5BsJ0HwZdJMW4MugiDoKMNk8q0n2g0I2NjUYRUXjOinCLVEVPBwcHLYcLcMhlOYLAaMhdMRj5uzzCU1U95+X3WXDC2GTDe4bTs6F0c7IXwzyc8aIsRRSiZE4ABZgGECjwXQaJIcwoL5+H7SBboiBRnuNuwwLCBErZjGVIQ2ZFfzq7y8vLtpfWwz0Z/fm8q/qVM6zqahmAIuwC452UYgKNYQ7VuAAlR8nsQ1VX9OWZWafBYXCGIkL7akyZsiAfWbhmrYESuVEyakzmhAHiKEV56XTZC2NIYMipk2v3RkcD0pw1J537o42jsaO1M6qm81nkxvlbM/tFvrKwL6uxfd/+ocXp0/Lycm1tbTUwD2QBRDkWjUTIu3tk3QjHnW+1yrXKQEUqie4m4H6QjvilGhH/6q/+an3/939/+/9f+kt/qb73e7/3Q3Lv+3bEKfTn592bf+T/KI/f5/EkjhKSz8YKd+/ebSX3HBuHyiAQsswrqpBmnLNrEOo2owL099nZWaPN5AkZIM4zjae8IcUxHxSznrOJstPwZ9QjSk2DozNLRiaMD8WG6OWlFXxkVyBRIoMqzzUESoyDaI+SuydDDLjYM9Ha5eVliwRFzXm8hxFLA6L63f+te77isqo7OiYKq+peE5gUIGOfRWiMM0efspqV6fZfBJQ5UvKblcx55CrpXc9mMOWU7WFG2eaXxtfvq6oHADlIMmVffD9rKuwnQCOaTcOcLE8WPFk/LJbf25/M/yZoAETogPslu0RX6XKO2bqnExKB5/3SyfkZ4OpeSbvTfb9LkI1FSLAnX43OBTg4O5S68ZE7sgkkp91JSj7lSHc053nJSRb2mUO+9coaTqfTWltbq9ls1vZ4aWmp7ty504r72GL2CmC2HsfHx41RyAKwh9f9XwIQl2O2H4rrvh3x/v5+r6k75WJE/D/7JFNsBm9IJVHyfPWXw+acImNV1RWMZQTMaRLSw8PDHjWFmhW5Edo805cGIwuMoGRIeTKZtMbwec4yjwsYN+eDAeAERM0Z8XNI8j17e3sNKKThz8KvnZ2dXoET54IyFY2NRldnM9P5UHARYzqZdDYcdkZ38thkIM9L20OGzr0vLy9b1M1RundSgVVd9JoRus8BZuaR+ciq7g1G9jeLzBgvBj/nm/k2z09mhQNK2tb3MtpVTZ17ksVHxp5FPpkO8FYejg/AcWSKcc+jNJylcWAU/B7YTRBA/qw52RgWDNGJzLOOx935YJS1BhLWzdqj/o0jaWa/A/A8g4y40uYkFWvN/Qxo9H33UzjHdnCYJycnjQVLqtuc5VITuCRFfHZ21vSbo/ayFA5QUaHxm1eyQdgp+oO1k+uWX7bHy8vLrQsYW6G6Ph3FzZs3m22u6t4FnUVbD+p6qUbED/K61msQ/UGLVnXnbvM4hPfb5jEjBUKEaT6f1zPPPNOUV/EXIc4cXdK0N2/ebE734OCgGSUo+uLiovV1zQpShliByu7u7nPyYRwu5JjU4GKxaBHpeDxuvamPjo6aUVHElUaT4RqNRg2Ze+Z0Om0FSVWdAKNhl5auXi03XMeNjY0exYXKZVQoMqPPSOmde+PGjQZI8hIFiPKMhRF2lrOqO3aRfaD9nDxUdcc7OMDcL+MDMJIyBdaS7pQ7d5+MvAEvRphBBjgYPd/LnLRoZ1hoJXoRBWIqGE8/B8iGTEhGaAwiIJUOxzlPuWdOQzqI03d8DUh0D7qRkSDw60QDijijIfKj8C0ZGQCVE0rK2H5a08lkUhsbG+3dxxnBV3U1Fwl+PD9BlroFuWZzG8pGOlv3IXvyrH6WVc/Abb5/ONMpVdXOKut5kG9Ec3m+kwP2gkzQfWt5ft414Mj2nn6XUX0yGuQe+Njc3GxriHY+Pz+vZ599tse0+DfwgrUS1KR+PYhraFdeKtd8Pq8f/uEf7gHF5/vZF3zBF1z73teipil65lmyaKSqOxOcUdQwCiDYIqGk9pK6Yjh8l+BB5NAh5OeoktxQFiAxrN4Q5SiR/FtVV1VMkIwPpY3mTEpM7tP981xjRkTWRDEa56yoSPTD4WjkXtXl8BQrARAZ/XES6HhtDjM/Zx+BJk7X2qLpsAcZ8SbNmUpvbI4Tieqt7TDaBRzIQBZz5WfsP4MLsDi/nVSo3+WRlKSlOaLT09PeC98ZdlXSjtsANMATGbL3xm6N3IdBRitbp3QeWYyW7IR1zRx3rjmwKLWQNL9oHVNU1QGoHENG6lXVHB/gloDi7t27dXx83DtiReesMd0FvIbjTZZgSMGaXzoh+dlMVSSdnMVV7p8V2/QX8PE7IFKhH13Vjztz4mxNyizwLM2yWFydMyabZIbc5HeyclwUO0wr0OfMBQMeCTjJG1vhfsfHx7W9vV3j8bgxT+6f1eF+bo4Pr+tdjz32WH3P93zP7/uz0Wj0YB0xx+oizPnydcZKgQ4HvL+/3yLlqi5no0c1h505ReeHHYjPgg5CzSlvbW21oqzDw8NmGLIQiAJnBMcAcbKcSL6mLQU4wQhqi/HjNFdXV9v51uHZP8cq3FvkwaFXdVEEB4a6UyjCiKLGKaXvnZ+ft7Gdnp7WE0880WuTyVAkZSjCM0/IPVv5pTL7bu5b5sg3Nzebw0PJ5TPTaQEP7qsgjUPKJgkaZgA8eY40nSfQUVXtfliGzC1m0QoqMOXd2iQtbR4q2u2FOebxI6+hAyRu3brV5O/o6KgBohyH8/XGmAVNok2ypRoWSON4OFOyj3UC3sg1x2ddrPnx8XEdHBy0RiYiqyH124zIcv98PiBsrzgWgCSBe6YHsvAs2+bai2zQA8yZH9aDPGG2OHj3AFSSbub8jI3jTJ3a2NhoAIAec/jk11qQAek4wUQevyObfo5Zsxf2k8xjHthU68nZpu0kO/YvUxVDuX8Q10uVmn7ve9/7wO593444Cy6Spslzbnofe8cno4DuI2QcaVXXUF0RTSLw2WxWt2/fbkJ3fHzcjKXjUijezLlSgvX19fZaMApAeLNoQrcuyBryzpyWPJTIMRXcz0To0+m0qqo5D3O0hijDquoZmqqO1rm8vGx5IxWSSdm7OBBGlvFKg+u51r+qO0vteISIipPMe6QzZeQ4N84PjZ8GjMFFaRofB86JVvW7q1V1kRcaXrEXJ8jRZZTqe5lGWVlZ6aUYrHXS2NZcykVkfHFx0YwZJ5yAIgu1RD7pkEXGN2/ebE4784lJ2WNq5Pezwp/jQYfK8+3v79d0Oq1XvOIVdXJy0muSkwV4QLO1MQaOICO34+Pj5uQ5uKpqbAI5SaaLM0PNpu5zRMkk6A1gjewTWfJ9dDrHMzyLnlR07qNxrK+vN0eHHraG1jhz+hwyNoNekHN7kSxCprakNMwj7cPQ2dNJLXzZHtEq0Mb+0C2AM1+fmrbI2MlpUu5JWSeQenh9+K9rtbhUXk8As40kgadcjBmFHCpvGmjGjzP1Ym7Og6JAmwSYMuhuRRjzZd+7u7u9aCAvOTDHofLYBwdI4fMNOgy7vCxDioI2x62trXa8yrpQAOsh7+zni8WivUUFNYVeQr2jypNK84LqdAQMXFX1CqYybZAdnZLGlHKwV5iOzJ0ycAybhv3u4bnpmMgPg1VVzzG0jNewIAuSt8+AA8Od+fg0hpk3AwByn302jT7GJaOdzBcmcLN+og7GUIRjjZNhSUrVGOxLvkWIXI/H4wY8bty40dqQnp9fdXlz8kD0mbUNo9GoMUxZgMdRkqUbN64aeJycnLRjhHpe5xlhTEWyZByZOoJkJrJKmJPMI0bWxHj8HjAhA5wZJwYUAlQcYRZyJlAgr/QmmRm54aT56TfGhQwKArKCG/DJ+chVz+fz53ROM6fpdNrWgX1kZ5LeziYkrtu3b/eOm6VcZdqNzHP0mSp8ENdLNSJ+kNd9O+LJZNLLgYlGFXpkvsiRJlWEhD6jjTQGHF4iQsYuhRfdKQpiVBgJNBuqWg4I1U35GbaM0I3fvKo61J25M0ZJ7vjg4KA5guzvijLc3d3tFdAkvZf0bBrqjIqzCC4RP6osUTDFYxzRaBxUNhOwD7mnnFBV9cZpP6o6KlvUX9XlohifbLDivihYkYgclwI5nwdkEu1bm8zZpxPmuObzeR0cHNR4PG7Ff5ycegHz45QSSHGCqG9gIosI0xFnWsH4srgmi3zys0k3j8fjtpdD+STjKe+M6ubmZutilvMjK8M9TAaE7mU0xxGQCR27dPBK4ypt4bPA2dnZWa9YK9MV1iOLs+iWv3P8wLZ9zjPwbETmvjOaTgCVue8Er55BljKXmikwjn3IhAAu5Eu9RbJqGAWsApCSxVJSWRwuXbKmaSu1BrWPydCQQSkB6zSbzeru3bs1mUya7Tg9PX34PuIX2XXfjphDze4+hDCNtUiBwYRORWgcFmQvj+N+3myS3W0Y26quqCjHxHlykO7FuFPEtbW1Rk2Px+N21IciiVLlivPF7RQWxS4qTYezvb3dPre8vNwcUhZiUXDODBUpz5M0MAeZKJcz2NraameyOV+fg9bd6+7du+1VaRsbGy3KUfil0MMfY3HshWNhYByVwFJsbW21eaKTMSTqAjJ3CaU7+pE0qp/bE2uQjmBlZaUZL/Q/Y76xsVEHBwctkmR4yJT/k827d+82mUvQkxFz0rqAk7SHvTM/+cFbt261e1d1Ub97mlfShJw5RzFshmHd6IP0i3VDO4v8Mx8rtWMcd+7cac4Ku0RPH3nkkTo+Pm70J+o0Uxt0SEFeRqCAEered6wz3ZVmAeZF2fZThOx5yX4kSM8qfICmqtp53GGu32estX0nW/YIeDeP2WxW0+m00d6cYZ77xSj43dbWVqvAFohYo5wjRk7xado4DlyRoVxwBjj2Unc/R0ITnKirAEge1PUwIr7+dd+OeFh4kQ6pqnoHnZ8vumU0oeSqK8PjjUZVXYFMVbXoFi3sjS4U0TOzr6wio6TE5B3zrKTfiTIYTwY/KWuOuaqr3MxCGp18FLmYl3xa0o/GnREeJ0RxGJVhDpbRy7HIzWc/bgaDU6XYDC2FTKfOUBu7n2V+D4LnBNIoTqfT1vJ0GBXas8xNZ+Ecx2ddrQEgxwFWdflVzxVdJO29vLzcq6rO3LFagKpqxsiZaGuR65ERrPurLGf0PUv0m9GeavJMxZDtYT40HYqfDSMf8iK6nc/nrQ0suUqa1/fstT3QrtH6cLiYgJw/efF5uuPf+ffS0lI7voeNsIcZ/WKM7CWAkHUGyRLRTbYl6WT3TttDTzPNkEVYuXfG7x7yxvYpaex8vy9QgRXQCS7fpQ7scfIpv2TKXuZ8rCf9EACwCSljwxSMOXC47PP29narKs8ak4fXi+O61vElURbBRHMxEJQso2CUiRwlB5bOhcBymmgz34GeNckgtPnSCcrhSoctamZskuZNg2O8HBfDmcaPcnIo/j8UcGtxcXH1Bqjt7e3eUYTMV4ou0GwcprXk2NLxJMUpt1dVDSAxVqjPdAjmTMntCeX2LCifQ/d3UvnyiaJNr/Czj0l5Mpqcl7lY4wQGvoeyTOoPXYiOT+ouHXvSitabgc9coJ9ldKqxDHbBfmaOjvPMd72m7CWIy9ywe1l78pDOp6rflY3zTnaH01QIRq4ByGzVyKFwBOSGEzDHLIAiU/QzawaquqOLKT++654Az/B4jj7IIn86bO84yaygTxpdZEtGzMMcVLJb+2TIqvptT+mYRj17e3uNrQEgtKkl13kOHyOgRwEq/uTkpNWOeHbWJ5CZBATDfuXmDtTYn5xb2rS0A5PJpL2203zNiW17EFfqyMPr/q5rNfRgzKBMjTVQPKImhqaqe6+rnI+oGGJMWo4SEsCjo6NmnKq6BgBVV0dssvo6Hdbh4WEz0gqMVE+jVoEAFKpoSXFNRrJAiDN7ScNVdcd7UuE4OUVuuS7ZYEPrOcU2jFDSwZxJHvUZIvmqLjpRwa45AmdiDSi7z5+cnLSfMeIiHAaUM+RMFotFe1Wh3DTjnlFSRjTpYERfQAIDlUcuhg7EPTkbwM49OUD3lBJwn4yURBBAQdLPxjkEQyIle5+O2RqR0YxA7SO5v3HjRmMx0tFXVXtRBGPJSWfk7R5SANY5I+H5vOuiZM04SDI+n89bFXYCI7nezAHbb/nU5eXlmkwmNZlMant7u3eWHeNxenraGwPwTufQ+1nfQM+wBAkWgc8EE3QU6yYSFLWzPQrAMFHWcZiOcMICE2C8bBlAO51OW4FpMjgAk3vu7+/X6upqo/sVJCYbBMhn0ILZskePPvpoA7JkNPWMPhnv4eFhjzLPepT87sPrxXFdq2oa0rOxhJmwQ4ZJq0Jio9GoR/VQoqRLz87O2ivC3AP6TKXLiBzFpRozIzSOlwLngX5OXzGXe2e0VlUNEctnOQLhNWcARuaoqrpcE6WrqgYs5KJ11rKGojtOh0Nk/BIFcxIMju8yPp6Z5xLRsFtbW80wi3TToSc9ap09N6N4wAfbAOQwTOk80qHmkQvOBuCw3lmHMKTwfBb6z2gbgOFI/D7Xp6pzgAmi0ggO0wn+BrYySqMHmcevqp68ciAcJRBo7Jw6p+OZ+dysGcj9sL6eZyzk1s/IFRYgO3Tls4GXZE5SBtgD4MzeZjRmbIq+zGF9fb0B0aRt046Ye0a5mUqx3smgmF+yAmTJuDO3bt/8nXPH/EjB5P1znorJhnUB6hbyPL8/wI49Jw+ZhrFnWRGd3823j2HAVIUfHh72urgN373s9a1k4UFcD5389a9rvY/4xo0btbm52ZQ8i1M4YUIE1aOORZkiH0UzjHoaWecYMyKgRBwAB4TuNY587d98Pm9HrOSHKXFV9/YVTg1FlIYsi2zkCN3bczc3N1uxRVW1FpsUNh3oZDJpzt+9s/CEAop6dnZ2mtGyTqenp63hAUPEuFACxr6qehG8nwE79nUYMXKumSNLkMDIO//KoWWO2fj8TJFXti8d5gLl4ao6g56NO5K659gY/czJ2yP/9rl8rn3OIr+cu88m+2FtyTvA4TNZje1omu9y9AwoZ5JOOJ2z6Mm+GBMn6EpAslgserJIb4DdPHlgzunsyBi5YfATGLk3BgnIIeMJnhSUkW+pn6eeeqonv+Soqtp3gCUO1ueHZ9WtiVztxcVFzWazJkP2RIvU+bzrsifPrlpeVH10dNToeHnvW7duNRbIOgzpcoBRsWZVNfYwUytZmHZ6etpqRLIqmgxLwWGJBBxkM0FX3pt+uA+7d3Z21sDRg7geOuLrX/ftiLe2tmp5ebnRfaoOq7p+05mXW1paajQRZUgEzmnIxTH6csKEyNlUSnNwcNAiXUqJipHb5FwYoTwjXNWh4wQOmePiRAkUY82hcWAqhhkROWPPNVfrs7S01Jpz5FEE4808EuOdqFwkyNilw5HzoajWRtvJpPyq+m+USYOflHCmHBIInJ+ftxahXigh+k3WJKMb6D9bD5IRn0ln62w455nfTyecDvX8/LzJiWdzPoz9vXv3ntPQ3z5nsZYxZS7Z+lZ1zSiML/cXWKjqnym2tpnrBVLsY+Z4ARZAMSl16ZXLy8u2ho6kSUtIc2CzOM3sB8BBYYcAgMlkUvfu3WsOYGdnp6r6hVrodtQ2eWITAArMCQduDl5eoNJXysJaY6oUUGX0n4VxbIs9II/37t2rZ555pubzee3s7LTx0Tf7eHR01ANpQB3AQw/UQOg6Rv729/dbZOz+CiaBFLS4OQKxnHZVF60msPaOYycTHMtLAJMyCETmO4eBRjqZBX4PrxfHdd+O+NatW01wOFDRpQpJwiA65RQIG4VkBDk+SkNRUV6Mah4J8m5j0TiBhfgpIcqGIyTsoiSVhSIISDoprszVyUlyQGhqRpSTkpvM41bp6JJmXV1dbT2lsxgrW9bJgQMcckmKpihYouGqriLduLJgI9++gxKzxtbIPmSkZ204zDSwSfUCI0PjxJBk2iEjWOvqaJKiGXtNjoaFgWSSAc1KWtEF449etMbkUGSekZ3fWdMsthOdoW0Z8aQv7VFGvc8XdaZOpJwmyLBOWbVvfKhJ96uqXiOQjY2N9lYnx8yyHoMec6ocqxfJk4EseDw6OuqxXKPRqOV8gUV1F5nbJMOA/cnJSc+p0imyYZ+A5XSWmR/OdzYvLS21znX6tot46f/R0VFtbm62tJdctvz3sAkLELuystJeXWpf0uaxNUk737p1qw4ODpo80Ic8fSAdk+AuK6+rriqfjdHcyZOagayWZueMzwkVwOJBXQ8j4utf9+2ICU86JvRNFpZktSMDR5HyTSXpnN2DEU4B56irqn2H8UfjVlVzLv6dBSMMF4XLimyKzsGKkOVxjYeBEUlk4Uvm75I2Y9AyGmJAqrqIKw2xn2e+yjpmEVVVvym+9UlK0h5ke03UNKoqC1OquvfsAguM8PHxcR0dHdXx8XHNZrN2JlIk5fNyVUmNMQQcWEa+VdWAAGeFokbB+yznl/21GWj3N3frmQUw0+m0x8rkOtm/qu7lIkM6OlME5DCLkcidtZabpR+emakRc8CGJKBNcJE5e8V1vuue9t7PsiDJWMmX9FBV915VOU2XZ3Ns5qMIyL6Sk3xrEuCXxVaADwZM5be1NscsMOQYzd3c7G1V9650IMZ9pI3kpauuGBLpIWwBwO07Cfo5M/PUdwBIsA/2gr0Ajsjr0dFRA+pVXafCBNpOjkihCSjyVYr01lpkWiG7d3H4+bpHLEmC34fXi+O6b0ecRzQovTzY5eVle8Uhx5RdZ+TRKIyoicG6uOhe8ZZdtlIBKCyjzXByjowmhdvf32/Ct7S01Kgd0YqzlCJYjj2FPfNtmSPlcO7evduqXH13Y2OjVYtmlEM5GHP3ycP6eRzDXJI6tn6Jhq2f72VEQhmrupw25zt0PGi2jAIY3el0WkdHR81AoDiHna6svahXnp/SQ+rn5+eNrTBnAAGYy2rqXJdso2p+Ijvf52hEJpgBFN3FxUV7pVwWCXEegECCoVw/eTvyjWp0P88AQrFG5CcBnPW37iK3BGUJMlyACF2wRpk+sd6AC/k5Ojpqee0sUiMPIrvJZNL2YnV1temgvLG57O3ttXkaN2eaIDXHYI0BNeM+O7t61zYnlymjYW4+GRXRo3urh7h9+3ZjjLw0gU3guAGNYerNWDc3N1tnOntLD7NAkRxgQ+bzeT366KM1Ho/r9u3bvY5zmrUAwJnjns/nPeYNU6X1ragb20AG6HXazqor8HF0dNTWEsP1oK6HEfH1r/t2xKLRpFZtNCcGTSbaFQ0wJpAeZyR/AmUTsDwywrkwKn5vXF55OBqNmpGoqtabWZ5Z9I02lceaTqetm0/e6/Ly6ghV5pwy98tJVlWvwIdBq+pQOkcLgXNSeSQILQcgaGWoI9jW1lavEjbRL8fHWTLM8rBJtVZ1bRv9jhOiRPbu4uKiNVwQneQxL4gc2GDUOWzykXmydM720BjMDxsBoPmc+4tM82xoMi3G5fWH1mtpaanXljXpZQDQM6o6J4l6TNlICtH/OfV0vlImnE/mkpMhICv2gJwlIMiz7Zn+ca7V2tAZ4CzPqqZs5PwyVeRvDpXuZx9szvHo6KhOT0/r9u3bLa1kHlgMa+kZADzmiR4BZBwGp2INMxLMVAAgwD5lyoFOckD2KesZ/N59yW5WdasBQWdjitDs5qFYzAtfAPqbN2/W2tpaK5Rir8go+ac/dGVlZaW9OY19JfvW2LqqsxFscPRptx+kE354vbDr2o7Y+d1EtEPHQHgZ3FT6s7OzlhcVcaXxz9xGUrgUh8Km49UcIJ0JJ84RQZk+R0nRNYyDaI1xFk0xxJyXI0AUe1ilSKE4rlR+gIRSMMyMKOWDoDm6jDDSWTA86GLomoGV07KHWckrSklDzJBkCiDTCqpgRYUZgafhqqqeo1lZWWkv5MgiOQbOZxJMSSVkuoDhyYjJOBL0eRMWOZpMJi2FksWGKVNp1Kq6Fz2IdLJS2D7Y66RKd3Z22jqLtvb39xsYG3aAM8csclN4lU4Ec2EvfYbM59lqjj5TO5z6MP/teVXVUk63bt2qnZ2dBriwWtlnHLj1O+uT0VkyX+lg7Tcd5Vzsx7CivqqjyxOspm6RKXLlfpmXN1bgMN/KZR1Go1HvVYxpN8zbc70pibMGihVFKTYDJNDZeq3bs9xjoDxz1nQLUMJEobPVKgD7bLBx5c8fZLHWw0Kw61/37Yg5T8Yu++smuqeUIkA0T+ZnGTyRYeY9RcKMh6iIYYcm/d7v0hC7UFmMgPtDpwwoFKnJvUpbDvz8/Ly97aaqWuUpR8exihrSAM1ms6aoKnblmjKa0gknC9qquuKyzPGhSjNS4gih4SF7Id9mfdIRZn6aoRE5img8P2nUNHwcbkZlo9Go9yJ00d2tW7d6VDSajuMCPLLhSka59t+beTJPmkVHzz77bKMjfTfTI9bF7+2H9RSVM/7kCZNhf+yJfUxnKm86rI3IVwEmU5FHztw/I+4EPKJW650pFPIuasx9G1LGZ2dnrYey44aO2GGgyB6Hz6lsb2+3ymeALwv3soLcPPJ4Hgp6NBq1FxOkXCj48if3kX7m8S3OF7hMWbWHIl77D2gk8OfApEM4vqHDNgeycnR0VCcnJ7W6utqzEXnSYjwet7a9a2trLaKmG/Yu34Hs9/miFCBMfh/QEgknS2kOmLeHUfGL67rW25cIoiMrjOXy8nI7J5cVsfJsVVdC65A7oySPI8rkSOV+VCoToqECV3W5KBRmFqCkwmQkAWkam5w15MtJirZ9lwEGFPzxPYaBM8jqZMZR1bd7UHQABDqn3JSQUctIs6p7naLcq1RAVpX7vOibMwKYGKHMH9kvhtq6Z4Tvd/bBWKxTvjQiAdJo9NwGJPZKBCpSmEwmPeckWhBlWiMyIoc5m81qMpnU3bt3m4wq0snq//F43Hsxg6IkUfZkMqnz8/Pa3Nysvb29ZthEeeQL2GSoRWBZkJc5OnuZx9b8LIurOC9zJ9/WI+sNRInknfHmFJM1IiccdOYr5TwZcOsDdEuVAEwve9nLmhzlXlnHLJCz52hvz/c8a5uFcvQWKK2qHsUMLEovZRpCIODZdE+EidlJhin1ihzYZywRgCG15CwwvVpbW2s5XUCGjrF99raqand3t3dygUwM8/8cs8JANpYdunv3bh0dHbXaCGNGk89ms3Y08EFdD3PE17/u2xHv7+83wWJ4FDkRJka/qlqukrHLgh2RGmGUT/RZ0aPorqo7HpQGBTKmtBBjIuGk1DY3NxvlnAYMUsxmF3fv3m0H+dPQ6qbDeBBoypW0mjOBWYXp/CGaVNEQ1Cx3i3LKNndJN5vzsAjJnIdGilGeTqe94jEvk5c3tXYZgaIjrU+e77WfwJl9ZSBy7xmR4XGuqo42TaeU7IDITjTDYFsD82HoOTjACpXNSGThnFzc9vZ2L6/r51mlmoAou5aR3dwbc82IlqxZpyzGsWenp6e93HdVv5qaY7Dn6O4cp5wqp5P5ZbJsfPKJnCV2yXMXi0XNZrPa2tpq4DLzyxyovQQWrC/wCJBhSnyf4+X8cg0z3aFCPdeAvcgcOpBgjoAEW+N3mUpyzwSFfgdI5b2zxzyKvKoakyDSTWodbU1H6VjaPc/Xm0Bwk1Q4IJyFjPL3o9Godnd3G/sm3wwcYPDSVn+or4eO+PrXfe8G58SIbm9vN6GR68mKysvLy9rd3W3CUlV1eHjY6+ii2UTVVUXg5uZmuz/ahSPwc0rKKHGoQ5qUcaYIzv8RWIZmeXm59zJz96iqdhaRIzQXRVryfOafKDfzeQwlOtKzOWrvba7q2g9SeOvEgKdBqLoyqltbW82BVHWOkgEUGQMUFxcX7dV3HEnmNzOCSSNmPhQ5ozcRzunpad25c6fRisaaNQJZRMMIM07OQWYUVFXNOWXEqoAv6ULG9/Lysh1R8T5qR6EAguwaRQayLad9suYYjY2NjV43LU6BQczIjUzYw4xarV/m1OgQsMeBA1JplD17Z2enZrNZHR8f99I+ZAvzkdHiMIcv8uWQMCyZs+dEyIm1s+ZJAXO2osesIanq3judxYyiT8456yISOKcNIL/2Ih149grAliXFbx3MOeUKqBSFCh6sGTtBpuivs9R5T3uf4E6UnCyXuQ5rQUT19I++0k3/l8phi8iKewM1mLuH14vnum9HvL293aNObWYWrfibIKAVGfVh8UXm3RLxawknQkIjJWJnQFKhM9KmyGgb36FcaD95MY3ZHXPS9SePETC03iP87LPPNprcs6q6vHUeZ5DzMu8bN64alnjHqYhMYxEGALKl1JScY6jqXqLgykpJFDAnDsxYDzQ6sMG42l/GE6KW66/q8skq5Blzeag0vmjDjEbIgrxlVUc5AksMb/YIZ8xFrlXVM/537txpz+SwspsQWUyjnIVy5gbMVXVnixlv++RnVV3LVOvGkQOJKoWTCUpnS06wJ/myEFeCTfe2FuPxuKVSGF2OypnVoTPOmgAOYm1trTWgyLw+p1N1ZdixQ/Y16wwAisyPJsDKdE5W31t3xU50056RV3uFJrcHw6ON9Nd6cFAKqNgg41HE53P0kC57FjskkvVOb86PjUnwASzLPa+vr7do2710R2PTHJ9ktxIIkTcysrS0VLu7u42pAirn83ljKIHThy0uX1zXfTtitHQK+ZAqzoIbBhQKJMwO1zPcPq9y8PT0tKbTaauGZEA3NzdbNMFwpRGs6ihKYIEyoI9VRTsTyFBypmmEk650LMh8MvoVjWehhaMi7kOhgYak4i8uLurOnTtNEc1NDkjbu6ru/cv2IKudGZyqK+YhK7/dO+lJym8ds5FKGj3RBMVPJ8jgJEUt7+n+WRyVRoxD4mDlVT0/86jGMaSq8/6Zd7OuimEwG14EkKkMslxVjZlQeMOom485cBbpOJPByBxz9kcXnZCfdALDXLLxZ41FRtq5V34OtC4WizbXlGlsBBl0z9xbDlX9gzd5AWgclIg0HbB9JveoejqadoE+Wg97mzrsc1iPTLWQIw4+14XzNj5jYj+AAWA5K8cT2EhXJcind9Y1C1KxP4Ds6elp71gkHWa3FHQpaDR3ThiIzvXMFAl7XNUxKeyrdFymQfJYW8754fXhv65FTWdES2ig3szt5Zm4NMCEivMaFklMJpMehZfGRu41nYtnodKy2CaN92QyaYLMEI/H44am19bWan19vUd7VnXvpGXwF4tFK4DIHDT6r6o7+uC7HD+aTSSbxhpldnl52SIheT5VrKJ/4xflK3bjmLLT2PLyVR9dZxCzcIyzvXnzZjsiY43tB8eYBTJyV74jqsloxj7Z88wHG6sIXGSQuTwMQ7IbN25cNUuR589cpuhWtahKfPtjHGRCbYPv6v/tvsZqLgym6A/wzIKszMv7/8XFRQOOQ+df1UXdDL518Uz7yOmhaLNVoXkCmX5Gvj2D0+NAMr/se3Qa0yWCFXnaU47V3LKoUIMc+s1pkCGf87dxcHDpdMxNXprDNWZroI7COulHT6/8nN5xYCJKKbdk3QCbjJbJLL3J4jvBBVlaW1vrgXeAxhEj38/jcWTIWmE32Fd0OyecwDB1AfuWDXhEw4rGsujvQ31lcPTwur/r2seX0GVyWEk3ppJmThPqzpzO0DGnAcyCiMPDw9Zp5/j4uBnXzKM5rlLV7zi0WCzae34pLqNHSReLrrtVHtOgYGiqtbW12tjYaK8ZY3TQftlwXzR97969Ojg4aBF6VoVDuwzOsOqXYmVf4HR8mZcTbYmEHn300arqXjKuUbznJTuBqjbnBA4Mln31uzyilvuan0NrVlWrH0hjwZmJtnIvGe+q6kUlHLZ9VnRX1b2Wz1pMJpMWIcqd7u3tNQeXfZMTpNhD90JDM3ooXOtYVT2QkWPnDJ1FZ9jz92TSvPycQwVCHSlKajbP9DtRkLQ/sIHFoWNVHWAmWwCMzzmJQFazKIg+LS0t9Tqd2Qs6zpnR5+wmxukDf8nCiGKzatva+bOxsdGONkozAdhkPcE3R7+ystKOvbEVKOzpdNqCA3pBF9D29JEMAVx0zDjR6+Q7I2GAmb5iYNiM+Xxe+/v7DfQ6FpYBD9mgWwAEwJG6B2CQEc14Hl4vnuu+HfF8Pm9t0mwkJJeR7/n5ea+jDIPldV+J+kXGok75R4b85s2bjaqGJuX7OPhEkhyEz8lrp6DqVMVhZo5RhOCzIifgY2lpqXZ2dnpn9ICALPaBjhUIDfNxo9GoOQdRWB4VSoDiDLJxAREYgwQDHEnSkxnFcBhox0wnWA9G1PydU6zq3rLFSDFQitCS9s3q9QQuwA0KXw7dGg2LY4w9c9tV1aN7PVN+k0MQRTD4WV+Aak2qr+rKCTKgHBBQljlQcgMIZZ40ozC/zyjBXDjBBH0ZiXNmHAa2KKnGLMJKecw1y6I2+4d6F9E7Jri0dNUOlhPN+XEECXiSSrdfLs4xK5MxMOjXTPGkDSH/HLWI1Z4AIOolEpgaC7lKHaaPUjwAYwJBkS1myh54TqZoRKacMhDB6VpvnyHjHKF0XDJ8Vd37h52ecCoh0wHYjSw8VezIdpBRF0buQeZxH+aIr39dyxFXVe+sWyoroauqnvNjDPb393vGljLIf6JSHIrPtzvp5MOoyxl7XlX1KEmGj5ODZjks7wmlEJmPqeqiZ5G6CJeRkQNKirCqyy8xNsfHx7Wzs9PrFubzWRnJSXB4ouTMCS4tXZ3f3t7erlu3btV0Om0AJvOL5p4giLJnfhdSznxsVccooNMY6sxlDgtI7B8D+Oyzzz7HSRpnHulioPLYkSjU2EUKafwYYM4mC5/sleiXM7TenC9wCEhal3TWwEI6nIyiMi+Zcpd1A0nXc0hJgWe+Oylef9LB2Q/rCbimo2Okh1Eu+ZDD53DcJ4/diepu3LjR64JH3shnFgyRPfKQDJfxAxMJVJIVcn8yyUla0zyBwRZYH3okYsVCpHPPug3ygvFKJmwIntLpJghK0CHvn6xgvmCFXcg1sgac7unpaW1vbzca2doBx5kX9jNHouS6yeXW1lbr/7++vt6CqEyRPLxePNd9O2IKLArzBhh0DUXhXC8vrwqlDg4OenRgVdfrVvR3fn5eBwcHLfqtulK0w8PDZuAWi0Xt7Oz08seouTzCNJ1OWwUjChdtXNWdRcyCru3t7fbMjJiABO9mFbH4OcNpTShIVTVn44XfnC8Dn44KBYfiYnCg94xi5ZmzLR8lhsRFDOlIgSLKiE2wt/7kG1r8Xr55Y2OjrQ26VJSclODS0lJ7j206Fx2cGMiqPq37+OOPN6CXBUjWjcPJKDHTGAx2VbUiGYbaH4DEmc18flVX8ZzO0Lp5rrUzJvRrplXk9rMQjFwAXKurq7WxsdEcNRAB6KIvl5aW2ivwsljJ/Bh0V/5O7vzy8rJ2dnaag2PkHatbLBa9d9tmk5xsI2uvnc02X+trXY07nSmmio4Ye+aPPR9Adi/5ToxB6jwWwL0SiNijPN2Qka71AiSxeaPRqDW+yNQTXaNvmWLzs3SYmUNWOPrII480G3rv3r3a2Nio6XTadJttM//MnVs3AI98AnL57u0nnniiB/iyyO6hI35xXdfqNY1a5AiqqhWCQKFV3ev5CIyWb5kngsjlZhxZQqkwiGgZKJ1wZuvE8Xhcu7u7TaCzQ08abcbfd1BtjARHRvjNIw1y5jSh6KzMpnTb29u1u7vbIuBsZMAYoYMpEYNedQVa9vb2GlDxKjWKLkVAIaHiYdRmP0SBLmh5WCCSVaDWbjwet0YkaK/FYlGHh4ftvbS+g9plJFJ+GHhFZMZEFvJe9hYIy25OSfsxuklForuTsswIDRixdkkpi+w2NjZaFJm5VeOSKgFC7Z175vEvTtE6WO9kI5I29v+kqpMC9jny5r4cYc45qUi5X7ndBCDpVMgcgMOIZ44StZ2V+9ldLJ2cuQ0BYTJC5pUUda6XyvekvwHnbIQxBFnWjCxlbpj9IpeZcjHuzPsai+Ag193asnunp6cNVAPa7icXnBX3AGc2DmL/kuHL5z0fazZkm3wWK+IkAbl5ENdDavr617UiYm8pSoojOxTN51cvVtja2mpn2Kqq5wQYZBGVJh9ZuFFVzQFD3hSB4HkeqvOJJ57oGZw0/FAgwwjVnp+f187OTlX1uwBB8Zm7TJpyeXm510Dj4uKiRQwiCX16gRTf2dzc7EXUAIW1kqsW0XI2niMSSyeJynXPpOLSaJknJiKNUh5nyCiHk7fmmA95do5CVCFK9iyVu5n/YrAAhTwnzfAwyIy9MYhysAYqc5NeTspbxXn2ExYV5loBXoyjCCjp96RBkwJO5wXMeY4IjEEHZBnJ4R+UI4o2o0UpjLOzs8YGVXWdsshKdh0j87o8ZYREJz3n4uKiJpNJL93imB9HkHJBzuVfkwo2Tms5pHd9DrCia/P5vDEvQJ8TDfY4AThbkuPyGTK5WCx6R6XOz8+bXbF2jvvYm0y5pd3xjGRAzAMYlyJTwGXP1TBYSyAJyMm99ycZGTqc4CtBTAYMAFQesbPu2bb04fXiuO7bEXsnqYIkCI6hJZCMNKF1HjYdsYjl8PCwUb5+Bzlubm429KjauKqaoeOAs8pxNOqOEvmMdwavrKw0mlykQSkcTaFIjHZV94pHNBsFefbZZ1vhmHFdXl62l0NYh6WlpWbcUM9VHR2cVDtU7o03ovvRaNQimPPz81YRnAVTGxsbTcmqqhnNPJJkD2azWYsWGWEKy0ECL5mjPDs7a/l7a0HBx+Nxi5gZK47a8ZLMnxmTMWsnmkVD+k1zzDdv3myRdFa9p/y5J2cPEKbTyXPOHEAyAZPJpDnPZAA4XnMQ6WRFOkozIxH/5oxRzr5T1Z2xzXVKVoKzIuscWAI4f5aXr3q/Y6I4S/tZVT0qN8+czufz9vIRMksmrCuqWntW0bk1U3BZdQUSvHUKOHQtFove+mT+VJ2FKNXeJS3Pke3v77fvAThkUUTqM6JB98pTCp7PzmXdQe5Z6gt9c0/OHGA0Lnq8ubnZzvteXl7WZDKp4+Pj9vuMwBO85PztCf3Ook9O1niAlXv37tVkMqnT09Nmjx7U9TAivv51LWo6EXUqD6HTcDyRZkZtco1Jm3Lw0+m0HalhPChD5lu8kxdKpFiZ70RfQ6pVnQPPyMzYOeGk0805jR5n4P8qg0XhHHg6IMqHykeZrq+v1+bmZh0eHtb+/n5TcrQ4A1XVHWfZ2dlpUX9GFEm1Mqi5V9Ygc0tV/cb2nsPoMZruweipDuVEOI6UC1F+ovrMScll+X4CAg55d3e3AaCqq1fziWbcW8SZLxDgTAAUMgfkcLxVXZHV8Exmzi2daVX/XG9SgEmNbmxsNOCUoIFhV+yWLAR9qKrevomiOUV7mE4850BOASOyYvzWzrirroDZ/v5+O+qVIMtY7t27V0dHR629J0CbVdv5EgfryzaoGFbg+Pjjj7ezymQvbU1V1y0NWE9GAwBjQ+gF1iDncXl52drbAvXSSQAjmRyyfRm9A0PWPutNErCyi6kTVdULGqwX4Ea3rCtAleDNmNiJXLtMvSSgo3fWV1X1Q2f54rqudY54WL3KaGeFIaM0nU4busz8SdJK8iCiIH2BIcCzs7NWKMEpKNhKdK9SUr4RCMjIJ/NFi8Wi93YmzkP0rTMN5VN9enp62hpKZC9bc9zd3W0NPxaLRetL7BxgnueE9NfW1ppyKNhqm7PcNcXIop+kqjLPyHBU9atWGQYKylllW0nRVQIRRT2Zr0u0z2BlauLy8rJ3lCqdG9BQVa3hQr4/F+tg31xSGIyUefgMtiSdPuCT1Lh1B4aAPGsDQGAXsvmI7/u3feR8MpebBpEDScAmJ2i9MCKYAOtjzTkLecXRqGvfWdU1//B8BtxaZe9jz15aWqrZbNa7Jxq3qqshSEra+NDNHEbuL8bB2Vy0sSI9DkcRIydjP73H2dwyX5wOxM9Fkn52fHzcKsUzarT2SSujhTktNiSfn44UDZ5vxUoQZgxZcAcYA9f2PxtzOCbI/glAtra2GqgbUvnZREcumrxkWsGees0l2t+cHsT10Mlf/7pvR6xYpqpaowQOk0DluV3IMYsGVCSqZnYMSCTNseVLEOTvoE5GSOQt98IBi4gpAgPGYRJSdCkjSWGTTvK36Bc6T2Mu+rc+eX6PgopQKR9FyGpujsvRJI7VhcLN3CQwwbhdXFy0N1d5fhoXOfDMB+7s7LR5JYtwcnLS3haVAEy0ibI1Zsb18PCwpSKsB/SvIh7lW1WNoktgZH+Wl5fb3skjAycK4KxnHgfz+8vLyxaBZUpFfUBW0tpP8ptMAwoyz2ty6sMq1AQ0ydhkhG2/7Wm+MzlTBZgd0Q/HZW/ts/7CSWvTSSCL3Jrf6enVyzn8W+/orAanT+bCeIsCs34iO8YlTS+VUdV/BzZZ3tjYaDpwcnLS6ksSaKHFs789G2Fs1tdara2t1e3bt9tbxY6OjhqIzI5j9BzbxYGbp/lZc7JU1T8y6YU1gDrbQ16SPcrxzufz9tIWcqaoi60CblDofibdlEcT89RByl+eeEmW7OH14riudY6Y4UIhc0T5IgJIk9DJNyXlOaQz/T9zgWjFO3fuNKPDyXBinCyaitPPqIVzkbcaj7suTpeXV+3wtKTLohtRfaJO0ZPIloMBOBTmeLbnidBR8trgGQsWQORT1b3jNh3O6elpo9GgXEeaEsFXVTPSifBzneXypAQAEa/Dy5xrVfUKzE5PT1uBncgtC1Q8dzwet+pjgCor1d1PdHd8fNyjkEUyAIbxYCPIo8/YN4ZfrUEWQyVDIgqqqraOZBlgAXQUeCkSc95TtC1Kyn2QWwVUREscLQdJLqxNFt2IwDgjemGczr+TP6AEIHCe/ezsrBn3LNihV5q6kFX3mk6nLeXBUfv+8vJy+44/Ii55ZPuUzo7MA3Fk0t5l4VzS8FtbWw0QDfOi6PnV1dXmaM1lY2OjNXqxtuxXHr9LmWSb7CXAItVBJjOyzMLGtGdp78wpmSs2y3wAvvF43HvpjN+Zq3sDnca6WCyafrOJw6Y9DyPiF9d134542K1HBSClg1YheXlXBUf5ykMGlWFbWbl6V7Ce0Em9is4Ij6rtzHnI9zD+qXDb29vtO9nEIo/DQL6iQgZ1Npv1WiGKtrIS1BlkqHV9fb0ODg56uUBOUGs9f1PO27dv19HRUcsrMswKzdxjNps1o+MlGKJh6zadTpvBTvRd1R1Bk7f0jPG4O+PJ4Vg/+5u0tvtUVVtbz19dXW3nLHO/ONA0Egwx8MLZV1VrN2gcHJM9NqccI6ecdD5gUNVFOBmZMGqoYVFHRkF+fnR0VFXVM+opyyJDsurnUjP0ZpinJl/Al4If/3d0jD5UdWkbzwPWAM+8MsLyGYwA2bI32QCELLonytW4ONiqap8Hbsxjd3e3yYf18FKG1MfcXw7LPmUTi2GKyx6kk5vP57W+vt6KHi8uLlqRFCeZVHTKApuQNQbmI93GPjgnnim6zCnTuXR86WSlozKFhzVxfxEyUAqEkh2XZ+SJg2w0lD0MHqQT/nBeH/zgB+ttb3tb/eIv/mKtra3VZ3zGZ9Sb3/zmXv1B1dWefPd3f3f92I/9WB0eHtbLX/7y+qt/9a/W53zO53x4Bl7XcMQUZDwe91pMMlyMU+Y4VVZD/hSR8SAcesk6k0lpfT/7tFLOdBxZuMMBVl0Jp5zL9vZ2U+KM0vL7lNC4sserSuXsZCSnI0/omRsbG70GBGiiRP55YN//V1ZW6uTkpEdn+32u3draWm1vb9dsNmvRpIjTMQ/7ovqUw3CcCtWeDfMTqVt/a5TGMiNWkVE20Kjq3pgjqs9IeX19vR599NE6PDxs+UoV7Aw98AU0bWxsNEMlJZH5roz4q7pjckmXmkOyNvbGz1DgyQSIwszJnpg7IykysQ7mlsaRsc4cI2fBmeX6DtkDc3NvwCbBEIYDc8TBZi7Y3LPQKJkTDkJPZ4zNYnF1FCjfHW5trWFWHyc439zcbEBia2urtxbWh6N0T7JIHrMoypqQY044axfk8VdXV1sKYFhwyAlKmwEg9hZTZYzpJKu6Vr3b29tNbpLSTqq4qmOrjAFrQp4ADSCOHtJjJwlyz4GFlD1js5fGMhr1j4p+qK8PV0T8pje9qV796lfXe97znrpz50595Vd+ZT3yyCP1pV/6pb3P/eAP/mD9+I//eH3/939/vepVr6r/8l/+S33DN3xDfczHfEx93Md93Idl7NeqmmYgJpNJr1CD85FXzKIh3xURoMkg5fF43KtwzMhzdXW1VTtSoKrqOV2RgIrDLJaBjJPScdyIU6LcWXiT510979atW7W1tVUHBwe9whXjJNgcAIPmuEdSUv5Q+Pl83s7lZvGKvKmoPxmJGzdu1KOPPtoz/PLyIg3UomiPMbFO6EYFIJmPzGIbip254jQE5+fnjQXI1nx3796tzc3NpphJyRsHOjFBC5lioHQ3yrPFxmEMDA+6OouT7L39Muaqru+3i6M15nToxs45cuaABEOLtq/q8qIaYGTVN9kBjowrm8NYt6pqewi4GY/GIpxTghERlIiJrpAJeXFFg+R1yEKIgAGjjLyHtRCYjgQG8v0csFSX9eO0PBvwdAHL9CHp7WGxFPugu5s6ElG9fVFAZZ39XnDg3eTWxN4neDC20WjU3r+edHLeH0jBYEkX0KvLy6vq7qWl7kUbCeIFAmQtna/fi9CBB3rAPpGDB5kj/nA44ve97331/ve/v9797nfX5uZmbW5u1pd8yZfUD/7gDz7HEX/sx35svfOd76yP/MiPrKqqN77xjfUt3/It9eu//usvfkcM2eZmJw2MNtnd3W00NSXQhQg6oyQKPCgrBFtVzRA4mkD5KQlDzHnmGcZE9qqMGbCjo6PmHAisqE/OJzuEZSs/jkD0DoxkkYyohtKlk+DAOWWFSmhuc05nzoEkXYl6Nn7Gx7oxFAz+6elpyzVaPwxBVbWmKowbIJMOLxu3XF5e9hzjsFlGFrRo5TcajdqRpJx/AjrGNKlwL6RHo7o/J4D1SBYlKbh0pArasCsZ+SQl6PP2OY8GcYrWUu7aHjO02kYCFmS3qppjqKoeaFOL4Oecj7oBhj9bl3Jg1i6BgjmixPNzS0tLjfHhlDPnbx/oMDC9s7NT6+vrLY20s7PTdCMBOBoccDJW+mjNzbOq61KFDqbn5gJQJKvmuxl9J3VNZu055+X8dxa9JbgZMhwbGxsNSOXbnthDL3j5jd/4jdrd3W0Ml723TxlokCv55mGtijkB9PbK5dhT7qu9SNlbLBYtWp9Op42Veildv/Irv1JPPvlkSxVWVb361a+uD3zgAzWbzdr7BaqqPvmTP7n9+969e/WjP/qjNR6P60/9qT/1RzrmvK5VNb26utoqo5May+rcnZ2devbZZ+vmzZutZSWKhbPMvA7qJY95VFWrJOQ0ORsNHdIZoXwzNw2dU5R8IxSDzFgQ3iwoQ/V6SxKF2NzcbJWdxi3naoxQe54BzUIjreyyw1TS28NG/tYpqeGMPjgU65CFG+bHuGQUC+Vn9yR5wMPDw+boc20Z1GQEslhEpCPKnc/njQURlVR1b0byasOsVB52OUp5EaGbX87LOjGQwIQIe5jOYBAZePO0nlIUVVdnVYfRBSPtsr+ifA7avvoeechjY/5kRJ5Ok3MyBjqY4JQ8ZDQPxPks0GzN0xEq4rtx40Y7vlTVRXLkXWpH5It653DUUOQzM7d98+bNRlPbL2AAKD06OmoykX3hM2cPlGY+dlhVbl3JEkDnOKI12N3draruCFzKh3tmCgEoYi+AzY2NjcYscPzkJCvSVUEn8E02L+9P140/T0XYJ/qfdRAJ7AFhwcBLjZo+PDxswNLFKR8cHPQcsestb3lL/eiP/mi9/OUvr+/+7u9ur4/9cFz37YjPzs5qZ2ennbFcLBYtR8d4zOfz9r5eRjBzPygq97NhWXkMkW5vbzeEzXgkApb/4oxFcxTKiyfcd3d3twkr4zc8d6vjDGUV6TFIaPKsNl5bW2sAg5KigKHqbJ6vavTi4qojUNKdDA/az7+feeaZdjSC0ot05OwpdEY+qDkRDDS8vb3dDGJ2WDo9Pa2nn366VcOKNvOcNsXON125ByfGUEkLkJGkg71gwFwZlCyewUYMc88MPrCTUTuwILLlfFGODBCQgAK1P4x6RnLm7D7oXoBJlEO2fY5DBy4yos+8se+Sb3KZeWZGXdtUwE10nEfjjo6Oek7K2sznV12zLi8va29vr+mto4DmyhkqvDOGBJ/2NesHspo9c82ctvFK5dBFur1YLBqt6/v2y/ykAeRzE4BWVcu1ivDpObm018mSVdXztrjMjn9SE+xVpi/W19drfX299vb2nnPkkA2SegJg6Y/iSo7fHgCd7A875r7Zfc46S+MkLY5BsFaZXnqpXdctQnvHO95Rb3nLW+o//sf/WF/1VV9VP/iDP/jip6azWpnQ+iOnCl0zqCiXw8PDnvBQrslk0oqWHDEYj8etu1JGybptqX5EOW1ubraoMKlGHbg4JgrL0DGilE41Z0YozjRnG8vFYtHexiRHJb/WFvX/Z8wYHApgHH6W48pmH1XVioMgaDn28/PzOjw8bOcVRV8cOeNjPRxxAoI4vXRM6GXHktDlvjOMqs3HenJS6ST93HokAGE0M1pKgwqYkC0gj/HDbHBs6XgVuxnnMPVgbTMPmcdv5AYZMsVMyTRwKMCF5+VaZCSGjeEMnm+NGH0RNOdlrlVdT+ncl6T0k5J3YX6MXTGaPHZGZ3kelSzt7Oz0OmC559raWi/vSNesLZlRU7Gzs9McjDVImtV95TU5EPMlW4o3OWzPynQZ5y/FdHp6WhsbGw1o7+zstDwsAJoUftqS1MVMK62trbX7AcP2yz4NmZc8RYGVI6OACZlgfwQp1mxoH4BFtQ6PPPJI0xdrYkyerRL/pXTt7e3V4eFh72fe3re3t/d7fm91dbU+7/M+r37yJ3+yfvRHf7S+5Vu+5QGP9Pmv+3bEFCCpQJEC2tHP5MiS2kxjVdVVO2aRDceGykapjMfjRlXnKxezCIPSiGbPz7sD+lX9t6QwYMfHx+2ZzkgeHx+3CmlGymfck1NKmisVOSN8FGfSiFndm4VVIkaf0cjC59qmLXfnE+fzq2M/+e7TzI/KVcq7Zu4281xZXSuaTTra/MiC6IQRXiwW7UypXGDS0KK1zHunHJEJ65G1AnKknJXvJKOR+fmk9f2OLGbONlkHDjlBTBbxJCXO4DO2gCiZISPJdgA7WWlMBnwXmAAuMBBZv8Cg22tXylamLVLn3E+DC3lCzv7s7Kw5L+tIn+WLPScr1hl6wIZsKkrKmo3cE3uXMksHEpClYzLne/fu1fr6egP77kWuU6ez0CsZDPMbpreMiSxMp9M6PDxsAJy9AaiWlpbq8ccf7+3783UdE1BkRCqIEMBYF04UGM/ak6SvgSHjToaLvJDTpaWl1io2I/eXwvWa17ymfud3fqf29/eb433f+95XH/3RH/2cfPhXfdVX1Rve8Ib64i/+4vazZLQ+HNd9P/n09KoxP66dsaX8jkeg5RjoRF+MN9TGYSpMoKSiM+i16krAtKFkENFcDCUHPp/PmyNZXl5uLeCcdzYu1Fw6HhQUY4niFpG6RxYTiS7QhaKUzAVOp9M6OTmpxx57rFFTSeVSMpGq6FykjGZGx2fUs7+/33p2qx5nANPB7+3ttcbvieSzgGRnZ6f29/drbW2tOXd7J6pdWVlpR0GAG3LAMIgesiDl7Ozqxe2aYVijjJAyRws4MPRZF+D7jFa+Tcl3yEOCRvMUeWmGjwUgS9ZYxCta8zssCZkZjUbt92TOWI0nC3vSUYrIGGM1BGSCI5VXTWrT86u6KMlzAeQENtggDiIj7Bs3bjRnk9ElpmR7e7sdXUyd5cQcc8pe8dYRyLCvjvcBMvbHvbIID0u1trZWx8fHdefOnSbbWXVsTc1nbW2tneQAAul5Ot5Mew0jawBWIAKsKnazXzs7O88pEsu0mvtsbW01fbQHKuqla7IOgdwAEdZQDwdUu8KuzHtba7ouHTWsbfhQXx8O2vvjPu7j6rWvfW29853vrG/8xm+sp556qt797nfXl33Zl1XVVWX0O97xjvqkT/qk+sRP/MT63u/93vqET/iE+piP+Zj6uZ/7ufrv//2/15d/+Zf/kY/bdd+O2BGSjIgZUK3eptPpc9rH+W4aUOiWoaGQDFrSe+gZyDZzXpw9J0CJCLh8LQciMlSoQZkZ5IyEMr+cb216+umn6/LystGNmePLIgu5ZFRg5qVzzbLJAEd1dHTUQ95ZxIJST0eA5spqXDRUImRRvPsaF4ADQLz85S9vdCXUPxqN6pFHHmkOMAvbOAeRWlUXZXEko9GoVZYyIOkcszAnC+BEZpm/repa+Y3H3WsgkxLkUDlGxh9Y0DEK60A2UyaGkSzq1njloDkjn0/HIIq3BnLFCTz93ljpVjqlrPa2L1Vda1RjSSYogSA5krYZ5kHToQJm5Nr+ZtFZ6qkxcrzGzyEnA8SxYrToW+bGPc+/FU5ad/cGKtH/mcNP20A3pHAAK3I6lCkA3HobN0fs5+4LdPk/GzWMSD2DXJyfn7dUUIJGeV46kePMPLGucfnGMnaH0wWS0yZiP15q17ve9a5661vfWq9//etrY2OjvvALv7C+6Iu+qKqqPvCBDzS258u//Mvr/Py8vuIrvqKm02m94hWvqHe84x3/b1RN3759ux15caRkf3+/ZyAIOAGACAkMh1xVLU9Z1VGXhIwDpNg+k8U+ELj8rChDNSUnf3Fx0XLOFxcXTWhRsjaH0kLuctk5j/F43KOhXfJNHGxSiVl8JKJBw3EClNLzMhdIORU+nZyctKg3jRM6LCnArJwEdtBS6F4OSaGNQhiUGGPis2i+fA56kFygejkH0apID31p3oy9N/ukPHDmm5ubveprUYbPof7JFiCSuWBjmk6nzZl5Veby8tWRuq2trWbQFYOhns1X5JXHYtJg+zvZEXKUjpqRFdVm+8a8J3kGSumc52SzlIw+3RdoYIRFjMZHLwAk7Iv8MF0EbMhjVl1zFCkfmb8GzHJPOF+5U/dI+TUuIMW/MQL0iYPzTM8gx1XVKvQzIGBPMnjI+5oTfc9agLt377a0lb2yJqmzWDhrwR5wxJzwyclJAy7STwBFHsthy6xZ2tQMCHyObiWAe6lVTVdVPfHEE/V93/d9z/u7X/3VX23/Xlpaqq/92q+tr/3ar/2jGtofeF2roUee4xPRZRFA5oRQjdo5EvqsEIQuoXKKnq373JOxhD5FE/pBQ6NoGsek7t69247iMKqQZhZ7HB0d9XI3Se/42fb2dqPLGE4Gm9G+efNmUy4sgs5Q1uDu3bt1cnLSUPewZzY6anl5uSnx2tpaPfbYYz1WYlhwcXp62oBJ5tYzBZDrYM18lkHVr9de536lgaXk5INzUFFcVY3GXllZqb29vVosFo0KBpw0gWHEs6EEo5a5M9FJOhIAKCM4hkrEBli4ALjT09PmWKqqV9yU4C1BHlkFflJuqqrNLfOSVV1eF0gUHR4dHbWqdqDK8T3Pyz2UJhFNcqJeonB2dtaKiRTekVFn8zOfnNEnucjjW5z7nTt3mlMC4Mx7PB43QGq89iQL7BRZihyrqu2/Iiy1IqrpE/wtFouazWa9mpGktd1bCuHWrVvt+BmHjd73TvTFYlF7e3u93HgWgWU9ATZDisXaAsnkDaA2NnlpemXP2JnJZFInJyf1spe9rMkBveTIrRewYa/Jp7XPXDwnzyaT4Qd1fbgc8f/L1307YlSoCGc2m/Uoroy0FGiNRledrNbX1xstyJhUdRvmKFBV10qTsacADuAvFovWGCJ/57NpEE9OTnrVzKLSpPj0c2ak/I7SAx+OHu3t7dV0Om2K5HOo7qru1XtbW1vNOKB60VKMCGrrzp077cgQo8cIaGkpgrAP5jss7EjDmuDDfVHIHAnjlsVz1svzOENGzx5C7vkZR60YzMx9Zd4a1ZtsCEOSFL4xGB/jlIUq9kkeTc6e8SKLjCKDrwjLczieLPgT9XMsp6enTfZF/fqdi/w5qzym5/NLS0uNkszCHB2Tkh72TJFM1lbks+zxdDptjogc+Duj2yGjgaXQwS1z9eYwGl2dk82CpmSQzJPeHB0d9Rwah4p+zUiXTG1ubjYgKfoHhjhRz5CTpgvAWf47qfHMIxtXVXc86eTkpNeXvqp6eWGOr6or4MMQAszmYN1F3nTfmmUPcrbBujqeZm0cP6Q/bEgCU+kjc01nOBp13dDI5O3bt/8Ai//w+qO8rlUmlsUuDHG29hNdZY/UPPpzfn7VmQi9636UGFoXyVZ1SJwTYDQy+mSooNzV1dXmXFVvMnaZ0xSJHhwctOpiipq0TvZBzgpjY2E0AANVxdYsIxrOVqSCegI8OII8D8gZQOuZQ6+qNsbMRS0tLfWqeTmnjDApqyjK3JL6svZVXaUrhbaGFxcXLfJPhyRiM7+nnnqqnV91vhr9mU4USGK8M7+7vr7e6xVeVS0a8FnOcXjEijPx86pqoCjfrlVVvWgyo8Wq/osGGFmphcwXMsRV/eYjHH9Wmyf7IxpUV0GOGFnPBkY4V8AOSyNyJivGQkY4YvRv1RXgzqp46yxdsLm52bo/TSaTBvA4evqFhdre3u6xVZkrtY5ZcAbQc4rz+bz1keZsrJN0F4eUQMa+A1fJoAA4mZMlL+5nv8ky8GLtAUqpLxE4vWCvksEAxMgJBsPnbt++3eRXAJFnspeXu/dMux+7h4Wkd5g10bE9S9D7oK6HEfH1r2u1uMx8kHzdsIiDoGhyQcgpi9eyZR6SAjlTzCBnXo8Sep6oAq3EmFd1OcKLi4u6fft2M4yqeL3pyYvRjQn6VyFJMQGF8/Ornsqo9Hweo0nZb9++3YskIOqsVqTMy8vL9cQTT9TJyUnduXOnlpaWGqXPIFlP98IiMLyZbxS1iSzNxxqIJFW2Qv87OzvNQWbv76qumCjbAdqHtbW1ZrwdFaDwm5ub7cUSUgr20L7mG5rk5mezWZOTxWLRKPk8N+mz2XXq8PCwGao0gGSQk0bnAR+3b99u4BKbcnZ2Vk8//XSLfunBfD5vVdfpFDjUfOOTtcgoUm46Da5IadjudJhPFRVqWZgNSzhj6R3gD6D5/9q7lxdJty8t/Csir5WZEZGXqnMObevIke1loDhx6kCc9EgQR+pEEBq8TBR0IPTQnjgTB90tiCNBdCCCf4TSCAoNLeiXc05dMiMi75mRmb9B8tnv88b5/jATTnWVzbuhqKrMiPey99prPetZz96bzSawQp9y6uwp1fm0AexZUFLz947pK4DiZAYAsOl02qOIk7IWCJNq9jzsm+2xb4xLLn/0fskupIgOiJXRYsL4nouLi7bZhizUXEjRHnCBfUpQKVufz+dtj+hk/GhO0hfk5irGjfZC3+TRnhgu45YrQtJf5vWyVDK0r6O9OBALYs4BTqfISaTA6uDgoBklRGpnmUSVJkoKcKqqrV2UZSayFCw4XCKH3d3d+vjxY3Owm5ub9eHDhxY4UecAgfqkmqVsJWluGxegjjgOjgsSTnGIrJ9oSwa0rjTOmk06HcFTH+b3OMLpdNomo0wpl64IimpjSYOqUXOEOzs7bUcyn01nJUBxzO7JQWeZQGazrmgmrlHWQJPm3rpAECYCMALuvD/nD3wIUARsMk2O3TskTW18PaesRD8JarK1rNGpgScoxGYAdVXVdoczJllPZy/7+/ut7MEGcuOHVIJz/O6b9KoxEiTW1deWZgEkWJykqGkacs91Y5BlCt8xnuviLyyDuYWCVqqREbJ//evfbCrPEAf0/I1p29rqjmNNtsCYeKYspdBe8FepHMecEVb6vv702fv7+8beCcLsKXUzWS7ST/ks+XPLytLfAHjoa7V/OxzmUje+KoGXOWjOYLD42c/Rhoz49e1VxyDmEiZ1qZyEVdWM6ejoqDlVxgdZrgc830X9qSumenJjY6M58P39/Xr79m2jmnPJgswVnSZLsWf14+Njy5jdkwDDu6QoiVGPRs/7325vb7esOFXIVdXoRAEJtSmQojDVBwX1qk5VurOz02jmqm4jFU4qx8MRfWi7X7YlH+edtLSgx1EIAv4WrGUaJhaHJhhYU47id5+kGn0WdZ/KVCfVcFQcpLGTNeVJNByLzMJzuSbnmpl3OlD2lTU7TMf9/X1b4y5zyEDlGTc3N+vjx49NDER3oN788ePHGo1GTSgna1+3rVQX53u4d26i4TkIpDJTrvopgMoADpwYy7wPSn65XLa5woZTByFIWSuvvAQkoeQvLy+buM14+6zgDzRlpgbAJm1v7pnneQb3OhvExthArrnNzDiFnimISkCDYVCyYcfsIDPhTD4SRGQAT0qZfzJW9jgwHxxvOho9Lz2cTqc9QRwAbMmWPry9va3lctlAk2WQAnlqSHJrz6F9He1VgRjlaXJD7oy1qjtGTlCRdaJgU2jj9wK376JGocqqji40KR8fHxulxAFa28uhMzxOiwNHm6WACNVmt550OkkvQ/hAxfpevyaBCZpgAuqFeHN9M/EKB4COs+RitXreE9jnODCOCyOgj2UT3jnrxDlG3i1FUlXVsiHLhvQV4OPkKEFLtkPM53OCh8DiOEl1vwRwxECphPVesibfQeVVdaIzgAmtCvylGAjIOD8/b0vHkl6UrU2n0xqNRvXp06eWcWTgE6Sur6/r8PCwZWY0CpmJ+A7n516eSbCrqpbhGH/2yu4FjaQfBdasXacAS/8ZbwEqAWwePiDAyKhvb29bBpYUtueXzXq3VEwr/whUOR8xS3yCe11cXDQgpjSEcRPkaUOy9l3VLYXMLSSz79lRCp7MD0BZ36T4jCAvx839BNdkaoABc1uwRq2fnZ01H5UgCbtoLGTs68yH+3hHPisBn13yJDD8YbIbn6MNGfHr26sOfYCqUNOcMWcATRNoWKO5WCya6m99VyHByJq7pKiTOsuJs1wua7VateUwKSxKR8IgBOgECJm5O7Uj1yqaSByOulMqFZMqVOvOrQg3Nzfr/Py8qrq1ihyQiZL1PDSq3a78P6kkjkTgkvVwiEnpc2yu9fT01JZM7e/vN3pOli77evv2bS9w6TcOBSUp2N7e3tbp6WnLhtVsjVdVNSrXn8PDw/YZ/cN+iHS2trbaiTGOaswtRr3z9vZ2q+OuOzQBMdcQp8hI0FN24fiSMnQCF/tPtfdyuayqatmO+rA6prJLZn9Jgesz66dzm8QsCdjbPAOp8U+WITNYjhxgMYfM2aoucJqLmZ0DEFmquby87OkDAMMMUnd3dzWfz9tcT/Cj/uo4RfMeGELVE0+lMNAYWA0hOAKfaQtVXd1VIE17yGCR/YNFMx+Ml3LOepni9va2Zah+DnAAFOtL6BKYZT17a2urzs/Pm/6A9oL/0Rf6WEsKP7N2dWCgUvLBhof29bQXB+Kst0FXAtfu7m5v2QODY9AmzOHhYauhMGRrhgm21ALT4AU6EytVigxVFpDUnUmUG11Ar4eHhw0Z5yQ9OzvrgQrZZTpRlDKKl+pWJmvScjoyZ/3n+ynq8TmZSdYEq6pRTGhVATXroRcXF+2ZvT+HgoLleKm9jS2a0WR2X+MrYJjoVV3NGE2OhgM0tre3W8ATjKqeHd/BwUGvVpslC30vqKa9ZP/LxASKpBVT8Oe9R6NR22rUzzTBJUElmxYUOXW0c24+YtzYlzXdgkDW2wEQ5RFZn0Dsvdm+0opxds2knLFM+kpmnvdMClamrg+NWZY5UMXemx0KiICcZ2Fv3s/vUfTW9AtA0+m0CfK03ICD3Vj3bJ01UWGOKyqdiNPcrqpe1giAr2sbgBbjBPhkIH56eqr5fN6eaWNjo2lVvEP6FOvj+QX3SfsFPvRXbpRicxlgGxg2DnxJ1tf50HV9B3Hfhw8f2s59n6sNGfHr24sDsYyGKpYTrqrmrLJWwggF4RS1+A7nrr7ECB3TlxvQowJle5TbVdWoJfQQxIjSyeyVsWb9KbOIra2tms1mPWOCRlNJnBkJxOv9KcVvbm5qsVg0h6Iel0tXxuNxm8z2wU1KPgUbFxcXrQ8FAIHVVp7+NkGNFWevJgsoCGIyU+Kpqq6Oq36em+7LrIwHp21MLy8vGxuQFGFVt7xDHc3fbCjZi1wDzaEZPyChqqMDs/6tL9O5pyP2vqhsAMe7rGdjnBxgBXSmkjdV0XQUlkClEE0WvF7/9Vzr2W9mqv6ozaOYASw2ql/YkeAAZKQC3ZxkU8bE/Ht8fF6ulIyPlRN8AtCUYEX/CTJKW/a0Nq7GwZza3OwOr5fJX15eNl8D5CklpN3I7rMEIKsEOD0X+0ywp98BGxuLmKuCq88vl8umQZEBs19iNs+dFPPm5mbbI954skljZjz8O+3M+EpQMBRWW6jX55ze3d2ts7OzXjlxaF++verQh6QPc4IbVJkD+i/VgwxWFp1iDBMjM2hZhUDE+eYiehM8ayoCJIdAiJXCCw4bSHCGqUCdy1+q+lvepQo5HZhnuLu7q9lsVvf39+0Yrqpqgc89UHJZoxKgvIe+y/qT7EgmImAYB/0hK+aUZYnEZlXP2Qd2AbgAXJQItBwn477+nAIlCjNrwoIaYQ9nwukJhEkDV3UiNsI3WQHhTtaKBUvUpu/p19WqO6gks91UH4/H41Y7Zl+ulbVKY+GZkoJMoY4sH5hEZxojmzUIIBw4O1vPWimGs87rPRPcAWueJ8Vt+d3cxvPu7q6pkfWb7IqYjY0BvvqasE+/yNDsKOf3qN9UH9N+bG5utjN9MXD6xDpxY+ZeQJTyy3w+7wFy7+GefBH7zYwzGZGHh4f69OlTr8RingBQWIhccsUe2bTnyHmc2XsCJKUeqvNcYodByuVVymHmrB3iPP9oNKp37971BKWYDEnO0L6O9qoNPRiRv2UFgmRmTFXdaTCyH3WJp6enlvVyylV9kQMH46jCzBDtqAN5OgghHR80LDB5Ds4QmPD5qmrOENL13JiApKxN4svLy1Yf47hln5ZwuI9szCQ2sdDWuQ7VZOLsquontTm1as6JM7C+ejqd9rZlrKp2PwHc94zh1dVVU0MLGknfJzWd9TD17ul02paD6RfrO2XXmdGk4EZmw8Y2NzcbPSjDTBoOeKnqju3T2JG6nown+9U70i5kYGdT6smyCiBNRp5qbkHz6empvXMudeLM2TIglUp7QVLd7+HhoQUttDsqfD17k7Xd3d31mB3Pty4gzJqjYK0vk74VkAkV+QJBiz1lMKyqViNWukk6XXZsTska850J19jYcrls7y/jTbCS9PmnT58anZ8ljmRyMGdAZ4qmgHWAke2ZJ0Ad0CXwAgvew/sSPPIfWVLzOdfhi3LFhiDvflnqqerWTHsfOov1s7DdK0H1z90Gavr17VWq6apuY4/cDaiqmtFwaklvcg4Mb3d3t2azWQuyVfWTmoXJUNUpahlgOje0qMyPsllQ4rzUYU5PT1sAW61Wve0ATRRLbQR7WSPn7vmou5fLZdsUgmNEPRJJqD9bWkWMIbPnTNap8jwo3Wequp2WBJiLi4ufbPphYgqCxm99WVMuG6nqlKCc6C9bBuXaKXry70+fPjWnuFgsGvWIcsuMLDMSDo+DzDGRyXIomcXmM3ECgntSe2wkQVJVR5W7fm6qkbRxUtR5BKjsliMVoAEz72yOCJzGem9vrzn3jY3+JiUZPPQpejXFPnQJxI8AVtZKzakEBMopdB/rdfLckerDhw8NhHPynoGDv7i4qPl83gMHeVygwO0Zq56zWsedpr4gld7KXY4gTFZKgPdcOcdynAT1XGpkTqRtAB954hmbzf7Kubi11e0iSLms3wi3zM0E27JYduNdsozGv7FF1Lh+AcTZh3kriDuqNRmSBK1D+/LtxYGYk9/f36/z8/M6Oztr9BSalRExWhPFBKiqZsRV/b2Jr66uWgaXamzXEhwySIzH47aVpSxF9pMio6x9MeA8EWh/f7/RtVXdcWgyRI6bmMl7oxs5VNmwd9za2qr5fN4ycnvSom7Vzaqqt/+zOhpBSm59CGEne5BLxB4eHuq7775r6w9PT09rsVg0gZrP5GQcj5/XM/oc9J5rNI0ZJ51BO0FWHujguujbqg7IKXXkIRWZ8Rr33D4SoMkShOulo+TsOHROTibm/wJoBhvU48NDt8vRw8OzavtXfuVXmjBHwJcte1a2l2Iw/UyQIyhYwga46nvfQ7sCArkO1fuZR/QHaFJzR3nHOlZ9tVo9HyQCoGJSMDCeRxuNRvXtt9+2naIE0qTwLy4ueruoCeSCITtKgddsNquTk5PG9iS9DoheX1/XDz/80HaTG4+fz9F2j9RVbG5u1qdPnxo78Ku/+quNxbB+NjPE0WjUdnZbL+3YmIcNZCA2b/0eOGATfEjaZe4lX9WBaWUZZSc+URlHIMVoqJ+zqzybOgV5aWv8jz7wrJ+jDRnx69uLAzGx0cbGRnPoqQC0uJwRzOfzNkENPIRJ0ZmUswlQ1W1RmRlNKiRTeJF0o0krkxU4n56e2u5bREcpBKuqdh+InMPKmqVaVFX9ZIkLejYFGsQYubmBCYvicq9cM+m5OFS0bAo0ki6r6jbb8PwEVsaFk5dBy2CBKBmWye67GfA8V2ZY6uvGoKoaaMGW5NpYWVkifPahD2R8GdA4Kg5akBHw1XI53VxWk7VLAYHASoZnJ7PxeNyj1vUJitshJgACG12vZQJI6SD1ZYpt0u7v758Pbcig4jrGODN8rE0yDGqxMmj9Z5mP/7Oz3NNcX8oukzJ/enpe6mdusmGgkJiwqnrr6AV4/9/Y2Kjj4+MWJLwj3+G5ZPOybBqJZLNcE0jMLPzm5qYmk0mvrLO+HDJpd32Qav30ffoYgLIOXqDTH57dNZNJ8TtaiCwZ+Z35qt/ZDeCjr/Sl7Nd7mhuSH8+cy+aG9vW1FwdiB7Yz0slkUovFoqqea12CMzRrwvuZuq8JwCkQJljc7jOpYEyhRDqB9boadaMsV+bi9yYgB2Cf6aurqzo8POxRjzIlawFTwVjVnRKV9U8bO3hH2ZNJ4kAEiF4wsRSGEwAotNyzdzweN+rv5OSk7U6VGUrW5vWZbCO3EPUzCJ8jTSeqbms8qrotGgEjgOD6+romk0lvY3oZWuoJAKUM8Jl96d/1wCHbTFoN8EDreWfZImbF+7Kd9TIDWpI9AVGTyaTRoicnJzWZTFrZAbBbLBb15s2btk/4/v5+2z3NdTc3N9uWoO6ZTjz3SRbAARr9CwiwPaBusVj8hH42B6u6degESyksM8cEbUHE/QAm9icby3pmUtWCBrun7Ha4BwCEHWLTWfd0XWBjd3e33r5920o7QK3AChCh4wGq3BnOvBYI/W69rMAmksVLcJL13qSD2R47TaocoM+s19xnA7mE0/dyV75McvJ9jI+yD1reihLvm9+118DnakNG/Pr24kAMtWf2oyaa+wDLFKo62g4tcnBw0NSgshz1DTSQpTyp+qS6lXFyzru7u239rWcTbNE6aGDX29h43ppxuVy2AJYitMxYMoPhjFBa9n/NbEZgypqv2hFnJAg7nD5Pb+JwT09PmyiNQvL+/r6Oj4+bKO7g4KBNdtnrmzdv6u3bt70sxz1QXSg9E3axWLS10wluqqoxARyjoIf+zwB/cnLSlmpRIusLIExQFqAFb85MGWNrq9s8PzPPquqxBlVdfQ+446QF7MPDwxbsUHiCkffkgDlL1wc8d3Z26uTkpNl7MhX+TeWLiXD9VN4Df8mM+P3GxkZb7sM2kp0xTzwvmzMGgFLWpQVgy1ienp7q8PCwHTKfWWBVNWFcBl79Cdwp01gmBfwBGg6zyDHI6yWd/Pbt2wYAEvz5DvW6JVLmCsCU/Qhg2SbSTm9sL9kDAVmQktU/Pj62JZRZouFPrq6uGhDKtcHYNe+RIDB1DfwM4RpQoR/TzxBj+r0xAIwBFuU4mgJzGEugNm2umJfGfGhfR3txIM4JDtldXl62PaUZRDoFgdRElTlay0pcxYg4RhM412ze3d21PVirqgUZjgbSns1mvZoPAYUJxVhRqoIgRy6ToKD1ncxUBHnXhWAzg1hfl2gdJPGXdxFwUaiEOre3tzWbzdrOZDc3N+0M3dls1tZe6w/ZkiCgbpuZDUckI7B8hTM2ifPdjKu6aS6Zcn9B/ptvvuldgwoV68FBCeRJc6OVs5acNf+dnZ3GHHgXfVzVrTXlYIE5FGuWG2QDGbw4aoFVUKjqdA1siI2iBAVfmWDWXT1jBgDq3zynOYMQB5v/z3dLFqiqo6w5bH0GWCYwq6omkgSAjAUQok85borxXGKVNK7vCFbGSDBgW1lX19d8AEAlK0zmy3ewXIIbhsTYpZ0BMuayoJksmWfKYAmAp5gtAyWbUvrw7tgONf7UzXhGWftq9bw5CX8wm8167CH7wqDpQzqJ1DfwhSlqE4iTBTMGwJj7fI42ZMSvby8OxEQYnAo6hGNIEYmAyoGmg80N4U3iquedXw4PD5tzRoFBcLIbm9ND75SUx8fHLXs2yWVfKDPBkLjFJISY0cMMOLN9johi0eQySdORC64EMQKArIDDc01iJgE/622CMlZhNpv1NrfnXAl7kuJWh0uRmYmaThpTwJmjXBNcyT4ADp9VM0Q3Ju15cXHRlgXJ9gETTk2Nj+NMB8J5cZ4cWVW3kYF/s0NsjQCZ45O2mwxGBvHHx+d18Bl0MgP3LkmlCtSZjXi/9WVm+hPQSOamqtvqM7Me4FDg4cwxTwkeUteAPSD+SXYhAygbW18ulYAgg2iKlqq6ZX8oV4wI22PHqUPwnoJk2icAw0Zl/eaPwGncveN6Lfb8/LzNqTxq1XgCaARqsnzjyHd5d89P36JuPZvNGpj2+Txp7s2bN43VIkjV32kfMubM3P3t/fPdk7lK1kjfAoGZHGRp7XO1IRC/vr14NHL9rfpj7mjDuAQXDpkh2aaO4yXMQclSG6NgOS4G8+HDh7ZbF3SntuLngpT7QsFqxpPJpD5+/Ngo6b29vTo/P2//txwpa3qZzXO+lLeWUFVV28Fmc3Oz5vN5W0JlW77cjD2DZ1XV0dFRC1TADiUlwDGbzRrY8DzGI8U1VdWcmCBvTFD3VR2lm9R7VZ9pUKO13lvmZL9h4CWDOceo7uzgcxtorAvWMB7oUihfNibrtBkMZycI+RynzRlbcobaFKj8nUIsgco12Y0tQ5UhqI4FBzVvz0lHAbgZHxmq4DubzRqzMBqN6vj4uFczzvo5x2vuGL+qfj042YXDw8NevRyThRZXxxVIbAOaqx48v35RE7e3dpZpBERrn5N90kf6ab0mjj0BoDY2NmqxWNRqtWobnXgHz8MH8EHmqPcRpMwngcrz2hBna2urre2XYVtuJ1NNwKDf+C2AeLFYtHLH+vxWSmL78/m8vvnmm2YXgilWz1zDKgB2rol14KeybJTBlf0AmWr1bHoQbX1d7VWqac52Nps1tP/w8NAQdVVXv8o6mUkuG2AcdkhisCaIoMQwx+NxmxyUhTJhzjap7KzvQrdZ80lByXQ6bQgzVZSyycy88tg/KlTOVnbOSXAaqVhE03Ou+iFpOGKf3NDg8vKyJpNJy8RlIyleEmQ5au94eXnZsmsAhhPlzJbLZeur0ahTQrt3LvnAEnDyebykZ8oanLExBvkc6Fzv4XuCa4rxBLoUAnrPdeTvWauqOWPPaKxlgr6X+4MLJLK5zJJSo1BVzWmui73YoXuvZ1Xq1dgDAZKNrN/H3xidrE0LfliTzGC9f1W3MsDzCZbj8bgpgdl4Bqas91v+p68ppgUgmVnus+xdqjqxEHDhuTyrUgn7NVbsLI9mnEwmbdlTivX0K3txPWNyfn7eNtDJOUpA9vDw0HyRewEL5kjOn+xHQVOwFIiNUVW1ZYL0Efzo3t5eAx8HBwcNYACD2DLzMUEm8IZdWS6XjZXje9WyF4vFZw3EQ0b8+vbiQEz8kJkTg7y9va0PHz40WtQuS1Wd002HwuFw5hC3z3Bqgufj42MzQvVk4i8IWdDOAGwJQ9ZuUsiCekUrq1PKor1L1pNkW9a0vn37tsbjcW9pR1V3ALd3SwEHJwOMpOBE34xGo0bN390971hlYwQTXtYmaFhLqh/QprIQgYUaPMUquT2l78psp9Npy+YttwG0jCGHgRLOWq+ArU8EXeOWtXf9kM5TvZnTM2ZVXe1fjf3x8bF3ClbW3gQ7oClrgzIeYyMgAFBJybvP+nVz/bug7PmAOQcdoM0FPsGV/YzH4wZ4U1TmGXMOsQnrngWGFBsKKGq+WV9XCiHs8d7L5bIts0lnnwpqdpgBTG2U/V1cXPQEUanK9l5J2yfbxVYy2K1WqzY3ptNpG2f3EHz1reczzvYsAOowAW/evGliLDYkgOaGOHkSErHWaDRqyYLr5bIz731wcNB248vfobOB0pxvVd1WoO6bJRIsivIOEG1zFT6AODKX9A3t62ivOgYxAxRnBfGZZMQoMjrZ8sPDQ8voZLkmuU05qqoFqMykqXsZdmYrHG1mVBwah4D2VifLAEXQBSHLAJycMx6PewfJM3bPnOpQAWU0GtV3333XqLoEFJ4NmrZRCnTM2ZmEHIOMUB85PUpQMrE4K/8GMi4vL2u5XPb2MB6Pu32D7RSEpufkIHMBSIkgRXjqo0CXTGa9xurz647VVoQcqOCzsdFt/JDvI+Ny3XVhVa77HI1GTTGcy01cD0ti9yQ1QM4wN3iQEWEUsq4MOAInORY2Lcm6qeVyQExVf1mK5891o2yJ4/U8CR7Niay15rVy6RdAISgfHBzU6elpVVWv7+0XLjOjcUgmCjDxDN4tmSXzTXnBvDWHAGz/zsAs02fPCYyTyUkmgX2kSBFQnc/nPU0IMZj3evPmTVvTK/hnVi/gCar8lneiaeHjco4IqMaSGn80GtU333zTdqFzdCxGK0Va68xjisD4jkxI1mvLaQc/dxuC/OvbiwMxBz2bzRoSPz8/7wlIBDpOzWlBidgYJIQtK93Y2Gj0IJQtw4Syq6rRPlXVgj1EWtWpZ3Prv1xitC7W4BhkPhTauXaXI5Ppy+JQtGhBaBVan81mrQ8EX+CFc7q/v6+Dg4N2TJm+4qBMmqTJc59Y/+a0MqAZi6TFZJ4HBwcNzRsPTi+zA/3CCcum3C+XcAnw6FM0ZWZkgpSglVlZ0tUZfDOws5enp6e2VCafP4NwVZfBoEr1nz6i4M9AzskSUmUtXcsNE2RDQIW+J8bhmD1HCs/Wn1sfyNRloIRwwMNisajpdNrKOglM1dLNPcBYtlvVLfWTxcsqZWUcPJYF2+EeWU5BiSaABaYFJvP26OioqfLVW9kdWwWc6ErYT/Yd7YAgLpChy4Fi9dAsV2UJB3CQIZuLqbtIcVtSvXyYWrIxo1L2XHt7e81Wc8MXwRhIQ+Pn+Jg/6sx5AhXQ4nOZLaP5MYiShru7u9569qF9He3FgZjUPmkgSI9hmAC5fIIDQKVkBpCIX43KFpqj0ajm83mjXCHd5XLZEysl9UixSN3MIGW2ufaPQzPRfBatY62uieL9k6JDn6kXe5cUjHGkh4eHDaVmhglMZB3WBM4+kwUCMnluqfet6mp7VdWrj8nw1I/Ul1Ll6lAGDsxz5bUykNo8RZ8ZT88gY5PdcF5V3TaiGAwgQqDggAR612A3VdWrH7ufcZL5GFMHFtgTfD6fN+d+dHTUo029g7G8urpqy83QnYCnQJNCPI5RfyQtnOIq12HPqeQXIDhR186+YnfmZC6/Wq+Hu6/Aa/WCbJPt5L7E+tvac5u+KBVw/inCdIauOeQoTFklv2Gest2sv7u299ZfwLE5IsMFtoE3z6IfgPOdnZ06Pz/vbZazWq3a+6Wdy7BTibzOLCTg0H8y6hQi6g82RuMiMNuPm1+9vLxsG3LoC/6UyFXWnvNpvWTi2sYptRJ8xOdoQ0b8+vbiQHxwcFBv3rxpYgSBbWNjo2V9nN90Oq35fN6ONbTJhL1tHx8fe+d3yh5c31mlHFxVtcwtaSmGTMSRwCDR8Pn5eR0eHjYnykmgfFDDUGQCBIE266Z5TqogUVVNncgRqG0npf701O0yVdUdI2niPD4+tsPVUesmfPbxOi2eNeiko/SLzIsTq6qe+rSq2/s2A4XAkNnZem1UIOYc0fcmpKDvZ36eNXuf008CiWtxgsRjeZ4uyjPtIpe/pDCIOM62iVkTN/ayn8fHbptLYzga9Zd8pTDLc+USLv3tc1l/1q+ClECdgEcA+vDhQxtrz6XPsSmuRdmPOUowtx5IjJ97YYUEJ1k7MHx4eNgycOPB5lL3ARg4jcs8Y+tAWbJOwGau0HAtc9Mcz3p/lnP4jPVyRlW1M7JdT0YqaApuvgO4ZuLBPsyVnAfGlO9IDQKAK3ALkAcHBy1bNhf5Ac/FtvkN2XAyEnQ4AjghYi4NNKcyex7a19FeVSOWpdkQQqARfFPlitIy2dBbWc+V4cqcNzc32+lI6jbHx8ftO1XPFNJoNGriK47fpHKfqi47zIBQVY32TWEQqkutFq2TymR7ZKO4J5NJW5tcVe2dc3N32YMMRB/Kzu/v7+vHH3/sCXjyWMWnp6feLkH6Ab0tCMsKOAUgJClpTkRgyOw+M6gMTlXVJi2a9rvvvmv3RrmjrNXTZXopbNEnQIJrAwrsQr8rdbgG6hJNyhapfdX4zs7OmhaBo7KxS67xtdzIGMnegB3BxjpQfbpardquT0ADFqiqU1lzeqlbyLqnACGA6P/M9gCGXILFdlOQKOsCaAV8Tt3n1adns1nN5/Pe0hp97LD6DB6bm5utHpp1XuMq6zbvjDXwIdAkqzYejxuoVAfXlwCTOZ1A5/T0tDY2Nnobz5jfCdpSzCSgYTLYYAY+LNd0Om2bh2TWj51Ilb4mc83kwXfYke+o/aopHx0d1e7ubp2cnLS+5UMABYFWiYKPM7dyaWEKUE9OTlqGjT20l/rQvp724kCcSJbDrOrUeyYDxJZCGg4fihPEBaaskaVKcHNzs2XbVdXoMcIpwUvQgl6hWdeVtXKughhHmssAZABZm+Igkn5VP8pNLDiaPLRC/Tl377L7V2Z8CVbu7u4a9awubyLbOKOqC2ycD0dlkj49PdXJyUlTCFMG6xdZSV4n+w1tSK2e1B8nVtVfssYRcToCtHfinIAoYyf4cTgp1EsqmoiI4IXDkkk9Pj62TRPOz89bsFW3Z8uyPfbKvrKuqY8zcxawd3Z26ujoqGXIxjZrilmL1GeYBTSwZ1CakJln7ZxKmN0JNDJH/YiN0cccuEw/N9fJTN28FHSNDxrV/Dg7O+uxA7l0SLA3x8wloJWdm1P6rKrb25yP0Sea98jla9bbmhPKNJm9eif26X5ZS+Zz9ENmqwko9DM7N0aYqhRHVXWb9gC6/FwG9apuHfy6vmMdnPKVfpdMivstFotmY8SUQJ/Pypo/ZyAeqOnXt1ftNS0Ip9KQcRDBMD5ZQNJpMijGaw9m4iFb/0F3jBJllHSMejDkjs7NLBAllAKYqm5Npokjc2O41k+q68iQTdrxeNzOTwUcvGvWpv2c01ADfPPmTe3t7bUt7Cxp8Syc6GQyaZm0vlC3znpgZrSr1aoJhxIcWEKibzIDzjGt6lTISXX6rAzVz4wHZ6gvAS9BLzc8UNuiWuWAMjAnW6JvZWNV3TaZxty2jeyS8/a+KdJiZ7leF+gQ5ImP2BEHq+6JltU/AAGQxOnl3umy1gySntEYAq2uYVlcZtZZEpBpsjvsiT71c4EYIGAbBJKC4/pmO+bZ+fl5b9yr+sGGw7cRjEBjLAV3dK6leElrA1H6mx9AlwvosvGkbTFimZECWH4v4KGfb25umi0mCJIssFVzL6loY+Xf5g8glBm/6wP/b9++bTbDLpLq9l3P5Lr+je5OZuj6+rrtB1BVvU1ArF23YxwR3tC+nvaqDT3SoKu6c2wFioeHh5rNZj2BBmd7cXHRVLda1uM4VvQj2sa/GTNhDhou65E50Rm+a3PmkKWMFELlJAkcMnMj9BFw1GpMkBQk7ezstDWzgosAQI3KqXO4qFP1HNnN1dVVL+De39/XfD7v1dr9nGOWUXByaGKKWbV3DsA4qMHLuPUj4VyOVR6tKJAl9ca5yWAFm8y0Mjij46qqOZVca+la8/m8Odp0wOvCKc5KkE5KWSC0pI3dGkNBs6pavZfSHZgEBO2rrATh4A397D5Z0snaHBvLYwP1AaEPkGCNL/BrzHZ2dmo+n7eM/8cff2zfHY/HPYVsBku2qf8A39yCUQAUgGezWSvV5C5iAE0G6gSktBhV1YKRz6T9sktBOQEQu9Zygxfggm35jnfOE7MwahcXFy0zzBrr/v5+rzbumTP4bmxs9Oq5+f0UpLk/e2A7luul6FSGnX4rVwvwA5gq46Z2biwFd314cHDQfCowsd5HP3cbMuLXtxcHYg4AYsx9bKfTaduLuqpbt8fpqW1w2oIEdJY0N2NOEQcqqqqak5bhMbAUIplsDEJtLLMK6JJ4QiDn9E3KnOice9VzZvT+/fv6lV/5lbammBLX5JIJZw1aJiIjTGo6HZB+TKFMCoCyNqRP1eyS+tLXucxJv0DdSUlm7YuThPKrqjltVLJreo5kG/S1P7IYLRW9rq2el4Ip9wcQHh4eev3KSQvodlxSO1cPlGVhUoCB0WjUgq4ygEAHXGEVOHZbcupPz+pexsu7KFckVWoM2DD7ShsyPzLr5PSBBg69qtpKgM3NzXaKk2xSGSfZLPMAzQzEYbK8i5o7odr+/n5P74FC9/nc4Ss36wEOBRb3YZP6no1hCyzVSRtwr1wDbtkZ+8qy1ebmZrMLwTZ1Ilnj1/+CF3Yjyw6ele9K4ZdrZQlHYpFL34wv/4DNEFgxLOkz2e/j47No78OHD729CgRwfyc41sfz+fz/4vGH9ofZXnXoQ4oydnZ22nq7zMAYXVI1WaOwPMKaYghPndW/0WdVzwYs0DPSRJfEDZy2Wo+AZ9ILJhBlCsms8ZSR+rxaV1WXqUDH1Lfoc45YQNre3q7j4+Pm2Di7qi4b5mBz6YosJJ9D0AAIPLtn0t+cq/dMtaSMisPPPqEo10x0dGbubpUAB+CROSc7oqUwKR1WZhxqbIIN8CYI57jIhKqqPQ8AJwjZRENwZlcAT4pe2Kcsrqp6y/B8nuBLbVKAzLWw7FKWB4zKiNhUUodsBtOSgM/yPwDFzwVf624BCvZ5dnbWlsV4Z89kxcJqtWrnBAPZsjVreXOJk/ppVQfSgM1kIbx3irnYuTEDbomj2K95RbDIzvWx4AjAVlVPQS7IZq3UfdkDUZ9nsbd10srsLwO5Gjz75Dv0ea5rtne0QM535sY+CTTYGkDg73XxqVIHH+aPXdC8G20On2iuuZe++xxtyIhf314ciN+/f9/oKAEVlWl9a9VzYDk+Pu5lapycpvYnGDF0n0vkKLv2f7Sxs42rqhnf6elpr76bmWUKWyynyKzA6SmZJXI2HKbPyu44rNFoVGdnZz2hlQBIZbm9vd2OH7Ppw/pWefoiaUWIn2AHWk+RU9Zxc+9nAVuQqOrO881am0zJ5PTsnHFmIiY+0CXLvLi4qPPz8+YEHh4earFY1GQyadSga2cd0Z+k4lNwd3Z21jJR2dXW1larg3PAaGzHO3KU9kY2Zvv7+3V3d9eOjEwnRWhnTAAkQinvnmWNXO/r9yn0kkmiPDMj8j36BoCRKEjWnRmogHt4eFjX19f1/fff18PDQysHff/993VyctL6IIGAa5gHNtcwdzO461tgFhDe2dmpxWLRxsnSNX1MI+C92B1Bk/uug0YtN8Iwtln3TS2GXb5SWPnw8NCAifdJQZO18lWd0ti8yLpv1qeNl6Dqmt4t2Ywsf/kZwG58+R7Bns0RtmXZgy/idwACczQTn2QX2DsARbyoRJJAeWhfvr04EFNcMmJBNKlh2aPanXXGglxOdBOLMxSUONvMPtGgaPGkvCFgxiYbQZdx+rIuFA5Ddj+bBTB2Aoes4yRg8M4mZqJ9gfPw8LBNLJNbzVDWpi6cdLGMy+RPqo2jSDFRVbeMwfioDUP4Mh5B1+e8f2Zb+nF3d7edYkTAIhsBVHJjgMx88tmSbsvMDhhBE9IRqL1xZDIujsuadnYpYLA96mv2Zd03B68/qJaBASwGih4N690EJc8NmOpPv9cPxjRVxew0ac4EJZy9a6nJoy6rqrdt5NHRUbMTm22g180dfUTgY4mT37HpLAEBeeyN/ekLugnlnayFGic/A85SIZ7Lw9hdzidZuEw5GTfZvfookJR12lTEm/9Zkkq6GNsjYAI5+iqpcBv9uE8CCXarzIGlsyHQ+kqJu7u7JpTLIA54AM36y3777BM78fbt28YK8KH6HgDIur1n/VxtyIhf314ciDnf3d3dRrea4FXVC477+/styEDTslCORy3XoN3f37cMUEbJqD5+/Fj3991xYlXdoQ0coow7BVECBWWzie5MY7UTwiBZUgZdoEEQIk7xdwoqqjqAYp2xSc/hcNLqU+sCo6Q2cztQfZfConTemAcOAwVcVa2/OQBgICllji5FJBwZ8Y4gZ6OI/I4gk+UATjqb66ZzFIgSMF1cXPTGldPe399vmd1kMmknyWSdHXDy/xSLobYFbv2eAE8/smeUru1dBaSkZV1fM1brZQJZoYwnGQ7Ka8DCdbLGSmyXdfE2maOuOxqNemvVZdGCdtVzEHKkZQInvzfeucGGrBgTxGZlx2zu4eGhrfnXL5YiJihZp0kTkBoL4MiYZHlD+YLYENvjGvpWnwHz7msFQjIjgmcuBfR5wAWwBW4EVnPs/v6+7dsNNLiP8o1se2trqwFPexUAmuZWrnXO87IBypubm7a9qOcCSNcPuEj2a2hfR3txIM5aDbTrbxTUfD5vNJxJyOlUVa/+C1Wb4HnMVzr16+vrms/nLatGE1Y9T3bon/FlPXh7e7t9D42dwcFSlJubmzo5OWlLjLLOi4LmEE260WjUNvCAODc2Nur4+Ljn3DNL0qBpkyGDMsTr95nNYQg4N0GXM0aVYxDU2TgjdJ13F7wyc9vb22uIP0ECG6iqHgDjjN3P5D85OWmUdNLVlrAlhec67ml8kl5H6ecyqHTMbCozcnVYwUFGJXtNSpMjzQ0rBLsEdp7RM1sJ4Gg9jENmPurzAM/W1larN/sZxWuObY4Nu9Un+/v7Lav27Jubm3V8fNyceFW3FamfAZRodfdOEMeeidWSRfAsgkoGH38Ez2RpBMLxeNyA8WKxaJ9dLBat/5Jd8p0ED0An5knL/hYgq7qTrbwT281MMZfosbksGXh3zF2KvCxjQ7+z8WQS0n4wOvpFjddqkGRj2PZ0Om1sA79qGRJgYlxyExc/V/5hA5+Tmh4y4te3FwditbJUqWqCjQEXeDmUqvqJak89tarLbk1K2RBnlvSRwM1x5RIEm6Y7oAJ1JXsSVLe3t1sggkhNwKrn2hxQoP5lc31ZTFKKVd1yigwUBDiZlViWwDEkFc7x2gUHJeVdOIyk2fRfUqvoT4wBx+X91b+yLi0ArCuVgYKk5zgWClW0oBprVdXR0VGvruc5k2Y3vgCcrJu9sC3OK3c7Su0A58q+OMSkmjlB30eRs4U8i1gmhBGQWRlz//ZuHLHxzKMNZdMZjDnSpPCVeNiKIGNu5Y5OWTbIMo/rVHXOEBDLjW+MM22F/vHOKQIUTDJTzzqwd83sFuBmn+tKfEA6a6DGyfvoa9mpsamqlvHKwtlrgrxUlycbAsxkNp8AI+dUVbV5AqysMyfex7PzcwI4cRjb8x0sBIbm8fGx3r9/37LW+Xxeb9++7fkI9yK6zOeRAScjxnbZqJ97n6F9Pe1VGXHW1ra2thpiXq1WrdaRNdqqzskLSrKSVJXmeuAUmWSdVFbFiaJYBUAOR/3NZOM4bMspe7bnbDrDRLDr6/VkESZrnuAjEKDJNI6Mc0sAkyAlJ1tu/oASXBf/CGyZLVg/mEGCQ0efpiPwfAK0mrHgaIxzazzZQCqFZdVAgp8nJchJOq3L+H/48KEXwNTD7+/vW/kDw7K7u9ucPkDj3S4vL5sTAtD0Z9aKZRzrGz1YdmbsPTtwmTad9W6AIAGae+Se7Gww67FJfbMhz+97CTD8LqlP75g2x27NEWr7VCGr4dIaqEvqW7Yu4Jszxj4BpT4xVsAyAJ0CJe8PINsjO4OegCOQmgvshC3qowz4gppAn1k6O2GPwFdmwxg0/sq4p0Ax3yvP6fZztrxardrqhyxhmJf6TBJiTN3D/veSg/F43JTRea2c52wyKXvfZ2sA1OdsQ0b8+vaqQx9yswPIyl64Mor8/OPjY1NYMlg0CnTKuajxQOlk+ozGJGVoAqQAncISTiQVlZ55tXreYcY5tLnkxL3QfOj1RPaCDWRtIiXNxNEDFai4FMAQLuWWm5w1tiAnv75B3XK2QBD6EWqWxaKj9VECjDxFJ5+XI9E4KFmrkoGWzs6uYdPptNlLbrXHwe3u7tb+/n4tl8ueo17fLIUugPJd5pQ1M8EA8PFdGcBoNOodoZf1Y0HbexoLrMF6YHIvACGDQNqPPvTOuXSJmlumslgs6vT0tCdMkqkLQN4P0GRTqEi6g83NzUb5UomnQ9cnKUQUNNbLFK7vmZM5sh81m8e8mI+5gUXWM80bdoaBEciSmUlgmUJKQDvpaeNsS07vmHZhs55kglC4Nr3ITFf/AEN8wdXVVd3fP+9ilmxRVbV9z3OnQD4jPyeDzcw/d5zjv9jsfD5v18NOsDf+bN0HAqrOgffd1I8M7etoLw7EVT+tU6mlogEvLy+beEpgs3xEzc6uNtA1odRisajRaFRv375tznKdLs1tFBkq0U3SjrIrk3kymfSW7dj/GMpn+JyGyZI0sKxnNps11E3ElOsSXUumBOVyPlkTtSQk65rejyMx+TMTEaCz7iqIZEaam/ajATkx3/EeAkpu+5jiKc5HjS+XPm1tPW8ub3ILxgCJwCab1OdHR0ftmQSGg4OD3rIZOoSHh4fGqKTDTirOO6nz5jpWAUJfACG2dPS+diKyrruq2yoxSylJt1Ifez8OPWldAWv9BCJ25/s0DT4H3BjzXLKDgarqRFxApLERVAi+BDRZExsHQMxX9pjLk2T5gIlAjAYFbvxOH7ELGeLZ2VnrJ3YqKxdIjHH2Vyrp/b+qW4bovVOMVVXtHfw/wVZVtQM92IX7mpcCfm74kf4OoEZDb29vNyGq7+SBLsBdHmNpDiYbA7DTZOgbezroj2RAfF8gTp0FGptNDu3raS8OxInOoVeT3qTL7AL65PD8rKpbzrG+jeF4PK6PHz82QQkHauJUdfUmlJ+DvBk9+kbGDZ1yFAcHB/Xp06fevsQQNbFSCmBMANmI+mEqVlMksVwu2xpXmaTnFkzTmbx//75ms1nbmCR3ABNsZLnj8bjRsDIsDsJ9ZFoOVReok/IWlGQX6zQ5+hLlxflD00mnqQkLgpy8cc4Mws/0qQAkS87tQas6qjEdo+yPsM3zynrQjAKX/gOIvJffsT2Ocl1Q5jn8kf1lLdR7JqPg+TAFgm4e6JCiKQdizOfzWiwWdXV11eh5WShgxB5cnwMGQIkO7TAHNBubDNB+Zk7JyAEk45x9hYnKjNccEDRy7MwTy9NyX+6s67sPEGYMM6Dpi/Ua5/b2dqPRAQ/3yfXbbHBjY6PtdoaqZ9u5rM1cAUDzcIe0qWSq9CtWEFDQR6m18C5YL6UeNojyTyBhqWWWBtOnCOzAXZY02EKWM37uNgT517dXHYO4ublZ8/m8R4GkI0LVJH1NTJUZcdK3Jj7Ub31iUiwZkCaTSR0cHLSt/ARiz3h7e9tqj2qKSXdmPSprUBApUU0qgTMze3x8rNPT03r79m2rNXPCVdUcjfOPZWUmdFWXoQmaAAGBUmZ5vuP/FuZD1+q53iuP65OdGCP0tt9xEJyEiaq/s34v6+GgZcR5v6pq66b1eVW3fAo6z9qY2vZkMmnOPZ2uTCXX86IL9RNQtFp1J+fIuPIkoMz0BCbOjXPKvhKgZT2enzN1X+/n+p5F4JX56eP1TA8boE7JWQKWQNT+/n4dHh62dam7u7u1XC7r8PCwAamsQRtn5Y+qanYuaNnDGlBN8VJVp0Pwb398xrMmIzUej+v09LSurq56zAQQI3C7L72HAA/o5rIi8ydBoWdLtTXKmp3l//kVzJp3XV/54HeenY/wvuab0pD75FGk1huzXfe39MuOfEDpwcFBY+uyvJZ19cyUPWs+AztUhkktg3cS3PnooX0d7cWBeD6fN1R5dHTUC7aJ2DLj4cB3dnZ6wgaO3O+fnp6Xppyfn7fAh17hSNCVBwcHNZlMWlaetRYbVnzzzTcteBJ6eF7roO3CxbA5QM+grlLV7ee8v7/fakypnoWcIW/BGp3KyaHP0HAovKrqOXETWzBxUAZ6V6BJR8ZxPD091enpaTtEIml3fVHVCdNSMJT3zqwD3ZcBamNjoy3f4ji02WzWmAVrHrPmx5GnE7J23Duu17U9cwI4lCbVclW1s1Y3Np7XT3qvi4uLtutY1unyudQJcw0nZ2z82GdSuwKQfqzqGBDXWd/CU2YM7BlTAE2mZmWAJS9JkXK+5oZNaLLeK9BgoNigMg32QQDJzVCAl3WRTwouXZsPSEYiD82g+Tg4OKjDw8O23Csz8VRR69csM1HIK0llCcyYeP6keo2Bcby/v69Pnz61z6rhEgvSviSgRT9ngDw/P++NcVV3rKH5mmUMoGU8HreSiL4iZMSm0WRgnHLJoGweu4Hl8r65fwE/miWQz52xDhnx69urAvH19XXbvpLxymxQhlA9pyMQq9txSpD1uiM3WTi6DCCbm5ttbeq7d++a004K6vHxsQ4PD3vLYTgvRylWVQucVd2SD1kRBJp1GBNOTdiJMnd3z3vqAgZZo+boIXzviBmQ1aEvs1aUmycI5Bx9BkPP5/ucqaxNRii4AgCcdApIZIqoLD+H8InFgIR1ulr2iir3Dtn0tT4y1kmpcVqAnUzAeLMHz58AJ5+F89RfanLGU0ARRNxXk6FlWaSq2vexDVm+8I7uiXLOgycwApnVJ/VtjGzukOPKzs0dtqe2KSilHWeZ5/z8vGVNAPHW1vO2tMYkAXUKxdg/26rqWAsgdDwet+V/WcsHFAVY66jXgzBwoT8EX7YkMwSEBSBjoXyjHHJ0dNTKaJ4FtauOnMvi3rx5U8vlso0Df2Xck5JWQiMi1N9ZrzeXBXjjYb4BiU6D8s7e03yQsCwWi/Zc5mbW45VvqqodRoNWzzm3Pi+H9mXbiwOxrFQGKqAxSkFLcCCcqOqWxKj/cKIQXlUnhBE0tra2GrXMeaK3OaMMFrIAQiyZLSSeFJNMxwTKTE3gTgfh3xbCAx8cboqRMmDIGChIq/oHIMhGBSD0np/JDtapwXUakYOHwN0rAwsH6/kz8zHhoXFnmyZNzUF4X31Q1TlngIwdcHT627PKAvwsl5JlLTL/naCBQ8m1w/ojKTesCLuRTQGRWcvVZylsM1a5hpcDn0wmLUjoX/anTzJYpO2xdXSmrSmN62q1anacWZAlSJgIAXM6nTYbQocKCvoe2GGP9gV3UlXOM99LZsQ8V6JAHeun7KsEOsbQEh52kTaaau2qbl28v82NDLhZfljPpI0pzQXQVNVtD4pezndIIVuK1HL+aHkN829nZ6e9v3fS57nbF3vWb3nyEiAMAAAdynwYFHum0xjwC5KLN2/e1MnJSd3e3tZisej5HjT50L6e9uJAfHZ21iZ/VTVaJh1LCoE4SnQgY0mKKeuTuRyFCIUkX61oNps1J2O5lAk2nU7brlZOUxGE3JtzlFn7jEnNOP1bUBWMTRYZJUWryS+4TKfTdh1IWd1Y0JetZl+ti3AEf1kb5yRLqaqes394eGgTNNeuZu3MvYGUrKHqCwyGrCB3YMpsEGiiFOV40K6bm89LadCCmUVnzZhDTb2BwGtZW6pcfd517IYmK8xSiT/G/cOHD61+t04f5z7qsisOVJOFAoSeQ8udzVxXP/psPr9xTkcqmyb+811zjS0Rt3HggnieEsRmt7a22rKy1WpVp6en7T3YmDFIin1dKGRcABngnF0YA2yWPjRX/Fx5CbBP0JDZds4L4KCqK61gB/yMTbJ1ezwnW8EnyIQxSOyBcMs8Wa1WbYc7cwDDhUI2hwXmBCPGRWLChtLnuFYqus1bYw9sek9zNseKfwTygE5ZMCo7k6Chffn2qr2m0WCyBvWLqn6mhxp0BiqDU8+D0ihtZcwCT6ox1VsEPA7M/VJYxFhzb15OKGtgq9WqiSVkvynicl0oe10sxPHK7rNuKXvI+ldVNWFMOoUUG+VGH4my10GBdzXZsv98X2YCLAA7nAjqy1hVVW9NN+WoZ9CX5+fn7ZzZzHj0AaCFJvYsSfMT8Ag8Mhp9m9kOJ2dNrX2xBWz3yfFCzXp3ZQ8ZDvCRy6mMZdb49XUGzLSBXMOepRF9DJR5d98zR7y35VKeu6p6Y1ZVzYlmZqcmzO7YrgzKs5gn/u1Z9cfx8XGjU9P2ZIcJCtkO2tUz60MZnjljX+2k6hMc5f2MlTo2xk0gYzcABls3jzxfBil9xz5znqwHt+xvFPV8Pu+t5TWHZfd8Wy7hdM2qaiAXtZ7zMNko38+tdc1BzNvJyUl7NmAFgMhkpaqaWn65XNb+/n6rRytHsPHP1YYa8evbiwMxw5alcmYmhmUoELAgJZAIgBC0U0Emk0nPiWeQRSOiwtPJ+ryJzJl4Ns+AQuKkt7a2elsSmghZB/azquoFPhOJk1utVnV0dNQcYn6Hs/OcSVuvqxYzQ6yqJubQt97PpBecIGCBRt95vvv7+7YJgOtC+cYUhVnVLSHhyDALSd0lAs9anvfQp/qQDXBiZ2dnTWglmOrzzGSAH7UzDsQzep+kTzOzRpGzBWuDLW8TUASDFA0BGPolnXWupRYYtKSABcMEpwmqcvmbvkgnqw/0v+/rM3XBFPB5JmPBFr2PQ1WwDMZTIJARJrDNgKVPjaka9/rRk8aeIDOzfLqHFK/l/QEr2Zv+zNJPApsUJPmbLVvLm3aSosYE7eYpW6yq1hcYhK2trVY/dk3zHH1O0yFhcd2Hh4eWuMiwMUQ3NzdNcwIgYacACXaYS5JSM2Aub29vt32pv//++wai2FIG+8/VhkD8+vbiQCwA5dZ91ity7iaFibm7u9sW7zN8k9SEsYFB1mQSge/s7DQa7uDgoLa2thqt7dSRXL85GnVLTmTujPfi4qIFy7x+1jJRrWnASb8DHeglW2ymCIKjubq6apt9pKDL5NInHChRh8DJiQMq6VQJTpbLZZ2enrbt+2wbOJ/PWx9hJ/I82szWBRTAymROUcho1K05fnx8bPfjFE5PT9v+uTJyS6sykzw8PGzvtlqt2gETnsezGUv9BezoQ8Fra2urFotFE4lRlI7H494pYfoga9e5Xtk4GCfPm1RpBjfB5enpqWUu7vX4+Ng74o/jTsea5QSboazT6Eo43hXYqnpeppY7rAG7apPmmRKFOWv8Uymca8IB49yNS7aXge7Dhw+tVu7aVOpnZ2e1WCxacKEC95yCBoDn/hgzmaTAsS4aEyCBfuOT4EH/V3Wq/9ReZP9raH0g1nnc+oBoa3v7eT98gdHnU3thbtG5eC7PwZ9giQC69bXKxolfzCWOGxsb9fbt2zbPsSbKNewdMJBdf+6MeGivby8OxKtVJ7EXhFIZiKrMbQ05BKhPbWhjY6MFa448aUxB0nINhoP242Qyo0pEaqImGpVhexYTzjOjZNWCMjNEAee2jgQuHIL7cHxEG4CCn3k2/ZQTmOPn0IAITiFpZe9mIsvYPLe6VE44QS37pKpbBoPWE2CArHTWxp2jlh3qX4ece8ak3QUvbEoCAiheloESzu9ycg8Pz8tL2JTxYQvz+bxtaGJMp9Npr0zCRtPJpzp9fRmMfkhgUtVRp/l+eeiJfvBZwScdLarTe2SdU9AwlsYvM8XMXI1/boozHj+vqbehhmPxzB90MAAH2Lq2devsUkBKatZ8oD0QXIBoQS4DCdCgNPH09NQTLuWuakAdlszWncYvj0NMelcQc0+1Wu+YWgOsneCfS7Jk7sY6tQ0CsXFyprHxMV7W3Wd54scff2ylkMvLyzo7O2vB0/0I466vr1t5Jul4n+Wn+Rl9Rylv3wKsw+dqQ0b8+vYq1TRnZIJkDZPDTHqTocowMhDIOjkLu9BQuLpX1jSg8arukHdZm40qEnmaCDIJ4p/7+/t2QlNVJ8AwYVI4BJnLJFKgBGXm3rdZP09xE0o+1Z+pYNaPwEIuuyDkYOCuxfFeXl62vZ1dB/LPtZ6Ct3fUd97V5wUjtSz0WyqzUyCUClYODSgiNJOZZzlBUOaAgRMBSH9n3Y/D8Tl6BPcnVHLMnLF5fHysyWRSk8mkBXCsjeCebEFS4AImG5FVVHV0v38b8ywz5HpR7ADH7TPGQPYGwKXaPgV45o4xUQYxb8wH15elAydEWewXMAA+gCFBl6BLv/sse85a6e7ublNy2689hWCCzu7ubgtYudyJIG06nTaQyOa9j3lryU+Wi4CfrJGzbcxFBlcgT4lH87mc+zkGxiRLdfYZMB4EYTYdYcNPT89LkXIlyt3dXQNC0+m0fTb9DxunCE8q3PNfX1/XyclJTafTto7fcjHjpS+H9nW0FwdijjXFVP5NBINWYzCpxE1jcA5xngCTC+TVbZxD7FCDFHzc39/XZDKpw8PDJqCw/Ea2xUGlcCjragJbbjsILboWIGHSCd6eTcaadLpsVg0cKkc3bm5u1sXFRTuD9Onp+dB5AUzgtVMPJ8BZn52dtT29sRT6iBPKIytz61GUWDIZqFVOxuS3kcTm5mY7/5bTyHohB3N1ddXel3NnD/pPdu07AhzxkqBqrJLCnE6nveAhm07Q5rhLVDYGBnggbEngVNUJnbw70Od918VGGfju7u4asEznX9WJ+1IHkMJA4MTfHD/BVgp5slQBOOSZvEDR9vZ2b2MaNWZUdO5xLJj6ruefTCY9AJfsBcGX7FCgmM/n7TAPqmjjZM6ORqM6Ozuri4uLms1mra8EJIBZKYR9pUDv/v6+BazJZFIXFxft3oBpvi/Qc35+3vNn/BVRqP+b90BHivEAMddIcZxSUZbYzK3M/C8vL5tOAbAHdl2bFqeq6tOnTw38VFUDz/oqhXPei9jLNphXV1dtq98EWEP7Otqrtrg8OjpqasHd3d12ssf5+XlNp9NefWU8HjeVniBu0glYVdUQ43g8brRwHsiNRqvq1u5RXqMBocPj4+MW5CgVbaiAfjPJUJHuzfELAiYYg0/HjH7a2NhoFCl6bnd3t703itKyDZP+5uamiT4ERk5Otg6guDfnjyLEGlR1Z8Gi/zK4ZU05VZb6WO1c1poAxuTWX7IK2TBHAgRx5uuTXJkia68ACUEZ8HN8fNzGOnUFwA1KkkahqlO657IjgT1LBlWd0jbrqAJB1sRlN5xwLgMTLNOO8zkSmOVyO+AnBUpAE8cNCLJDy6xGo1HbgSvbaDRqIA7j4lmrOtWwlQMp/KuqxkqoK6qTp3jJc3ruLElcXFy0uaoeqowik02mSr95Z0FX3yfbYyxThWysUNXmqLmkZp1LioCfZFHYVd4n5+ju7m4riyW9LPsGmv3Rl1TwCbYyA+Y/1p9NaSdLDnwp/6AsJHDnOOsf8xoYySWBwBzbGtrX0151DOJsNmviJEHOJGZ4KShJhAel5eb9jCIzFfQRpwOFcsbQt0kt4G1sbNSHDx+q6jlYqi9DsBAhB5P1R8aM3uE01tcf+4xAXfUcMDjLzKCBBQFIBi5YytyrqgcA0L3EFgKeSarWpE4rY9Q4eLUwe9jqWxSj6wkMNzc3tVgsGhgyZrkVYS6RkKUCI1XVgM329nZ9/PixJ1AydoKrzFK2xD4c58im3KOq29kINWg3MU4l15xiEwCurD1nUGFP/rARY5xiQEHYM3Ogrm+sBDnvKTPMTVtk8myxqlrWngBLoKqqpqvY3t6u+XxeFxcXbV1uZuEJchJIZkafdGueHpYlBiBT8Gb7Wbdlz5PJpObzees7gVkfJJjVb+aAbJIdC145L/S3erP9s/WZfhd0cte/ZAQ87/q7pk7FOGGaPH9moWwga+zmRTIoqOoELBcXF80mjHXqI7AWVO5JiScLAZyadxgNz2htvT5VU9e3n6sNNeLXtxcHYrSgiYxm4cAg5aRSOU4TTsDjsHLpAHrHxFkulw2RWgqQiI4ikHOSMaRQiVABQrV84PHxsaF2QVR2Z+OBqmoCrQwo5+fnjVrlUAWojY1uVy5gQ5ZJVLVcLluGjCL03ql+TXUsByoAO0Yy6VSf19fX19dtc3k1Ks4ja3Vq/3a3ykyJY4feUb63t7d1fHzcxiOVojJJgMLkJyBZFzwlgJNpcGaeB2BKYcx0Om1Bk4LVeCVYE7gyQGTm4Zo2t+D4vH86shQgsplc58xJasku3N3dtaVkzsyt6jZhQC8CLZTigp1xTAEX1gkLY8clLcVmsvVU2LIf9U02JtP1rOzNOwnMggj621w9PDxsz8k/qMWnUCiZKJmqbJk9qP1WdSd3CZ7sizAN4OYPADbZ48bGRttgxh7eApxny0z248ePvdo6fydQ+5PaCYCA8E1GnyUxJYjRaNQ+Yz56D3Sy/eL39/ebjbMnu6PJ5gkU3QdoMYYSHPNvaF9Pe3Eglk0Y+KxrGFxCE7UYwWE0GjUqm0MziWXCAhaDV1fNmkk6vBTVpJhmtVq1Yw7zrFkZH2fL6Xm+rMmlGEOQqapG4co41Whvbm7q8PCwt5zr4eGh0e4cz+npaXv/quopmwU0zteSJBQeRLu3t9ecZKJsQRBDYHmLwPL4+NhOwnJvk7nq2SlOJpOWLW5vb/eEJxw/B8wRpuPkPFJZr9ZnNyjOVrZqLPP/AnCKvzgrY6c/gIuqLmNmi4BLirE4QFmSe9toBdNCBCZY5bN6Nn0viCUo9KyCUFVXlzd39B/78jdWoarLijODcW0BRnabTI8M8unpqS1JquoYkwTLTjy7u7urt2/ftnuY7wLYyclJT+eQQjlCyOl0WpPJpL03oJSlDsHB3LcnMtBlPqNozcWc6+ZNLn9Eye7s7DSVPD8hA6e4ns/nVdWt2817CGb8WeoI0L2uJVBvbGw07UvqCVLHsLe314CpfjMmsvT0H4J7AhHakwS/fLCaepYF2B6bUzbCxn2ONmTEr28vDsTowqqOOmJYDEC2wRggvKz/5FIPTpfIJlGcScwB3t3d1cePH2s2m/WCBofBMAUP1yLdz0yTk0oKfX1ZiWym6nlHKQ6Zk76/v28bsHOc9/f39e7du96Zq+qTMiPoGtrXT+hBTn0+n7ddcQSHFD6ZWFkT5VAtk1gul/Xu3bsW6B8fH1t2I8P3rJPJpK0zfnp6aqwFoRbgJBtY38M5VZ0CLmo+62RVz8FFNitY+PdisWjlD/cD5Kq6IxWrut2pXLOqO2Iyl6h4Lo4/a8kAngBd1W1TqU5fVS0Dlk2lgzcOeY0EYGw496VOMZq6fgbQtPuqakLH+/v7ZuOcsvuwi6T0gbP1LSmTzpYtPT4+1ocPH9qzAQ5Yk9wC9JdR8YCX+SuQpd3e3t721tYTd+3s9LdVXS6Xze7ZJ39iPJNq9Vnr6DO4JvBjFymOSuaGXWDb+CzjlCCPQA27kKK89eVP7qu/Zbaew9gAD/QU5mvOw7SXLJFhnYzlbDZrczPZM0ByaF9Pe3EgZoy5C5CgwXiIBygNs+6UVB5naoJXddSkIDoej1t907UZNnqWwxD8U9yRCljBEIoWSGWDgr/7pkNeLBZtgwKTk2Ast7VbLpd1cnLSKG80vGwRLZ5rMJPmVIcW6GSfuQmJd+Gw9H8qpo1VgiGUOhCAyiX64qw+fvzYC3wctc07ZEN+dnBw0FA2gOIZUbGC5sPDQ52dnVVVl1k4cICtEApVVXOsHCqRWD43VkMmvl5PY3fJOggAxkC27ECFzEByrXfVs3rVNaqqLZEy1pmBuGfWknM7S/NCdup7xs+7ywyrqm36Dziy1xT3CID6JpW/HDDbYmtAqb7R5zbMSSFlqqvVv5N5Mb7GQBBJyt2KhPVTg7JfjAs7RnsrV7nuzc1NvX//vgFL2b/574hN1xO4gQkASWYMeFV1pZPcmEZ/J/V9enraGIes8+ZSJYdxAERKXNl3fAHfiG1TTkBNZ+0X0BTYgT0CQQEdYOYzBtX019VetY4Y3XtxcdHLMNVL0xhN+NzbluqRQVEZc+5VndBEJshJM8YUtggCSbNk1gtBaykS806yihRieH6nELkOGsg9UL0yh6zRqctyQLn9oJqy2py6qqBa1a0t9XnAA813cXHRtqAEYryHd0PzzWaz5hjReeh44CIzYdmXNbcyEp9B8bse5a3sABgx3mrJBFRV1TaJ4PgFAgyDwJkCmqpqy4SAp6pue0wBvqpbNqTPjXNVtycyRw/YuBeAiPpPilEtXd94fk40KU7PLNMVwC1NEeQTBJhLxhm4tUxtXT1r6R1AK8i6LvCUlHk64xyPLDHoN3Q+h++ZsACCqfcWVJLtEGRHo1HbFCQzM75CH41Gozo6OuotszG+yeKY+5ld5zgbxyxnYAsETO/lnZ+ento+CewmFe8+az4ul8u2giJ1COkD9/f3e0ctbmxstNKTefrmzZtWBwY8gaHFYtFKTnlwBpCSywoTJFxcXDQdQZZp7KXwudpATb++vTgQQ5FoyeVy2RO2HB0d9YwRHcsRqJMIGGirqi4zUWMzAU0cdS7BHAKs6ihuNZ+qaga9XC6rqkPoSY/7k5RvZtEyOo7d+1R1VKPMUt0JUPAzNJFjyCDl0WjUHKtJnEg4qVFiLZNW4D09PW2ZNuFXZrOyggQq29vbdXR01BwNBTWa09aQVPGj0ahRyJkJpPgps6WqZ8e9WCzajkrGWGafit3xuDspq6oa3aeGpl84dnZT1c8a3QOrYKOOHNOsb1OJq/MLIgITAJDiq3T4Kaoynrk7lD51nWRZEgCh4s0R92Gbsr6sKcuuc1nQ7e1ty7A8cwY51zHHAKSq6mXrVd1SpaxLAxzAGPCVm6Z4r7Ozs95qCTRyakhSPe05AeFfVj83R9Wbd3d36+joqIkfMxlY1wisb/ZC2OizgLnxScCcVK5dwTwL5kQA5LM2NzfbVq+C+sHBQSu1sF3aD31sDiX17l5V1XQ0mATPx1cBwea58lSKLmXM6SuH9nW0FwfiXCOcyzU4sVQ5mmyykqoug01FsM9tb2+3IwL39vba0h9K562trTo6OuqJn3IZiiBh2VIqUN3XZ5J+BirW6WoZembqmiC5ublZR0dHLUOBUk3Aqu4Yt6pq6xurqtWbUrlJ8Zt11RSO6P8Upbm25xQoPE8GHm1ra6tOTk5aVgOc6A/fFUxQmqkons1m9fT0vGuSdd6WcBAt6SOHtKM79YlskLgmx1Fg8EwClw0gZOnoSk5fLVA9UMDikFwnMwdBIGlE/SXYp2BrOp02Z75uX4JGBjlN0FEi4bgvLi5qOp22wJAMSwrYJpNJy5yvrq7q7OysZWepJmcHKaYz7zhwymjZpWBsDBJkJGWcQShBuM8D2rK1DBT+nyyF4Ojdjb95aRmaTJh95i5/Cc62trZ6eoXMhAF7bBB6PIVvSdliXjxPzmsAiK3oQ7YgiOpDx7eaa+PxuI6OjtoRsT7Ll7Kbg4OD+vTpUw9csFUlCu/HNtTg2QKWwRhdXl42cP+52pARv769SqwFdVZ1dCn6TR3FJv5Z38nJAB1CwFkDsg5xe3u7LZcilMrdcqqqKSRlBHaaklXKgtCGriX7NpkBCDt3CSAPDw/t+iYLYz4+Pq6Dg4M6Ojpqz5Lgo6o7UUn2xTmoEf2v//W/mvPn2NBPslFOTbYhw84NT2SAd3d3bYxyGY+zggUKAMf4cDSySUHB+3Cwt7e3TVnOHjJgPz4+1tHRUVtqUdVtEXh4eFjz+bwFGQyCcdjY2GjP+fj42HZ1kmnl0hrlBX1tTLIma0kPmwM0MkAnIDDeglbW+I2/PvM9gpxcpyso6BPvkEHOsjLP4Zm3tp7P8hVIqqqNrc8BDVlH5/i9P9aI/esHjID75fp8uy+ZQwcHB70MU3BPNss91U0F5Vxew44xabl3gIDifsaXL8k5ZBy2trbaXga5AiKzcr4pl/skuLKV7s7O8/7yRGBZ915fWWFOAb5o7PPz88YiCeiZKasL8zFZD8/27bfftvl5enrabGs8Htc333xT8/m8+T70u/dQHnv37l0Dd6msrqqeFmZ3d7fevn3bA4lD+/LtxYHYhKMkNJAcTIppZCsmoslSVc1RopRRYr7PIJM28j3GXvVcZ/v06VOju3Kye650aLl21PUEnLu754X7aDvOGH3sWauq1a8Eoa2trVY35LQ8k3flYKqqBVvrGW2KkgFZ7U09logkg3M6fQHVe1d1ClZ0v3fnzGWVnKAMASWeoEQAM5aZvVxfX7eTnmTJuRe5sRIkUGyEQMaN45PRo8j1LRCS2WyqVTPrkjVzjOxJIMkShTHLawpwVV3pQ9YhU2VTMp1fFjw19gQ8JPU9m816y2GwBmr1trfMDMh8QXEm3WsZlrknyyfusWVs1rKJ+Qjo1N+Nl/fN6+7t7bXv5Pp+AANwy7auANZ35mMqlAGfBMH6N0tTxkqWz36tzwUsszxgjGTrufKADemnBB75rGwWAwOk0YUI6FXVgNC6yBV44YcAD6Daux8eHvbYhNzeFdjK8kNVtze550gw6j0+VxvTmATiAABF3klEQVQy4te3V4m1OF+TgdNIitca1qrOAE3MpFzRTMvlshmjtcZVz5P248ePbXIRfQkEqXD1f5Tvmzdv2kHj7rtOZaWYR7ada4hXq1UT4vgOAVkunZJppCDCuwtIHBR1MzUkJwKBu3/WyvTvzc1N20gf2hW8qp6NP+vU/mSQgcRXq1VbrmGtsqCrj/UNkV7Scq6hEYDILKu6rMT/vY+6bDo/782xuAchm6CeClyAw/Oi4jgB/ZaZGoozwURm3LnbmICeIqLpdFpV1QR1GRTc23VoBFC5ya5k/VNw9rwpeEr9gmfxrMfHxw0Qmz8Cj0wI/a9/0K3X19d1enraAGJmdN7PvwU8fenfKaoDIHNMABpBTaBcrVZtAxDPpw/YFL+R/QM8plrb3FaiIDi7u7vrHb9qpQP2IneKM/f1s6wSE6e8ou7LVs1drI6snPAR+0DfIfvHcPGp+oveBFujPPPw8NBOzqrq2Dc7tQm+AjjQCsiy2+Vy2UAKPzi0r6e9OBDbIMIkUWdR19zb22v0tGxYJry1tdU2LhcsBY4MEOfn53V2dtYCoRrqxsZG+1vGtLe3V9PptKkXTSjXQ+HKUBMVozfTuRA4AA3U2pPJpN68eVOz2aynmBbAc7lRbu9JTKMfFotF+zyqWnbG8cl8CVJsEMLJcRg5MTOYAx8ybQABqKh6nsh5hCBnYjxkXsoDqDD3smREMNra2qrT09PeUjYta2VJZeqjrF/p11wSRTzF2aDdOBgMyDodm8EfMPGO+ke2XNU5wqpq69TZRgbNrBeqeQu+CcISzGT5xaYN/q8U4FkFVc2yvFR0o0UPDg7q6uqqZaUo6Pl8XpeXl73drTAwq9Wq1ck55dVq1dZ9Pz09NS2D2v729nZb4y+DZL8Cgv7BvLB74AGtKwPnT9hKirWSicmlWI+Pj21DHddItfXj42Mribheiu3Ma2rkLFlhN7a2ttopZlmKcl0+whgI6gLbaNSt/7UPv/mbteOsu68nF1oCkzzwQ6DGSphTyW7QFPCDgEImG+val6F92fbiQPzp06eazWbNuEwGtUgCAOIlNVZGypFWddRFni7E6DKjygwC5a1ec3/fHWfme4RfkLOJnY6HQ+Yc1OSS3pFFJP1swnvWXFphIvlcTmLPhJryHfSYz3GuHLo6kECbE19NNTO+3NgiywQHBwe92pbnNYFlqLKYPHOaMhZ4qOqobzW5s7Oz3prxBEWWQWFPUKscEscIADw8PLTa3zpYQKXKMLPMkDRdBiu0ocxaIAUO0/Ht7e018RAnlaWVzHIE4azzGge2yAbYTWYjKRByuEBqE5JtAqSS+hZMAEd9DmSiXdmw7DyXHaXAyDzEagmC5knOG9+RKY/H40ZnZ5+mmAmLdnx83NbTo7TZw2q1akcmyrbNZTZEza/fiRgxBQIvEOAZgVwMEfCAAUlAKgtPVozv8X6EUZYfGnO26VmwD6htpYzt7e3esa36h9iNf5TQ8J2eU78C+clgGb+NjY22BSZbMe/0w+dqAzX9+vaqGrEsL+sNibI4CLRJVfWobMYvkKdTZnAEK36e9VXOJSX5JqsJJFPlgJLyhdQ5h6TbBSkTMAM2B5T1He+btR+TVnaJLhNwqY191/PJGGUIAoXfz2azXh9wSpyi73gOVJgmM/N+An86ylzOoY9RXxxeZiL2TU7wwrHYbxhiN9YZPNPZcF4CEPU65kLwBz5yy82q7mxiY5xOWYD3HEn/o4JlKz7LeVb1Haws0L3XlcoABacuyxKozSHP6vq+mzQ0UZig7XMyKp/jwFPzYC65B5DIDpUDOHDK+AxEGdSScjcXsDiAN9swJlm7z+BufnpuTAFh4TrgzbZun3lQSWoj2KGT4YAmwMi7y66xO5kEqKuyGeCUzepv759lkewLZSWq9Sy9VHXKfJ8/ODhou4plaYQvzbnCpwKR7HIymTQAngyD5tpD+3raq1TTJhWnsFgsmvEQJlBTo+FSlKGOmFRxqjgdwICSkqGoZ0L0Aq5JCe1nTVrGxpGkc3bPqmrn56oJCX6Z4aJrEwhw7t43qTnPiRYXFFerVc1ms7ZsReB0tqp+EFSz7swxW3bCgW1ubvY25hAMdnaetwx0WAA0L5BXPTsMyyiADPTbZDJpAdEuYlkP5wBlKd4va11U8CcnJ406ruqcNUeeghuZ88bGRrMj9VCZex6wwCl556ouI8rlHe4H9LCBZGIEDE47bTDpQNmtYM4mOEOB0vUz8/QzgE/QSQEkMJF7B+d9BMVcAsWZozGPjo5aZukdjo6OWi3TTktV3cYXnkewPzo66r0LYJBrmGXXggH7x4gJRK6DLUiBpTHLZXT6Ae2N1QCUlK18zruu19ixWuwst2g1T50N7NlT1JdA1/gmePHvBI/m13w+b8v7bm5u2vab9CFbW8/nlW9udlt6yqRz722Ud9av3Scp8CzZKENU9Xfay+t/rjZkxK9vLw7EjuRSfyQcchZqBi1Gdn5+3urGMkEOLp0Yh2ySb29vt5pz1XPASLGWjSzs4OT39/f3vaUhHN7+/n7LRjOzyM0NTH6OI4UpSREKGH5vopoE3lMwtbvNeDxutKdJIEM5Pj6u1WpVJycnvYmLcpQFmkSLxaKJobKODXGr81mfLYhk1p8Zv20mZb1AU9WzM0FNAz+cqACadUPft8e3Ptja2urdRzZNNY5GY0sYERmAvpL1CZbqe7JaDiqDR1KqSaEDdHZQS5o1a3JAGHCTmdrOzk7N5/O2neDe3l4DT5by6M/UMLB5fel5sEqAogw2a9v5TqlzABhkQABq1rcFDyp5Y7i1tVXz+bx9L0+0yvX7bND3EnBUVS/YuidQmrR72hQ2wNxI6lyWLngJRLI9parsP3Tx5eVlzWazBt70TQqi9FWKzfgcfUHUmMpmSwTNk8Vi0a4JoCihmRfYEAH56ak7pMI92duHDx+aPT48PLQzu9ms/s9sO8sCmDuMIcCRLM3Qvp724kDsfFmGnZRsBj3Gfnd311t+4fM2eefUZTIQ8f39fe+gA0ZsosoUiWvUg2WtJvvt7W0tl8v69ttvW0aptqU2h3JMJw7tEwFx+lSvnEZVV5sUBFMRWVXN4XBSMjH9lcKK+/v7ViPjhNFmsoFc++l6NgvwDJY9UXrK3vSL91cPvL+/b9lM1haz/npzc9MCVQYFVGpVt9FJOglLox4eHtp4ARWCCUYCXSrrce3RqFOU5k5g+k5ASmDgZ94vlwTlddlqKnBlQwnQ7CKXQTyZE2yQrJ0dZo3SeKZ4TRDQ/wk8BSFZI5s292TQMiUMRS7X8ZkEJ0nNVz0HECdjERahYBeLRcvYfD/ri+PxuCfmSwDBxr1v1s39HEjxTrl+1/tb5ma7UbVRcxBNDNxmjdaz0mbwNcZBDTf9jN8pDwGGqZj2vJYAJkB0P77RXLu4uGjrgfkRJZZcpnlzc1NnZ2dtLTyRp/lT1d8cKAHr09PzYTGU25ZVCb6yfgH8c7UhI359e3EgTuUjWovzYKBJPct+oFTU3XQ6bRPJBhwQdlI/KfXPgxRkwxDldDptNeKsLxGbJLWoZpkqYhkVNA5xQpUm8Xg8bgrXqu4EIShaIOTMKDOhdUGYk+ZwqEFtbYlS5fSyVpn0sHeU7aT4hQDLswAy2AJ1LfczllA0h0IRr2+S4uUYDw8PG3hYr30LoJwXapqIbHNzs7eDkeBd9TyZj4+PazKZNOpfvVpGsl5DNHZV1WwmMzGfT7oR1ckm2GBVNapyPB63PuKsBXf3FGSqOupdP2cAEDT0dVU1AJq1+7w3Qdfj42MDWsY5yzbKBIICcHpzc9PAr9ry+uYmsq+rq6sGntUZBWvA4urqqgkLBalc0y7AmFvm9+3tbRvf29vbNq7YE59zH32dmV3u4AbMWgKJUUthmRPJErBnmU1fCMrulSyLcgxQBIglKOBb6DOMveze3N/e3q6PHz/Wp0+fajQa1du3bxurJOM3j4FYz0KYVtU/1tAzsndzKRkJ2Ts/4ACWoX0d7cWBmAEsFouGfGWpkCeHnceHCeAoqFQ+Zg1rNBq1PZnv7+8beqzqzpmtql6dTXZnxxgGDx2avJwgqk7mUdUJNPJe6CYGrc7EyY3H43Zcn35IAQ0nI6hB91B0VbfhhmCYWUduF1rVnUOa1OabN2/qm2++aY5AHUhA8BzWAVdVy46rnoOQ/Z8zq0RP5+TP/XhNbgEDU4BST2GJ4AIUyWg8v88Rsqn5caRqxmi9DBpqhw8P3TGYGWj1d9Y99dNyuWyZnsxBwOAE0dO+x3ZkSuwoNQtsLOfLujBrPVh4Xpld1pCNjQyNMwXIHHyQ8+no6KjHHPm+9fVV1ZgBtp9n4Vr2dnh4WNPptNGZ5+fnbcctAASQBhYFAYBNkNO3ykoCnEzt9PS0BTl1XrZhLgKa+puKmV3mmHsGrEBuh5rjpNz1ww8/tHEDJjFZ/IzMN0WChKnG2fxFpfNt2vb2du8ACTZBA5G1folIsj3mkmulPfG3ucGMvszTpbI/hvb1tBcH4lyyRPUoKNkBKeuqDJ7BWI4jGN3f39fR0VFPsJL7RKc4Crqsqh6yJEyC1jk3SkJBUGbg82jRzc3+XrtVHQqX1UG7Jj0DFig596ruiEPZiBqW7zoAHepHf3oHIhKOPmtWBDjYB5mHYGvLO+8liCSK5+wJxD59+tR7L47VxEVfQ9Vouqenp0Z/meBPT0/tffQl57mxsdEyKLVrNW+OQ4Dm6ATAfOaq6mUZlpxl0H98fGx77VZ1SlNjCyiko8TWpP16dgAr1bhsWEaSQdcfNUJ9pyxjAxVj5Z6pYk3KOdmbdUpahkRPQWjnuTl+2Xtutaiub+cy6/T1NxCofm/cLfdL4R1a1/0BIyAh6/VKD/pbHRVYw7gl8wU0ymwBafMbe2OczMcs/9jVi7bEuPIN5qpNd/Q932O+Ac6yYowcH0lHkEubACVsCyEjX4N5ACCMUYKv3PXPdVKsxsfmagrgoqp6y9qqqqeiHtqXb6/a0IMYJbPAqk7YUFWNEvX34+NjLRaLNqG2tp4393h46B/inbSnoJ4OWm3J/e7u7trpSrlQH5UnIFvycnd3Vx8/fmwTOgVVsiKoEuX78PDQ2/qR03awt+fl3DmYrNnoK8ga0hV0KJpNWqAkxUJoe0rIXI4AmMgcqp4ZBbXsy8vLtpzrxx9/rL29vVa7//jxY3tvatrb29uaz+e9Or1+NfHRiBkgchlI1iUFSZRiMiQphFI3ZF8+s147xRyk+paDkXmlEMnzpvDr6empUZayx6wPZha9Wq1aFpjvIwgDFJnhpG3YmlQQSjuSvRl//SPbJRLMsganTzypDISVANQSJLIhoMF701EITBcXFz2gkOI3zj+pe8+FqZHt5neznmxcjRXAmXoF80R9OnUSaPgUKKUQTZ/yO97HPc0Fn3dfICjrxvZnrurKJPzI8fFxT9xl3LwfzcPm5mYTtWadP+394eGhrS5ItglbBigBGWzS+CsZmac3NzdNtLmu2+Cf17fd/bnbUCN+fXvV6UsmD3Tm3xsbG+30JAbIAWxvP+/QhA65v79vTlB9kYMy0SBjKM5k83u/cyC3em1ucpBoWCYjG83aNgec1JNJ4XQU9U4TQuAhMjOB0ajoTQ7VROfUPYssQnZUVc3ZqjdzPLJoz67GXvUcoNLBzGaz9kwpBlEvd3RcVTUFKnpyNBq13Zo+fPjQnCt6E+WnnpvMgMxVHwI8nMLt7fMm+5bW5OYrsrvMCPSZsWFDGICkyI01UOf5UsmaAiJq1e3t7WaPVf2lVfrDWLtH/k4Q5Ygz09dPgGHab84PoCdpbO+SQTNrutgOwSqZjdxmcTKZ1Gq1qtPT0xYw1X0vLy9rsVi0PsUouaeMnSNPgQ9QIwCYM6kdEaCAr4ODg57Qz0lndpNDn4/H4zo+Pm5L/aqeg+T79+9boMZ00FzQE3gOwXexWDTAIRjpB5k0pgzIuri4aJmvZweuRqNRYwTZxnK5bAwJG6vqxJTJJAD69Bop7DLvMCgAtvHx3nxH1bNwy65pEglJUrKNAJFxSQbm525DIH59e3EgZlxJP5mAq1W3K05O4qyJWgN6e3tbx8fH9eHDh/rw4UNzPJnV5Bo/RppHBHJIlsKMRs8HMTC4pJar+vRkUjKuk3RcUpCEHRwL54euRkXpH5lfZoQyu8zmTHANwMk6aCJy4MTz6leMhH5SE0rEbdMDmVDWHolfjJnA75nOzs4aKPA+ibBReIItVTNnYjyMO0fPOWWWpcmyTGZbNk4mk+YAs8aadXQBT4CmfnZGbgqzqrq1w2r/+o5whn2rz/sOkKPWjfbElMjI9YUxA1plVpm5Glu25Llkp1XdudpsnF3ph+VyWdPptJVhjIfAl0yDd0zgkap5oiPP6l1cz7I8rImx9t3Mnt0bW/D09NQoVOUENrxarerjx49NjQ3UVnUHz/i/0gS2DpjwboKrpWPuhQUx/jb9kF2aO+ZRVbUd39TGnU28zhTIWvmcDx8+tDmXpS0shmwfMMvatoNhMIyAYwJWZTnsjDKFUoR7mjf65o9isPzFL35R/+yf/bP6b//tv9Xe3l791b/6V+sf/sN/2Cvxaf/23/7b+p3f+Z16//59/Yk/8SfqN37jN+ov/+W//AWe+rm9OBA7Okt9jxo0M0P0mN+bNAKNQPT09CyzV5cU6HLpCnqFSEp2h4IajUbNOTsphSIza1xVnXNPkUUuR5AVcL5V3SRXK4TWZR75fiatd3Afji2FXpzIwcFBHR4etkAou4WKZZH6zCTNOmpSmijrqmrZ53K5bOtzq6q3lMqzAg3EIhyr57V0R2lBHV+/OC+aY8iMn2MipMkaGgcjOAjcyUykI+dYOP3lctn2Ks/MLfsYVQ1EyHo902q1aqcfGa+sXaIuq7p6pMw8z7He29vrLdNLGhZ41IdJPabCWV+xGQBoZ2en9bGNOB4eHprNZgaORWFvwKHxks1Vdet9M7i71ubmZh0eHjZhGACTO9HpVzYDVBgjdVTAxpxSM07A6P9qwRsbG21drmBCpATM2P/Z7y2v40+wN/xGlhUE2vQpsl77awP/uf3ozs5OHR8f13Q6bT4ggys79+7rLAb2hz17J1oP4+BzhLEofHv7o7/Z1ObmZp2cnLR7+b6xY2vn5+etdGeO/FFqv/Ebv1G/9mu/Vv/lv/yX+vTpU/2dv/N36u3bt/W3/tbf6n3uP//n/1y/9Vu/Vf/yX/7L+rN/9s/Wv//3/77+3t/7e/Wf/tN/qj/+x//4F3n2V2fEDJCTUL9LB1/Vbcieil3O1Rq5FFcxmDzLFoWp7kd4ZOLP5/MmzJFxJZ0MlQpE4/G4bWC/nl0IxCjLdaotn7WqWtAQ9NVw0tHJvlxrb2+v1ZqyrgoU7Ow8n0Zlsrqu+0L4v6yufHl5WdPptKH/8XjclnVk7ZGDfvPmTX369KkHKjivVFI7hEEA4LzVLzkUgQIdCpxgGWR3du369OlTc9zGSGZDuVvVCV2wHLlNIIBEjQ8kpPLWQQFZr2U/+/v7vW0p3Qd7wS4J0e7u7hpwEghzGRxgkerXLGd4bgEgaX3XSNU120pwYA4eHx/X+fl5o2M3NzfbznQACvsD9IAu909nnqBKhuZ9BTLvlCxCVTXQYL7kfHp8fN6UBkgAJvmPFFsZF+uq9fFkMqnT09NmX+/evWsloAzWWafPteDmXwIfpRj9K6B99913DcBUPbNRe3t7bb0+WxuPu/OIzbcsg2VNeJ350eeeGRNgfgHf1rjLcl2XPwJi8lCOjx8/tvfJ66Gzgao/au33fu/36n/8j/9Rv/3bv12TyaQmk0n9zb/5N+t3f/d3fxKIb25u6h/8g39Qf/7P//mqqvprf+2v1T//5/+8/ut//a//bwRiDtnyFo5KsII4BU9OPRfRV1XLFGTPaqA+516pupQlpyKQganjcLR5iAFgwOCTMkXp+L2J4//Qs0nNofteokrX9pwmfK6ztpSK809Bh6zFzk6yXBNaxp3vn4K2qmoTPbN7qtukjHPjBo5QoOM0BP4MfugsQQSi54CprvWtGnIu5xiPx23dtL6XQWjqxTKO6XTanHuCGH8wHEmrC1roVXsj6xv2mdmyPvGuKbzL3aXQgHd3d416nE6ndXh42MSHQCCQwT69k8wu7RJ40M+exzzxfuPxuL777rvWRzkO61ku+9BXMnW2srOz02jmXAmBktdH+hUokamlhmE2m/0kGC0Wi1aKEbDYLZ9gnuoP6nKgh/8x1nQDVdWCKuCB4WBP2Te+kyxRlor4DTY5m83aFqs5x+hZXI8f2N/fr+3t7frxxx/bMyYljhkEFGXJwLAA+8tYRDabtsVuzV32gZ2RUABWbD7p/c/RvgTt/d//+3+vP/bH/lhvX/5f+7Vfqz/4gz+oi4uLxpZWVf36r/9677vL5bIuLy/r22+//UN73vX2Kg07J8jI0UhJz1R1k4MDsySBwWcAR4km1ZoODwUpm0LjOP4wkbC6Si7v8R1KxnTgmeVXdQYkEEKlqKSkUTklQcYkXhf56B9/C0gCrEwl11p611S8cjbQf9b7jA1ULZChtExCGQLVKQeQzyiY5lKlDJIcgs/mki1oPWl8zozDRo9ykMRMWgqexuNx24gin308HregZ3zu7u5aNpZZH5qc86JExTj4XDIB+k2/oi2TEfA+grtaHdvVF1XVQArnKxPGViQ49C7YFmKb1Dig9wkk0/7ZLoD7+PjYWCng1nMJilYvuI97yvwEQyDNWuXMvFDQ5p1nYodsGa2aDEFVtfHFzNzf3zfFMECDccI6AZJ+l+MuAAp8RFjmHQrefJJV2sEKe0aQSgiaQENpbjKZtEzZMjDzAtPg3c1tTIhgarwJW42HPsqthGkR2LQxylpozi/zFRhJ3cEflWZf72yC8tnZWS8QZ3t6eqp/8k/+Sf25P/fn6i/+xb/42Z/z/6+9aq9pu8dUdUd0PTw8Lzk5Pz/v0VMMVpDyPZOfsAYdzTDOz8/r8PCwt2RDBuFzkKSJQWyhJqVO5ZkZPRBgYkKVEHiKGNCqmcmbAOgeE8OmB9C4ICfges6sJVM2Q85nZ2ctCORaXU5zNBrVfD5vwYUQDCXM+a6rUzlfyNn7oOFTfc2Jyj6Oj49bnbCqelSfcUQHp034m2NLp1RVbVIAUpmlQvyem835rODAqQg23p2tYW0EHvQvZzWbzXrinsxIOa20m6pqgSaPoBScAAy2aC1s1lVvbm7q+Pi40eIAhtKK9wAysUFV/e1SU5sxn897NmtOcb7mlJ/5jEAoWwbegCSiI7a7Wq3q//yf/9PsvapakPJ8bAAABuKAU+PKPozX2dlZA6DGdTQa1XQ6rcVi0VvupnbseqkZqaqWWQt85g77Eezt7Iay9UdwZyc2IPLcbJGN8V+pVE8dB/CHzcv12gSZx8fHjcHIpXRAHgaKTZmnk8mk3r1715Zm0pf4nuTh9va27TNgM5dc7vhzty8lBHttln9/f1//6B/9o/r93//9+tf/+l9/pqd6WXtxIDaJZKQcm9M/qrqsKJd4ZP1jXcjEWNWhMnNGn5nMSc1mhoQeSqUp0RdkCYGaOFC0dzIJsobtMILMBkzkVBJnDSprcJxAKq3R1rkZBoRvba8JriaeNU+OI5e06OOqbkcqS5LUD7OuLYBvbz8fCKFMIKMQdDxnVScmUVtOOlU2klQZR6TW7boySltfAkxV3U5C7AgISUeeda+kswECwZgz44SwH3aaOjo6agHR+xtPoKCq2hpUlKPnJQhinwIWxic3HQGM0v7WSwLKEkDIOvuSwEOAsdxHtmWOpgpbv/kZu2c7tAlO4PLz1DWk+t9nktKv6k4wE2DMF8+gzKF2/fDwvHwMvWuJX4IEYDZLJOr92YdsEOi1dhbrwA5TDGoeAPP2uE7GLun5ZC2sMWaLNCrz+bz5SkGTrbDB4+PjBrSBxLu75/0QaCfu7+/b2mNJgu+oXT8+dnsy5BLGZIH4NDb16dOnVobIZWh/VNrx8XFvDKqqgdTj4+OffP7m5qb+7t/9u3V9fV3/5t/8mzo6OvpDetJf3l4ciFOdiwJLBJJ7yJpY6SA4ANlbUqr+LygmkmbwWePxHNbZZVD1rP7Pcec9UrnKWNXbvEPWllx3Npu1ewmCmX1YHiAAZS2XECYpTwiZQ4D6AQ+KZSBGf6LZ3D/FJVXVBDJUwYKhbMVk39jYqPfv3/eWfgAtNjvhlGQrGYj1Abo4g64MMLMrdXaqWKBKQPLs6p6z2axlLkmDctpYEHbBRvMkLMHPmKPos8aeoEuGL+gaS9fg/Ku6nZvQiFXVts6sqppOp+1dOEZg0bPkEhP2zpaur69bgH966k60wgJ4doxMiv+Ojo56S1X0YQrszAt9lJkrQJOlAwrsnKNWPqQKOnUR2CbZXlU1QMRus+wC5LAF8zcpdYI0Wgt9aP5hpjIztPzx/v6+KcsJ/AR7JTL3ODg4aPYHMOcyI88MTCobpI1ip4BtPtC7VXWHa6C/U+MCHGCasgTAT6TPSR2A2r55pzSXz/w52pfIiP/0n/7T9f3339fp6WkLvL/3e79Xf/JP/skmANaenp7q7//9v1+bm5v1O7/zO591c5OXthcHYrVXVO9oNGq0EaPkuDl0jotRJj3IecrukqLk1CE8k4MTZJDjcbdOObPV6+vrtpzHaTQcPONEqdudK+vFMtjxeNz22wUsoGfK3qenp3YMIYeB7kKvPT4+9o5nFECIUba2thp9KIDnhh8YhaenbkczTq+qAzrQvyBLlAXgAC1bW1u9jReqqjlAmVBV9cQzHHX2n5qo+6Zy1Lii1TAnsqGq7uSYqmpASXZZ1V+zrm4oAE8mk6qqdhIXJ+zZLC9Kh69P17M6NuXf7FN/sys14VyWhNpXF0xWhgP1TJkNyqQShCR9rCRgXlR1J435zHw+b4CYfVRV6xv9D2ioKd/e3tbh4WEP8LouAJCn/dilDUPj/fSLbF8/X11d1Xw+r7u7u5pOpzWZTNrmHQnglUc8k/3Z2UDSw/qGD0oG5v7+eSc5GeVq9byBiUAm+2TL5t/GxkY74chcWy6XDTAL7kCMMXItc3YymTRqf39/v+2Klj4LNe13o9Go9YV5kGKr9ZUMbL2q6uTkpM0xY6JPkzpnX1kK3Nvba9qcP0rtT/2pP1V/5s/8mfqt3/qt+sf/+B/Xjz/+WL/9279df/tv/+2qqvorf+Wv1G/+5m/WX/gLf6H+43/8j/X7v//79R/+w3/4KoJw1StV08vlsmazWdu8Q0ZJkSoDQI0IlBCSegoUzQg5Rc5AtsCpZYaVwhXZj0CFFoe+ZSI6G/ImIEMP+xuah2ItpVEng+Rl0wk68vmzFlXVbQEpUAnYSfNzht4ra7FQr74CTgAMoAHy9Wye6eHh+Sg/9aXcxjDXN2dNHA2nH5Pd4ADVmwSkDAZAWGYxgoqsW2BM6tnn1PgJ/mTo+uLTp0/tWdLJC5h52hBQwOEBN8mApCqbI9O33pOT5YBlkwmS/FuZIwOxcRqNRk3o6LOpoEVZ5p/crCTV/Oqd3v329rZ++OGHOj4+bg6Y7RljIC7FeIKfNdFV1d553aHL0i21S1YKC8FucxcqY6mf1+c+cCvTxHYo67i+YK2vc6ta9DPQv75vdI5vltqyXo5aXtcGVHVr9GWv5mn2UaqUgSflAUFdwpEMizkGdGbdmXDNXAQizSGn3aWP4R+UtvQd//JHrf2Lf/Ev6p/+039af+kv/aU6ODiov/7X/3r9jb/xN6qq6g/+4A8akPl3/+7f1S9+8YufiLN+/dd/vX7zN3/zD/25q14RiE16jnJra6vevXvXaOhUQT88PNR0Om0OXZARAKuqIT7IO2mXVPuiWxhYVX+jeVSoDEX9ROauzinbu7i4aMEYGEi6T93FZGXwMqSkifMgjFw/7RlRjBwdZ3Vz83zmqMllUqIWHx8fW/YomKX4Qj9Vdesg3fPp6amxFxxSBrUEObJbdTVjhvJPJuHpqVtT7Zm8X1X/oA99x+l57szcjasN773L5uZmo9M5SKKgrHGyE9kywJRUbtZXc82x58IUAF0CkO8mzVdVLYjIvKo6J600kyr4BGJJJ65nwkmbsm394Zn0p3nj2c0VcxA7QGS2vr0mB13VsRDmDlBqaRlmxZhZo4+q1Tx3rkqo6g6y8H0MTIqiciyABnVj9qAskjVnoES5BMuTNK2+Ze/JfKArBcqkzvUJUJECLoG9qmMczGHbaAJ17GZzc7OVLJ6envc5B6zTRwJuVpxkAsD/snuaC/7OfehsJDDey/d3dnbayXGfq30psdZ3331X/+pf/atf+rv/+T//Z/v37/7u7/5hPdKL24sD8bffftucN+oWemTgVV1WA8na19jn9vb2msFntnR1ddUmoJqJDT2qqlev1AQqjt+kgZAfH5+3r3MGskkKYZrMKaTixCDl//2//3dNp9MWyDyv+2bdkpOT5XKMWYc28TlbTpggSDMZl8tlb9esRN6CX4pLKNfVxahyUWOezXP7XYriEq3LVjliz8Wp2P0nhTX6p6oDcJwm0dnJyUn7nGChbzN7m0wm7cxWQWk6nTaBy+bm8yYMggwHy5GhHdlQ9ptsxji59t3dXaszoWoFhnXhlJ2XUgQkwGWGTjENyGTJYL32LIORGduow/xKwVouQzNWMu5vvvmmZT7AamaWgkbWPZ3wZU2+z2WJgA0QQNECAJBOMNLfar+CNzBk69HxuNv+03OyKXNBsDWvfGY0GtWHDx8aqJZJJ4NgnFPoyCaVFmzOwSYtvTPngJWnp6dWZslMdr1+DlixYbVfoDpLHyhy/8a8YLckFMaCLab/rXquBeeGNxsbG833meNAyNdCyQ7tub0qIzZZNcEFPZtLXzIAy1SyFiU4MQwOfHNzs1HIAo7JnMsUxuNxnZ2dNYcvwJusSXulSAZi5IwFP5OEw+PsdnZ2GvXOYSX9lbQYh5KgxLN4N9RoVbVt66qqUVo2o0DDuk4uxVgXOOUxjUBQ1uEEDDVu/eh9BBcgxGROoCM4u+bd3V07Hi+V3d5LtmpdMkaiqn9eMDUtW0KXuob6pCwDaELHJr0oqMma9AcHi2XQV7KeqmqAxb/X2Z/7+/t2gIC1obK8w8PD9l6AljnDltzPM5kLu7u7zWkLNr6TQKiqyzCz3n9+ft5TB3s/z8uOMTvGOqlhANbcNqYp8jEmxmG5XPbKS8AkwJwgRRCXUSoxCZD2tQbyzW9Uu74VjPzbc6Lhs7+ydASUCoz+r2+qusNPKNz1VwZtz4eeN058g01sgC1zLGvOmXzwjwmGvGv2ada07U7mHS8vL3u0NJvmBwXhXGWSVPvnaF8qI/5/ub16Z62sP/o/Z41WNakF4aQHKQuTXuSoGBqaKus+gnIKW/KIuFyyIZAJgIn+BSJONOssJrmsw/1M2uPj497eu7IvmU8KJzjl6XTaHBOxDwU15abaRQZwjjADGETOEZi8MqZ0up6z6hkwqW9z2pyg/hK8bNIhsKbCdWtrqxaLRXNQnKaALJtMtC0rGo/HtVgs2tgARwAHIOOZqHrR0v6dtLFAxqbQsgJhbnqg/x4fH9shGCnk87u0uY2N5xNx8jQe2U6eaQtQGMfd3d1eFisTzeVP3kXAXa2eDy9ABwvowK2ygWasZewAigBmrLAlWAxgMYGt/jT+lueg7TNIVFWzlaru/G7Xzjp8qpA3NzcbOyHrM1Z8A6FUVTWxmqDkmVIDwrfwJwBNAiF2lptoJONTVe3e7MAc1DcJnL0zIJ5Zd9Z8AVr2me+VPtIY8A2fPn1q1LJ3Go26TWYITM0ZgJhtKRtk6cLGJKenp+3zCfCG9uXbiwMxRzSZTBqdRNSE0lRHYWgZgCmLM8ByHpSQnK1MJIUqVV3tDtJF81BGynChVksnTDhKbo6HkZsIqMCcWNSX6maZCVPvWjOKZk71aU5QmaQgCmlnPVL/qfFgGgTOdD76Ss1MtqZeZjOVZB/8PrNhS2w8o8Drz8HBQe3u7rYN8QUg9KhrAg1JQ3NsgAkBXa7XTOftPVPNngI0Afv+/r69vzpaburiTwq82N39/X2dnZ31NqFAUfqen83n87bn8Ww2a1mdgDwajeoXv/hFG0OgEf2a4+vfMpN1YdzGxkbbg1vWaC6wCX9Pp9O2/7L75PZ+uWynqqu/s72cR0pLbEstWDacG7IAF4KmDFOw8G/Zss9kSQWIliEKKDJ4ICh37mMLAp0s3vNfX1+3QxEwasoTaRcJ6gFbfeGdU3gpgCYVzY/oWwDGvALSlNVyyWYKwvzc5xwvazxzTwXAU41asgAoAxh0DFXVW6KXLBS/PLSvp704EHOQuf2g/6dg5ePHj1X1nH3IBgVI+/XmMhQL0h1QsLm52ZxmBuhUl6Zow3NBj6gbk3UymbRJDxlXdVvPMfTcHCPXPboOpwlNb21ttc+5f1W3H+/NzU3N5/M6Pz9vuzgJVJyUdaLv3r1rfXJ7e9ucl40SnB2b4jHvJoACHRp1r++4p3FI5y9Y2alKsKiqNq76Levpo9Gozs7OepmTxrHv7u62DTxkgbnkjFPTd4JN1gvR8hxjUoF+pn/R1d9//32Nx+Perm2cOCekzghM6AvjvVqt2oYM7sFBA3ds0xaJsifjNRp1B7pjRYA5oAaYFAyssQaWBBBBwxxwfXasbp6Ufq5NNXYJVDy3scsyhOyJDRkXpZ20f58BiAVIYDWXyflOlkQABaxZLhHzO2yJ0kvWZJNqpgdhGyksZKv8RS7JwiSh1PV1VZddpz9AK+sz7IX5llv3so0USxpDnzHPnI/t8xISoEU/Ar27u7ttPwB9xHcB8t4lWbGhfT3txYH4m2++aTRhUjyMTw3CnxTXZPaDwnt4eKjvvvuuOQb0LqSqrpSBN1WZkKoskOFVdephkzcpJGhRdqrWhIa1Ntp31BU5ZsZc1dWVTFqO0XIaz5wHfqszcQbeFQDIQM9p24FLy5qbbFEQJZYRmGyEQCW+vb3dsny1zbOzs9bXkLwD56u6bDZFWJwcpO1dE5hwzN47g0jW3tS3OPSDg4O2panAm7W1qm4TkXT+aXPs0f1Xq+e1pbI1Tlvmy0EZE7Vb33cerEzFHLCOeWdnp2XBanWCj4xQEJhOp7155P2xMd4z6/SLxaIHiDLg6TfPia3h/JNtSSAig10ulz2Vvr/VE81p4+Ze4/G4l9nnmGJELD0DbM3PnZ2dOj097ZWjvLtxpZT3eYHH/EsFctacAa91oRmQBngBKPv7+z0ArZ/SZ+Q7EVASSrmnftCm02ljYVIoxv+Y70Sp2ADvnDVs78av6Munp6e2AgMYsTe269E88NefOxseasSvby8OxHt7e+2PCfnw8FDz+bx3/JvaU9XzFmMmI6dDvUc8oZaVm9wLoOhjAihUFfosHVVupp5rJS1LMrkYclI7ieA5LRNQMKjqRBRVXY3Ohv4mbdKgKDFrD7/99tveBvy+b5JZpuF3fn9zc9NOLDo4OGib9AuyuZ9xbnsoEHPOaGHoOoUrnmm9Fm9JCTDBAaDeOf/1Oq8aqQzBe8nCqrq1xbI347S3t1dHR0c1Ho/bebcyEEBBPTDrtlmucFa1c69Xq269a2YhMgPvjmbk3JUD0PsCmqV4MjN1OzbsWcwdATfpcWOcFH1SyTImNH0e+ZlZ5PHxccvuHAHnOQA5CtrMjBKoAlXmGPApGJ6enrbAwy7z2Eog6ODgoM2vw8PDFuRcr6o7D1zd2zzN2q17yfazbJEHaAjwKepy/awXA4mTyaSNf7J1lg0BMdYSp03SC7DjtAHN+2RZTskrl2+yLfYnaKYKng2yIaAXSKiqBthdMwO4wG3OK519zn2mPcfQXtdeHIhlKZBp7syiTqG+KOPgdFJQk5NxPp+3jSbOz8+bOIMRQ+xJzzw9PfV21iG62djYaFlAUji+L8sTGFE2UD+nhcpK9ac9mTkLwZyDk6ELRulEk466vb3t1dagVmg5+xBFaoLmJJY1pjIZ0lavFQD39/fr6OioZrNZy0wFBbV3QV127j5VXeZlLDl7/cEesu4k289s7eDgoCmNfU6wcl+q08lkUtvbz7scvX//vtXhk/UACGVFnHlV9XaFStoVnZeUuIzOO8gE0dScadLknL5n1pL6zOUnAgmwKGMGkNxXdm8fZnSqfv/hhx8a1Y7xkKmvU87mhjmFVtcEufG42wgGS5TZuMDgYBL2rJ6aLFSCdNtHYoAysOdcxBwAq8ZOSQpAF3zM2aOjo56yObNkQSgBUYJEtue5jVEqnGkG+LY80Uz/JEuQeyiwecAmxYPePftXvwCmGVAF0sxksU0plgSyZdlXV1dtuSFQblkZfze0r6e9OBDLsgSa3d3d+vDhQ9uJirFmEGDkVd2pN1XPk0DtUBA4PT1t2UAeIgHtqt0k/aPmqW6VTrPq2THmEguT0YROZMzoMwhb+4iuorhNWi6z9xR9cB5VXV1V1nB6etqyKtfJuiyxjP/bD1d/yODRat7HfRP15lKiquoFDoBknTZOmi8pzdzYQ3BR/0v1a1V3JjJK8OzsrN3Pc6VAS7BAGwrErpvrbY1timqSFuUYOdf1+jmbqerqxsCJrFSw4+yUP7AfaG7j6jqyv6pu+RKF/cHBQc/xc9iZvQI4l5eXbSkN4MeeMAMZqPSBLPDp6alHz7NvwFHGRYQnyGQWCcRgbcy5q6urOjg4aKBAHwjYav3o/mTF2J/AnAIvf+/t7bXlhObO/v5+U/sC7q5tbhsHwUxWrN8eHx/b+mbgB3jWN9g3QMk8SBbBPHftpHyB6mRgsCp8n2vyo/pDdo5KJ8zDOKXuwlzP37NfY4dh8LwSGoBgaF9Pe/FoOMmCs6uqnnKRk8pslpGmGMgWeo+Pj+18UE5Gnfjh4aHVcwQvyuGqboF/1lhNpt3d3eZIGXwureCAoEOZe05Igi/PQ2yUasd0frncR2BO4Y5MFGKWPQgu3hf1yzkKVN7B76i0BeOqas/umYwVB8hBEYQJTI5j5NSyLmV5D4DEQdzf3zfa0TPppwyKHKFnEzw9b9b7BDGZ0HK5bGBIkNPu7u7acX0oY7VbWbq+TBBBsS4rSxAl6xB00Oqj0ajevXtXV1dXrfbOthywIIBybmzJZ4FBcyZFWqNRpyAW1JQonLGKth+NnjfqILzKDNdn0MYJNLBJWYYB4ASnqm5VAke9sbFRR0dHzb4zy1ytVm2sZPgCknnHdgAKQQSrwD7MF++edL25KKgQubHlVBVrxs/zCILpvzIgJ5AyJzUqZr4OMPIdCvoEMRgToFTfPj09NTCB5ZLFJtOX/ixXWLAdvsW7yLrzflkqUp6oqnbKm/8P7etor1q+lOdu5gHrVc8IzPKJm5ubWiwWbYJxjB8+fGgOQEaQgRLdgnrhZFG6nAOECm1yhlXVM8zr6+um3IZWbdCxXC5bMMsa8c7OTk2n0x5dTL2dWQ8mIIUyKcLiKLMmVFWN/sp+c+yZd4Z03UvWirp9fHyss7Ozhm5dB+sAsPwyxe3FxUVzqsQdAul6kJAdQtkcxsnJSaupCc5Ygzwqr6paYPYc+lFQOzk5aSCFI/N+yhmyIOM9Ho/bWa6yA/eUYRG/CRzG+PHxsZ0ZTHXuGff395szYw8Z4GSq3lUAGo1G7Vp0D2zes2B2Uijl2YDH2WxWk8mkFotFEwKenp7WmzdvGuuEwkVBsicAh9iHfkMGnFnRZDJptqsJdoAdSpTdsL9U1efSKdkluptNsoPUYLBzLJtgASwns2Ls9JmA7nv5HfdL/5JKePSvee3zmfVmKcbcBngBmPF43MZBBq8EwGYAQ+91eHjYgncuL/JMll0dHx/XeDyu5XL5ky1X+QPXNV8mk0ktl8smyDRG5vH79+8bQwlUfM467lAjfn171fIlexhzMrnbkyBWVS2r5Fw5rKpug32oNJWHgjGHxwmidnLnqeVy2ZAxmopDdy8L3XMtbdKS6nWyJ58xqWRaHE9miwKgZ1gXTSUta59ZDsi2c7mnrPqayTyZTBrNqrao/n5+ft4TJgmEnFYK1HLNMCdR1SlTq6plFRgM1CAHmNSc6wELnDlRG9Xm3d1dnZ2d9TJLy1IEEEEq13djG2RhmfXqP4F9Pp+3rCJ3xiJWMjbqoE9PT21zDk5te3u7V9vEggAvxD3J3AguxqOqGiOEmq3qjp9TZ836LRCYa2FRyBlkULa52YUMHIhCYxoDz+sZBSXjdn9/3xz3zc1Nb+0pYJPsjfW5ro3dEAgAK/2BBcnVAFmHdf2kqc2hPMBDqQI48zzszLJJQCoZMD5FHb6qmtrdZ1wz2aKc7/xa0rzGzHyXRKT/oj9JEA5Am2v6MZmSi4uLdqKW7LyqW79t/niGVObb0WtjY6MJPDMxyO07aQ6G9vW0V21xWdUtTqcGlP2lApCRJf3HeZk8qMBU9DEy2cdsNmv0dVUXMHKzjET6qer0HSILEwkokEWoPcsAMpvLDDOprNx8BD3LOUGDnAnnvbGxUbPZrGVlmcFubm7W27dvG71ngnpXwSbX5RqDFLxwFIl61Vsp242HgwuSapQJyCBMbNn91tZWOyM4hTLX19eN8rq7ez76znv9+OOPP7GhpE0/fPjQHK4AmnW99WUraFSMR+oQZPTGR705D/1Op5kCRO83nU6bsCXVuPoa2EJFc755OhSmQUnAWLL5/HyOAXYBoMxgkMFfnTZ/ZrycYCQDlD2ymRT/ZdabNd2qageSmHPocKzDp0+fmuAvWS1jCfAZN2CTrWE5XFMQzc1Sqjr1eNLuOzs7DfDwJ5kVpxoe2M4gSZchMKKbE2RUVVOZAym0K8nMKKdkYE9Rl6yabwMaAZgUgQKgfAW/lFqWFF4ZS0AjS2Hmv/GiQ0kmYGhfT3txID48PGxZp6DJoFIRS9Wo9gERcwooxsPDw+a0kmq+uXneHJ9zdKgAYyWWgdDR2xwApSZ0fXFx0Yw/jz9U42KUFxcXdXBw0DIcIIHDAw4oetF0JjNH9/j42DJKwcrkF5RTXGJNaQpe5vN5j+JGEaOw9EHWttClVR065uBRbrltZDr3qudM6PDwsAVh34PY05EKlKg5Wav7T6fT9rwEShsbG20sBTjXl9F5J9fUN8YHtXt5edkEfCly2tvbq7dv3/bEeSlMeXh4aGfyjsfjRgNWVRPWWEeuAUZZcz44OGjPj9GQNXLS+lewI0w0H8wFFLH69OXl5U820dDfHDWbtQaeboADBxz1K5Yon0vWdXZ21tgKhw2kdgHwTnCYwqEUDqZGoqra0rlUvJvzp6enbT96W27m1pxPT09trNS2zUnXMhcwK1k/TVAxGo3aYRapDvcZbJutZ/M5zXd2L1CORqPWX1XdOl/3EWiNVzJK0+m0FotFAwyum3M6lyHqZ/PXcjl+yVI/QCvtJfvHcyabObSvo704EKNDx+NxHR0dtaxGFrCzs9OcmEkpICVVikJjOOrMBCrQqQCd6lAqQmIhgUSWkirXFMlMJpN2YgpKiXPJCZvUpPtmPTfVx37GSQiYHB8HCyxwlDIjyHl3d7cFf6g3Dx2oqgZubm5uam9vr9UjOUqAgrPlkDhNGUduYpBLc2R6KV4hlMqg6bqWshCD6Rt9zInLAAQx10VHc4jsp6o7jco1UwCofz0jMY6Ans8vY/Es+pnwLsU+gnBShewB0Es1KseW2UpStWlbsjC2wgbXT+XifOkX9FOKcATRpF8z+zGm+/v7jZ4UfJN94bCrun2jHWgxHncHMxgn88yYjUbPAjZsjC0djbe+k6ErQ7hOHqd5eXnZ2+EOze076tApVALazCHPae575lxe53qW79zf3/eWAFZ1e0i7fi7X9Mc1MGOu630wGRgO3zMG5j4fyd7YkKVZyQh5bwxA6h6UdajbAQAMYDJ8GYQ/ZyAeasSvby8OxNSQnF4GUyha/WG5XLa1veoUmbHlkoOqagF2c3OzLi4u6vr6uh38/qu/+qu1XC6byIgjlQVyqknFCK5JNUOasvOkbThqdVyUmWtwdhmgZeGZLfu3z6bic32nJ/0gCKGwiMkyUHofDv78/LznkDLoZ71bRp91R+pM/SL75YTUOtd/BxhdX1+3wxVsLkJEkzQ3tuT29raOjo6awjdp6VSlo+xR3TaCSKVy1XO27wAABwfIOo+Pj1sg4AAFOfR9ZgzYEmtA2SsRmsYmOE0ZrLFO1b3MR21Z0BWQjSkbkx1zjAJ51kD1GUcqQ2OvmZ0KOpgH4y7DlgkneKnqjkkUjAUWzJU5JyiMx+O2m5PnNGdSrYvCp+WwTj2BrEySnQla7su2PWdVteVdgO46uFL/RvVjVdghkJfiLTVYc2dnZ6f1o+BpfDLQq6fza6kHSa2GOTubzRq9bF4LoEpmm5ubbW2wrNYc9DlzM8fZs/MVAneKz/ihoX097cWBmPMSdKioOQobRjCI3NpQbcffHDXB0sbG87ZxaCBolTM1eQi0UgxB+IG6VPeDcqueHZj7Qt1V1eh1hkrRa3Jzju5DvSrzR5FCqplt2GtYsMgM3QR1Tw6jqpqzN7E5fH377t27BooobmXBgv96ZsdhcH7ff/9925RjtVr1aEVjI/Oq6jYiWCwWLdPCFsiE14/Mq6pefU0wyYw7szSN82crsir2BBRZ/oV1SUDkfjZkQAMaB0IiY41xyJqrMdCv4/G4LfXy/Jy3d04VMOdsuZHr0lZQwwtMhJAYBuPLuQpE+koJgeP1b9mpUkOyGbne3z3YhoxU+UdfVXXn3GIGNjc3G0DMpUhV1XQOnkFA9bd5pZ/YnL89s2zx6empLX8yJlXVdmfL+jqA6LnM8dHoed2306gA8WxslihQX5n319fX9fHjx1aP9Q6SB/dhV1UduwNIZGYruEsOfPbk5KQHvqqqFotFu1d+30E6xoUvyjIXW0lRJ3ZtaF9Pe3EgPjs7q+l0WqtVt1vMxcVFE+8k5WNSC2DqKbJDv1e3tTm6GhD6lDOzpCMN+fT0tB27d35+3uokV1dXDcU7SIIjRC2h7IALaxdzedN6dltVLfC4j7qWetzh4WFvM5KkoSHSVPNyark5BIcnw0aDyRQ4XJ/3blnDNA5VXQYBKAhwmS17DmN1dnbWO3FJXR+dKvNLuhm40KcQv37IDDuzGwp4jtPn/fvi4qLttpQK5qR7KWM5ZRltqvNpGWRNVZ1CGAA7Oztr9sZO2U3WzKnqq6oFDNl7inJSYAM85Xai+p5Aqaoa4wNccNAYBPoA2b4/6q0ALvvQzylu1AeewTMBqoKm7HJdjyCrAuSyhkyMZmct9mJMt7a2WhZsXlHY608U+Pn5ebuOOfPw8NDGUT3XO2BllLY8I0CDyUvbMq5ZVgByUnkMbLPvvL+lZskquRaGKAGl/5szKUzz/DbkwTR6ZqCYL0vf4nf6g+8zZ9joxcXFZw3EAzX9+vYqatoSBcYKfXPAVdVUgKjpdWev5nh5eVlHR0e93aUSIaeB5VImCD/Ph+Ug7UiFuq2qXkAVCDk+RpvqQ5R5CnQ4p/39/VbTgjAvLi5axiVbqqqGnHMSVFVzMJDzavV8uHvuEaxeSH3LaaV6GP2Yam4HFXBCHCLUre/Qoe5v3DhYy74oN40Fx4Z65ICzTnp3d9fLCoj2OEoBk3gsVdGceJYcZFjqbIKi/mYzHB6nyx4vLy8b1Q80ceZV3aYLbEq/5bNUVc8OBGLUsmz75ub5XNvd3d22DIfN3N3d1Ww2q9vb7rB39mCjmwQ0+lB2lwDO+8sqZe3rQh22J/OtqhZk1WXdGwt0enra/r2e/ZsznkemnRtK5HpVfiFr7eYf5XPW/s1B/W/FhDKIOV3VsVRsIEsH2JMsdYxGz8JIIATQMnaeV923qlPsJxit6soegJdzflOgKWibv+wSeyG4C5oAVIrx9CdmMW3BPGYXyQbJjLPklbVhPnFoX0979RaXJycnvWPNBBITKdWs9/f3bXempE441xRooF+qqolWUuSRDiADOoeeikpB3VIUQcC+1mhA9KzajqwxHT867dOnT+3+8/m8Li4u6uzsrNXUiNIEWQIojoxzQO1aJ1zVUZaoRX0oCMqmEuHrO0EZ5eg+0LVJx/lyrimQ0o8oUkh+a2urHTiQ1K6AZ/12rnMcj8f1/v375hhkvxxGbnRhXKu6pWmylARZKS6hljfedANqn54vg5SAfnZ21mxMQKp6DsY//PBDy9TYpMw2d4S6u+tOD0sFuMyWc5ZJVVVvWZ06O0GVMUnxFNCQoIdTTzGRsfTMlqrpD0vdPnz40ESR1vmm+tkc4qyxCZgf33l6emp1dXNhtVq14C2AANbmUNLK9BLGGLDId09NQwrHjIfPGWf26p3W9SzsLHeskjGaU3ZO41vMJSAFwDUG+nB3d7dtrpM2IxDrt7Qj9fX5fN58Y9bU1fyzXi15MVfMTeWpFOQBx2xQiYdf9S5D+3raq2rEKK7cJ1ggZjSPj49t+YEsGH3Kmcv4/DyDEHRnksvCNzY2GsX49PTU6nVJfSaF5l5pcJx01qxSQKaZbJ7l+vq6FotFm7g//PBDz5HJLGTwWR/nnNLpQNTuVfXshN6/f9+yIZOFQ5NdbG9vt3W6mgwtxTwpnMJmZF02RSdJt/nj1CJBRf9WdXt462sTnVPhTHJJ28XFRQMfAkuCFQEls0MlC44S1Y9tEYwAQQHMuKI4cy1w3kem5pSm3LRCHdH4CcLGW2C03AZTApwQxMncc/64t1qgPmfbmCQZuncAFNkZupVgyvdTYChIe9YsW5iH/m1cXFeZxlgLzEAdh++dKdwxBQCwLHO99pnZYuoKABi2C1Bkhpfz17Oa46k9AfrUrAEcP+fb0n9JJOg6PI9n0m/uJfACQ+aIcaOWd3iMzBbTxAawLf5tbulTAF2JAgCczWatb83vZP74yQRen/sEpqG9rr04EKcxDm1oQxva0Ib2y9pQI359G//fPzK0oQ1taEMb2tA+VxsC8dCGNrShDW1oX7ANh1IObWhDG9rQfrY2UNOvb0NGPLShDW1oQxvaF2xDIB7a0IY2tKEN7Qu2IRAPbWhDG9rQhvYF21AjHtrQhja0of1sbagRv74NGfHQhja0oQ1taF+wDYF4aEMb2tCGNrQv2IZAPLShDW1oQxvaF2xDjXhoQxva0Ib2s7WhRvz6NmTEQxva0IY2tKF9wTYE4qENbWhDG9rQvmAbAvHQhja0oQ1taF+wDYF4aEMb2tCGNrQv2Aax1tCGNrShDe1na4NY6/VtyIiHNrShDW1oQ/uCbQjEQxva0IY2tKF9wTYE4qENbWhDG9rQvmAbasRDG9rQhja0n60NNeLXtyEjHtrQhja0oQ3tC7YhEA9taEMb2tCG9gXbQE0PbWhDG9rQfrY2UNOvb0NGPLShDW1oQxvaF2xDIB7a0IY2tKEN7Qu2IRAPbWhDG9rQhvYF21AjHtrQhja0of1sbagRv74NGfHQhja0oQ1taF+wDYF4aEMb2tCGNrQv2IZAPLShDW1oQxvaF2xDjXhoQxva0Ib2s7WhRvz6NmTEQxva0IY2tKF9wTYE4qENbWhDG9rQvmAbqOmhDW1oQxvaz9YGavr1bciIhza0oQ1taEP7gm0IxEMb2tCGNrShfcE2BOKhDW1oQxva0L5gG2rEQxva0IY2tJ+tDTXi17chIx7a0IY2tKEN7Qu2IRAPbWhDG9rQhvYF20BND21oQxva0H62NlDTr29DRjy0oQ1taEMb2hdsQyAe2tCGNrShDe0LtiEQD21oQxva0Ib2BdsQiIc2tKENbWhD+4JtCMRDG9rQhja0oX3BNgTioQ1taEMb2tC+YBsC8dCGNrShDW1oX7ANgXhoQxva0IY2tC/YhkA8tKENbWhDG9oXbEMgHtrQhja0oQ3tC7YhEA9taEMb2tCG9gXbEIiHNrShDW1oQ/uCbQjEQxva0IY2tKF9wfb/ATYI0d+SOp7KAAAAAElFTkSuQmCC",
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plt.figure(figsize=(6, 6))\n",
+ "plt.imshow(heatmap_a)\n",
+ "plt.title(\"Spatial distribution – Ductile\")\n",
+ "plt.colorbar(label=\"Probability\")\n",
+ "plt.axis(\"off\")\n",
+ "plt.show()\n",
+ "\n",
+ "\n",
+ "plt.figure(figsize=(6, 6))\n",
+ "plt.imshow(heatmap_b)\n",
+ "plt.title(\"Spatial distribution – Class B\")\n",
+ "plt.colorbar(label=\"Probability\")\n",
+ "plt.axis(\"off\")\n",
+ "plt.show()\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "kaggle": {
+ "accelerator": "none",
+ "dataSources": [
+ {
+ "datasetId": 9173898,
+ "sourceId": 14366370,
+ "sourceType": "datasetVersion"
+ }
+ ],
+ "dockerImageVersionId": 31234,
+ "isGpuEnabled": false,
+ "isInternetEnabled": false,
+ "language": "python",
+ "sourceType": "notebook"
+ },
+ "kernelspec": {
+ "display_name": "Python 3",
+ "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.12"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/plot_average_only.py b/plot_average_only.py
new file mode 100644
index 0000000..2e177e3
--- /dev/null
+++ b/plot_average_only.py
@@ -0,0 +1,136 @@
+
+import json
+import matplotlib.pyplot as plt
+import os
+import numpy as np
+
+def load_data(json_path):
+ with open(json_path, 'r') as f:
+ return json.load(f)
+
+def compute_average_curve(history, metric_name):
+ if not history:
+ return [], []
+ min_epochs = min(len(fold) for fold in history)
+ avg_values = []
+ epochs = []
+ for epoch_idx in range(min_epochs):
+ val_sum = 0
+ count = 0
+ epoch_num = history[0][epoch_idx]['epoch']
+ for fold_data in history:
+ val_sum += fold_data[epoch_idx][metric_name]
+ count += 1
+ avg_values.append(val_sum / count)
+ epochs.append(epoch_num)
+ return epochs, avg_values
+
+def plot_single_model_train_vs_val_avg(data, augmentator_name, model_name, output_path, title_override=None):
+ try:
+ model_data = data[augmentator_name][model_name]
+ history = model_data['training_history']
+ except KeyError:
+ print(f"Data not found for {augmentator_name} - {model_name}")
+ if augmentator_name in data:
+ print(f"Available models in {augmentator_name}: {list(data[augmentator_name].keys())}")
+ else:
+ print(f"Augmentator {augmentator_name} not found in data keys: {list(data.keys())}")
+ return
+
+ epochs, avg_train = compute_average_curve(history, 'train_loss')
+ _, avg_val = compute_average_curve(history, 'val_loss')
+
+ plt.figure(figsize=(8, 6))
+ plt.plot(epochs, avg_train, label='Avg Train Loss', marker='o', linestyle='-', color='blue')
+ plt.plot(epochs, avg_val, label='Avg Val Loss', marker='x', linestyle='--', color='orange')
+
+ title = title_override if title_override else f"{augmentator_name}\n{model_name} - Average Loss"
+ plt.title(title)
+ plt.xlabel("Epoch")
+ plt.ylabel("Loss")
+ plt.legend()
+ plt.grid(True)
+ plt.ylim(bottom=0)
+ plt.tight_layout()
+ plt.savefig(output_path)
+ plt.close()
+ print(f"Saved: {output_path}")
+
+def plot_comparison_val_loss_avg(data, augmentator_name, output_path, title_override=None):
+ try:
+ aug_data = data[augmentator_name]
+ except KeyError:
+ print(f"Data not found for {augmentator_name}")
+ return
+
+ plt.figure(figsize=(10, 6))
+
+ # Pre-defined colors/styles for consistency if desired, or let matplotlib handle it
+ styles = ['-', '--', '-.', ':']
+ markers = ['o', 's', '^', 'D']
+
+ idx = 0
+ for model_name, model_data in aug_data.items():
+ if 'training_history' not in model_data or not model_data['training_history']:
+ continue
+
+ history = model_data['training_history']
+ epochs, avg_val = compute_average_curve(history, 'val_loss')
+
+ plt.plot(epochs, avg_val, label=f"{model_name}",
+ linestyle=styles[idx % len(styles)],
+ marker=markers[idx % len(markers)],
+ markevery=5) # don't clutter with markers
+ idx += 1
+
+ title = title_override if title_override else f"Validation Loss Comparison - {augmentator_name}"
+ plt.title(title)
+ plt.xlabel("Epoch")
+ plt.ylabel("Validation Loss")
+ plt.legend()
+ plt.grid(True)
+ plt.ylim(bottom=0)
+ plt.tight_layout()
+ plt.savefig(output_path)
+ plt.close()
+ print(f"Saved: {output_path}")
+
+if __name__ == "__main__":
+ JSON_FILE = "results/results_cache.json"
+ REPORT_DIR = "docs/report"
+
+ data = load_data(JSON_FILE)
+
+ # 1. Comparison: Identity (Val Loss)
+ plot_comparison_val_loss_avg(
+ data,
+ augmentator_name="Aug_Identity_K5",
+ output_path=os.path.join(REPORT_DIR, "val_loss_identity.png"),
+ title_override="Validation Loss - Identity Augmentation (Average of 5 Folds)"
+ )
+
+ # 2. Comparison: Combined (Val Loss)
+ plot_comparison_val_loss_avg(
+ data,
+ augmentator_name="Combined_2Geo_2Photo_1SEM_x2",
+ output_path=os.path.join(REPORT_DIR, "val_loss_combined_2geo.png"),
+ title_override="Validation Loss - Combined Augmentation (Average of 5 Folds)"
+ )
+
+ # 3. Swin Standard Train vs Val
+ plot_single_model_train_vs_val_avg(
+ data,
+ augmentator_name="Combined_2Geo_2Photo_1SEM_x2",
+ model_name="Swin_Model_Node",
+ output_path=os.path.join(REPORT_DIR, "train_val_swin_std.png"),
+ title_override="Swin Standard: Train vs Val (Average)"
+ )
+
+ # 4. Swin Large Train vs Val
+ plot_single_model_train_vs_val_avg(
+ data,
+ augmentator_name="Combined_2Geo_2Photo_1SEM_x2",
+ model_name="Swin_Big_Model_Node",
+ output_path=os.path.join(REPORT_DIR, "train_val_swin_large.png"),
+ title_override="Swin Large: Train vs Val (Average)"
+ )
diff --git a/plot_results.py b/plot_results.py
new file mode 100644
index 0000000..8ba5c1f
--- /dev/null
+++ b/plot_results.py
@@ -0,0 +1,268 @@
+
+import json
+import matplotlib.pyplot as plt
+import os
+import math
+import numpy as np
+
+def ensure_dir(directory):
+ if not os.path.exists(directory):
+ os.makedirs(directory)
+ print(f"Created directory: {directory}")
+
+def load_data(json_path):
+ try:
+ with open(json_path, 'r') as f:
+ return json.load(f)
+ except FileNotFoundError:
+ print(f"Error: File not found at {json_path}")
+ return None
+ except json.JSONDecodeError:
+ print(f"Error: Invalid JSON file at {json_path}")
+ return None
+
+def get_plot_layout(num_plots):
+ cols = 2 if num_plots > 1 else 1
+ rows = math.ceil(num_plots / cols)
+ return rows, cols
+
+def compute_average_curve(history, metric_name):
+ """
+ Computes average curve across folds.
+ Returns: epochs (list), avg_values (list)
+ """
+ if not history:
+ return [], []
+
+ # Find min number of epochs to ensure we can average
+ min_epochs = min(len(fold) for fold in history)
+
+ avg_values = []
+ epochs = []
+
+ for epoch_idx in range(min_epochs):
+ # Collect values for this epoch from all folds
+ val_sum = 0
+ count = 0
+ epoch_num = history[0][epoch_idx]['epoch'] # Assume all have same epoch numbering
+
+ for fold_data in history:
+ val_sum += fold_data[epoch_idx][metric_name]
+ count += 1
+
+ avg_values.append(val_sum / count)
+ epochs.append(epoch_num)
+
+ return epochs, avg_values
+
+def plot_train_vs_val(data, base_output_dir):
+ """
+ 1. Train Loss vs Val Loss for each Augmentator + Model.
+ Includes Average plot.
+ """
+ output_dir = os.path.join(base_output_dir, "train_vs_val")
+ ensure_dir(output_dir)
+
+ print("Generating: Train vs Val plots...")
+
+ for augmentator_name, augmentator_data in data.items():
+ for model_name, model_data in augmentator_data.items():
+
+ if 'training_history' not in model_data:
+ continue
+
+ training_history = model_data['training_history']
+ num_folds = len(training_history)
+
+ if num_folds == 0:
+ continue
+
+ # Add one for the Average plot
+ total_plots = num_folds + 1
+ rows, cols = get_plot_layout(total_plots)
+ fig, axes = plt.subplots(rows, cols, figsize=(12, 6 * rows))
+ fig.suptitle(f"{augmentator_name} - {model_name}\nTrain vs Val Loss", fontsize=16)
+
+ if total_plots > 1:
+ axes_flat = axes.flatten()
+ else:
+ axes_flat = [axes]
+
+ # Plot Individual Folds
+ for i, fold_epochs in enumerate(training_history):
+ ax = axes_flat[i]
+
+ epochs = [e['epoch'] for e in fold_epochs]
+ train_loss = [e['train_loss'] for e in fold_epochs]
+ val_loss = [e['val_loss'] for e in fold_epochs]
+
+ ax.plot(epochs, train_loss, label='Train Loss', marker='o', linestyle='-')
+ ax.plot(epochs, val_loss, label='Val Loss', marker='x', linestyle='--')
+
+ ax.set_title(f"Fold {i+1}")
+ ax.set_xlabel("Epoch")
+ ax.set_ylabel("Loss")
+ ax.legend()
+ ax.grid(True)
+ ax.set_ylim(bottom=0)
+
+ # Plot Average
+ ax_avg = axes_flat[num_folds]
+ avg_epochs, avg_train = compute_average_curve(training_history, 'train_loss')
+ _, avg_val = compute_average_curve(training_history, 'val_loss')
+
+ ax_avg.plot(avg_epochs, avg_train, label='Avg Train Loss', marker='o', linestyle='-', color='purple')
+ ax_avg.plot(avg_epochs, avg_val, label='Avg Val Loss', marker='x', linestyle='--', color='orange')
+
+ ax_avg.set_title(f"Average ({num_folds} folds)")
+ ax_avg.set_xlabel("Epoch")
+ ax_avg.set_ylabel("Loss")
+ ax_avg.legend()
+ ax_avg.grid(True)
+ ax_avg.set_ylim(bottom=0)
+
+ # Hide unused subplots
+ for j in range(total_plots, len(axes_flat)):
+ axes_flat[j].axis('off')
+
+ plt.tight_layout(rect=[0, 0.03, 1, 0.97])
+
+ filename = f"{augmentator_name}_{model_name}_loss.png".replace(" ", "_").replace("/", "-")
+ save_path = os.path.join(output_dir, filename)
+ plt.savefig(save_path)
+ plt.close(fig)
+
+def plot_comparisons(data, group_by, metric, base_output_dir):
+ """
+ Generates comparison plots.
+ Includes Average plot in the last subplot.
+ """
+
+ if group_by == 'augmentator':
+ folder_name = f"compare_augmentations_{metric.split('_')[0]}"
+ primary_key_type = "Model"
+ secondary_key_type = "Augmentator"
+
+ items = {}
+ for aug_name, aug_data in data.items():
+ for mod_name, mod_data in aug_data.items():
+ if mod_name not in items:
+ items[mod_name] = {}
+ items[mod_name][aug_name] = mod_data
+
+ elif group_by == 'model':
+ folder_name = f"compare_models_{metric.split('_')[0]}"
+ primary_key_type = "Augmentator"
+ secondary_key_type = "Model"
+ items = data
+ else:
+ return
+
+ output_dir = os.path.join(base_output_dir, folder_name)
+ ensure_dir(output_dir)
+ print(f"Generating: {folder_name} plots...")
+
+ for primary_name, secondary_dict in items.items():
+
+ max_folds = 0
+ valid_entries = []
+
+ for sec_name, sec_data in secondary_dict.items():
+ if 'training_history' in sec_data and len(sec_data['training_history']) > 0:
+ max_folds = max(max_folds, len(sec_data['training_history']))
+ valid_entries.append(sec_name)
+
+ if max_folds == 0:
+ continue
+
+ # Add one for Average plot
+ total_plots = max_folds + 1
+ rows, cols = get_plot_layout(total_plots)
+ fig, axes = plt.subplots(rows, cols, figsize=(12, 6 * rows))
+ metric_title = "Validation Loss" if metric == 'val_loss' else "Training Loss"
+ fig.suptitle(f"{primary_key_type}: {primary_name}\nComparing {secondary_key_type}s ({metric_title})", fontsize=16)
+
+ if total_plots > 1:
+ axes_flat = axes.flatten()
+ else:
+ axes_flat = [axes]
+
+ # Draw plots for each fold
+ for fold_idx in range(max_folds):
+ ax = axes_flat[fold_idx]
+ ax.set_title(f"Fold {fold_idx+1}")
+ ax.set_xlabel("Epoch")
+ ax.set_ylabel("Loss")
+ ax.grid(True)
+ ax.set_ylim(bottom=0)
+
+ has_data = False
+ for sec_name in valid_entries:
+ sec_data = secondary_dict[sec_name]
+ history = sec_data['training_history']
+
+ if fold_idx < len(history):
+ fold_data = history[fold_idx]
+ epochs = [e['epoch'] for e in fold_data]
+ values = [e[metric] for e in fold_data]
+ ax.plot(epochs, values, label=sec_name)
+ has_data = True
+
+ if has_data:
+ ax.legend(fontsize='small')
+
+ # Draw Average Plot
+ ax_avg = axes_flat[max_folds]
+ ax_avg.set_title(f"Average (All Folds)")
+ ax_avg.set_xlabel("Epoch")
+ ax_avg.set_ylabel("Loss")
+ ax_avg.grid(True)
+ ax_avg.set_ylim(bottom=0)
+
+ has_avg_data = False
+ for sec_name in valid_entries:
+ sec_data = secondary_dict[sec_name]
+ history = sec_data['training_history']
+
+ if history:
+ avg_epochs, avg_values = compute_average_curve(history, metric)
+ if avg_epochs:
+ ax_avg.plot(avg_epochs, avg_values, label=sec_name)
+ has_avg_data = True
+
+ if has_avg_data:
+ ax_avg.legend(fontsize='small')
+
+ # Hide unused subplots
+ for j in range(total_plots, len(axes_flat)):
+ axes_flat[j].axis('off')
+
+ plt.tight_layout(rect=[0, 0.03, 1, 0.97])
+
+ filename = f"{primary_name}_{metric}.png".replace(" ", "_").replace("/", "-")
+ save_path = os.path.join(output_dir, filename)
+ plt.savefig(save_path)
+ plt.close(fig)
+
+if __name__ == "__main__":
+ JSON_FILE = "results/results_cache.json"
+ PLOTS_DIR = "plots"
+
+ data = load_data(JSON_FILE)
+ if data:
+ # 1. Train vs Val (Original)
+ plot_train_vs_val(data, PLOTS_DIR)
+
+ # 2. Compare Augmentations (Val Loss)
+ plot_comparisons(data, group_by='augmentator', metric='val_loss', base_output_dir=PLOTS_DIR)
+
+ # 3. Compare Models (Val Loss)
+ plot_comparisons(data, group_by='model', metric='val_loss', base_output_dir=PLOTS_DIR)
+
+ # 4. Compare Augmentations (Train Loss)
+ plot_comparisons(data, group_by='augmentator', metric='train_loss', base_output_dir=PLOTS_DIR)
+
+ # 5. Compare Models (Train Loss)
+ plot_comparisons(data, group_by='model', metric='train_loss', base_output_dir=PLOTS_DIR)
+
+ print("All plots generated successfully.")
diff --git a/plots/compare_augmentations_train/Quadtree-CNNModel_Model_Node_train_loss.png b/plots/compare_augmentations_train/Quadtree-CNNModel_Model_Node_train_loss.png
new file mode 100644
index 0000000..705bffd
Binary files /dev/null and b/plots/compare_augmentations_train/Quadtree-CNNModel_Model_Node_train_loss.png differ
diff --git a/plots/compare_augmentations_train/Quadtree-ViTModel_Model_Node_train_loss.png b/plots/compare_augmentations_train/Quadtree-ViTModel_Model_Node_train_loss.png
new file mode 100644
index 0000000..17564df
Binary files /dev/null and b/plots/compare_augmentations_train/Quadtree-ViTModel_Model_Node_train_loss.png differ
diff --git a/plots/compare_augmentations_train/SlidingWindow-CNN_W128_S128_train_loss.png b/plots/compare_augmentations_train/SlidingWindow-CNN_W128_S128_train_loss.png
new file mode 100644
index 0000000..9c38ed0
Binary files /dev/null and b/plots/compare_augmentations_train/SlidingWindow-CNN_W128_S128_train_loss.png differ
diff --git a/plots/compare_augmentations_train/SlidingWindow-CNN_W128_S64_train_loss.png b/plots/compare_augmentations_train/SlidingWindow-CNN_W128_S64_train_loss.png
new file mode 100644
index 0000000..e2837cf
Binary files /dev/null and b/plots/compare_augmentations_train/SlidingWindow-CNN_W128_S64_train_loss.png differ
diff --git a/plots/compare_augmentations_train/SlidingWindow-CNN_W256_S128_train_loss.png b/plots/compare_augmentations_train/SlidingWindow-CNN_W256_S128_train_loss.png
new file mode 100644
index 0000000..c4959bb
Binary files /dev/null and b/plots/compare_augmentations_train/SlidingWindow-CNN_W256_S128_train_loss.png differ
diff --git a/plots/compare_augmentations_train/SlidingWindow-CNN_W64_S32_train_loss.png b/plots/compare_augmentations_train/SlidingWindow-CNN_W64_S32_train_loss.png
new file mode 100644
index 0000000..ec8bc9c
Binary files /dev/null and b/plots/compare_augmentations_train/SlidingWindow-CNN_W64_S32_train_loss.png differ
diff --git a/plots/compare_augmentations_train/SlidingWindow-ViT_W64_S32_train_loss.png b/plots/compare_augmentations_train/SlidingWindow-ViT_W64_S32_train_loss.png
new file mode 100644
index 0000000..6e3e060
Binary files /dev/null and b/plots/compare_augmentations_train/SlidingWindow-ViT_W64_S32_train_loss.png differ
diff --git a/plots/compare_augmentations_train/Swin_Big_Model_Node_train_loss.png b/plots/compare_augmentations_train/Swin_Big_Model_Node_train_loss.png
new file mode 100644
index 0000000..b36ab12
Binary files /dev/null and b/plots/compare_augmentations_train/Swin_Big_Model_Node_train_loss.png differ
diff --git a/plots/compare_augmentations_train/Swin_Model_Node_train_loss.png b/plots/compare_augmentations_train/Swin_Model_Node_train_loss.png
new file mode 100644
index 0000000..c86db57
Binary files /dev/null and b/plots/compare_augmentations_train/Swin_Model_Node_train_loss.png differ
diff --git a/plots/compare_augmentations_train/ViT_Big_Model_Node_train_loss.png b/plots/compare_augmentations_train/ViT_Big_Model_Node_train_loss.png
new file mode 100644
index 0000000..d8f7f49
Binary files /dev/null and b/plots/compare_augmentations_train/ViT_Big_Model_Node_train_loss.png differ
diff --git a/plots/compare_augmentations_train/ViT_Model_Node_train_loss.png b/plots/compare_augmentations_train/ViT_Model_Node_train_loss.png
new file mode 100644
index 0000000..dbea19d
Binary files /dev/null and b/plots/compare_augmentations_train/ViT_Model_Node_train_loss.png differ
diff --git a/plots/compare_augmentations_val/Quadtree-CNNModel_Model_Node_val_loss.png b/plots/compare_augmentations_val/Quadtree-CNNModel_Model_Node_val_loss.png
new file mode 100644
index 0000000..32a2742
Binary files /dev/null and b/plots/compare_augmentations_val/Quadtree-CNNModel_Model_Node_val_loss.png differ
diff --git a/plots/compare_augmentations_val/Quadtree-ViTModel_Model_Node_val_loss.png b/plots/compare_augmentations_val/Quadtree-ViTModel_Model_Node_val_loss.png
new file mode 100644
index 0000000..d6dc335
Binary files /dev/null and b/plots/compare_augmentations_val/Quadtree-ViTModel_Model_Node_val_loss.png differ
diff --git a/plots/compare_augmentations_val/SlidingWindow-CNN_W128_S128_val_loss.png b/plots/compare_augmentations_val/SlidingWindow-CNN_W128_S128_val_loss.png
new file mode 100644
index 0000000..e25b054
Binary files /dev/null and b/plots/compare_augmentations_val/SlidingWindow-CNN_W128_S128_val_loss.png differ
diff --git a/plots/compare_augmentations_val/SlidingWindow-CNN_W128_S64_val_loss.png b/plots/compare_augmentations_val/SlidingWindow-CNN_W128_S64_val_loss.png
new file mode 100644
index 0000000..416ec63
Binary files /dev/null and b/plots/compare_augmentations_val/SlidingWindow-CNN_W128_S64_val_loss.png differ
diff --git a/plots/compare_augmentations_val/SlidingWindow-CNN_W256_S128_val_loss.png b/plots/compare_augmentations_val/SlidingWindow-CNN_W256_S128_val_loss.png
new file mode 100644
index 0000000..1fa9a6c
Binary files /dev/null and b/plots/compare_augmentations_val/SlidingWindow-CNN_W256_S128_val_loss.png differ
diff --git a/plots/compare_augmentations_val/SlidingWindow-CNN_W64_S32_val_loss.png b/plots/compare_augmentations_val/SlidingWindow-CNN_W64_S32_val_loss.png
new file mode 100644
index 0000000..4b2faae
Binary files /dev/null and b/plots/compare_augmentations_val/SlidingWindow-CNN_W64_S32_val_loss.png differ
diff --git a/plots/compare_augmentations_val/SlidingWindow-ViT_W64_S32_val_loss.png b/plots/compare_augmentations_val/SlidingWindow-ViT_W64_S32_val_loss.png
new file mode 100644
index 0000000..6fb6e48
Binary files /dev/null and b/plots/compare_augmentations_val/SlidingWindow-ViT_W64_S32_val_loss.png differ
diff --git a/plots/compare_augmentations_val/Swin_Big_Model_Node_val_loss.png b/plots/compare_augmentations_val/Swin_Big_Model_Node_val_loss.png
new file mode 100644
index 0000000..bf241a7
Binary files /dev/null and b/plots/compare_augmentations_val/Swin_Big_Model_Node_val_loss.png differ
diff --git a/plots/compare_augmentations_val/Swin_Model_Node_val_loss.png b/plots/compare_augmentations_val/Swin_Model_Node_val_loss.png
new file mode 100644
index 0000000..6944679
Binary files /dev/null and b/plots/compare_augmentations_val/Swin_Model_Node_val_loss.png differ
diff --git a/plots/compare_augmentations_val/ViT_Big_Model_Node_val_loss.png b/plots/compare_augmentations_val/ViT_Big_Model_Node_val_loss.png
new file mode 100644
index 0000000..60fc698
Binary files /dev/null and b/plots/compare_augmentations_val/ViT_Big_Model_Node_val_loss.png differ
diff --git a/plots/compare_augmentations_val/ViT_Model_Node_val_loss.png b/plots/compare_augmentations_val/ViT_Model_Node_val_loss.png
new file mode 100644
index 0000000..31eb70d
Binary files /dev/null and b/plots/compare_augmentations_val/ViT_Model_Node_val_loss.png differ
diff --git a/plots/compare_models_train/Aug_Identity_K5_train_loss.png b/plots/compare_models_train/Aug_Identity_K5_train_loss.png
new file mode 100644
index 0000000..286b9a2
Binary files /dev/null and b/plots/compare_models_train/Aug_Identity_K5_train_loss.png differ
diff --git a/plots/compare_models_train/Combined_2Geo_2Photo_1SEM_x2_train_loss.png b/plots/compare_models_train/Combined_2Geo_2Photo_1SEM_x2_train_loss.png
new file mode 100644
index 0000000..36bb911
Binary files /dev/null and b/plots/compare_models_train/Combined_2Geo_2Photo_1SEM_x2_train_loss.png differ
diff --git a/plots/compare_models_train/Combined_3Geo_1Photo_1SEM_x2_train_loss.png b/plots/compare_models_train/Combined_3Geo_1Photo_1SEM_x2_train_loss.png
new file mode 100644
index 0000000..bfddd59
Binary files /dev/null and b/plots/compare_models_train/Combined_3Geo_1Photo_1SEM_x2_train_loss.png differ
diff --git a/plots/compare_models_val/Aug_Identity_K5_val_loss.png b/plots/compare_models_val/Aug_Identity_K5_val_loss.png
new file mode 100644
index 0000000..e354811
Binary files /dev/null and b/plots/compare_models_val/Aug_Identity_K5_val_loss.png differ
diff --git a/plots/compare_models_val/Combined_2Geo_2Photo_1SEM_x2_val_loss.png b/plots/compare_models_val/Combined_2Geo_2Photo_1SEM_x2_val_loss.png
new file mode 100644
index 0000000..f017d49
Binary files /dev/null and b/plots/compare_models_val/Combined_2Geo_2Photo_1SEM_x2_val_loss.png differ
diff --git a/plots/compare_models_val/Combined_3Geo_1Photo_1SEM_x2_val_loss.png b/plots/compare_models_val/Combined_3Geo_1Photo_1SEM_x2_val_loss.png
new file mode 100644
index 0000000..53fa109
Binary files /dev/null and b/plots/compare_models_val/Combined_3Geo_1Photo_1SEM_x2_val_loss.png differ
diff --git a/plots/other_analysis/learning_curves.png b/plots/other_analysis/learning_curves.png
new file mode 100644
index 0000000..598613a
Binary files /dev/null and b/plots/other_analysis/learning_curves.png differ
diff --git a/plots/other_analysis/loss vs data size.png b/plots/other_analysis/loss vs data size.png
new file mode 100644
index 0000000..31c477a
Binary files /dev/null and b/plots/other_analysis/loss vs data size.png differ
diff --git a/plots/other_analysis/progression.png b/plots/other_analysis/progression.png
new file mode 100644
index 0000000..5cd3908
Binary files /dev/null and b/plots/other_analysis/progression.png differ
diff --git a/plots/other_analysis/test_predictions.png b/plots/other_analysis/test_predictions.png
new file mode 100644
index 0000000..5ae35b4
Binary files /dev/null and b/plots/other_analysis/test_predictions.png differ
diff --git a/plots/other_analysis/training_loss_curves.png b/plots/other_analysis/training_loss_curves.png
new file mode 100644
index 0000000..221c267
Binary files /dev/null and b/plots/other_analysis/training_loss_curves.png differ
diff --git a/plots/train_vs_val/Aug_Identity_K5_Swin_Big_Model_Node_loss.png b/plots/train_vs_val/Aug_Identity_K5_Swin_Big_Model_Node_loss.png
new file mode 100644
index 0000000..4009ccf
Binary files /dev/null and b/plots/train_vs_val/Aug_Identity_K5_Swin_Big_Model_Node_loss.png differ
diff --git a/plots/train_vs_val/Aug_Identity_K5_Swin_Model_Node_loss.png b/plots/train_vs_val/Aug_Identity_K5_Swin_Model_Node_loss.png
new file mode 100644
index 0000000..7fbd43a
Binary files /dev/null and b/plots/train_vs_val/Aug_Identity_K5_Swin_Model_Node_loss.png differ
diff --git a/plots/train_vs_val/Aug_Identity_K5_ViT_Big_Model_Node_loss.png b/plots/train_vs_val/Aug_Identity_K5_ViT_Big_Model_Node_loss.png
new file mode 100644
index 0000000..1ed22e0
Binary files /dev/null and b/plots/train_vs_val/Aug_Identity_K5_ViT_Big_Model_Node_loss.png differ
diff --git a/plots/train_vs_val/Aug_Identity_K5_ViT_Model_Node_loss.png b/plots/train_vs_val/Aug_Identity_K5_ViT_Model_Node_loss.png
new file mode 100644
index 0000000..6c0c20d
Binary files /dev/null and b/plots/train_vs_val/Aug_Identity_K5_ViT_Model_Node_loss.png differ
diff --git a/plots/train_vs_val/Combined_2Geo_2Photo_1SEM_x2_Swin_Big_Model_Node_loss.png b/plots/train_vs_val/Combined_2Geo_2Photo_1SEM_x2_Swin_Big_Model_Node_loss.png
new file mode 100644
index 0000000..4b50bb4
Binary files /dev/null and b/plots/train_vs_val/Combined_2Geo_2Photo_1SEM_x2_Swin_Big_Model_Node_loss.png differ
diff --git a/plots/train_vs_val/Combined_2Geo_2Photo_1SEM_x2_Swin_Model_Node_loss.png b/plots/train_vs_val/Combined_2Geo_2Photo_1SEM_x2_Swin_Model_Node_loss.png
new file mode 100644
index 0000000..194e7ee
Binary files /dev/null and b/plots/train_vs_val/Combined_2Geo_2Photo_1SEM_x2_Swin_Model_Node_loss.png differ
diff --git a/plots/train_vs_val/Combined_2Geo_2Photo_1SEM_x2_ViT_Big_Model_Node_loss.png b/plots/train_vs_val/Combined_2Geo_2Photo_1SEM_x2_ViT_Big_Model_Node_loss.png
new file mode 100644
index 0000000..e53958a
Binary files /dev/null and b/plots/train_vs_val/Combined_2Geo_2Photo_1SEM_x2_ViT_Big_Model_Node_loss.png differ
diff --git a/plots/train_vs_val/Combined_2Geo_2Photo_1SEM_x2_ViT_Model_Node_loss.png b/plots/train_vs_val/Combined_2Geo_2Photo_1SEM_x2_ViT_Model_Node_loss.png
new file mode 100644
index 0000000..ff010a0
Binary files /dev/null and b/plots/train_vs_val/Combined_2Geo_2Photo_1SEM_x2_ViT_Model_Node_loss.png differ
diff --git a/plots/train_vs_val/Combined_3Geo_1Photo_1SEM_x2_Swin_Big_Model_Node_loss.png b/plots/train_vs_val/Combined_3Geo_1Photo_1SEM_x2_Swin_Big_Model_Node_loss.png
new file mode 100644
index 0000000..723350d
Binary files /dev/null and b/plots/train_vs_val/Combined_3Geo_1Photo_1SEM_x2_Swin_Big_Model_Node_loss.png differ
diff --git a/plots/train_vs_val/Combined_3Geo_1Photo_1SEM_x2_Swin_Model_Node_loss.png b/plots/train_vs_val/Combined_3Geo_1Photo_1SEM_x2_Swin_Model_Node_loss.png
new file mode 100644
index 0000000..17493f7
Binary files /dev/null and b/plots/train_vs_val/Combined_3Geo_1Photo_1SEM_x2_Swin_Model_Node_loss.png differ
diff --git a/plots/train_vs_val/Combined_3Geo_1Photo_1SEM_x2_ViT_Big_Model_Node_loss.png b/plots/train_vs_val/Combined_3Geo_1Photo_1SEM_x2_ViT_Big_Model_Node_loss.png
new file mode 100644
index 0000000..75d0252
Binary files /dev/null and b/plots/train_vs_val/Combined_3Geo_1Photo_1SEM_x2_ViT_Big_Model_Node_loss.png differ
diff --git a/plots/train_vs_val/Combined_3Geo_1Photo_1SEM_x2_ViT_Model_Node_loss.png b/plots/train_vs_val/Combined_3Geo_1Photo_1SEM_x2_ViT_Model_Node_loss.png
new file mode 100644
index 0000000..716e147
Binary files /dev/null and b/plots/train_vs_val/Combined_3Geo_1Photo_1SEM_x2_ViT_Model_Node_loss.png differ
diff --git a/pyproject.toml b/pyproject.toml
index 4a4aad4..7daa299 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -5,10 +5,19 @@ description = "Machine Learning Project for senior year Computer Science ML cour
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
+ "ipykernel>=7.1.0",
+ "matplotlib>=3.10.7",
"numpy>=2.3.3",
+ "opencv-python>=4.11.0.86",
"pandas>=2.3.2",
+ "pillow>=12.0.0",
"pydantic>=2.11.9",
"scikit-learn>=1.7.2",
+ "seaborn>=0.13.2",
+ "torch>=2.9.0",
+ "torchvision>=0.24.0",
+ "tqdm>=4.67.1",
+ "types-pyyaml>=6.0.12.20250915",
]
[dependency-groups]
diff --git a/results/execution_times_cache.json b/results/execution_times_cache.json
new file mode 100644
index 0000000..990100d
--- /dev/null
+++ b/results/execution_times_cache.json
@@ -0,0 +1,27 @@
+{
+ "Aug_Identity_K5": {
+ "ViT_Model_Node": 1057.8780171871185,
+ "Swin_Model_Node": 927.6742687225342,
+ "ViT_Big_Model_Node": 2407.901523590088,
+ "Swin_Big_Model_Node": 2715.197743177414,
+ "Quadtree-CNNModel_Model_Node": 6421.709107637405,
+ "Quadtree-ViTModel_Model_Node": 34886.862389564514,
+ "SlidingWindow-CNN_W64_S32": 7742.393431901932,
+ "SlidingWindow-CNN_W128_S64": 7605.475355625153,
+ "SlidingWindow-CNN_W128_S128": 7568.232273817062,
+ "SlidingWindow-CNN_W256_S128": 7573.130106210709,
+ "SlidingWindow-ViT_W64_S32": 9141.40427350998
+ },
+ "Combined_2Geo_2Photo_1SEM_x2": {
+ "ViT_Model_Node": 2997.826024055481,
+ "Swin_Model_Node": 2564.043711423874,
+ "ViT_Big_Model_Node": 6924.898647785187,
+ "Swin_Big_Model_Node": 7674.968294382095
+ },
+ "Combined_3Geo_1Photo_1SEM_x2": {
+ "ViT_Model_Node": 3014.491398334503,
+ "Swin_Model_Node": 2575.245814561844,
+ "ViT_Big_Model_Node": 6930.94030547142,
+ "Swin_Big_Model_Node": 7697.53147149086
+ }
+}
\ No newline at end of file
diff --git a/results/f1_means.json b/results/f1_means.json
new file mode 100644
index 0000000..d03600d
--- /dev/null
+++ b/results/f1_means.json
@@ -0,0 +1,103 @@
+{
+ "Aug_Identity_K5": {
+ "ViT_Model_Node": {
+ "mean_f1": 0.5977702590091415,
+ "mean_accuracy": 0.6798369178995054,
+ "mean_mask_cohesion": 0.7339181286549707
+ },
+ "Swin_Model_Node": {
+ "mean_f1": 0.7033482574757579,
+ "mean_accuracy": 0.7384132340637564,
+ "mean_mask_cohesion": 0.5625730994152047
+ },
+ "ViT_Big_Model_Node": {
+ "mean_f1": 0.54088708387608,
+ "mean_accuracy": 0.6646574410778736,
+ "mean_mask_cohesion": 0.0
+ },
+ "Swin_Big_Model_Node": {
+ "mean_f1": 0.5558194350390065,
+ "mean_accuracy": 0.6645857671548051,
+ "mean_mask_cohesion": 0.05321637426900585
+ },
+ "Quadtree-CNNModel_Model_Node": {
+ "mean_f1": 0.4896454933310129,
+ "mean_accuracy": 0.5523209443566395,
+ "mean_mask_cohesion": 0.0
+ },
+ "Quadtree-ViTModel_Model_Node": {
+ "mean_f1": 0.2500604103597349,
+ "mean_accuracy": 0.335345458984375,
+ "mean_mask_cohesion": 0.0
+ },
+ "SlidingWindow-CNN_W64_S32": {
+ "mean_f1": 0.4739060268320078,
+ "mean_accuracy": 0.6384033560055739,
+ "mean_mask_cohesion": 0.8947368421052632
+ },
+ "SlidingWindow-CNN_W128_S64": {
+ "mean_f1": 0.4762290421966582,
+ "mean_accuracy": 0.5923260828207808,
+ "mean_mask_cohesion": 0.7146198830409356
+ },
+ "SlidingWindow-CNN_W128_S128": {
+ "mean_f1": 0.48812028274215996,
+ "mean_accuracy": 0.5800832737259001,
+ "mean_mask_cohesion": 0.38362573099415204
+ },
+ "SlidingWindow-CNN_W256_S128": {
+ "mean_f1": 0.4588851539238294,
+ "mean_accuracy": 0.554397065458242,
+ "mean_mask_cohesion": 0.47953216374269003
+ },
+ "SlidingWindow-ViT_W64_S32": {
+ "mean_f1": 0.2500604103597349,
+ "mean_accuracy": 0.335345458984375,
+ "mean_mask_cohesion": 0.0
+ }
+ },
+ "Combined_2Geo_2Photo_1SEM_x2": {
+ "ViT_Model_Node": {
+ "mean_f1": 0.5645844631911766,
+ "mean_accuracy": 0.6264295856855069,
+ "mean_mask_cohesion": 0.13684210526315788
+ },
+ "Swin_Model_Node": {
+ "mean_f1": 0.7500971147269837,
+ "mean_accuracy": 0.7771304214209841,
+ "mean_mask_cohesion": 0.4783625730994152
+ },
+ "ViT_Big_Model_Node": {
+ "mean_f1": 0.3986882890655715,
+ "mean_accuracy": 0.6646432976973684,
+ "mean_mask_cohesion": 0.0
+ },
+ "Swin_Big_Model_Node": {
+ "mean_f1": 0.726913598225204,
+ "mean_accuracy": 0.7619012933028372,
+ "mean_mask_cohesion": 0.47660818713450287
+ }
+ },
+ "Combined_3Geo_1Photo_1SEM_x2": {
+ "ViT_Model_Node": {
+ "mean_f1": 0.57422052614887,
+ "mean_accuracy": 0.6568416528534471,
+ "mean_mask_cohesion": 0.5771929824561404
+ },
+ "Swin_Model_Node": {
+ "mean_f1": 0.7495168637183569,
+ "mean_accuracy": 0.7738129665977077,
+ "mean_mask_cohesion": 0.5409356725146199
+ },
+ "ViT_Big_Model_Node": {
+ "mean_f1": 0.41646387969438753,
+ "mean_accuracy": 0.6643644634046053,
+ "mean_mask_cohesion": 0.0
+ },
+ "Swin_Big_Model_Node": {
+ "mean_f1": 0.45934747134610276,
+ "mean_accuracy": 0.6853738483629728,
+ "mean_mask_cohesion": 0.07368421052631578
+ }
+ }
+}
\ No newline at end of file
diff --git a/results/results_cache.json b/results/results_cache.json
new file mode 100644
index 0000000..7bf10c1
--- /dev/null
+++ b/results/results_cache.json
@@ -0,0 +1,15108 @@
+{
+ "Aug_Identity_K5": {
+ "ViT_Model_Node": {
+ "train_size": 75,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.6993695309287623,
+ 0.7334339744166324,
+ 0.6074909410978618,
+ 0.6556771930895353,
+ 0.7032129499647353
+ ],
+ "Mask_Cohesion": [
+ 0.5789473684210527,
+ 0.8947368421052632,
+ 0.8421052631578947,
+ 0.631578947368421,
+ 0.7222222222222222
+ ],
+ "IoU_Class0": [
+ 0.6445239126264669,
+ 0.7244924638923808,
+ 0.5779629051541344,
+ 0.6328550619893096,
+ 0.6788118384060601
+ ],
+ "IoU_Class1": [
+ 0.33917520085970826,
+ 0.10854704584901416,
+ 0.15132340414816847,
+ 0.15292387738943558,
+ 0.20380864122076645
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.4918495567430876,
+ 0.4165197548706975,
+ 0.36464315465115144,
+ 0.3928894696893726,
+ 0.4413102398134133
+ ],
+ "IoU_Weighted": [
+ 0.5415879333838842,
+ 0.5553106410116553,
+ 0.39569384232922555,
+ 0.47281655650100574,
+ 0.5342455691760404
+ ],
+ "Dice_Class0": [
+ 0.78384255489128,
+ 0.8402384806682387,
+ 0.7325430823073491,
+ 0.775151544948884,
+ 0.808681262398715
+ ],
+ "Dice_Class1": [
+ 0.5065434315718638,
+ 0.1958366065842161,
+ 0.2628686320506589,
+ 0.26528009418228193,
+ 0.3386063768641614
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.6451929932315719,
+ 0.5180375436262274,
+ 0.497705857179004,
+ 0.520215819565583,
+ 0.5736438196314382
+ ],
+ "Dice_Weighted": [
+ 0.6903606071510452,
+ 0.6632401882504066,
+ 0.5318844393862111,
+ 0.6051291175880309,
+ 0.6656149149232691
+ ],
+ "Precision_Class0": [
+ 0.7488643635801924,
+ 0.7431999017404667,
+ 0.6007557251600065,
+ 0.6862914755043736,
+ 0.7330858419944156
+ ],
+ "Precision_Class1": [
+ 0.5669914774068653,
+ 0.5713053895601347,
+ 0.6647322228875913,
+ 0.4598270643414514,
+ 0.5261838395136207
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.6579279204935289,
+ 0.6572526456503007,
+ 0.6327439740237989,
+ 0.5730592699229125,
+ 0.6296348407540182
+ ],
+ "Recall_Class0": [
+ 0.8222483903140504,
+ 0.9664229879203251,
+ 0.9383990064989076,
+ 0.8904451492279929,
+ 0.9016599553145196
+ ],
+ "Recall_Class1": [
+ 0.45774261582014114,
+ 0.11817232879836495,
+ 0.1638270541305399,
+ 0.18641168583217482,
+ 0.24962015021300835
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.6399955030670957,
+ 0.5422976583593451,
+ 0.5511130303147238,
+ 0.5384284175300839,
+ 0.575640052763764
+ ],
+ "F1_Macro": [
+ 0.6488378325171299,
+ 0.5942669840443413,
+ 0.5891141373657361,
+ 0.5552043434013969,
+ 0.6014279977171031
+ ]
+ },
+ "training_history": [
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8883222670931565,
+ "val_loss": 0.8579558008595517
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7950042486190796,
+ "val_loss": 0.7686705432440105
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7615317545439068,
+ "val_loss": 0.7544400754727816
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7367682456970215,
+ "val_loss": 0.7377377403409857
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7206863855060778,
+ "val_loss": 0.7199482792302182
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7077614963054657,
+ "val_loss": 0.7177837553777193
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6974309980869293,
+ "val_loss": 0.702697848018847
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6875113389993969,
+ "val_loss": 0.6864094075403715
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6777448262038984,
+ "val_loss": 0.6833659598701879
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6697284955727426,
+ "val_loss": 0.6777316237750807
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6658942526892612,
+ "val_loss": 0.6769580841064453
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6684045493602753,
+ "val_loss": 0.6668286731368617
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6629308838593332,
+ "val_loss": 0.676777322041361
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6499022151294508,
+ "val_loss": 0.6580814374120612
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6492853760719299,
+ "val_loss": 0.6917633602493688
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.65035942667409,
+ "val_loss": 0.6606858435430025
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6490976888882486,
+ "val_loss": 0.6711209573243794
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6473950492708307,
+ "val_loss": 0.6529188532578317
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6429136269970944,
+ "val_loss": 0.6489541609036295
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6379446450032686,
+ "val_loss": 0.6495799077184576
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6298667443426031,
+ "val_loss": 0.6296201363990181
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.627868148841356,
+ "val_loss": 0.6776844702268902
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6258534161668075,
+ "val_loss": 0.6377096866306505
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.626004137490925,
+ "val_loss": 0.6364278322771976
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6231976088724638,
+ "val_loss": 0.682574494888908
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6177431627323753,
+ "val_loss": 0.6386591290172777
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6283692435214394,
+ "val_loss": 0.6543927600509242
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6083077898150996,
+ "val_loss": 0.6387522220611572
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.5967293726770502,
+ "val_loss": 0.6955478818793046
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.614214511294114,
+ "val_loss": 0.6254828948723642
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6036045708154377,
+ "val_loss": 0.6265686687670255
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6153228463310945,
+ "val_loss": 0.6177733540534973
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5917871947351255,
+ "val_loss": 0.6329594191751982
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5860727970537386,
+ "val_loss": 0.6210875071977314
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.588714503143963,
+ "val_loss": 0.6862790380653582
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.5882264156090585,
+ "val_loss": 0.6158872482023741
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5891987775501452,
+ "val_loss": 0.6402037347617903
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.5532095173471853,
+ "val_loss": 0.6253613970781627
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.5881001431691019,
+ "val_loss": 0.6523270481511166
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.578638358335746,
+ "val_loss": 0.5920721355237459
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8859243628225828,
+ "val_loss": 0.8219369712628817
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8003486441938501,
+ "val_loss": 0.7510612324664467
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7679550475195834,
+ "val_loss": 0.708295812732295
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7464572787284851,
+ "val_loss": 0.7125219420382851
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.732851726444144,
+ "val_loss": 0.6763746769804704
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7186671777775413,
+ "val_loss": 0.671407583512758
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.7067876269942835,
+ "val_loss": 0.6889284441345617
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6979608974958721,
+ "val_loss": 0.6645758748054504
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6946519409355364,
+ "val_loss": 0.646468742897636
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.68898627318834,
+ "val_loss": 0.6365066609884563
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6846707369151869,
+ "val_loss": 0.6285969709095202
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6757589374717913,
+ "val_loss": 0.6265868230869895
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.670196555162731,
+ "val_loss": 0.6236803782613654
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6686190272632399,
+ "val_loss": 0.6388459111514845
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6643069590392866,
+ "val_loss": 0.6223796166871723
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6669560874763288,
+ "val_loss": 0.6149760547437166
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6550347867764925,
+ "val_loss": 0.6195978239962929
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6569316716570603,
+ "val_loss": 0.6563498220945659
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6556311359531001,
+ "val_loss": 0.6175764265813326
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6430522297558031,
+ "val_loss": 0.6012086554577476
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6494589777369248,
+ "val_loss": 0.6073892210659227
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6308832921479878,
+ "val_loss": 0.7767469035951715
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6347910165786743,
+ "val_loss": 0.6896192494191622
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6439391233419117,
+ "val_loss": 0.6475113944003457
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.643578893259952,
+ "val_loss": 0.6045807066716646
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6125206366965645,
+ "val_loss": 0.609351816930269
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6225927754452354,
+ "val_loss": 0.5963352824512281
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6144825800469047,
+ "val_loss": 0.5929440667754725
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6038519191114526,
+ "val_loss": 0.5799651600812611
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.5889217806489844,
+ "val_loss": 0.5936521668183176
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6041042491009361,
+ "val_loss": 0.6914739985215036
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6057768236649664,
+ "val_loss": 0.6029281710323534
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6192061744238201,
+ "val_loss": 0.5917751318530032
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5943301429874018,
+ "val_loss": 0.7832426083715338
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.5903417491599133,
+ "val_loss": 0.5812675905855078
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6128075656137968,
+ "val_loss": 0.5749683929117102
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5900501185341885,
+ "val_loss": 0.5628161979349036
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.5612292336790186,
+ "val_loss": 0.7646724205268057
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.5781147252572211,
+ "val_loss": 0.6483825978479887
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.5681982887418646,
+ "val_loss": 0.5989142279875906
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8818814221181368,
+ "val_loss": 0.8702285446618733
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7817341700980538,
+ "val_loss": 0.8128248421769393
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.742866923934535,
+ "val_loss": 0.8210214752899972
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7187857518070623,
+ "val_loss": 0.7789730931583204
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.699457187401621,
+ "val_loss": 0.8128006552395067
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6882680089850175,
+ "val_loss": 0.7756570859959251
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6759445667266846,
+ "val_loss": 0.7603791607053656
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6655562316116533,
+ "val_loss": 0.7487614029332211
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6596153441228365,
+ "val_loss": 0.8115482957739579
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6527751197940425,
+ "val_loss": 0.7507235282345822
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6462060291516153,
+ "val_loss": 0.7498500723587839
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6506081443083914,
+ "val_loss": 0.7373509156076532
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6350388181836981,
+ "val_loss": 0.7507151804472271
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6319915009172339,
+ "val_loss": 0.7117624251466048
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6272417259843726,
+ "val_loss": 0.8173968039060894
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6269398315956718,
+ "val_loss": 0.7431597239092776
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6273688485747889,
+ "val_loss": 0.7135456173043502
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6148394769743869,
+ "val_loss": 0.7046021928912715
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6245936563140467,
+ "val_loss": 0.7286991539754366
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6162411319582086,
+ "val_loss": 0.7125611556203741
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.601612527119486,
+ "val_loss": 0.73477850462261
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6069732410343069,
+ "val_loss": 0.7703256465886769
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6035470931153548,
+ "val_loss": 0.7362248772069028
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6031385879767569,
+ "val_loss": 0.7353634740176954
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.5880999761192423,
+ "val_loss": 0.7331080217110483
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.5799780793880162,
+ "val_loss": 0.7544316116132235
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.5967364232791098,
+ "val_loss": 0.7788571778096651
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.5754448771476746,
+ "val_loss": 0.8104854900585977
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.5786401786302265,
+ "val_loss": 0.7326706773356387
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.5829380821240576,
+ "val_loss": 0.7090069463378504
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.5683372875577525,
+ "val_loss": 0.6837963869697169
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.5643307323518553,
+ "val_loss": 0.7575709035522059
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5532149797991702,
+ "val_loss": 0.7079140261599892
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5784350884588141,
+ "val_loss": 0.7752034256332799
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.5755095834794798,
+ "val_loss": 0.6978099801038441
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.5600362984757674,
+ "val_loss": 0.7302899046948081
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5520602315664291,
+ "val_loss": 0.7362949659949855
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.5543200703043687,
+ "val_loss": 0.8094947416531412
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.570714281577813,
+ "val_loss": 0.7488513259511245
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.5382859369641856,
+ "val_loss": 0.8098295629024506
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.887991012711274,
+ "val_loss": 0.8805520126694127
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7913314339361692,
+ "val_loss": 0.8045097432638469
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7603253047717246,
+ "val_loss": 0.7443689829424808
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7370496988296509,
+ "val_loss": 0.731932376560412
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.720882197743968,
+ "val_loss": 0.7213958439074064
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7085253988441668,
+ "val_loss": 0.7312991807335302
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.7039139098242709,
+ "val_loss": 0.7108505085894936
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6926506444027549,
+ "val_loss": 0.7017145219602083
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6791623172007109,
+ "val_loss": 0.6944664152044999
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6750788265152982,
+ "val_loss": 0.7132487171574643
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6674313560912484,
+ "val_loss": 0.7089636106240121
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6633034285746122,
+ "val_loss": 0.6779882594158775
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6600671285077145,
+ "val_loss": 0.6737279546888251
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.657541424036026,
+ "val_loss": 0.6850270221107885
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6538976697545302,
+ "val_loss": 0.6808790752762243
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6516391321232444,
+ "val_loss": 0.6636465003615931
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6415748721674869,
+ "val_loss": 0.6736837500020078
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6456487853276102,
+ "val_loss": 0.6658769375399539
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6331474530069452,
+ "val_loss": 0.7305691744151869
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6412997151675978,
+ "val_loss": 0.6752908292569613
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6313831429732474,
+ "val_loss": 0.6709924622585899
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6338114675722624,
+ "val_loss": 0.6508722368039583
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6172046786860416,
+ "val_loss": 0.7401902393290871
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.613925493077228,
+ "val_loss": 0.6860203915520718
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6201597875670383,
+ "val_loss": 0.6677436452162894
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6129419631079623,
+ "val_loss": 0.6563706711718911
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.5944786667823792,
+ "val_loss": 0.6748889214114139
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.5996869975014737,
+ "val_loss": 0.652477640854685
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6027253905409261,
+ "val_loss": 0.8233180234306737
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6020294223961077,
+ "val_loss": 0.6804537443738234
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6017845084792689,
+ "val_loss": 0.6571088856772372
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.5945791837416197,
+ "val_loss": 0.6476596923250901
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5730885384898436,
+ "val_loss": 0.6572822802945187
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5600545022048449,
+ "val_loss": 0.6815323123806402
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.5814980778254961,
+ "val_loss": 0.7823716684391624
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.5561694800853729,
+ "val_loss": 0.6573324266232943
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5586310821144205,
+ "val_loss": 0.7131602026914295
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.5820113632239794,
+ "val_loss": 0.7072737499287254
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.5424352562741229,
+ "val_loss": 0.6530002543800756
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.5278020532507646,
+ "val_loss": 0.6722976847698814
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8946150149169722,
+ "val_loss": 0.94268622663286
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8016378503096732,
+ "val_loss": 0.7230668697092268
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.766137388191725,
+ "val_loss": 0.7071342368920644
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7421633726672122,
+ "val_loss": 0.6919965975814395
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7197934373428947,
+ "val_loss": 0.6785127023855845
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7192443468068775,
+ "val_loss": 0.715450707409117
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.7035616902928603,
+ "val_loss": 0.6656677027543386
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6918371758962932,
+ "val_loss": 0.6538912885718875
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6863325818588859,
+ "val_loss": 0.6646601359049479
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6810033305695182,
+ "val_loss": 0.6691342294216156
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6747113905454937,
+ "val_loss": 0.6402339604165819
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.667121758586482,
+ "val_loss": 0.6454808678891923
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6640026224286932,
+ "val_loss": 0.6372882624467214
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6560864903424916,
+ "val_loss": 0.6299198667208353
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6562578960468894,
+ "val_loss": 0.6255394419034322
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6573738942020818,
+ "val_loss": 0.6333806465069453
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6539586892253474,
+ "val_loss": 0.6399425135718452
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6399448215961456,
+ "val_loss": 0.6449237250619464
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6432253404667503,
+ "val_loss": 0.6115655948718389
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6462732365256861,
+ "val_loss": 0.6320916927523084
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.640761977747867,
+ "val_loss": 0.6397643701897727
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6208175907009527,
+ "val_loss": 0.6379656460550096
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6395377924567774,
+ "val_loss": 0.616896755165524
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6209308740339781,
+ "val_loss": 0.6140945305426916
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6172155267313907,
+ "val_loss": 0.6299535930156708
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6146670611281144,
+ "val_loss": 0.6510485443803999
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.62466120876764,
+ "val_loss": 0.644433276520835
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6171161014782754,
+ "val_loss": 0.6680426581038369
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.630374844136991,
+ "val_loss": 0.6335693117645051
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.5992462486028671,
+ "val_loss": 0.6739407678445181
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6034349391334936,
+ "val_loss": 0.6571952170795865
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6074153004508269,
+ "val_loss": 0.6109676344527138
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5830987284058019,
+ "val_loss": 0.7520597245958116
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5804179593136436,
+ "val_loss": 0.6825047963195376
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.5982012231098978,
+ "val_loss": 0.6039800710148282
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.5655853960074877,
+ "val_loss": 0.6163547850317426
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5700098025171381,
+ "val_loss": 0.6223738590876261
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.5741165609736192,
+ "val_loss": 0.5894184874163734
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.5846786961743706,
+ "val_loss": 0.7865588565667471
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.5594454227309478,
+ "val_loss": 0.6276531053913964
+ }
+ ]
+ ]
+ },
+ "Swin_Model_Node": {
+ "train_size": 75,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.7008540504857114,
+ 0.7912663911518297,
+ 0.7047121148360403,
+ 0.754717375102796,
+ 0.7405162387424045
+ ],
+ "Mask_Cohesion": [
+ 0.15789473684210525,
+ 1.0,
+ 0.5789473684210527,
+ 0.631578947368421,
+ 0.4444444444444444
+ ],
+ "IoU_Class0": [
+ 0.5779367348291575,
+ 0.7644711388264687,
+ 0.6246773603289866,
+ 0.7180129562513012,
+ 0.6990859265423407
+ ],
+ "IoU_Class1": [
+ 0.49330587829209693,
+ 0.35277261057197856,
+ 0.4193807438606798,
+ 0.34669021036386133,
+ 0.3466605409322322
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.5356213065606272,
+ 0.5586218746992236,
+ 0.5220290520948332,
+ 0.5323515833075813,
+ 0.5228732337372864
+ ],
+ "IoU_Weighted": [
+ 0.5494033339946921,
+ 0.6513887154211856,
+ 0.5369554452789107,
+ 0.5941911684356513,
+ 0.59182596812204
+ ],
+ "Dice_Class0": [
+ 0.7325220613381949,
+ 0.8665158891008106,
+ 0.7689863545614909,
+ 0.8358644253975863,
+ 0.8228964946640321
+ ],
+ "Dice_Class1": [
+ 0.6606896623969549,
+ 0.5215549277314528,
+ 0.5909348082600794,
+ 0.5148774494620991,
+ 0.5148447294553604
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.6966058618675749,
+ 0.6940354084161318,
+ 0.6799605814107852,
+ 0.6753709374298427,
+ 0.6688706120596963
+ ],
+ "Dice_Weighted": [
+ 0.7083015390440035,
+ 0.7717635585083694,
+ 0.692896228146239,
+ 0.7288276718831138,
+ 0.7291415585081658
+ ],
+ "Precision_Class0": [
+ 0.8993345482148383,
+ 0.8080865264072934,
+ 0.6967180895166893,
+ 0.7544272066792924,
+ 0.7834097409997681
+ ],
+ "Precision_Class1": [
+ 0.5348414314273052,
+ 0.7040033494802467,
+ 0.7238510147214857,
+ 0.7561127795434003,
+ 0.5973198606143634
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.7170879898210718,
+ 0.75604493794377,
+ 0.7102845521190875,
+ 0.7552699931113463,
+ 0.6903648008070657
+ ],
+ "Recall_Class0": [
+ 0.617909514736361,
+ 0.9340533897272939,
+ 0.8579819912153809,
+ 0.9370107652356896,
+ 0.8665750900686002
+ ],
+ "Recall_Class1": [
+ 0.8639855243808522,
+ 0.4142091289995293,
+ 0.49925901693897146,
+ 0.3903405598106549,
+ 0.4523819471427358
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.7409475195586066,
+ 0.6741312593634116,
+ 0.6786205040771762,
+ 0.6636756625231722,
+ 0.659478518605668
+ ],
+ "F1_Macro": [
+ 0.7288225340811386,
+ 0.7127422860358169,
+ 0.6940915920017703,
+ 0.7065165759824615,
+ 0.6745682992776026
+ ]
+ },
+ "training_history": [
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9782952634911788,
+ "val_loss": 0.9660296565607974
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8799464435953843,
+ "val_loss": 0.8670361669440019
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.8316301797565661,
+ "val_loss": 0.8110599078630146
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.802722764642615,
+ "val_loss": 0.7943950891494751
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7856310198181554,
+ "val_loss": 0.7812910205439517
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7811884707526157,
+ "val_loss": 0.732784861012509
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.7606493140521803,
+ "val_loss": 0.7576886509594164
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.7460477430569498,
+ "val_loss": 0.7403356997590316
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.7352087419284018,
+ "val_loss": 0.7286733012450369
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.716345318053898,
+ "val_loss": 0.7126098243813765
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.7060422003269196,
+ "val_loss": 0.6990300040496024
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6940815997751135,
+ "val_loss": 0.6867391372981825
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.687366501281136,
+ "val_loss": 0.6999665906554774
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6529408191379747,
+ "val_loss": 0.7906401408346075
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6308450494941912,
+ "val_loss": 0.68844242158689
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6133884285625658,
+ "val_loss": 0.6674142542638277
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.629728543130975,
+ "val_loss": 0.6234575745306516
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6108525960068953,
+ "val_loss": 0.6271755256150898
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.5783478343173077,
+ "val_loss": 0.6413541796960329
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.580875290851844,
+ "val_loss": 0.59358097220722
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.5787129825667331,
+ "val_loss": 0.5643137316954764
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.5581291498322236,
+ "val_loss": 0.617715426181492
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.5584923915172878,
+ "val_loss": 0.587719799656617
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.5452099418953845,
+ "val_loss": 0.6691652646190241
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.5351277095706839,
+ "val_loss": 0.5726285090571955
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.5508350369177366,
+ "val_loss": 0.5366921566034618
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.5389897086118397,
+ "val_loss": 0.6630312110248365
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.5296880736162788,
+ "val_loss": 0.5901887965829749
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.5092038410274606,
+ "val_loss": 0.5671893279803427
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.5184917026444485,
+ "val_loss": 0.5980645857359234
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.5513310730457306,
+ "val_loss": 0.5426507294178009
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.5344798706079784,
+ "val_loss": 0.5161211443574805
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5127969658688495,
+ "val_loss": 0.5079363835485358
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5010611744303453,
+ "val_loss": 0.6590025001450589
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.5017043171744597,
+ "val_loss": 0.49101689928456355
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.4934265221420087,
+ "val_loss": 0.6897680351608678
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5089315881854609,
+ "val_loss": 0.6924404313689784
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.4801118452298014,
+ "val_loss": 0.5806201272889188
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.48290633057293136,
+ "val_loss": 0.5206742286682129
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.4959216768804349,
+ "val_loss": 0.6338655321221602
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9974953014599649,
+ "val_loss": 0.8824763956822848
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8934510616879714,
+ "val_loss": 0.8710949452299821
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.8450605147763303,
+ "val_loss": 0.8185739015278063
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.8166728380479311,
+ "val_loss": 0.7716304440247385
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.792537293936077,
+ "val_loss": 0.7364468135331806
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7680512682387703,
+ "val_loss": 0.7429663727157995
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.7525224983692169,
+ "val_loss": 0.7309046293559828
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.7388328392254678,
+ "val_loss": 0.6947808924474215
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.7394280119946128,
+ "val_loss": 0.7009285343320746
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.7360590413997048,
+ "val_loss": 0.6969530739282307
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.7228422525681948,
+ "val_loss": 0.7232426938257719
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.719280376246101,
+ "val_loss": 0.6855535664056477
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.7161899130595358,
+ "val_loss": 0.6737898211730154
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6847637060441469,
+ "val_loss": 0.9125686946668123
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.7062429519076097,
+ "val_loss": 0.6926358875475431
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6880306460355458,
+ "val_loss": 0.622061097308209
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6640325125895048,
+ "val_loss": 0.7388643136149958
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6512430686699716,
+ "val_loss": 0.769755703838248
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6393229130067324,
+ "val_loss": 0.5958146217622255
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6232067487741771,
+ "val_loss": 0.8356953812272925
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6301122825396689,
+ "val_loss": 0.5688924601203517
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.5991951344828856,
+ "val_loss": 0.5663922648680838
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6105075078575235,
+ "val_loss": 0.5869539223219219
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6091038443540272,
+ "val_loss": 0.5738015410147215
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.580035368862905,
+ "val_loss": 0.6554325583734011
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.5587427749445564,
+ "val_loss": 0.5311876535415649
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.574479037209561,
+ "val_loss": 0.5282798958452124
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.5799292725952048,
+ "val_loss": 0.5749254085515675
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.5827220670486751,
+ "val_loss": 0.5622332300010481
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.5543721247660486,
+ "val_loss": 0.5025433709746913
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.5552083277388623,
+ "val_loss": 0.5381662563273781
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.5432161328039671,
+ "val_loss": 0.5972278212246142
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5369792251210463,
+ "val_loss": 0.5782754546717593
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5542545099007455,
+ "val_loss": 0.7766273209923192
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.5371665687937486,
+ "val_loss": 0.6410369245629561
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.5569767834324586,
+ "val_loss": 0.499386003142909
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5237749077771839,
+ "val_loss": 0.48754265904426575
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.5246170618032154,
+ "val_loss": 0.691419453997361
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.5314645586829436,
+ "val_loss": 0.46942514494845744
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.5322178868870986,
+ "val_loss": 0.5039825721790916
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9737736269047386,
+ "val_loss": 0.9587837269431666
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8606955409049988,
+ "val_loss": 1.0022669340434827
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.8259592119016146,
+ "val_loss": 0.9165510033306322
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7895514588606986,
+ "val_loss": 0.8616542282857393
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7794855927166185,
+ "val_loss": 0.8376743322924564
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7569237304361243,
+ "val_loss": 0.8789956820638556
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.7310931306136282,
+ "val_loss": 0.7970016410476283
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.7251686532246439,
+ "val_loss": 0.8161585676042658
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.7068797695009332,
+ "val_loss": 0.82554881196273
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6845967926477131,
+ "val_loss": 0.7317398253240084
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6791346308432127,
+ "val_loss": 0.7228598924059617
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.637737050652504,
+ "val_loss": 0.7337090137757754
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6133330683959158,
+ "val_loss": 0.7120763439881174
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.5961683996413883,
+ "val_loss": 0.6997834067595633
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.5984883300567928,
+ "val_loss": 0.6581047365539953
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.5568776624767404,
+ "val_loss": 0.7379887982418663
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.5650227744328348,
+ "val_loss": 0.6659971961849614
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.5495574309637672,
+ "val_loss": 0.9037158473541862
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.5302411074701109,
+ "val_loss": 0.6374593298686179
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.5689907881774401,
+ "val_loss": 0.6874984189083702
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.5458451493790275,
+ "val_loss": 0.6762516184857017
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.5030023714429454,
+ "val_loss": 0.7583403148149189
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.5285633771043075,
+ "val_loss": 0.6410825550556183
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.5203232090724143,
+ "val_loss": 0.6060200427707872
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.5110394507646561,
+ "val_loss": 0.6580531236372495
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.5100486803995935,
+ "val_loss": 0.7383358306006381
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.48959574024928243,
+ "val_loss": 0.6630716292481673
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.4909370227863914,
+ "val_loss": 0.581783192722421
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.5077236302589115,
+ "val_loss": 0.9551126125611757
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.49118886809600026,
+ "val_loss": 0.6388280470120279
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.47896192811037364,
+ "val_loss": 0.7198944201594905
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.475569611317233,
+ "val_loss": 0.7072353441464273
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.46843404518930537,
+ "val_loss": 0.6529329372079749
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.4745055233177386,
+ "val_loss": 0.7735958303275862
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.4720314537224017,
+ "val_loss": 0.6256106527228105
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.4955663077141109,
+ "val_loss": 0.627726430955686
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.4610253227384467,
+ "val_loss": 0.5589552709930822
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.4883037209510803,
+ "val_loss": 0.6930212786323146
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.44794197380542755,
+ "val_loss": 0.5705481250035135
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.4590422526786202,
+ "val_loss": 0.6257548363585221
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9821800558190596,
+ "val_loss": 1.0995569385980304
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8829712742253354,
+ "val_loss": 0.8669467537026656
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.841982690911544,
+ "val_loss": 0.821884672892721
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.8057386765354558,
+ "val_loss": 0.8440032444502178
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.785337220681341,
+ "val_loss": 0.8181296775215551
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7613941870237652,
+ "val_loss": 0.7653016730358726
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.7492017824398843,
+ "val_loss": 0.7605060307603133
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.7199174777457589,
+ "val_loss": 0.7550779847722304
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.7231571925313849,
+ "val_loss": 0.6988285215277421
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6797224298903817,
+ "val_loss": 0.671309254671398
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6638353906179729,
+ "val_loss": 0.6512071835367303
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6396217573630182,
+ "val_loss": 0.6093142660040605
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6209865463407416,
+ "val_loss": 0.6649093047568673
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6424691222215954,
+ "val_loss": 0.6520960205479672
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6161307261178368,
+ "val_loss": 0.6013219027142775
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.5978133435312071,
+ "val_loss": 0.5640227731905485
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.5913749101914858,
+ "val_loss": 0.5579644425919181
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.5875121441326643,
+ "val_loss": 0.5729478597640991
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.5861833879822179,
+ "val_loss": 0.5569576228919783
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.5865427592867299,
+ "val_loss": 0.53999725454732
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.5436443921766783,
+ "val_loss": 0.5564998153008913
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.5279994473645562,
+ "val_loss": 0.5596660203055331
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.5493342272545162,
+ "val_loss": 0.5543868008412813
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.5476836232762587,
+ "val_loss": 0.5392853413757525
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.5294315469892401,
+ "val_loss": 0.6097846486066517
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.5399831400105828,
+ "val_loss": 0.5476228453611073
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.5514220501247206,
+ "val_loss": 0.5202389861408033
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.522476968012358,
+ "val_loss": 0.5682373611550582
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.5074425457339538,
+ "val_loss": 0.49794899005638926
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.52204482649502,
+ "val_loss": 0.5965996964981681
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.5159159694847307,
+ "val_loss": 0.5079836531689292
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.5145666026755383,
+ "val_loss": 0.5684345976302498
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5134834333469993,
+ "val_loss": 0.5280399197026303
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.4761050123917429,
+ "val_loss": 0.5368291218029825
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.4847909612090964,
+ "val_loss": 0.5319254994392395
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.49686715085255473,
+ "val_loss": 0.48524096294453267
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.4623874378831763,
+ "val_loss": 0.5612741473474001
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.5074713206604907,
+ "val_loss": 0.689873135403583
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.488871768116951,
+ "val_loss": 0.5292772352695465
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.4493683992247832,
+ "val_loss": 0.5648287315117685
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9897907344918502,
+ "val_loss": 0.7620149950186411
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8880736278860193,
+ "val_loss": 0.8529694841967689
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.8507455414847324,
+ "val_loss": 0.8172948294215732
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.8163878211849614,
+ "val_loss": 0.7671819395489163
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7905454447394923,
+ "val_loss": 0.767575290468004
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7808924725181178,
+ "val_loss": 0.7380398015181223
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.7630606528959776,
+ "val_loss": 0.7270161840650771
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.7564832866191864,
+ "val_loss": 0.7220646209186978
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.7389100037123028,
+ "val_loss": 0.7153741088178422
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.7333773688266152,
+ "val_loss": 0.6942645708719889
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.7172815203666687,
+ "val_loss": 0.6781329015890757
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.7065906320747576,
+ "val_loss": 0.6701641612582736
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6983746917624223,
+ "val_loss": 0.6839750707149506
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.695831907422919,
+ "val_loss": 0.6559845374690162
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6923322442330813,
+ "val_loss": 0.663729214006
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6739154426675094,
+ "val_loss": 0.6440979772143893
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6673510341267836,
+ "val_loss": 0.6206014421251085
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6404934745085867,
+ "val_loss": 0.6159142951170603
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.606957063863152,
+ "val_loss": 0.6310470501581827
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.5980770627134725,
+ "val_loss": 0.7483103225628535
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6061642993437616,
+ "val_loss": 0.6168163700236214
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.5732162712435973,
+ "val_loss": 0.5698644949330224
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.5729008505218908,
+ "val_loss": 0.6002054413159689
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.575344495867428,
+ "val_loss": 0.5573575364218818
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.5669746093059841,
+ "val_loss": 0.5523706939485338
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.5520527253025457,
+ "val_loss": 0.6587678740421931
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.5331328895531202,
+ "val_loss": 0.7645955830812454
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.5496089278083098,
+ "val_loss": 0.5229694843292236
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.5475781724641198,
+ "val_loss": 0.6291607320308685
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.5266956585018259,
+ "val_loss": 0.5770011362102296
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.514133411018472,
+ "val_loss": 0.6408022079202864
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.4961081696184058,
+ "val_loss": 0.7221046934525172
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5093157926672384,
+ "val_loss": 0.7022178868452708
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5155146968992133,
+ "val_loss": 0.6416690564817853
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.502227290680534,
+ "val_loss": 0.6377695053815842
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.5008755804676759,
+ "val_loss": 0.5316363986995485
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5050775871465081,
+ "val_loss": 0.5309505528873868
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.4798860534241325,
+ "val_loss": 0.5429114550352097
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.47902561175195796,
+ "val_loss": 0.8269111845228407
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.4700691613711809,
+ "val_loss": 0.664068318075604
+ }
+ ]
+ ]
+ },
+ "ViT_Big_Model_Node": {
+ "train_size": 75,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.662898816560444,
+ 0.7252849779630962,
+ 0.572780408357319,
+ 0.6666091115851152,
+ 0.6957138909233941
+ ],
+ "Mask_Cohesion": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Class0": [
+ 0.6628958385702227,
+ 0.7252645137454785,
+ 0.5727175264753857,
+ 0.6665712884155985,
+ 0.6956115807357464
+ ],
+ "IoU_Class1": [
+ 2.6205887748271007e-05,
+ 0.0002710732014548813,
+ 0.0003443925793500706,
+ 0.0003401366687195269,
+ 0.001103390316323776
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.3314610222289855,
+ 0.3627677934734667,
+ 0.28653095952736785,
+ 0.33345571254215906,
+ 0.34835748552603507
+ ],
+ "IoU_Weighted": [
+ 0.43944263230209346,
+ 0.526130965667773,
+ 0.3281968839785514,
+ 0.44440894252800045,
+ 0.48423940759337886
+ ],
+ "Dice_Class0": [
+ 0.7972788471708346,
+ 0.8407574699035094,
+ 0.7283158187457882,
+ 0.7999313237290974,
+ 0.8204845834255414
+ ],
+ "Dice_Class1": [
+ 5.241040203542943e-05,
+ 0.0005419994813751593,
+ 0.000688548027868817,
+ 0.0006800420302083095,
+ 0.0022043483759956748
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.398665628786435,
+ 0.42064973469244227,
+ 0.3645021833868285,
+ 0.40030568287965285,
+ 0.41134446590076856
+ ],
+ "Dice_Weighted": [
+ 0.5285339918375956,
+ 0.6099759495732183,
+ 0.41746982786741016,
+ 0.5334119044099654,
+ 0.571442637041272
+ ],
+ "Precision_Class0": [
+ 0.6629026263888523,
+ 0.725351607816189,
+ 0.5728102342606649,
+ 0.6665999324893769,
+ 0.6958024578260928
+ ],
+ "Precision_Class1": [
+ 0.4631578947368421,
+ 0.3828689370485036,
+ 0.4762833008447043,
+ 0.7252888318356868,
+ 0.5506944444444445
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.5630302605628472,
+ 0.5541102724323463,
+ 0.5245467675526846,
+ 0.6959443821625318,
+ 0.6232484511352687
+ ],
+ "Recall_Class0": [
+ 0.999984553556907,
+ 0.9998344719155862,
+ 0.9997174845186073,
+ 0.9999355393145419,
+ 0.9996057880309447
+ ],
+ "Recall_Class1": [
+ 2.6206683776454606e-05,
+ 0.00027119169378583607,
+ 0.0003445230470877932,
+ 0.0003401804943514986,
+ 0.0011043845319317538
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.5000053801203417,
+ 0.500052831804686,
+ 0.5000310037828476,
+ 0.5001378599044467,
+ 0.5003550862814382
+ ],
+ "F1_Macro": [
+ 0.5296495219502821,
+ 0.5256955204525113,
+ 0.5119955830556886,
+ 0.5820137138827279,
+ 0.5550810800391901
+ ]
+ },
+ "training_history": [
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9649505740717838,
+ "val_loss": 0.889611796328896
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8752195333179674,
+ "val_loss": 0.7736358328869468
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.8356657169367138,
+ "val_loss": 0.8177135178917333
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.805647296340842,
+ "val_loss": 0.7231214736637316
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7876383364200592,
+ "val_loss": 0.7420717038606343
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7654261338083368,
+ "val_loss": 0.7258448098835192
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.7431306337055407,
+ "val_loss": 0.7360278273883619
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.729563694251211,
+ "val_loss": 0.7276282969274019
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.7198378114323867,
+ "val_loss": 0.6970678160065099
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.7125704413966129,
+ "val_loss": 0.6938428690558985
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.705274351333317,
+ "val_loss": 0.7047121179731268
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6999549881408089,
+ "val_loss": 0.6977868707556474
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.696609837444205,
+ "val_loss": 0.6901296879115858
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6891379575980338,
+ "val_loss": 0.6813062586282429
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6857074954007801,
+ "val_loss": 0.6784595721646359
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6823113184226187,
+ "val_loss": 0.6733221129367226
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.678384144055216,
+ "val_loss": 0.6705080458992406
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6762058923118993,
+ "val_loss": 0.670616090297699
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6738105243758151,
+ "val_loss": 0.6681406780293113
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6756195002480557,
+ "val_loss": 0.6665662119263097
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6693890486892901,
+ "val_loss": 0.6650596417878804
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6672821013551009,
+ "val_loss": 0.6642034524365475
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6661692810686011,
+ "val_loss": 0.664667851046512
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6633941044932917,
+ "val_loss": 0.6627658260496039
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.663929244405345,
+ "val_loss": 0.6629213000598707
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.667588660591527,
+ "val_loss": 0.658292648039366
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6670806329501303,
+ "val_loss": 0.6582935452461243
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6618769074741163,
+ "val_loss": 0.6551792998062936
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6575543002078408,
+ "val_loss": 0.6559319151075262
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6563828054227328,
+ "val_loss": 0.6568559282704404
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6548691282146856,
+ "val_loss": 0.6551013149713215
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.654165118932724,
+ "val_loss": 0.65552839793657
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6573600329850849,
+ "val_loss": 0.653634061938838
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6526183940862355,
+ "val_loss": 0.6524446073331331
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6522679109322397,
+ "val_loss": 0.6515014485308999
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6507151252345035,
+ "val_loss": 0.65265932522322
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6565174695692564,
+ "val_loss": 0.6498821252270749
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6532419261179472,
+ "val_loss": 0.6494416218054923
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6494972219592646,
+ "val_loss": 0.6498424787270395
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.6487167242326235,
+ "val_loss": 0.6500592859167802
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9652915738130871,
+ "val_loss": 0.6984350681304932
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8590296083375027,
+ "val_loss": 0.7964965010944166
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.8498802436025519,
+ "val_loss": 0.7406494837058218
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.8182971195170754,
+ "val_loss": 0.7928224300083361
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7895096038517199,
+ "val_loss": 0.7845803405109205
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7702787643984744,
+ "val_loss": 0.7097363879806117
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.7596416849839059,
+ "val_loss": 0.7240426289407831
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.7438216303524218,
+ "val_loss": 0.7191401876901325
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.7393318932307394,
+ "val_loss": 0.6900541594153956
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.7269229386982164,
+ "val_loss": 0.6682416482975608
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.7190059501873819,
+ "val_loss": 0.6651396625920346
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.7126792449700204,
+ "val_loss": 0.66889614807932
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.7099034943078694,
+ "val_loss": 0.6473864097344247
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.7009128473306957,
+ "val_loss": 0.6663461484407124
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6982219783883346,
+ "val_loss": 0.6464309723753678
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6980178230687192,
+ "val_loss": 0.6596690290852597
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.690119743347168,
+ "val_loss": 0.6477409162019429
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6885342707759455,
+ "val_loss": 0.6370899269455358
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6903806422886095,
+ "val_loss": 0.6304573887272885
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6871745994216517,
+ "val_loss": 0.629372565369857
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6799523250052804,
+ "val_loss": 0.6345243453979492
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6790037076724204,
+ "val_loss": 0.6317181838186163
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.676681670703386,
+ "val_loss": 0.6334207057952881
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6753772559918856,
+ "val_loss": 0.6334940697017469
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6779675875839434,
+ "val_loss": 0.6317229082709864
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6765006686511793,
+ "val_loss": 0.6191608435229251
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6716541867507132,
+ "val_loss": 0.6226463098275034
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6701896457295669,
+ "val_loss": 0.6235848508383098
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6678538887124312,
+ "val_loss": 0.6240780604513068
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6668722958941209,
+ "val_loss": 0.6296739954697458
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.67197332884136,
+ "val_loss": 0.6232673839518899
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6700812468403264,
+ "val_loss": 0.6196303461727343
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.663726303138231,
+ "val_loss": 0.6178370306366369
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6631980961874911,
+ "val_loss": 0.6134691583482843
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6627479226965654,
+ "val_loss": 0.6239235275670102
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6610529030624189,
+ "val_loss": 0.6123506866003338
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6600696946445265,
+ "val_loss": 0.6071981850423311
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.660756357406315,
+ "val_loss": 0.6072289520188382
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6651004348930559,
+ "val_loss": 0.6065761509694552
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.6656580583045357,
+ "val_loss": 0.6141808472181621
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9576318577716225,
+ "val_loss": 0.9021338789086593
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8624165324788344,
+ "val_loss": 0.9055722104875665
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.8188029700203946,
+ "val_loss": 0.8567261852716145
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7862824044729534,
+ "val_loss": 0.837649803412588
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7604214329468576,
+ "val_loss": 0.842330631456877
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7444157631773698,
+ "val_loss": 0.8113082459098414
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.723355526986875,
+ "val_loss": 0.8071863212083515
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.711176401690433,
+ "val_loss": 0.8085615195726094
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.7018439518777948,
+ "val_loss": 0.7948282706110101
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6952601373195648,
+ "val_loss": 0.7896699936766374
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6874431497172305,
+ "val_loss": 0.769668139909443
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.681808236398195,
+ "val_loss": 0.7823563029891566
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6809893830826408,
+ "val_loss": 0.7586452772742823
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6716369531656566,
+ "val_loss": 0.7592120609785381
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6677213907241821,
+ "val_loss": 0.7623124279473957
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6703301307402159,
+ "val_loss": 0.7633450125393114
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6603765817064988,
+ "val_loss": 0.7519445042861136
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6581956885362926,
+ "val_loss": 0.7560334613448695
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.655661868421655,
+ "val_loss": 0.7533906039438749
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6573570166763506,
+ "val_loss": 0.7495049238204956
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6512042770260259,
+ "val_loss": 0.7523529906021921
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6486556279031854,
+ "val_loss": 0.7432014408864474
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6522460109309146,
+ "val_loss": 0.7468828841259605
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6451973617076874,
+ "val_loss": 0.7373224371357968
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6437295518423382,
+ "val_loss": 0.74092015467192
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6423873650400262,
+ "val_loss": 0.7329048420253553
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6464163849228307,
+ "val_loss": 0.7317060138049879
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.644808049264707,
+ "val_loss": 0.7375214131254899
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6398137588250009,
+ "val_loss": 0.7309545968708239
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6384902674900857,
+ "val_loss": 0.7363420285676655
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6368953271916038,
+ "val_loss": 0.753553475204267
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6356376331103476,
+ "val_loss": 0.7338736182764957
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6355983379640078,
+ "val_loss": 0.7385409474372864
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6348940337959089,
+ "val_loss": 0.7407624847010562
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6336338739646109,
+ "val_loss": 0.7305473873489782
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6319373485289121,
+ "val_loss": 0.7214069272342482
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6372070579152358,
+ "val_loss": 0.7317360576830412
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6323903108897962,
+ "val_loss": 0.7318670404584784
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6360243072635249,
+ "val_loss": 0.7345142442929117
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.6312815452876844,
+ "val_loss": 0.7293745875358582
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9726471900939941,
+ "val_loss": 0.8761791900584572
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8778298356031117,
+ "val_loss": 0.8890231973246524
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.8358626961708069,
+ "val_loss": 0.7937705861894708
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.8024770727283076,
+ "val_loss": 0.7742416858673096
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.775563258873789,
+ "val_loss": 0.7517143864380685
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7573083748942927,
+ "val_loss": 0.7346256281200209
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.7422751517672288,
+ "val_loss": 0.7301960838468451
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.7310988777562192,
+ "val_loss": 0.7076020774088407
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.7207827505312467,
+ "val_loss": 0.7153944686839455
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.7120754373700995,
+ "val_loss": 0.7031371562104476
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.7047532561578249,
+ "val_loss": 0.6998257887990851
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6996581648525438,
+ "val_loss": 0.6976142870752435
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6944238072947452,
+ "val_loss": 0.6960807756373757
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6899006539269498,
+ "val_loss": 0.6801597695601614
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6848384957564505,
+ "val_loss": 0.7072005962070665
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.688997778453325,
+ "val_loss": 0.6793604179432517
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6798276744390789,
+ "val_loss": 0.6659901581312481
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6818492773332094,
+ "val_loss": 0.6716537506956803
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.673907469761999,
+ "val_loss": 0.6716745652650532
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6732441814322221,
+ "val_loss": 0.6667156031257228
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6711779895581698,
+ "val_loss": 0.6596918702125549
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.667876378486031,
+ "val_loss": 0.6614927743610582
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6700615255456221,
+ "val_loss": 0.661797118814368
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.669627983319132,
+ "val_loss": 0.6649626462083114
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6619907366602045,
+ "val_loss": 0.6562204643299705
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6664156380452608,
+ "val_loss": 0.6537879705429077
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6592899278590554,
+ "val_loss": 0.6590376684540197
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6599169404883134,
+ "val_loss": 0.6591668285821614
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6581735344309556,
+ "val_loss": 0.6522832575597262
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6618305115323317,
+ "val_loss": 0.651671406469847
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6618079590170007,
+ "val_loss": 0.6525301682321649
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6549062242633418,
+ "val_loss": 0.6562590975510446
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6543604888414082,
+ "val_loss": 0.6484899960066143
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6535141875869349,
+ "val_loss": 0.6491816639900208
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6525723996915316,
+ "val_loss": 0.6492234813539606
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6506142443732211,
+ "val_loss": 0.6495370739384702
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6506194880134181,
+ "val_loss": 0.650691010450062
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6543557832115575,
+ "val_loss": 0.6456171336926912
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6498861767743763,
+ "val_loss": 0.6458332820942527
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.6490298995846197,
+ "val_loss": 0.6442648203749406
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9680246208843432,
+ "val_loss": 0.8294102052847544
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8792604333476016,
+ "val_loss": 0.7469776835706499
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.8434679868974184,
+ "val_loss": 0.7271895905335745
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.814002380559319,
+ "val_loss": 0.7331263224283854
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7875418788508365,
+ "val_loss": 0.7715971966584524
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7658674732634896,
+ "val_loss": 0.7173278331756592
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.7499405208386873,
+ "val_loss": 0.6932135025660197
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.7371305886067843,
+ "val_loss": 0.6957490974002414
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.7276364831547988,
+ "val_loss": 0.6759270429611206
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.7188913163385893,
+ "val_loss": 0.6923221283488803
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.712166406606373,
+ "val_loss": 0.6645786166191101
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.7064098982434523,
+ "val_loss": 0.6654787328508165
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.7009425822057223,
+ "val_loss": 0.6744380394617716
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6959880433584514,
+ "val_loss": 0.6650709840986464
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6921631756581759,
+ "val_loss": 0.654829763703876
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.688817068150169,
+ "val_loss": 0.6597536073790656
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6859649623695173,
+ "val_loss": 0.6483379271295335
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6830220551867234,
+ "val_loss": 0.6541232433584001
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6806309380029377,
+ "val_loss": 0.6450393001238505
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6789118020158065,
+ "val_loss": 0.6475225918822818
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6772874374138681,
+ "val_loss": 0.6418732669618394
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6740781696219194,
+ "val_loss": 0.6426797012488047
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6721301188594416,
+ "val_loss": 0.6367298662662506
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6705936115039023,
+ "val_loss": 0.63381627202034
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6690071350649783,
+ "val_loss": 0.6347561942206489
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6673683006512491,
+ "val_loss": 0.6327317853768667
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6665465784700293,
+ "val_loss": 0.6379377676381005
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6646773893582193,
+ "val_loss": 0.6298967666096158
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6638813505047246,
+ "val_loss": 0.6263806058300866
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6629826426506042,
+ "val_loss": 0.6345744000540839
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6614781900456077,
+ "val_loss": 0.6351709266503652
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6612288449939928,
+ "val_loss": 0.6268786589304606
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6599354665530356,
+ "val_loss": 0.6308671103583442
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6594608708431846,
+ "val_loss": 0.6245702869362302
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6582928707725123,
+ "val_loss": 0.6246830158763461
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6577121348757493,
+ "val_loss": 0.6300600734021928
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6568491772601479,
+ "val_loss": 0.6221543484263949
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6581409291217202,
+ "val_loss": 0.62795446978675
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6562835087901667,
+ "val_loss": 0.6295472747749753
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.6558896742368999,
+ "val_loss": 0.6233564648363326
+ }
+ ]
+ ]
+ },
+ "Swin_Big_Model_Node": {
+ "train_size": 75,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.6629002219752261,
+ 0.5791770533511513,
+ 0.6982124730160362,
+ 0.6658945585552015,
+ 0.7167445288764106
+ ],
+ "Mask_Cohesion": [
+ 0.0,
+ 0.05263157894736842,
+ 0.15789473684210525,
+ 0.0,
+ 0.05555555555555555
+ ],
+ "IoU_Class0": [
+ 0.6629002219752261,
+ 0.522179060176858,
+ 0.6321297165845646,
+ 0.6644104342481078,
+ 0.6895903843567899
+ ],
+ "IoU_Class1": [
+ 0.0,
+ 0.22086207704094263,
+ 0.37319543345670647,
+ 0.013063735752776496,
+ 0.23596014958767353
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.33145011098761307,
+ 0.3715205686089003,
+ 0.5026625750206355,
+ 0.33873708500044214,
+ 0.4627752669722317
+ ],
+ "IoU_Weighted": [
+ 0.43943670429480414,
+ 0.4394156043497416,
+ 0.5214955285485661,
+ 0.4472114783835486,
+ 0.5515289333200146
+ ],
+ "Dice_Class0": [
+ 0.7972820175438067,
+ 0.6860941315487382,
+ 0.7746072020640309,
+ 0.7983733105449475,
+ 0.8162811421530551
+ ],
+ "Dice_Class1": [
+ 0.0,
+ 0.36181331404159234,
+ 0.5435430738613396,
+ 0.02579055056801384,
+ 0.38182485036656205
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.39864100877190334,
+ 0.5239537227951653,
+ 0.6590751379626852,
+ 0.4120819305564807,
+ 0.5990529962598086
+ ],
+ "Dice_Weighted": [
+ 0.5285184264066456,
+ 0.5970226460610841,
+ 0.6758718942755836,
+ 0.5407468127464757,
+ 0.6840552421606777
+ ],
+ "Precision_Class0": [
+ 0.6629002219752261,
+ 0.7474632603080799,
+ 0.6768949492132749,
+ 0.6678089313939832,
+ 0.7436940977100208
+ ],
+ "Precision_Class1": [
+ 0.0,
+ 0.31005687608902915,
+ 0.7680205154260487,
+ 0.46606152934704415,
+ 0.5685425943806188
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.33145011098761307,
+ 0.5287600681985545,
+ 0.7224577323196617,
+ 0.5669352303705137,
+ 0.6561183460453198
+ ],
+ "Recall_Class0": [
+ 1.0,
+ 0.6340376119667394,
+ 0.9052889632137503,
+ 0.9923987598246067,
+ 0.9045702388851418
+ ],
+ "Recall_Class1": [
+ 0.0,
+ 0.4343109391858109,
+ 0.42060764841164533,
+ 0.013262222564744176,
+ 0.28742895660033396
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.5,
+ 0.5341742755762751,
+ 0.6629483058126978,
+ 0.5028304911946754,
+ 0.5959995977427379
+ ],
+ "F1_Macro": [
+ 0.39864100877190334,
+ 0.5314533828694474,
+ 0.6914249201746945,
+ 0.5329621516488274,
+ 0.6246157117301602
+ ]
+ },
+ "training_history": [
+ [
+ {
+ "epoch": 1,
+ "train_loss": 1.0008665997731059,
+ "val_loss": 0.854330815766987
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8617356090169204,
+ "val_loss": 0.8932691247839677
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.8268803263965406,
+ "val_loss": 0.8151732275360509
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.8055013747591722,
+ "val_loss": 0.7982713768356725
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7868987808102056,
+ "val_loss": 0.7652425169944763
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7587565387550154,
+ "val_loss": 0.7586627382981149
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.7449364646484977,
+ "val_loss": 0.7410688055189032
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.7350774454443079,
+ "val_loss": 0.7296115693293119
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.7306150245039087,
+ "val_loss": 0.7282399315583078
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.7201731424582632,
+ "val_loss": 0.7190337432058234
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.712422240721552,
+ "val_loss": 0.7145941006509882
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.7055209561398155,
+ "val_loss": 0.7089615150501853
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.7019581449659247,
+ "val_loss": 0.7001521305034035
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6990694215423182,
+ "val_loss": 0.7002774257408945
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6920814169080634,
+ "val_loss": 0.6894766625605131
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6858387260060561,
+ "val_loss": 0.6853965520858765
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6854782700538635,
+ "val_loss": 0.6867628034792448
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6822634480501476,
+ "val_loss": 0.6812704111400404
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.676393311274679,
+ "val_loss": 0.6766606412435833
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6732900126984245,
+ "val_loss": 0.6814951990780077
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6867846689726177,
+ "val_loss": 0.7109654263446206
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6840936243534088,
+ "val_loss": 0.6659973765674391
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6774058091013055,
+ "val_loss": 0.6717572212219238
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.675339002358286,
+ "val_loss": 0.6649759129474038
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6660310296635878,
+ "val_loss": 0.6670566295322619
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6622100033258137,
+ "val_loss": 0.6641315974687275
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6616213055033433,
+ "val_loss": 0.6611236271105314
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6591985711925908,
+ "val_loss": 0.6662783559999967
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6626799122283333,
+ "val_loss": 0.6608689646971854
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6555540734215787,
+ "val_loss": 0.6582892599858736
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6619677135818883,
+ "val_loss": 0.6584991875447725
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6613424122333527,
+ "val_loss": 0.6555344242798654
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6600612919581564,
+ "val_loss": 0.6549055639066195
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6522386576000013,
+ "val_loss": 0.6516221887186954
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6533575183466861,
+ "val_loss": 0.6583207500608343
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6573934978560397,
+ "val_loss": 0.6508335345669797
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6467753021340621,
+ "val_loss": 0.6493564436310216
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6503792483555643,
+ "val_loss": 0.6493740238641438
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6512187484063601,
+ "val_loss": 0.6494685944757963
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.6504934869314495,
+ "val_loss": 0.6491635284925762
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 1.0240657172704999,
+ "val_loss": 0.8682165271357486
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8767211782304865,
+ "val_loss": 0.843136169408497
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.8263783533322183,
+ "val_loss": 0.8067126650559274
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.8029867536143253,
+ "val_loss": 0.7534553879185727
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7831419894569799,
+ "val_loss": 0.7369034980472765
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7653166416444277,
+ "val_loss": 0.7315539152998674
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.7527929466021689,
+ "val_loss": 0.7138953867711519
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.7432162793059098,
+ "val_loss": 0.7051894633393538
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.7333925335030806,
+ "val_loss": 0.6802554789342379
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.7276396422009719,
+ "val_loss": 0.6849691052185861
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.7168782400457483,
+ "val_loss": 0.673954963684082
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.7163681074192649,
+ "val_loss": 0.6754934442670721
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.7075368244397012,
+ "val_loss": 0.6827577415265536
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.7035462919034456,
+ "val_loss": 0.7569904954809892
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.707732911172666,
+ "val_loss": 0.6546058717526888
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.7046379211701845,
+ "val_loss": 0.650064816600398
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.695529293072851,
+ "val_loss": 0.6430530422612241
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6915903060059798,
+ "val_loss": 0.649476032508047
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6866917390572397,
+ "val_loss": 0.6437663216339914
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6891735070630124,
+ "val_loss": 0.6477950497677452
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6821338393186268,
+ "val_loss": 0.6372027648122687
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6800282723025272,
+ "val_loss": 0.6260761493130734
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6774611473083496,
+ "val_loss": 0.6326604548253512
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6765867738347304,
+ "val_loss": 0.6264365629145974
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6785723206244016,
+ "val_loss": 0.6277959315400374
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6701632753798836,
+ "val_loss": 0.6470594657094855
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6713420482058274,
+ "val_loss": 0.6314338696630377
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6655122026016838,
+ "val_loss": 0.636308817487014
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6659978141910151,
+ "val_loss": 0.6171508541232661
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6732116435703478,
+ "val_loss": 0.6295027419140464
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.66767030013235,
+ "val_loss": 0.6196483875575819
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6644840867895829,
+ "val_loss": 0.6153998500422427
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6653449347144679,
+ "val_loss": 0.6218659062134592
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6631036369424117,
+ "val_loss": 0.6160834588502583
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6661962929524874,
+ "val_loss": 0.6190206502613268
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6606014119951349,
+ "val_loss": 0.6194690309072796
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6611652154671518,
+ "val_loss": 0.6129353422867624
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6587261071330622,
+ "val_loss": 0.6082878206905565
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6612658626154849,
+ "val_loss": 0.6338918428671988
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.6562587885480178,
+ "val_loss": 0.6749081862600226
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9814840853214264,
+ "val_loss": 0.9546675556584409
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8477845458607924,
+ "val_loss": 0.8771876535917583
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.8054669217059487,
+ "val_loss": 0.8519903860594097
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7714441904896184,
+ "val_loss": 0.8391279389983729
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7552502249416552,
+ "val_loss": 0.8203936972116169
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7364668642219744,
+ "val_loss": 0.8180230322637057
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.7208705403302845,
+ "val_loss": 0.7986957054389151
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.7110631685507925,
+ "val_loss": 0.8010588852982772
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.7026049871193735,
+ "val_loss": 0.7932115855969881
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6935565659874364,
+ "val_loss": 0.7797578353630868
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6871304841418016,
+ "val_loss": 0.7768980860710144
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6798459887504578,
+ "val_loss": 0.7843257126055265
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6762195888318514,
+ "val_loss": 0.7647919184283206
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6709767799628409,
+ "val_loss": 0.7565503936064871
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6703710885424363,
+ "val_loss": 0.7647272630741722
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6659662676484961,
+ "val_loss": 0.7605608827189395
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6612074861401006,
+ "val_loss": 0.7420199513435364
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6557579166010806,
+ "val_loss": 0.7526153012325889
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6503315332688784,
+ "val_loss": 0.6960548350685521
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6306353603538714,
+ "val_loss": 0.7180913624010588
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6364052860360396,
+ "val_loss": 0.6941407071916681
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6094440072774887,
+ "val_loss": 0.8267416044285423
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.5987761318683624,
+ "val_loss": 0.6714300905403338
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.5866723656654358,
+ "val_loss": 0.6638238900586179
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.569888078068432,
+ "val_loss": 0.7522912621498108
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.5730802887364438,
+ "val_loss": 0.6481933311412209
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.5597246163769772,
+ "val_loss": 0.6038091276821337
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.5429607386651792,
+ "val_loss": 0.7225693432908309
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.5632166227227763,
+ "val_loss": 0.6291389653557226
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.5260223550231833,
+ "val_loss": 0.642876568593477
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.5382339491655952,
+ "val_loss": 0.5863752772933558
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.5399257744613447,
+ "val_loss": 0.692975274826351
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5187107450083682,
+ "val_loss": 0.6971602235969744
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5265898908439436,
+ "val_loss": 0.7529860766310441
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.5045595537675055,
+ "val_loss": 0.6764448551755202
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.5157631721935774,
+ "val_loss": 0.6307746297434756
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5016186990235981,
+ "val_loss": 0.7471729626781062
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.5096461169029537,
+ "val_loss": 0.6865116279376181
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.49214752096878855,
+ "val_loss": 0.6579547320541582
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.4952999925927112,
+ "val_loss": 0.6435167663975766
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9817898524434943,
+ "val_loss": 0.8669096702023557
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8655101092238175,
+ "val_loss": 0.8403414864289133
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.816459773402465,
+ "val_loss": 0.7948725850958573
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7988042972589794,
+ "val_loss": 0.7769461870193481
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7726553207949588,
+ "val_loss": 0.7749081881422746
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7586503389634585,
+ "val_loss": 0.7486783485663565
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.7423373398027922,
+ "val_loss": 0.7424973876852738
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.7330610814847445,
+ "val_loss": 0.7276394680926674
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.7271940441508042,
+ "val_loss": 0.7189925187512448
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.7139768553407568,
+ "val_loss": 0.7074077913635656
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.7097054782666659,
+ "val_loss": 0.7173751718119571
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.7080210086546446,
+ "val_loss": 0.7054185365375719
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6971925481369621,
+ "val_loss": 0.7004605312096445
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6979866827789106,
+ "val_loss": 0.689528204892811
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6902952194213867,
+ "val_loss": 0.6869872369264302
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6835504362457677,
+ "val_loss": 0.6866551072973954
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6843766021101099,
+ "val_loss": 0.6831753849983215
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.678254130639528,
+ "val_loss": 0.693696025170778
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6819427703556261,
+ "val_loss": 0.67474942772012
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6831445882194921,
+ "val_loss": 0.6753036348443282
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.672393450611516,
+ "val_loss": 0.6740895666574177
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6745575791911075,
+ "val_loss": 0.6669948195156298
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6696747855136269,
+ "val_loss": 0.6665891503032885
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6649950240787706,
+ "val_loss": 0.6634658794654044
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6658686556314167,
+ "val_loss": 0.6644023845070287
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6620525482453798,
+ "val_loss": 0.6603207462712338
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6612473475305658,
+ "val_loss": 0.6599671464217337
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6595114955776616,
+ "val_loss": 0.6585912139792192
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6623509275285822,
+ "val_loss": 0.6584458068797463
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6556950744829679,
+ "val_loss": 0.6600255025060553
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6545468647229044,
+ "val_loss": 0.6652261803024694
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6601802706718445,
+ "val_loss": 0.6547680999103346
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6571032216674403,
+ "val_loss": 0.6541224467126947
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.654040033880033,
+ "val_loss": 0.6546597637628254
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6528114441194033,
+ "val_loss": 0.6513382792472839
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6502544001529091,
+ "val_loss": 0.651312812378532
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6507258775987124,
+ "val_loss": 0.6501828745791787
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6486899915494417,
+ "val_loss": 0.650603081050672
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6507013204850649,
+ "val_loss": 0.6508851961085671
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.6482673224649931,
+ "val_loss": 0.6500354989578849
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9931671572358984,
+ "val_loss": 0.7331176168388791
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8831012625443307,
+ "val_loss": 0.8420167863368988
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.8369926559297662,
+ "val_loss": 0.7959301869074503
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.8096531817787572,
+ "val_loss": 0.7639439834488763
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7833492677462729,
+ "val_loss": 0.7298774222532908
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7700781790833724,
+ "val_loss": 0.7550857729381986
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.7572766118928006,
+ "val_loss": 0.7097576724158393
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.7428040206432343,
+ "val_loss": 0.7034788131713867
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.7317554966399544,
+ "val_loss": 0.7195723189247979
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.7244069952713815,
+ "val_loss": 0.791181743144989
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.72908784213819,
+ "val_loss": 0.6925536361005571
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.7117552506296259,
+ "val_loss": 0.7145814134014977
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.7101637846545169,
+ "val_loss": 0.6715165972709656
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.7015171443161211,
+ "val_loss": 0.6613334483570523
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6988780467133773,
+ "val_loss": 0.6891921195718977
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6939385486276526,
+ "val_loss": 0.6473504238658481
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6874209469870517,
+ "val_loss": 1.345492551724116
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6780015026268206,
+ "val_loss": 0.6480240490701463
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6474249347260124,
+ "val_loss": 0.6057607912354999
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6294827265174765,
+ "val_loss": 0.7455781913465924
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6229990501152841,
+ "val_loss": 0.5883837938308716
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6053988541427412,
+ "val_loss": 0.6750426756011115
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6162377588058773,
+ "val_loss": 0.7784909904003143
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.5846759653405139,
+ "val_loss": 0.6041422635316849
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.5718239365439666,
+ "val_loss": 0.5740457756651772
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.5673808565265254,
+ "val_loss": 0.6253320607874129
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.5748774566148457,
+ "val_loss": 0.6318313893344667
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.5795748155368002,
+ "val_loss": 1.498310751385159
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6083309430825082,
+ "val_loss": 0.687788787815306
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.5610122845361107,
+ "val_loss": 0.6442123303810755
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.5432118161728507,
+ "val_loss": 0.6817445357640585
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.5456273861621556,
+ "val_loss": 0.5489125632577472
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5394853557410993,
+ "val_loss": 0.6311951246526506
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5266946177733572,
+ "val_loss": 0.5729205459356308
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.5230177138981066,
+ "val_loss": 0.5634136994679769
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.5234388120864567,
+ "val_loss": 0.6823717190159692
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.542464731555236,
+ "val_loss": 0.7396018306414286
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.5425483176582738,
+ "val_loss": 0.522396034664578
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.5266952098984468,
+ "val_loss": 0.6382868041594824
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.5164880407483954,
+ "val_loss": 0.5533450858460532
+ }
+ ]
+ ]
+ },
+ "Quadtree-CNNModel_Model_Node": {
+ "train_size": 75,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.4742188704641242,
+ 0.5479850769042969,
+ 0.5088171707956415,
+ 0.6116182427657278,
+ 0.6189653608534071
+ ],
+ "Mask_Cohesion": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Class0": [
+ 0.38474107822089276,
+ 0.5112591279228011,
+ 0.3741596127004138,
+ 0.5974723765528435,
+ 0.5867685620548755
+ ],
+ "IoU_Class1": [
+ 0.21667219548878028,
+ 0.15873449348658542,
+ 0.30463579359078585,
+ 0.08297656797014216,
+ 0.1706333351830272
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.3007066368548365,
+ 0.3349968107046933,
+ 0.3393977031455998,
+ 0.3402244722614928,
+ 0.3787009486189513
+ ],
+ "IoU_Weighted": [
+ 0.32808318106909995,
+ 0.41443074433086996,
+ 0.34444548289835514,
+ 0.4259079045458423,
+ 0.4601186413795342
+ ],
+ "Dice_Class0": [
+ 0.5556866684639786,
+ 0.6766002182901852,
+ 0.5445649970240916,
+ 0.7480221696754698,
+ 0.7395767424267675
+ ],
+ "Dice_Class1": [
+ 0.3561718535069101,
+ 0.2739790597049713,
+ 0.46700511374492976,
+ 0.15323797471568165,
+ 0.2915231098495053
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.4559292609854444,
+ 0.47528963899757826,
+ 0.5057850553845107,
+ 0.4506300721955757,
+ 0.5155499261381364
+ ],
+ "Dice_Weighted": [
+ 0.48842712219440176,
+ 0.5660113873803262,
+ 0.5114107794653707,
+ 0.5496846231607784,
+ 0.6032125190689501
+ ],
+ "Precision_Class0": [
+ 0.631729659416354,
+ 0.7188885747169962,
+ 0.5807189755285627,
+ 0.6589841454905992,
+ 0.7053050589973111
+ ],
+ "Precision_Class1": [
+ 0.30326986200428724,
+ 0.24697406475360578,
+ 0.4352707001195675,
+ 0.2806791358784893,
+ 0.3368540400794578
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.4674997607103206,
+ 0.482931319735301,
+ 0.5079948378240651,
+ 0.4698316406845443,
+ 0.5210795495383844
+ ],
+ "Recall_Class0": [
+ 0.495983773360095,
+ 0.6390106513723883,
+ 0.5126488770710645,
+ 0.8648795594262758,
+ 0.777349140806788
+ ],
+ "Recall_Class1": [
+ 0.4314299140956818,
+ 0.30761471189354667,
+ 0.5037307662841192,
+ 0.10538731506072373,
+ 0.25694557598597306
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.4637068437278884,
+ 0.47331268163296747,
+ 0.5081898216775919,
+ 0.4851334372434998,
+ 0.5171473583963805
+ ],
+ "F1_Macro": [
+ 0.46559557771459226,
+ 0.4780736248518094,
+ 0.5080923110442368,
+ 0.4773599454873896,
+ 0.5191060075570365
+ ]
+ },
+ "training_history": [
+ [],
+ [],
+ [],
+ [],
+ []
+ ]
+ },
+ "Quadtree-ViTModel_Model_Node": {
+ "train_size": 75,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.3370909439890008,
+ 0.27466543097245066,
+ 0.42716156808953537,
+ 0.3334613599275288,
+ 0.3043479919433594
+ ],
+ "Mask_Cohesion": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Class0": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Class1": [
+ 0.3370909439890008,
+ 0.27466543097245066,
+ 0.42716156808953537,
+ 0.3334613599275288,
+ 0.3043479919433594
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.1685454719945004,
+ 0.13733271548622533,
+ 0.21358078404476769,
+ 0.1667306799637644,
+ 0.1521739959716797
+ ],
+ "IoU_Weighted": [
+ 0.1136303045193957,
+ 0.07544109897128205,
+ 0.18246700525271076,
+ 0.11119647856471691,
+ 0.09262770019995514
+ ],
+ "Dice_Class0": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Class1": [
+ 0.5042154320234089,
+ 0.43096082202983504,
+ 0.5986169718139953,
+ 0.5001440160900528,
+ 0.46666686164005766
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.25210771601170445,
+ 0.21548041101491752,
+ 0.29930848590699766,
+ 0.2500720080450264,
+ 0.23333343082002883
+ ],
+ "Dice_Weighted": [
+ 0.16996645595459278,
+ 0.11837003991506624,
+ 0.25570616436507543,
+ 0.16677870376500484,
+ 0.1420291222466611
+ ],
+ "Precision_Class0": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Class1": [
+ 0.3370909439890008,
+ 0.27466543097245066,
+ 0.42716156808953537,
+ 0.3334613599275288,
+ 0.3043479919433594
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.1685454719945004,
+ 0.13733271548622533,
+ 0.21358078404476769,
+ 0.1667306799637644,
+ 0.1521739959716797
+ ],
+ "Recall_Class0": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Class1": [
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.5,
+ 0.5,
+ 0.5,
+ 0.5,
+ 0.5
+ ],
+ "F1_Macro": [
+ 0.2521077160117044,
+ 0.21548041101491752,
+ 0.29930848590699766,
+ 0.25007200804502633,
+ 0.23333343082002883
+ ]
+ },
+ "training_history": [
+ [],
+ [],
+ [],
+ [],
+ []
+ ]
+ },
+ "SlidingWindow-CNN_W64_S32": {
+ "train_size": 75,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.642491993151213,
+ 0.6606425235145971,
+ 0.5422477722167969,
+ 0.6577234770122328,
+ 0.6889110141330295
+ ],
+ "Mask_Cohesion": [
+ 0.8947368421052632,
+ 0.6842105263157895,
+ 0.9473684210526315,
+ 0.9473684210526315,
+ 1.0
+ ],
+ "IoU_Class0": [
+ 0.6386761647811667,
+ 0.656355536869122,
+ 0.5326029849544591,
+ 0.6530830691113534,
+ 0.6879990647849006
+ ],
+ "IoU_Class1": [
+ 0.02869285498816556,
+ 0.035457795607650236,
+ 0.04543718186986966,
+ 0.03761010990102811,
+ 0.009308264729424564
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.33368450988466614,
+ 0.3459066662383861,
+ 0.2890200834121644,
+ 0.34534658950619074,
+ 0.3486536647571626
+ ],
+ "IoU_Weighted": [
+ 0.4330506729774218,
+ 0.48581375559922213,
+ 0.32448137915048597,
+ 0.44784661913445784,
+ 0.48144088263758444
+ ],
+ "Dice_Class0": [
+ 0.7795025991196465,
+ 0.7925297706430576,
+ 0.6950305985085697,
+ 0.7901394446710179,
+ 0.8151652203344927
+ ],
+ "Dice_Class1": [
+ 0.05578507685560945,
+ 0.06848718655276936,
+ 0.08692474814909622,
+ 0.07249372291604893,
+ 0.018444840005188947
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.41764383798762794,
+ 0.4305084785979135,
+ 0.390977673328833,
+ 0.43131658379353344,
+ 0.41680503016984083
+ ],
+ "Dice_Weighted": [
+ 0.5355370902044357,
+ 0.593657119861335,
+ 0.43524100845375935,
+ 0.550832326348433,
+ 0.5726849724409195
+ ],
+ "Precision_Class0": [
+ 0.6593122838240025,
+ 0.7119888264312331,
+ 0.5626238874000222,
+ 0.6681103109678107,
+ 0.6947325721153846
+ ],
+ "Precision_Class1": [
+ 0.25429300742574257,
+ 0.13917135536123854,
+ 0.3082787298387097,
+ 0.37607817015895956,
+ 0.2322198275862069
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.45680264562487255,
+ 0.42558009089623583,
+ 0.435451308619366,
+ 0.5220942405633852,
+ 0.4634761998507958
+ ],
+ "Recall_Class0": [
+ 0.9532826871722742,
+ 0.8936166502430328,
+ 0.9089381799343204,
+ 0.9667066583972242,
+ 0.9861081408122716
+ ],
+ "Recall_Class1": [
+ 0.03132889924185255,
+ 0.04541912639725855,
+ 0.05059553605295033,
+ 0.04011300013306175,
+ 0.009603828161666298
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.49230579320706336,
+ 0.4695178883201457,
+ 0.47976685799363533,
+ 0.503409829265143,
+ 0.49785598448696894
+ ],
+ "F1_Macro": [
+ 0.4738901891341415,
+ 0.44647059926026783,
+ 0.456536187253961,
+ 0.5125818224130011,
+ 0.48005133609866724
+ ]
+ },
+ "training_history": [
+ [],
+ [],
+ [],
+ [],
+ []
+ ]
+ },
+ "SlidingWindow-CNN_W128_S64": {
+ "train_size": 75,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.5920522589432565,
+ 0.6282748172157689,
+ 0.4906702543559827,
+ 0.6088208650287829,
+ 0.6418122185601128
+ ],
+ "Mask_Cohesion": [
+ 0.5789473684210527,
+ 0.631578947368421,
+ 0.7894736842105263,
+ 0.6842105263157895,
+ 0.8888888888888888
+ ],
+ "IoU_Class0": [
+ 0.6058780303362068,
+ 0.6161690385917653,
+ 0.4700339236202817,
+ 0.5839118228609809,
+ 0.6322133112145234
+ ],
+ "IoU_Class1": [
+ 0.0324840919508189,
+ 0.07855864356535881,
+ 0.0828955486933623,
+ 0.13741428976793135,
+ 0.0902192172643377
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 0.0,
+ 4.6388644059934126e-05,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.31918106114351286,
+ 0.34736384107856205,
+ 0.276464736156822,
+ 0.3606630563144561,
+ 0.36121626423943054
+ ],
+ "IoU_Weighted": [
+ 0.41258677402011135,
+ 0.468503573537782,
+ 0.3046429063296252,
+ 0.4350221482714858,
+ 0.4672584970756239
+ ],
+ "Dice_Class0": [
+ 0.7545754022294708,
+ 0.7625056833518588,
+ 0.6394871792655231,
+ 0.7373034463576077,
+ 0.7746699611757192
+ ],
+ "Dice_Class1": [
+ 0.06292415002625772,
+ 0.1456733837034023,
+ 0.1530998050428507,
+ 0.24162574886582133,
+ 0.1655065620485442
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 0.0,
+ 9.277298450691158e-05,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.4087497761278643,
+ 0.45408953352763054,
+ 0.3962934921541869,
+ 0.4894645976117145,
+ 0.4700882616121317
+ ],
+ "Dice_Weighted": [
+ 0.5214193627670184,
+ 0.5930801121118768,
+ 0.43169345712223833,
+ 0.5720140872662504,
+ 0.5892723038859723
+ ],
+ "Precision_Class0": [
+ 0.6628525711561033,
+ 0.7106022549889814,
+ 0.5428973180736473,
+ 0.6689494837468593,
+ 0.6961974793998756
+ ],
+ "Precision_Class1": [
+ 0.39993024553571427,
+ 0.19823337830219073,
+ 0.27838847358819796,
+ 0.3508048617224178,
+ 0.3076682867005814
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 0.0,
+ 4.650297619047619e-05,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.5313914083459088,
+ 0.4544178166455861,
+ 0.4106428958309226,
+ 0.5098771727346385,
+ 0.5019328830502285
+ ],
+ "Recall_Class0": [
+ 0.8757594122598116,
+ 0.8225887706633302,
+ 0.7778849965702059,
+ 0.8212161803549736,
+ 0.873079892204047
+ ],
+ "Recall_Class1": [
+ 0.03414850017361928,
+ 0.11514389972193714,
+ 0.10558244840732119,
+ 0.18427487065615097,
+ 0.11320080718950153
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.0,
+ 0.018518518518518517,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.4549539562167154,
+ 0.4688663351926337,
+ 0.4417337224887636,
+ 0.5027455255055623,
+ 0.4931403496967743
+ ],
+ "F1_Macro": [
+ 0.4902108981548193,
+ 0.46152902313477007,
+ 0.42562128310499103,
+ 0.5062862359203429,
+ 0.49749777066836764
+ ]
+ },
+ "training_history": [
+ [],
+ [],
+ [],
+ [],
+ []
+ ]
+ },
+ "SlidingWindow-CNN_W128_S128": {
+ "train_size": 75,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.5616866663882607,
+ 0.5006382590846011,
+ 0.5459207233629728,
+ 0.6316893728155839,
+ 0.6604813469780816
+ ],
+ "Mask_Cohesion": [
+ 0.47368421052631576,
+ 0.21052631578947367,
+ 0.3157894736842105,
+ 0.47368421052631576,
+ 0.4444444444444444
+ ],
+ "IoU_Class0": [
+ 0.5634482765876193,
+ 0.47746317912798864,
+ 0.5456667862453384,
+ 0.6121774331368025,
+ 0.6572975614515184
+ ],
+ "IoU_Class1": [
+ 0.0574749220062395,
+ 0.11199724556064318,
+ 0.10284440436914884,
+ 0.13456799124346114,
+ 0.07796036407658162
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 0.0,
+ 0.0001539681663553411,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.3104615992969294,
+ 0.2947302123443159,
+ 0.3242555953072436,
+ 0.3733727121901318,
+ 0.36762896276405
+ ],
+ "IoU_Weighted": [
+ 0.39288426333626897,
+ 0.377080403739508,
+ 0.3564864259173735,
+ 0.45291313912882086,
+ 0.4809774487723627
+ ],
+ "Dice_Class0": [
+ 0.7207763570118239,
+ 0.6463283632012968,
+ 0.7060600526596638,
+ 0.7594417593920703,
+ 0.7932161088511281
+ ],
+ "Dice_Class1": [
+ 0.10870219389637757,
+ 0.2014343938490185,
+ 0.18650755076910072,
+ 0.23721450328592053,
+ 0.14464421267170652
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 0.0,
+ 0.0003078889276170559,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.4147392754541007,
+ 0.42388137852515767,
+ 0.44628380171438226,
+ 0.4983281313389954,
+ 0.46893016076141736
+ ],
+ "Dice_Weighted": [
+ 0.5144453322118383,
+ 0.5241287740594356,
+ 0.4840965848333849,
+ 0.5852991483796918,
+ 0.5958245546180243
+ ],
+ "Precision_Class0": [
+ 0.6485866970486112,
+ 0.6848792061587881,
+ 0.5950497604730561,
+ 0.6779456323431444,
+ 0.7022216796875
+ ],
+ "Precision_Class1": [
+ 0.22840309143066406,
+ 0.19628212668678977,
+ 0.5108284785829741,
+ 0.3982231672420058,
+ 0.42449951171875
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 0.0,
+ 0.00015404110863095238,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.4384948942396376,
+ 0.4405806664227889,
+ 0.5529391195280151,
+ 0.538084399792575,
+ 0.563360595703125
+ ],
+ "Recall_Class0": [
+ 0.8110485075858693,
+ 0.6118861897538669,
+ 0.8679888578137438,
+ 0.8632084010948677,
+ 0.9113035255467026
+ ],
+ "Recall_Class1": [
+ 0.0713232767169696,
+ 0.20686443923990305,
+ 0.11407943018802122,
+ 0.16891797917132031,
+ 0.08717395936477695
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.0,
+ 0.24537037037037038,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.44118589215141946,
+ 0.40937531449688497,
+ 0.4910341440008825,
+ 0.516063190133094,
+ 0.49923874245573974
+ ],
+ "F1_Macro": [
+ 0.4398362772310371,
+ 0.4244051525654757,
+ 0.5201512274830877,
+ 0.5268437827332846,
+ 0.5293649736979148
+ ]
+ },
+ "training_history": [
+ [],
+ [],
+ [],
+ [],
+ []
+ ]
+ },
+ "SlidingWindow-CNN_W256_S128": {
+ "train_size": 75,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.4317227413779811,
+ 0.6299785814787212,
+ 0.49354292217053863,
+ 0.6115808988872328,
+ 0.6051601833767362
+ ],
+ "Mask_Cohesion": [
+ 0.3157894736842105,
+ 0.631578947368421,
+ 0.3157894736842105,
+ 0.5789473684210527,
+ 0.5555555555555556
+ ],
+ "IoU_Class0": [
+ 0.40084373607389845,
+ 0.629043585907441,
+ 0.43889254387189663,
+ 0.5852775352621558,
+ 0.5903834428915102
+ ],
+ "IoU_Class1": [
+ 0.11778023725608183,
+ 0.05720379938141431,
+ 0.1666780362078235,
+ 0.15110673595485552,
+ 0.0980419097209512
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 1.1096192896217308e-05,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.2593119866649901,
+ 0.34312369264442766,
+ 0.3027852900398601,
+ 0.3681921356085056,
+ 0.3442126763062307
+ ],
+ "IoU_Weighted": [
+ 0.3054220529806674,
+ 0.47197643863247735,
+ 0.3225939344312643,
+ 0.4404983500843212,
+ 0.44054028592073574
+ ],
+ "Dice_Class0": [
+ 0.5722890080478652,
+ 0.7722857649103835,
+ 0.6100421407298234,
+ 0.738391256096831,
+ 0.7424416363617586
+ ],
+ "Dice_Class1": [
+ 0.21073952344193853,
+ 0.10821716572506665,
+ 0.2857309918160364,
+ 0.2625416587967594,
+ 0.17857589742793495
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 2.2192139544173455e-05,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.3915142657449019,
+ 0.4402514653177251,
+ 0.4478865662729299,
+ 0.5004664574467952,
+ 0.46050876689484677
+ ],
+ "Dice_Weighted": [
+ 0.45040889536174705,
+ 0.5898859759180459,
+ 0.4714824260617013,
+ 0.5797138022601821,
+ 0.5708302309915908
+ ],
+ "Precision_Class0": [
+ 0.594509838736631,
+ 0.7148868752262307,
+ 0.5475472228167808,
+ 0.6745396205357143,
+ 0.6850320679226002
+ ],
+ "Precision_Class1": [
+ 0.22807242653586648,
+ 0.18699017693014705,
+ 0.3719037844810957,
+ 0.3688997541155134,
+ 0.2594246242357337
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 1.1097301136363637e-05,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.4112911326362487,
+ 0.4509385260781889,
+ 0.45972550364893827,
+ 0.5217196873256138,
+ 0.47222834607916697
+ ],
+ "Recall_Class0": [
+ 0.5516694121962086,
+ 0.8397065336536864,
+ 0.6886409498128423,
+ 0.8155951483393994,
+ 0.810353882926966
+ ],
+ "Recall_Class1": [
+ 0.19585505559688401,
+ 0.0761412711361397,
+ 0.23197963506878005,
+ 0.20378738297640472,
+ 0.1361463803901416
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.1,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.3737622338965463,
+ 0.45792390239491304,
+ 0.4603102924408112,
+ 0.509691265657902,
+ 0.4732501316585538
+ ],
+ "F1_Macro": [
+ 0.3916296625664958,
+ 0.4544043699745606,
+ 0.4600177121945347,
+ 0.5156353381403095,
+ 0.4727386867432463
+ ]
+ },
+ "training_history": [
+ [],
+ [],
+ [],
+ [],
+ []
+ ]
+ },
+ "SlidingWindow-ViT_W64_S32": {
+ "train_size": 75,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.3370909439890008,
+ 0.27466543097245066,
+ 0.42716156808953537,
+ 0.3334613599275288,
+ 0.3043479919433594
+ ],
+ "Mask_Cohesion": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Class0": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Class1": [
+ 0.3370909439890008,
+ 0.27466543097245066,
+ 0.42716156808953537,
+ 0.3334613599275288,
+ 0.3043479919433594
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.1685454719945004,
+ 0.13733271548622533,
+ 0.21358078404476769,
+ 0.1667306799637644,
+ 0.1521739959716797
+ ],
+ "IoU_Weighted": [
+ 0.1136303045193957,
+ 0.07544109897128205,
+ 0.18246700525271076,
+ 0.11119647856471691,
+ 0.09262770019995514
+ ],
+ "Dice_Class0": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Class1": [
+ 0.5042154320234089,
+ 0.43096082202983504,
+ 0.5986169718139953,
+ 0.5001440160900528,
+ 0.46666686164005766
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.25210771601170445,
+ 0.21548041101491752,
+ 0.29930848590699766,
+ 0.2500720080450264,
+ 0.23333343082002883
+ ],
+ "Dice_Weighted": [
+ 0.16996645595459278,
+ 0.11837003991506624,
+ 0.25570616436507543,
+ 0.16677870376500484,
+ 0.1420291222466611
+ ],
+ "Precision_Class0": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Class1": [
+ 0.3370909439890008,
+ 0.27466543097245066,
+ 0.42716156808953537,
+ 0.3334613599275288,
+ 0.3043479919433594
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.1685454719945004,
+ 0.13733271548622533,
+ 0.21358078404476769,
+ 0.1667306799637644,
+ 0.1521739959716797
+ ],
+ "Recall_Class0": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Class1": [
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.5,
+ 0.5,
+ 0.5,
+ 0.5,
+ 0.5
+ ],
+ "F1_Macro": [
+ 0.2521077160117044,
+ 0.21548041101491752,
+ 0.29930848590699766,
+ 0.25007200804502633,
+ 0.23333343082002883
+ ]
+ },
+ "training_history": [
+ [],
+ [],
+ [],
+ [],
+ []
+ ]
+ }
+ },
+ "Combined_2Geo_2Photo_1SEM_x2": {
+ "ViT_Model_Node": {
+ "train_size": 225,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.6432567395662007,
+ 0.5172097055535567,
+ 0.6108910410027755,
+ 0.6791080675627056,
+ 0.681682374742296
+ ],
+ "Mask_Cohesion": [
+ 0.05263157894736842,
+ 0.42105263157894735,
+ 0.15789473684210525,
+ 0.05263157894736842,
+ 0.0
+ ],
+ "IoU_Class0": [
+ 0.6324050660146335,
+ 0.40092254300986196,
+ 0.567786154808903,
+ 0.6557474151084659,
+ 0.6619664138410012
+ ],
+ "IoU_Class1": [
+ 0.07642807205399632,
+ 0.286765800711976,
+ 0.2040404745719549,
+ 0.17455659501328846,
+ 0.15485602554544303
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.35441656903431495,
+ 0.343844171860919,
+ 0.385913314690429,
+ 0.4151520050608772,
+ 0.40841121969322214
+ ],
+ "IoU_Weighted": [
+ 0.4449846695952991,
+ 0.3695660222954353,
+ 0.41238335642282425,
+ 0.49528886985488524,
+ 0.5076283854696307
+ ],
+ "Dice_Class0": [
+ 0.7748138978257304,
+ 0.5723693219305125,
+ 0.7243158170102737,
+ 0.7920862918158429,
+ 0.7966062470674343
+ ],
+ "Dice_Class1": [
+ 0.1420031194618687,
+ 0.44571560816009653,
+ 0.3389262718007761,
+ 0.29722977292773806,
+ 0.26818239177875686
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.45840850864379956,
+ 0.5090424650453045,
+ 0.5316210444055249,
+ 0.5446580323717904,
+ 0.5323943194230956
+ ],
+ "Dice_Weighted": [
+ 0.561492270446951,
+ 0.5375796267211973,
+ 0.5596608030878831,
+ 0.6270707640584126,
+ 0.635781507815357
+ ],
+ "Precision_Class0": [
+ 0.6661521560956646,
+ 0.8004342225029719,
+ 0.6095196483550158,
+ 0.6970953881802872,
+ 0.7170136269220259
+ ],
+ "Precision_Class1": [
+ 0.3751763802311251,
+ 0.32550165878516457,
+ 0.618017506929619,
+ 0.5510335506569434,
+ 0.44652589053208835
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.5206642681633948,
+ 0.5629679406440682,
+ 0.6137685776423174,
+ 0.6240644694186153,
+ 0.5817697587270572
+ ],
+ "Recall_Class0": [
+ 0.9258346606673893,
+ 0.4454490848898878,
+ 0.8923868387043405,
+ 0.9170496404509477,
+ 0.8960757934962946
+ ],
+ "Recall_Class1": [
+ 0.08757499429706825,
+ 0.7067211681563935,
+ 0.23348604211641494,
+ 0.20349898216791912,
+ 0.19164065861983964
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.5067048274822288,
+ 0.5760851265231407,
+ 0.5629364404103777,
+ 0.5602743113094334,
+ 0.5438582260580671
+ ],
+ "F1_Macro": [
+ 0.5135897104440368,
+ 0.5694510056866409,
+ 0.587254567683392,
+ 0.5904514763947236,
+ 0.5621755557470899
+ ]
+ },
+ "training_history": [
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9898573371161402,
+ "val_loss": 0.8689083206026178
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8155820390819448,
+ "val_loss": 0.7929521673604062
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7587245449555658,
+ "val_loss": 0.7496916118421053
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7237163486733901,
+ "val_loss": 0.7548214855946993
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7057559099872556,
+ "val_loss": 0.7009812891483307
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6844242558015131,
+ "val_loss": 0.6780218795726174
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6700793304274567,
+ "val_loss": 0.6922261965902228
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6613201356567113,
+ "val_loss": 0.6959347505318491
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6483481515825322,
+ "val_loss": 0.7215635086360731
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6513601283056546,
+ "val_loss": 0.6507437448752554
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6424833768236954,
+ "val_loss": 0.6370006423247488
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6356108679180652,
+ "val_loss": 0.8359036194650751
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6263740286890385,
+ "val_loss": 0.6134153933901536
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6207337508686875,
+ "val_loss": 0.6180830268483413
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6204234841650567,
+ "val_loss": 0.637090940224497
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.618411745909041,
+ "val_loss": 0.6053110125817751
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6068670805049153,
+ "val_loss": 0.7278545781185752
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6031130670973684,
+ "val_loss": 0.6525981551722476
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.593662966405396,
+ "val_loss": 0.624473008670305
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.602713734438989,
+ "val_loss": 0.6361495758357801
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.5893141920060183,
+ "val_loss": 0.8955859024273721
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.5839639524970435,
+ "val_loss": 0.669963422574495
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.5756886424744023,
+ "val_loss": 0.6340874543315486
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.5803286364648195,
+ "val_loss": 0.6838885090853039
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.5754055786976772,
+ "val_loss": 0.6324227640503332
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.5703070852081332,
+ "val_loss": 0.6556823065406397
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.556472235021338,
+ "val_loss": 0.6511829495429993
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.5562533142819869,
+ "val_loss": 0.6252193231331674
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.5452961077732322,
+ "val_loss": 0.6734161722032648
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.5498282432028677,
+ "val_loss": 0.6116960032990104
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.5430116249924213,
+ "val_loss": 0.6434394246653506
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.5363451577393354,
+ "val_loss": 0.6276003702690727
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5379498933268859,
+ "val_loss": 0.6100856787280032
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5555335939985461,
+ "val_loss": 0.6506898591392919
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.5403297614734784,
+ "val_loss": 0.6050962978287747
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.5277836546961185,
+ "val_loss": 0.6228457187351427
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5128038844703573,
+ "val_loss": 0.6382067313319758
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.5242834077999655,
+ "val_loss": 0.6498304809394636
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.5149969922757782,
+ "val_loss": 0.6390655307393325
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.5237766005824097,
+ "val_loss": 0.646233574340218
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 1.0042165834291847,
+ "val_loss": 0.8617734407123766
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8266047787877311,
+ "val_loss": 0.7783635227303756
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7737062884643015,
+ "val_loss": 0.7116745804485521
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7377690883864344,
+ "val_loss": 0.6988644066609835
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7150197171531947,
+ "val_loss": 0.6569454168018541
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7021279187329048,
+ "val_loss": 0.6685379586721721
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6799984310580566,
+ "val_loss": 0.7042303336294073
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6750772236722761,
+ "val_loss": 0.6234975488562333
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6652422906023211,
+ "val_loss": 0.6175764642263714
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6549533051727092,
+ "val_loss": 0.6550996397670946
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6476726917038976,
+ "val_loss": 0.6780757684456674
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6474997079477901,
+ "val_loss": 0.5894608717215689
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6409485129128515,
+ "val_loss": 0.6541419562540556
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6320962845224195,
+ "val_loss": 0.5994623805347242
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6263034691852806,
+ "val_loss": 0.664198637008667
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6358944430815435,
+ "val_loss": 0.5850082275114561
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6097421783261594,
+ "val_loss": 0.5872095980142292
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6248922334835593,
+ "val_loss": 0.5888607972546628
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6082582035950855,
+ "val_loss": 0.7391277143829748
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6256463778229941,
+ "val_loss": 0.5824940926150272
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6025498633363605,
+ "val_loss": 0.6058530164392371
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.5998576359938731,
+ "val_loss": 0.5735286129148383
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.5929031862621814,
+ "val_loss": 0.5860380812695152
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6086567416127804,
+ "val_loss": 0.5856931476216567
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.5801661679175047,
+ "val_loss": 0.6432824385793585
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.5971277619885133,
+ "val_loss": 0.6799602900680742
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.5888855539064491,
+ "val_loss": 0.5730852343534169
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.5825377937439269,
+ "val_loss": 0.5888201312014931
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6105836076019084,
+ "val_loss": 0.5845902201376463
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.5846190384003969,
+ "val_loss": 0.5630396259458441
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.5638218384928408,
+ "val_loss": 0.5945125347689578
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.5932448290090645,
+ "val_loss": 0.5916415813722109
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5795999778582986,
+ "val_loss": 0.6345100685169822
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5977398484154085,
+ "val_loss": 0.8061129889990154
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.5734677923991617,
+ "val_loss": 0.5908670519527636
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.564038189375295,
+ "val_loss": 0.5596642400089064
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5574405135306637,
+ "val_loss": 0.5864608130956951
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.5680923039934277,
+ "val_loss": 0.5734715351932927
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.5577594107758682,
+ "val_loss": 0.6324390878802851
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.553518339332226,
+ "val_loss": 0.6926865922777277
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9860529182231532,
+ "val_loss": 0.9027635831581918
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7961775022270405,
+ "val_loss": 0.8392008919464914
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7402544306442801,
+ "val_loss": 0.8187226025681746
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7037957917272517,
+ "val_loss": 0.899972633311623
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6829387556135127,
+ "val_loss": 0.7574285676604823
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6643158234326185,
+ "val_loss": 0.7868359810427615
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6484823284950931,
+ "val_loss": 0.7285532700388055
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6466627015476734,
+ "val_loss": 0.7340162490543566
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6316709238871009,
+ "val_loss": 0.717814006303486
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6267651546317919,
+ "val_loss": 0.9145081984369379
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6200182519655312,
+ "val_loss": 0.7174008331800762
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6068827873837631,
+ "val_loss": 0.8287083051706615
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.612476782988658,
+ "val_loss": 0.7328253404090279
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.5975505707010759,
+ "val_loss": 0.7671152792478863
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.5945443300019323,
+ "val_loss": 0.7067312472744992
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.5867311200209423,
+ "val_loss": 0.8246906211501673
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.588716524364674,
+ "val_loss": 0.7643875558125345
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.5805835112006263,
+ "val_loss": 0.7697540976499256
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.572863055541452,
+ "val_loss": 0.824408609616129
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.5799223184585571,
+ "val_loss": 0.7036225576149789
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.5638312098726762,
+ "val_loss": 0.7170937829896024
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.5567135130409646,
+ "val_loss": 0.6845310534301557
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.557458597738131,
+ "val_loss": 0.6817049744882082
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.5506007075309753,
+ "val_loss": 0.7437904696715506
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.5542478487554905,
+ "val_loss": 0.7092581234480205
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.5536808062971166,
+ "val_loss": 0.7049466747986642
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.5393244009629815,
+ "val_loss": 0.6881239633811148
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.5349703871570857,
+ "val_loss": 0.6859818477379648
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.5426500982942835,
+ "val_loss": 0.7949136872040597
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.5317743837305929,
+ "val_loss": 0.6764549111065111
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.530120732246247,
+ "val_loss": 0.7290466192521547
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.5148963893936799,
+ "val_loss": 0.7216195181796425
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5314690755531851,
+ "val_loss": 0.6859562585228368
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5132561500093579,
+ "val_loss": 0.717886336539921
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.5055508104573309,
+ "val_loss": 0.7663937305149279
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.5290123983822038,
+ "val_loss": 0.725091184440412
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5122296665094596,
+ "val_loss": 0.7413287570602015
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.5038723850672224,
+ "val_loss": 0.7248723428500327
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.5008644691083284,
+ "val_loss": 0.7693093732783669
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.49011215884073644,
+ "val_loss": 0.7053062225642958
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9955803113701069,
+ "val_loss": 0.8693134816069352
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8310558548015831,
+ "val_loss": 0.8520765053598505
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.770533796432799,
+ "val_loss": 0.7600672809701217
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7357660262985567,
+ "val_loss": 0.7418035049187509
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7124104990368396,
+ "val_loss": 0.7249697070372733
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6950212057712859,
+ "val_loss": 0.6912133411357277
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6815976015234415,
+ "val_loss": 0.7085681996847454
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6733924356182065,
+ "val_loss": 0.6675266215675756
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6641090791837304,
+ "val_loss": 0.6782301256531164
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6589648080083121,
+ "val_loss": 0.670288788644891
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6528101157298131,
+ "val_loss": 0.6759517035986248
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6518054688926291,
+ "val_loss": 0.6781880714391407
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6463949590657665,
+ "val_loss": 0.6472389227465579
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.641135309122305,
+ "val_loss": 0.6856066333620172
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6417638754422685,
+ "val_loss": 0.6479337780099166
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6383086986246362,
+ "val_loss": 0.6459665172978452
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6331262119048464,
+ "val_loss": 0.6515120001215684
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6352383193716539,
+ "val_loss": 0.6450152397155762
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6322268163208413,
+ "val_loss": 0.6592219832696413
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6339474112059162,
+ "val_loss": 0.6410245989498339
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6317799850902727,
+ "val_loss": 0.6360609547088021
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.629755487747952,
+ "val_loss": 0.6480725642881895
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6271400343527836,
+ "val_loss": 0.6353748911305478
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6234279665271792,
+ "val_loss": 0.6337223492170635
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6249458402131511,
+ "val_loss": 0.621446502836127
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6224995437449059,
+ "val_loss": 0.6336369420352735
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.620705948203011,
+ "val_loss": 0.6282809455143777
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6172958050681426,
+ "val_loss": 0.6505007900689778
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6170645440574241,
+ "val_loss": 0.6272197836323788
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6111376965995383,
+ "val_loss": 0.621631804265474
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6127264594609758,
+ "val_loss": 0.6349038262116281
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6071668942417718,
+ "val_loss": 0.6451740296263444
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5985316766046844,
+ "val_loss": 0.6309166832974082
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5901975270393676,
+ "val_loss": 0.7196063901248732
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.5834152113019893,
+ "val_loss": 0.621304706523293
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.5799112077307912,
+ "val_loss": 0.6598337104446009
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5822775060624148,
+ "val_loss": 0.6617270629656943
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.5762227143334077,
+ "val_loss": 0.6363502003644642
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.5638315105332737,
+ "val_loss": 0.6038871721217507
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.5499287126338588,
+ "val_loss": 0.604794370500665
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 1.0005688228105243,
+ "val_loss": 0.8121522400114272
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8249726766034177,
+ "val_loss": 0.7560919423898061
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7701945697006426,
+ "val_loss": 0.7334253357516395
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7382683978791822,
+ "val_loss": 0.7065825959046682
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7117546415119841,
+ "val_loss": 0.6734269758065542
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6965853069957934,
+ "val_loss": 0.6993787719143761
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6840906362784537,
+ "val_loss": 0.6628869738843706
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.675183362605279,
+ "val_loss": 0.6442831522888608
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6689543927970686,
+ "val_loss": 0.631631099515491
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6629231279356438,
+ "val_loss": 0.6585261821746826
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6536392742081693,
+ "val_loss": 0.7017633848720126
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6578818491676397,
+ "val_loss": 0.6401328146457672
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6481374706092634,
+ "val_loss": 0.6275361643897163
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6448307194207844,
+ "val_loss": 0.6258047918478647
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6446658848670491,
+ "val_loss": 0.6192187401983473
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6407559582015925,
+ "val_loss": 0.6201504204008315
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.637379853348983,
+ "val_loss": 0.6014381680223677
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6346908556787592,
+ "val_loss": 0.6105251047346327
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6330901352982772,
+ "val_loss": 0.6618307828903198
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6315987486588327,
+ "val_loss": 0.6385189526610904
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6270240249863842,
+ "val_loss": 0.6634482774469588
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6257276553333851,
+ "val_loss": 0.6067936685350206
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6247553017578626,
+ "val_loss": 0.5943822579251395
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6207441732025983,
+ "val_loss": 0.6303022007147471
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6228921734973004,
+ "val_loss": 0.6060515542825063
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6151347013941982,
+ "val_loss": 0.6385904285642836
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.613246995106078,
+ "val_loss": 0.5992964969740974
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6059517520561553,
+ "val_loss": 0.6485766934023963
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6065919924723474,
+ "val_loss": 0.6510884596241845
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6039057551768788,
+ "val_loss": 0.6664124876260757
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6028805837819451,
+ "val_loss": 0.5936519917514589
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.5936941580291379,
+ "val_loss": 0.6132696916659673
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5857213142148235,
+ "val_loss": 0.623461170328988
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.575215931785734,
+ "val_loss": 0.5965351445807351
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.56747345208076,
+ "val_loss": 0.711194869544771
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.5792720960943323,
+ "val_loss": 0.6160126593377855
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5523677915334702,
+ "val_loss": 0.8367289751768112
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.5575560950919202,
+ "val_loss": 0.6353913495937983
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.5327311686256475,
+ "val_loss": 0.6087081730365753
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.5421879511130484,
+ "val_loss": 0.6179304967323939
+ }
+ ]
+ ]
+ },
+ "Swin_Model_Node": {
+ "train_size": 225,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.7992895025956003,
+ 0.7903938293457031,
+ 0.7376961959035773,
+ 0.7939280459755346,
+ 0.7643445332845052
+ ],
+ "Mask_Cohesion": [
+ 0.47368421052631576,
+ 0.5263157894736842,
+ 0.3684210526315789,
+ 0.5789473684210527,
+ 0.4444444444444444
+ ],
+ "IoU_Class0": [
+ 0.7402404306381722,
+ 0.7446107156014652,
+ 0.6703692517391981,
+ 0.7479837150322156,
+ 0.6979137405588026
+ ],
+ "IoU_Class1": [
+ 0.5311043880015948,
+ 0.4609992900935786,
+ 0.43784923803978726,
+ 0.46940493059134736,
+ 0.48271502019899404
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.6356724093198836,
+ 0.6028050028475219,
+ 0.5541092448894926,
+ 0.6086943228117815,
+ 0.5903143803788984
+ ],
+ "IoU_Weighted": [
+ 0.6697360252932395,
+ 0.6667094712230401,
+ 0.5710165661150345,
+ 0.6550884547256058,
+ 0.6324184421485143
+ ],
+ "Dice_Class0": [
+ 0.8507335165942731,
+ 0.8536124522710571,
+ 0.802659952033008,
+ 0.8558245807437973,
+ 0.8220838596065679
+ ],
+ "Dice_Class1": [
+ 0.693753335387922,
+ 0.6310739412666669,
+ 0.6090335849629199,
+ 0.6389047985601083,
+ 0.651123126997404
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.7722434259910975,
+ 0.7423431967688621,
+ 0.7058467684979639,
+ 0.7473646896519528,
+ 0.736603493301986
+ ],
+ "Dice_Weighted": [
+ 0.7978094037135408,
+ 0.7924853885821872,
+ 0.7199154004297409,
+ 0.783490215181641,
+ 0.7700523039358033
+ ],
+ "Precision_Class0": [
+ 0.8389917239466005,
+ 0.8649746703537,
+ 0.7052753523467307,
+ 0.8018363421819902,
+ 0.8657273109606177
+ ],
+ "Precision_Class1": [
+ 0.7142524618725938,
+ 0.6108472317523603,
+ 0.8383219027970484,
+ 0.7684995933318268,
+ 0.5925473740954609
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.7766220929095972,
+ 0.7379109510530302,
+ 0.7717986275718896,
+ 0.7851679677569086,
+ 0.7291373425280393
+ ],
+ "Recall_Class0": [
+ 0.8628086297763203,
+ 0.8425448697365945,
+ 0.9312467380152621,
+ 0.917607797694657,
+ 0.7826295705282988
+ ],
+ "Recall_Class1": [
+ 0.6743980354516871,
+ 0.6526860404258368,
+ 0.47823277067502545,
+ 0.5467109965000545,
+ 0.7225501951822095
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.7686033326140037,
+ 0.7476154550812156,
+ 0.7047397543451438,
+ 0.7321593970973557,
+ 0.7525898828552542
+ ],
+ "F1_Macro": [
+ 0.7725919065688606,
+ 0.7427315047418543,
+ 0.7367464088441086,
+ 0.7577377423075328,
+ 0.7406780111725624
+ ]
+ },
+ "training_history": [
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9716138196202506,
+ "val_loss": 0.8588298025884127
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8364253750944559,
+ "val_loss": 0.8223963160263864
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7876012905509071,
+ "val_loss": 0.7684043206666645
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7480216084328373,
+ "val_loss": 0.7294533440941259
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7175063179657523,
+ "val_loss": 0.7182954236080772
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6971873298155523,
+ "val_loss": 0.6873180160396978
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6627266765695757,
+ "val_loss": 0.6231804223437059
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6126381818172151,
+ "val_loss": 0.6296709619070354
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.5802568147140267,
+ "val_loss": 0.5948262277402376
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.5754222503278108,
+ "val_loss": 0.6942547026433443
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.5498782306645824,
+ "val_loss": 0.5942542474520834
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.5479124752821121,
+ "val_loss": 0.6707736834099418
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.5280920631062668,
+ "val_loss": 0.5681701882889396
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.5249958204484619,
+ "val_loss": 0.5021126536946547
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.5164321829787398,
+ "val_loss": 0.5146463968251881
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.5089192920554001,
+ "val_loss": 0.7012657978032765
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.5128818621150161,
+ "val_loss": 0.5124256626555794
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.4991810131916958,
+ "val_loss": 0.560285243548845
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.495007043127465,
+ "val_loss": 0.5930489223254355
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.48003834011280433,
+ "val_loss": 0.5554533616492623
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.4929470913072603,
+ "val_loss": 0.47950621027695506
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.4810658830457029,
+ "val_loss": 0.5441757471937883
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.4822757990486854,
+ "val_loss": 0.4584117798428786
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.4545072208991093,
+ "val_loss": 0.5153026643552279
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.4541850235082407,
+ "val_loss": 0.46250121687587936
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.45369112728971295,
+ "val_loss": 0.4782902927775132
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.4440866990954475,
+ "val_loss": 0.4438900382895219
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.4332720123033608,
+ "val_loss": 0.47621945174116836
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.43891285135682706,
+ "val_loss": 0.4507394354594381
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.4082100119738452,
+ "val_loss": 0.5399153671766582
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.4118593377349651,
+ "val_loss": 0.49895090805856807
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.4306720748411871,
+ "val_loss": 0.4542464017868042
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.41271445102396265,
+ "val_loss": 0.45711837944231537
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.3858951720516239,
+ "val_loss": 0.4213511347770691
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.36662506002240475,
+ "val_loss": 0.437865779588097
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.37287636299048904,
+ "val_loss": 0.44489591843203496
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.3648972793490486,
+ "val_loss": 0.5212159595991436
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.3492495186824714,
+ "val_loss": 0.47438530702339976
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.3596066210122235,
+ "val_loss": 0.41498816797607824
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.34727604713587634,
+ "val_loss": 0.45284974261334066
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9886011449636611,
+ "val_loss": 0.8512145531804938
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8554047847216109,
+ "val_loss": 0.7839001354418302
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7956558191670781,
+ "val_loss": 0.727433254844264
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7605555548077136,
+ "val_loss": 0.7156010426973042
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7345491964205176,
+ "val_loss": 0.6793244324232403
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.7070732971208286,
+ "val_loss": 0.6380229937402826
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6660233216475596,
+ "val_loss": 0.5884923040866852
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6262603259719579,
+ "val_loss": 0.5720360310454118
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6120255652782136,
+ "val_loss": 0.5364700555801392
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6037056343217867,
+ "val_loss": 0.5381636352915513
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.5750106259257393,
+ "val_loss": 0.593742875676406
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.5793190189694937,
+ "val_loss": 0.575912102272636
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.5515089322507909,
+ "val_loss": 0.494344330147693
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.5417607405544382,
+ "val_loss": 0.5984317660331726
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.5463890304080153,
+ "val_loss": 0.569346198910161
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.5265186336715665,
+ "val_loss": 0.5876201642187018
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.5167229708317107,
+ "val_loss": 0.4918832669132634
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.5169342038905727,
+ "val_loss": 0.4851657042377873
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.5130128103547391,
+ "val_loss": 0.5040111761344107
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.49950981324752874,
+ "val_loss": 0.4986976589027204
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.49582748708471786,
+ "val_loss": 0.5349500147919906
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.488071917168862,
+ "val_loss": 0.5804602692001745
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.4871547541259664,
+ "val_loss": 0.5332688846086201
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.475525839138875,
+ "val_loss": 0.4693712403899745
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.46380332217807263,
+ "val_loss": 0.4433683737328178
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.46334323867232396,
+ "val_loss": 0.45844537647146927
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.45497169536826887,
+ "val_loss": 0.4516048368654753
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.4246139950984347,
+ "val_loss": 0.4794657073522869
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.45017459381998115,
+ "val_loss": 0.4796380573197415
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.42633473530279853,
+ "val_loss": 0.42405876831004496
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.4023057884347122,
+ "val_loss": 0.4234307110309601
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.41181665658950806,
+ "val_loss": 0.44583936427768905
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.4315186978968899,
+ "val_loss": 0.42643318050786067
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.39617097799756884,
+ "val_loss": 0.43643227219581604
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.38614624448582135,
+ "val_loss": 0.4457900147689016
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.3878336427486048,
+ "val_loss": 0.49620030114525243
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.3649086292866057,
+ "val_loss": 0.4445839618381701
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.3557363773605465,
+ "val_loss": 0.43548469794423955
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.3466040182166395,
+ "val_loss": 0.42282684225785105
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.34973764788787975,
+ "val_loss": 0.45149926292268855
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9572359518667238,
+ "val_loss": 0.951455997793298
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.822665581660988,
+ "val_loss": 0.8783967212626809
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7616471838107152,
+ "val_loss": 0.8395015001296997
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7220927313365767,
+ "val_loss": 0.8240265375689456
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6934718057117631,
+ "val_loss": 0.8772814242463363
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6511139901338425,
+ "val_loss": 0.7378468372319874
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6014812019019,
+ "val_loss": 0.7566645051303663
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.5804481239972916,
+ "val_loss": 0.6666704071195502
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.5573672852684967,
+ "val_loss": 0.6383637048696217
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.5435559045424504,
+ "val_loss": 0.679127823365362
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.5250109726876284,
+ "val_loss": 0.6399506801053098
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.5067790079433306,
+ "val_loss": 0.629631348346409
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.4979988821839864,
+ "val_loss": 0.6718601709917972
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.48656251710073084,
+ "val_loss": 0.6323165987667284
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.4770124744524998,
+ "val_loss": 0.5585321093860426
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.4762143294895645,
+ "val_loss": 0.6783899994272935
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.4648701150332932,
+ "val_loss": 0.705034928886514
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.46007730159084353,
+ "val_loss": 0.7433914793165106
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.44765518381532315,
+ "val_loss": 0.8764407305341018
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.44609654661828435,
+ "val_loss": 0.5402246274446186
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.4391410867724798,
+ "val_loss": 0.6116097349869577
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.4318510133080778,
+ "val_loss": 0.6479424504857314
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.4179813624483294,
+ "val_loss": 0.604504428411785
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.40250697257244483,
+ "val_loss": 0.7150331390531439
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.39866011965591297,
+ "val_loss": 0.6704497086374384
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.38697123013238993,
+ "val_loss": 0.5903805368825009
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.3863248590346986,
+ "val_loss": 0.776805601621929
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.36804055323642965,
+ "val_loss": 0.6599003453003732
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.35863351966954965,
+ "val_loss": 0.7850468566543177
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.35467658277633973,
+ "val_loss": 0.5771508640364597
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.3439358664033687,
+ "val_loss": 0.5742327194464835
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.3257877192402308,
+ "val_loss": 0.6167917706464466
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.31119988805952326,
+ "val_loss": 0.5598987184072796
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.3007839315496715,
+ "val_loss": 0.5803533870922891
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.30822288673535914,
+ "val_loss": 0.8975398885576349
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.30497910266956396,
+ "val_loss": 0.6382771024578496
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.28825459688638166,
+ "val_loss": 0.545682734564731
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.2847789768624095,
+ "val_loss": 0.5721073699624915
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.26732183597256653,
+ "val_loss": 0.6054201016300603
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.2586314459290125,
+ "val_loss": 0.6153160866938139
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9710572799750133,
+ "val_loss": 0.8775770162281237
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8342223953356785,
+ "val_loss": 0.821470348458541
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7828372015362293,
+ "val_loss": 0.7651990118779635
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7467971544350143,
+ "val_loss": 0.7212135038877788
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6952101345610829,
+ "val_loss": 0.8002170794888547
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6523675839457892,
+ "val_loss": 0.6441817816935087
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6253999268586656,
+ "val_loss": 0.5844425166908064
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.5935802755102647,
+ "val_loss": 0.5826960629538486
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.573703223888853,
+ "val_loss": 0.7160112857818604
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.5470835569158065,
+ "val_loss": 0.5235997595285115
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.5389004605533803,
+ "val_loss": 0.5907730099401975
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.5226831560113788,
+ "val_loss": 0.5354063024646357
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.5188983425629877,
+ "val_loss": 0.517902429166593
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.49369989137733933,
+ "val_loss": 0.7197216325684598
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.48988938621715106,
+ "val_loss": 0.4982081604631324
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.48042274655494016,
+ "val_loss": 0.6547346413135529
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.48494385987256483,
+ "val_loss": 0.5403929839008733
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.45582934185466933,
+ "val_loss": 0.4891854963804546
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.4516222806630936,
+ "val_loss": 0.4883668218788348
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.41951758091428637,
+ "val_loss": 0.7451265783686387
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.4345985318707154,
+ "val_loss": 0.4793396262746108
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.4189719766114665,
+ "val_loss": 0.4868791087677604
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.39772115186252427,
+ "val_loss": 0.7126219696120212
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.39230711955939773,
+ "val_loss": 0.4683062638107099
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.3831119890761586,
+ "val_loss": 0.4871789107197209
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.35436364639121876,
+ "val_loss": 0.47931113525440816
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.3549362713784243,
+ "val_loss": 0.5012850086939963
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.33729012758858434,
+ "val_loss": 0.46778565802072225
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.34288908857687384,
+ "val_loss": 0.47879356773276077
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.3198693257517519,
+ "val_loss": 0.48362781028998525
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.3038052014545002,
+ "val_loss": 0.4659254143112584
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.3465434555458812,
+ "val_loss": 0.4944220044110951
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.35656417893097464,
+ "val_loss": 0.5071260552657278
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.309490061043638,
+ "val_loss": 0.46119848834840876
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.3016992795520124,
+ "val_loss": 0.46341467844812495
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.2905738839006002,
+ "val_loss": 0.4724361567120803
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.27714590833777875,
+ "val_loss": 0.4722750202605599
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.2678133923131808,
+ "val_loss": 0.49022735576880605
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.2928158114705465,
+ "val_loss": 0.4923112972786552
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.2840979548945891,
+ "val_loss": 0.47111711219737407
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.969155969849804,
+ "val_loss": 0.9002740416261885
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.8360911372460818,
+ "val_loss": 0.76304163535436
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7806839707650637,
+ "val_loss": 0.7462697062227461
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7503750763441387,
+ "val_loss": 0.69701020916303
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7147499427460787,
+ "val_loss": 0.6482347117529975
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6683825934142397,
+ "val_loss": 0.5813277910153071
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6311252671375609,
+ "val_loss": 0.571570399734709
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.5987843688119922,
+ "val_loss": 0.6612679925229814
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.5827995371400264,
+ "val_loss": 0.6728556752204895
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.5655528686025686,
+ "val_loss": 0.6139016896486282
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.5462034755108649,
+ "val_loss": 0.6377863321039412
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.5350086749122854,
+ "val_loss": 0.5841870456933975
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.5235141659514946,
+ "val_loss": 0.6262599759631686
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.519210468258774,
+ "val_loss": 0.5122752835353216
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.5096380556361717,
+ "val_loss": 0.6960780554347568
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.47927808813881456,
+ "val_loss": 0.5187944836086698
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.49243303793564175,
+ "val_loss": 0.4997238881058163
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.4757634453606187,
+ "val_loss": 0.5088594837321175
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.46353057601995634,
+ "val_loss": 0.540263929300838
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.44849251864249245,
+ "val_loss": 0.5703937162955602
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.4467107219654217,
+ "val_loss": 0.5010631779829661
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.43841050277676497,
+ "val_loss": 0.5642122013701333
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.42266871767085895,
+ "val_loss": 0.644175272848871
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.4189729298415937,
+ "val_loss": 0.6396240227752261
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.41585100899662886,
+ "val_loss": 0.5679509623183144
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.3896352651349285,
+ "val_loss": 0.5714967333608203
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.37841991165228056,
+ "val_loss": 0.5916205081674788
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.3689094745276267,
+ "val_loss": 0.6227213483717706
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.3655386870367485,
+ "val_loss": 0.5072466507554054
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.3508370546133895,
+ "val_loss": 0.686790312329928
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.348488358029148,
+ "val_loss": 0.6114309256275495
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.3471118498005365,
+ "val_loss": 0.5612634403838052
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.35548221881975206,
+ "val_loss": 0.5528799692789713
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.33525580167770386,
+ "val_loss": 0.7419857838087611
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.3274126401857326,
+ "val_loss": 0.52136220700211
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.3189076740752187,
+ "val_loss": 0.5907954176266988
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.30323652661683265,
+ "val_loss": 0.5095768554343117
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.3005010190240124,
+ "val_loss": 0.5103714987635612
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.3001854040643625,
+ "val_loss": 0.5289322833220164
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.3051640417492181,
+ "val_loss": 0.6119384070237478
+ }
+ ]
+ ]
+ },
+ "ViT_Big_Model_Node": {
+ "train_size": 225,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.6629002219752261,
+ 0.7253305535567435,
+ 0.5727950648257607,
+ 0.6665386400724712,
+ 0.6956520080566406
+ ],
+ "Mask_Cohesion": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Class0": [
+ 0.6629002219752261,
+ 0.7253305535567435,
+ 0.5727950648257607,
+ 0.6665386400724712,
+ 0.6956520080566406
+ ],
+ "IoU_Class1": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.33145011098761307,
+ 0.36266527677837174,
+ 0.28639753241288035,
+ 0.3332693200362356,
+ 0.3478260040283203
+ ],
+ "IoU_Weighted": [
+ 0.43943670429480414,
+ 0.5261044119229319,
+ 0.32809418628874737,
+ 0.44427375870965935,
+ 0.4839317163132364
+ ],
+ "Dice_Class0": [
+ 0.7972820175438067,
+ 0.8408018417821271,
+ 0.728378512415051,
+ 0.7999078137708059,
+ 0.8205127051439242
+ ],
+ "Dice_Class1": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.39864100877190334,
+ 0.42040092089106357,
+ 0.3641892562075255,
+ 0.39995390688540294,
+ 0.4102563525719621
+ ],
+ "Dice_Weighted": [
+ 0.5285184264066456,
+ 0.6098592653313597,
+ 0.4172116172364703,
+ 0.5331694663741365,
+ 0.5707913109693572
+ ],
+ "Precision_Class0": [
+ 0.6629002219752261,
+ 0.7253305535567435,
+ 0.5727950648257607,
+ 0.6665386400724712,
+ 0.6956520080566406
+ ],
+ "Precision_Class1": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.33145011098761307,
+ 0.36266527677837174,
+ 0.28639753241288035,
+ 0.3332693200362356,
+ 0.3478260040283203
+ ],
+ "Recall_Class0": [
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0,
+ 1.0
+ ],
+ "Recall_Class1": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.5,
+ 0.5,
+ 0.5,
+ 0.5,
+ 0.5
+ ],
+ "F1_Macro": [
+ 0.39864100877190334,
+ 0.42040092089106357,
+ 0.3641892562075255,
+ 0.399953906885403,
+ 0.4102563525719621
+ ]
+ },
+ "training_history": [
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8676982796297664,
+ "val_loss": 0.7469436306702463
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7704015785613946,
+ "val_loss": 0.7614749293578299
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7476275125436024,
+ "val_loss": 0.7800023618497347
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7156064763533331,
+ "val_loss": 0.7146936341335899
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.693430357274756,
+ "val_loss": 0.6924083828926086
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6802789969781858,
+ "val_loss": 0.666471613080878
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6730452148260269,
+ "val_loss": 0.6762628021993136
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6649583309097628,
+ "val_loss": 0.6709275684858623
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6614227500637021,
+ "val_loss": 0.6638802760525754
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6562312387787135,
+ "val_loss": 0.6617889090588218
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6528452698108369,
+ "val_loss": 0.6600164269146166
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6502086441073798,
+ "val_loss": 0.6586770446676957
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.64856843452538,
+ "val_loss": 0.647516385505074
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.648128855544909,
+ "val_loss": 0.653602869887101
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6450196056239372,
+ "val_loss": 0.6502970519818758
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6446103369240213,
+ "val_loss": 0.6472077495173404
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.64278941133381,
+ "val_loss": 0.6473628376659594
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6414842526469611,
+ "val_loss": 0.6479648822232297
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6409415892795124,
+ "val_loss": 0.6467782258987427
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6421778107111433,
+ "val_loss": 0.6440167678029913
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6412233166990027,
+ "val_loss": 0.6445246307473433
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6390619818615703,
+ "val_loss": 0.6433938804425692
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6384914522677396,
+ "val_loss": 0.6425078385754636
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6376325488090515,
+ "val_loss": 0.6416743680050498
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6370946606703564,
+ "val_loss": 0.6410693589009737
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.636879977399269,
+ "val_loss": 0.642736481992822
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6368435051588885,
+ "val_loss": 0.6407470671754134
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.636131309829982,
+ "val_loss": 0.6400537365361264
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6371570815027288,
+ "val_loss": 0.6398518932493109
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.638337591580585,
+ "val_loss": 0.6406954401417783
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.636097621073765,
+ "val_loss": 0.6395826841655531
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6354721800415917,
+ "val_loss": 0.6392063373013547
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6355793091048182,
+ "val_loss": 0.6411467727861906
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6359135708977691,
+ "val_loss": 0.6391528687979046
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6360087489659807,
+ "val_loss": 0.6383986284858302
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6365435872457724,
+ "val_loss": 0.6390739146031832
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6344321354300575,
+ "val_loss": 0.6388166609563326
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6344294774848803,
+ "val_loss": 0.6382842158016405
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6345749624007571,
+ "val_loss": 0.6391266082462511
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.6342231668202223,
+ "val_loss": 0.6377868526860287
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8985293301860843,
+ "val_loss": 0.7344679016815988
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7888136701246279,
+ "val_loss": 0.7371555943238107
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7431401415208799,
+ "val_loss": 0.6743121680460478
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7155663471306319,
+ "val_loss": 0.6497537399593153
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7001211653768489,
+ "val_loss": 0.6515719231806303
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6910152092444158,
+ "val_loss": 0.6390786861118517
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6810186447295468,
+ "val_loss": 0.6265583979456049
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6754621747320733,
+ "val_loss": 0.6300154422458849
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6698767025913812,
+ "val_loss": 0.6225866016588713
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6664408819865336,
+ "val_loss": 0.6211616114566201
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6635912757004256,
+ "val_loss": 0.6152551048680356
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6608437025441533,
+ "val_loss": 0.617341791328631
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6582141402548394,
+ "val_loss": 0.6065479203274375
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6567227497565008,
+ "val_loss": 0.6064017948351408
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6544243118404287,
+ "val_loss": 0.6031277587539271
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6536429065518674,
+ "val_loss": 0.6034210324287415
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6534084799015416,
+ "val_loss": 0.599513053894043
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6510167106062965,
+ "val_loss": 0.6040215868698923
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.650467221715809,
+ "val_loss": 0.6016956065830431
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6505252012109335,
+ "val_loss": 0.6119322180747986
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6490229468430038,
+ "val_loss": 0.6040551913411993
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6491801791486487,
+ "val_loss": 0.6046854069358424
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6486328951025431,
+ "val_loss": 0.5974274136518177
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6484354191121802,
+ "val_loss": 0.6023327520019129
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6474046068908894,
+ "val_loss": 0.6025064838560004
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6476726057255162,
+ "val_loss": 0.6009845325821325
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6468077201758866,
+ "val_loss": 0.6040682165246261
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6463840197672887,
+ "val_loss": 0.5961990795637432
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6456825965273697,
+ "val_loss": 0.6054364191858392
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.645617809970822,
+ "val_loss": 0.5987054266427693
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.644960842828835,
+ "val_loss": 0.6011268088692113
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6473844024987347,
+ "val_loss": 0.5965872563813862
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.64662836452501,
+ "val_loss": 0.6034412446774935
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6465802023896073,
+ "val_loss": 0.6042267240976033
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6459982664184233,
+ "val_loss": 0.5975554475658819
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6459148162234146,
+ "val_loss": 0.5979890290059542
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6453409041978616,
+ "val_loss": 0.6053296735412196
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.644246059181416,
+ "val_loss": 0.6011884934023807
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6437107335149714,
+ "val_loss": 0.6006264592471876
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.645020722287946,
+ "val_loss": 0.5984158296334116
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8827183847933744,
+ "val_loss": 0.9954920599335119
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7630565999883466,
+ "val_loss": 0.8049772287669935
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7238937015027072,
+ "val_loss": 0.8272251329923931
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.6963240166681003,
+ "val_loss": 0.8025932468866047
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6739863315514759,
+ "val_loss": 0.7727768703510887
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6626296998125262,
+ "val_loss": 0.7679434199082223
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6517999710234921,
+ "val_loss": 0.7512396448536923
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6464503987700538,
+ "val_loss": 0.754038039006685
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6409639378564548,
+ "val_loss": 0.7561237686558774
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6382503541169968,
+ "val_loss": 0.7341365469129462
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6344317419339071,
+ "val_loss": 0.756902205316644
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.631139284741562,
+ "val_loss": 0.793930278012627
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6318186827465496,
+ "val_loss": 0.7369769058729473
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6286086165799504,
+ "val_loss": 0.7308662784726996
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6257688347744731,
+ "val_loss": 0.7274297820894342
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6248130429107531,
+ "val_loss": 0.7259557404016194
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6254538506005718,
+ "val_loss": 0.7337523589008733
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.622428049555922,
+ "val_loss": 0.7129873570642973
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6213850616353803,
+ "val_loss": 0.7223403798906427
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6210530300055985,
+ "val_loss": 0.727067370163767
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6203535062021914,
+ "val_loss": 0.7225833974386516
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6200914454143659,
+ "val_loss": 0.7313527794260728
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6193472025668727,
+ "val_loss": 0.7254130620705453
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6191992783440953,
+ "val_loss": 0.7075992948130557
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6186659225320394,
+ "val_loss": 0.7244520108950766
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6181080995407779,
+ "val_loss": 0.7303061265694467
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6177008378822192,
+ "val_loss": 0.7225431721461447
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6194901368786804,
+ "val_loss": 0.7196813413971349
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6190691110307136,
+ "val_loss": 0.7103182391116494
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6176957522345855,
+ "val_loss": 0.7142676428744668
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6185124896268929,
+ "val_loss": 0.714935440766184
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6167146516057243,
+ "val_loss": 0.7424385469210776
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6184053072887185,
+ "val_loss": 0.7275447045501909
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6164048772997561,
+ "val_loss": 0.7109460987542805
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.617832937335546,
+ "val_loss": 0.7217170674549905
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6160449251136949,
+ "val_loss": 0.727050958495391
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6158518677791663,
+ "val_loss": 0.7236304706648776
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6181734206929671,
+ "val_loss": 0.7233174286390606
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6159919561010546,
+ "val_loss": 0.7211907286393014
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.6154739759137146,
+ "val_loss": 0.7238649192609286
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.895842841241212,
+ "val_loss": 0.7923891293375116
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.777429258401415,
+ "val_loss": 0.7444454651129874
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7357844483535901,
+ "val_loss": 0.7130346141363445
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7092309842067482,
+ "val_loss": 0.749277180746982
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7036162235040581,
+ "val_loss": 0.6915544396952579
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6837395366314238,
+ "val_loss": 0.6757652539955942
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.67261591158082,
+ "val_loss": 0.6767958684971458
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6660311601858223,
+ "val_loss": 0.6649402442731356
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6609553573405849,
+ "val_loss": 0.6575920958268014
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6582742829238419,
+ "val_loss": 0.658344711128034
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6539716055962892,
+ "val_loss": 0.6534254425450375
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.651608642223662,
+ "val_loss": 0.6549229057211625
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6497949256306201,
+ "val_loss": 0.6531098422251249
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6469991608003599,
+ "val_loss": 0.6461318511711923
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6452673241100481,
+ "val_loss": 0.6463743918820432
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6432668445384608,
+ "val_loss": 0.6446562346659208
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.642623166594885,
+ "val_loss": 0.6444314467279535
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6419366089116155,
+ "val_loss": 0.6433160995182238
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6412351738035151,
+ "val_loss": 0.6439227618669209
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6399965423398313,
+ "val_loss": 0.6419918537139893
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6409590555503305,
+ "val_loss": 0.6408904194831848
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6406692208442013,
+ "val_loss": 0.6410677683980841
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6381523693557334,
+ "val_loss": 0.6410233127443414
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6382948576876547,
+ "val_loss": 0.6386622253217196
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6377651802206461,
+ "val_loss": 0.6392237449947157
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6373394991444276,
+ "val_loss": 0.6386324920152363
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.638888049969631,
+ "val_loss": 0.6381344042326275
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6364985593652304,
+ "val_loss": 0.6391888668662623
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6361768324818231,
+ "val_loss": 0.6364350287537826
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6353769959074206,
+ "val_loss": 0.6372914753462139
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6358246940427121,
+ "val_loss": 0.6367695896249068
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6356814105426316,
+ "val_loss": 0.6377084788523222
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6354711846967714,
+ "val_loss": 0.6369474404736569
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6353197498659117,
+ "val_loss": 0.6359302527026126
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6372097160963885,
+ "val_loss": 0.637944370508194
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6359708931593768,
+ "val_loss": 0.635823949387199
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6345496124925867,
+ "val_loss": 0.6358601984224821
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6343456659696799,
+ "val_loss": 0.6352364107182151
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6359661870298132,
+ "val_loss": 0.6362085499261555
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.6347245552898508,
+ "val_loss": 0.6354508180367319
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8974713761555521,
+ "val_loss": 0.7373339268896315
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7830517710300914,
+ "val_loss": 0.7377216120560964
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7368365369345012,
+ "val_loss": 0.7078747120168474
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7103670004167055,
+ "val_loss": 0.681788937913047
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.693892417769683,
+ "val_loss": 0.6631654169824388
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6834745600558164,
+ "val_loss": 0.6557001438405778
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6752896063160478,
+ "val_loss": 0.6515020098951128
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6682767533419425,
+ "val_loss": 0.6413944065570831
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6631816724936167,
+ "val_loss": 0.6409551203250885
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6599109183278,
+ "val_loss": 0.6361329919762082
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6569799922014538,
+ "val_loss": 0.6270718740092384
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6535719852698477,
+ "val_loss": 0.6299362116389804
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6530232978494543,
+ "val_loss": 0.6267280711068047
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6507798135280609,
+ "val_loss": 0.6251212159792582
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6485473931881419,
+ "val_loss": 0.624280634853575
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6475618701232108,
+ "val_loss": 0.627081400818295
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6464717477037195,
+ "val_loss": 0.6196649438805051
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6463990953930637,
+ "val_loss": 0.6207783553335402
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6450365347820416,
+ "val_loss": 0.6206175453133054
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6444584648860129,
+ "val_loss": 0.6187917987505595
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6433287147890058,
+ "val_loss": 0.6164814300007291
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6431017125907698,
+ "val_loss": 0.6176451510853238
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6426275879667517,
+ "val_loss": 0.6141722997029623
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6430661275721433,
+ "val_loss": 0.6182732913229201
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6425611930981017,
+ "val_loss": 0.6144691871272193
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6413294348800391,
+ "val_loss": 0.6199106673399607
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6414338826087483,
+ "val_loss": 0.6184608936309814
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6408515101984927,
+ "val_loss": 0.6200940211613973
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6409714143527182,
+ "val_loss": 0.614784386422899
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.640290812965025,
+ "val_loss": 0.6154061026043363
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6399426936057576,
+ "val_loss": 0.6118146214220259
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6398243151212993,
+ "val_loss": 0.6190709869066874
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6396378906149613,
+ "val_loss": 0.616287393702401
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6393419117258307,
+ "val_loss": 0.6134037309222751
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6391931416695578,
+ "val_loss": 0.623112185133828
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6392218339861485,
+ "val_loss": 0.6156649589538574
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6392019315246951,
+ "val_loss": 0.6150778333346049
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6388898397746839,
+ "val_loss": 0.6132999625470903
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6386263880290484,
+ "val_loss": 0.6187854342990451
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.639280564952315,
+ "val_loss": 0.6124114725324843
+ }
+ ]
+ ]
+ },
+ "Swin_Big_Model_Node": {
+ "train_size": 225,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.7785716809724507,
+ 0.8084383512798109,
+ 0.6983975862201891,
+ 0.754498933490954,
+ 0.7695999145507812
+ ],
+ "Mask_Cohesion": [
+ 0.47368421052631576,
+ 0.5263157894736842,
+ 0.21052631578947367,
+ 0.8947368421052632,
+ 0.2777777777777778
+ ],
+ "IoU_Class0": [
+ 0.7303156446568756,
+ 0.7765694144988138,
+ 0.6290431918350473,
+ 0.6768373628586659,
+ 0.7194309772703694
+ ],
+ "IoU_Class1": [
+ 0.44696019588895947,
+ 0.42681913618627954,
+ 0.38273167544376935,
+ 0.4946649072438984,
+ 0.43696572897019115
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.5886379202729175,
+ 0.6016942753425467,
+ 0.5058874336394084,
+ 0.5857511350512822,
+ 0.5781983531202803
+ ],
+ "IoU_Weighted": [
+ 0.6347926373127413,
+ 0.6805019852815544,
+ 0.523801098485455,
+ 0.6160898880680282,
+ 0.6334632461564278
+ ],
+ "Dice_Class0": [
+ 0.8441415263302418,
+ 0.8742348125112702,
+ 0.7722854679211508,
+ 0.8072784849030274,
+ 0.8368244922660173
+ ],
+ "Dice_Class1": [
+ 0.6177919712772243,
+ 0.5982806444930604,
+ 0.5535877744623688,
+ 0.6619074346985779,
+ 0.6081783582735057
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.730966748803733,
+ 0.7362577285021653,
+ 0.6629366211917598,
+ 0.7345929598008027,
+ 0.7225014252697615
+ ],
+ "Dice_Weighted": [
+ 0.7678336839694887,
+ 0.7984362315595375,
+ 0.67883272647643,
+ 0.7588028568077586,
+ 0.7672365005197841
+ ],
+ "Precision_Class0": [
+ 0.7913068315200485,
+ 0.8345220671133373,
+ 0.680419814855131,
+ 0.846632433356287,
+ 0.8247491306331334
+ ],
+ "Precision_Class1": [
+ 0.7387352741775337,
+ 0.705514180171592,
+ 0.7527960743011733,
+ 0.6120015932042374,
+ 0.6303391889408358
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.7650210528487911,
+ 0.7700181236424646,
+ 0.7166079445781521,
+ 0.7293170132802622,
+ 0.7275441597869846
+ ],
+ "Recall_Class0": [
+ 0.9045364386135636,
+ 0.9179160623138501,
+ 0.8928291892471664,
+ 0.7714206020567778,
+ 0.8492587048034759
+ ],
+ "Recall_Class1": [
+ 0.5308783229628323,
+ 0.5193437891985299,
+ 0.4377487275443121,
+ 0.7206750866858171,
+ 0.5875228223222156
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.717707380788198,
+ 0.71862992575619,
+ 0.6652889583957392,
+ 0.7460478443712975,
+ 0.7183907635628457
+ ],
+ "F1_Macro": [
+ 0.740609330248212,
+ 0.7434370632435737,
+ 0.6899955445308902,
+ 0.7375875639157392,
+ 0.7229384891876051
+ ]
+ },
+ "training_history": [
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8917598487001605,
+ "val_loss": 0.805128464573308
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.772838619957983,
+ "val_loss": 0.7609455836446661
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.731357607166324,
+ "val_loss": 0.7147030673528972
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7083493893125415,
+ "val_loss": 0.7093111527593512
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6933188369843812,
+ "val_loss": 0.6878478370214763
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6826516163032667,
+ "val_loss": 0.6799541586323788
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6733636180911444,
+ "val_loss": 0.6736749630225333
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6673002928759144,
+ "val_loss": 0.667719116336421
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6559566755210404,
+ "val_loss": 0.7411779573089198
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6235201888907272,
+ "val_loss": 0.6229961969350514
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.598983044909165,
+ "val_loss": 0.6113497818771162
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.5804287216304678,
+ "val_loss": 0.5721047818660736
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.5727443890234011,
+ "val_loss": 0.6127221976455889
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.5695450316488215,
+ "val_loss": 0.5481671189007006
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.5424258405655886,
+ "val_loss": 0.5488015209373674
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.5359476519896921,
+ "val_loss": 0.7088915297859594
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.5245866176828874,
+ "val_loss": 0.5235991823045831
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.5177670511524234,
+ "val_loss": 0.5405555194930026
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.5141613209142094,
+ "val_loss": 0.6574231166588632
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.5067527718776095,
+ "val_loss": 0.489681003909362
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.4894807082889354,
+ "val_loss": 0.5155867197011647
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.4930926653663669,
+ "val_loss": 0.49826044628494665
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.49848856467061337,
+ "val_loss": 0.5581648396818262
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.48300219004133105,
+ "val_loss": 0.4976841681881955
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.4860639577418302,
+ "val_loss": 0.5382909586555079
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.4860538560732276,
+ "val_loss": 0.49023093361603587
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.4741814479363703,
+ "val_loss": 0.540085820775283
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.4734160103629121,
+ "val_loss": 0.5027086201467013
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.47569870131205666,
+ "val_loss": 0.49366370470900284
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.4822099459382285,
+ "val_loss": 0.4846410892511669
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.47445478956256293,
+ "val_loss": 0.5740452204879961
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.4514117850139078,
+ "val_loss": 0.48764702050309433
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.46124824907927386,
+ "val_loss": 0.4681115715127242
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.4757058517595308,
+ "val_loss": 0.5831693771638369
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.44973109359234836,
+ "val_loss": 0.5027831385010167
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.44680783416317627,
+ "val_loss": 0.7433130913659146
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.45153793929952435,
+ "val_loss": 0.4912541069482502
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.4390299425715894,
+ "val_loss": 0.4925599678566581
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.43312489775429786,
+ "val_loss": 0.508252109351911
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.4451583072675013,
+ "val_loss": 0.46857470430825887
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9058272021006694,
+ "val_loss": 0.7828855012592516
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7839501684745857,
+ "val_loss": 0.7329867827264887
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7448541516751315,
+ "val_loss": 0.6850364772897017
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.71955425391155,
+ "val_loss": 0.6714039193956476
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.70400295953835,
+ "val_loss": 0.6499958916714317
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.692624732456376,
+ "val_loss": 0.6419658943226463
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6836745680960934,
+ "val_loss": 0.6322501019427651
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6757972541108596,
+ "val_loss": 0.6349055045529416
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6695814032470231,
+ "val_loss": 0.6228507443478233
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6657036379375288,
+ "val_loss": 0.6172964039601778
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.664321531764174,
+ "val_loss": 0.6158422043449
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6580257130935129,
+ "val_loss": 0.6154074668884277
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.658592736299059,
+ "val_loss": 0.6176360531857139
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6556185125249677,
+ "val_loss": 0.6143022179603577
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6557892761399261,
+ "val_loss": 0.606652949985705
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6525174110336641,
+ "val_loss": 0.6084216268439042
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6512733186240744,
+ "val_loss": 0.6083758755734092
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6519962696902519,
+ "val_loss": 0.606989672309474
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6490170728843824,
+ "val_loss": 0.6061749677909049
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6340749617171498,
+ "val_loss": 0.5764101994665045
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6144347272615517,
+ "val_loss": 0.6073641510386216
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6066071214401616,
+ "val_loss": 0.5495570709830836
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.587669201393043,
+ "val_loss": 0.5483305626793912
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.5833842440516548,
+ "val_loss": 0.5231484033559498
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.5923179472442222,
+ "val_loss": 0.5677530875331477
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.5666272210336365,
+ "val_loss": 0.5418889177472967
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.5507918630553558,
+ "val_loss": 0.5237200589556443
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.5528494673492634,
+ "val_loss": 0.49094610308345993
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.5400162613497371,
+ "val_loss": 0.48359287726251704
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.5398322267342458,
+ "val_loss": 0.6126459523251182
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.5311622221385484,
+ "val_loss": 0.4977121619801772
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.5358329755542552,
+ "val_loss": 0.6354336001371083
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5152583633903909,
+ "val_loss": 0.4634457199197066
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5260674193897078,
+ "val_loss": 0.5212508298848805
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.5227703789694119,
+ "val_loss": 0.5238294130877444
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.4972838435025342,
+ "val_loss": 0.5232394808217099
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5006852685350233,
+ "val_loss": 0.5683100678418812
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.5070133172305284,
+ "val_loss": 0.46398189977595683
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.4999240921661917,
+ "val_loss": 0.539654476077933
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.5039812898741359,
+ "val_loss": 0.4558097939742239
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8790953539114083,
+ "val_loss": 0.8794299269977369
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7524317281436076,
+ "val_loss": 0.8296938413067868
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7091166588057459,
+ "val_loss": 0.7838297705901297
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.6899794276836699,
+ "val_loss": 0.7815121443648088
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6650801817927741,
+ "val_loss": 0.7766399822737041
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6592699400091593,
+ "val_loss": 0.7601279271276373
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6513894200325012,
+ "val_loss": 0.7609173404543024
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6451359818466996,
+ "val_loss": 0.7580269826085944
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6401048766828217,
+ "val_loss": 0.7455770812536541
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6361939342676011,
+ "val_loss": 0.7302818078743784
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6328574909573108,
+ "val_loss": 0.7394678780907079
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6300501322324297,
+ "val_loss": 0.7431393846085197
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6112949848175049,
+ "val_loss": 0.6547945769209611
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.5945991944950239,
+ "val_loss": 0.6995927895370283
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.573690670781431,
+ "val_loss": 0.6719094232509011
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.5589852422739552,
+ "val_loss": 0.5930393181349102
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.5413765197834083,
+ "val_loss": 0.7479752992328844
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.5208491340147711,
+ "val_loss": 0.6037450919025823
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.5052720783031093,
+ "val_loss": 0.7447022905475215
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.4999031981008243,
+ "val_loss": 0.7398806590782968
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.5036549510154049,
+ "val_loss": 0.5838604475322523
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.48296853873582013,
+ "val_loss": 0.599448108359387
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.47764807174691054,
+ "val_loss": 0.7249806052760074
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.4789146572615193,
+ "val_loss": 0.6719655692577362
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.46816937849584933,
+ "val_loss": 0.7493447520230946
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.4698802644172601,
+ "val_loss": 0.5305190462815134
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.46157195267424117,
+ "val_loss": 0.6111509941126171
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.4634588250016744,
+ "val_loss": 0.5710222564245525
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.45705030528844987,
+ "val_loss": 0.5758043041354731
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.45086783750922277,
+ "val_loss": 0.5829519802018216
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.45137393263588965,
+ "val_loss": 0.661967092438748
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.44927769978489496,
+ "val_loss": 0.7036176966993433
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.4367876582968552,
+ "val_loss": 0.6523014510932722
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.44035989415329113,
+ "val_loss": 0.5863195485190341
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.4392524403808391,
+ "val_loss": 0.6971023929746527
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.4330711697055175,
+ "val_loss": 0.6123408769306383
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.4195118514837417,
+ "val_loss": 0.6116761737748196
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.42522311210632324,
+ "val_loss": 0.6234921797325736
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.4223247239020018,
+ "val_loss": 0.6368430586237657
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.4238569267028201,
+ "val_loss": 0.7217209652850503
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8940239295495295,
+ "val_loss": 0.85766968915337
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7714007987385303,
+ "val_loss": 0.7479480034426639
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.730305333580591,
+ "val_loss": 0.7223013325741416
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7058522738186659,
+ "val_loss": 0.7075489414365668
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6777713314621849,
+ "val_loss": 0.6556790737729323
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6726760806235592,
+ "val_loss": 0.677976740034003
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6735453457958931,
+ "val_loss": 0.6706611545462358
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6678149900605194,
+ "val_loss": 0.665115867790423
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6599643504725093,
+ "val_loss": 0.6593725430338007
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6518385304813892,
+ "val_loss": 0.6457769368824205
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6169757864116567,
+ "val_loss": 0.6167558886502919
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.5955388390384944,
+ "val_loss": 0.6020281644243943
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.5678887799777815,
+ "val_loss": 0.5576325858894148
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.563858666251191,
+ "val_loss": 0.7698246397470173
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.543284260329947,
+ "val_loss": 0.5717194895995291
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.5401290415135105,
+ "val_loss": 0.6198285206368095
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.5342835720661467,
+ "val_loss": 0.5622654431744626
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.5279159456227733,
+ "val_loss": 0.5082549405725378
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.5252638707118752,
+ "val_loss": 0.5571470213563818
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.511864209860827,
+ "val_loss": 0.49981226105439036
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.49666966458337497,
+ "val_loss": 0.5552476205323872
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.5144865850431729,
+ "val_loss": 0.532578562435351
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.5024438052050835,
+ "val_loss": 0.759268017191636
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.48503118938049383,
+ "val_loss": 0.49229707686524643
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.4923796118360705,
+ "val_loss": 0.6119068751209661
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.4848806771029413,
+ "val_loss": 0.5135837871777383
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.4720074740131344,
+ "val_loss": 0.5732325425273493
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.46736269688184284,
+ "val_loss": 0.6771795012448963
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.4519182266914739,
+ "val_loss": 0.5818690014512915
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.4750927902955924,
+ "val_loss": 0.5851883856873763
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.4638032480678727,
+ "val_loss": 0.5378845412480203
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.4405090832604771,
+ "val_loss": 0.5246158540248871
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.44951222375430894,
+ "val_loss": 0.5285246889842184
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.45198325245781285,
+ "val_loss": 0.574352110687055
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.43291426232430785,
+ "val_loss": 0.5557145068519994
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.43756893039804645,
+ "val_loss": 0.5231670941177168
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.4298242839564264,
+ "val_loss": 0.5430447333737424
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.4352227185679748,
+ "val_loss": 0.6258152864481273
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.4265158964996844,
+ "val_loss": 0.5299542154136457
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.4232338492849232,
+ "val_loss": 0.5625477652800711
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8994265810439461,
+ "val_loss": 0.8056320879194472
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7802295548874035,
+ "val_loss": 0.7521736025810242
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.742778085825736,
+ "val_loss": 0.7109150588512421
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7145724557993705,
+ "val_loss": 0.6829386287265353
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6958519450405187,
+ "val_loss": 0.6566644741429223
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6819576025009155,
+ "val_loss": 0.6581905980904897
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6665439814851996,
+ "val_loss": 0.5977724707788892
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6332321436258784,
+ "val_loss": 0.5754021621412702
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.612496281140729,
+ "val_loss": 0.6031805409325494
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.5929990447404092,
+ "val_loss": 0.6164284861750073
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.5821619841613268,
+ "val_loss": 0.5618671625852585
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.5429135310022455,
+ "val_loss": 0.6750003182225757
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.5335590374051479,
+ "val_loss": 0.6642519781986872
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.5254980192372674,
+ "val_loss": 0.5960282451576657
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.5113698228409416,
+ "val_loss": 0.9114658104048835
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.5058739885949252,
+ "val_loss": 0.9833624213933945
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.5145458278425953,
+ "val_loss": 0.6116533726453781
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.5041013101213857,
+ "val_loss": 0.5214400572909249
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.49366281665208045,
+ "val_loss": 0.525258837474717
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.47645885458118037,
+ "val_loss": 0.6620222595002916
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.4869424679823089,
+ "val_loss": 0.6340583264827728
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.47819590620827257,
+ "val_loss": 0.5731261902385287
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.4979949839282454,
+ "val_loss": 0.6483464969529046
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.4669779275070157,
+ "val_loss": 0.6646218415763643
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.4617596953584437,
+ "val_loss": 0.6352019268605444
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.45009254677253857,
+ "val_loss": 0.5430711276001401
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.45890350378396216,
+ "val_loss": 0.5568863368696637
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.4570412790043312,
+ "val_loss": 0.5946431110302607
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.4433604523278119,
+ "val_loss": 0.6133593685097165
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.4431120876157493,
+ "val_loss": 0.5685129364331564
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.43844405914607804,
+ "val_loss": 0.5426358497805066
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.42673622542305994,
+ "val_loss": 0.5906089411841499
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.45014090862190514,
+ "val_loss": 0.5906466344992319
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.4439516645251659,
+ "val_loss": 0.5932624000642035
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.4090514854903807,
+ "val_loss": 0.6616470002465777
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.426599186240581,
+ "val_loss": 0.6238274143801795
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.41404051320594654,
+ "val_loss": 0.7168597247865465
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.40320220615780145,
+ "val_loss": 0.5319957683483759
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.4159978024269405,
+ "val_loss": 0.6370016866260104
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.40779243122067366,
+ "val_loss": 0.5335885087649027
+ }
+ ]
+ ]
+ }
+ },
+ "Combined_3Geo_1Photo_1SEM_x2": {
+ "ViT_Model_Node": {
+ "train_size": 225,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.6152122899105674,
+ 0.7374468351665296,
+ 0.6237742775364926,
+ 0.6419639587402344,
+ 0.6658109029134115
+ ],
+ "Mask_Cohesion": [
+ 0.7894736842105263,
+ 0.9473684210526315,
+ 0.3157894736842105,
+ 0.0,
+ 0.8333333333333334
+ ],
+ "IoU_Class0": [
+ 0.5875872881773542,
+ 0.7268064206470158,
+ 0.5294581622703189,
+ 0.5726493452962942,
+ 0.646678843597835
+ ],
+ "IoU_Class1": [
+ 0.1482722407916765,
+ 0.12918273479218928,
+ 0.3476238025704392,
+ 0.3117764041218939,
+ 0.1394382261611757
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.3679297644845153,
+ 0.4279945777196026,
+ 0.4385409824203791,
+ 0.44221287470909404,
+ 0.3930585348795054
+ ],
+ "IoU_Weighted": [
+ 0.4394929733784199,
+ 0.5626569349423918,
+ 0.45176255099139134,
+ 0.48565829956398443,
+ 0.492301180248878
+ ],
+ "Dice_Class0": [
+ 0.7402267485423618,
+ 0.8417925853839359,
+ 0.6923473623945283,
+ 0.7282606857136413,
+ 0.7854340828050039
+ ],
+ "Dice_Class1": [
+ 0.2582527653711287,
+ 0.2288074920238018,
+ 0.5159062965604886,
+ 0.4753499196085902,
+ 0.24474907539472335
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.4992397569567453,
+ 0.5353000387038689,
+ 0.6041268294775084,
+ 0.6018053026611158,
+ 0.5150915790998636
+ ],
+ "Dice_Weighted": [
+ 0.5777511443874553,
+ 0.6734233903429354,
+ 0.6169484949507614,
+ 0.6439247177079378,
+ 0.6208776865258048
+ ],
+ "Precision_Class0": [
+ 0.6699238976269868,
+ 0.7476812242671663,
+ 0.6512111457655765,
+ 0.736933113429124,
+ 0.7096992009379438
+ ],
+ "Precision_Class1": [
+ 0.36874087484347007,
+ 0.5920966688438929,
+ 0.5728111170138916,
+ 0.46478938066131525,
+ 0.39198924863153245
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.5193323862352284,
+ 0.6698889465555296,
+ 0.6120111313897341,
+ 0.6008612470452197,
+ 0.5508442247847382
+ ],
+ "Recall_Class0": [
+ 0.8270149809297002,
+ 0.963007241161686,
+ 0.7390310560225396,
+ 0.7197900027501217,
+ 0.8792639020648298
+ ],
+ "Recall_Class1": [
+ 0.19871158412851758,
+ 0.14180255490352592,
+ 0.46928598186013304,
+ 0.4864015105218128,
+ 0.17791871562725003
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.5128632825291088,
+ 0.552404898032606,
+ 0.6041585189413363,
+ 0.6030957566359673,
+ 0.5285913088460399
+ ],
+ "F1_Macro": [
+ 0.5160775624007862,
+ 0.6055007752082221,
+ 0.6080594736357351,
+ 0.6019764282478092,
+ 0.5394883912517975
+ ]
+ },
+ "training_history": [
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8993089209615657,
+ "val_loss": 0.8180962706867018
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7823050686743407,
+ "val_loss": 0.7533614698209261
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7335246200055148,
+ "val_loss": 0.7303290806318584
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7029186286757477,
+ "val_loss": 0.7001821116397255
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6847153759635656,
+ "val_loss": 0.6829729425279718
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6667227333625861,
+ "val_loss": 0.6693930061239945
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6618855120861424,
+ "val_loss": 0.6572688002335397
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6526673768473937,
+ "val_loss": 0.6687972545623779
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.645913575075369,
+ "val_loss": 0.6524633796591508
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6383376896908853,
+ "val_loss": 0.7089119741791173
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6386325538685892,
+ "val_loss": 0.6499195255731282
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6338745529672741,
+ "val_loss": 0.7057329541758487
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6314211967772088,
+ "val_loss": 0.6331600735062047
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6263293565902035,
+ "val_loss": 0.6459201856663352
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6261821320626588,
+ "val_loss": 0.6351176845399957
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6147004896033127,
+ "val_loss": 0.7940259434674916
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6160639790834579,
+ "val_loss": 0.6658275974424261
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6190452907992675,
+ "val_loss": 0.648086108659443
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6077231611825723,
+ "val_loss": 0.6396953808633905
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6019631948618762,
+ "val_loss": 0.667694418053878
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6003542331467687,
+ "val_loss": 0.6332734384034809
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.5939932336321975,
+ "val_loss": 0.7249836937377327
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.593462866491976,
+ "val_loss": 0.6830165354829085
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.5935565636748761,
+ "val_loss": 0.6631840890959689
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.5930214935699395,
+ "val_loss": 0.6242835568754297
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.5850595961102342,
+ "val_loss": 0.6481382470381888
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.5778479502264378,
+ "val_loss": 0.675132731073781
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.5805403851302324,
+ "val_loss": 0.656380716123079
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.5662975474796464,
+ "val_loss": 0.6844907042227293
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.5671262456252512,
+ "val_loss": 0.7010904848575592
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.5509323227194558,
+ "val_loss": 0.7650235703116969
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.5575797700248988,
+ "val_loss": 0.6691567160581288
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5608938528900653,
+ "val_loss": 0.7077634522789403
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5630601842319016,
+ "val_loss": 0.6353863885528163
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.5588860102986868,
+ "val_loss": 0.647250843675513
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.5530681955603372,
+ "val_loss": 0.7112657286618885
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5361707094496331,
+ "val_loss": 0.7676055650962027
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.5261363951505813,
+ "val_loss": 0.668402080473147
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.5383906554331822,
+ "val_loss": 0.6847008686316641
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.5142898483086477,
+ "val_loss": 0.6854500441174758
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9034551318767852,
+ "val_loss": 0.7772229345221269
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7936306711846748,
+ "val_loss": 0.738762030476018
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7438538459549963,
+ "val_loss": 0.6767895033485011
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7134747246725369,
+ "val_loss": 0.6845715861571463
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6937286046753942,
+ "val_loss": 0.687970199083027
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6757418708463686,
+ "val_loss": 0.6617308196268583
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6691337059029435,
+ "val_loss": 0.6323305776244715
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6530210660622183,
+ "val_loss": 0.8870536998698586
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6568241124659513,
+ "val_loss": 0.6187295223537245
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6500633786209916,
+ "val_loss": 0.6433447285702354
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.640871386612411,
+ "val_loss": 0.6961376102347123
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.628893942432066,
+ "val_loss": 0.6010656733261911
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6359129131367777,
+ "val_loss": 0.5868400899987471
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.628404606232601,
+ "val_loss": 0.7344568434514498
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.626706111220132,
+ "val_loss": 0.6486098421247382
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6210908314823049,
+ "val_loss": 0.6912451386451721
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6196584730549196,
+ "val_loss": 0.5712897824613672
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6091709967735595,
+ "val_loss": 0.6175570503661507
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6262324001409311,
+ "val_loss": 0.5768441181433829
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6072520539823887,
+ "val_loss": 0.7877673742018247
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6010938394913631,
+ "val_loss": 0.6748455339356473
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.5994450397196069,
+ "val_loss": 0.5953718157191026
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.5981348030335081,
+ "val_loss": 0.6926063440347973
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.5901081662262435,
+ "val_loss": 0.5945640331820438
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.5885318588894025,
+ "val_loss": 0.6249897213358628
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.5850065403280005,
+ "val_loss": 0.6503909198861373
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.5776649386481901,
+ "val_loss": 0.6270899239339327
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.5746482518394437,
+ "val_loss": 0.5919165799492284
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.5700774403799952,
+ "val_loss": 0.5811242994509245
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.5567552893035179,
+ "val_loss": 0.6577093742395702
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.5557888717250486,
+ "val_loss": 0.8217045915754217
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.5709457513505378,
+ "val_loss": 0.564332979290109
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5426069424215672,
+ "val_loss": 0.6347068109010395
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5704694445681783,
+ "val_loss": 0.6366955606560958
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.5619211262833755,
+ "val_loss": 0.6378047607446972
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.548106042684707,
+ "val_loss": 0.6326059150068384
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5527543145998389,
+ "val_loss": 0.7531145716968336
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.5513497292995453,
+ "val_loss": 0.7464794736159476
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.543661422961581,
+ "val_loss": 0.6462961294149098
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.5323584776009078,
+ "val_loss": 0.5677156432678825
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8897112903341783,
+ "val_loss": 0.9020578390673587
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7702533354801414,
+ "val_loss": 0.8152477960837515
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7185502342418232,
+ "val_loss": 0.8036094370641207
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.6851180480644766,
+ "val_loss": 0.7626682457170988
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6669367662573282,
+ "val_loss": 0.7485243427126032
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6512038454545283,
+ "val_loss": 0.8174769078430376
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6390656021843969,
+ "val_loss": 0.7204951016526473
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6310545617500237,
+ "val_loss": 0.7364769546609176
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6223157921196085,
+ "val_loss": 0.7090300290208114
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6188560663598829,
+ "val_loss": 0.7074370509699771
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6158775223039947,
+ "val_loss": 0.7188454615442377
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6107817606060906,
+ "val_loss": 0.7259232715556496
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6028239410535424,
+ "val_loss": 0.7846239522883767
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6030985533663656,
+ "val_loss": 0.7349632868641301
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.5915799478514004,
+ "val_loss": 0.6969939156582481
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.5977161632174939,
+ "val_loss": 0.6929642197332884
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.5892392602642026,
+ "val_loss": 0.7338932658496656
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.5819652222954066,
+ "val_loss": 0.738570058032086
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.5810136702735867,
+ "val_loss": 0.676110964072378
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.5732342265875994,
+ "val_loss": 0.7348742610529849
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.5731763684116633,
+ "val_loss": 0.6723349157132601
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.5691472608431251,
+ "val_loss": 0.7087816188209936
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.5651815198691545,
+ "val_loss": 0.6284340667097192
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.5621992252041809,
+ "val_loss": 0.6679622138801374
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.5477459720805683,
+ "val_loss": 0.7715175183195817
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.550111265309089,
+ "val_loss": 0.6584052004312214
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.5530061922242157,
+ "val_loss": 0.729504964853588
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.5522482416271108,
+ "val_loss": 0.6814344509651786
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.5413387974279117,
+ "val_loss": 0.6765110257424807
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.5374660069963574,
+ "val_loss": 0.7288992593162938
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.5450237963051923,
+ "val_loss": 0.7183158899608412
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.5304273715061424,
+ "val_loss": 0.7527931991376375
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5188308814458088,
+ "val_loss": 0.6836196309641788
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5303732024884857,
+ "val_loss": 0.682487732485721
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.5284386533024037,
+ "val_loss": 0.6737794562389976
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.5127642244364308,
+ "val_loss": 0.6873402783745214
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5133642364392238,
+ "val_loss": 0.7519985076628233
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.4982144808874721,
+ "val_loss": 0.6561151868418643
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.48149979536512255,
+ "val_loss": 0.6913946678763941
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.49476075119676843,
+ "val_loss": 0.6760999403501812
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8957941405541074,
+ "val_loss": 0.8711955892412286
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7738504879242551,
+ "val_loss": 0.8583019752251474
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7307358505451573,
+ "val_loss": 0.8082776195124576
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7090119097085126,
+ "val_loss": 0.7197280369306865
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.683812685772381,
+ "val_loss": 0.7216640177525973
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6740330747798481,
+ "val_loss": 0.7255551313099108
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6603876436706138,
+ "val_loss": 0.7011049954514754
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6585369484614482,
+ "val_loss": 0.6894845680186623
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6505977606351396,
+ "val_loss": 0.7194659019771376
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6472495667702329,
+ "val_loss": 0.6619534586605272
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6381815434557147,
+ "val_loss": 0.682240504967539
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6352438103836194,
+ "val_loss": 0.6510491292727622
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.633256636889635,
+ "val_loss": 0.6466291421338132
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6283511348002779,
+ "val_loss": 0.6558682181333241
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6225633251983508,
+ "val_loss": 0.6810781030278457
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6217181239507895,
+ "val_loss": 0.6660310782884297
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6191146168033633,
+ "val_loss": 0.7342293136998227
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6178712478253694,
+ "val_loss": 0.7326965786908802
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6191128284002827,
+ "val_loss": 0.665951480990962
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6164504213143239,
+ "val_loss": 0.626249296100516
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6040049543422935,
+ "val_loss": 0.6832187505144822
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6129834317000566,
+ "val_loss": 0.6545914066465277
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.5994448398066833,
+ "val_loss": 0.6560289797029997
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6056063280696362,
+ "val_loss": 0.6585049001794112
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.5888194505092317,
+ "val_loss": 0.6386092069901919
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.5947222617347684,
+ "val_loss": 0.8565570335639151
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.5758870180729216,
+ "val_loss": 0.677435532996529
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.582036654242372,
+ "val_loss": 0.6356308852371416
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.5833168227588181,
+ "val_loss": 0.7229195566553819
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.5793067976436784,
+ "val_loss": 0.6502411083171242
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.5844296773450565,
+ "val_loss": 0.6483304218242043
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.5750091682493159,
+ "val_loss": 0.6640583904165971
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5652401634549673,
+ "val_loss": 0.614235763487063
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5659114172500846,
+ "val_loss": 0.6249433865672663
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.5653759188356653,
+ "val_loss": 0.7033489440616808
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.5533813839465116,
+ "val_loss": 0.642991453409195
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5494397126995356,
+ "val_loss": 0.6463884946547056
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.54819249658458,
+ "val_loss": 0.6333742314263394
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.5502025139542808,
+ "val_loss": 0.6605173835628911
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.5503365223386646,
+ "val_loss": 0.6375890907488371
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9023731586180235,
+ "val_loss": 0.8850545518928103
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7829043195958723,
+ "val_loss": 0.7570352123843299
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7355584157140631,
+ "val_loss": 0.7068324585755666
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7083994343615415,
+ "val_loss": 0.674527515967687
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6913385584689024,
+ "val_loss": 0.6656649344497256
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6724083031478681,
+ "val_loss": 0.758908126089308
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6690316273455035,
+ "val_loss": 0.6355013880464766
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6569524684496093,
+ "val_loss": 0.6297131710582309
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6486253069158185,
+ "val_loss": 0.6663328839672936
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6471878115022391,
+ "val_loss": 0.6507828401194679
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6426990471388164,
+ "val_loss": 0.6362271043989394
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.644665213007676,
+ "val_loss": 0.6109129322899712
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6286071860476544,
+ "val_loss": 0.6538509362273746
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6309420414138258,
+ "val_loss": 0.6084673040442996
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.631039684801771,
+ "val_loss": 0.6720006896389855
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.630091189815287,
+ "val_loss": 0.682161357667711
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6260696863918974,
+ "val_loss": 0.6175706817044152
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6180730043796071,
+ "val_loss": 0.6065394795603223
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6206133538170865,
+ "val_loss": 0.610801613993115
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6197516395334612,
+ "val_loss": 0.6508671144644419
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6192166457573572,
+ "val_loss": 0.7519601186116537
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6030193431335583,
+ "val_loss": 0.6836004985703362
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.5980483779781743,
+ "val_loss": 0.5961034893989563
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6050177067518234,
+ "val_loss": 0.6462569534778595
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.5885201810221923,
+ "val_loss": 0.6332353154818217
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6010308323199289,
+ "val_loss": 0.7050961173242993
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.5813196732809669,
+ "val_loss": 0.6277725713120567
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.5916912409297207,
+ "val_loss": 0.6038520452049043
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.586746089814002,
+ "val_loss": 0.7529010706477695
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.583434712729956,
+ "val_loss": 0.6705369883113437
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.5892547457887415,
+ "val_loss": 0.5985193302234014
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.5832510540882746,
+ "val_loss": 0.6407704270548291
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.5738505376012701,
+ "val_loss": 0.6876046806573868
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.5744525233381673,
+ "val_loss": 0.6077772312694125
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.5616286841400883,
+ "val_loss": 0.6208899799320433
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.5643327364273238,
+ "val_loss": 0.590452758802308
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.5510888489191992,
+ "val_loss": 0.622683079706298
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.5468855586491133,
+ "val_loss": 0.6350208934810426
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.5563460575384006,
+ "val_loss": 0.5849934236870872
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.5658711189763588,
+ "val_loss": 0.6699727227290472
+ }
+ ]
+ ]
+ },
+ "Swin_Model_Node": {
+ "train_size": 225,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.7996661537571957,
+ 0.7870302702251234,
+ 0.7543499595240543,
+ 0.7898268448679071,
+ 0.7381916046142578
+ ],
+ "Mask_Cohesion": [
+ 0.5789473684210527,
+ 0.42105263157894735,
+ 0.47368421052631576,
+ 0.8421052631578947,
+ 0.3888888888888889
+ ],
+ "IoU_Class0": [
+ 0.7360164032012699,
+ 0.7345404975360447,
+ 0.6523589381176745,
+ 0.7407567462082452,
+ 0.6663054291779588
+ ],
+ "IoU_Class1": [
+ 0.5462043923098194,
+ 0.48146286927731075,
+ 0.5443315948394181,
+ 0.47385052430637403,
+ 0.45140394427742275
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.6411103977555447,
+ 0.6080016834066777,
+ 0.5983452664785462,
+ 0.6073036352573096,
+ 0.5588546867276908
+ ],
+ "IoU_Weighted": [
+ 0.672025991274185,
+ 0.6650258721749502,
+ 0.6061855178610612,
+ 0.6517538344797285,
+ 0.6009005937828344
+ ],
+ "Dice_Class0": [
+ 0.8479371529485921,
+ 0.8469568725313437,
+ 0.7896092344933545,
+ 0.8510743937333897,
+ 0.7997398526231392
+ ],
+ "Dice_Class1": [
+ 0.7065099478780605,
+ 0.6499830394158695,
+ 0.7049413437611092,
+ 0.6430102869887411,
+ 0.6220238632493904
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.7772235504133262,
+ 0.7484699559736065,
+ 0.7472752891272318,
+ 0.7470423403610653,
+ 0.7108818579362648
+ ],
+ "Dice_Weighted": [
+ 0.8002558321784982,
+ 0.7928515688377908,
+ 0.7534081224707799,
+ 0.7816930537462127,
+ 0.7456523481210113
+ ],
+ "Precision_Class0": [
+ 0.853367370383288,
+ 0.884537052440227,
+ 0.7750788078001237,
+ 0.8063990419688597,
+ 0.8546273394465534
+ ],
+ "Precision_Class1": [
+ 0.6979292277419441,
+ 0.5924169333301254,
+ 0.7239366489612802,
+ 0.7414802801813974,
+ 0.5547752280083983
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.7756482990626161,
+ 0.7384769928851762,
+ 0.7495077283807019,
+ 0.7739396610751286,
+ 0.7047012837274759
+ ],
+ "Recall_Class0": [
+ 0.8425756065530475,
+ 0.8124397953873579,
+ 0.8046948745172088,
+ 0.9009901944453564,
+ 0.751477076299818
+ ],
+ "Recall_Class1": [
+ 0.7153042864009349,
+ 0.7199408495098082,
+ 0.6869173835613155,
+ 0.5676281833217632,
+ 0.7078248359787034
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.7789399464769913,
+ 0.7661903224485831,
+ 0.7458061290392621,
+ 0.7343091888835598,
+ 0.7296509561392607
+ ],
+ "F1_Macro": [
+ 0.7772906379426375,
+ 0.7520784422356468,
+ 0.7476523471174232,
+ 0.7536037634432291,
+ 0.7169591278528479
+ ]
+ },
+ "training_history": [
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8839423951849473,
+ "val_loss": 0.802761906071713
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7678033530184653,
+ "val_loss": 0.7847297191619873
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7240125362851978,
+ "val_loss": 0.7257767194195798
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.6958151428045425,
+ "val_loss": 0.6679160406714991
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6752839853278304,
+ "val_loss": 0.6525112327776457
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.640952333939814,
+ "val_loss": 0.5929591342022544
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.5970319889288033,
+ "val_loss": 0.6420082496969324
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.5849417065624642,
+ "val_loss": 0.6500550916320399
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.5638953964267157,
+ "val_loss": 0.571794309114155
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.5498321968897254,
+ "val_loss": 0.6262560471108085
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.5459295171024525,
+ "val_loss": 0.7694312224262639
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.5397213114573892,
+ "val_loss": 0.4967372056684996
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.5285895261089358,
+ "val_loss": 0.47349032445957784
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.529532957920986,
+ "val_loss": 0.5110050471205461
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.5105200438900331,
+ "val_loss": 0.48601215293532923
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.4938963485502564,
+ "val_loss": 0.4742331457765479
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.5016254977842348,
+ "val_loss": 0.4810808708793239
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.49862292435316913,
+ "val_loss": 0.528297317655463
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.4773760556647208,
+ "val_loss": 0.47420593155057805
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.4875656593162402,
+ "val_loss": 0.6112595404449263
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.4688387220412229,
+ "val_loss": 0.6645170528637735
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.47431056900361995,
+ "val_loss": 0.46906538699802597
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.463087936154509,
+ "val_loss": 0.45440905972530965
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.44507599694538963,
+ "val_loss": 0.5597531842558008
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.4382311867929138,
+ "val_loss": 0.5372626342271504
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.44391585741422873,
+ "val_loss": 0.427789568901062
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.4323232158095436,
+ "val_loss": 0.4900759427171004
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.40743144766419337,
+ "val_loss": 0.4475107177307731
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.4114002777939349,
+ "val_loss": 0.4449237914461839
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.3973194935680491,
+ "val_loss": 0.5445800737330788
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.40097482753010977,
+ "val_loss": 0.4735398700362758
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.3751566788527818,
+ "val_loss": 0.4363133264215369
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.36457721414291755,
+ "val_loss": 0.49467400971211883
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.35794531362774096,
+ "val_loss": 0.4304960236737603
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.3560614811371913,
+ "val_loss": 0.5050376230164578
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.35722006158491154,
+ "val_loss": 0.42072248772570964
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.34267143285380003,
+ "val_loss": 0.48126181803251566
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.35477422652518853,
+ "val_loss": 0.4533076364743082
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.3357979108010773,
+ "val_loss": 0.41837046883608164
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.31607277045207743,
+ "val_loss": 0.4276274881864849
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8798469547676829,
+ "val_loss": 0.8208362867957667
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7739129862954132,
+ "val_loss": 0.7479854979013142
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7344583019746088,
+ "val_loss": 0.6875428463283338
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7062052150743198,
+ "val_loss": 0.6660219493665194
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6913303606278074,
+ "val_loss": 0.6686949667177702
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6746901827575886,
+ "val_loss": 0.6509183692304712
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6465760290095236,
+ "val_loss": 0.5979217698699549
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6218992453233331,
+ "val_loss": 0.6209119919099306
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6035018929865508,
+ "val_loss": 0.5626929841543499
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.5900753810342434,
+ "val_loss": 1.2447614387462014
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.5836948723392149,
+ "val_loss": 0.6135349556019432
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.5761012970346265,
+ "val_loss": 0.772676745527669
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.566587168558509,
+ "val_loss": 0.6139969088529286
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.5548068110921741,
+ "val_loss": 0.5270958982015911
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.5412287511656769,
+ "val_loss": 0.6715772026463559
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.549504666729311,
+ "val_loss": 0.4554220139980316
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.5374388947951055,
+ "val_loss": 0.45890140690301595
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.5248811933319125,
+ "val_loss": 0.5245467251852939
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.5171249010394104,
+ "val_loss": 0.5005425826499337
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.5192578205492644,
+ "val_loss": 0.5759970796735663
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.5184489820910766,
+ "val_loss": 0.5838350998727899
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.5143268018697216,
+ "val_loss": 0.6787536144256592
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.49939539923077136,
+ "val_loss": 0.4552243540161534
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.5004116742484337,
+ "val_loss": 0.5749654518930536
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.4939210894888481,
+ "val_loss": 0.48459587285393163
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.4753891427432541,
+ "val_loss": 0.6242054857705769
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.5073830762268168,
+ "val_loss": 1.1049623614863346
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.48728692584333166,
+ "val_loss": 0.5247819611900731
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.46897600472500894,
+ "val_loss": 0.4891825679101442
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.4669299357760269,
+ "val_loss": 0.5370996923823106
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.46648574323780767,
+ "val_loss": 0.6459982457913851
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.4676152196078174,
+ "val_loss": 0.6592234875026503
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.4528551935094648,
+ "val_loss": 0.4225053363724759
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.45659917935860894,
+ "val_loss": 0.5432377849754534
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.4402652506807209,
+ "val_loss": 0.47957666296707957
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.42657895293910947,
+ "val_loss": 0.5812474191188812
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.4312237287517143,
+ "val_loss": 0.5409864124498869
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.44905907443139403,
+ "val_loss": 0.5098638126724645
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.421394378210591,
+ "val_loss": 0.545491193470202
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.4129481394734003,
+ "val_loss": 0.46745744190717997
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8662151762869506,
+ "val_loss": 0.8536684481721175
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.748046904538585,
+ "val_loss": 0.803268900043086
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7065234622069164,
+ "val_loss": 0.7860456328642996
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.6802662173203663,
+ "val_loss": 0.7746627989568209
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6583293412638976,
+ "val_loss": 0.7351943866202706
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6404500556203117,
+ "val_loss": 0.6706967322449935
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.5944014041297203,
+ "val_loss": 0.6599967636560139
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.5624311009867001,
+ "val_loss": 0.6522652585255472
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.5451598019726509,
+ "val_loss": 0.7594535648822784
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.5265793576177242,
+ "val_loss": 0.7418613292668995
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.5270105450026756,
+ "val_loss": 0.933944295895727
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.5167866238450582,
+ "val_loss": 0.9820152066255871
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.5056768840393134,
+ "val_loss": 0.8251037362374758
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.4985036203819039,
+ "val_loss": 0.837490511567969
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.502116076186695,
+ "val_loss": 0.6804894569673037
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.483656135016838,
+ "val_loss": 0.5572256900762257
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.4786477985635268,
+ "val_loss": 0.6976452124746222
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.4762321810806747,
+ "val_loss": 0.9533129099168276
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.4690537734896736,
+ "val_loss": 0.6683090940902108
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.46922984903892584,
+ "val_loss": 0.6444827961294275
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.4675531408428091,
+ "val_loss": 0.555857556430917
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.46850321672659007,
+ "val_loss": 0.9186670137079138
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.45601005496177,
+ "val_loss": 0.6144629120826721
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.456990643412666,
+ "val_loss": 0.743945377437692
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.44069477836642645,
+ "val_loss": 0.6646420359611511
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.4395724461141941,
+ "val_loss": 0.5447586319948498
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.45419206608713203,
+ "val_loss": 0.5617563002987912
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.42703077381691046,
+ "val_loss": 0.6372997258838854
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.4326313565262651,
+ "val_loss": 0.7394436331171739
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.44413590536708325,
+ "val_loss": 0.6526857253752256
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.4298314504918799,
+ "val_loss": 0.809188005171324
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.4160427983883208,
+ "val_loss": 0.7316613338495556
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.41583860025996655,
+ "val_loss": 0.5934823249515734
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.42399689317804523,
+ "val_loss": 0.5703932611565841
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.3982628779073732,
+ "val_loss": 0.7634792923927307
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.3846347223868412,
+ "val_loss": 0.6096941875784021
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.3822701019523418,
+ "val_loss": 0.6223355139556684
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.3862642851551022,
+ "val_loss": 0.5373531140779194
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.37984262591442175,
+ "val_loss": 0.6278510517195651
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.36425652572539,
+ "val_loss": 0.597368427013096
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8848156069232299,
+ "val_loss": 0.8311264420810499
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7699581389933561,
+ "val_loss": 0.8008518438590201
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7213191089376939,
+ "val_loss": 0.7295772401910079
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.672420672083323,
+ "val_loss": 0.6662930908956026
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6606267838351494,
+ "val_loss": 0.6332515101683768
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6264289158635434,
+ "val_loss": 0.6138237087350142
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6075898443175628,
+ "val_loss": 0.5816102733737544
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.5852568051983825,
+ "val_loss": 0.5611446750791449
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.5641822282191926,
+ "val_loss": 0.7856900472390024
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.5543068581450302,
+ "val_loss": 0.5179886755190397
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.5421130636097056,
+ "val_loss": 0.596318361006285
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.5357282921276262,
+ "val_loss": 0.5694030335074977
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.5358722025314263,
+ "val_loss": 0.5214128368779233
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.5232655115887127,
+ "val_loss": 0.6251687109470367
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.513395559207528,
+ "val_loss": 0.5063666089584953
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.5031737627181332,
+ "val_loss": 0.5267140630044436
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.4993875322088731,
+ "val_loss": 0.4824032799193734
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.49651313039053857,
+ "val_loss": 0.4959413236693332
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.4830883179618194,
+ "val_loss": 0.4715312186040376
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.4747174363748162,
+ "val_loss": 0.4847998352427232
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.44303284651410263,
+ "val_loss": 0.4959327939309572
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.48772617752573133,
+ "val_loss": 0.6017549775148693
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.45883088380889553,
+ "val_loss": 0.5053565172772658
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.43507863567993704,
+ "val_loss": 0.48829621704001175
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.4313305186486877,
+ "val_loss": 0.5500534568962298
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.41366122676207956,
+ "val_loss": 0.5471384588040804
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.41286635346117273,
+ "val_loss": 0.4903075302902021
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.40658572263422266,
+ "val_loss": 0.5420514392225366
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.41013938716027587,
+ "val_loss": 0.4775576905200356
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.3890961936089845,
+ "val_loss": 0.5044502349276292
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.3773131924392903,
+ "val_loss": 0.5352739754476046
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.39351744527837873,
+ "val_loss": 0.5639868159043161
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.38606396607593096,
+ "val_loss": 0.4897398525162747
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.3739088515264798,
+ "val_loss": 0.5625837115865004
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.338530479542977,
+ "val_loss": 0.4668419721879457
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.35008449470047404,
+ "val_loss": 0.46418335092695134
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.33893984833122354,
+ "val_loss": 0.4753913440202412
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.3450792265676819,
+ "val_loss": 0.5096281547295419
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.34125189997453603,
+ "val_loss": 0.4761912524700165
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.31244111905055766,
+ "val_loss": 0.47102535633664383
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8941336666282854,
+ "val_loss": 0.9340544607904222
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7775932733427015,
+ "val_loss": 0.7889872193336487
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7364804200958788,
+ "val_loss": 0.6806161039405398
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7116290304744453,
+ "val_loss": 0.6534764634238349
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6897171599823132,
+ "val_loss": 0.9225868317815993
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6644074054140794,
+ "val_loss": 0.6337505148516761
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6445402309559939,
+ "val_loss": 0.7763307326369815
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6153632525289268,
+ "val_loss": 0.652237786187066
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.5930219091344298,
+ "val_loss": 1.0419278509087033
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.5763210429434191,
+ "val_loss": 0.6318351609839333
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.5688863718195966,
+ "val_loss": 0.7014518529176712
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.561566438852695,
+ "val_loss": 0.6046888844834434
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.5735499754286649,
+ "val_loss": 0.6790683385398653
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.5476395634182712,
+ "val_loss": 0.7522978484630585
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.5451063743808813,
+ "val_loss": 0.6172075917323431
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.5367945892768994,
+ "val_loss": 0.7541877080996832
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.5287065281156909,
+ "val_loss": 0.734409413403935
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.5216820734111887,
+ "val_loss": 0.6100455075502396
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.519567000238519,
+ "val_loss": 0.6401580373446146
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.5157094273650855,
+ "val_loss": 0.597977489233017
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.5143605441900722,
+ "val_loss": 0.5954229649570253
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.5028004661986702,
+ "val_loss": 0.7974254604842927
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.5073346879921461,
+ "val_loss": 0.6424906055132548
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.4999615395801109,
+ "val_loss": 0.5428881496191025
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.4953806533625251,
+ "val_loss": 0.5346276577976015
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.48113801264972017,
+ "val_loss": 0.73554418153233
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.4738157025554724,
+ "val_loss": 0.725593646367391
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.4726755504022565,
+ "val_loss": 0.8334331926372316
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.4754559917930971,
+ "val_loss": 0.5933412743939294
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.4652518120251204,
+ "val_loss": 0.6226986332072152
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.4521416935481523,
+ "val_loss": 0.6229822784662247
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.46055463531561064,
+ "val_loss": 0.6018167932828268
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.4692876652667397,
+ "val_loss": 0.5939956837230258
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.43850796839647127,
+ "val_loss": 0.6122137655814489
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.4450778788641879,
+ "val_loss": 0.627866667177942
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.4498305043630433,
+ "val_loss": 0.648936261733373
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.4224094128922412,
+ "val_loss": 0.8435033129321204
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.42556672530216083,
+ "val_loss": 0.6153177536196179
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.4164919591786569,
+ "val_loss": 0.6834810260269377
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.41764603241493825,
+ "val_loss": 0.6456256657838821
+ }
+ ]
+ ]
+ },
+ "ViT_Big_Model_Node": {
+ "train_size": 225,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.6629002219752261,
+ 0.7239363820929277,
+ 0.5727950648257607,
+ 0.6665386400724712,
+ 0.6956520080566406
+ ],
+ "Mask_Cohesion": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Class0": [
+ 0.6629002219752261,
+ 0.7236177015726454,
+ 0.5727950648257607,
+ 0.6665386400724712,
+ 0.6956520080566406
+ ],
+ "IoU_Class1": [
+ 0.0,
+ 0.004159415031110512,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.33145011098761307,
+ 0.36388855830187794,
+ 0.28639753241288035,
+ 0.3332693200362356,
+ 0.3478260040283203
+ ],
+ "IoU_Weighted": [
+ 0.43943670429480414,
+ 0.5260044755672585,
+ 0.32809418628874737,
+ 0.44427375870965935,
+ 0.4839317163132364
+ ],
+ "Dice_Class0": [
+ 0.7972820175438067,
+ 0.839649883976487,
+ 0.728378512415051,
+ 0.7999078137708059,
+ 0.8205127051439242
+ ],
+ "Dice_Class1": [
+ 0.0,
+ 0.00828437192112897,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.39864100877190334,
+ 0.423967127948808,
+ 0.3641892562075255,
+ 0.39995390688540294,
+ 0.4102563525719621
+ ],
+ "Dice_Weighted": [
+ 0.5285184264066456,
+ 0.6112991457225737,
+ 0.4172116172364703,
+ 0.5331694663741365,
+ 0.5707913109693572
+ ],
+ "Precision_Class0": [
+ 0.6629002219752261,
+ 0.7254677563213554,
+ 0.5727950648257607,
+ 0.6665386400724712,
+ 0.6956520080566406
+ ],
+ "Precision_Class1": [
+ 0.0,
+ 0.31161150298426477,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.33145011098761307,
+ 0.5185396296528101,
+ 0.28639753241288035,
+ 0.3332693200362356,
+ 0.3478260040283203
+ ],
+ "Recall_Class0": [
+ 1.0,
+ 0.9964882026639503,
+ 1.0,
+ 1.0,
+ 1.0
+ ],
+ "Recall_Class1": [
+ 0.0,
+ 0.004197988941811473,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.5,
+ 0.5003430958028809,
+ 0.5,
+ 0.5,
+ 0.5
+ ],
+ "F1_Macro": [
+ 0.39864100877190334,
+ 0.5092788740351436,
+ 0.3641892562075255,
+ 0.399953906885403,
+ 0.4102563525719621
+ ]
+ },
+ "training_history": [
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8942126647560997,
+ "val_loss": 0.7488822874269987
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7829052325898567,
+ "val_loss": 0.7525459026035509
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7318574770361976,
+ "val_loss": 0.7147824544655649
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.708054155902525,
+ "val_loss": 0.6930569378953231
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6895090784646768,
+ "val_loss": 0.686559115585528
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6811994140127063,
+ "val_loss": 0.6754221257410551
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6716311573982239,
+ "val_loss": 0.6678675852323833
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6666712713452567,
+ "val_loss": 0.6654441450771532
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.660133829686494,
+ "val_loss": 0.6596009668550993
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6560935309502931,
+ "val_loss": 0.6559284172560039
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6526379131637843,
+ "val_loss": 0.6549388985884818
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6505232057740203,
+ "val_loss": 0.6519297392744767
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6484337064017237,
+ "val_loss": 0.6492531142736736
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6466650704366971,
+ "val_loss": 0.6494034873811823
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6463898531103556,
+ "val_loss": 0.6486934109737998
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6452415822881513,
+ "val_loss": 0.6465045370553669
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6422276053808432,
+ "val_loss": 0.6465348350374323
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6418059790028935,
+ "val_loss": 0.6443464881495425
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6404302035812783,
+ "val_loss": 0.6468798753462339
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6398973707604197,
+ "val_loss": 0.6433864455474051
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6391467821281568,
+ "val_loss": 0.6425145770374098
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6385631592927781,
+ "val_loss": 0.6437538423036274
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6380631108727075,
+ "val_loss": 0.6425707936286926
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6377863488366119,
+ "val_loss": 0.6434471324870461
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6367085243748353,
+ "val_loss": 0.6409340469460738
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6380346427976558,
+ "val_loss": 0.6406438821240475
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6363998646229769,
+ "val_loss": 0.6392465610253183
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6375211709368546,
+ "val_loss": 0.6408828371449521
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.635750784546928,
+ "val_loss": 0.6404778267207899
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6373528985850578,
+ "val_loss": 0.6427107516087984
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6352712000365806,
+ "val_loss": 0.6394154084356207
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6357865702789441,
+ "val_loss": 0.6402524741072404
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6354077284314991,
+ "val_loss": 0.6387858390808105
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6347919270000627,
+ "val_loss": 0.637655026034305
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6346947814510987,
+ "val_loss": 0.6382462570541784
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6340743387694907,
+ "val_loss": 0.6390012502670288
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.634186816426505,
+ "val_loss": 0.6391188596424303
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6342116694534774,
+ "val_loss": 0.6389302830947073
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6342850127051362,
+ "val_loss": 0.639225865665235
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.6343772959920158,
+ "val_loss": 0.637248883121892
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8981628613134401,
+ "val_loss": 0.7158734798431396
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7892352342605591,
+ "val_loss": 0.677670136878365
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7422740612409812,
+ "val_loss": 0.6735734374899613
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7176556149415211,
+ "val_loss": 0.651888972834537
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.702321875939327,
+ "val_loss": 0.6386707927051344
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6901885719425911,
+ "val_loss": 0.6280457220579448
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6833079035303234,
+ "val_loss": 0.6165013830912741
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6758221830941934,
+ "val_loss": 0.6181379682139346
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6705329012026829,
+ "val_loss": 0.6181834747916773
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6688360318673395,
+ "val_loss": 0.6171802690154627
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6634139070468666,
+ "val_loss": 0.6182611616034257
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6606073337318623,
+ "val_loss": 0.6097492857983238
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6595358658680873,
+ "val_loss": 0.6102200809277987
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6568251320746092,
+ "val_loss": 0.6087768705267655
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6553517089480847,
+ "val_loss": 0.6110675648639077
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6563079362421964,
+ "val_loss": 0.6106082075520566
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6524947823676388,
+ "val_loss": 0.6029423381152906
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6517552005506195,
+ "val_loss": 0.6093194108260306
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6524390388379054,
+ "val_loss": 0.6066194396269949
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6500321083364233,
+ "val_loss": 0.6132673370210748
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6496558046973913,
+ "val_loss": 0.6063706561138755
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6491616051808923,
+ "val_loss": 0.6026785593283804
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6491249652035469,
+ "val_loss": 0.6024442220989027
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6483705307530091,
+ "val_loss": 0.605033162393068
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6492437989310881,
+ "val_loss": 0.6024801982076544
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6474761577834071,
+ "val_loss": 0.6047101648230302
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6470618458975733,
+ "val_loss": 0.6143855264312342
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6481015998705298,
+ "val_loss": 0.6037509786455255
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6474982927330827,
+ "val_loss": 0.6010193950251529
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6471560760936906,
+ "val_loss": 0.6021673302901419
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6466680501414611,
+ "val_loss": 0.6003287497319674
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6455812670488273,
+ "val_loss": 0.6023598501556798
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6449406859094062,
+ "val_loss": 0.6012968546465823
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6453852901416542,
+ "val_loss": 0.6014750599861145
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6451717409412417,
+ "val_loss": 0.6021530220383092
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6445876570929469,
+ "val_loss": 0.6043872707768491
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6450351360624871,
+ "val_loss": 0.6039923586343464
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6447031809165414,
+ "val_loss": 0.6024049457750822
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.644999647035008,
+ "val_loss": 0.6012706411512274
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.6441140559922277,
+ "val_loss": 0.5997983907398424
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8845536476742905,
+ "val_loss": 0.8324170520431117
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7615348902423825,
+ "val_loss": 0.8026620682917143
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7143082887725493,
+ "val_loss": 0.7928849552807055
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.6889241132061038,
+ "val_loss": 0.780125451715369
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6708403224438693,
+ "val_loss": 0.7782181940580669
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6600170905611157,
+ "val_loss": 0.7634717194657576
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6521486233820958,
+ "val_loss": 0.7527681055821871
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.645647978360674,
+ "val_loss": 0.7493035542337518
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6411853232214936,
+ "val_loss": 0.7568961570137426
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6384237951936975,
+ "val_loss": 0.7347345665881508
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6343229908858781,
+ "val_loss": 0.7403867589799982
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6319332025219909,
+ "val_loss": 0.725965961029655
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6304884320866745,
+ "val_loss": 0.7441235623861614
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6284856975606058,
+ "val_loss": 0.7255912046683463
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6263053274787633,
+ "val_loss": 0.7318837328961021
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.625348278906493,
+ "val_loss": 0.7323719388560245
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6243097589606732,
+ "val_loss": 0.7319033067477377
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6230584789166408,
+ "val_loss": 0.7267156337436876
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6223726963574907,
+ "val_loss": 0.7217012894781012
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6215117386484568,
+ "val_loss": 0.726970845147183
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6213942932344116,
+ "val_loss": 0.7078024337166234
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6206164929719098,
+ "val_loss": 0.7217827407937301
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6202224208190378,
+ "val_loss": 0.7297948175355008
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6211354394929599,
+ "val_loss": 0.7193318228972586
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.618798983835541,
+ "val_loss": 0.7198255156215868
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6186798193813425,
+ "val_loss": 0.7274388733663057
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6179270203662129,
+ "val_loss": 0.7266835272312164
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6182613900277467,
+ "val_loss": 0.7172767990513852
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6192395302046717,
+ "val_loss": 0.716121505749853
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6179776840505347,
+ "val_loss": 0.7185518835720263
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6176993751947859,
+ "val_loss": 0.7172904642004716
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6186625804521341,
+ "val_loss": 0.7157662887322275
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6187573800572251,
+ "val_loss": 0.715128459428486
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6158184359031441,
+ "val_loss": 0.7228333840244695
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.617250241009535,
+ "val_loss": 0.7248814764775728
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6182707221107145,
+ "val_loss": 0.723286824791055
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6163408189220766,
+ "val_loss": 0.7153131899080778
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6162923115544614,
+ "val_loss": 0.7176047328271364
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6157084017728283,
+ "val_loss": 0.7211208767012546
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.61590183730674,
+ "val_loss": 0.7294175295453322
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8931862685532697,
+ "val_loss": 0.8075786044723109
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7763795013976308,
+ "val_loss": 0.7375703610871968
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7403004575619655,
+ "val_loss": 0.707974236262472
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7119308367239691,
+ "val_loss": 0.7034800993768793
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6923708615049852,
+ "val_loss": 0.6844510718395835
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6799155035905079,
+ "val_loss": 0.6741666982048437
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6714867416736299,
+ "val_loss": 0.6691197909806904
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6675961481786408,
+ "val_loss": 0.6636416441515872
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.660590475111936,
+ "val_loss": 0.6585277099358408
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6582501293283648,
+ "val_loss": 0.6558746977856285
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.653604856634562,
+ "val_loss": 0.6526469054975008
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6512363236562341,
+ "val_loss": 0.6500889564815321
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6488217178699189,
+ "val_loss": 0.6481296200501291
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6470569783607415,
+ "val_loss": 0.6473908047927054
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6474071936269777,
+ "val_loss": 0.6465186445336593
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6441359852267577,
+ "val_loss": 0.6443555951118469
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6433045637291089,
+ "val_loss": 0.650710758410002
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6439293981653399,
+ "val_loss": 0.6431837866180822
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6411207460724146,
+ "val_loss": 0.6414359055067363
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6404956492175043,
+ "val_loss": 0.6411500886866921
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.639363055208088,
+ "val_loss": 0.6434355440892672
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6391522119530534,
+ "val_loss": 0.6406761219626979
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6379534807880368,
+ "val_loss": 0.6389905841727006
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6401310399570296,
+ "val_loss": 0.6392036080360413
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6388914015440814,
+ "val_loss": 0.6381661452745137
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6387947534037902,
+ "val_loss": 0.6390913128852844
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6371809452508403,
+ "val_loss": 0.6375852038985804
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6369105292632516,
+ "val_loss": 0.6371250340813085
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6365319005155985,
+ "val_loss": 0.6383859860269647
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6361709801496658,
+ "val_loss": 0.6386110876735888
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6356855166696869,
+ "val_loss": 0.6369725120695013
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6356124978149886,
+ "val_loss": 0.6372301390296534
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6372705481748665,
+ "val_loss": 0.6364320014652453
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.637377062993767,
+ "val_loss": 0.6368054057422438
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6351951503120692,
+ "val_loss": 0.635748991840764
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6353331498340168,
+ "val_loss": 0.6369760412918893
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6355217505345302,
+ "val_loss": 0.6376288627323351
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6347866881210192,
+ "val_loss": 0.6362573692673131
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6346771957072537,
+ "val_loss": 0.6360074250321639
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.6341186677460122,
+ "val_loss": 0.6364941973435251
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8882601276824349,
+ "val_loss": 0.7899234096209208
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7970677117506663,
+ "val_loss": 0.706703801949819
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7502679380408505,
+ "val_loss": 0.699653661913342
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7231070179688303,
+ "val_loss": 0.6874427166250017
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7055611992091463,
+ "val_loss": 0.665693054596583
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6924856827970136,
+ "val_loss": 0.6510100232230293
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6826533272601011,
+ "val_loss": 0.657113297118081
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6765030235574957,
+ "val_loss": 0.6492187745041318
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6704570274603995,
+ "val_loss": 0.6432322495513492
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6654178458347655,
+ "val_loss": 0.6342761516571045
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6617343833571986,
+ "val_loss": 0.6420435706774393
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6585936096676609,
+ "val_loss": 0.6452327569325765
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6553524340453901,
+ "val_loss": 0.6268363793691
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6537535493833977,
+ "val_loss": 0.6261253191365136
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6514930228392283,
+ "val_loss": 0.6313199036651187
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6502401551656556,
+ "val_loss": 0.6271715429094102
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.649091873252601,
+ "val_loss": 0.6208069092697568
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6478293778603537,
+ "val_loss": 0.6270367436938815
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6467262862021463,
+ "val_loss": 0.6336583296457926
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6457367366866061,
+ "val_loss": 0.6228897637791104
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6456376805640104,
+ "val_loss": 0.6158999039067162
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6460123788892177,
+ "val_loss": 0.6359087725480398
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6445917968164411,
+ "val_loss": 0.6205551988548703
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6434269923912851,
+ "val_loss": 0.621029900179969
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6430615672939702,
+ "val_loss": 0.6188261045349969
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6425036939612606,
+ "val_loss": 0.6220210558838315
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6420589722039407,
+ "val_loss": 0.6155796051025391
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6417933095965469,
+ "val_loss": 0.6201332840654585
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6415537982656244,
+ "val_loss": 0.6200363669130537
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6412967971542425,
+ "val_loss": 0.6150747603840299
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6409913761573925,
+ "val_loss": 0.613513857126236
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6405454304134637,
+ "val_loss": 0.6173641184965769
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6406424960546326,
+ "val_loss": 0.6138157943884531
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6408268338755557,
+ "val_loss": 0.610118203692966
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6406495597278863,
+ "val_loss": 0.6270148356755575
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6400554810176816,
+ "val_loss": 0.615208711889055
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6397575406651748,
+ "val_loss": 0.6134497159057193
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6398960880020208,
+ "val_loss": 0.614513486623764
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6396314760572032,
+ "val_loss": 0.6134420368406508
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.6393942205529464,
+ "val_loss": 0.6144988967312707
+ }
+ ]
+ ]
+ },
+ "Swin_Big_Model_Node": {
+ "train_size": 225,
+ "test_size": 19,
+ "evaluations": {
+ "Accuracy": [
+ 0.6629002219752261,
+ 0.7253305535567435,
+ 0.6764478181537829,
+ 0.6665386400724712,
+ 0.6956520080566406
+ ],
+ "Mask_Cohesion": [
+ 0.0,
+ 0.0,
+ 0.3684210526315789,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Class0": [
+ 0.6629002219752261,
+ 0.7253305535567435,
+ 0.6151819689492832,
+ 0.6665386400724712,
+ 0.6956520080566406
+ ],
+ "IoU_Class1": [
+ 0.0,
+ 0.0,
+ 0.3298276522500783,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "IoU_Macro": [
+ 0.33145011098761307,
+ 0.36266527677837174,
+ 0.47250481059968075,
+ 0.3332693200362356,
+ 0.3478260040283203
+ ],
+ "IoU_Weighted": [
+ 0.43943670429480414,
+ 0.5261044119229319,
+ 0.4932628929183772,
+ 0.44427375870965935,
+ 0.4839317163132364
+ ],
+ "Dice_Class0": [
+ 0.7972820175438067,
+ 0.8408018417821271,
+ 0.7617494261027129,
+ 0.7999078137708059,
+ 0.8205127051439242
+ ],
+ "Dice_Class1": [
+ 0.0,
+ 0.0,
+ 0.49604571192666574,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Dice_Macro": [
+ 0.39864100877190334,
+ 0.42040092089106357,
+ 0.6288975690146893,
+ 0.39995390688540294,
+ 0.4102563525719621
+ ],
+ "Dice_Weighted": [
+ 0.5285184264066456,
+ 0.6098592653313597,
+ 0.6482179760561738,
+ 0.5331694663741365,
+ 0.5707913109693572
+ ],
+ "Precision_Class0": [
+ 0.6629002219752261,
+ 0.7253305535567435,
+ 0.658716919379171,
+ 0.6665386400724712,
+ 0.6956520080566406
+ ],
+ "Precision_Class1": [
+ 0.0,
+ 0.0,
+ 0.7412650163202584,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Precision_Macro": [
+ 0.33145011098761307,
+ 0.36266527677837174,
+ 0.6999909678497147,
+ 0.3332693200362356,
+ 0.3478260040283203
+ ],
+ "Recall_Class0": [
+ 1.0,
+ 1.0,
+ 0.9029895816282215,
+ 1.0,
+ 1.0
+ ],
+ "Recall_Class1": [
+ 0.0,
+ 0.0,
+ 0.3727391556318238,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Class2": [
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0,
+ 0.0
+ ],
+ "Recall_Macro": [
+ 0.5,
+ 0.5,
+ 0.6378643686300227,
+ 0.5,
+ 0.5
+ ],
+ "F1_Macro": [
+ 0.39864100877190334,
+ 0.42040092089106357,
+ 0.6674851676101817,
+ 0.399953906885403,
+ 0.4102563525719621
+ ]
+ },
+ "training_history": [
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8800016995024892,
+ "val_loss": 0.8952924546442533
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7760772942441755,
+ "val_loss": 0.7586952542003832
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7347698280241637,
+ "val_loss": 0.720664730197505
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7047171344799278,
+ "val_loss": 0.6921980130045038
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6853320224095235,
+ "val_loss": 0.6584004006887737
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6781889748784293,
+ "val_loss": 0.682343043779072
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6740475070160047,
+ "val_loss": 0.6769733585809407
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6661015437767569,
+ "val_loss": 0.6659902647921914
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6607331728513262,
+ "val_loss": 0.6701677127888328
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6600617792754047,
+ "val_loss": 0.6590045847390827
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6530233204892252,
+ "val_loss": 0.6527561263034218
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6495822409612942,
+ "val_loss": 0.6529378985103808
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6462803013556826,
+ "val_loss": 0.6508903189709312
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6465355258072372,
+ "val_loss": 0.649973423857438
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6457413055200493,
+ "val_loss": 0.6471869490648571
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6412044573674159,
+ "val_loss": 0.6382800277910734
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6390508381666336,
+ "val_loss": 0.6456080141820406
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6407184389840185,
+ "val_loss": 0.6453343347499245
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6372635406730449,
+ "val_loss": 0.6321291390218233
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6383841507202757,
+ "val_loss": 0.638793165746488
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6384672353752946,
+ "val_loss": 0.642366487728922
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6378316082785614,
+ "val_loss": 0.640407134043543
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.636797866990081,
+ "val_loss": 0.6393029313338431
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6370930940704008,
+ "val_loss": 0.6414585615459242
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6374510194348023,
+ "val_loss": 0.6401607111880654
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6354499941906043,
+ "val_loss": 0.6383518733476338
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6360334922782088,
+ "val_loss": 0.6385097440920378
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6349564526988342,
+ "val_loss": 0.6374835968017578
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6341127194134535,
+ "val_loss": 0.6375411905740437
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6347714214198357,
+ "val_loss": 0.6382780357411033
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6344256279742824,
+ "val_loss": 0.6363884492924339
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6359942339162911,
+ "val_loss": 0.6370844056731776
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6360334134207363,
+ "val_loss": 0.6366927717861376
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6345345731330129,
+ "val_loss": 0.6361144749741805
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6328615076773989,
+ "val_loss": 0.6412726703443026
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.633380748001875,
+ "val_loss": 0.6367800988649067
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6351752186243513,
+ "val_loss": 0.6380180496918527
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6337804219364065,
+ "val_loss": 0.638141647765511
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.633278836191228,
+ "val_loss": 0.6354116295513353
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.6330794164564757,
+ "val_loss": 0.6355133088011491
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8908295499540009,
+ "val_loss": 0.7611890999894393
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7782317331406923,
+ "val_loss": 0.7930862778111508
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7474932385756906,
+ "val_loss": 0.6860295847842568
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7202887983448738,
+ "val_loss": 0.6689128028719049
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7043676766674075,
+ "val_loss": 0.6486367834241766
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6936851186034954,
+ "val_loss": 0.6337523836838571
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6843414839390105,
+ "val_loss": 0.638860887602756
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6796207976552238,
+ "val_loss": 0.630120575428009
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.673864822999566,
+ "val_loss": 0.6313727278458444
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6680810836564123,
+ "val_loss": 0.6168383799101177
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6642725230318255,
+ "val_loss": 0.6159535583696867
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6630853683547636,
+ "val_loss": 0.6145947293231362
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6607305560491782,
+ "val_loss": 0.6127003305836728
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6576899174040398,
+ "val_loss": 0.6070335209369659
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.661067521677608,
+ "val_loss": 0.6150243439172444
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6568767423123385,
+ "val_loss": 0.6069644218996951
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6534879128489874,
+ "val_loss": 0.6069420011419999
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6514197419175004,
+ "val_loss": 0.6064109112087049
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6517093360951517,
+ "val_loss": 0.6036153718044883
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6501823378875192,
+ "val_loss": 0.6084617219473186
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6494949085522542,
+ "val_loss": 0.6045021352015043
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6500291919286272,
+ "val_loss": 0.6041555718371743
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.648451945423025,
+ "val_loss": 0.6043681659196553
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6474773303597374,
+ "val_loss": 0.5978377084983023
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6478694466363012,
+ "val_loss": 0.6056096679285953
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6454528485779214,
+ "val_loss": 0.6024507409647891
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6468238967709836,
+ "val_loss": 0.5985918107785677
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.646322753049631,
+ "val_loss": 0.5976059515225259
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6449275607556368,
+ "val_loss": 0.6014986853850516
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6465247938063292,
+ "val_loss": 0.5980784155820545
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6447900000926667,
+ "val_loss": 0.6017966897864091
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6460752054653337,
+ "val_loss": 0.6021393882600885
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6453396878411285,
+ "val_loss": 0.6047747762579667
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6449699248887796,
+ "val_loss": 0.6014163086288854
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6454640278774025,
+ "val_loss": 0.6002783492991799
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6429713521383505,
+ "val_loss": 0.5993405925600153
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6433577252700265,
+ "val_loss": 0.5976648252261313
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6434609489103335,
+ "val_loss": 0.5995283126831055
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6433903458899102,
+ "val_loss": 0.6011102670117429
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.6436839082599741,
+ "val_loss": 0.6002498645531503
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8776716463333738,
+ "val_loss": 0.8832003254639474
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7569854734218227,
+ "val_loss": 0.8230477728341755
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7156853106169574,
+ "val_loss": 0.7887811535283139
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.6876840644178137,
+ "val_loss": 0.8180132571019625
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6703553637572094,
+ "val_loss": 0.7390168215099134
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6629758777871596,
+ "val_loss": 0.8364050796157435
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6329694912496922,
+ "val_loss": 0.6521271028016743
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6097606048647282,
+ "val_loss": 0.628749712517387
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.5879306964642179,
+ "val_loss": 0.6329518525223983
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.5681673014058476,
+ "val_loss": 0.7058157779668507
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6414095464005934,
+ "val_loss": 0.7569679084577059
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6334249749120358,
+ "val_loss": 0.7458322173670718
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6291308334443422,
+ "val_loss": 0.7299818773018686
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6265092017376317,
+ "val_loss": 0.7354751135173597
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6256441460774008,
+ "val_loss": 0.731028183510429
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6229019481523902,
+ "val_loss": 0.7169668533300099
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.5674591847753103,
+ "val_loss": 0.616447693423221
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.5312375990690383,
+ "val_loss": 1.0503722962580229
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.5318142202048175,
+ "val_loss": 0.5847713962981576
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.5076573013731863,
+ "val_loss": 0.9818570896198875
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.51355983250964,
+ "val_loss": 0.6804559434715071
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.5027364233953763,
+ "val_loss": 0.5969719729925457
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.49389333033983684,
+ "val_loss": 0.6183245040868458
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.49389265807329025,
+ "val_loss": 0.7738751000479648
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.48881894086314515,
+ "val_loss": 0.6552413686325675
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.48926624391986206,
+ "val_loss": 0.6181567044634568
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.47530759330344413,
+ "val_loss": 0.5981827942948592
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.48205221147663824,
+ "val_loss": 0.5904272920206973
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.47730939762782204,
+ "val_loss": 0.743780327470679
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.46902197256552436,
+ "val_loss": 0.6758609508213244
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.4730514066409221,
+ "val_loss": 0.8121970421389529
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.467436476091368,
+ "val_loss": 0.663163025128214
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.4612729151164536,
+ "val_loss": 0.6215351035720423
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.4578543999026307,
+ "val_loss": 0.5879028753230446
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.4613112583097103,
+ "val_loss": 0.6364665878446478
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.4525306849880556,
+ "val_loss": 0.5910730361938477
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.46393916274593994,
+ "val_loss": 0.6415283507422397
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.46065199849879845,
+ "val_loss": 0.5673327696950812
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.4601934069553308,
+ "val_loss": 0.6738319789108477
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.4433968270246961,
+ "val_loss": 0.621228897257855
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.8907391576640373,
+ "val_loss": 0.8299851919475355
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7786326461133704,
+ "val_loss": 0.7502167130771437
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7373749392222514,
+ "val_loss": 0.7265696462831999
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7110151368959815,
+ "val_loss": 0.7056921877359089
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.6951173517556317,
+ "val_loss": 0.6914366609171817
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6847987264658497,
+ "val_loss": 0.6778246101580168
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6735770496646915,
+ "val_loss": 0.6684061320204484
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6701293719553314,
+ "val_loss": 0.6650788878139696
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6618708308819121,
+ "val_loss": 0.6621472364977786
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6583560409799086,
+ "val_loss": 0.6561459491127416
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.653306920444016,
+ "val_loss": 0.6540532833651492
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6505389361254936,
+ "val_loss": 0.6510821612257707
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6482230889058747,
+ "val_loss": 0.6479950547218323
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6467416792844249,
+ "val_loss": 0.6476603150367737
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6444604681656424,
+ "val_loss": 0.6463101663087544
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.646907032063577,
+ "val_loss": 0.6492583594824138
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6470750575571989,
+ "val_loss": 0.6447219597665887
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.6442667208941637,
+ "val_loss": 0.6427853452531915
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6432601798952153,
+ "val_loss": 0.6422980804192392
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6412486565851532,
+ "val_loss": 0.6406004083784003
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6434386166851077,
+ "val_loss": 0.6400069688495836
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6399415346373499,
+ "val_loss": 0.641149883207522
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6385080118094926,
+ "val_loss": 0.6398793364826002
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6383962852764974,
+ "val_loss": 0.6374852280867728
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6391189496601577,
+ "val_loss": 0.6394540134229159
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6385780557066993,
+ "val_loss": 0.6375186286474529
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.636596829490324,
+ "val_loss": 0.6376591544402274
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.6365241772305649,
+ "val_loss": 0.6360727830937034
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6359680490156191,
+ "val_loss": 0.6369265004208213
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6344080287798316,
+ "val_loss": 0.6373466159168043
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6370481226296552,
+ "val_loss": 0.636344777910333
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6370160587593517,
+ "val_loss": 0.6360333248188621
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6359076900819761,
+ "val_loss": 0.6362430327817014
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.635151925340163,
+ "val_loss": 0.6363057211825722
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6349096725472306,
+ "val_loss": 0.6355266853382713
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6340228962687264,
+ "val_loss": 0.6351752500785025
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6339949601519425,
+ "val_loss": 0.6345793981301157
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6330008633368838,
+ "val_loss": 0.634985089302063
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6325327601053018,
+ "val_loss": 0.6400713920593262
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.634708594959394,
+ "val_loss": 0.6377209738681191
+ }
+ ],
+ [
+ {
+ "epoch": 1,
+ "train_loss": 0.9111717322416473,
+ "val_loss": 0.794701099395752
+ },
+ {
+ "epoch": 2,
+ "train_loss": 0.7783909304100171,
+ "val_loss": 0.7245373593436347
+ },
+ {
+ "epoch": 3,
+ "train_loss": 0.7412470453663876,
+ "val_loss": 0.701075103547838
+ },
+ {
+ "epoch": 4,
+ "train_loss": 0.7191103900733747,
+ "val_loss": 0.6881146762106154
+ },
+ {
+ "epoch": 5,
+ "train_loss": 0.7013415709922188,
+ "val_loss": 0.6479683054818047
+ },
+ {
+ "epoch": 6,
+ "train_loss": 0.6902791303500795,
+ "val_loss": 0.6659223536650339
+ },
+ {
+ "epoch": 7,
+ "train_loss": 0.6789704127269879,
+ "val_loss": 0.6368989646434784
+ },
+ {
+ "epoch": 8,
+ "train_loss": 0.6632888839955915,
+ "val_loss": 0.5612126059002347
+ },
+ {
+ "epoch": 9,
+ "train_loss": 0.6519976864781296,
+ "val_loss": 0.7466063698132833
+ },
+ {
+ "epoch": 10,
+ "train_loss": 0.6351309763757806,
+ "val_loss": 0.5897334069013596
+ },
+ {
+ "epoch": 11,
+ "train_loss": 0.6534792174372757,
+ "val_loss": 0.6606428027153015
+ },
+ {
+ "epoch": 12,
+ "train_loss": 0.6598151347093415,
+ "val_loss": 0.6415654983785417
+ },
+ {
+ "epoch": 13,
+ "train_loss": 0.6553860881872344,
+ "val_loss": 0.6229742334948646
+ },
+ {
+ "epoch": 14,
+ "train_loss": 0.6524026268406918,
+ "val_loss": 0.6221608320871989
+ },
+ {
+ "epoch": 15,
+ "train_loss": 0.6515135195171624,
+ "val_loss": 0.6264926758077409
+ },
+ {
+ "epoch": 16,
+ "train_loss": 0.6494242209091521,
+ "val_loss": 0.6199958920478821
+ },
+ {
+ "epoch": 17,
+ "train_loss": 0.6481867437822777,
+ "val_loss": 0.6219358974032931
+ },
+ {
+ "epoch": 18,
+ "train_loss": 0.647550073109175,
+ "val_loss": 0.6237328516112434
+ },
+ {
+ "epoch": 19,
+ "train_loss": 0.6460597096827992,
+ "val_loss": 0.6172988613446554
+ },
+ {
+ "epoch": 20,
+ "train_loss": 0.6459642601640601,
+ "val_loss": 0.6175627443525527
+ },
+ {
+ "epoch": 21,
+ "train_loss": 0.6451984058346665,
+ "val_loss": 0.617321726348665
+ },
+ {
+ "epoch": 22,
+ "train_loss": 0.6444445508613921,
+ "val_loss": 0.6166701780425178
+ },
+ {
+ "epoch": 23,
+ "train_loss": 0.6428611126908085,
+ "val_loss": 0.6187809407711029
+ },
+ {
+ "epoch": 24,
+ "train_loss": 0.6422653104129591,
+ "val_loss": 0.6163720024956597
+ },
+ {
+ "epoch": 25,
+ "train_loss": 0.6423134782858062,
+ "val_loss": 0.6163382099734412
+ },
+ {
+ "epoch": 26,
+ "train_loss": 0.6409621233480018,
+ "val_loss": 0.6220410366853079
+ },
+ {
+ "epoch": 27,
+ "train_loss": 0.6420760758613285,
+ "val_loss": 0.6247921486695608
+ },
+ {
+ "epoch": 28,
+ "train_loss": 0.642000123597028,
+ "val_loss": 0.6205902198950449
+ },
+ {
+ "epoch": 29,
+ "train_loss": 0.6405635434284545,
+ "val_loss": 0.6132683124807146
+ },
+ {
+ "epoch": 30,
+ "train_loss": 0.6401937687606142,
+ "val_loss": 0.6121750407748752
+ },
+ {
+ "epoch": 31,
+ "train_loss": 0.6405957607846511,
+ "val_loss": 0.6148053275214301
+ },
+ {
+ "epoch": 32,
+ "train_loss": 0.6400156643307,
+ "val_loss": 0.6138863530423906
+ },
+ {
+ "epoch": 33,
+ "train_loss": 0.6400589425312845,
+ "val_loss": 0.6103036204973856
+ },
+ {
+ "epoch": 34,
+ "train_loss": 0.6399407292667189,
+ "val_loss": 0.6143762800428603
+ },
+ {
+ "epoch": 35,
+ "train_loss": 0.6401690516555518,
+ "val_loss": 0.6147250831127167
+ },
+ {
+ "epoch": 36,
+ "train_loss": 0.6390854496704904,
+ "val_loss": 0.6152887940406799
+ },
+ {
+ "epoch": 37,
+ "train_loss": 0.6399010414617103,
+ "val_loss": 0.61203733086586
+ },
+ {
+ "epoch": 38,
+ "train_loss": 0.6387392925588709,
+ "val_loss": 0.6088068121009402
+ },
+ {
+ "epoch": 39,
+ "train_loss": 0.6394444050496084,
+ "val_loss": 0.6092244817150964
+ },
+ {
+ "epoch": 40,
+ "train_loss": 0.6395142852214345,
+ "val_loss": 0.6096394062042236
+ }
+ ]
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/results/scripts/calculate_f1_means.py b/results/scripts/calculate_f1_means.py
new file mode 100644
index 0000000..1be8743
--- /dev/null
+++ b/results/scripts/calculate_f1_means.py
@@ -0,0 +1,85 @@
+"""Calculate mean F1 scores for each model and augmentator."""
+
+import json
+from pathlib import Path
+from typing import Any
+
+# Directory where this script lives
+SCRIPT_DIR = Path(__file__).parent
+# Results directory is the parent of scripts/
+RESULTS_DIR = SCRIPT_DIR.parent
+
+
+def calculate_f1_means(results: dict[str, Any]) -> dict[str, dict[str, float]]:
+ """
+ Calculate the mean F1_Macro score for each model and augmentator.
+
+ Returns a dictionary with structure:
+ {
+ "augmentator_name": {
+ "model_name": mean_f1_score,
+ ...
+ },
+ ...
+ }
+ """
+ f1_means: dict[str, dict[str, float]] = {}
+
+ for experiment_key, experiment_data in results.items():
+ f1_means[experiment_key] = {}
+
+ for model_key, model_data in experiment_data.items():
+ if "evaluations" not in model_data:
+ continue
+
+ evaluations = model_data["evaluations"]
+
+ data = {}
+
+ # Calculate mean F1_Macro if it exists
+ if "F1_Macro" in evaluations:
+ f1_values = evaluations["F1_Macro"]
+ mean_f1 = sum(f1_values) / len(f1_values) if f1_values else 0.0
+ data["mean_f1"] = mean_f1
+ if "Accuracy" in evaluations:
+ acc_values = evaluations["Accuracy"]
+ mean_acc = sum(acc_values) / len(acc_values) if acc_values else 0.0
+ data["mean_accuracy"] = mean_acc
+ if "Mask_Cohesion" in evaluations:
+ mask_values = evaluations["Mask_Cohesion"]
+ mean_mask = sum(mask_values) / len(mask_values) if mask_values else 0.0
+ data["mean_mask_cohesion"] = mean_mask
+
+ f1_means[experiment_key][model_key] = data
+ return f1_means
+
+
+def main() -> None:
+ """Run the F1 mean calculation."""
+ results_path = RESULTS_DIR / "results_cache.json"
+ output_path = RESULTS_DIR / "f1_means.json"
+
+ if not results_path.exists():
+ print(f"Error: {results_path} does not exist")
+ return
+
+ # Load the results
+ print(f"Loading results from {results_path}...")
+ with open(results_path) as f:
+ results = json.load(f)
+
+ # Calculate F1 means
+ print("Calculating mean F1 scores for each model and augmentator...")
+ f1_means = calculate_f1_means(results)
+
+ # Save the F1 means
+ print(f"Saving F1 means to {output_path}...")
+ with open(output_path, "w") as f:
+ json.dump(f1_means, f, indent=2)
+
+ print("Done! F1 means have been saved.")
+ print(f"Results saved to: {output_path}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/results/scripts/recalculate_metrics.py b/results/scripts/recalculate_metrics.py
new file mode 100644
index 0000000..84ba7fa
--- /dev/null
+++ b/results/scripts/recalculate_metrics.py
@@ -0,0 +1,104 @@
+"""Recalculate macro metrics in the results cache file."""
+
+import json
+from pathlib import Path
+from typing import Any
+
+# Directory where this script lives
+SCRIPT_DIR = Path(__file__).parent
+# Results directory is the parent of scripts/
+RESULTS_DIR = SCRIPT_DIR.parent
+
+
+def recalculate_macro_metrics(results: dict[str, Any]) -> dict[str, Any]:
+ """
+ Recalculate macro metrics as the average of Class0 and Class1 only.
+
+ Also calculates F1_Macro using the recalculated Precision_Macro and Recall_Macro.
+ """
+ for experiment_key, experiment_data in results.items():
+ for model_key, model_data in experiment_data.items():
+ if "evaluations" not in model_data:
+ continue
+
+ evaluations = model_data["evaluations"]
+
+ # Recalculate IoU_Macro
+ if "IoU_Class0" in evaluations and "IoU_Class1" in evaluations:
+ class0 = evaluations["IoU_Class0"]
+ class1 = evaluations["IoU_Class1"]
+ evaluations["IoU_Macro"] = [
+ (c0 + c1) / 2 for c0, c1 in zip(class0, class1)
+ ]
+
+ # Recalculate Dice_Macro
+ if "Dice_Class0" in evaluations and "Dice_Class1" in evaluations:
+ class0 = evaluations["Dice_Class0"]
+ class1 = evaluations["Dice_Class1"]
+ evaluations["Dice_Macro"] = [
+ (c0 + c1) / 2 for c0, c1 in zip(class0, class1)
+ ]
+
+ # Recalculate Precision_Macro
+ if "Precision_Class0" in evaluations and "Precision_Class1" in evaluations:
+ class0 = evaluations["Precision_Class0"]
+ class1 = evaluations["Precision_Class1"]
+ evaluations["Precision_Macro"] = [
+ (c0 + c1) / 2 for c0, c1 in zip(class0, class1)
+ ]
+
+ # Recalculate Recall_Macro
+ if "Recall_Class0" in evaluations and "Recall_Class1" in evaluations:
+ class0 = evaluations["Recall_Class0"]
+ class1 = evaluations["Recall_Class1"]
+ evaluations["Recall_Macro"] = [
+ (c0 + c1) / 2 for c0, c1 in zip(class0, class1)
+ ]
+
+ # Calculate F1_Macro using Precision_Macro and Recall_Macro
+ if "Precision_Macro" in evaluations and "Recall_Macro" in evaluations:
+ precision = evaluations["Precision_Macro"]
+ recall = evaluations["Recall_Macro"]
+ evaluations["F1_Macro"] = [
+ (2 * p * r / (p + r)) if (p + r) > 0 else 0.0
+ for p, r in zip(precision, recall)
+ ]
+
+ return results
+
+
+def main() -> None:
+ """Run the recalculation of macro metrics."""
+ results_path = RESULTS_DIR / "results_cache.json"
+
+ if not results_path.exists():
+ print(f"Error: {results_path} does not exist")
+ return
+
+ # Load the results
+ print(f"Loading results from {results_path}...")
+ with open(results_path, "r") as f:
+ results = json.load(f)
+
+ # Recalculate macro metrics
+ print("Recalculating macro metrics...")
+ results = recalculate_macro_metrics(results)
+
+ # Save the updated results
+ print(f"Saving updated results to {results_path}...")
+ with open(results_path, "w") as f:
+ json.dump(results, f, indent=2)
+
+ print("Done! Macro metrics have been recalculated.")
+ print(
+ "- IoU_Macro, Dice_Macro, Precision_Macro, Recall_Macro: "
+ "average of Class0 and Class1",
+ )
+ print(
+ "- F1_Macro: calculated as "
+ "2 * (Precision_Macro * Recall_Macro) / (Precision_Macro + Recall_Macro)",
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/setup/__init__.py b/setup/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/setup/augmentators/__init__.py b/setup/augmentators/__init__.py
new file mode 100644
index 0000000..7871214
--- /dev/null
+++ b/setup/augmentators/__init__.py
@@ -0,0 +1,5 @@
+from .identity import get_identity_augmentator_node
+
+__all__ = [
+ "get_identity_augmentator_node",
+]
diff --git a/setup/augmentators/baseline.py b/setup/augmentators/baseline.py
new file mode 100644
index 0000000..75d4c7d
--- /dev/null
+++ b/setup/augmentators/baseline.py
@@ -0,0 +1,23 @@
+"""Baseline augmentation node with no augmentation."""
+
+from auto_ml.implementations.augmentators.composite import MultiplyDatasetAugmentator
+from auto_ml.implementations.augmentators.identity import IdentityAugmentator
+from auto_ml.implementations.nodes import DataAugmentatorNode
+
+
+def get_baseline_node(num_copies: int = 1) -> DataAugmentatorNode:
+ """
+ Create a baseline node with no augmentation for comparison.
+
+ Args:
+ num_copies: Number of copies to create (default: 1).
+
+ """
+ return DataAugmentatorNode(
+ augmentator=MultiplyDatasetAugmentator(
+ augmentators=[IdentityAugmentator()],
+ num_copies=num_copies,
+ include_original=False,
+ ),
+ name=f"Baseline_NoAugmentation_x{num_copies}",
+ )
diff --git a/setup/augmentators/combined_2geo_2photo_1sem.py b/setup/augmentators/combined_2geo_2photo_1sem.py
new file mode 100644
index 0000000..59716a9
--- /dev/null
+++ b/setup/augmentators/combined_2geo_2photo_1sem.py
@@ -0,0 +1,72 @@
+"""Combined augmentation node: 2 geometric, 2 photometric, 1 SEM."""
+
+from auto_ml.implementations.augmentators.composite import (
+ MultiplyDatasetAugmentator,
+ RandomApplyAugmentator,
+ SequentialAugmentator,
+)
+from auto_ml.implementations.augmentators.geometric import (
+ HorizontalFlipAugmentator,
+ RotationAugmentator,
+)
+from auto_ml.implementations.augmentators.photometric import (
+ BrightnessAugmentator,
+ ContrastAugmentator,
+)
+from auto_ml.implementations.augmentators.sem_specific import (
+ ElasticDeformationAugmentator,
+)
+from auto_ml.implementations.nodes import DataAugmentatorNode
+
+
+def get_combined_2geo_2photo_1sem_node(num_copies: int = 1) -> DataAugmentatorNode:
+ """
+ Create a node with 2 geometric, 2 photometric, and 1 SEM augmentation.
+
+ Args:
+ num_copies: Number of augmented copies to create (default: 1).
+
+ """
+ return DataAugmentatorNode(
+ augmentator=MultiplyDatasetAugmentator(
+ augmentators=[
+ SequentialAugmentator(
+ augmentators=[
+ # 2 Geometric augmentations
+ RandomApplyAugmentator(
+ augmentator=HorizontalFlipAugmentator(),
+ probability=0.5,
+ ),
+ RandomApplyAugmentator(
+ augmentator=RotationAugmentator(angle_range=(-15.0, 15.0)),
+ probability=0.5,
+ ),
+ # 2 Photometric augmentations (independent)
+ RandomApplyAugmentator(
+ augmentator=BrightnessAugmentator(
+ brightness_range=(0.85, 1.15),
+ ),
+ probability=0.3,
+ ),
+ RandomApplyAugmentator(
+ augmentator=ContrastAugmentator(
+ contrast_range=(0.85, 1.15),
+ ),
+ probability=0.4,
+ ),
+ # 1 SEM augmentation
+ RandomApplyAugmentator(
+ augmentator=ElasticDeformationAugmentator(
+ alpha=25.0,
+ sigma=3.5,
+ ),
+ probability=0.5,
+ ),
+ ],
+ ),
+ ],
+ num_copies=num_copies,
+ include_original=True,
+ ),
+ name=f"Combined_2Geo_2Photo_1SEM_x{num_copies}",
+ )
diff --git a/setup/augmentators/combined_3geo_1photo_1sem.py b/setup/augmentators/combined_3geo_1photo_1sem.py
new file mode 100644
index 0000000..371179d
--- /dev/null
+++ b/setup/augmentators/combined_3geo_1photo_1sem.py
@@ -0,0 +1,68 @@
+"""Combined augmentation node: 3 geometric, 1 photometric, 1 SEM."""
+
+from auto_ml.implementations.augmentators.composite import (
+ MultiplyDatasetAugmentator,
+ RandomApplyAugmentator,
+ SequentialAugmentator,
+)
+from auto_ml.implementations.augmentators.geometric import (
+ HorizontalFlipAugmentator,
+ RotationAugmentator,
+ VerticalFlipAugmentator,
+)
+from auto_ml.implementations.augmentators.photometric import (
+ ContrastAugmentator,
+)
+from auto_ml.implementations.augmentators.sem_specific import (
+ AdaptiveHistogramEqualizationAugmentator,
+)
+from auto_ml.implementations.nodes import DataAugmentatorNode
+
+
+def get_combined_3geo_1photo_1sem_node(num_copies: int = 1) -> DataAugmentatorNode:
+ """
+ Create a node with 3 geometric, 1 photometric, and 1 SEM augmentation.
+
+ Args:
+ num_copies: Number of augmented copies to create (default: 1).
+
+ """
+ return DataAugmentatorNode(
+ augmentator=MultiplyDatasetAugmentator(
+ augmentators=[
+ SequentialAugmentator(
+ augmentators=[
+ # Geometric augmentations (independent)
+ RandomApplyAugmentator(
+ augmentator=HorizontalFlipAugmentator(),
+ probability=0.5,
+ ),
+ RandomApplyAugmentator(
+ augmentator=VerticalFlipAugmentator(),
+ probability=0.5,
+ ),
+ RandomApplyAugmentator(
+ augmentator=RotationAugmentator(angle_range=(-15.0, 15.0)),
+ probability=0.4,
+ ),
+ # 1 Photometric augmentation
+ RandomApplyAugmentator(
+ augmentator=ContrastAugmentator(contrast_range=(0.8, 1.2)),
+ probability=0.4,
+ ),
+ # 1 SEM augmentation
+ RandomApplyAugmentator(
+ augmentator=AdaptiveHistogramEqualizationAugmentator(
+ clip_limit=2.0,
+ tile_grid_size=(8, 8),
+ ),
+ probability=0.6,
+ ),
+ ],
+ ),
+ ],
+ num_copies=num_copies,
+ include_original=True,
+ ),
+ name=f"Combined_3Geo_1Photo_1SEM_x{num_copies}",
+ )
diff --git a/setup/augmentators/identity.py b/setup/augmentators/identity.py
new file mode 100644
index 0000000..e3194f1
--- /dev/null
+++ b/setup/augmentators/identity.py
@@ -0,0 +1,12 @@
+from auto_ml.implementations.augmentators.identity import IdentityAugmentator
+from auto_ml.implementations.nodes import DataAugmentatorNode
+
+
+def get_identity_augmentator_node() -> DataAugmentatorNode:
+ """Return an identity augmentator node."""
+ return DataAugmentatorNode(
+ augmentator=IdentityAugmentator(),
+ name="Aug_Identity_K5",
+ k_folds=5,
+ random_seed=42,
+ )
diff --git a/setup/augmentators/setup.py b/setup/augmentators/setup.py
new file mode 100644
index 0000000..16989fb
--- /dev/null
+++ b/setup/augmentators/setup.py
@@ -0,0 +1,35 @@
+from typing import List
+
+from auto_ml.implementations.nodes import DataAugmentatorNode
+from setup.augmentators.combined_2geo_2photo_1sem import (
+ get_combined_2geo_2photo_1sem_node,
+)
+from setup.augmentators.combined_3geo_1photo_1sem import (
+ get_combined_3geo_1photo_1sem_node,
+)
+from setup.augmentators.identity import (
+ get_identity_augmentator_node,
+)
+
+
+def get_augmentator_nodes(
+ baseline_copies: int = 0,
+ combined_2geo_2photo_1sem_copies: int = 2,
+ combined_3geo_1photo_1sem_copies: int = 2,
+) -> List[DataAugmentatorNode]:
+ """
+ Return a list of augmentator nodes with efficient combinations for optimal results.
+
+ Args:
+ baseline_copies: Number of copies for baseline node (default: 0).
+ combined_2geo_2photo_1sem_copies: Number of copies for 2geo+2photo+1sem
+ (default: 2).
+ combined_3geo_1photo_1sem_copies: Number of copies for 3geo+1photo+1sem
+ (default: 2).
+
+ """
+ return [
+ get_identity_augmentator_node(),
+ get_combined_2geo_2photo_1sem_node(num_copies=combined_2geo_2photo_1sem_copies),
+ get_combined_3geo_1photo_1sem_node(num_copies=combined_3geo_1photo_1sem_copies),
+ ]
diff --git a/setup/evaluator/__init__.py b/setup/evaluator/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/setup/evaluator/setup.py b/setup/evaluator/setup.py
new file mode 100644
index 0000000..efd1ba2
--- /dev/null
+++ b/setup/evaluator/setup.py
@@ -0,0 +1,68 @@
+from auto_ml.implementations.evaluators import (
+ AccuracyEvaluator,
+ AutoencoderMaskEvaluator,
+ DiceClass0Evaluator,
+ DiceClass1Evaluator,
+ DiceClass2Evaluator,
+ DiceMacroAverageEvaluator,
+ DiceWeightedAverageEvaluator,
+ IoUClass0Evaluator,
+ IoUClass1Evaluator,
+ IoUClass2Evaluator,
+ IoUMacroAverageEvaluator,
+ IoUWeightedAverageEvaluator,
+ PrecisionClass0Evaluator,
+ PrecisionClass1Evaluator,
+ PrecisionClass2Evaluator,
+ PrecisionMacroAverageEvaluator,
+ RecallClass0Evaluator,
+ RecallClass1Evaluator,
+ RecallClass2Evaluator,
+ RecallMacroAverageEvaluator,
+)
+from auto_ml.implementations.nodes import EvaluatorNode
+from auto_ml.interfaces import SegmentationDatasetInterface
+
+
+def get_evaluator_node(dataset: SegmentationDatasetInterface) -> EvaluatorNode:
+ """Return evaluator node for use in Auto-ML."""
+ evaluators = {
+ # General
+ "Accuracy": AccuracyEvaluator(),
+
+ # Autoencoder (requires training on reference masks)
+ "Mask_Cohesion": AutoencoderMaskEvaluator(
+ reference_masks=dataset.masks,
+ latent_dim=8,
+ epochs=40,
+ nu=0.5,
+ device="auto",
+ ),
+
+ # IoU Metrics
+ "IoU_Class0": IoUClass0Evaluator(),
+ "IoU_Class1": IoUClass1Evaluator(),
+ "IoU_Class2": IoUClass2Evaluator(),
+ "IoU_Macro": IoUMacroAverageEvaluator(),
+ "IoU_Weighted": IoUWeightedAverageEvaluator(),
+
+ # Dice Metrics
+ "Dice_Class0": DiceClass0Evaluator(),
+ "Dice_Class1": DiceClass1Evaluator(),
+ "Dice_Class2": DiceClass2Evaluator(),
+ "Dice_Macro": DiceMacroAverageEvaluator(),
+ "Dice_Weighted": DiceWeightedAverageEvaluator(),
+
+ # Precision Metrics
+ "Precision_Class0": PrecisionClass0Evaluator(),
+ "Precision_Class1": PrecisionClass1Evaluator(),
+ "Precision_Class2": PrecisionClass2Evaluator(),
+ "Precision_Macro": PrecisionMacroAverageEvaluator(),
+
+ # Recall Metrics
+ "Recall_Class0": RecallClass0Evaluator(),
+ "Recall_Class1": RecallClass1Evaluator(),
+ "Recall_Class2": RecallClass2Evaluator(),
+ "Recall_Macro": RecallMacroAverageEvaluator(),
+ }
+ return EvaluatorNode(evaluators=evaluators)
diff --git a/setup/models/__init__.py b/setup/models/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/setup/models/setup.py b/setup/models/setup.py
new file mode 100644
index 0000000..51da602
--- /dev/null
+++ b/setup/models/setup.py
@@ -0,0 +1,151 @@
+from pathlib import Path
+from typing import List, Optional
+
+from auto_ml.implementations.classifiers.cnn import CNNModel
+from auto_ml.implementations.classifiers.vit import ViTModel as ViTClassificationModel
+from auto_ml.implementations.nodes import ModelNode
+from auto_ml.implementations.segmentators.quadtree import QuadtreeSegmentationModel
+from auto_ml.implementations.segmentators.sliding_window import (
+ SlidingWindowSegmentationModel,
+)
+from auto_ml.implementations.segmentators.swin import SwinModel
+from auto_ml.implementations.segmentators.vit import ViTModel as ViTSegmentationModel
+from auto_ml.interfaces import ClassificationModelInterface
+
+
+def create_vit_model_node() -> ModelNode:
+ """Create a ModelNode with a ViT segmentation model."""
+ vit_model = ViTSegmentationModel(
+ epochs=40,
+ batch_size=2,
+ dim=512,
+ depth=12,
+ heads=16,
+ mlp_dim=2048,
+ device="auto",
+ )
+ return ModelNode(model=vit_model, name="ViT_Big_Model_Node")
+
+
+def create_swin_model_node() -> ModelNode:
+ """Create a ModelNode with a Swin segmentation model."""
+ swin_model = SwinModel(
+ epochs=40,
+ batch_size=2,
+ embed_dim=128,
+ depths=[2, 2, 18, 2],
+ num_heads=[8, 16, 32, 64],
+ device="auto",
+ )
+ return ModelNode(model=swin_model, name="Swin_Big_Model_Node")
+
+
+def _create_quadtree_model_node(
+ classifier: ClassificationModelInterface,
+ classifier_dataset_dir: Path,
+ optimize_metric: Optional[str] = None,
+) -> ModelNode:
+ """Create a QuadTree segmentation model node given a classifier."""
+ quadtree_model = QuadtreeSegmentationModel(
+ classifier,
+ classifier_dataset_dir,
+ min_region_size=8,
+ threshold=0.5,
+ optimize_metric=optimize_metric,
+ )
+ return ModelNode(
+ model=quadtree_model,
+ name=f"Quadtree-{classifier.__class__.__name__}_Model_Node",
+ )
+
+
+def create_quadtree_model_nodes(classifier_dataset_dir: Path) -> List[ModelNode]:
+ """Create a list of QuadTree segmentation model nodes with different classifiers."""
+ classifiers = [
+ CNNModel(train_epochs=50),
+ ViTClassificationModel(
+ train_epochs=40,
+ dim=256,
+ depth=6,
+ heads=8,
+ mlp_dim=512,
+ device="auto",
+ ),
+ ]
+
+ # change this parameters at taste
+ optimize_metric = "f1_score"
+
+ return [
+ _create_quadtree_model_node(classifier, classifier_dataset_dir, optimize_metric)
+ for classifier in classifiers
+ ]
+
+
+def _create_sliding_window_model_node(
+ classifier: ClassificationModelInterface,
+ classifier_dataset_dir: Path,
+ window_size: int,
+ stride: int,
+) -> ModelNode:
+ """Create a SlidingWindow model node with specified parameters."""
+ model = SlidingWindowSegmentationModel(
+ classifier=classifier,
+ classifier_dataset_dir=classifier_dataset_dir,
+ window_size=window_size,
+ stride=stride,
+ aggregation_method="majority_vote",
+ )
+
+ classifier_name = classifier.__class__.__name__.replace("Model", "")
+ node_name = f"SlidingWindow-{classifier_name}_W{window_size}_S{stride}"
+
+ return ModelNode(model=model, name=node_name)
+
+
+def create_sliding_window_model_nodes(classifier_dataset_dir: Path) -> List[ModelNode]:
+ """Create SlidingWindow model nodes with different configurations."""
+ # Two classifiers
+ classifiers = [
+ CNNModel(train_epochs=50, num_blocks=5), # Supports min 32x32
+ ViTClassificationModel(
+ train_epochs=40,
+ dim=256,
+ depth=6,
+ heads=8,
+ mlp_dim=512,
+ device="auto",
+ ),
+ ]
+
+ # Four window/stride configurations
+ configs = [
+ {"window_size": 64, "stride": 32}, # Fine-grained
+ {"window_size": 128, "stride": 64}, # Balanced
+ {"window_size": 128, "stride": 128}, # Fast (non-overlapping)
+ {"window_size": 256, "stride": 128}, # Coarse
+ ]
+
+ nodes = []
+ for classifier in classifiers:
+ for config in configs:
+ nodes.append(
+ _create_sliding_window_model_node(
+ classifier=classifier,
+ classifier_dataset_dir=classifier_dataset_dir,
+ window_size=config["window_size"],
+ stride=config["stride"],
+ ),
+ )
+
+ return nodes
+
+
+def get_model_nodes(classifier_dataset_dir: Path) -> List[ModelNode]:
+ """Return a list of model nodes for use in Auto-ML."""
+ return [
+ create_vit_model_node(),
+ create_swin_model_node(),
+ *create_quadtree_model_nodes(classifier_dataset_dir),
+ *create_sliding_window_model_nodes(classifier_dataset_dir),
+ ]
diff --git a/src/core/example.py b/src/core/example.py
deleted file mode 100644
index 01179e0..0000000
--- a/src/core/example.py
+++ /dev/null
@@ -1,3 +0,0 @@
-def add_two_numbers(a: int, b: int) -> int:
- """Add two numbers."""
- return a + b
diff --git a/tests/test_augmentator_node.py b/tests/test_augmentator_node.py
new file mode 100644
index 0000000..703825e
--- /dev/null
+++ b/tests/test_augmentator_node.py
@@ -0,0 +1,47 @@
+import numpy as np
+
+from auto_ml.implementations import DataAugmentatorNode, IdentityAugmentator
+from auto_ml.interfaces import SegmentationDatasetInterface
+
+
+def test_augmentator() -> None: # noqa: D103
+ print("Initializing Verification for DataAugmentatorNode...")
+
+ # 1. Create dummy dataset (100 samples)
+ print("Creating dummy dataset...")
+ image = np.zeros((512, 512, 3), dtype=np.uint8) # Dummy image
+ mask = np.zeros((512, 512), dtype=np.uint8) # Dummy mask
+
+ dataset = SegmentationDatasetInterface()
+ for _ in range(100):
+ dataset.add_sample(image, mask)
+
+ print(f"Dataset created with {len(dataset)} samples.")
+
+ # 2. Test K-Folds = 5 (default)
+ print("\nTesting K=5 Folds...")
+ node_k5 = DataAugmentatorNode(IdentityAugmentator(), k_folds=5)
+ splits_k5 = node_k5.process(dataset)
+
+ print(f"Number of splits: {len(splits_k5)}")
+ assert len(splits_k5) == 5, f"Expected 5 splits, got {len(splits_k5)}"
+
+ for i, (train, val) in enumerate(splits_k5):
+ print(f" Split {i + 1}: Train={len(train)}, Val={len(val)}")
+ assert len(train) == 80, f"Expected 80 train samples, got {len(train)}"
+ assert len(val) == 20, f"Expected 20 val samples, got {len(val)}"
+
+ # 3. Test K-Folds = 1 (Single Split)
+ print("\nTesting K=1 Fold (Single Split)...")
+ node_k1 = DataAugmentatorNode(IdentityAugmentator(), k_folds=1, test_size=0.3)
+ splits_k1 = node_k1.process(dataset)
+
+ print(f"Number of splits: {len(splits_k1)}")
+ assert len(splits_k1) == 1, f"Expected 1 split, got {len(splits_k1)}"
+
+ train, val = splits_k1[0]
+ print(f" Split 1: Train={len(train)}, Val={len(val)}")
+ assert len(train) == 70, f"Expected 70 train samples, got {len(train)}"
+ assert len(val) == 30, f"Expected 30 val samples, got {len(val)}"
+
+ print("\nVERIFICATION SUCCESSFUL!")
diff --git a/tests/test_automl.py b/tests/test_automl.py
new file mode 100644
index 0000000..7856f7a
--- /dev/null
+++ b/tests/test_automl.py
@@ -0,0 +1,81 @@
+from pathlib import Path
+
+from auto_ml.automl import AutoML
+from auto_ml.implementations import (
+ AccuracyEvaluator,
+ DataAugmentatorNode,
+ EvaluatorNode,
+ IdentityAugmentator,
+ ModelNode,
+ SwinModel,
+ ViTModel,
+ load_dataset_from_directories,
+)
+
+
+def test_automl() -> None: # noqa: D103
+ print("=== Starting AutoML Verification ===")
+
+ # Paths
+ base_dir = Path(".")
+ input_dir = base_dir / "vega_3_tescan_unlabeled_images"
+ target_dir = base_dir / "vega_3_tescan_labeled_images"
+
+ # 1. Load Dataset
+ print("\n--- Step 1: Loading Dataset ---")
+ dataset = load_dataset_from_directories(input_dir, target_dir)
+
+ if len(dataset) == 0:
+ print("Error: No data loaded.")
+ return
+
+ # 2. Setup Nodes
+ print("\n--- Step 2: Setting up Nodes ---")
+
+ # Augmentators
+ # We can create two identical augmentators just to test the graph logic
+ # In reality one would be Rotated, one Scaled etc.
+ aug_node_1 = DataAugmentatorNode(
+ augmentator=IdentityAugmentator(),
+ name="Aug_Identity_K5",
+ k_folds=5,
+ random_seed=42,
+ )
+
+ aug_node_2 = DataAugmentatorNode(
+ augmentator=IdentityAugmentator(), # reusing identity for now
+ name="Aug_Identity_K3",
+ k_folds=3,
+ random_seed=42,
+ )
+
+ augmentators = [aug_node_1, aug_node_2]
+
+ # Models
+ # Swin and ViT
+ vit_model = ViTModel(epochs=2, batch_size=2, device="auto")
+ swin_model = SwinModel(epochs=2, batch_size=2, device="auto")
+
+ model_node_vit = ModelNode(model=vit_model, name="ViT_Model_Node")
+ model_node_swin = ModelNode(model=swin_model, name="Swin_Model_Node")
+
+ models = [model_node_vit, model_node_swin]
+
+ # Evaluator Node with named evaluators
+ evaluator_node = EvaluatorNode(
+ evaluators={
+ "accuracy": AccuracyEvaluator(),
+ },
+ name="MainEvaluator",
+ )
+
+ # 3. Run AutoML
+ print("\n--- Step 3: Running AutoML Experiment ---")
+ automl = AutoML()
+ automl.run_experiment(dataset, augmentators, models, evaluator_node=evaluator_node)
+
+ # 4. Results
+ print("\n--- Step 4: Summary ---")
+ print(automl.get_summary())
+
+ print("\n=== AUTOML VERIFICATION SUCCESSFUL! ===")
diff --git a/tests/test_automl_autosaver.py b/tests/test_automl_autosaver.py
new file mode 100644
index 0000000..756216f
--- /dev/null
+++ b/tests/test_automl_autosaver.py
@@ -0,0 +1,212 @@
+"""Tests for AutoML autosaver caching functionality."""
+
+import json
+import shutil
+import tempfile
+from collections.abc import Generator
+from pathlib import Path
+
+import pytest
+
+from auto_ml import AutoML
+
+
+@pytest.fixture
+def temp_cache_dir() -> Generator[Path, None, None]:
+ """Create a temporary cache directory for testing."""
+ temp_dir = Path(tempfile.mkdtemp())
+ yield temp_dir
+ # Cleanup
+ shutil.rmtree(temp_dir, ignore_errors=True)
+
+
+class TestAutoMLAutosaver:
+ """Test suite for AutoML caching and autosaver functionality."""
+
+ def test_automl_initialization_creates_cache_dir(
+ self,
+ temp_cache_dir: Path,
+ ) -> None:
+ """Test that AutoML creates cache directory on init."""
+ automl = AutoML(cache_dir=temp_cache_dir)
+ assert Path(temp_cache_dir).exists()
+ assert automl.cache_dir == Path(temp_cache_dir)
+
+ def test_automl_initializes_empty_caches(self, temp_cache_dir: Path) -> None:
+ """Test that AutoML initializes with empty caches."""
+ automl = AutoML(cache_dir=temp_cache_dir)
+ assert automl.results_cache == {}
+ assert automl.execution_times_cache == {}
+
+ def test_cache_result_saves_to_json(self, temp_cache_dir: Path) -> None:
+ """Test that _cache_result saves results and times to JSON."""
+ automl = AutoML(cache_dir=temp_cache_dir)
+ result = {"mask_pairs": [[]], "evaluation": {"accuracy": 0.95}}
+ execution_time = 5.2
+
+ automl._cache_result("aug1", "model1", result, execution_time)
+
+ # Verify JSON files were created and contain data
+ results_file = Path(temp_cache_dir) / "results_cache.json"
+ times_file = Path(temp_cache_dir) / "execution_times_cache.json"
+
+ assert results_file.exists()
+ assert times_file.exists()
+
+ with open(results_file) as f:
+ results_data = json.load(f)
+ with open(times_file) as f:
+ times_data = json.load(f)
+
+ assert "aug1" in results_data
+ assert "model1" in results_data["aug1"]
+ assert results_data["aug1"]["model1"] == result
+ assert times_data["aug1"]["model1"] == execution_time
+
+ def test_is_cached_returns_true_when_cached(self, temp_cache_dir: Path) -> None:
+ """Test that _is_cached returns True for cached combinations."""
+ automl = AutoML(cache_dir=temp_cache_dir)
+ result = {"mask_pairs": [[]], "evaluation": {"accuracy": 0.95}}
+
+ automl._cache_result("aug1", "model1", result, 5.0)
+
+ assert automl._is_cached("aug1", "model1")
+
+ def test_is_cached_returns_false_when_not_cached(
+ self, temp_cache_dir: Path,
+ ) -> None:
+ """Test that _is_cached returns False for uncached combinations."""
+ automl = AutoML(cache_dir=temp_cache_dir)
+
+ assert not automl._is_cached("aug1", "model1")
+
+ def test_get_cached_result_retrieves_result(self, temp_cache_dir: Path) -> None:
+ """Test that _get_cached_result retrieves cached results."""
+ automl = AutoML(cache_dir=temp_cache_dir)
+ result = {"mask_pairs": [[]], "evaluation": {"accuracy": 0.95}}
+
+ automl._cache_result("aug1", "model1", result, 5.0)
+ retrieved = automl._get_cached_result("aug1", "model1")
+
+ assert retrieved == result
+
+ def test_get_cached_result_returns_none_when_not_cached(
+ self,
+ temp_cache_dir: Path,
+ ) -> None:
+ """Test that _get_cached_result returns None for uncached combos."""
+ automl = AutoML(cache_dir=temp_cache_dir)
+
+ assert automl._get_cached_result("aug1", "model1") is None
+
+ def test_load_caches_loads_existing_files(self, temp_cache_dir: Path) -> None:
+ """Test that _load_caches loads existing cache files."""
+ # Create initial cache
+ automl1 = AutoML(cache_dir=temp_cache_dir)
+ result = {"mask_pairs": [[]], "evaluation": {"accuracy": 0.95}}
+ automl1._cache_result("aug1", "model1", result, 5.0)
+
+ # Create new instance - should load cache
+ automl2 = AutoML(cache_dir=temp_cache_dir)
+
+ assert automl2._is_cached("aug1", "model1")
+ assert automl2._get_cached_result("aug1", "model1") == result
+ assert automl2.execution_times_cache["aug1"]["model1"] == 5.0
+
+ def test_clear_cache_entry_removes_entry(self, temp_cache_dir: Path) -> None:
+ """Test that _clear_cache_entry removes specific cache entry."""
+ automl = AutoML(cache_dir=temp_cache_dir)
+ result = {"mask_pairs": [[]], "evaluation": {"accuracy": 0.95}}
+
+ automl._cache_result("aug1", "model1", result, 5.0)
+ assert automl._is_cached("aug1", "model1")
+
+ automl._clear_cache_entry("aug1", "model1")
+ assert not automl._is_cached("aug1", "model1")
+
+ def test_clear_cache_entry_persists_to_json(self, temp_cache_dir: Path) -> None:
+ """Test that _clear_cache_entry persists changes to JSON."""
+ automl1 = AutoML(cache_dir=temp_cache_dir)
+ result = {"mask_pairs": [[]], "evaluation": {"accuracy": 0.95}}
+
+ automl1._cache_result("aug1", "model1", result, 5.0)
+ automl1._clear_cache_entry("aug1", "model1")
+
+ # Create new instance - should reflect cleared state
+ automl2 = AutoML(cache_dir=temp_cache_dir)
+ assert not automl2._is_cached("aug1", "model1")
+
+ def test_multiple_augmentators_and_models(self, temp_cache_dir: Path) -> None:
+ """Test caching with multiple augmentators and models."""
+ automl = AutoML(cache_dir=temp_cache_dir)
+ result1 = {"mask_pairs": [[]], "evaluation": {"accuracy": 0.90}}
+ result2 = {"mask_pairs": [[]], "evaluation": {"accuracy": 0.95}}
+ result3 = {"mask_pairs": [[]], "evaluation": {"accuracy": 0.92}}
+
+ automl._cache_result("aug1", "model1", result1, 5.0)
+ automl._cache_result("aug1", "model2", result2, 6.0)
+ automl._cache_result("aug2", "model1", result3, 5.5)
+
+ assert automl._is_cached("aug1", "model1")
+ assert automl._is_cached("aug1", "model2")
+ assert automl._is_cached("aug2", "model1")
+ assert not automl._is_cached("aug2", "model2")
+
+ # Verify nested structure is preserved
+ assert automl.results_cache["aug1"]["model1"] == result1
+ assert automl.results_cache["aug1"]["model2"] == result2
+ assert automl.results_cache["aug2"]["model1"] == result3
+
+ def test_cache_handles_corrupted_json(self, temp_cache_dir: Path) -> None:
+ """Test that corrupted JSON files are handled gracefully."""
+ # Create corrupted JSON file
+ cache_dir = Path(temp_cache_dir)
+ cache_dir.mkdir(exist_ok=True)
+ results_file = cache_dir / "results_cache.json"
+ results_file.write_text("{ invalid json")
+
+ # Should not raise, just print warning
+ automl = AutoML(cache_dir=temp_cache_dir)
+ assert automl.results_cache == {}
+
+ def test_nested_dict_structure_preserved(self, temp_cache_dir: Path) -> None:
+ """Test that nested dictionary structure is preserved."""
+ automl = AutoML(cache_dir=temp_cache_dir)
+ result = {"mask_pairs": [[]], "evaluation": {"accuracy": 0.95}}
+
+ automl._cache_result("aug1", "model1", result, 5.0)
+
+ # Check structure
+ assert isinstance(automl.results_cache, dict)
+ assert isinstance(automl.results_cache["aug1"], dict)
+ assert "model1" in automl.results_cache["aug1"]
+ assert automl.results_cache["aug1"]["model1"]["evaluation"]["accuracy"] == 0.95
+
+ def test_execution_time_tracking(self, temp_cache_dir: Path) -> None:
+ """Test that execution times are properly tracked."""
+ automl = AutoML(cache_dir=temp_cache_dir)
+ result = {"mask_pairs": [[]], "evaluation": {"accuracy": 0.95}}
+
+ automl._cache_result("aug1", "model1", result, 12.345)
+
+ assert automl.execution_times_cache["aug1"]["model1"] == 12.345
+
+ # Verify it's saved to JSON
+ times_file = Path(temp_cache_dir) / "execution_times_cache.json"
+ with open(times_file) as f:
+ times_data = json.load(f)
+ assert times_data["aug1"]["model1"] == 12.345
+
+ def test_run_experiment_clear_cache_parameter(self, temp_cache_dir: Path) -> None:
+ """Test that run_experiment accepts clear_cache parameter."""
+ automl = AutoML(cache_dir=temp_cache_dir)
+
+ # Manually add to cache for testing
+ automl._cache_result("aug1", "model1", {"data": "test"}, 5.0)
+ assert automl._is_cached("aug1", "model1")
+
+ # Use clear_cache parameter (without running actual experiment)
+ # Just test that the parameter is accepted and clears entries
+ automl._clear_cache_entry("aug1", "model1")
+
+ assert not automl._is_cached("aug1", "model1")
diff --git a/tests/test_automl_history.py b/tests/test_automl_history.py
new file mode 100644
index 0000000..1938adc
--- /dev/null
+++ b/tests/test_automl_history.py
@@ -0,0 +1,96 @@
+"""Test training history tracking."""
+
+from pathlib import Path
+from tempfile import TemporaryDirectory
+
+import numpy as np
+
+from auto_ml.automl import AutoML
+from auto_ml.implementations import (
+ DataAugmentatorNode,
+ IdentityAugmentator,
+ ModelNode,
+ ViTModel,
+)
+from auto_ml.interfaces import SegmentationDatasetInterface
+
+
+class MockModel(ViTModel):
+ """Mock model to test history tracking."""
+
+ def __init__(self, epochs: int = 2) -> None:
+ """Initialize Mock Model."""
+ super().__init__(epochs=epochs, batch_size=2, device="cpu")
+
+
+def test_automl_history_tracking() -> None:
+ """Test that AutoML correctly captures training history."""
+ # 1. Create Dummy Dataset
+ print("Creating dummy dataset...")
+ images = [
+ np.random.randint(0, 255, (512, 512, 3), dtype=np.uint8) for _ in range(10)
+ ]
+ masks = [
+ np.random.randint(0, 3, (512, 512), dtype=np.uint8) for _ in range(10)
+ ]
+ dataset = SegmentationDatasetInterface.from_pairs(list(zip(images, masks)))
+
+ # 2. Setup Nodes
+ aug_node = DataAugmentatorNode(
+ augmentator=IdentityAugmentator(),
+ name="Aug_Test",
+ k_folds=2, # Use 2 folds to test list of histories
+ random_seed=42,
+ )
+
+ # Use a small epoch count
+ epochs = 3
+ model = MockModel(epochs=epochs)
+ model_node = ModelNode(model=model, name="ViT_Test")
+
+ # 3. Run AutoML
+ with TemporaryDirectory() as tmp_dir:
+ automl = AutoML(cache_dir=Path(tmp_dir))
+ # Verify results
+ results = automl.run_experiment(dataset, [aug_node], [model_node])
+
+ # 4. Verify Results
+ assert "Aug_Test" in results
+ assert "ViT_Test" in results["Aug_Test"]
+
+ model_result = results["Aug_Test"]["ViT_Test"]
+
+ # Check if history exists
+ assert "training_history" in model_result
+ history = model_result["training_history"]
+
+ print(f"Captured History: {history}")
+
+ # Should have 2 entries (one per fold)
+ assert len(history) == 2
+
+ # Check that losses are not identical
+ # (suggests identical training path/weights reuse)
+ # We initialized random seeds so training *should* be deterministic per fold
+ # IF the data is the same, but folds have different data.
+ loss_fold_0 = history[0][-1]["train_loss"]
+ loss_fold_1 = history[1][-1]["train_loss"]
+ # It's highly unlikely they are EXACTLY the same float
+ # if trained on different data
+ assert loss_fold_0 != loss_fold_1
+
+ # Check content of history
+ for fold_history in history:
+ assert isinstance(fold_history, list)
+ assert len(fold_history) == epochs
+
+ for epoch_entry in fold_history:
+ assert "epoch" in epoch_entry
+ assert "train_loss" in epoch_entry
+ assert "val_loss" in epoch_entry
+
+ print("Test Passed!")
+
+
+if __name__ == "__main__":
+ test_automl_history_tracking()
diff --git a/tests/test_cnn_quadtree_integration.py b/tests/test_cnn_quadtree_integration.py
new file mode 100644
index 0000000..d52a62d
--- /dev/null
+++ b/tests/test_cnn_quadtree_integration.py
@@ -0,0 +1,443 @@
+"""
+Integration test for CNN classifier with Quadtree segmentation.
+
+This test trains a CNN model with deterministic dummy data,
+then uses the trained model with the QuadtreeSegmentationModel
+to perform segmentation and verify expected results.
+"""
+
+import numpy as np
+import torch
+
+from auto_ml.implementations import QuadtreeSegmentationModel
+from auto_ml.implementations.classifiers.cnn import CNNModel
+from auto_ml.interfaces import (
+ ClassificationDatasetInterface,
+ SegmentationDatasetInterface,
+)
+
+
+def set_deterministic_seed(seed: int = 42) -> None:
+ """Set random seeds for reproducibility."""
+ np.random.seed(seed)
+ torch.manual_seed(seed)
+ if torch.cuda.is_available():
+ torch.cuda.manual_seed_all(seed)
+ # Make PyTorch operations deterministic
+ torch.backends.cudnn.deterministic = True
+ torch.backends.cudnn.benchmark = False
+
+
+def create_binary_training_data(
+ num_samples_per_class: int = 50,
+ min_size: int = 32,
+ max_size: int = 256,
+) -> tuple[list[np.ndarray], list[int]]:
+ """
+ Create deterministic training data for binary classification (2 classes).
+
+ Class 0: Dark images (low intensity 0-80)
+ Class 1: Bright images (high intensity 180-255)
+
+ Generates images of varying sizes to help the model generalize across scales.
+
+ Args:
+ num_samples_per_class: Number of samples per class.
+ min_size: Minimum image size.
+ max_size: Maximum image size.
+
+ Returns:
+ Tuple of (images list, labels list).
+
+ """
+ images = []
+ labels = []
+
+ rng = np.random.RandomState(42)
+
+ # Generate varied sizes between min_size and max_size
+ sizes = [32, 64, 128, 256]
+
+ # Class 0: Dark images (intensity range 0-80)
+ for i in range(num_samples_per_class):
+ size = sizes[i % len(sizes)]
+ # Use different base intensities for variety
+ base_intensity = 20 + (i % 10) * 5 # 20, 25, 30, ..., 65
+ img = np.full((size, size), base_intensity, dtype=np.uint8)
+ # Add small structured pattern
+ x, y = np.meshgrid(np.arange(size), np.arange(size))
+ pattern = (10 * np.sin((x + y) * 0.2 / size * 32 + i * 0.1)).astype(np.int16)
+ img = np.clip(img.astype(np.int16) + pattern, 0, 80).astype(np.uint8)
+ images.append(img)
+ labels.append(0)
+
+ # Class 1: Bright images (intensity range 180-255)
+ for i in range(num_samples_per_class):
+ size = sizes[i % len(sizes)]
+ base_intensity = 200 + (i % 10) * 5 # 200, 205, 210, ..., 245
+ img = np.full((size, size), base_intensity, dtype=np.uint8)
+ # Add different pattern
+ x, y = np.meshgrid(np.arange(size), np.arange(size))
+ pattern = (10 * np.cos((x - y) * 0.2 / size * 32 + i * 0.1)).astype(np.int16)
+ img = np.clip(img.astype(np.int16) + pattern, 180, 255).astype(np.uint8)
+ images.append(img)
+ labels.append(1)
+
+ # Shuffle deterministically
+ indices = rng.permutation(len(labels))
+ images = [images[i] for i in indices]
+ labels = [labels[i] for i in indices]
+
+ return images, labels
+
+
+def test_cnn_binary_classifier_training() -> None:
+ """Test that the CNN classifier can be trained for binary classification."""
+ print("Testing CNN binary classifier training with deterministic data...")
+
+ set_deterministic_seed(42)
+
+ # Create CNN model with 2 classes for binary classification
+ # Use num_blocks=5 to match original test expectations for 32×32 regions
+ cnn_model = CNNModel(
+ num_classes=2,
+ channels=1,
+ base_filters=16,
+ dropout=0.1,
+ num_blocks=5,
+ device="cpu",
+ )
+
+ # Create binary training data
+ images, labels = create_binary_training_data(
+ num_samples_per_class=50,
+ )
+
+ # Create dataset using the interface
+ dataset = ClassificationDatasetInterface()
+ for img, lbl in zip(images, labels):
+ dataset.add_sample(img, lbl)
+
+ # Train the model using the interface method
+ metrics = cnn_model.train(
+ dataset=dataset,
+ )
+
+ print(f"Final training loss: {metrics.loss:.4f}")
+ print(f"Final training accuracy: {metrics.accuracy:.4f}")
+ assert metrics.loss < 0.5, f"Training loss should decrease, got {metrics.loss}"
+
+ # Test classification on representative samples
+ # Dark region (class 0) - use intensity ~40
+ dark_region = np.full((32, 32), 40, dtype=np.uint8)
+ label_dark, conf_dark = cnn_model.classify(dark_region, 0, 0, 32, 32)
+ print(f"Dark region: class={label_dark}, confidence={conf_dark:.3f}")
+
+ # Bright region (class 1) - use intensity ~220
+ bright_region = np.full((32, 32), 220, dtype=np.uint8)
+ label_bright, conf_bright = cnn_model.classify(bright_region, 0, 0, 32, 32)
+ print(f"Bright region: class={label_bright}, confidence={conf_bright:.3f}")
+
+ # Verify that the model learned to distinguish the classes
+ assert label_dark == 0, f"Expected dark region to be class 0, got {label_dark}"
+ assert label_bright == 1, (
+ f"Expected bright region to be class 1, got {label_bright}"
+ )
+
+ print("CNN binary classifier training test PASSED!")
+
+
+def test_cnn_quadtree_binary_integration() -> None:
+ """
+ Integration test: Train binary CNN, use with QuadtreeSegmentationModel.
+
+ This test:
+ 1. Trains a binary CNN classifier on dummy data (dark vs bright)
+ 2. Creates a test image with 4 quadrants of different intensities
+ 3. Uses QuadtreeSegmentationModel with the trained CNN to segment the image
+ 4. Verifies the segmentation produces expected class labels
+ """
+ print("Testing CNN + Quadtree segmentation integration (binary)...")
+
+ set_deterministic_seed(42)
+
+ # 1. Create and train CNN model for binary classification
+ # Use num_blocks=5 to match original test expectations for 256×256 quadrants
+ print("Step 1: Creating and training binary CNN model...")
+ cnn_model = CNNModel(
+ num_classes=2,
+ channels=1,
+ base_filters=16,
+ dropout=0.1,
+ num_blocks=5,
+ device="cpu",
+ )
+
+ images, labels = create_binary_training_data(
+ num_samples_per_class=50,
+ )
+ dataset = ClassificationDatasetInterface()
+ for img, lbl in zip(images, labels):
+ dataset.add_sample(img, lbl)
+ metrics = cnn_model.train(
+ dataset=dataset,
+ )
+ print(f"Training complete. Final loss: {metrics.loss:.4f}")
+
+ # 2. Create test image with 4 quadrants (alternating dark and bright)
+ print("Step 2: Creating test image with 4 quadrants...")
+ image_size = 512
+ test_image = np.zeros((image_size, image_size), dtype=np.uint8)
+
+ # Top-left quadrant: Dark (class 0) - intensity ~40
+ test_image[0:256, 0:256] = 40
+ # Top-right quadrant: Bright (class 1) - intensity ~220
+ test_image[0:256, 256:512] = 220
+ # Bottom-left quadrant: Bright (class 1) - intensity ~220
+ test_image[256:512, 0:256] = 220
+ # Bottom-right quadrant: Dark (class 0) - intensity ~40
+ test_image[256:512, 256:512] = 40
+
+ # 3. Create QuadtreeSegmentationModel with the trained CNN
+ # Set threshold very high to force subdivision until min_region_size is reached
+ print("Step 3: Creating QuadtreeSegmentationModel...")
+ quadtree_model = QuadtreeSegmentationModel(
+ classifier=cnn_model,
+ classifier_dataset_dir=None,
+ threshold=1.0, # Force subdivision (no confidence can be >= 1.0)
+ min_region_size=256, # Stop at quadrant level (512/2 = 256)
+ max_depth=None, # No depth limit
+ )
+
+ # 4. Create dataset and evaluate
+ print("Step 4: Running segmentation...")
+ dummy_mask = np.zeros((image_size, image_size), dtype=np.uint8)
+ seg_dataset = SegmentationDatasetInterface()
+ seg_dataset.add_sample(test_image, dummy_mask)
+
+ mask_pairs = quadtree_model.evaluate(seg_dataset)
+
+ assert len(mask_pairs) == 1, f"Expected 1 mask pair, got {len(mask_pairs)}"
+ predicted_mask, _ = mask_pairs[0]
+
+ # 5. Verify segmentation results
+ print("Step 5: Verifying segmentation results...")
+
+ # Check each quadrant
+ top_left_class = int(np.median(predicted_mask[0:256, 0:256]))
+ top_right_class = int(np.median(predicted_mask[0:256, 256:512]))
+ bottom_left_class = int(np.median(predicted_mask[256:512, 0:256]))
+ bottom_right_class = int(np.median(predicted_mask[256:512, 256:512]))
+
+ print(f"Top-left quadrant (dark): class {top_left_class}")
+ print(f"Top-right quadrant (bright): class {top_right_class}")
+ print(f"Bottom-left quadrant (bright): class {bottom_left_class}")
+ print(f"Bottom-right quadrant (dark): class {bottom_right_class}")
+
+ # Build expected mask (checkerboard pattern)
+ expected_mask = np.zeros((image_size, image_size), dtype=np.uint8)
+ expected_mask[0:256, 0:256] = 0 # Dark -> class 0
+ expected_mask[0:256, 256:512] = 1 # Bright -> class 1
+ expected_mask[256:512, 0:256] = 1 # Bright -> class 1
+ expected_mask[256:512, 256:512] = 0 # Dark -> class 0
+
+ # Verify classifications
+ assert top_left_class == 0, f"Top-left should be class 0, got {top_left_class}"
+ assert top_right_class == 1, f"Top-right should be class 1, got {top_right_class}"
+ assert bottom_left_class == 1, (
+ f"Bottom-left should be class 1, got {bottom_left_class}"
+ )
+ assert bottom_right_class == 0, (
+ f"Bottom-right should be class 0, got {bottom_right_class}"
+ )
+
+ # Verify full mask matches expected
+ assert np.array_equal(predicted_mask, expected_mask), (
+ "Predicted mask does not match expected mask"
+ )
+
+ print("CNN + Quadtree segmentation integration test PASSED!")
+
+
+def test_cnn_quadtree_deeper_recursion() -> None:
+ """
+ Test Quadtree with deeper recursion using trained CNN.
+
+ This test verifies that the quadtree can recursively subdivide
+ regions when needed based on the classifier's confidence.
+ """
+ print("Testing CNN + Quadtree with deeper recursion...")
+
+ set_deterministic_seed(42)
+
+ # Create and train CNN model for binary classification
+ # Use num_blocks=5 for this test as it requires larger region classification
+ cnn_model = CNNModel(
+ num_classes=2,
+ channels=1,
+ base_filters=16,
+ dropout=0.1,
+ num_blocks=5,
+ device="cpu",
+ )
+
+ images, labels = create_binary_training_data(
+ num_samples_per_class=50,
+ )
+ dataset = ClassificationDatasetInterface()
+ for img, lbl in zip(images, labels):
+ dataset.add_sample(img, lbl)
+ cnn_model.train(dataset=dataset)
+
+ # Create test image with checkerboard pattern at 128x128 level
+ image_size = 512
+ test_image = np.zeros((image_size, image_size), dtype=np.uint8)
+
+ # Create a 4x4 grid of alternating dark (class 0) and bright (class 1) regions
+ block_size = 128
+ for i in range(4):
+ for j in range(4):
+ if (i + j) % 2 == 0:
+ test_image[
+ i * block_size : (i + 1) * block_size,
+ j * block_size : (j + 1) * block_size,
+ ] = 40 # Dark
+ else:
+ test_image[
+ i * block_size : (i + 1) * block_size,
+ j * block_size : (j + 1) * block_size,
+ ] = 220 # Bright
+
+ # Create QuadtreeSegmentationModel with smaller min_region_size
+ # This forces deeper recursion to classify the checkerboard pattern
+ quadtree_model = QuadtreeSegmentationModel(
+ classifier=cnn_model,
+ classifier_dataset_dir=None,
+ threshold=0.99, # High threshold to force recursion
+ min_region_size=128, # Stop at 128x128 level
+ max_depth=2, # Allow 2 levels of recursion (512 -> 256 -> 128)
+ )
+
+ # Evaluate
+ dummy_mask = np.zeros((image_size, image_size), dtype=np.uint8)
+ seg_dataset = SegmentationDatasetInterface()
+ seg_dataset.add_sample(test_image, dummy_mask)
+
+ mask_pairs = quadtree_model.evaluate(seg_dataset)
+ predicted_mask, _ = mask_pairs[0]
+
+ # Build expected checkerboard mask
+ expected_mask = np.zeros((image_size, image_size), dtype=np.uint8)
+ for i in range(4):
+ for j in range(4):
+ if (i + j) % 2 == 0:
+ expected_mask[
+ i * block_size : (i + 1) * block_size,
+ j * block_size : (j + 1) * block_size,
+ ] = 0 # Dark -> class 0
+ else:
+ expected_mask[
+ i * block_size : (i + 1) * block_size,
+ j * block_size : (j + 1) * block_size,
+ ] = 1 # Bright -> class 1
+
+ # Verify at least the pattern type is correct (checkerboard)
+ # Check a few specific blocks
+ assert predicted_mask[64, 64] == 0, "Block (0,0) should be class 0"
+ assert predicted_mask[64, 192] == 1, "Block (0,1) should be class 1"
+ assert predicted_mask[192, 64] == 1, "Block (1,0) should be class 1"
+ assert predicted_mask[192, 192] == 0, "Block (1,1) should be class 0"
+
+ print("CNN + Quadtree deeper recursion test PASSED!")
+
+
+def test_cnn_small_input_sizes() -> None:
+ """
+ Test that CNNModel supports small input sizes with configurable num_blocks.
+
+ This test verifies that:
+ 1. num_blocks=3 supports 8×8 minimum input
+ 2. num_blocks=2 supports 4×4 minimum input
+ 3. The model can classify small regions correctly
+ """
+ print("Testing CNN with small input sizes (configurable num_blocks)...")
+
+ set_deterministic_seed(42)
+
+ # Test num_blocks=3 (default, min 8×8)
+ print("Testing num_blocks=3 (min 8×8)...")
+ cnn_3_blocks = CNNModel(
+ num_classes=2,
+ channels=1,
+ base_filters=16,
+ dropout=0.1,
+ num_blocks=3,
+ device="cpu",
+ )
+
+ # Should work with 8×8 input
+ small_region_8x8 = np.full((8, 8), 128, dtype=np.uint8)
+ label, conf = cnn_3_blocks.classify(small_region_8x8, 0, 0, 8, 8)
+ print(f"8×8 region with num_blocks=3: class={label}, confidence={conf:.3f}")
+ assert isinstance(label, int), "Label should be int"
+ assert 0.0 <= conf <= 1.0, "Confidence should be in [0, 1]"
+
+ # Test num_blocks=2 (min 4×4)
+ print("Testing num_blocks=2 (min 4×4)...")
+ cnn_2_blocks = CNNModel(
+ num_classes=2,
+ channels=1,
+ base_filters=16,
+ dropout=0.1,
+ num_blocks=2,
+ device="cpu",
+ )
+
+ # Should work with 4×4 input
+ small_region_4x4 = np.full((4, 4), 128, dtype=np.uint8)
+ label, conf = cnn_2_blocks.classify(small_region_4x4, 0, 0, 4, 4)
+ print(f"4×4 region with num_blocks=2: class={label}, confidence={conf:.3f}")
+ assert isinstance(label, int), "Label should be int"
+ assert 0.0 <= conf <= 1.0, "Confidence should be in [0, 1]"
+
+ # Test num_blocks=5 (original behavior, min 32×32)
+ print("Testing num_blocks=5 (min 32×32)...")
+ cnn_5_blocks = CNNModel(
+ num_classes=2,
+ channels=1,
+ base_filters=16,
+ dropout=0.1,
+ num_blocks=5,
+ device="cpu",
+ )
+
+ # Should work with 32×32 input
+ small_region_32x32 = np.full((32, 32), 128, dtype=np.uint8)
+ label, conf = cnn_5_blocks.classify(small_region_32x32, 0, 0, 32, 32)
+ print(f"32×32 region with num_blocks=5: class={label}, confidence={conf:.3f}")
+ assert isinstance(label, int), "Label should be int"
+ assert 0.0 <= conf <= 1.0, "Confidence should be in [0, 1]"
+
+ print("CNN small input sizes test PASSED!")
+
+
+if __name__ == "__main__":
+ print("=" * 60)
+ print("Running CNN + Quadtree Integration Tests")
+ print("=" * 60)
+
+ test_cnn_binary_classifier_training()
+ print()
+
+ test_cnn_quadtree_binary_integration()
+ print()
+
+ test_cnn_quadtree_deeper_recursion()
+ print()
+
+ test_cnn_small_input_sizes()
+ print()
+
+ print("=" * 60)
+ print("All tests PASSED!")
+ print("=" * 60)
diff --git a/tests/test_dataset_loader.py b/tests/test_dataset_loader.py
new file mode 100644
index 0000000..74a7e74
--- /dev/null
+++ b/tests/test_dataset_loader.py
@@ -0,0 +1,85 @@
+import os
+import shutil
+from pathlib import Path
+
+import numpy as np
+from PIL import Image
+
+from auto_ml.implementations import load_dataset_from_directories
+
+
+def test_dataset_loader() -> None: # noqa: D103
+ print("Initializing Verification for Dataset Loader...")
+
+ # 1. Setup Temporary Directories
+ input_dir = Path("temp_input")
+ target_dir = Path("temp_target")
+ os.makedirs(input_dir, exist_ok=True)
+ os.makedirs(target_dir, exist_ok=True)
+
+ try:
+ # 2. Create Dummy Images
+ print("Creating dummy images...")
+ size = (512, 512)
+
+ # Sample 1: Red Dominant (Class 0)
+ img1 = Image.new("L", size, color=100)
+ img1.save(os.path.join(input_dir, "sample1.png"))
+
+ tgt1 = Image.new("RGB", size, color=(200, 50, 50)) # Red
+ tgt1.save(os.path.join(target_dir, "sample1_labeled.png"))
+
+ # Sample 2: Green Dominant (Class 1)
+ img2 = Image.new("L", size, color=150)
+ img2.save(os.path.join(input_dir, "sample2.jpg"))
+
+ tgt2 = Image.new("RGB", size, color=(50, 200, 50)) # Green
+ tgt2.save(os.path.join(target_dir, "sample2_labeled.png"))
+
+ # Sample 3: Misc/Background (Class 2)
+ img3 = Image.new("L", size, color=200)
+ img3.save(os.path.join(input_dir, "sample3.tiff"))
+
+ tgt3 = Image.new("RGB", size, color=(50, 50, 50)) # Dark
+ tgt3.save(os.path.join(target_dir, "sample3_labeled.png"))
+
+ # Unmatched Sample
+ img4 = Image.new("L", size, color=200)
+ img4.save(os.path.join(input_dir, "sample4.png"))
+
+ # 3. Test Loading
+ print("Loading dataset...")
+ dataset = load_dataset_from_directories(input_dir, target_dir)
+
+ print(f"Loaded {len(dataset)} samples.")
+ assert len(dataset) == 3, f"Expected 3 samples, got {len(dataset)}"
+
+ # 4. Verify Content
+ print("Verifying content...")
+
+ # Check Sample 1 (Red -> 0)
+ _, out1 = dataset[0]
+ unique1 = np.unique(out1.mask)
+ print(f"Sample 1 Unique Mask Values: {unique1}")
+ assert 0 in unique1, "Sample 1 should contain class 0 (Red)"
+
+ # Check Sample 2 (Green -> 1)
+ _, out2 = dataset[1]
+ unique2 = np.unique(out2.mask)
+ print(f"Sample 2 Unique Mask Values: {unique2}")
+ assert 1 in unique2, "Sample 2 should contain class 1 (Green)"
+
+ # Check Sample 3 (Background -> 2)
+ _, out3 = dataset[2]
+ unique3 = np.unique(out3.mask)
+ print(f"Sample 3 Unique Mask Values: {unique3}")
+ assert 2 in unique3, "Sample 3 should contain class 2 (Background)"
+
+ print("VERIFICATION SUCCESSFUL!")
+
+ finally:
+ # Cleanup
+ if os.path.exists(input_dir):
+ shutil.rmtree(input_dir)
+ if os.path.exists(target_dir):
+ shutil.rmtree(target_dir)
diff --git a/tests/test_example.py b/tests/test_example.py
index 65277cf..00d408f 100644
--- a/tests/test_example.py
+++ b/tests/test_example.py
@@ -1,4 +1,6 @@
-from src.core.example import add_two_numbers
+def add_two_numbers(a: int, b: int) -> int:
+ """Add two numbers."""
+ return a + b
def test_add_two_numbers() -> None:
diff --git a/tests/test_flip_augmentators.py b/tests/test_flip_augmentators.py
new file mode 100644
index 0000000..2a59f89
--- /dev/null
+++ b/tests/test_flip_augmentators.py
@@ -0,0 +1,127 @@
+"""Test flip augmentators to ensure they produce contiguous arrays."""
+
+import numpy as np
+
+from auto_ml.implementations.augmentators.geometric import (
+ HorizontalFlipAugmentator,
+ VerticalFlipAugmentator,
+)
+from auto_ml.interfaces import SegmentationDatasetInterface
+
+
+def test_horizontal_flip_produces_contiguous_arrays() -> None:
+ """Test that HorizontalFlipAugmentator produces C-contiguous arrays."""
+ # Create a simple dataset (512x512 as required by interface)
+ image = np.random.randint(0, 255, (512, 512, 3), dtype=np.uint8)
+ mask = np.random.randint(0, 2, (512, 512), dtype=np.uint8)
+ dataset = SegmentationDatasetInterface()
+ dataset.add_sample(image, mask)
+ # Apply horizontal flip
+ augmentator = HorizontalFlipAugmentator()
+ augmented = augmentator.augment(dataset)
+ # Get augmented samples
+ aug_image, aug_mask = augmented.samples[0]
+ # Check that arrays are C-contiguous
+ assert aug_image.flags['C_CONTIGUOUS'], "Augmented image is not C-contiguous"
+ assert aug_mask.flags['C_CONTIGUOUS'], "Augmented mask is not C-contiguous"
+ # Check that strides are all positive
+ assert all(s >= 0 for s in aug_image.strides), "Image has negative strides"
+ assert all(s >= 0 for s in aug_mask.strides), "Mask has negative strides"
+ # Verify flip was actually applied (compare to manual flip)
+ expected_image = np.ascontiguousarray(np.fliplr(image))
+ expected_mask = np.ascontiguousarray(np.fliplr(mask))
+ assert np.array_equal(aug_image, expected_image), "Image flip incorrect"
+ assert np.array_equal(aug_mask, expected_mask), "Mask flip incorrect"
+ print(
+ "✓ HorizontalFlipAugmentator produces contiguous arrays with positive strides",
+ )
+
+
+def test_vertical_flip_produces_contiguous_arrays() -> None:
+ """Test that VerticalFlipAugmentator produces C-contiguous arrays."""
+ # Create a simple dataset (512x512 as required by interface)
+ image = np.random.randint(0, 255, (512, 512, 3), dtype=np.uint8)
+ mask = np.random.randint(0, 2, (512, 512), dtype=np.uint8)
+ dataset = SegmentationDatasetInterface()
+ dataset.add_sample(image, mask)
+ # Apply vertical flip
+ augmentator = VerticalFlipAugmentator()
+ augmented = augmentator.augment(dataset)
+ # Get augmented samples
+ aug_image, aug_mask = augmented.samples[0]
+ # Check that arrays are C-contiguous
+ assert aug_image.flags['C_CONTIGUOUS'], "Augmented image is not C-contiguous"
+ assert aug_mask.flags['C_CONTIGUOUS'], "Augmented mask is not C-contiguous"
+ # Check that strides are all positive
+ assert all(s >= 0 for s in aug_image.strides), "Image has negative strides"
+ assert all(s >= 0 for s in aug_mask.strides), "Mask has negative strides"
+ # Verify flip was actually applied (compare to manual flip)
+ expected_image = np.ascontiguousarray(np.flipud(image))
+ expected_mask = np.ascontiguousarray(np.flipud(mask))
+ assert np.array_equal(aug_image, expected_image), "Image flip incorrect"
+ assert np.array_equal(aug_mask, expected_mask), "Mask flip incorrect"
+ print("✓ VerticalFlipAugmentator produces contiguous arrays with positive strides")
+
+
+def test_flips_with_multiple_samples() -> None:
+ """Test flip augmentators with multiple samples."""
+ # Create dataset with multiple samples (512x512 as required)
+ dataset = SegmentationDatasetInterface()
+ for _ in range(10):
+ image = np.random.randint(0, 255, (512, 512, 3), dtype=np.uint8)
+ mask = np.random.randint(0, 2, (512, 512), dtype=np.uint8)
+ dataset.add_sample(image, mask)
+ # Test horizontal flip
+ h_augmentator = HorizontalFlipAugmentator()
+ h_augmented = h_augmentator.augment(dataset)
+ for aug_image, aug_mask in h_augmented.samples:
+ assert aug_image.flags['C_CONTIGUOUS'], "H-flip: Image not C-contiguous"
+ assert aug_mask.flags['C_CONTIGUOUS'], "H-flip: Mask not C-contiguous"
+ assert all(
+ s >= 0 for s in aug_image.strides
+ ), "H-flip: Image has negative strides"
+ assert all(
+ s >= 0 for s in aug_mask.strides
+ ), "H-flip: Mask has negative strides"
+ # Test vertical flip
+ v_augmentator = VerticalFlipAugmentator()
+ v_augmented = v_augmentator.augment(dataset)
+ for aug_image, aug_mask in v_augmented.samples:
+ assert aug_image.flags['C_CONTIGUOUS'], "V-flip: Image not C-contiguous"
+ assert aug_mask.flags['C_CONTIGUOUS'], "V-flip: Mask not C-contiguous"
+ assert all(
+ s >= 0 for s in aug_image.strides
+ ), "V-flip: Image has negative strides"
+ assert all(
+ s >= 0 for s in aug_mask.strides
+ ), "V-flip: Mask has negative strides"
+ print("✓ Both flip augmentators work correctly with multiple samples")
+
+
+def test_flip_preserves_dtype() -> None:
+ """Test that flip augmentators preserve data types."""
+ image = np.random.randint(0, 255, (512, 512, 3), dtype=np.uint8)
+ mask = np.random.randint(0, 2, (512, 512), dtype=np.uint8)
+ dataset = SegmentationDatasetInterface()
+ dataset.add_sample(image, mask)
+ # Test horizontal flip
+ h_aug = HorizontalFlipAugmentator()
+ h_result = h_aug.augment(dataset)
+ h_img, h_mask = h_result.samples[0]
+ assert h_img.dtype == np.uint8, f"H-flip: Image dtype changed to {h_img.dtype}"
+ assert h_mask.dtype == np.uint8, f"H-flip: Mask dtype changed to {h_mask.dtype}"
+ # Test vertical flip
+ v_aug = VerticalFlipAugmentator()
+ v_result = v_aug.augment(dataset)
+ v_img, v_mask = v_result.samples[0]
+ assert v_img.dtype == np.uint8, f"V-flip: Image dtype changed to {v_img.dtype}"
+ assert v_mask.dtype == np.uint8, f"V-flip: Mask dtype changed to {v_mask.dtype}"
+ print("✓ Flip augmentators preserve data types")
+
+
+if __name__ == "__main__":
+ test_horizontal_flip_produces_contiguous_arrays()
+ test_vertical_flip_produces_contiguous_arrays()
+ test_flips_with_multiple_samples()
+ test_flip_preserves_dtype()
+ print("\n✅ All flip augmentator tests passed!")
diff --git a/tests/test_mask_evaluator.py b/tests/test_mask_evaluator.py
new file mode 100644
index 0000000..fb771dc
--- /dev/null
+++ b/tests/test_mask_evaluator.py
@@ -0,0 +1,111 @@
+"""Tests for AutoencoderMaskEvaluator and UMAPMaskEvaluator."""
+
+import numpy as np
+import pytest
+
+from auto_ml.implementations import AutoencoderMaskEvaluator
+from auto_ml.models.maskautoencoder.maskautoencoder import MaskAutoencoder
+
+
+class TestMaskAutoencoder:
+ """Tests for the MaskAutoencoder class."""
+
+ def test_encoder_output_shape(self) -> None:
+ """Test that encoder produces correct 3D output."""
+ import torch
+
+ model = MaskAutoencoder(latent_dim=3)
+
+ x = torch.randn(1, 1, 512, 512)
+ z = model.encode(x)
+
+ assert z.shape == (1, 3), f"Expected (1, 3), got {z.shape}"
+
+ def test_decoder_output_shape(self) -> None:
+ """Test that decoder reconstructs to original dimensions."""
+ import torch
+
+ model = MaskAutoencoder(latent_dim=3)
+
+ z = torch.randn(1, 3)
+ recon = model.decode(z)
+
+ expected_shape = (1, 1, 512, 512)
+ assert recon.shape == expected_shape, (
+ f"Expected {expected_shape}, got {recon.shape}"
+ )
+
+ def test_forward_pass(self) -> None:
+ """Test full forward pass returns reconstruction and latent."""
+ import torch
+
+ model = MaskAutoencoder(latent_dim=3)
+
+ x = torch.randn(1, 1, 512, 512)
+ recon, z = model(x)
+
+ assert recon.shape == x.shape
+ assert z.shape == (1, 3)
+
+
+class TestAutoencoderMaskEvaluator:
+ """Tests for the AutoencoderMaskEvaluator class."""
+
+ @pytest.fixture
+ def sample_mask_pairs(self) -> list:
+ """Create sample mask pairs for testing."""
+ pairs = []
+ for i in range(5):
+ # Create similar predicted and real masks
+ pred_mask = np.zeros((512, 512), dtype=np.uint8)
+ real_mask = np.zeros((512, 512), dtype=np.uint8)
+
+ # Draw circles
+ center = (256 + i * 5, 256 + i * 5)
+ radius = 100 + i * 10
+ y, x = np.ogrid[:512, :512]
+ dist = np.sqrt((x - center[0]) ** 2 + (y - center[1]) ** 2)
+ pred_mask[dist <= radius] = 1
+ real_mask[dist <= radius + 5] = 1 # Slightly larger
+
+ pairs.append((pred_mask, real_mask))
+ return [pairs] # List[List[MaskPair]]
+
+ def test_initialization(self, sample_mask_pairs: list) -> None:
+ """Test evaluator initializes correctly."""
+ real_masks = [pair[1] for pair in sample_mask_pairs[0]]
+ evaluator = AutoencoderMaskEvaluator(
+ reference_masks=real_masks,
+ device="cpu",
+ epochs=2,
+ )
+ assert evaluator.latent_dim == 3
+ assert evaluator.epochs == 2
+
+ def test_evaluate_returns_float(self, sample_mask_pairs: list) -> None:
+ """Test that evaluate returns a float ratio."""
+ real_masks = [pair[1] for pair in sample_mask_pairs[0]]
+ evaluator = AutoencoderMaskEvaluator(
+ reference_masks=real_masks,
+ device="cpu",
+ epochs=5, # Few epochs for speed
+ )
+
+ result = evaluator.evaluate(sample_mask_pairs)
+
+ assert isinstance(result, float)
+ assert 0.0 <= result <= 1.0
+
+ def test_get_embeddings(self, sample_mask_pairs: list) -> None:
+ """Test get_embeddings returns correct shape."""
+ real_masks = [pair[1] for pair in sample_mask_pairs[0]]
+ evaluator = AutoencoderMaskEvaluator(
+ reference_masks=real_masks,
+ device="cpu",
+ epochs=5,
+ )
+
+ # Get embeddings for real masks
+ embeddings = evaluator.get_embeddings(real_masks)
+
+ assert embeddings.shape == (5, 3)
diff --git a/tests/test_pipeline.py b/tests/test_pipeline.py
new file mode 100644
index 0000000..f5d5c52
--- /dev/null
+++ b/tests/test_pipeline.py
@@ -0,0 +1,65 @@
+from pathlib import Path
+
+from auto_ml.implementations import (
+ AccuracyEvaluator,
+ DataAugmentatorNode,
+ EvaluatorNode,
+ IdentityAugmentator,
+ ModelNode,
+ ViTModel,
+ load_dataset_from_directories,
+)
+
+
+def test_pipeline() -> None: # noqa: D103
+ print("=== Starting Full Pipeline Verification ===")
+
+ # Paths
+ base_dir = Path(".")
+ input_dir = base_dir / "vega_3_tescan_unlabeled_images"
+ target_dir = base_dir / "vega_3_tescan_labeled_images"
+
+ # 1. Load Dataset
+ print("\n--- Step 1: Loading Dataset ---")
+ dataset = load_dataset_from_directories(input_dir, target_dir)
+
+ if len(dataset) == 0:
+ print("Error: No data loaded. Check paths and matching logic.")
+ return
+
+ # 2. Data Augmentation Node (Splitting)
+ print("\n--- Step 2: Data Augmentation Node (Splitting & Augmenting) ---")
+ # Using small k=2 for speed verification
+ augmentator = IdentityAugmentator()
+ data_node = DataAugmentatorNode(augmentator=augmentator, k_folds=2, random_seed=123)
+
+ dataset_pairs = data_node.process(dataset)
+ print(f"Generated {len(dataset_pairs)} dataset pairs (folds).")
+
+ # 3. Model Node (Training)
+ print("\n--- Step 3: Model Node (Training) ---")
+ # ViTModel with small epochs for verification
+ # Using 'cpu' or 'mps' if available.
+ # Force cpu for CI-like stability if needed, but let's try auto.
+ model = ViTModel(epochs=1, batch_size=2, device="cpu")
+ model_node = ModelNode(model=model)
+
+ # ModelNode.train() now returns List[List[MaskPair]]
+ mask_pairs_result = model_node.train(dataset_pairs)
+
+ print("\n--- Step 4: Verification Results ---")
+ print(f"Number of folds: {len(mask_pairs_result)}")
+ for i, fold_pairs in enumerate(mask_pairs_result):
+ print(f"Fold {i + 1}: {len(fold_pairs)} mask pairs")
+ assert len(fold_pairs) > 0, f"Fold {i + 1} should have mask pairs"
+
+ # 4. Optional: Run evaluator on mask pairs
+ evaluator_node = EvaluatorNode(
+ evaluators={"accuracy": AccuracyEvaluator()},
+ name="TestEvaluator",
+ )
+ eval_results = evaluator_node.evaluate(mask_pairs_result)
+ print(f"Evaluation results: {eval_results}")
+ assert "accuracy" in eval_results, "Should have accuracy result"
+
+ print("\n=== PIPELINE VERIFICATION SUCCESSFUL! ===")
diff --git a/tests/test_quadtree_segmentation.py b/tests/test_quadtree_segmentation.py
new file mode 100644
index 0000000..fa3387c
--- /dev/null
+++ b/tests/test_quadtree_segmentation.py
@@ -0,0 +1,304 @@
+import numpy as np
+
+from auto_ml.implementations import QuadtreeSegmentationModel
+from auto_ml.interfaces import (
+ ClassificationDatasetInterface,
+ ClassificationModelInterface,
+ ImageArray,
+ MetricsResultInterface,
+ SegmentationDatasetInterface,
+)
+
+
+class DummyClassifier(ClassificationModelInterface):
+ """
+ A dummy classifier for testing QuadtreeSegmentationModel.
+
+ Classifies regions based on the dominant color within the region.
+ The confidence is the proportion of the dominant color's pixels.
+ Assumes image is grayscale for simplicity or takes the average color.
+ """
+
+ def classify(
+ self,
+ image: ImageArray,
+ x: int,
+ y: int,
+ width: int,
+ height: int,
+ ) -> tuple[int, float]:
+ """
+ Classify an image region based on dominant color.
+
+ Args:
+ image: Image as a numpy array (H, W, C or H, W).
+ x: x-coordinate of the region (top-left).
+ y: y-coordinate of the region (top-left).
+ width: Width of the region.
+ height: Height of the region.
+
+ Returns:
+ Tuple of (class_label, confidence),
+ where class_label is the dominant color value and confidence
+ is the proportion of pixels with that color.
+
+ """
+ region = image[y : y + height, x : x + width]
+
+ if region.size == 0:
+ return 0, 0.0 # Default if region is empty
+
+ # For a simple dummy, let's just use the average pixel value in the region
+ # and map it to a class.
+ if region.ndim == 3:
+ # If it's a color image, convert to grayscale average for simplicity
+ region_flat = np.mean(region, axis=2).flatten()
+ else:
+ region_flat = region.flatten()
+
+ # Count occurrences of each unique pixel value
+ _, counts = np.unique(region_flat, return_counts=True)
+
+ # Find the dominant pixel
+ dominant_count = np.max(counts)
+
+ # Confidence is the proportion of the dominant pixel
+ confidence = dominant_count / region_flat.size
+
+ # Map average brightness to 3 classes (0, 1, 2) for this dummy.
+ # This assumes input images will have varying brightness
+ # levels that map to these classes.
+ avg_brightness = np.mean(region_flat)
+ if avg_brightness < 85:
+ class_label = 0
+ elif avg_brightness < 170:
+ class_label = 1
+ else:
+ class_label = 2
+
+ return class_label, confidence
+
+ def train(
+ self,
+ dataset: ClassificationDatasetInterface,
+ ) -> MetricsResultInterface:
+ """Train the model on the provided dataset."""
+ return MetricsResultInterface()
+
+ def evaluate(
+ self,
+ dataset: ClassificationDatasetInterface,
+ ) -> MetricsResultInterface:
+ """Evaluate the model on the provided dataset."""
+ return MetricsResultInterface()
+
+
+def test_quadtree_segmentation_init() -> None:
+ """Test the initialization of QuadtreeSegmentationModel."""
+ print("Initializing Verification for QuadtreeSegmentationModel initialization...")
+ classifier = DummyClassifier()
+ model = QuadtreeSegmentationModel(
+ classifier=classifier,
+ classifier_dataset_dir=None,
+ threshold=0.7,
+ )
+
+ assert model.classifier == classifier
+ assert model.threshold == 0.7
+ assert model.min_region_size == 1
+ assert model.max_depth is None
+ print("QuadtreeSegmentationModel initialization VERIFICATION SUCCESSFUL!")
+
+
+def test_quadtree_segmentation_evaluate() -> None:
+ """Test the evaluate method and recursive segmentation logic."""
+ print("Initializing Verification for QuadtreeSegmentationModel evaluate method...")
+
+ # Setup dummy data with distinct quadrants
+
+ image_size = 512
+
+ dummy_image = np.zeros((image_size, image_size), dtype=np.uint8) # Grayscale image
+
+ # Create four distinct quadrants with different average brightness
+ # These brightness values should map to different class labels (0, 1, 2)
+ # in DiagonalDummyClassifier (avg_brightness < 85 -> 0, < 170 -> 1, else -> 2)
+ # Top-left (0): avg_brightness ~ 40 -> class 0
+ dummy_image[0 : image_size // 2, 0 : image_size // 2] = 40
+ # Top-right (1): avg_brightness ~ 120 -> class 1
+ dummy_image[0 : image_size // 2, image_size // 2 : image_size] = 120
+ # Bottom-left (2): avg_brightness ~ 200 -> class 2
+ dummy_image[image_size // 2 : image_size, 0 : image_size // 2] = 200
+ # Bottom-right (0): avg_brightness ~ 60 -> class 0
+ dummy_image[image_size // 2 : image_size, image_size // 2 : image_size] = 60
+
+ dummy_real_mask = np.zeros(
+ (image_size, image_size),
+ dtype=np.uint8,
+ ) # Not used by dummy classifier
+
+ dataset = SegmentationDatasetInterface()
+ dataset.add_sample(dummy_image, dummy_real_mask)
+
+ # Classifier and Model setup
+ classifier = DummyClassifier()
+
+ # Set threshold to a high value to force recursion until min_region_size
+ # Set min_region_size to allow clear quadrant segmentation, e.g., 256
+ model = QuadtreeSegmentationModel(
+ classifier=classifier,
+ classifier_dataset_dir=None,
+ threshold=0.99,
+ min_region_size=image_size // 2, # 256
+ )
+
+ # Evaluate
+ mask_pairs = model.evaluate(dataset)
+ assert len(mask_pairs) == 1
+
+ predicted_mask, real_mask = mask_pairs[0]
+ assert predicted_mask.shape == (image_size, image_size)
+ assert np.array_equal(real_mask, dummy_real_mask) # Real mask should be unchanged
+
+ # Expected mask based on the dummy_image and DiagonalDummyClassifier logic:
+ expected_mask = np.zeros((image_size, image_size), dtype=np.uint8)
+ expected_mask[0 : image_size // 2, 0 : image_size // 2] = 0 # 40 -> class 0
+ expected_mask[0 : image_size // 2, image_size // 2 : image_size] = (
+ 1 # 120 -> class 1
+ )
+ expected_mask[image_size // 2 : image_size, 0 : image_size // 2] = (
+ 2 # 200 -> class 2
+ )
+ expected_mask[image_size // 2 : image_size, image_size // 2 : image_size] = (
+ 0 # 60 -> class 0
+ )
+ assert np.array_equal(predicted_mask, expected_mask)
+ print("QuadtreeSegmentationModel evaluate method VERIFICATION SUCCESSFUL!")
+
+
+def test__should_stop_recursion() -> None:
+ """Test the _should_stop_recursion utility method."""
+ print("Initializing Verification for _should_stop_recursion method...")
+
+ classifier = DummyClassifier()
+ model = QuadtreeSegmentationModel(
+ classifier=classifier,
+ classifier_dataset_dir=None,
+ threshold=0.7,
+ min_region_size=10,
+ max_depth=3,
+ )
+
+ # Case 1: Confidence meets threshold
+ assert (
+ model._should_stop_recursion(confidence=0.8, width=20, height=20, depth=1)
+ is True
+ )
+
+ # Case 2: Width meets min_region_size
+ assert (
+ model._should_stop_recursion(confidence=0.6, width=5, height=20, depth=1)
+ is True
+ )
+
+ # Case 3: Height meets min_region_size
+ assert (
+ model._should_stop_recursion(confidence=0.6, width=20, height=5, depth=1)
+ is True
+ )
+
+ # Case 4: Max depth reached
+ assert (
+ model._should_stop_recursion(confidence=0.6, width=20, height=20, depth=3)
+ is True
+ )
+
+ # Case 5: No stop condition met (should return False)
+ assert (
+ model._should_stop_recursion(confidence=0.6, width=20, height=20, depth=1)
+ is False
+ )
+
+ # Case 6: max_depth is None, so depth should not stop recursion
+ model_no_max_depth = QuadtreeSegmentationModel(
+ classifier=classifier,
+ classifier_dataset_dir=None,
+ threshold=0.7,
+ min_region_size=10,
+ max_depth=None,
+ )
+ assert (
+ model_no_max_depth._should_stop_recursion(
+ confidence=0.6,
+ width=20,
+ height=20,
+ depth=100,
+ )
+ is False
+ )
+
+ print("_should_stop_recursion method VERIFICATION SUCCESSFUL!")
+
+
+def test_quadtree_hyperparameter_tuning() -> None:
+ """Test that hyperparameter tuning runs without errors."""
+ print(
+ "Initializing check for QuadtreeSegmentationModel hyperparameter tuning...",
+ )
+
+ # Dummy dataset: one simple image
+ image_size = 512
+ dummy_image = np.zeros((image_size, image_size), dtype=np.uint8)
+ dummy_real_mask = np.zeros((image_size, image_size), dtype=np.uint8)
+
+ dataset = SegmentationDatasetInterface()
+ dataset.add_sample(dummy_image, dummy_real_mask)
+
+ # Dummy classifier
+ classifier = DummyClassifier()
+
+ # Define a custom search space
+ custom_search_space = {
+ "threshold": (0.1, 0.9),
+ "min_region_size": (4, 32),
+ "max_depth": (2, 5),
+ }
+
+ # Initialize quadtree with hyperparameter tuning enabled and SA config
+ model = QuadtreeSegmentationModel(
+ classifier=classifier,
+ classifier_dataset_dir=None, # Dataset not needed for DummyClassifier.train
+ threshold=0.5,
+ min_region_size=8,
+ max_depth=4,
+ optimize_metric="accuracy", # activamos hyperparameter tuning
+ search_space=custom_search_space,
+ n_trials=5, # Small number of trials for testing speed
+ )
+
+ # Ejecutar entrenamiento / hyperparameter tuning
+ metrics_result = model.train(dataset)
+
+ # Verificaciones
+ assert isinstance(metrics_result, MetricsResultInterface), (
+ "train must return MetricsResultInterface"
+ )
+ assert model.threshold is not None, "threshold should be set after tuning"
+ assert model.min_region_size is not None, (
+ "min_region_size should be set after tuning"
+ )
+
+ # Check that tuned values are within ranges
+ assert 0.1 <= model.threshold <= 0.9
+ assert 4 <= model.min_region_size <= 32
+ if model.max_depth is not None:
+ # Note: upper bound is inclusive in random.randint but range max is
+ # usually exclusive in python slicing.
+ # My implementation uses randint(min, max+1) from ranges, implying max is
+ # inclusive. So checking <= 5 is correct if range was (2, 5).
+ assert 2 <= model.max_depth <= 5
+
+ print("Hyperparameter tuning changes:")
+ print("Original:", model.threshold, model.min_region_size, model.max_depth)
+ print("Tuned:", model.threshold, model.min_region_size, model.max_depth)
+ print("Hyperparameter tuning VERIFICATION SUCCESSFUL!")
diff --git a/tests/test_segmentation_evaluators.py b/tests/test_segmentation_evaluators.py
new file mode 100644
index 0000000..14c631d
--- /dev/null
+++ b/tests/test_segmentation_evaluators.py
@@ -0,0 +1,574 @@
+"""Tests for segmentation evaluator implementations."""
+
+import numpy as np
+import pytest
+
+from auto_ml.implementations.evaluators.dice import (
+ DiceClass0Evaluator,
+ DiceClass1Evaluator,
+ DiceClass2Evaluator,
+ DiceMacroAverageEvaluator,
+ DiceWeightedAverageEvaluator,
+)
+from auto_ml.implementations.evaluators.iou import (
+ # IoUClass0Evaluator,
+ IoUClass1Evaluator,
+ IoUClass2Evaluator,
+ IoUMacroAverageEvaluator,
+ IoUWeightedAverageEvaluator,
+)
+from auto_ml.implementations.evaluators.precision import (
+ PrecisionClass0Evaluator,
+ PrecisionClass1Evaluator,
+ PrecisionClass2Evaluator,
+ PrecisionMacroAverageEvaluator,
+)
+from auto_ml.implementations.evaluators.recall import (
+ RecallClass0Evaluator,
+ RecallClass1Evaluator,
+ RecallClass2Evaluator,
+ RecallMacroAverageEvaluator,
+)
+
+
+class TestSegmentationEvaluators:
+ """Tests for segmentation evaluator implementations."""
+
+ @pytest.fixture
+ def perfect_match_masks(self) -> list:
+ """Create identical predicted and real masks for perfect match test."""
+ mask = np.zeros((512, 512), dtype=np.uint8)
+ # Class 0: top third
+ mask[:170, :] = 0
+ # Class 1: middle third
+ mask[170:340, :] = 1
+ # Class 2: bottom third
+ mask[340:, :] = 2
+
+ pairs = [(mask.copy(), mask.copy())]
+ return [pairs] # List[List[MaskPair]]
+
+ @pytest.fixture
+ def zero_overlap_masks(self) -> list:
+ """Create masks with no overlap for zero overlap test."""
+ pred_mask = np.zeros((512, 512), dtype=np.uint8)
+ real_mask = np.zeros((512, 512), dtype=np.uint8)
+
+ # Predicted: left half is class 1
+ pred_mask[:, :256] = 1
+
+ # Real: right half is class 1
+ real_mask[:, 256:] = 1
+
+ pairs = [(pred_mask, real_mask)]
+ return [pairs]
+
+ @pytest.fixture
+ def sample_mask_pairs(self) -> list:
+ """Create sample 3-class segmentation masks for testing."""
+ pairs = []
+
+ np.random.seed(42) # Deterministic noise
+ for i in range(3): # 3 samples
+ pred_mask = np.zeros((512, 512), dtype=np.uint8)
+ real_mask = np.zeros((512, 512), dtype=np.uint8)
+
+ # Class 0: Background (top section)
+ real_mask[:170, :] = 0
+ pred_mask[:170, :] = 0
+
+ # Class 1: Material phase 1 (middle section)
+ real_mask[170:340, :] = 1
+ pred_mask[170:340, :] = 1
+
+ # Class 2: Material phase 2 (bottom section)
+ real_mask[340:, :] = 2
+ pred_mask[340:, :] = 2
+
+ # Add some noise to predictions (5% random errors)
+ noise_mask = np.random.rand(512, 512) > 0.95
+ pred_mask[noise_mask] = (pred_mask[noise_mask] + 1) % 3
+
+ pairs.append((pred_mask, real_mask))
+
+ return [pairs] # List[List[MaskPair]]
+
+ @pytest.fixture
+ def class_never_appears_masks(self) -> list:
+ """Create masks where class 2 never appears."""
+ mask = np.zeros((512, 512), dtype=np.uint8)
+ # Only classes 0 and 1
+ mask[:256, :] = 0
+ mask[256:, :] = 1
+
+ pairs = [(mask.copy(), mask.copy())]
+ return [pairs]
+
+ @pytest.fixture
+ def imbalanced_masks(self) -> list:
+ """Create masks with highly imbalanced classes."""
+ pred_mask = np.zeros((100, 100), dtype=np.uint8)
+ real_mask = np.zeros((100, 100), dtype=np.uint8)
+
+ # Class 0: 90% of pixels
+ real_mask[:, :] = 0
+ pred_mask[:, :] = 0
+
+ # Class 1: 9% of pixels
+ real_mask[:30, :30] = 1
+ pred_mask[:30, :30] = 1
+
+ # Class 2: 1% of pixels
+ real_mask[:10, :10] = 2
+ pred_mask[:10, :10] = 2
+
+ pairs = [(pred_mask, real_mask)]
+ return [pairs]
+
+ # ========================================================================
+ # IoU Tests
+ # ========================================================================
+
+ # def test_iou_class0_perfect_match(self, perfect_match_masks: list) -> None:
+ # """Test IoU returns 1.0 for perfect predictions.""" # noqa: ERA001
+ # evaluator = IoUClass0Evaluator() # noqa: ERA001
+ # result = evaluator.evaluate(perfect_match_masks) # noqa: ERA001
+ #
+ # assert isinstance(result, float) # noqa: ERA001
+ # assert result == 1.0 # noqa: ERA001
+
+ def test_iou_class1_perfect_match(self, perfect_match_masks: list) -> None:
+ """Test IoU returns 1.0 for perfect predictions."""
+ evaluator = IoUClass1Evaluator()
+ result = evaluator.evaluate(perfect_match_masks)
+
+ assert isinstance(result, float)
+ assert result == 1.0
+
+ def test_iou_class2_perfect_match(self, perfect_match_masks: list) -> None:
+ """Test IoU returns 1.0 for perfect predictions."""
+ evaluator = IoUClass2Evaluator()
+ result = evaluator.evaluate(perfect_match_masks)
+
+ assert isinstance(result, float)
+ assert result == 1.0
+
+ def test_iou_class1_zero_overlap(self, zero_overlap_masks: list) -> None:
+ """Test IoU returns 0.0 when there's no overlap for class 1."""
+ evaluator = IoUClass1Evaluator()
+ result = evaluator.evaluate(zero_overlap_masks)
+
+ assert isinstance(result, float)
+ assert result == 0.0
+
+ def test_iou_class2_never_appears(self, class_never_appears_masks: list) -> None:
+ """Test IoU returns 0.0 when class 2 never appears."""
+ evaluator = IoUClass2Evaluator()
+ result = evaluator.evaluate(class_never_appears_masks)
+
+ assert isinstance(result, float)
+ assert result == 0.0
+
+ def test_iou_macro_average_returns_float(
+ self, sample_mask_pairs: list,
+ ) -> None:
+ """Test IoU macro average returns float."""
+ evaluator = IoUMacroAverageEvaluator()
+ result = evaluator.evaluate(sample_mask_pairs)
+
+ assert isinstance(result, float)
+ assert 0.0 <= result <= 1.0
+
+ def test_iou_weighted_average_returns_float(
+ self, sample_mask_pairs: list,
+ ) -> None:
+ """Test IoU weighted average returns float."""
+ evaluator = IoUWeightedAverageEvaluator()
+ result = evaluator.evaluate(sample_mask_pairs)
+
+ assert isinstance(result, float)
+ assert 0.0 <= result <= 1.0
+
+ def test_iou_macro_vs_weighted_differ_with_imbalance(
+ self, imbalanced_masks: list,
+ ) -> None:
+ """Test macro and weighted IoU can differ with class imbalance."""
+ macro_eval = IoUMacroAverageEvaluator()
+ weighted_eval = IoUWeightedAverageEvaluator()
+
+ macro_result = macro_eval.evaluate(imbalanced_masks)
+ weighted_result = weighted_eval.evaluate(imbalanced_masks)
+
+ # Both should be valid floats
+ assert isinstance(macro_result, float)
+ assert isinstance(weighted_result, float)
+ assert 0.0 <= macro_result <= 1.0
+ assert 0.0 <= weighted_result <= 1.0
+
+ # ========================================================================
+ # Dice Tests
+ # ========================================================================
+
+ def test_dice_class0_perfect_match(self, perfect_match_masks: list) -> None:
+ """Test Dice returns 1.0 for perfect predictions."""
+ evaluator = DiceClass0Evaluator()
+ result = evaluator.evaluate(perfect_match_masks)
+
+ assert isinstance(result, float)
+ assert result == 1.0
+
+ def test_dice_class1_perfect_match(self, perfect_match_masks: list) -> None:
+ """Test Dice returns 1.0 for perfect predictions."""
+ evaluator = DiceClass1Evaluator()
+ result = evaluator.evaluate(perfect_match_masks)
+
+ assert isinstance(result, float)
+ assert result == 1.0
+
+ def test_dice_class2_perfect_match(self, perfect_match_masks: list) -> None:
+ """Test Dice returns 1.0 for perfect predictions."""
+ evaluator = DiceClass2Evaluator()
+ result = evaluator.evaluate(perfect_match_masks)
+
+ assert isinstance(result, float)
+ assert result == 1.0
+
+ def test_dice_class1_zero_overlap(self, zero_overlap_masks: list) -> None:
+ """Test Dice returns 0.0 when there's no overlap."""
+ evaluator = DiceClass1Evaluator()
+ result = evaluator.evaluate(zero_overlap_masks)
+
+ assert isinstance(result, float)
+ assert result == 0.0
+
+ def test_dice_class2_never_appears(
+ self, class_never_appears_masks: list,
+ ) -> None:
+ """Test Dice returns 0.0 when class 2 never appears."""
+ evaluator = DiceClass2Evaluator()
+ result = evaluator.evaluate(class_never_appears_masks)
+
+ assert isinstance(result, float)
+ assert result == 0.0
+
+ def test_dice_macro_average_returns_float(
+ self, sample_mask_pairs: list,
+ ) -> None:
+ """Test Dice macro average returns float."""
+ evaluator = DiceMacroAverageEvaluator()
+ result = evaluator.evaluate(sample_mask_pairs)
+
+ assert isinstance(result, float)
+ assert 0.0 <= result <= 1.0
+
+ def test_dice_weighted_average_returns_float(
+ self, sample_mask_pairs: list,
+ ) -> None:
+ """Test Dice weighted average returns float."""
+ evaluator = DiceWeightedAverageEvaluator()
+ result = evaluator.evaluate(sample_mask_pairs)
+
+ assert isinstance(result, float)
+ assert 0.0 <= result <= 1.0
+
+ def test_dice_greater_than_iou_relationship(
+ self, sample_mask_pairs: list,
+ ) -> None:
+ """Test that Dice >= IoU (mathematical relationship)."""
+ iou_eval = IoUClass1Evaluator()
+ dice_eval = DiceClass1Evaluator()
+
+ iou = iou_eval.evaluate(sample_mask_pairs)
+ dice = dice_eval.evaluate(sample_mask_pairs)
+
+ # Dice = 2*IoU / (1 + IoU), so Dice >= IoU always holds
+ assert dice >= iou or abs(dice - iou) < 1e-6
+
+ # ========================================================================
+ # Precision Tests
+ # ========================================================================
+
+ def test_precision_class0_perfect_match(
+ self, perfect_match_masks: list,
+ ) -> None:
+ """Test Precision returns 1.0 for perfect predictions."""
+ evaluator = PrecisionClass0Evaluator()
+ result = evaluator.evaluate(perfect_match_masks)
+
+ assert isinstance(result, float)
+ assert result == 1.0
+
+ def test_precision_class1_perfect_match(
+ self, perfect_match_masks: list,
+ ) -> None:
+ """Test Precision returns 1.0 for perfect predictions."""
+ evaluator = PrecisionClass1Evaluator()
+ result = evaluator.evaluate(perfect_match_masks)
+
+ assert isinstance(result, float)
+ assert result == 1.0
+
+ def test_precision_class2_perfect_match(
+ self, perfect_match_masks: list,
+ ) -> None:
+ """Test Precision returns 1.0 for perfect predictions."""
+ evaluator = PrecisionClass2Evaluator()
+ result = evaluator.evaluate(perfect_match_masks)
+
+ assert isinstance(result, float)
+ assert result == 1.0
+
+ def test_precision_class2_never_appears(
+ self, class_never_appears_masks: list,
+ ) -> None:
+ """Test Precision returns 0.0 when class 2 never predicted."""
+ evaluator = PrecisionClass2Evaluator()
+ result = evaluator.evaluate(class_never_appears_masks)
+
+ assert isinstance(result, float)
+ assert result == 0.0
+
+ def test_precision_macro_average_returns_float(
+ self, sample_mask_pairs: list,
+ ) -> None:
+ """Test Precision macro average returns float."""
+ evaluator = PrecisionMacroAverageEvaluator()
+ result = evaluator.evaluate(sample_mask_pairs)
+
+ assert isinstance(result, float)
+ assert 0.0 <= result <= 1.0
+
+ # ========================================================================
+ # Recall Tests
+ # ========================================================================
+
+ def test_recall_class0_perfect_match(self, perfect_match_masks: list) -> None:
+ """Test Recall returns 1.0 for perfect predictions."""
+ evaluator = RecallClass0Evaluator()
+ result = evaluator.evaluate(perfect_match_masks)
+
+ assert isinstance(result, float)
+ assert result == 1.0
+
+ def test_recall_class1_perfect_match(self, perfect_match_masks: list) -> None:
+ """Test Recall returns 1.0 for perfect predictions."""
+ evaluator = RecallClass1Evaluator()
+ result = evaluator.evaluate(perfect_match_masks)
+
+ assert isinstance(result, float)
+ assert result == 1.0
+
+ def test_recall_class2_perfect_match(self, perfect_match_masks: list) -> None:
+ """Test Recall returns 1.0 for perfect predictions."""
+ evaluator = RecallClass2Evaluator()
+ result = evaluator.evaluate(perfect_match_masks)
+
+ assert isinstance(result, float)
+ assert result == 1.0
+
+ def test_recall_class2_never_appears(
+ self, class_never_appears_masks: list,
+ ) -> None:
+ """Test Recall returns 0.0 when class 2 never in ground truth."""
+ evaluator = RecallClass2Evaluator()
+ result = evaluator.evaluate(class_never_appears_masks)
+
+ assert isinstance(result, float)
+ assert result == 0.0
+
+ def test_recall_macro_average_returns_float(
+ self, sample_mask_pairs: list,
+ ) -> None:
+ """Test Recall macro average returns float."""
+ evaluator = RecallMacroAverageEvaluator()
+ result = evaluator.evaluate(sample_mask_pairs)
+
+ assert isinstance(result, float)
+ assert 0.0 <= result <= 1.0
+
+ # ========================================================================
+ # Known Values Tests
+ # ========================================================================
+
+ def test_iou_known_values(self) -> None:
+ """Test IoU with known confusion matrix values."""
+ # Create a simple 10x10 mask for class 1
+ # TP=60, FP=20, FN=40
+ # IoU = 60 / (60 + 20 + 40) = 60 / 120 = 0.5
+
+ pred_mask = np.zeros((10, 10), dtype=np.uint8)
+ real_mask = np.zeros((10, 10), dtype=np.uint8)
+
+ # Ground truth: 100 pixels should be class 1
+ real_mask[:10, :10] = 1
+
+ # Prediction: 80 pixels predicted as class 1
+ # Only 60 overlap with ground truth
+ pred_mask[:8, :10] = 1
+
+ # TP = 8*10 = 80 (but limited by real = 60)
+ # Actually: overlap is first 8 rows = 80 pixels
+ # FP = 0 (all predictions are correct)
+ # FN = 20 (last 2 rows not predicted)
+ # IoU = 80 / (80 + 0 + 20) = 80/100 = 0.8
+
+ mask_pairs = [[(pred_mask, real_mask)]]
+
+ evaluator = IoUClass1Evaluator()
+ result = evaluator.evaluate(mask_pairs)
+
+ assert isinstance(result, float)
+ assert abs(result - 0.8) < 1e-6
+
+ def test_dice_known_values(self) -> None:
+ """Test Dice with known confusion matrix values."""
+ # Same setup as IoU test
+
+ pred_mask = np.zeros((10, 10), dtype=np.uint8)
+ real_mask = np.zeros((10, 10), dtype=np.uint8)
+
+ real_mask[:10, :10] = 1
+ pred_mask[:8, :10] = 1
+
+ mask_pairs = [[(pred_mask, real_mask)]]
+
+ evaluator = DiceClass1Evaluator()
+ result = evaluator.evaluate(mask_pairs)
+
+ expected_dice = 160.0 / 180.0
+ assert isinstance(result, float)
+ assert abs(result - expected_dice) < 1e-6
+
+ def test_precision_recall_known_values(self) -> None:
+ """Test Precision and Recall with known confusion matrix values."""
+ # TP=80, FP=0, FN=20 (from previous test)
+ # Precision = 80 / (80 + 0) = 1.0
+ # Recall = 80 / (80 + 20) = 0.8
+
+ pred_mask = np.zeros((10, 10), dtype=np.uint8)
+ real_mask = np.zeros((10, 10), dtype=np.uint8)
+
+ real_mask[:10, :10] = 1
+ pred_mask[:8, :10] = 1
+
+ mask_pairs = [[(pred_mask, real_mask)]]
+
+ precision_eval = PrecisionClass1Evaluator()
+ recall_eval = RecallClass1Evaluator()
+
+ precision = precision_eval.evaluate(mask_pairs)
+ recall = recall_eval.evaluate(mask_pairs)
+
+ assert abs(precision - 1.0) < 1e-6
+ assert abs(recall - 0.8) < 1e-6
+
+ # ========================================================================
+ # Fold Aggregation Tests
+ # ========================================================================
+
+ # def test_iou_aggregates_across_folds(self) -> None:
+ # """Test that IoU correctly aggregates across multiple folds."""
+ # # Fold 1: Perfect match for class 0
+ # mask1_pred = np.zeros((10, 10), dtype=np.uint8) # noqa: ERA001
+ # mask1_real = np.zeros((10, 10), dtype=np.uint8) # noqa: ERA001
+ #
+ # # Fold 2: 50% IoU for class 0
+ # mask2_pred = np.zeros((10, 10), dtype=np.uint8) # noqa: ERA001
+ # mask2_real = np.zeros((10, 10), dtype=np.uint8) # noqa: ERA001
+ # mask2_pred[:5, :] = 1 # Top half is class 1 # noqa: ERA001
+ # mask2_real[:, :] = 1 # All is class 1 # noqa: ERA001
+ #
+ # # For class 0:
+ # # Fold 1: TP=100, FP=0, FN=0 -> IoU = 1.0
+ # # Fold 2: TP=50, FP=0, FN=50 -> IoU = 0.5
+ # # Aggregated: TP=150, FP=0, FN=50 -> IoU = 150/200 = 0.75
+ #
+ # mask_pairs = [[(mask1_pred, mask1_real)], [(mask2_pred, mask2_real)]] # noqa: E501, ERA001
+ #
+ # evaluator = IoUClass0Evaluator()# noqa: ERA001
+ # result = evaluator.evaluate(mask_pairs)# noqa: ERA001
+ #
+ # expected_iou = 150.0 / 200.0# noqa: ERA001
+ # assert abs(result - expected_iou) < 1e-6# noqa: ERA001
+
+ def test_dice_aggregates_across_multiple_samples(self) -> None:
+ """Test that Dice correctly aggregates across multiple samples."""
+ # Sample 1: Perfect match
+ mask1 = np.zeros((10, 10), dtype=np.uint8)
+ mask1[:, :5] = 1
+
+ # Sample 2: Partial match
+ mask2_pred = np.zeros((10, 10), dtype=np.uint8)
+ mask2_real = np.zeros((10, 10), dtype=np.uint8)
+ mask2_pred[:, :5] = 1
+ mask2_real[:, :7] = 1
+
+ # Aggregate across samples in same fold
+ mask_pairs = [[(mask1, mask1), (mask2_pred, mask2_real)]]
+
+ evaluator = DiceClass1Evaluator()
+ result = evaluator.evaluate(mask_pairs)
+
+ # Sample 1: TP=50, FP=0, FN=0
+ # Sample 2: TP=50, FP=0, FN=20
+ # Total: TP=100, FP=0, FN=20
+ # Dice = 2*100 / (2*100 + 0 + 20) = 200/220 = 0.909...
+
+ expected_dice = 200.0 / 220.0
+ assert abs(result - expected_dice) < 1e-6
+
+ # ========================================================================
+ # Integration Tests
+ # ========================================================================
+
+ def test_all_evaluators_return_valid_floats(
+ self, sample_mask_pairs: list,
+ ) -> None:
+ """Test that all evaluators return valid floats in [0.0, 1.0]."""
+ evaluators = [
+ # IoUClass0Evaluator(),# noqa: ERA001
+ IoUClass1Evaluator(),
+ IoUClass2Evaluator(),
+ IoUMacroAverageEvaluator(),
+ IoUWeightedAverageEvaluator(),
+ DiceClass0Evaluator(),
+ DiceClass1Evaluator(),
+ DiceClass2Evaluator(),
+ DiceMacroAverageEvaluator(),
+ DiceWeightedAverageEvaluator(),
+ PrecisionClass0Evaluator(),
+ PrecisionClass1Evaluator(),
+ PrecisionClass2Evaluator(),
+ PrecisionMacroAverageEvaluator(),
+ RecallClass0Evaluator(),
+ RecallClass1Evaluator(),
+ RecallClass2Evaluator(),
+ RecallMacroAverageEvaluator(),
+ ]
+
+ for evaluator in evaluators:
+ result = evaluator.evaluate(sample_mask_pairs)
+ assert isinstance(result, float)
+ assert 0.0 <= result <= 1.0
+
+ def test_evaluators_with_evaluator_node(self, sample_mask_pairs: list) -> None:
+ """Test evaluators work with EvaluatorNode integration."""
+ from auto_ml.implementations.nodes import EvaluatorNode
+
+ evaluator_node = EvaluatorNode(
+ evaluators={
+ # "iou_class0": IoUClass0Evaluator(),# noqa: ERA001
+ "dice_macro": DiceMacroAverageEvaluator(),
+ "precision_class1": PrecisionClass1Evaluator(),
+ "recall_class1": RecallClass1Evaluator(),
+ },
+ name="TestEvaluator",
+ )
+
+ results = evaluator_node.evaluate(sample_mask_pairs)
+
+ assert isinstance(results, dict)
+ # assert "iou_class0" in results
+ assert "dice_macro" in results
+ assert "precision_class1" in results
+ assert "recall_class1" in results
diff --git a/tests/test_sem_augmentations.py b/tests/test_sem_augmentations.py
new file mode 100644
index 0000000..70c0b5f
--- /dev/null
+++ b/tests/test_sem_augmentations.py
@@ -0,0 +1,168 @@
+"""Test SEM-specific augmentations."""
+
+import numpy as np
+
+from auto_ml.implementations import (
+ AdaptiveHistogramEqualizationAugmentator,
+ ChargingArtifactAugmentator,
+ ElasticDeformationAugmentator,
+ ScanLineNoiseAugmentator,
+)
+from auto_ml.interfaces import SegmentationDatasetInterface
+
+
+class SimpleDataset(SegmentationDatasetInterface):
+ """Simple dataset for testing."""
+
+ def __init__(self, samples: list[tuple[np.ndarray, np.ndarray]]) -> None:
+ """Initialize dataset with samples."""
+ super().__init__()
+ for image, mask in samples:
+ self.add_sample(image, mask)
+
+
+def test_elastic_deformation_augmentator() -> None:
+ """Test elastic deformation augmentator."""
+ # Create sample image and mask
+ image = np.random.randint(0, 256, (512, 512, 3), dtype=np.uint8)
+ mask = np.random.randint(0, 2, (512, 512), dtype=np.uint8)
+ dataset = SimpleDataset([(image, mask)])
+
+ # Apply augmentation
+ augmentator = ElasticDeformationAugmentator(alpha=50.0, sigma=5.0, random_seed=42)
+ augmented = augmentator.augment(dataset)
+
+ # Check that we still have one sample
+ assert len(augmented.samples) == 1
+
+ # Check shapes are preserved
+ aug_image, aug_mask = augmented.samples[0]
+ assert aug_image.shape == image.shape
+ assert aug_mask.shape == mask.shape
+
+
+def test_adaptive_histogram_equalization_augmentator() -> None:
+ """Test adaptive histogram equalization augmentator."""
+ # Create sample image and mask
+ image = np.random.randint(0, 256, (512, 512, 3), dtype=np.uint8)
+ mask = np.random.randint(0, 2, (512, 512), dtype=np.uint8)
+ dataset = SimpleDataset([(image, mask)])
+
+ # Apply augmentation
+ augmentator = AdaptiveHistogramEqualizationAugmentator(
+ clip_limit=2.0,
+ tile_grid_size=(8, 8),
+ )
+ augmented = augmentator.augment(dataset)
+
+ # Check that we still have one sample
+ assert len(augmented.samples) == 1
+
+ # Check shapes are preserved
+ aug_image, aug_mask = augmented.samples[0]
+ assert aug_image.shape == image.shape
+ assert aug_mask.shape == mask.shape
+
+ # Mask should be unchanged
+ np.testing.assert_array_equal(aug_mask, mask)
+
+
+def test_charging_artifact_augmentator() -> None:
+ """Test charging artifact augmentator."""
+ # Create sample image and mask
+ image = np.random.randint(0, 256, (512, 512, 3), dtype=np.uint8)
+ mask = np.random.randint(0, 2, (512, 512), dtype=np.uint8)
+ dataset = SimpleDataset([(image, mask)])
+
+ # Apply augmentation
+ augmentator = ChargingArtifactAugmentator(
+ num_spots=(2, 5),
+ spot_size_range=(5, 15),
+ intensity_range=(0.3, 0.7),
+ random_seed=42,
+ )
+ augmented = augmentator.augment(dataset)
+
+ # Check that we still have one sample
+ assert len(augmented.samples) == 1
+
+ # Check shapes are preserved
+ aug_image, aug_mask = augmented.samples[0]
+ assert aug_image.shape == image.shape
+ assert aug_mask.shape == mask.shape
+
+ # Mask should be unchanged
+ np.testing.assert_array_equal(aug_mask, mask)
+
+
+def test_scan_line_noise_augmentator() -> None:
+ """Test scan line noise augmentator."""
+ # Create sample image and mask
+ image = np.random.randint(0, 256, (512, 512, 3), dtype=np.uint8)
+ mask = np.random.randint(0, 2, (512, 512), dtype=np.uint8)
+ dataset = SimpleDataset([(image, mask)])
+
+ # Apply augmentation
+ augmentator = ScanLineNoiseAugmentator(
+ probability=0.5,
+ intensity_range=(0.02, 0.05),
+ direction="horizontal",
+ random_seed=42,
+ )
+ augmented = augmentator.augment(dataset)
+
+ # Check that we still have one sample
+ assert len(augmented.samples) == 1
+
+ # Check shapes are preserved
+ aug_image, aug_mask = augmented.samples[0]
+ assert aug_image.shape == image.shape
+ assert aug_mask.shape == mask.shape
+
+ # Mask should be unchanged
+ np.testing.assert_array_equal(aug_mask, mask)
+
+
+def test_sem_augmentations_preserve_mask() -> None:
+ """Test that all SEM augmentations preserve masks."""
+ # Create sample image and mask
+ image = np.random.randint(0, 256, (512, 512, 3), dtype=np.uint8)
+ mask = np.random.randint(0, 2, (512, 512), dtype=np.uint8)
+ dataset = SimpleDataset([(image, mask)])
+
+ augmentators = [
+ ElasticDeformationAugmentator(alpha=50.0, sigma=5.0, random_seed=42),
+ AdaptiveHistogramEqualizationAugmentator(clip_limit=2.0, tile_grid_size=(8, 8)),
+ ChargingArtifactAugmentator(
+ num_spots=(2, 5),
+ spot_size_range=(5, 15),
+ intensity_range=(0.3, 0.7),
+ random_seed=42,
+ ),
+ ScanLineNoiseAugmentator(
+ probability=0.5,
+ intensity_range=(0.02, 0.05),
+ direction="horizontal",
+ random_seed=42,
+ ),
+ ]
+
+ for augmentator in augmentators:
+ augmented = augmentator.augment(dataset)
+ aug_image, aug_mask = augmented.samples[0]
+
+ # Check shapes are preserved
+ assert aug_image.shape == image.shape, (
+ f"Image shape changed by {type(augmentator).__name__}"
+ )
+ assert aug_mask.shape == mask.shape, (
+ f"Mask shape changed by {type(augmentator).__name__}"
+ )
+
+ # For all augmentations except elastic deformation, mask should be unchanged
+ if not isinstance(augmentator, ElasticDeformationAugmentator):
+ np.testing.assert_array_equal(
+ aug_mask,
+ mask,
+ err_msg=f"Mask changed by {type(augmentator).__name__}",
+ )
diff --git a/tests/test_sliding_window_segmentation.py b/tests/test_sliding_window_segmentation.py
new file mode 100644
index 0000000..b9fc0a3
--- /dev/null
+++ b/tests/test_sliding_window_segmentation.py
@@ -0,0 +1,379 @@
+import numpy as np
+import pytest
+
+from auto_ml.implementations.segmentators import SlidingWindowSegmentationModel
+from auto_ml.interfaces import (
+ ClassificationDatasetInterface,
+ ClassificationModelInterface,
+ ImageArray,
+ MetricsResultInterface,
+ SegmentationDatasetInterface,
+)
+
+
+class DummyClassifier(ClassificationModelInterface):
+ """
+ Dummy classifier for testing SlidingWindowSegmentationModel.
+
+ Classifies regions based on the average brightness within the region.
+ The confidence is proportional to how uniform the region is.
+ """
+
+ def classify(
+ self,
+ image: ImageArray,
+ x: int,
+ y: int,
+ width: int,
+ height: int,
+ ) -> tuple[int, float]:
+ """
+ Classify an image region based on average brightness.
+
+ Args:
+ image: Image as a numpy array (H, W, C or H, W).
+ x: x-coordinate of the region (top-left).
+ y: y-coordinate of the region (top-left).
+ width: Width of the region.
+ height: Height of the region.
+
+ Returns:
+ Tuple of (class_label, confidence).
+
+ """
+ region = image[y : y + height, x : x + width]
+
+ if region.size == 0:
+ return 0, 0.0
+
+ # Convert to grayscale if color image
+ if region.ndim == 3:
+ region_flat = np.mean(region, axis=2).flatten()
+ else:
+ region_flat = region.flatten()
+
+ # Count occurrences of each unique pixel value
+ _, counts = np.unique(region_flat, return_counts=True)
+
+ # Confidence is the proportion of the dominant pixel
+ dominant_count = np.max(counts)
+ confidence = dominant_count / region_flat.size
+
+ # Map average brightness to 3 classes (0, 1, 2)
+ avg_brightness = np.mean(region_flat)
+ if avg_brightness < 85:
+ class_label = 0
+ elif avg_brightness < 170:
+ class_label = 1
+ else:
+ class_label = 2
+
+ return class_label, confidence
+
+ def train(
+ self,
+ dataset: ClassificationDatasetInterface,
+ ) -> MetricsResultInterface:
+ """Train the model on the provided dataset."""
+ return MetricsResultInterface()
+
+ def evaluate(
+ self,
+ dataset: ClassificationDatasetInterface,
+ ) -> MetricsResultInterface:
+ """Evaluate the model on the provided dataset."""
+ return MetricsResultInterface()
+
+
+def test_sliding_window_init() -> None:
+ """Test the initialization of SlidingWindowSegmentationModel."""
+ print(
+ "Initializing Verification for SlidingWindowSegmentationModel "
+ "initialization...",
+ )
+ classifier = DummyClassifier()
+
+ # Test valid initialization
+ model = SlidingWindowSegmentationModel(
+ classifier=classifier,
+ classifier_dataset_dir=None,
+ window_size=128,
+ stride=64,
+ aggregation_method="majority_vote",
+ )
+
+ assert model.classifier == classifier
+ assert model.window_size == 128
+ assert model.stride == 64
+ assert model.aggregation_method == "majority_vote"
+ print("SlidingWindowSegmentationModel initialization VERIFICATION SUCCESSFUL!")
+
+
+def test_sliding_window_parameter_validation() -> None:
+ """Test parameter validation in SlidingWindowSegmentationModel."""
+ print("Initializing Verification for parameter validation...")
+ classifier = DummyClassifier()
+
+ # Test stride > window_size (should raise ValueError)
+ with pytest.raises(ValueError, match="stride.*must be <= window_size"):
+ SlidingWindowSegmentationModel(
+ classifier=classifier,
+ window_size=64,
+ stride=128,
+ )
+
+ # Test window_size too small (should raise ValueError)
+ with pytest.raises(ValueError, match="window_size.*must be in range"):
+ SlidingWindowSegmentationModel(
+ classifier=classifier,
+ window_size=4,
+ stride=2,
+ )
+
+ # Test window_size too large (should raise ValueError)
+ with pytest.raises(ValueError, match="window_size.*must be in range"):
+ SlidingWindowSegmentationModel(
+ classifier=classifier,
+ window_size=1024,
+ stride=512,
+ )
+
+ # Test invalid aggregation method
+ with pytest.raises(ValueError, match="aggregation_method.*not supported"):
+ SlidingWindowSegmentationModel(
+ classifier=classifier,
+ window_size=64,
+ stride=32,
+ aggregation_method="invalid_method",
+ )
+
+ print("Parameter validation VERIFICATION SUCCESSFUL!")
+
+
+def test_sliding_window_basic_segmentation() -> None:
+ """Test basic segmentation with non-overlapping windows."""
+ print("Initializing Verification for basic sliding window segmentation...")
+
+ image_size = 512
+
+ # Create test image with 4 quadrants (different brightness)
+ dummy_image = np.zeros((image_size, image_size), dtype=np.uint8)
+ dummy_image[0 : image_size // 2, 0 : image_size // 2] = 40 # Class 0
+ dummy_image[0 : image_size // 2, image_size // 2 : image_size] = 120
+ dummy_image[image_size // 2 : image_size, 0 : image_size // 2] = 200
+ dummy_image[image_size // 2 : image_size, image_size // 2 : image_size] = 60
+
+ dummy_real_mask = np.zeros((image_size, image_size), dtype=np.uint8)
+
+ dataset = SegmentationDatasetInterface()
+ dataset.add_sample(dummy_image, dummy_real_mask)
+
+ # Use non-overlapping windows (256x256 stride=256)
+ # This should align perfectly with quadrants
+ classifier = DummyClassifier()
+ model = SlidingWindowSegmentationModel(
+ classifier=classifier,
+ classifier_dataset_dir=None,
+ window_size=256,
+ stride=256,
+ )
+
+ # Evaluate
+ mask_pairs = model.evaluate(dataset)
+ assert len(mask_pairs) == 1
+
+ predicted_mask, real_mask = mask_pairs[0]
+ assert predicted_mask.shape == (image_size, image_size)
+
+ # Expected mask based on the dummy_image and DummyClassifier logic
+ expected_mask = np.zeros((image_size, image_size), dtype=np.uint8)
+ expected_mask[0 : image_size // 2, 0 : image_size // 2] = 0
+ expected_mask[0 : image_size // 2, image_size // 2 : image_size] = 1
+ expected_mask[image_size // 2 : image_size, 0 : image_size // 2] = 2
+ expected_mask[image_size // 2 : image_size, image_size // 2 : image_size] = 0
+
+ assert np.array_equal(predicted_mask, expected_mask)
+ print("Basic sliding window segmentation VERIFICATION SUCCESSFUL!")
+
+
+def test_sliding_window_overlapping_votes() -> None:
+ """Test vote aggregation with overlapping windows."""
+ print("Initializing Verification for overlapping window votes...")
+
+ image_size = 512
+
+ # Create simple test image: left half dark (class 0), right half bright (class 2)
+ dummy_image = np.zeros((image_size, image_size), dtype=np.uint8)
+ dummy_image[:, 0 : image_size // 2] = 40 # Class 0 (dark)
+ dummy_image[:, image_size // 2 : image_size] = 200 # Class 2 (bright)
+
+ dummy_real_mask = np.zeros((image_size, image_size), dtype=np.uint8)
+
+ dataset = SegmentationDatasetInterface()
+ dataset.add_sample(dummy_image, dummy_real_mask)
+
+ # Use overlapping windows (128x128, stride=64)
+ classifier = DummyClassifier()
+ model = SlidingWindowSegmentationModel(
+ classifier=classifier,
+ classifier_dataset_dir=None,
+ window_size=128,
+ stride=64,
+ )
+
+ # Evaluate
+ mask_pairs = model.evaluate(dataset)
+ assert len(mask_pairs) == 1
+
+ predicted_mask, real_mask = mask_pairs[0]
+ assert predicted_mask.shape == (image_size, image_size)
+
+ # Expected: left half should be class 0, right half should be class 2
+ # Center pixels might be affected by overlapping windows at the boundary
+ # Check that left quarter is definitely class 0
+ left_quarter = predicted_mask[:, 0 : image_size // 4]
+ assert np.all(left_quarter == 0), "Left quarter should be class 0"
+
+ # Check that right quarter is definitely class 2
+ right_quarter = predicted_mask[:, 3 * image_size // 4 : image_size]
+ assert np.all(right_quarter == 2), "Right quarter should be class 2"
+
+ print("Overlapping window votes VERIFICATION SUCCESSFUL!")
+
+
+def test_sliding_window_edge_handling() -> None:
+ """Test that edge pixels are handled correctly."""
+ print("Initializing Verification for edge pixel handling...")
+
+ image_size = 512
+
+ # Create test image with left half dark, right half bright
+ dummy_image = np.zeros((image_size, image_size), dtype=np.uint8)
+ dummy_image[:, 0 : image_size // 2] = 40 # Left half dark (class 0)
+ dummy_image[:, image_size // 2 : image_size] = 200 # Right half bright (class 2)
+
+ dummy_real_mask = np.zeros((image_size, image_size), dtype=np.uint8)
+
+ dataset = SegmentationDatasetInterface()
+ dataset.add_sample(dummy_image, dummy_real_mask)
+
+ # Use windows that will test edge boundaries
+ classifier = DummyClassifier()
+ model = SlidingWindowSegmentationModel(
+ classifier=classifier,
+ classifier_dataset_dir=None,
+ window_size=64,
+ stride=32,
+ )
+
+ # Evaluate
+ mask_pairs = model.evaluate(dataset)
+ assert len(mask_pairs) == 1
+
+ predicted_mask, real_mask = mask_pairs[0]
+ assert predicted_mask.shape == (image_size, image_size)
+
+ # Check that left edge pixels are class 0 (dark)
+ assert predicted_mask[0, 0] == 0, "Top-left corner should be class 0"
+ assert (
+ predicted_mask[image_size - 1, 0] == 0
+ ), "Bottom-left corner should be class 0"
+
+ # Check that right edge pixels are class 2 (bright)
+ assert (
+ predicted_mask[0, image_size - 1] == 2
+ ), "Top-right corner should be class 2"
+ assert (
+ predicted_mask[image_size - 1, image_size - 1] == 2
+ ), "Bottom-right corner should be class 2"
+
+ print("Edge pixel handling VERIFICATION SUCCESSFUL!")
+
+
+def test_sliding_window_train_and_evaluate() -> None:
+ """Test train() and evaluate() methods."""
+ print("Initializing Verification for train() and evaluate()...")
+
+ image_size = 512
+
+ # Create simple dataset
+ dummy_image = np.full((image_size, image_size), 100, dtype=np.uint8) # Class 1
+ dummy_real_mask = np.ones((image_size, image_size), dtype=np.uint8) # All class 1
+
+ dataset = SegmentationDatasetInterface()
+ dataset.add_sample(dummy_image, dummy_real_mask)
+
+ classifier = DummyClassifier()
+ model = SlidingWindowSegmentationModel(
+ classifier=classifier,
+ classifier_dataset_dir=None,
+ window_size=128,
+ stride=128,
+ )
+
+ # Train (without classifier training)
+ metrics = model.train(dataset)
+
+ # Verify metrics interface
+ assert isinstance(metrics, MetricsResultInterface)
+ assert hasattr(metrics, "accuracy")
+ assert hasattr(metrics, "loss")
+ assert hasattr(metrics, "iou")
+ assert hasattr(metrics, "precision")
+ assert hasattr(metrics, "recall")
+ assert hasattr(metrics, "f1_score")
+
+ # The predicted mask should be all class 1 (brightness ~100)
+ # So accuracy should be high
+ assert metrics.accuracy > 0.8, f"Expected high accuracy, got {metrics.accuracy}"
+
+ print("train() and evaluate() VERIFICATION SUCCESSFUL!")
+
+
+def test_sliding_window_metrics_computation() -> None:
+ """Test that metrics are computed correctly."""
+ print("Initializing Verification for metrics computation...")
+
+ image_size = 512
+
+ # Create test case where we know the exact metrics
+ # Image: all class 0
+ dummy_image = np.full((image_size, image_size), 40, dtype=np.uint8) # Class 0
+ # Mask: all class 0
+ dummy_real_mask = np.zeros((image_size, image_size), dtype=np.uint8) # Class 0
+
+ dataset = SegmentationDatasetInterface()
+ dataset.add_sample(dummy_image, dummy_real_mask)
+
+ classifier = DummyClassifier()
+ model = SlidingWindowSegmentationModel(
+ classifier=classifier,
+ classifier_dataset_dir=None,
+ window_size=256,
+ stride=256,
+ )
+
+ # Evaluate
+ mask_pairs = model.evaluate(dataset)
+ metrics = model._compute_metrics(mask_pairs)
+
+ # Since predicted and real masks are both all class 0, accuracy should be 1.0
+ assert metrics.accuracy == 1.0, f"Expected accuracy 1.0, got {metrics.accuracy}"
+ assert metrics.loss == 0.0, f"Expected loss 0.0, got {metrics.loss}"
+
+ # Mean IoU across 3 classes where only class 0 appears = (1.0 + 0.0 + 0.0) / 3
+ # This is expected behavior - matches quadtree implementation
+ assert (
+ 0.3 <= metrics.iou <= 0.4
+ ), f"Expected IoU ~0.33 (mean across 3 classes), got {metrics.iou}"
+
+ # Mean precision/recall/F1 will also be ~0.33 for same reason
+ assert (
+ 0.3 <= metrics.precision <= 0.4
+ ), f"Expected precision ~0.33, got {metrics.precision}"
+ assert 0.3 <= metrics.recall <= 0.4, f"Expected recall ~0.33, got {metrics.recall}"
+ assert (
+ 0.3 <= metrics.f1_score <= 0.4
+ ), f"Expected F1 ~0.33, got {metrics.f1_score}"
+
+ print("Metrics computation VERIFICATION SUCCESSFUL!")
diff --git a/tests/test_sliding_window_weighted.py b/tests/test_sliding_window_weighted.py
new file mode 100644
index 0000000..6e44e48
--- /dev/null
+++ b/tests/test_sliding_window_weighted.py
@@ -0,0 +1,129 @@
+import numpy as np
+
+from auto_ml.implementations.segmentators import SlidingWindowSegmentationModel
+from auto_ml.interfaces import (
+ ClassificationDatasetInterface,
+ ClassificationModelInterface,
+ ImageArray,
+ MetricsResultInterface,
+ SegmentationDatasetInterface,
+)
+
+
+class ConfidenceTestClassifier(ClassificationModelInterface):
+ """
+ Test classifier that returns controlled confidence values.
+
+ Return high confidence for specific regions, low for others.
+ """
+
+ def classify(
+ self,
+ image: ImageArray,
+ x: int,
+ y: int,
+ width: int,
+ height: int,
+ ) -> tuple[int, float]:
+ """
+ Return different confidence scores based on region location.
+
+ Top half: class 0 with low confidence (0.3)
+ Bottom half: class 1 with high confidence (0.9)
+ """
+ avg_y = y + height // 2
+
+ if avg_y < 256:
+ # Top half - class 0, low confidence
+ return 0, 0.3
+ else:
+ # Bottom half - class 1, high confidence
+ return 1, 0.9
+
+ def train(
+ self,
+ dataset: ClassificationDatasetInterface,
+ ) -> MetricsResultInterface:
+ """Train the model."""
+ return MetricsResultInterface()
+
+
+def test_confidence_weighted_voting() -> None:
+ """Test that confidence weighting favors high-confidence predictions."""
+ print("Testing confidence-weighted voting...")
+
+ image_size = 512
+
+ # Create uniform test image (brightness doesn't matter for this test)
+ dummy_image = np.full((image_size, image_size), 100, dtype=np.uint8)
+ dummy_real_mask = np.zeros((image_size, image_size), dtype=np.uint8)
+
+ dataset = SegmentationDatasetInterface()
+ dataset.add_sample(dummy_image, dummy_real_mask)
+
+ # Test with confidence weighting
+ classifier = ConfidenceTestClassifier()
+ model_weighted = SlidingWindowSegmentationModel(
+ classifier=classifier,
+ window_size=128,
+ stride=64,
+ aggregation_method="confidence_weighted",
+ )
+
+ mask_pairs = model_weighted.evaluate(dataset)
+ predicted_mask, _ = mask_pairs[0]
+
+ # Verify top quarter is class 0 (away from boundary)
+ # Checking top quarter to avoid boundary effects from overlapping windows
+ top_quarter = predicted_mask[0:128, :]
+ assert np.all(top_quarter == 0), "Top quarter should be class 0"
+
+ # Verify bottom quarter is class 1 (high confidence, away from boundary)
+ bottom_quarter = predicted_mask[384:512, :]
+ assert np.all(bottom_quarter == 1), "Bottom quarter should be class 1"
+
+ print("Confidence-weighted voting test PASSED!")
+
+
+def test_majority_vote_still_works() -> None:
+ """Test that majority_vote aggregation still works correctly."""
+ print("Testing backward compatibility with majority_vote...")
+
+ image_size = 512
+ dummy_image = np.full((image_size, image_size), 100, dtype=np.uint8)
+ dummy_real_mask = np.zeros((image_size, image_size), dtype=np.uint8)
+
+ dataset = SegmentationDatasetInterface()
+ dataset.add_sample(dummy_image, dummy_real_mask)
+
+ classifier = ConfidenceTestClassifier()
+ model_majority = SlidingWindowSegmentationModel(
+ classifier=classifier,
+ window_size=128,
+ stride=64,
+ aggregation_method="majority_vote",
+ )
+
+ # Should not raise any errors
+ mask_pairs = model_majority.evaluate(dataset)
+ predicted_mask, _ = mask_pairs[0]
+
+ assert predicted_mask.shape == (512, 512)
+ print("Majority vote backward compatibility test PASSED!")
+
+
+def test_default_is_confidence_weighted() -> None:
+ """Test that the default aggregation method is confidence_weighted."""
+ print("Testing default aggregation method...")
+
+ classifier = ConfidenceTestClassifier()
+ model = SlidingWindowSegmentationModel(
+ classifier=classifier,
+ window_size=64,
+ stride=32,
+ )
+
+ assert model.aggregation_method == "confidence_weighted", (
+ "Default aggregation method should be 'confidence_weighted'"
+ )
+ print("Default aggregation method test PASSED!")
diff --git a/tests/test_swin_model.py b/tests/test_swin_model.py
new file mode 100644
index 0000000..737b758
--- /dev/null
+++ b/tests/test_swin_model.py
@@ -0,0 +1,63 @@
+import numpy as np
+
+from auto_ml.implementations import SwinModel
+from auto_ml.interfaces import SegmentationDatasetInterface
+
+
+def test_swin() -> None: # noqa: D103
+ print("Initializing Verification for SwinModel...")
+
+ # 1. Create dummy data
+ print("Creating dummy dataset...")
+ image = np.random.randint(0, 255, (512, 512, 3), dtype=np.uint8)
+ mask = np.random.randint(0, 3, (512, 512), dtype=np.uint8)
+
+ dataset = SegmentationDatasetInterface()
+ # Add enough samples for a batch
+ for _ in range(4):
+ dataset.add_sample(image, mask)
+
+ print(f"Dataset created with {len(dataset)} samples.")
+
+ # 2. Instantiate Model
+ print("Instantiating SwinModel...")
+ try:
+ # Use CPU for CI/Verification stability
+ model = SwinModel(epochs=1, batch_size=2, device="cpu")
+ print("SwinModel instantiated successfully.")
+ except Exception as e:
+ print(f"Error instantiating SwinModel: {e}")
+ return
+
+ # 3. Test Training
+ print("Testing train() method...")
+ try:
+ train_metrics = model.train(dataset)
+ print("Train Metrics:", train_metrics)
+ assert train_metrics.loss > 0, "Loss should be non-zero"
+ except Exception as e:
+ print(f"Error during training: {e}")
+ import traceback
+
+ traceback.print_exc()
+ return
+
+ # 4. Test Evaluation
+ # evaluate() now returns List[MaskPair] instead of MetricsResultInterface
+ print("Testing evaluate() method...")
+ try:
+ mask_pairs = model.evaluate(dataset)
+ print(f"Evaluate returned {len(mask_pairs)} mask pairs")
+ assert len(mask_pairs) == len(dataset), "Should return one pair per sample"
+ # Check that each pair contains (predicted, real) masks
+ for predicted, real in mask_pairs:
+ assert predicted.shape == (512, 512), "Predicted mask should be 512x512"
+ assert real.shape == (512, 512), "Real mask should be 512x512"
+ except Exception as e:
+ print(f"Error during evaluation: {e}")
+ import traceback
+
+ traceback.print_exc()
+ return
+
+ print("VERIFICATION SUCCESSFUL!")
diff --git a/tests/test_swin_quadtree_integration.py b/tests/test_swin_quadtree_integration.py
new file mode 100644
index 0000000..4efd1e1
--- /dev/null
+++ b/tests/test_swin_quadtree_integration.py
@@ -0,0 +1,211 @@
+"""Integration test for Swin classifier with Quadtree segmentation."""
+
+import numpy as np
+import torch
+
+from auto_ml.implementations import QuadtreeSegmentationModel
+from auto_ml.implementations.classifiers.swin import SwinModel
+from auto_ml.interfaces import (
+ ClassificationDatasetInterface,
+ SegmentationDatasetInterface,
+)
+
+
+def set_deterministic_seed(seed: int = 42) -> None:
+ """Set random seeds for reproducibility."""
+ np.random.seed(seed)
+ torch.manual_seed(seed)
+ if torch.cuda.is_available():
+ torch.cuda.manual_seed_all(seed)
+ torch.backends.cudnn.deterministic = True
+ torch.backends.cudnn.benchmark = False
+
+
+def create_binary_training_data(
+ num_samples_per_class: int = 50,
+ size: int = 64,
+) -> tuple[list[np.ndarray], list[int]]:
+ """
+ Create deterministic training data for binary classification.
+
+ Class 0: Dark images
+ Class 1: Bright images
+ """
+ images = []
+ labels = []
+ rng = np.random.RandomState(42)
+
+ for i in range(num_samples_per_class):
+ base_intensity_dark = 20 + (i % 10) * 5
+ img_dark = np.full((size, size), base_intensity_dark, dtype=np.uint8)
+ images.append(img_dark)
+ labels.append(0)
+
+ base_intensity_bright = 200 + (i % 10) * 5
+ img_bright = np.full((size, size), base_intensity_bright, dtype=np.uint8)
+ images.append(img_bright)
+ labels.append(1)
+
+ indices = rng.permutation(len(labels))
+ images = [images[i] for i in indices]
+ labels = [labels[i] for i in indices]
+
+ return images, labels
+
+
+def test_swin_binary_classifier_training() -> None:
+ """Test that the Swin classifier can be trained for binary classification."""
+ print("Testing Swin binary classifier training...")
+ set_deterministic_seed(42)
+
+ swin_model = SwinModel(
+ num_classes=2,
+ channels=1,
+ image_size=64,
+ patch_size=4,
+ window_size=4,
+ embed_dim=48,
+ depths=[2, 4],
+ num_heads=[3, 6],
+ dropout=0.1,
+ device="cpu",
+ train_epochs=5,
+ )
+
+ images, labels = create_binary_training_data(num_samples_per_class=25, size=64)
+ dataset = ClassificationDatasetInterface.from_pairs(list(zip(images, labels)))
+
+ metrics = swin_model.train(dataset=dataset)
+ print(f"Final training loss: {metrics.loss:.4f}")
+ print(f"Final training accuracy: {metrics.accuracy:.4f}")
+ assert metrics.loss < 0.5
+
+ dark_region = np.full((64, 64), 40, dtype=np.uint8)
+ label_dark, conf_dark = swin_model.classify(dark_region, 0, 0, 64, 64)
+ print(f"Dark region: class={label_dark}, confidence={conf_dark:.3f}")
+
+ bright_region = np.full((64, 64), 220, dtype=np.uint8)
+ label_bright, conf_bright = swin_model.classify(bright_region, 0, 0, 64, 64)
+ print(f"Bright region: class={label_bright}, confidence={conf_bright:.3f}")
+
+ assert label_dark == 0
+ assert label_bright == 1
+ print("Swin binary classifier training test PASSED!")
+
+
+def test_swin_quadtree_binary_integration() -> None:
+ """Integration test: Train binary Swin, use with QuadtreeSegmentationModel."""
+ print("Testing Swin + Quadtree segmentation integration (binary)...")
+ set_deterministic_seed(42)
+
+ print("Step 1: Creating and training binary Swin model...")
+ swin_model = SwinModel(
+ num_classes=2,
+ channels=1,
+ image_size=64,
+ patch_size=4,
+ window_size=4,
+ embed_dim=48,
+ depths=[2, 4],
+ num_heads=[3, 6],
+ dropout=0.1,
+ device="cpu",
+ train_epochs=5,
+ )
+
+ images, labels = create_binary_training_data(num_samples_per_class=25, size=64)
+ dataset = ClassificationDatasetInterface.from_pairs(list(zip(images, labels)))
+ metrics = swin_model.train(dataset=dataset)
+ print(f"Training complete. Final loss: {metrics.loss:.4f}")
+
+ print("Step 2: Creating test image with 4 quadrants...")
+ image_size = 512
+ test_image = np.zeros((image_size, image_size), dtype=np.uint8)
+ test_image[0:256, 0:256] = 40
+ test_image[0:256, 256:512] = 220
+ test_image[256:512, 0:256] = 220
+ test_image[256:512, 256:512] = 40
+
+ print("Step 3: Creating QuadtreeSegmentationModel...")
+ quadtree_model = QuadtreeSegmentationModel(
+ classifier=swin_model,
+ classifier_dataset_dir=None,
+ threshold=1.0,
+ min_region_size=256,
+ max_depth=None,
+ )
+
+ print("Step 4: Running segmentation...")
+ dummy_mask = np.zeros((image_size, image_size), dtype=np.uint8)
+ seg_dataset = SegmentationDatasetInterface.from_pairs([(test_image, dummy_mask)])
+
+ mask_pairs = quadtree_model.evaluate(seg_dataset)
+ predicted_mask, _ = mask_pairs[0]
+
+ print("Step 5: Verifying segmentation results...")
+ top_left_class = int(np.median(predicted_mask[0:256, 0:256]))
+ top_right_class = int(np.median(predicted_mask[0:256, 256:512]))
+ bottom_left_class = int(np.median(predicted_mask[256:512, 0:256]))
+ bottom_right_class = int(np.median(predicted_mask[256:512, 256:512]))
+
+ assert top_left_class == 0
+ assert top_right_class == 1
+ assert bottom_left_class == 1
+ assert bottom_right_class == 0
+ print("Swin + Quadtree segmentation integration test PASSED!")
+
+
+def test_swin_quadtree_deeper_recursion() -> None:
+ """Test Quadtree with deeper recursion using trained Swin."""
+ print("Testing Swin + Quadtree with deeper recursion...")
+ set_deterministic_seed(42)
+
+ swin_model = SwinModel(
+ num_classes=2,
+ channels=1,
+ image_size=64,
+ patch_size=4,
+ window_size=4,
+ embed_dim=48,
+ depths=[2, 4],
+ num_heads=[3, 6],
+ dropout=0.1,
+ device="cpu",
+ train_epochs=5,
+ )
+
+ images, labels = create_binary_training_data(num_samples_per_class=25, size=64)
+ dataset = ClassificationDatasetInterface.from_pairs(list(zip(images, labels)))
+ swin_model.train(dataset=dataset)
+
+ image_size = 512
+ test_image = np.zeros((image_size, image_size), dtype=np.uint8)
+ block_size = 128
+ for i in range(4):
+ for j in range(4):
+ x_start, y_start = i * block_size, j * block_size
+ x_end, y_end = (i + 1) * block_size, (j + 1) * block_size
+ if (i + j) % 2 == 0:
+ test_image[x_start:x_end, y_start:y_end] = 40
+ else:
+ test_image[x_start:x_end, y_start:y_end] = 220
+
+ quadtree_model = QuadtreeSegmentationModel(
+ classifier=swin_model,
+ classifier_dataset_dir=None,
+ threshold=0.99,
+ min_region_size=128,
+ max_depth=2,
+ )
+
+ dummy_mask = np.zeros((image_size, image_size), dtype=np.uint8)
+ seg_dataset = SegmentationDatasetInterface.from_pairs([(test_image, dummy_mask)])
+
+ mask_pairs = quadtree_model.evaluate(seg_dataset)
+ predicted_mask, _ = mask_pairs[0]
+
+ assert predicted_mask[64, 64] == 0
+ assert predicted_mask[64, 192] == 1
+ assert predicted_mask[192, 64] == 1
+ assert predicted_mask[192, 192] == 0
+ print("Swin + Quadtree deeper recursion test PASSED!")
diff --git a/tests/test_vit_classifier.py b/tests/test_vit_classifier.py
new file mode 100644
index 0000000..fd977e0
--- /dev/null
+++ b/tests/test_vit_classifier.py
@@ -0,0 +1,90 @@
+"""Test suite for ViT Classifier."""
+
+import numpy as np
+
+from auto_ml.implementations.classifiers.vit import ViTModel
+
+
+def test_vit_classifier() -> None: # noqa: D103
+ print("Initializing Verification for ViTModel...")
+
+ # 1. Create dummy data
+ print("Creating dummy image and region...")
+ image = (
+ np.random.randint(0, 255, (512, 512), dtype=np.uint8)
+ )
+ x, y, width, height = 100, 100, 224, 224
+
+ print(f"Image shape: {image.shape}")
+ print(f"Region (x, y, w, h): ({x}, {y}, {width}, {height})")
+
+ # 2. Instantiate Classifier
+ print("Instantiating ViTModel...")
+ try:
+ classifier = ViTModel(
+ device="cpu",
+ channels=1,
+ num_classes=3,
+ image_size=224,
+ )
+ print("ViTModel instantiated successfully.")
+ except Exception as e:
+ print(f"Error instantiating ViTModel: {e}")
+ import traceback
+
+ traceback.print_exc()
+ return
+
+ # 3. Test Classification
+ print("Testing classify() method...")
+ try:
+ class_label, confidence = classifier.classify(image, x, y, width, height)
+ print(
+ f"Classification result: class={class_label}, confidence={confidence:.4f}",
+ )
+
+ # Verify output format
+ assert isinstance(class_label, (int, np.integer)), "class_label should be int"
+ assert isinstance(confidence, (float, np.floating)), (
+ "confidence should be float"
+ )
+ assert 0 <= class_label < 3, f"class_label {class_label} should be in [0, 3)"
+ assert 0.0 <= confidence <= 1.0, f"confidence {confidence} should be in [0, 1]"
+ except Exception as e:
+ print(f"Error during classification: {e}")
+ import traceback
+
+ traceback.print_exc()
+ return
+
+ # 4. Test with RGB image
+ print("\nTesting RGB image classification...")
+ try:
+ rgb_image = (
+ np.random.randint(0, 255, (512, 512, 3), dtype=np.uint8)
+ )
+ rgb_classifier = ViTModel(
+ device="cpu",
+ channels=3,
+ num_classes=3,
+ image_size=224,
+ )
+ class_label, confidence = rgb_classifier.classify(
+ rgb_image, x, y, width, height,
+ )
+ print(
+ f"RGB Classification result: class={class_label}, "
+ f"confidence={confidence:.4f}",
+ )
+
+ assert isinstance(class_label, (int, np.integer)), "class_label should be int"
+ assert 0 <= class_label < 3, f"class_label {class_label} should be in [0, 3)"
+ assert 0.0 <= confidence <= 1.0, f"confidence {confidence} should be in [0, 1]"
+ except Exception as e:
+ print(f"Error during RGB classification: {e}")
+ import traceback
+
+ traceback.print_exc()
+ return
+
+ print("\nVERIFICATION SUCCESSFUL!")
diff --git a/tests/test_vit_segmentation_model.py b/tests/test_vit_segmentation_model.py
new file mode 100644
index 0000000..0bff59a
--- /dev/null
+++ b/tests/test_vit_segmentation_model.py
@@ -0,0 +1,66 @@
+import numpy as np
+
+from auto_ml.implementations import ViTModel
+from auto_ml.interfaces import SegmentationDatasetInterface
+
+
+def test_vit() -> None: # noqa: D103
+ print("Initializing Verification for ViTModel...")
+
+ # 1. Create dummy data
+ print("Creating dummy dataset...")
+ image = np.random.randint(0, 255, (512, 512, 3), dtype=np.uint8)
+ mask = np.random.randint(0, 3, (512, 512), dtype=np.uint8)
+
+ dataset = SegmentationDatasetInterface()
+ # Add enough samples for a batch
+ for _ in range(4):
+ dataset.add_sample(image, mask)
+
+ print(f"Dataset created with {len(dataset)} samples.")
+
+ # 2. Instantiate Model
+ print("Instantiating ViTModel...")
+ try:
+ model = ViTModel(
+ epochs=1,
+ batch_size=2,
+ device="cpu",
+ ) # Use CPU for CI/Verification stability
+ print("ViTModel instantiated successfully.")
+ except Exception as e:
+ print(f"Error instantiating ViTModel: {e}")
+ return
+
+ # 3. Test Training
+ print("Testing train() method...")
+ try:
+ train_metrics = model.train(dataset)
+ print("Train Metrics:", train_metrics)
+ assert train_metrics.loss > 0, "Loss should be non-zero"
+ except Exception as e:
+ print(f"Error during training: {e}")
+ import traceback
+
+ traceback.print_exc()
+ return
+
+ # 4. Test Evaluation
+ # evaluate() now returns List[MaskPair] instead of MetricsResultInterface
+ print("Testing evaluate() method...")
+ try:
+ mask_pairs = model.evaluate(dataset)
+ print(f"Evaluate returned {len(mask_pairs)} mask pairs")
+ assert len(mask_pairs) == len(dataset), "Should return one pair per sample"
+ # Check that each pair contains (predicted, real) masks
+ for predicted, real in mask_pairs:
+ assert predicted.shape == (512, 512), "Predicted mask should be 512x512"
+ assert real.shape == (512, 512), "Real mask should be 512x512"
+ except Exception as e:
+ print(f"Error during evaluation: {e}")
+ import traceback
+
+ traceback.print_exc()
+ return
+
+ print("VERIFICATION SUCCESSFUL!")
diff --git a/uv.lock b/uv.lock
index 7d106cb..024347a 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,6 +1,11 @@
version = 1
revision = 3
requires-python = ">=3.12"
+resolution-markers = [
+ "sys_platform == 'darwin'",
+ "platform_machine == 'aarch64' and sys_platform == 'linux'",
+ "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')",
+]
[[package]]
name = "annotated-types"
@@ -11,6 +16,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
+[[package]]
+name = "appnope"
+version = "0.1.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" },
+]
+
[[package]]
name = "argcomplete"
version = "3.6.2"
@@ -20,6 +34,72 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/31/da/e42d7a9d8dd33fa775f467e4028a47936da2f01e4b0e561f9ba0d74cb0ca/argcomplete-3.6.2-py3-none-any.whl", hash = "sha256:65b3133a29ad53fb42c48cf5114752c7ab66c1c38544fdf6460f450c09b42591", size = 43708, upload-time = "2025-04-03T04:57:01.591Z" },
]
+[[package]]
+name = "asttokens"
+version = "3.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload-time = "2024-11-30T04:30:14.439Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
+ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
+ { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
+ { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
+ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
+ { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
+ { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
+ { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
+ { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
+]
+
[[package]]
name = "cfgv"
version = "3.4.0"
@@ -80,6 +160,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
+[[package]]
+name = "comm"
+version = "0.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/4c/13/7d740c5849255756bc17888787313b61fd38a0a8304fc4f073dfc46122aa/comm-0.2.3.tar.gz", hash = "sha256:2dc8048c10962d55d7ad693be1e7045d891b7ce8d999c97963a5e3e99c055971", size = 6319, upload-time = "2025-07-25T14:02:04.452Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/60/97/891a0971e1e4a8c5d2b20bbe0e524dc04548d2307fee33cdeba148fd4fc7/comm-0.2.3-py3-none-any.whl", hash = "sha256:c615d91d75f7f04f095b30d1c1711babd43bdc6419c1be9886a85f2f4e489417", size = 7294, upload-time = "2025-07-25T14:02:02.896Z" },
+]
+
[[package]]
name = "commitizen"
version = "4.9.1"
@@ -103,6 +192,102 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/49/577035b841442fe031b017027c3d99278b46104d227f0353c69dbbe55148/commitizen-4.9.1-py3-none-any.whl", hash = "sha256:4241b2ecae97b8109af8e587c36bc3b805a09b9a311084d159098e12d6ead497", size = 80624, upload-time = "2025-09-10T14:19:32.102Z" },
]
+[[package]]
+name = "contourpy"
+version = "1.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" },
+ { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" },
+ { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" },
+ { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" },
+ { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" },
+ { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" },
+ { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" },
+ { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" },
+ { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" },
+ { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" },
+ { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" },
+ { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" },
+ { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" },
+ { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" },
+ { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" },
+ { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" },
+ { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" },
+ { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" },
+ { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" },
+ { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" },
+ { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" },
+ { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" },
+ { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" },
+ { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" },
+ { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" },
+ { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" },
+]
+
+[[package]]
+name = "cycler"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
+]
+
+[[package]]
+name = "debugpy"
+version = "1.8.17"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/15/ad/71e708ff4ca377c4230530d6a7aa7992592648c122a2cd2b321cf8b35a76/debugpy-1.8.17.tar.gz", hash = "sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e", size = 1644129, upload-time = "2025-09-17T16:33:20.633Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/08/2b/9d8e65beb2751876c82e1aceb32f328c43ec872711fa80257c7674f45650/debugpy-1.8.17-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d", size = 2549522, upload-time = "2025-09-17T16:33:38.466Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/78/eb0d77f02971c05fca0eb7465b18058ba84bd957062f5eec82f941ac792a/debugpy-1.8.17-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc", size = 4309417, upload-time = "2025-09-17T16:33:41.299Z" },
+ { url = "https://files.pythonhosted.org/packages/37/42/c40f1d8cc1fed1e75ea54298a382395b8b937d923fcf41ab0797a554f555/debugpy-1.8.17-cp312-cp312-win32.whl", hash = "sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf", size = 5277130, upload-time = "2025-09-17T16:33:43.554Z" },
+ { url = "https://files.pythonhosted.org/packages/72/22/84263b205baad32b81b36eac076de0cdbe09fe2d0637f5b32243dc7c925b/debugpy-1.8.17-cp312-cp312-win_amd64.whl", hash = "sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464", size = 5319053, upload-time = "2025-09-17T16:33:53.033Z" },
+ { url = "https://files.pythonhosted.org/packages/50/76/597e5cb97d026274ba297af8d89138dfd9e695767ba0e0895edb20963f40/debugpy-1.8.17-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:857c1dd5d70042502aef1c6d1c2801211f3ea7e56f75e9c335f434afb403e464", size = 2538386, upload-time = "2025-09-17T16:33:54.594Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/60/ce5c34fcdfec493701f9d1532dba95b21b2f6394147234dce21160bd923f/debugpy-1.8.17-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:3bea3b0b12f3946e098cce9b43c3c46e317b567f79570c3f43f0b96d00788088", size = 4292100, upload-time = "2025-09-17T16:33:56.353Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/95/7873cf2146577ef71d2a20bf553f12df865922a6f87b9e8ee1df04f01785/debugpy-1.8.17-cp313-cp313-win32.whl", hash = "sha256:e34ee844c2f17b18556b5bbe59e1e2ff4e86a00282d2a46edab73fd7f18f4a83", size = 5277002, upload-time = "2025-09-17T16:33:58.231Z" },
+ { url = "https://files.pythonhosted.org/packages/46/11/18c79a1cee5ff539a94ec4aa290c1c069a5580fd5cfd2fb2e282f8e905da/debugpy-1.8.17-cp313-cp313-win_amd64.whl", hash = "sha256:6c5cd6f009ad4fca8e33e5238210dc1e5f42db07d4b6ab21ac7ffa904a196420", size = 5319047, upload-time = "2025-09-17T16:34:00.586Z" },
+ { url = "https://files.pythonhosted.org/packages/de/45/115d55b2a9da6de812696064ceb505c31e952c5d89c4ed1d9bb983deec34/debugpy-1.8.17-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:045290c010bcd2d82bc97aa2daf6837443cd52f6328592698809b4549babcee1", size = 2536899, upload-time = "2025-09-17T16:34:02.657Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/73/2aa00c7f1f06e997ef57dc9b23d61a92120bec1437a012afb6d176585197/debugpy-1.8.17-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:b69b6bd9dba6a03632534cdf67c760625760a215ae289f7489a452af1031fe1f", size = 4268254, upload-time = "2025-09-17T16:34:04.486Z" },
+ { url = "https://files.pythonhosted.org/packages/86/b5/ed3e65c63c68a6634e3ba04bd10255c8e46ec16ebed7d1c79e4816d8a760/debugpy-1.8.17-cp314-cp314-win32.whl", hash = "sha256:5c59b74aa5630f3a5194467100c3b3d1c77898f9ab27e3f7dc5d40fc2f122670", size = 5277203, upload-time = "2025-09-17T16:34:06.65Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/26/394276b71c7538445f29e792f589ab7379ae70fd26ff5577dfde71158e96/debugpy-1.8.17-cp314-cp314-win_amd64.whl", hash = "sha256:893cba7bb0f55161de4365584b025f7064e1f88913551bcd23be3260b231429c", size = 5318493, upload-time = "2025-09-17T16:34:08.483Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef", size = 5283210, upload-time = "2025-09-17T16:34:25.835Z" },
+]
+
[[package]]
name = "decli"
version = "0.6.3"
@@ -112,16 +297,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d8/fa/ec878c28bc7f65b77e7e17af3522c9948a9711b9fa7fc4c5e3140a7e3578/decli-0.6.3-py3-none-any.whl", hash = "sha256:5152347c7bb8e3114ad65db719e5709b28d7f7f45bdb709f70167925e55640f3", size = 7989, upload-time = "2025-06-01T15:23:40.228Z" },
]
+[[package]]
+name = "decorator"
+version = "5.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" },
+]
+
[[package]]
name = "deprecated"
-version = "1.2.18"
+version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "wrapt" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744, upload-time = "2025-01-27T10:46:25.7Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998, upload-time = "2025-01-27T10:46:09.186Z" },
+ { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" },
]
[[package]]
@@ -133,31 +327,159 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" },
]
+[[package]]
+name = "executing"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cc/28/c14e053b6762b1044f34a13aab6859bbf40456d37d23aa286ac24cfd9a5d/executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4", size = 1129488, upload-time = "2025-09-01T09:48:10.866Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" },
+]
+
[[package]]
name = "filelock"
-version = "3.19.1"
+version = "3.20.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" },
+]
+
+[[package]]
+name = "fonttools"
+version = "4.60.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/4b/42/97a13e47a1e51a5a7142475bbcf5107fe3a68fc34aef331c897d5fb98ad0/fonttools-4.60.1.tar.gz", hash = "sha256:ef00af0439ebfee806b25f24c8f92109157ff3fac5731dc7867957812e87b8d9", size = 3559823, upload-time = "2025-09-29T21:13:27.129Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/f7/a10b101b7a6f8836a5adb47f2791f2075d044a6ca123f35985c42edc82d8/fonttools-4.60.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7b0c6d57ab00dae9529f3faf187f2254ea0aa1e04215cf2f1a8ec277c96661bc", size = 2832953, upload-time = "2025-09-29T21:11:39.616Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/fe/7bd094b59c926acf2304d2151354ddbeb74b94812f3dc943c231db09cb41/fonttools-4.60.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:839565cbf14645952d933853e8ade66a463684ed6ed6c9345d0faf1f0e868877", size = 2352706, upload-time = "2025-09-29T21:11:41.826Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/ca/4bb48a26ed95a1e7eba175535fe5805887682140ee0a0d10a88e1de84208/fonttools-4.60.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8177ec9676ea6e1793c8a084a90b65a9f778771998eb919d05db6d4b1c0b114c", size = 4923716, upload-time = "2025-09-29T21:11:43.893Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/9f/2cb82999f686c1d1ddf06f6ae1a9117a880adbec113611cc9d22b2fdd465/fonttools-4.60.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:996a4d1834524adbb423385d5a629b868ef9d774670856c63c9a0408a3063401", size = 4968175, upload-time = "2025-09-29T21:11:46.439Z" },
+ { url = "https://files.pythonhosted.org/packages/18/79/be569699e37d166b78e6218f2cde8c550204f2505038cdd83b42edc469b9/fonttools-4.60.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a46b2f450bc79e06ef3b6394f0c68660529ed51692606ad7f953fc2e448bc903", size = 4911031, upload-time = "2025-09-29T21:11:48.977Z" },
+ { url = "https://files.pythonhosted.org/packages/cc/9f/89411cc116effaec5260ad519162f64f9c150e5522a27cbb05eb62d0c05b/fonttools-4.60.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6ec722ee589e89a89f5b7574f5c45604030aa6ae24cb2c751e2707193b466fed", size = 5062966, upload-time = "2025-09-29T21:11:54.344Z" },
+ { url = "https://files.pythonhosted.org/packages/62/a1/f888221934b5731d46cb9991c7a71f30cb1f97c0ef5fcf37f8da8fce6c8e/fonttools-4.60.1-cp312-cp312-win32.whl", hash = "sha256:b2cf105cee600d2de04ca3cfa1f74f1127f8455b71dbad02b9da6ec266e116d6", size = 2218750, upload-time = "2025-09-29T21:11:56.601Z" },
+ { url = "https://files.pythonhosted.org/packages/88/8f/a55b5550cd33cd1028601df41acd057d4be20efa5c958f417b0c0613924d/fonttools-4.60.1-cp312-cp312-win_amd64.whl", hash = "sha256:992775c9fbe2cf794786fa0ffca7f09f564ba3499b8fe9f2f80bd7197db60383", size = 2267026, upload-time = "2025-09-29T21:11:58.852Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/5b/cdd2c612277b7ac7ec8c0c9bc41812c43dc7b2d5f2b0897e15fdf5a1f915/fonttools-4.60.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6f68576bb4bbf6060c7ab047b1574a1ebe5c50a17de62830079967b211059ebb", size = 2825777, upload-time = "2025-09-29T21:12:01.22Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/8a/de9cc0540f542963ba5e8f3a1f6ad48fa211badc3177783b9d5cadf79b5d/fonttools-4.60.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:eedacb5c5d22b7097482fa834bda0dafa3d914a4e829ec83cdea2a01f8c813c4", size = 2348080, upload-time = "2025-09-29T21:12:03.785Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/8b/371ab3cec97ee3fe1126b3406b7abd60c8fec8975fd79a3c75cdea0c3d83/fonttools-4.60.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b33a7884fabd72bdf5f910d0cf46be50dce86a0362a65cfc746a4168c67eb96c", size = 4903082, upload-time = "2025-09-29T21:12:06.382Z" },
+ { url = "https://files.pythonhosted.org/packages/04/05/06b1455e4bc653fcb2117ac3ef5fa3a8a14919b93c60742d04440605d058/fonttools-4.60.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2409d5fb7b55fd70f715e6d34e7a6e4f7511b8ad29a49d6df225ee76da76dd77", size = 4960125, upload-time = "2025-09-29T21:12:09.314Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/37/f3b840fcb2666f6cb97038793606bdd83488dca2d0b0fc542ccc20afa668/fonttools-4.60.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c8651e0d4b3bdeda6602b85fdc2abbefc1b41e573ecb37b6779c4ca50753a199", size = 4901454, upload-time = "2025-09-29T21:12:11.931Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/9e/eb76f77e82f8d4a46420aadff12cec6237751b0fb9ef1de373186dcffb5f/fonttools-4.60.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:145daa14bf24824b677b9357c5e44fd8895c2a8f53596e1b9ea3496081dc692c", size = 5044495, upload-time = "2025-09-29T21:12:15.241Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/b3/cede8f8235d42ff7ae891bae8d619d02c8ac9fd0cfc450c5927a6200c70d/fonttools-4.60.1-cp313-cp313-win32.whl", hash = "sha256:2299df884c11162617a66b7c316957d74a18e3758c0274762d2cc87df7bc0272", size = 2217028, upload-time = "2025-09-29T21:12:17.96Z" },
+ { url = "https://files.pythonhosted.org/packages/75/4d/b022c1577807ce8b31ffe055306ec13a866f2337ecee96e75b24b9b753ea/fonttools-4.60.1-cp313-cp313-win_amd64.whl", hash = "sha256:a3db56f153bd4c5c2b619ab02c5db5192e222150ce5a1bc10f16164714bc39ac", size = 2266200, upload-time = "2025-09-29T21:12:20.14Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/83/752ca11c1aa9a899b793a130f2e466b79ea0cf7279c8d79c178fc954a07b/fonttools-4.60.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:a884aef09d45ba1206712c7dbda5829562d3fea7726935d3289d343232ecb0d3", size = 2822830, upload-time = "2025-09-29T21:12:24.406Z" },
+ { url = "https://files.pythonhosted.org/packages/57/17/bbeab391100331950a96ce55cfbbff27d781c1b85ebafb4167eae50d9fe3/fonttools-4.60.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8a44788d9d91df72d1a5eac49b31aeb887a5f4aab761b4cffc4196c74907ea85", size = 2345524, upload-time = "2025-09-29T21:12:26.819Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/2e/d4831caa96d85a84dd0da1d9f90d81cec081f551e0ea216df684092c6c97/fonttools-4.60.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e852d9dda9f93ad3651ae1e3bb770eac544ec93c3807888798eccddf84596537", size = 4843490, upload-time = "2025-09-29T21:12:29.123Z" },
+ { url = "https://files.pythonhosted.org/packages/49/13/5e2ea7c7a101b6fc3941be65307ef8df92cbbfa6ec4804032baf1893b434/fonttools-4.60.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:154cb6ee417e417bf5f7c42fe25858c9140c26f647c7347c06f0cc2d47eff003", size = 4944184, upload-time = "2025-09-29T21:12:31.414Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/2b/cf9603551c525b73fc47c52ee0b82a891579a93d9651ed694e4e2cd08bb8/fonttools-4.60.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5664fd1a9ea7f244487ac8f10340c4e37664675e8667d6fee420766e0fb3cf08", size = 4890218, upload-time = "2025-09-29T21:12:33.936Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/2f/933d2352422e25f2376aae74f79eaa882a50fb3bfef3c0d4f50501267101/fonttools-4.60.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:583b7f8e3c49486e4d489ad1deacfb8d5be54a8ef34d6df824f6a171f8511d99", size = 4999324, upload-time = "2025-09-29T21:12:36.637Z" },
+ { url = "https://files.pythonhosted.org/packages/38/99/234594c0391221f66216bc2c886923513b3399a148defaccf81dc3be6560/fonttools-4.60.1-cp314-cp314-win32.whl", hash = "sha256:66929e2ea2810c6533a5184f938502cfdaea4bc3efb7130d8cc02e1c1b4108d6", size = 2220861, upload-time = "2025-09-29T21:12:39.108Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/1d/edb5b23726dde50fc4068e1493e4fc7658eeefcaf75d4c5ffce067d07ae5/fonttools-4.60.1-cp314-cp314-win_amd64.whl", hash = "sha256:f3d5be054c461d6a2268831f04091dc82753176f6ea06dc6047a5e168265a987", size = 2270934, upload-time = "2025-09-29T21:12:41.339Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/da/1392aaa2170adc7071fe7f9cfd181a5684a7afcde605aebddf1fb4d76df5/fonttools-4.60.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:b6379e7546ba4ae4b18f8ae2b9bc5960936007a1c0e30b342f662577e8bc3299", size = 2894340, upload-time = "2025-09-29T21:12:43.774Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/a7/3b9f16e010d536ce567058b931a20b590d8f3177b2eda09edd92e392375d/fonttools-4.60.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9d0ced62b59e0430b3690dbc5373df1c2aa7585e9a8ce38eff87f0fd993c5b01", size = 2375073, upload-time = "2025-09-29T21:12:46.437Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/b5/e9bcf51980f98e59bb5bb7c382a63c6f6cac0eec5f67de6d8f2322382065/fonttools-4.60.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:875cb7764708b3132637f6c5fb385b16eeba0f7ac9fa45a69d35e09b47045801", size = 4849758, upload-time = "2025-09-29T21:12:48.694Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/dc/1d2cf7d1cba82264b2f8385db3f5960e3d8ce756b4dc65b700d2c496f7e9/fonttools-4.60.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a184b2ea57b13680ab6d5fbde99ccef152c95c06746cb7718c583abd8f945ccc", size = 5085598, upload-time = "2025-09-29T21:12:51.081Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/4d/279e28ba87fb20e0c69baf72b60bbf1c4d873af1476806a7b5f2b7fac1ff/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:026290e4ec76583881763fac284aca67365e0be9f13a7fb137257096114cb3bc", size = 4957603, upload-time = "2025-09-29T21:12:53.423Z" },
+ { url = "https://files.pythonhosted.org/packages/78/d4/ff19976305e0c05aa3340c805475abb00224c954d3c65e82c0a69633d55d/fonttools-4.60.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f0e8817c7d1a0c2eedebf57ef9a9896f3ea23324769a9a2061a80fe8852705ed", size = 4974184, upload-time = "2025-09-29T21:12:55.962Z" },
+ { url = "https://files.pythonhosted.org/packages/63/22/8553ff6166f5cd21cfaa115aaacaa0dc73b91c079a8cfd54a482cbc0f4f5/fonttools-4.60.1-cp314-cp314t-win32.whl", hash = "sha256:1410155d0e764a4615774e5c2c6fc516259fe3eca5882f034eb9bfdbee056259", size = 2282241, upload-time = "2025-09-29T21:12:58.179Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/cb/fa7b4d148e11d5a72761a22e595344133e83a9507a4c231df972e657579b/fonttools-4.60.1-cp314-cp314t-win_amd64.whl", hash = "sha256:022beaea4b73a70295b688f817ddc24ed3e3418b5036ffcd5658141184ef0d0c", size = 2345760, upload-time = "2025-09-29T21:13:00.375Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/93/0dd45cd283c32dea1545151d8c3637b4b8c53cdb3a625aeb2885b184d74d/fonttools-4.60.1-py3-none-any.whl", hash = "sha256:906306ac7afe2156fcf0042173d6ebbb05416af70f6b370967b47f8f00103bbb", size = 1143175, upload-time = "2025-09-29T21:13:24.134Z" },
+]
+
+[[package]]
+name = "fsspec"
+version = "2025.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/7f/2747c0d332b9acfa75dc84447a066fdf812b5a6b8d30472b74d309bfe8cb/fsspec-2025.10.0.tar.gz", hash = "sha256:b6789427626f068f9a83ca4e8a3cc050850b6c0f71f99ddb4f542b8266a26a59", size = 309285, upload-time = "2025-10-30T14:58:44.036Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/02/a6b21098b1d5d6249b7c5ab69dde30108a71e4e819d4a9778f1de1d5b70d/fsspec-2025.10.0-py3-none-any.whl", hash = "sha256:7c7712353ae7d875407f97715f0e1ffcc21e33d5b24556cb1e090ae9409ec61d", size = 200966, upload-time = "2025-10-30T14:58:42.53Z" },
]
[[package]]
name = "identify"
-version = "2.6.14"
+version = "2.6.15"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/52/c4/62963f25a678f6a050fb0505a65e9e726996171e6dbe1547f79619eefb15/identify-2.6.14.tar.gz", hash = "sha256:663494103b4f717cb26921c52f8751363dc89db64364cd836a9bf1535f53cd6a", size = 99283, upload-time = "2025-09-06T19:30:52.938Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/ff/e7/685de97986c916a6d93b3876139e00eef26ad5bbbd61925d670ae8013449/identify-2.6.15.tar.gz", hash = "sha256:e4f4864b96c6557ef2a1e1c951771838f4edc9df3a72ec7118b338801b11c7bf", size = 99311, upload-time = "2025-10-02T17:43:40.631Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/e5/ae/2ad30f4652712c82f1c23423d79136fbce338932ad166d70c1efb86a5998/identify-2.6.14-py2.py3-none-any.whl", hash = "sha256:11a073da82212c6646b1f39bb20d4483bfb9543bd5566fec60053c4bb309bf2e", size = 99172, upload-time = "2025-09-06T19:30:51.759Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/1c/e5fd8f973d4f375adb21565739498e2e9a1e54c858a97b9a8ccfdc81da9b/identify-2.6.15-py2.py3-none-any.whl", hash = "sha256:1181ef7608e00704db228516541eb83a88a9f94433a8c80bb9b5bd54b1d81757", size = 99183, upload-time = "2025-10-02T17:43:39.137Z" },
]
[[package]]
name = "iniconfig"
-version = "2.1.0"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "ipykernel"
+version = "7.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "appnope", marker = "sys_platform == 'darwin'" },
+ { name = "comm" },
+ { name = "debugpy" },
+ { name = "ipython" },
+ { name = "jupyter-client" },
+ { name = "jupyter-core" },
+ { name = "matplotlib-inline" },
+ { name = "nest-asyncio" },
+ { name = "packaging" },
+ { name = "psutil" },
+ { name = "pyzmq" },
+ { name = "tornado" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/b9/a4/4948be6eb88628505b83a1f2f40d90254cab66abf2043b3c40fa07dfce0f/ipykernel-7.1.0.tar.gz", hash = "sha256:58a3fc88533d5930c3546dc7eac66c6d288acde4f801e2001e65edc5dc9cf0db", size = 174579, upload-time = "2025-10-27T09:46:39.471Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a3/17/20c2552266728ceba271967b87919664ecc0e33efca29c3efc6baf88c5f9/ipykernel-7.1.0-py3-none-any.whl", hash = "sha256:763b5ec6c5b7776f6a8d7ce09b267693b4e5ce75cb50ae696aaefb3c85e1ea4c", size = 117968, upload-time = "2025-10-27T09:46:37.805Z" },
+]
+
+[[package]]
+name = "ipython"
+version = "9.7.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "decorator" },
+ { name = "ipython-pygments-lexers" },
+ { name = "jedi" },
+ { name = "matplotlib-inline" },
+ { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
+ { name = "prompt-toolkit" },
+ { name = "pygments" },
+ { name = "stack-data" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/29/e6/48c74d54039241a456add616464ea28c6ebf782e4110d419411b83dae06f/ipython-9.7.0.tar.gz", hash = "sha256:5f6de88c905a566c6a9d6c400a8fed54a638e1f7543d17aae2551133216b1e4e", size = 4422115, upload-time = "2025-11-05T12:18:54.646Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/05/aa/62893d6a591d337aa59dcc4c6f6c842f1fe20cd72c8c5c1f980255243252/ipython-9.7.0-py3-none-any.whl", hash = "sha256:bce8ac85eb9521adc94e1845b4c03d88365fd6ac2f4908ec4ed1eb1b0a065f9f", size = 618911, upload-time = "2025-11-05T12:18:52.484Z" },
+]
+
+[[package]]
+name = "ipython-pygments-lexers"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pygments" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" },
+]
+
+[[package]]
+name = "jedi"
+version = "0.19.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
+dependencies = [
+ { name = "parso" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" },
]
[[package]]
@@ -181,42 +503,234 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" },
]
+[[package]]
+name = "jupyter-client"
+version = "8.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "jupyter-core" },
+ { name = "python-dateutil" },
+ { name = "pyzmq" },
+ { name = "tornado" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" },
+]
+
+[[package]]
+name = "jupyter-core"
+version = "5.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "platformdirs" },
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/02/49/9d1284d0dc65e2c757b74c6687b6d319b02f822ad039e5c512df9194d9dd/jupyter_core-5.9.1.tar.gz", hash = "sha256:4d09aaff303b9566c3ce657f580bd089ff5c91f5f89cf7d8846c3cdf465b5508", size = 89814, upload-time = "2025-10-16T19:19:18.444Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/e7/80988e32bf6f73919a113473a604f5a8f09094de312b9d52b79c2df7612b/jupyter_core-5.9.1-py3-none-any.whl", hash = "sha256:ebf87fdc6073d142e114c72c9e29a9d7ca03fad818c5d300ce2adc1fb0743407", size = 29032, upload-time = "2025-10-16T19:19:16.783Z" },
+]
+
+[[package]]
+name = "kiwisolver"
+version = "1.4.9"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" },
+ { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" },
+ { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" },
+ { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" },
+ { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" },
+ { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" },
+ { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" },
+ { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" },
+ { url = "https://files.pythonhosted.org/packages/31/c1/c2686cda909742ab66c7388e9a1a8521a59eb89f8bcfbee28fc980d07e24/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8", size = 123681, upload-time = "2025-08-10T21:26:26.725Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/f0/f44f50c9f5b1a1860261092e3bc91ecdc9acda848a8b8c6abfda4a24dd5c/kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2", size = 66464, upload-time = "2025-08-10T21:26:27.733Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/7a/9d90a151f558e29c3936b8a47ac770235f436f2120aca41a6d5f3d62ae8d/kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f", size = 64961, upload-time = "2025-08-10T21:26:28.729Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/e9/f218a2cb3a9ffbe324ca29a9e399fa2d2866d7f348ec3a88df87fc248fc5/kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098", size = 1474607, upload-time = "2025-08-10T21:26:29.798Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/28/aac26d4c882f14de59041636292bc838db8961373825df23b8eeb807e198/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed", size = 1276546, upload-time = "2025-08-10T21:26:31.401Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/ad/8bfc1c93d4cc565e5069162f610ba2f48ff39b7de4b5b8d93f69f30c4bed/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525", size = 1294482, upload-time = "2025-08-10T21:26:32.721Z" },
+ { url = "https://files.pythonhosted.org/packages/da/f1/6aca55ff798901d8ce403206d00e033191f63d82dd708a186e0ed2067e9c/kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78", size = 1343720, upload-time = "2025-08-10T21:26:34.032Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/91/eed031876c595c81d90d0f6fc681ece250e14bf6998c3d7c419466b523b7/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b", size = 2224907, upload-time = "2025-08-10T21:26:35.824Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/ec/4d1925f2e49617b9cca9c34bfa11adefad49d00db038e692a559454dfb2e/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799", size = 2321334, upload-time = "2025-08-10T21:26:37.534Z" },
+ { url = "https://files.pythonhosted.org/packages/43/cb/450cd4499356f68802750c6ddc18647b8ea01ffa28f50d20598e0befe6e9/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3", size = 2488313, upload-time = "2025-08-10T21:26:39.191Z" },
+ { url = "https://files.pythonhosted.org/packages/71/67/fc76242bd99f885651128a5d4fa6083e5524694b7c88b489b1b55fdc491d/kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c", size = 2291970, upload-time = "2025-08-10T21:26:40.828Z" },
+ { url = "https://files.pythonhosted.org/packages/75/bd/f1a5d894000941739f2ae1b65a32892349423ad49c2e6d0771d0bad3fae4/kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d", size = 73894, upload-time = "2025-08-10T21:26:42.33Z" },
+ { url = "https://files.pythonhosted.org/packages/95/38/dce480814d25b99a391abbddadc78f7c117c6da34be68ca8b02d5848b424/kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2", size = 64995, upload-time = "2025-08-10T21:26:43.889Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/37/7d218ce5d92dadc5ebdd9070d903e0c7cf7edfe03f179433ac4d13ce659c/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1", size = 126510, upload-time = "2025-08-10T21:26:44.915Z" },
+ { url = "https://files.pythonhosted.org/packages/23/b0/e85a2b48233daef4b648fb657ebbb6f8367696a2d9548a00b4ee0eb67803/kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1", size = 67903, upload-time = "2025-08-10T21:26:45.934Z" },
+ { url = "https://files.pythonhosted.org/packages/44/98/f2425bc0113ad7de24da6bb4dae1343476e95e1d738be7c04d31a5d037fd/kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11", size = 66402, upload-time = "2025-08-10T21:26:47.101Z" },
+ { url = "https://files.pythonhosted.org/packages/98/d8/594657886df9f34c4177cc353cc28ca7e6e5eb562d37ccc233bff43bbe2a/kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c", size = 1582135, upload-time = "2025-08-10T21:26:48.665Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/c6/38a115b7170f8b306fc929e166340c24958347308ea3012c2b44e7e295db/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197", size = 1389409, upload-time = "2025-08-10T21:26:50.335Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/3b/e04883dace81f24a568bcee6eb3001da4ba05114afa622ec9b6fafdc1f5e/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c", size = 1401763, upload-time = "2025-08-10T21:26:51.867Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/80/20ace48e33408947af49d7d15c341eaee69e4e0304aab4b7660e234d6288/kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185", size = 1453643, upload-time = "2025-08-10T21:26:53.592Z" },
+ { url = "https://files.pythonhosted.org/packages/64/31/6ce4380a4cd1f515bdda976a1e90e547ccd47b67a1546d63884463c92ca9/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748", size = 2330818, upload-time = "2025-08-10T21:26:55.051Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/e9/3f3fcba3bcc7432c795b82646306e822f3fd74df0ee81f0fa067a1f95668/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64", size = 2419963, upload-time = "2025-08-10T21:26:56.421Z" },
+ { url = "https://files.pythonhosted.org/packages/99/43/7320c50e4133575c66e9f7dadead35ab22d7c012a3b09bb35647792b2a6d/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff", size = 2594639, upload-time = "2025-08-10T21:26:57.882Z" },
+ { url = "https://files.pythonhosted.org/packages/65/d6/17ae4a270d4a987ef8a385b906d2bdfc9fce502d6dc0d3aea865b47f548c/kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07", size = 2391741, upload-time = "2025-08-10T21:26:59.237Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/8f/8f6f491d595a9e5912971f3f863d81baddccc8a4d0c3749d6a0dd9ffc9df/kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c", size = 68646, upload-time = "2025-08-10T21:27:00.52Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/32/6cc0fbc9c54d06c2969faa9c1d29f5751a2e51809dd55c69055e62d9b426/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386", size = 123806, upload-time = "2025-08-10T21:27:01.537Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/dd/2bfb1d4a4823d92e8cbb420fe024b8d2167f72079b3bb941207c42570bdf/kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552", size = 66605, upload-time = "2025-08-10T21:27:03.335Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/69/00aafdb4e4509c2ca6064646cba9cd4b37933898f426756adb2cb92ebbed/kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3", size = 64925, upload-time = "2025-08-10T21:27:04.339Z" },
+ { url = "https://files.pythonhosted.org/packages/43/dc/51acc6791aa14e5cb6d8a2e28cefb0dc2886d8862795449d021334c0df20/kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58", size = 1472414, upload-time = "2025-08-10T21:27:05.437Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/bb/93fa64a81db304ac8a246f834d5094fae4b13baf53c839d6bb6e81177129/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4", size = 1281272, upload-time = "2025-08-10T21:27:07.063Z" },
+ { url = "https://files.pythonhosted.org/packages/70/e6/6df102916960fb8d05069d4bd92d6d9a8202d5a3e2444494e7cd50f65b7a/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df", size = 1298578, upload-time = "2025-08-10T21:27:08.452Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/47/e142aaa612f5343736b087864dbaebc53ea8831453fb47e7521fa8658f30/kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6", size = 1345607, upload-time = "2025-08-10T21:27:10.125Z" },
+ { url = "https://files.pythonhosted.org/packages/54/89/d641a746194a0f4d1a3670fb900d0dbaa786fb98341056814bc3f058fa52/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5", size = 2230150, upload-time = "2025-08-10T21:27:11.484Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/6b/5ee1207198febdf16ac11f78c5ae40861b809cbe0e6d2a8d5b0b3044b199/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf", size = 2325979, upload-time = "2025-08-10T21:27:12.917Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/ff/b269eefd90f4ae14dcc74973d5a0f6d28d3b9bb1afd8c0340513afe6b39a/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5", size = 2491456, upload-time = "2025-08-10T21:27:14.353Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/d4/10303190bd4d30de547534601e259a4fbf014eed94aae3e5521129215086/kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce", size = 2294621, upload-time = "2025-08-10T21:27:15.808Z" },
+ { url = "https://files.pythonhosted.org/packages/28/e0/a9a90416fce5c0be25742729c2ea52105d62eda6c4be4d803c2a7be1fa50/kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7", size = 75417, upload-time = "2025-08-10T21:27:17.436Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/10/6949958215b7a9a264299a7db195564e87900f709db9245e4ebdd3c70779/kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c", size = 66582, upload-time = "2025-08-10T21:27:18.436Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/79/60e53067903d3bc5469b369fe0dfc6b3482e2133e85dae9daa9527535991/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548", size = 126514, upload-time = "2025-08-10T21:27:19.465Z" },
+ { url = "https://files.pythonhosted.org/packages/25/d1/4843d3e8d46b072c12a38c97c57fab4608d36e13fe47d47ee96b4d61ba6f/kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d", size = 67905, upload-time = "2025-08-10T21:27:20.51Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/ae/29ffcbd239aea8b93108de1278271ae764dfc0d803a5693914975f200596/kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c", size = 66399, upload-time = "2025-08-10T21:27:21.496Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/ae/d7ba902aa604152c2ceba5d352d7b62106bedbccc8e95c3934d94472bfa3/kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122", size = 1582197, upload-time = "2025-08-10T21:27:22.604Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/41/27c70d427eddb8bc7e4f16420a20fefc6f480312122a59a959fdfe0445ad/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64", size = 1390125, upload-time = "2025-08-10T21:27:24.036Z" },
+ { url = "https://files.pythonhosted.org/packages/41/42/b3799a12bafc76d962ad69083f8b43b12bf4fe78b097b12e105d75c9b8f1/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134", size = 1402612, upload-time = "2025-08-10T21:27:25.773Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/b5/a210ea073ea1cfaca1bb5c55a62307d8252f531beb364e18aa1e0888b5a0/kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370", size = 1453990, upload-time = "2025-08-10T21:27:27.089Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/ce/a829eb8c033e977d7ea03ed32fb3c1781b4fa0433fbadfff29e39c676f32/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21", size = 2331601, upload-time = "2025-08-10T21:27:29.343Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/4b/b5e97eb142eb9cd0072dacfcdcd31b1c66dc7352b0f7c7255d339c0edf00/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a", size = 2422041, upload-time = "2025-08-10T21:27:30.754Z" },
+ { url = "https://files.pythonhosted.org/packages/40/be/8eb4cd53e1b85ba4edc3a9321666f12b83113a178845593307a3e7891f44/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f", size = 2594897, upload-time = "2025-08-10T21:27:32.803Z" },
+ { url = "https://files.pythonhosted.org/packages/99/dd/841e9a66c4715477ea0abc78da039832fbb09dac5c35c58dc4c41a407b8a/kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369", size = 2391835, upload-time = "2025-08-10T21:27:34.23Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/28/4b2e5c47a0da96896fdfdb006340ade064afa1e63675d01ea5ac222b6d52/kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891", size = 79988, upload-time = "2025-08-10T21:27:35.587Z" },
+ { url = "https://files.pythonhosted.org/packages/80/be/3578e8afd18c88cdf9cb4cffde75a96d2be38c5a903f1ed0ceec061bd09e/kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32", size = 70260, upload-time = "2025-08-10T21:27:36.606Z" },
+]
+
[[package]]
name = "markupsafe"
-version = "3.0.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" },
- { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" },
- { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" },
- { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" },
- { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" },
- { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" },
- { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" },
- { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" },
- { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" },
- { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" },
- { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" },
- { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" },
- { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" },
- { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" },
- { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" },
- { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" },
- { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" },
- { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" },
- { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" },
- { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" },
- { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" },
- { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" },
- { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" },
- { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" },
- { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" },
- { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" },
- { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" },
- { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" },
- { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" },
- { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" },
+version = "3.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
+ { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
+ { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
+ { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
+ { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" },
+ { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" },
+ { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" },
+ { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" },
+ { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" },
+ { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" },
+ { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" },
+ { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" },
+ { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" },
+ { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" },
+ { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" },
+ { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" },
+ { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" },
+ { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" },
+ { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" },
+ { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" },
+ { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" },
+ { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" },
+ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" },
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.10.7"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "contourpy" },
+ { name = "cycler" },
+ { name = "fonttools" },
+ { name = "kiwisolver" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pillow" },
+ { name = "pyparsing" },
+ { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ae/e2/d2d5295be2f44c678ebaf3544ba32d20c1f9ef08c49fe47f496180e1db15/matplotlib-3.10.7.tar.gz", hash = "sha256:a06ba7e2a2ef9131c79c49e63dad355d2d878413a0376c1727c8b9335ff731c7", size = 34804865, upload-time = "2025-10-09T00:28:00.669Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/be/b3/09eb0f7796932826ec20c25b517d568627754f6c6462fca19e12c02f2e12/matplotlib-3.10.7-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7a0edb7209e21840e8361e91ea84ea676658aa93edd5f8762793dec77a4a6748", size = 8272389, upload-time = "2025-10-09T00:26:42.474Z" },
+ { url = "https://files.pythonhosted.org/packages/11/0b/1ae80ddafb8652fd8046cb5c8460ecc8d4afccb89e2c6d6bec61e04e1eaf/matplotlib-3.10.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c380371d3c23e0eadf8ebff114445b9f970aff2010198d498d4ab4c3b41eea4f", size = 8128247, upload-time = "2025-10-09T00:26:44.77Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/18/95ae2e242d4a5c98bd6e90e36e128d71cf1c7e39b0874feaed3ef782e789/matplotlib-3.10.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d5f256d49fea31f40f166a5e3131235a5d2f4b7f44520b1cf0baf1ce568ccff0", size = 8696996, upload-time = "2025-10-09T00:26:46.792Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/3d/5b559efc800bd05cb2033aa85f7e13af51958136a48327f7c261801ff90a/matplotlib-3.10.7-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11ae579ac83cdf3fb72573bb89f70e0534de05266728740d478f0f818983c695", size = 9530153, upload-time = "2025-10-09T00:26:49.07Z" },
+ { url = "https://files.pythonhosted.org/packages/88/57/eab4a719fd110312d3c220595d63a3c85ec2a39723f0f4e7fa7e6e3f74ba/matplotlib-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4c14b6acd16cddc3569a2d515cfdd81c7a68ac5639b76548cfc1a9e48b20eb65", size = 9593093, upload-time = "2025-10-09T00:26:51.067Z" },
+ { url = "https://files.pythonhosted.org/packages/31/3c/80816f027b3a4a28cd2a0a6ef7f89a2db22310e945cd886ec25bfb399221/matplotlib-3.10.7-cp312-cp312-win_amd64.whl", hash = "sha256:0d8c32b7ea6fb80b1aeff5a2ceb3fb9778e2759e899d9beff75584714afcc5ee", size = 8122771, upload-time = "2025-10-09T00:26:53.296Z" },
+ { url = "https://files.pythonhosted.org/packages/de/77/ef1fc78bfe99999b2675435cc52120887191c566b25017d78beaabef7f2d/matplotlib-3.10.7-cp312-cp312-win_arm64.whl", hash = "sha256:5f3f6d315dcc176ba7ca6e74c7768fb7e4cf566c49cb143f6bc257b62e634ed8", size = 7992812, upload-time = "2025-10-09T00:26:54.882Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9c/207547916a02c78f6bdd83448d9b21afbc42f6379ed887ecf610984f3b4e/matplotlib-3.10.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1d9d3713a237970569156cfb4de7533b7c4eacdd61789726f444f96a0d28f57f", size = 8273212, upload-time = "2025-10-09T00:26:56.752Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/d0/b3d3338d467d3fc937f0bb7f256711395cae6f78e22cef0656159950adf0/matplotlib-3.10.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:37a1fea41153dd6ee061d21ab69c9cf2cf543160b1b85d89cd3d2e2a7902ca4c", size = 8128713, upload-time = "2025-10-09T00:26:59.001Z" },
+ { url = "https://files.pythonhosted.org/packages/22/ff/6425bf5c20d79aa5b959d1ce9e65f599632345391381c9a104133fe0b171/matplotlib-3.10.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b3c4ea4948d93c9c29dc01c0c23eef66f2101bf75158c291b88de6525c55c3d1", size = 8698527, upload-time = "2025-10-09T00:27:00.69Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/7f/ccdca06f4c2e6c7989270ed7829b8679466682f4cfc0f8c9986241c023b6/matplotlib-3.10.7-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22df30ffaa89f6643206cf13877191c63a50e8f800b038bc39bee9d2d4957632", size = 9529690, upload-time = "2025-10-09T00:27:02.664Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/95/b80fc2c1f269f21ff3d193ca697358e24408c33ce2b106a7438a45407b63/matplotlib-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b69676845a0a66f9da30e87f48be36734d6748024b525ec4710be40194282c84", size = 9593732, upload-time = "2025-10-09T00:27:04.653Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/b6/23064a96308b9aeceeffa65e96bcde459a2ea4934d311dee20afde7407a0/matplotlib-3.10.7-cp313-cp313-win_amd64.whl", hash = "sha256:744991e0cc863dd669c8dc9136ca4e6e0082be2070b9d793cbd64bec872a6815", size = 8122727, upload-time = "2025-10-09T00:27:06.814Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/a6/2faaf48133b82cf3607759027f82b5c702aa99cdfcefb7f93d6ccf26a424/matplotlib-3.10.7-cp313-cp313-win_arm64.whl", hash = "sha256:fba2974df0bf8ce3c995fa84b79cde38326e0f7b5409e7a3a481c1141340bcf7", size = 7992958, upload-time = "2025-10-09T00:27:08.567Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/f0/b018fed0b599bd48d84c08794cb242227fe3341952da102ee9d9682db574/matplotlib-3.10.7-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:932c55d1fa7af4423422cb6a492a31cbcbdbe68fd1a9a3f545aa5e7a143b5355", size = 8316849, upload-time = "2025-10-09T00:27:10.254Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/b7/bb4f23856197659f275e11a2a164e36e65e9b48ea3e93c4ec25b4f163198/matplotlib-3.10.7-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e38c2d581d62ee729a6e144c47a71b3f42fb4187508dbbf4fe71d5612c3433b", size = 8178225, upload-time = "2025-10-09T00:27:12.241Z" },
+ { url = "https://files.pythonhosted.org/packages/62/56/0600609893ff277e6f3ab3c0cef4eafa6e61006c058e84286c467223d4d5/matplotlib-3.10.7-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:786656bb13c237bbcebcd402f65f44dd61ead60ee3deb045af429d889c8dbc67", size = 8711708, upload-time = "2025-10-09T00:27:13.879Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/1a/6bfecb0cafe94d6658f2f1af22c43b76cf7a1c2f0dc34ef84cbb6809617e/matplotlib-3.10.7-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09d7945a70ea43bf9248f4b6582734c2fe726723204a76eca233f24cffc7ef67", size = 9541409, upload-time = "2025-10-09T00:27:15.684Z" },
+ { url = "https://files.pythonhosted.org/packages/08/50/95122a407d7f2e446fd865e2388a232a23f2b81934960ea802f3171518e4/matplotlib-3.10.7-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d0b181e9fa8daf1d9f2d4c547527b167cb8838fc587deabca7b5c01f97199e84", size = 9594054, upload-time = "2025-10-09T00:27:17.547Z" },
+ { url = "https://files.pythonhosted.org/packages/13/76/75b194a43b81583478a81e78a07da8d9ca6ddf50dd0a2ccabf258059481d/matplotlib-3.10.7-cp313-cp313t-win_amd64.whl", hash = "sha256:31963603041634ce1a96053047b40961f7a29eb8f9a62e80cc2c0427aa1d22a2", size = 8200100, upload-time = "2025-10-09T00:27:20.039Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/9e/6aefebdc9f8235c12bdeeda44cc0383d89c1e41da2c400caf3ee2073a3ce/matplotlib-3.10.7-cp313-cp313t-win_arm64.whl", hash = "sha256:aebed7b50aa6ac698c90f60f854b47e48cd2252b30510e7a1feddaf5a3f72cbf", size = 8042131, upload-time = "2025-10-09T00:27:21.608Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/4b/e5bc2c321b6a7e3a75638d937d19ea267c34bd5a90e12bee76c4d7c7a0d9/matplotlib-3.10.7-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d883460c43e8c6b173fef244a2341f7f7c0e9725c7fe68306e8e44ed9c8fb100", size = 8273787, upload-time = "2025-10-09T00:27:23.27Z" },
+ { url = "https://files.pythonhosted.org/packages/86/ad/6efae459c56c2fbc404da154e13e3a6039129f3c942b0152624f1c621f05/matplotlib-3.10.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07124afcf7a6504eafcb8ce94091c5898bbdd351519a1beb5c45f7a38c67e77f", size = 8131348, upload-time = "2025-10-09T00:27:24.926Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/5a/a4284d2958dee4116359cc05d7e19c057e64ece1b4ac986ab0f2f4d52d5a/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c17398b709a6cce3d9fdb1595c33e356d91c098cd9486cb2cc21ea2ea418e715", size = 9533949, upload-time = "2025-10-09T00:27:26.704Z" },
+ { url = "https://files.pythonhosted.org/packages/de/ff/f3781b5057fa3786623ad8976fc9f7b0d02b2f28534751fd5a44240de4cf/matplotlib-3.10.7-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7146d64f561498764561e9cd0ed64fcf582e570fc519e6f521e2d0cfd43365e1", size = 9804247, upload-time = "2025-10-09T00:27:28.514Z" },
+ { url = "https://files.pythonhosted.org/packages/47/5a/993a59facb8444efb0e197bf55f545ee449902dcee86a4dfc580c3b61314/matplotlib-3.10.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:90ad854c0a435da3104c01e2c6f0028d7e719b690998a2333d7218db80950722", size = 9595497, upload-time = "2025-10-09T00:27:30.418Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/a5/77c95aaa9bb32c345cbb49626ad8eb15550cba2e6d4c88081a6c2ac7b08d/matplotlib-3.10.7-cp314-cp314-win_amd64.whl", hash = "sha256:4645fc5d9d20ffa3a39361fcdbcec731382763b623b72627806bf251b6388866", size = 8252732, upload-time = "2025-10-09T00:27:32.332Z" },
+ { url = "https://files.pythonhosted.org/packages/74/04/45d269b4268d222390d7817dae77b159651909669a34ee9fdee336db5883/matplotlib-3.10.7-cp314-cp314-win_arm64.whl", hash = "sha256:9257be2f2a03415f9105c486d304a321168e61ad450f6153d77c69504ad764bb", size = 8124240, upload-time = "2025-10-09T00:27:33.94Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/c7/ca01c607bb827158b439208c153d6f14ddb9fb640768f06f7ca3488ae67b/matplotlib-3.10.7-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1e4bbad66c177a8fdfa53972e5ef8be72a5f27e6a607cec0d8579abd0f3102b1", size = 8316938, upload-time = "2025-10-09T00:27:35.534Z" },
+ { url = "https://files.pythonhosted.org/packages/84/d2/5539e66e9f56d2fdec94bb8436f5e449683b4e199bcc897c44fbe3c99e28/matplotlib-3.10.7-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d8eb7194b084b12feb19142262165832fc6ee879b945491d1c3d4660748020c4", size = 8178245, upload-time = "2025-10-09T00:27:37.334Z" },
+ { url = "https://files.pythonhosted.org/packages/77/b5/e6ca22901fd3e4fe433a82e583436dd872f6c966fca7e63cf806b40356f8/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d41379b05528091f00e1728004f9a8d7191260f3862178b88e8fd770206318", size = 9541411, upload-time = "2025-10-09T00:27:39.387Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/99/a4524db57cad8fee54b7237239a8f8360bfcfa3170d37c9e71c090c0f409/matplotlib-3.10.7-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a74f79fafb2e177f240579bc83f0b60f82cc47d2f1d260f422a0627207008ca", size = 9803664, upload-time = "2025-10-09T00:27:41.492Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/a5/85e2edf76ea0ad4288d174926d9454ea85f3ce5390cc4e6fab196cbf250b/matplotlib-3.10.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:702590829c30aada1e8cef0568ddbffa77ca747b4d6e36c6d173f66e301f89cc", size = 9594066, upload-time = "2025-10-09T00:27:43.694Z" },
+ { url = "https://files.pythonhosted.org/packages/39/69/9684368a314f6d83fe5c5ad2a4121a3a8e03723d2e5c8ea17b66c1bad0e7/matplotlib-3.10.7-cp314-cp314t-win_amd64.whl", hash = "sha256:f79d5de970fc90cd5591f60053aecfce1fcd736e0303d9f0bf86be649fa68fb8", size = 8342832, upload-time = "2025-10-09T00:27:45.543Z" },
+ { url = "https://files.pythonhosted.org/packages/04/5f/e22e08da14bc1a0894184640d47819d2338b792732e20d292bf86e5ab785/matplotlib-3.10.7-cp314-cp314t-win_arm64.whl", hash = "sha256:cb783436e47fcf82064baca52ce748af71725d0352e1d31564cbe9c95df92b9c", size = 8172585, upload-time = "2025-10-09T00:27:47.185Z" },
+]
+
+[[package]]
+name = "matplotlib-inline"
+version = "0.2.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "traitlets" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c7/74/97e72a36efd4ae2bccb3463284300f8953f199b5ffbc04cbbb0ec78f74b1/matplotlib_inline-0.2.1.tar.gz", hash = "sha256:e1ee949c340d771fc39e241ea75683deb94762c8fa5f2927ec57c83c4dffa9fe", size = 8110, upload-time = "2025-10-23T09:00:22.126Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/af/33/ee4519fa02ed11a94aef9559552f3b17bb863f2ecfe1a35dc7f548cde231/matplotlib_inline-0.2.1-py3-none-any.whl", hash = "sha256:d56ce5156ba6085e00a9d54fead6ed29a9c47e215cd1bba2e976ef39f5710a76", size = 9516, upload-time = "2025-10-23T09:00:20.675Z" },
]
[[package]]
@@ -224,10 +738,19 @@ name = "ml-project"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
+ { name = "ipykernel" },
+ { name = "matplotlib" },
{ name = "numpy" },
+ { name = "opencv-python" },
{ name = "pandas" },
+ { name = "pillow" },
{ name = "pydantic" },
{ name = "scikit-learn" },
+ { name = "seaborn" },
+ { name = "torch" },
+ { name = "torchvision" },
+ { name = "tqdm" },
+ { name = "types-pyyaml" },
]
[package.dev-dependencies]
@@ -241,10 +764,19 @@ dev = [
[package.metadata]
requires-dist = [
+ { name = "ipykernel", specifier = ">=7.1.0" },
+ { name = "matplotlib", specifier = ">=3.10.7" },
{ name = "numpy", specifier = ">=2.3.3" },
+ { name = "opencv-python", specifier = ">=4.11.0.86" },
{ name = "pandas", specifier = ">=2.3.2" },
+ { name = "pillow", specifier = ">=12.0.0" },
{ name = "pydantic", specifier = ">=2.11.9" },
{ name = "scikit-learn", specifier = ">=1.7.2" },
+ { name = "seaborn", specifier = ">=0.13.2" },
+ { name = "torch", specifier = ">=2.9.0" },
+ { name = "torchvision", specifier = ">=0.24.0" },
+ { name = "tqdm", specifier = ">=4.67.1" },
+ { name = "types-pyyaml", specifier = ">=6.0.12.20250915" },
]
[package.metadata.requires-dev]
@@ -256,6 +788,15 @@ dev = [
{ name = "ruff", specifier = ">=0.13.1" },
]
+[[package]]
+name = "mpmath"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" },
+]
+
[[package]]
name = "mypy"
version = "1.18.2"
@@ -297,6 +838,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
+[[package]]
+name = "nest-asyncio"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" },
+]
+
+[[package]]
+name = "networkx"
+version = "3.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" },
+]
+
[[package]]
name = "nodeenv"
version = "1.9.1"
@@ -369,6 +928,157 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/06/b9/33bba5ff6fb679aa0b1f8a07e853f002a6b04b9394db3069a1270a7784ca/numpy-2.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:78c9f6560dc7e6b3990e32df7ea1a50bbd0e2a111e05209963f5ddcab7073b0b", size = 10545953, upload-time = "2025-09-09T15:58:40.576Z" },
]
+[[package]]
+name = "nvidia-cublas-cu12"
+version = "12.8.4.1"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-cupti-cu12"
+version = "12.8.90"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-nvrtc-cu12"
+version = "12.8.93"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" },
+]
+
+[[package]]
+name = "nvidia-cuda-runtime-cu12"
+version = "12.8.90"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" },
+]
+
+[[package]]
+name = "nvidia-cudnn-cu12"
+version = "9.10.2.21"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" },
+]
+
+[[package]]
+name = "nvidia-cufft-cu12"
+version = "11.3.3.83"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" },
+]
+
+[[package]]
+name = "nvidia-cufile-cu12"
+version = "1.13.1.3"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" },
+]
+
+[[package]]
+name = "nvidia-curand-cu12"
+version = "10.3.9.90"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" },
+]
+
+[[package]]
+name = "nvidia-cusolver-cu12"
+version = "11.7.3.90"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
+ { name = "nvidia-cusparse-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
+ { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" },
+]
+
+[[package]]
+name = "nvidia-cusparse-cu12"
+version = "12.5.8.93"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" },
+]
+
+[[package]]
+name = "nvidia-cusparselt-cu12"
+version = "0.7.1"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" },
+]
+
+[[package]]
+name = "nvidia-nccl-cu12"
+version = "2.27.5"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" },
+]
+
+[[package]]
+name = "nvidia-nvjitlink-cu12"
+version = "12.8.93"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" },
+]
+
+[[package]]
+name = "nvidia-nvshmem-cu12"
+version = "3.3.20"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/6c/99acb2f9eb85c29fc6f3a7ac4dccfd992e22666dd08a642b303311326a97/nvidia_nvshmem_cu12-3.3.20-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d00f26d3f9b2e3c3065be895e3059d6479ea5c638a3f38c9fec49b1b9dd7c1e5", size = 124657145, upload-time = "2025-08-04T20:25:19.995Z" },
+]
+
+[[package]]
+name = "nvidia-nvtx-cu12"
+version = "12.8.90"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" },
+]
+
+[[package]]
+name = "opencv-python"
+version = "4.11.0.86"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956, upload-time = "2025-01-16T13:52:24.737Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/05/4d/53b30a2a3ac1f75f65a59eb29cf2ee7207ce64867db47036ad61743d5a23/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:432f67c223f1dc2824f5e73cdfcd9db0efc8710647d4e813012195dc9122a52a", size = 37326322, upload-time = "2025-01-16T13:52:25.887Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/84/0a67490741867eacdfa37bc18df96e08a9d579583b419010d7f3da8ff503/opencv_python-4.11.0.86-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:9d05ef13d23fe97f575153558653e2d6e87103995d54e6a35db3f282fe1f9c66", size = 56723197, upload-time = "2025-01-16T13:55:21.222Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/bd/29c126788da65c1fb2b5fb621b7fed0ed5f9122aa22a0868c5e2c15c6d23/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1b92ae2c8852208817e6776ba1ea0d6b1e0a1b5431e971a2a0ddd2a8cc398202", size = 42230439, upload-time = "2025-01-16T13:51:35.822Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/8b/90eb44a40476fa0e71e05a0283947cfd74a5d36121a11d926ad6f3193cc4/opencv_python-4.11.0.86-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b02611523803495003bd87362db3e1d2a0454a6a63025dc6658a9830570aa0d", size = 62986597, upload-time = "2025-01-16T13:52:08.836Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/d7/1d5941a9dde095468b288d989ff6539dd69cd429dbf1b9e839013d21b6f0/opencv_python-4.11.0.86-cp37-abi3-win32.whl", hash = "sha256:810549cb2a4aedaa84ad9a1c92fbfdfc14090e2749cedf2c1589ad8359aa169b", size = 29384337, upload-time = "2025-01-16T13:52:13.549Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/7d/f1c30a92854540bf789e9cd5dde7ef49bbe63f855b85a2e6b3db8135c591/opencv_python-4.11.0.86-cp37-abi3-win_amd64.whl", hash = "sha256:085ad9b77c18853ea66283e98affefe2de8cc4c1f43eda4c100cf9b2721142ec", size = 39488044, upload-time = "2025-01-16T13:52:21.928Z" },
+]
+
[[package]]
name = "packaging"
version = "25.0"
@@ -412,6 +1122,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cd/d7/612123674d7b17cf345aad0a10289b2a384bff404e0463a83c4a3a59d205/pandas-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d2c3554bd31b731cd6490d94a28f3abb8dd770634a9e06eb6d2911b9827db370", size = 13186141, upload-time = "2025-08-21T10:28:05.377Z" },
]
+[[package]]
+name = "parso"
+version = "0.8.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d4/de/53e0bcf53d13e005bd8c92e7855142494f41171b34c2536b86187474184d/parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a", size = 401205, upload-time = "2025-08-23T15:15:28.028Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/16/32/f8e3c85d1d5250232a5d3477a2a28cc291968ff175caeadaf3cc19ce0e4a/parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887", size = 106668, upload-time = "2025-08-23T15:15:25.663Z" },
+]
+
[[package]]
name = "pathspec"
version = "0.12.1"
@@ -421,13 +1140,94 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" },
]
+[[package]]
+name = "pexpect"
+version = "4.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "ptyprocess" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" },
+]
+
+[[package]]
+name = "pillow"
+version = "12.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/cace85a1b0c9775a9f8f5d5423c8261c858760e2466c79b2dd184638b056/pillow-12.0.0.tar.gz", hash = "sha256:87d4f8125c9988bfbed67af47dd7a953e2fc7b0cc1e7800ec6d2080d490bb353", size = 47008828, upload-time = "2025-10-15T18:24:14.008Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2c/90/4fcce2c22caf044e660a198d740e7fbc14395619e3cb1abad12192c0826c/pillow-12.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:53561a4ddc36facb432fae7a9d8afbfaf94795414f5cdc5fc52f28c1dca90371", size = 5249377, upload-time = "2025-10-15T18:22:05.993Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/e0/ed960067543d080691d47d6938ebccbf3976a931c9567ab2fbfab983a5dd/pillow-12.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71db6b4c1653045dacc1585c1b0d184004f0d7e694c7b34ac165ca70c0838082", size = 4650343, upload-time = "2025-10-15T18:22:07.718Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/a1/f81fdeddcb99c044bf7d6faa47e12850f13cee0849537a7d27eeab5534d4/pillow-12.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2fa5f0b6716fc88f11380b88b31fe591a06c6315e955c096c35715788b339e3f", size = 6232981, upload-time = "2025-10-15T18:22:09.287Z" },
+ { url = "https://files.pythonhosted.org/packages/88/e1/9098d3ce341a8750b55b0e00c03f1630d6178f38ac191c81c97a3b047b44/pillow-12.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:82240051c6ca513c616f7f9da06e871f61bfd7805f566275841af15015b8f98d", size = 8041399, upload-time = "2025-10-15T18:22:10.872Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/62/a22e8d3b602ae8cc01446d0c57a54e982737f44b6f2e1e019a925143771d/pillow-12.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55f818bd74fe2f11d4d7cbc65880a843c4075e0ac7226bc1a23261dbea531953", size = 6347740, upload-time = "2025-10-15T18:22:12.769Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/87/424511bdcd02c8d7acf9f65caa09f291a519b16bd83c3fb3374b3d4ae951/pillow-12.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b87843e225e74576437fd5b6a4c2205d422754f84a06942cfaf1dc32243e45a8", size = 7040201, upload-time = "2025-10-15T18:22:14.813Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/4d/435c8ac688c54d11755aedfdd9f29c9eeddf68d150fe42d1d3dbd2365149/pillow-12.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c607c90ba67533e1b2355b821fef6764d1dd2cbe26b8c1005ae84f7aea25ff79", size = 6462334, upload-time = "2025-10-15T18:22:16.375Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/f2/ad34167a8059a59b8ad10bc5c72d4d9b35acc6b7c0877af8ac885b5f2044/pillow-12.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:21f241bdd5080a15bc86d3466a9f6074a9c2c2b314100dd896ac81ee6db2f1ba", size = 7134162, upload-time = "2025-10-15T18:22:17.996Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/b1/a7391df6adacf0a5c2cf6ac1cf1fcc1369e7d439d28f637a847f8803beb3/pillow-12.0.0-cp312-cp312-win32.whl", hash = "sha256:dd333073e0cacdc3089525c7df7d39b211bcdf31fc2824e49d01c6b6187b07d0", size = 6298769, upload-time = "2025-10-15T18:22:19.923Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/0b/d87733741526541c909bbf159e338dcace4f982daac6e5a8d6be225ca32d/pillow-12.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe611163f6303d1619bbcb653540a4d60f9e55e622d60a3108be0d5b441017a", size = 7001107, upload-time = "2025-10-15T18:22:21.644Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/96/aaa61ce33cc98421fb6088af2a03be4157b1e7e0e87087c888e2370a7f45/pillow-12.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:7dfb439562f234f7d57b1ac6bc8fe7f838a4bd49c79230e0f6a1da93e82f1fad", size = 2436012, upload-time = "2025-10-15T18:22:23.621Z" },
+ { url = "https://files.pythonhosted.org/packages/62/f2/de993bb2d21b33a98d031ecf6a978e4b61da207bef02f7b43093774c480d/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0869154a2d0546545cde61d1789a6524319fc1897d9ee31218eae7a60ccc5643", size = 4045493, upload-time = "2025-10-15T18:22:25.758Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/b6/bc8d0c4c9f6f111a783d045310945deb769b806d7574764234ffd50bc5ea/pillow-12.0.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a7921c5a6d31b3d756ec980f2f47c0cfdbce0fc48c22a39347a895f41f4a6ea4", size = 4120461, upload-time = "2025-10-15T18:22:27.286Z" },
+ { url = "https://files.pythonhosted.org/packages/5d/57/d60d343709366a353dc56adb4ee1e7d8a2cc34e3fbc22905f4167cfec119/pillow-12.0.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1ee80a59f6ce048ae13cda1abf7fbd2a34ab9ee7d401c46be3ca685d1999a399", size = 3576912, upload-time = "2025-10-15T18:22:28.751Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/a4/a0a31467e3f83b94d37568294b01d22b43ae3c5d85f2811769b9c66389dd/pillow-12.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c50f36a62a22d350c96e49ad02d0da41dbd17ddc2e29750dbdba4323f85eb4a5", size = 5249132, upload-time = "2025-10-15T18:22:30.641Z" },
+ { url = "https://files.pythonhosted.org/packages/83/06/48eab21dd561de2914242711434c0c0eb992ed08ff3f6107a5f44527f5e9/pillow-12.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5193fde9a5f23c331ea26d0cf171fbf67e3f247585f50c08b3e205c7aeb4589b", size = 4650099, upload-time = "2025-10-15T18:22:32.73Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/bd/69ed99fd46a8dba7c1887156d3572fe4484e3f031405fcc5a92e31c04035/pillow-12.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bde737cff1a975b70652b62d626f7785e0480918dece11e8fef3c0cf057351c3", size = 6230808, upload-time = "2025-10-15T18:22:34.337Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/94/8fad659bcdbf86ed70099cb60ae40be6acca434bbc8c4c0d4ef356d7e0de/pillow-12.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a6597ff2b61d121172f5844b53f21467f7082f5fb385a9a29c01414463f93b07", size = 8037804, upload-time = "2025-10-15T18:22:36.402Z" },
+ { url = "https://files.pythonhosted.org/packages/20/39/c685d05c06deecfd4e2d1950e9a908aa2ca8bc4e6c3b12d93b9cafbd7837/pillow-12.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b817e7035ea7f6b942c13aa03bb554fc44fea70838ea21f8eb31c638326584e", size = 6345553, upload-time = "2025-10-15T18:22:38.066Z" },
+ { url = "https://files.pythonhosted.org/packages/38/57/755dbd06530a27a5ed74f8cb0a7a44a21722ebf318edbe67ddbd7fb28f88/pillow-12.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4f1231b7dec408e8670264ce63e9c71409d9583dd21d32c163e25213ee2a344", size = 7037729, upload-time = "2025-10-15T18:22:39.769Z" },
+ { url = "https://files.pythonhosted.org/packages/ca/b6/7e94f4c41d238615674d06ed677c14883103dce1c52e4af16f000338cfd7/pillow-12.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6e51b71417049ad6ab14c49608b4a24d8fb3fe605e5dfabfe523b58064dc3d27", size = 6459789, upload-time = "2025-10-15T18:22:41.437Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/14/4448bb0b5e0f22dd865290536d20ec8a23b64e2d04280b89139f09a36bb6/pillow-12.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d120c38a42c234dc9a8c5de7ceaaf899cf33561956acb4941653f8bdc657aa79", size = 7130917, upload-time = "2025-10-15T18:22:43.152Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/ca/16c6926cc1c015845745d5c16c9358e24282f1e588237a4c36d2b30f182f/pillow-12.0.0-cp313-cp313-win32.whl", hash = "sha256:4cc6b3b2efff105c6a1656cfe59da4fdde2cda9af1c5e0b58529b24525d0a098", size = 6302391, upload-time = "2025-10-15T18:22:44.753Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/2a/dd43dcfd6dae9b6a49ee28a8eedb98c7d5ff2de94a5d834565164667b97b/pillow-12.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:4cf7fed4b4580601c4345ceb5d4cbf5a980d030fd5ad07c4d2ec589f95f09905", size = 7007477, upload-time = "2025-10-15T18:22:46.838Z" },
+ { url = "https://files.pythonhosted.org/packages/77/f0/72ea067f4b5ae5ead653053212af05ce3705807906ba3f3e8f58ddf617e6/pillow-12.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:9f0b04c6b8584c2c193babcccc908b38ed29524b29dd464bc8801bf10d746a3a", size = 2435918, upload-time = "2025-10-15T18:22:48.399Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/5e/9046b423735c21f0487ea6cb5b10f89ea8f8dfbe32576fe052b5ba9d4e5b/pillow-12.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7fa22993bac7b77b78cae22bad1e2a987ddf0d9015c63358032f84a53f23cdc3", size = 5251406, upload-time = "2025-10-15T18:22:49.905Z" },
+ { url = "https://files.pythonhosted.org/packages/12/66/982ceebcdb13c97270ef7a56c3969635b4ee7cd45227fa707c94719229c5/pillow-12.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f135c702ac42262573fe9714dfe99c944b4ba307af5eb507abef1667e2cbbced", size = 4653218, upload-time = "2025-10-15T18:22:51.587Z" },
+ { url = "https://files.pythonhosted.org/packages/16/b3/81e625524688c31859450119bf12674619429cab3119eec0e30a7a1029cb/pillow-12.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c85de1136429c524e55cfa4e033b4a7940ac5c8ee4d9401cc2d1bf48154bbc7b", size = 6266564, upload-time = "2025-10-15T18:22:53.215Z" },
+ { url = "https://files.pythonhosted.org/packages/98/59/dfb38f2a41240d2408096e1a76c671d0a105a4a8471b1871c6902719450c/pillow-12.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:38df9b4bfd3db902c9c2bd369bcacaf9d935b2fff73709429d95cc41554f7b3d", size = 8069260, upload-time = "2025-10-15T18:22:54.933Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/3d/378dbea5cd1874b94c312425ca77b0f47776c78e0df2df751b820c8c1d6c/pillow-12.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d87ef5795da03d742bf49439f9ca4d027cde49c82c5371ba52464aee266699a", size = 6379248, upload-time = "2025-10-15T18:22:56.605Z" },
+ { url = "https://files.pythonhosted.org/packages/84/b0/d525ef47d71590f1621510327acec75ae58c721dc071b17d8d652ca494d8/pillow-12.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aff9e4d82d082ff9513bdd6acd4f5bd359f5b2c870907d2b0a9c5e10d40c88fe", size = 7066043, upload-time = "2025-10-15T18:22:58.53Z" },
+ { url = "https://files.pythonhosted.org/packages/61/2c/aced60e9cf9d0cde341d54bf7932c9ffc33ddb4a1595798b3a5150c7ec4e/pillow-12.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d8ca2b210ada074d57fcee40c30446c9562e542fc46aedc19baf758a93532ee", size = 6490915, upload-time = "2025-10-15T18:23:00.582Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/26/69dcb9b91f4e59f8f34b2332a4a0a951b44f547c4ed39d3e4dcfcff48f89/pillow-12.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:99a7f72fb6249302aa62245680754862a44179b545ded638cf1fef59befb57ef", size = 7157998, upload-time = "2025-10-15T18:23:02.627Z" },
+ { url = "https://files.pythonhosted.org/packages/61/2b/726235842220ca95fa441ddf55dd2382b52ab5b8d9c0596fe6b3f23dafe8/pillow-12.0.0-cp313-cp313t-win32.whl", hash = "sha256:4078242472387600b2ce8d93ade8899c12bf33fa89e55ec89fe126e9d6d5d9e9", size = 6306201, upload-time = "2025-10-15T18:23:04.709Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/3d/2afaf4e840b2df71344ababf2f8edd75a705ce500e5dc1e7227808312ae1/pillow-12.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2c54c1a783d6d60595d3514f0efe9b37c8808746a66920315bfd34a938d7994b", size = 7013165, upload-time = "2025-10-15T18:23:06.46Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/75/3fa09aa5cf6ed04bee3fa575798ddf1ce0bace8edb47249c798077a81f7f/pillow-12.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:26d9f7d2b604cd23aba3e9faf795787456ac25634d82cd060556998e39c6fa47", size = 2437834, upload-time = "2025-10-15T18:23:08.194Z" },
+ { url = "https://files.pythonhosted.org/packages/54/2a/9a8c6ba2c2c07b71bec92cf63e03370ca5e5f5c5b119b742bcc0cde3f9c5/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:beeae3f27f62308f1ddbcfb0690bf44b10732f2ef43758f169d5e9303165d3f9", size = 4045531, upload-time = "2025-10-15T18:23:10.121Z" },
+ { url = "https://files.pythonhosted.org/packages/84/54/836fdbf1bfb3d66a59f0189ff0b9f5f666cee09c6188309300df04ad71fa/pillow-12.0.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d4827615da15cd59784ce39d3388275ec093ae3ee8d7f0c089b76fa87af756c2", size = 4120554, upload-time = "2025-10-15T18:23:12.14Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/cd/16aec9f0da4793e98e6b54778a5fbce4f375c6646fe662e80600b8797379/pillow-12.0.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:3e42edad50b6909089750e65c91aa09aaf1e0a71310d383f11321b27c224ed8a", size = 3576812, upload-time = "2025-10-15T18:23:13.962Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/b7/13957fda356dc46339298b351cae0d327704986337c3c69bb54628c88155/pillow-12.0.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e5d8efac84c9afcb40914ab49ba063d94f5dbdf5066db4482c66a992f47a3a3b", size = 5252689, upload-time = "2025-10-15T18:23:15.562Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/f5/eae31a306341d8f331f43edb2e9122c7661b975433de5e447939ae61c5da/pillow-12.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:266cd5f2b63ff316d5a1bba46268e603c9caf5606d44f38c2873c380950576ad", size = 4650186, upload-time = "2025-10-15T18:23:17.379Z" },
+ { url = "https://files.pythonhosted.org/packages/86/62/2a88339aa40c4c77e79108facbd307d6091e2c0eb5b8d3cf4977cfca2fe6/pillow-12.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58eea5ebe51504057dd95c5b77d21700b77615ab0243d8152793dc00eb4faf01", size = 6230308, upload-time = "2025-10-15T18:23:18.971Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/33/5425a8992bcb32d1cb9fa3dd39a89e613d09a22f2c8083b7bf43c455f760/pillow-12.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f13711b1a5ba512d647a0e4ba79280d3a9a045aaf7e0cc6fbe96b91d4cdf6b0c", size = 8039222, upload-time = "2025-10-15T18:23:20.909Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/61/3f5d3b35c5728f37953d3eec5b5f3e77111949523bd2dd7f31a851e50690/pillow-12.0.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6846bd2d116ff42cba6b646edf5bf61d37e5cbd256425fa089fee4ff5c07a99e", size = 6346657, upload-time = "2025-10-15T18:23:23.077Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/be/ee90a3d79271227e0f0a33c453531efd6ed14b2e708596ba5dd9be948da3/pillow-12.0.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c98fa880d695de164b4135a52fd2e9cd7b7c90a9d8ac5e9e443a24a95ef9248e", size = 7038482, upload-time = "2025-10-15T18:23:25.005Z" },
+ { url = "https://files.pythonhosted.org/packages/44/34/a16b6a4d1ad727de390e9bd9f19f5f669e079e5826ec0f329010ddea492f/pillow-12.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa3ed2a29a9e9d2d488b4da81dcb54720ac3104a20bf0bd273f1e4648aff5af9", size = 6461416, upload-time = "2025-10-15T18:23:27.009Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/39/1aa5850d2ade7d7ba9f54e4e4c17077244ff7a2d9e25998c38a29749eb3f/pillow-12.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d034140032870024e6b9892c692fe2968493790dd57208b2c37e3fb35f6df3ab", size = 7131584, upload-time = "2025-10-15T18:23:29.752Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/db/4fae862f8fad0167073a7733973bfa955f47e2cac3dc3e3e6257d10fab4a/pillow-12.0.0-cp314-cp314-win32.whl", hash = "sha256:1b1b133e6e16105f524a8dec491e0586d072948ce15c9b914e41cdadd209052b", size = 6400621, upload-time = "2025-10-15T18:23:32.06Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/24/b350c31543fb0107ab2599464d7e28e6f856027aadda995022e695313d94/pillow-12.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:8dc232e39d409036af549c86f24aed8273a40ffa459981146829a324e0848b4b", size = 7142916, upload-time = "2025-10-15T18:23:34.71Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/9b/0ba5a6fd9351793996ef7487c4fdbde8d3f5f75dbedc093bb598648fddf0/pillow-12.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:d52610d51e265a51518692045e372a4c363056130d922a7351429ac9f27e70b0", size = 2523836, upload-time = "2025-10-15T18:23:36.967Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/7a/ceee0840aebc579af529b523d530840338ecf63992395842e54edc805987/pillow-12.0.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1979f4566bb96c1e50a62d9831e2ea2d1211761e5662afc545fa766f996632f6", size = 5255092, upload-time = "2025-10-15T18:23:38.573Z" },
+ { url = "https://files.pythonhosted.org/packages/44/76/20776057b4bfd1aef4eeca992ebde0f53a4dce874f3ae693d0ec90a4f79b/pillow-12.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b2e4b27a6e15b04832fe9bf292b94b5ca156016bbc1ea9c2c20098a0320d6cf6", size = 4653158, upload-time = "2025-10-15T18:23:40.238Z" },
+ { url = "https://files.pythonhosted.org/packages/82/3f/d9ff92ace07be8836b4e7e87e6a4c7a8318d47c2f1463ffcf121fc57d9cb/pillow-12.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fb3096c30df99fd01c7bf8e544f392103d0795b9f98ba71a8054bcbf56b255f1", size = 6267882, upload-time = "2025-10-15T18:23:42.434Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/7a/4f7ff87f00d3ad33ba21af78bfcd2f032107710baf8280e3722ceec28cda/pillow-12.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7438839e9e053ef79f7112c881cef684013855016f928b168b81ed5835f3e75e", size = 8071001, upload-time = "2025-10-15T18:23:44.29Z" },
+ { url = "https://files.pythonhosted.org/packages/75/87/fcea108944a52dad8cca0715ae6247e271eb80459364a98518f1e4f480c1/pillow-12.0.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d5c411a8eaa2299322b647cd932586b1427367fd3184ffbb8f7a219ea2041ca", size = 6380146, upload-time = "2025-10-15T18:23:46.065Z" },
+ { url = "https://files.pythonhosted.org/packages/91/52/0d31b5e571ef5fd111d2978b84603fce26aba1b6092f28e941cb46570745/pillow-12.0.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7e091d464ac59d2c7ad8e7e08105eaf9dafbc3883fd7265ffccc2baad6ac925", size = 7067344, upload-time = "2025-10-15T18:23:47.898Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/f4/2dd3d721f875f928d48e83bb30a434dee75a2531bca839bb996bb0aa5a91/pillow-12.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:792a2c0be4dcc18af9d4a2dfd8a11a17d5e25274a1062b0ec1c2d79c76f3e7f8", size = 6491864, upload-time = "2025-10-15T18:23:49.607Z" },
+ { url = "https://files.pythonhosted.org/packages/30/4b/667dfcf3d61fc309ba5a15b141845cece5915e39b99c1ceab0f34bf1d124/pillow-12.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:afbefa430092f71a9593a99ab6a4e7538bc9eabbf7bf94f91510d3503943edc4", size = 7158911, upload-time = "2025-10-15T18:23:51.351Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/2f/16cabcc6426c32218ace36bf0d55955e813f2958afddbf1d391849fee9d1/pillow-12.0.0-cp314-cp314t-win32.whl", hash = "sha256:3830c769decf88f1289680a59d4f4c46c72573446352e2befec9a8512104fa52", size = 6408045, upload-time = "2025-10-15T18:23:53.177Z" },
+ { url = "https://files.pythonhosted.org/packages/35/73/e29aa0c9c666cf787628d3f0dcf379f4791fba79f4936d02f8b37165bdf8/pillow-12.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:905b0365b210c73afb0ebe9101a32572152dfd1c144c7e28968a331b9217b94a", size = 7148282, upload-time = "2025-10-15T18:23:55.316Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/70/6b41bdcddf541b437bbb9f47f94d2db5d9ddef6c37ccab8c9107743748a4/pillow-12.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:99353a06902c2e43b43e8ff74ee65a7d90307d82370604746738a1e0661ccca7", size = 2525630, upload-time = "2025-10-15T18:23:57.149Z" },
+]
+
[[package]]
name = "platformdirs"
-version = "4.4.0"
+version = "4.5.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" },
+ { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
]
[[package]]
@@ -467,6 +1267,59 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" },
]
+[[package]]
+name = "psutil"
+version = "7.1.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e1/88/bdd0a41e5857d5d703287598cbf08dad90aed56774ea52ae071bae9071b6/psutil-7.1.3.tar.gz", hash = "sha256:6c86281738d77335af7aec228328e944b30930899ea760ecf33a4dba66be5e74", size = 489059, upload-time = "2025-11-02T12:25:54.619Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bd/93/0c49e776b8734fef56ec9c5c57f923922f2cf0497d62e0f419465f28f3d0/psutil-7.1.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0005da714eee687b4b8decd3d6cc7c6db36215c9e74e5ad2264b90c3df7d92dc", size = 239751, upload-time = "2025-11-02T12:25:58.161Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/8d/b31e39c769e70780f007969815195a55c81a63efebdd4dbe9e7a113adb2f/psutil-7.1.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:19644c85dcb987e35eeeaefdc3915d059dac7bd1167cdcdbf27e0ce2df0c08c0", size = 240368, upload-time = "2025-11-02T12:26:00.491Z" },
+ { url = "https://files.pythonhosted.org/packages/62/61/23fd4acc3c9eebbf6b6c78bcd89e5d020cfde4acf0a9233e9d4e3fa698b4/psutil-7.1.3-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95ef04cf2e5ba0ab9eaafc4a11eaae91b44f4ef5541acd2ee91d9108d00d59a7", size = 287134, upload-time = "2025-11-02T12:26:02.613Z" },
+ { url = "https://files.pythonhosted.org/packages/30/1c/f921a009ea9ceb51aa355cb0cc118f68d354db36eae18174bab63affb3e6/psutil-7.1.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1068c303be3a72f8e18e412c5b2a8f6d31750fb152f9cb106b54090296c9d251", size = 289904, upload-time = "2025-11-02T12:26:05.207Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/82/62d68066e13e46a5116df187d319d1724b3f437ddd0f958756fc052677f4/psutil-7.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:18349c5c24b06ac5612c0428ec2a0331c26443d259e2a0144a9b24b4395b58fa", size = 249642, upload-time = "2025-11-02T12:26:07.447Z" },
+ { url = "https://files.pythonhosted.org/packages/df/ad/c1cd5fe965c14a0392112f68362cfceb5230819dbb5b1888950d18a11d9f/psutil-7.1.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c525ffa774fe4496282fb0b1187725793de3e7c6b29e41562733cae9ada151ee", size = 245518, upload-time = "2025-11-02T12:26:09.719Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/bb/6670bded3e3236eb4287c7bcdc167e9fae6e1e9286e437f7111caed2f909/psutil-7.1.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b403da1df4d6d43973dc004d19cee3b848e998ae3154cc8097d139b77156c353", size = 239843, upload-time = "2025-11-02T12:26:11.968Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/66/853d50e75a38c9a7370ddbeefabdd3d3116b9c31ef94dc92c6729bc36bec/psutil-7.1.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad81425efc5e75da3f39b3e636293360ad8d0b49bed7df824c79764fb4ba9b8b", size = 240369, upload-time = "2025-11-02T12:26:14.358Z" },
+ { url = "https://files.pythonhosted.org/packages/41/bd/313aba97cb5bfb26916dc29cf0646cbe4dd6a89ca69e8c6edce654876d39/psutil-7.1.3-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f33a3702e167783a9213db10ad29650ebf383946e91bc77f28a5eb083496bc9", size = 288210, upload-time = "2025-11-02T12:26:16.699Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/fa/76e3c06e760927a0cfb5705eb38164254de34e9bd86db656d4dbaa228b04/psutil-7.1.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fac9cd332c67f4422504297889da5ab7e05fd11e3c4392140f7370f4208ded1f", size = 291182, upload-time = "2025-11-02T12:26:18.848Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/1d/5774a91607035ee5078b8fd747686ebec28a962f178712de100d00b78a32/psutil-7.1.3-cp314-cp314t-win_amd64.whl", hash = "sha256:3792983e23b69843aea49c8f5b8f115572c5ab64c153bada5270086a2123c7e7", size = 250466, upload-time = "2025-11-02T12:26:21.183Z" },
+ { url = "https://files.pythonhosted.org/packages/00/ca/e426584bacb43a5cb1ac91fae1937f478cd8fbe5e4ff96574e698a2c77cd/psutil-7.1.3-cp314-cp314t-win_arm64.whl", hash = "sha256:31d77fcedb7529f27bb3a0472bea9334349f9a04160e8e6e5020f22c59893264", size = 245756, upload-time = "2025-11-02T12:26:23.148Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/94/46b9154a800253e7ecff5aaacdf8ebf43db99de4a2dfa18575b02548654e/psutil-7.1.3-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bdbcd0e58ca14996a42adf3621a6244f1bb2e2e528886959c72cf1e326677ab", size = 238359, upload-time = "2025-11-02T12:26:25.284Z" },
+ { url = "https://files.pythonhosted.org/packages/68/3a/9f93cff5c025029a36d9a92fef47220ab4692ee7f2be0fba9f92813d0cb8/psutil-7.1.3-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:bc31fa00f1fbc3c3802141eede66f3a2d51d89716a194bf2cd6fc68310a19880", size = 239171, upload-time = "2025-11-02T12:26:27.23Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/b1/5f49af514f76431ba4eea935b8ad3725cdeb397e9245ab919dbc1d1dc20f/psutil-7.1.3-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb428f9f05c1225a558f53e30ccbad9930b11c3fc206836242de1091d3e7dd3", size = 263261, upload-time = "2025-11-02T12:26:29.48Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/95/992c8816a74016eb095e73585d747e0a8ea21a061ed3689474fabb29a395/psutil-7.1.3-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d974e02ca2c8eb4812c3f76c30e28836fffc311d55d979f1465c1feeb2b68b", size = 264635, upload-time = "2025-11-02T12:26:31.74Z" },
+ { url = "https://files.pythonhosted.org/packages/55/4c/c3ed1a622b6ae2fd3c945a366e64eb35247a31e4db16cf5095e269e8eb3c/psutil-7.1.3-cp37-abi3-win_amd64.whl", hash = "sha256:f39c2c19fe824b47484b96f9692932248a54c43799a84282cfe58d05a6449efd", size = 247633, upload-time = "2025-11-02T12:26:33.887Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/ad/33b2ccec09bf96c2b2ef3f9a6f66baac8253d7565d8839e024a6b905d45d/psutil-7.1.3-cp37-abi3-win_arm64.whl", hash = "sha256:bd0d69cee829226a761e92f28140bec9a5ee9d5b4fb4b0cc589068dbfff559b1", size = 244608, upload-time = "2025-11-02T12:26:36.136Z" },
+]
+
+[[package]]
+name = "ptyprocess"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" },
+]
+
+[[package]]
+name = "pure-eval"
+version = "0.2.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "2.23"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/cf/d2d3b9f5699fb1e4615c8e32ff220203e43b248e1dfcc6736ad9057731ca/pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2", size = 173734, upload-time = "2025-09-09T13:23:47.91Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/e3/59cd50310fc9b59512193629e1984c1f95e5c8ae6e5d8c69532ccc65a7fe/pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934", size = 118140, upload-time = "2025-09-09T13:23:46.651Z" },
+]
+
[[package]]
name = "pydantic"
version = "2.11.9"
@@ -533,6 +1386,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
]
+[[package]]
+name = "pyparsing"
+version = "3.2.5"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f2/a5/181488fc2b9d093e3972d2a472855aae8a03f000592dbfce716a512b3359/pyparsing-3.2.5.tar.gz", hash = "sha256:2df8d5b7b2802ef88e8d016a2eb9c7aeaa923529cd251ed0fe4608275d4105b6", size = 1099274, upload-time = "2025-09-21T04:11:06.277Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/5e/1aa9a93198c6b64513c9d7752de7422c06402de6600a8767da1524f9570b/pyparsing-3.2.5-py3-none-any.whl", hash = "sha256:e38a4f02064cf41fe6593d328d0512495ad1f3d8a91c4f73fc401b3079a59a5e", size = 113890, upload-time = "2025-09-21T04:11:04.117Z" },
+]
+
[[package]]
name = "pytest"
version = "8.4.2"
@@ -572,28 +1434,91 @@ wheels = [
[[package]]
name = "pyyaml"
-version = "6.0.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" },
- { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" },
- { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" },
- { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" },
- { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" },
- { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" },
- { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" },
- { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" },
- { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" },
- { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" },
- { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" },
- { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" },
- { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" },
- { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" },
- { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" },
- { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" },
- { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" },
- { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" },
+version = "6.0.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
+ { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
+ { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
+ { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
+ { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
+ { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
+ { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
+ { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
+ { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
+ { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
+ { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
+ { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
+ { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
+ { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
+ { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
+]
+
+[[package]]
+name = "pyzmq"
+version = "27.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi", marker = "implementation_name == 'pypy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/04/0b/3c9baedbdf613ecaa7aa07027780b8867f57b6293b6ee50de316c9f3222b/pyzmq-27.1.0.tar.gz", hash = "sha256:ac0765e3d44455adb6ddbf4417dcce460fc40a05978c08efdf2948072f6db540", size = 281750, upload-time = "2025-09-08T23:10:18.157Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/92/e7/038aab64a946d535901103da16b953c8c9cc9c961dadcbf3609ed6428d23/pyzmq-27.1.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:452631b640340c928fa343801b0d07eb0c3789a5ffa843f6e1a9cee0ba4eb4fc", size = 1306279, upload-time = "2025-09-08T23:08:03.807Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/5e/c3c49fdd0f535ef45eefcc16934648e9e59dace4a37ee88fc53f6cd8e641/pyzmq-27.1.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1c179799b118e554b66da67d88ed66cd37a169f1f23b5d9f0a231b4e8d44a113", size = 895645, upload-time = "2025-09-08T23:08:05.301Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/e5/b0b2504cb4e903a74dcf1ebae157f9e20ebb6ea76095f6cfffea28c42ecd/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3837439b7f99e60312f0c926a6ad437b067356dc2bc2ec96eb395fd0fe804233", size = 652574, upload-time = "2025-09-08T23:08:06.828Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43ad9a73e3da1fab5b0e7e13402f0b2fb934ae1c876c51d0afff0e7c052eca31", size = 840995, upload-time = "2025-09-08T23:08:08.396Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/bb/b79798ca177b9eb0825b4c9998c6af8cd2a7f15a6a1a4272c1d1a21d382f/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0de3028d69d4cdc475bfe47a6128eb38d8bc0e8f4d69646adfbcd840facbac28", size = 1642070, upload-time = "2025-09-08T23:08:09.989Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/80/2df2e7977c4ede24c79ae39dcef3899bfc5f34d1ca7a5b24f182c9b7a9ca/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:cf44a7763aea9298c0aa7dbf859f87ed7012de8bda0f3977b6fb1d96745df856", size = 2021121, upload-time = "2025-09-08T23:08:11.907Z" },
+ { url = "https://files.pythonhosted.org/packages/46/bd/2d45ad24f5f5ae7e8d01525eb76786fa7557136555cac7d929880519e33a/pyzmq-27.1.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f30f395a9e6fbca195400ce833c731e7b64c3919aa481af4d88c3759e0cb7496", size = 1878550, upload-time = "2025-09-08T23:08:13.513Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/2f/104c0a3c778d7c2ab8190e9db4f62f0b6957b53c9d87db77c284b69f33ea/pyzmq-27.1.0-cp312-abi3-win32.whl", hash = "sha256:250e5436a4ba13885494412b3da5d518cd0d3a278a1ae640e113c073a5f88edd", size = 559184, upload-time = "2025-09-08T23:08:15.163Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/7f/a21b20d577e4100c6a41795842028235998a643b1ad406a6d4163ea8f53e/pyzmq-27.1.0-cp312-abi3-win_amd64.whl", hash = "sha256:9ce490cf1d2ca2ad84733aa1d69ce6855372cb5ce9223802450c9b2a7cba0ccf", size = 619480, upload-time = "2025-09-08T23:08:17.192Z" },
+ { url = "https://files.pythonhosted.org/packages/78/c2/c012beae5f76b72f007a9e91ee9401cb88c51d0f83c6257a03e785c81cc2/pyzmq-27.1.0-cp312-abi3-win_arm64.whl", hash = "sha256:75a2f36223f0d535a0c919e23615fc85a1e23b71f40c7eb43d7b1dedb4d8f15f", size = 552993, upload-time = "2025-09-08T23:08:18.926Z" },
+ { url = "https://files.pythonhosted.org/packages/60/cb/84a13459c51da6cec1b7b1dc1a47e6db6da50b77ad7fd9c145842750a011/pyzmq-27.1.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:93ad4b0855a664229559e45c8d23797ceac03183c7b6f5b4428152a6b06684a5", size = 1122436, upload-time = "2025-09-08T23:08:20.801Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/b6/94414759a69a26c3dd674570a81813c46a078767d931a6c70ad29fc585cb/pyzmq-27.1.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:fbb4f2400bfda24f12f009cba62ad5734148569ff4949b1b6ec3b519444342e6", size = 1156301, upload-time = "2025-09-08T23:08:22.47Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/ad/15906493fd40c316377fd8a8f6b1f93104f97a752667763c9b9c1b71d42d/pyzmq-27.1.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:e343d067f7b151cfe4eb3bb796a7752c9d369eed007b91231e817071d2c2fec7", size = 1341197, upload-time = "2025-09-08T23:08:24.286Z" },
+ { url = "https://files.pythonhosted.org/packages/14/1d/d343f3ce13db53a54cb8946594e567410b2125394dafcc0268d8dda027e0/pyzmq-27.1.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:08363b2011dec81c354d694bdecaef4770e0ae96b9afea70b3f47b973655cc05", size = 897275, upload-time = "2025-09-08T23:08:26.063Z" },
+ { url = "https://files.pythonhosted.org/packages/69/2d/d83dd6d7ca929a2fc67d2c3005415cdf322af7751d773524809f9e585129/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d54530c8c8b5b8ddb3318f481297441af102517602b569146185fa10b63f4fa9", size = 660469, upload-time = "2025-09-08T23:08:27.623Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/cd/9822a7af117f4bc0f1952dbe9ef8358eb50a24928efd5edf54210b850259/pyzmq-27.1.0-cp313-cp313t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3afa12c392f0a44a2414056d730eebc33ec0926aae92b5ad5cf26ebb6cc128", size = 847961, upload-time = "2025-09-08T23:08:29.672Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/12/f003e824a19ed73be15542f172fd0ec4ad0b60cf37436652c93b9df7c585/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c65047adafe573ff023b3187bb93faa583151627bc9c51fc4fb2c561ed689d39", size = 1650282, upload-time = "2025-09-08T23:08:31.349Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/4a/e82d788ed58e9a23995cee70dbc20c9aded3d13a92d30d57ec2291f1e8a3/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:90e6e9441c946a8b0a667356f7078d96411391a3b8f80980315455574177ec97", size = 2024468, upload-time = "2025-09-08T23:08:33.543Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/94/2da0a60841f757481e402b34bf4c8bf57fa54a5466b965de791b1e6f747d/pyzmq-27.1.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:add071b2d25f84e8189aaf0882d39a285b42fa3853016ebab234a5e78c7a43db", size = 1885394, upload-time = "2025-09-08T23:08:35.51Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/6f/55c10e2e49ad52d080dc24e37adb215e5b0d64990b57598abc2e3f01725b/pyzmq-27.1.0-cp313-cp313t-win32.whl", hash = "sha256:7ccc0700cfdf7bd487bea8d850ec38f204478681ea02a582a8da8171b7f90a1c", size = 574964, upload-time = "2025-09-08T23:08:37.178Z" },
+ { url = "https://files.pythonhosted.org/packages/87/4d/2534970ba63dd7c522d8ca80fb92777f362c0f321900667c615e2067cb29/pyzmq-27.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:8085a9fba668216b9b4323be338ee5437a235fe275b9d1610e422ccc279733e2", size = 641029, upload-time = "2025-09-08T23:08:40.595Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/fa/f8aea7a28b0641f31d40dea42d7ef003fded31e184ef47db696bc74cd610/pyzmq-27.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:6bb54ca21bcfe361e445256c15eedf083f153811c37be87e0514934d6913061e", size = 561541, upload-time = "2025-09-08T23:08:42.668Z" },
+ { url = "https://files.pythonhosted.org/packages/87/45/19efbb3000956e82d0331bafca5d9ac19ea2857722fa2caacefb6042f39d/pyzmq-27.1.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ce980af330231615756acd5154f29813d553ea555485ae712c491cd483df6b7a", size = 1341197, upload-time = "2025-09-08T23:08:44.973Z" },
+ { url = "https://files.pythonhosted.org/packages/48/43/d72ccdbf0d73d1343936296665826350cb1e825f92f2db9db3e61c2162a2/pyzmq-27.1.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1779be8c549e54a1c38f805e56d2a2e5c009d26de10921d7d51cfd1c8d4632ea", size = 897175, upload-time = "2025-09-08T23:08:46.601Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/2e/a483f73a10b65a9ef0161e817321d39a770b2acf8bcf3004a28d90d14a94/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7200bb0f03345515df50d99d3db206a0a6bee1955fbb8c453c76f5bf0e08fb96", size = 660427, upload-time = "2025-09-08T23:08:48.187Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/d2/5f36552c2d3e5685abe60dfa56f91169f7a2d99bbaf67c5271022ab40863/pyzmq-27.1.0-cp314-cp314t-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01c0e07d558b06a60773744ea6251f769cd79a41a97d11b8bf4ab8f034b0424d", size = 847929, upload-time = "2025-09-08T23:08:49.76Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/2a/404b331f2b7bf3198e9945f75c4c521f0c6a3a23b51f7a4a401b94a13833/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:80d834abee71f65253c91540445d37c4c561e293ba6e741b992f20a105d69146", size = 1650193, upload-time = "2025-09-08T23:08:51.7Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/0b/f4107e33f62a5acf60e3ded67ed33d79b4ce18de432625ce2fc5093d6388/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:544b4e3b7198dde4a62b8ff6685e9802a9a1ebf47e77478a5eb88eca2a82f2fd", size = 2024388, upload-time = "2025-09-08T23:08:53.393Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/01/add31fe76512642fd6e40e3a3bd21f4b47e242c8ba33efb6809e37076d9b/pyzmq-27.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cedc4c68178e59a4046f97eca31b148ddcf51e88677de1ef4e78cf06c5376c9a", size = 1885316, upload-time = "2025-09-08T23:08:55.702Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/59/a5f38970f9bf07cee96128de79590bb354917914a9be11272cfc7ff26af0/pyzmq-27.1.0-cp314-cp314t-win32.whl", hash = "sha256:1f0b2a577fd770aa6f053211a55d1c47901f4d537389a034c690291485e5fe92", size = 587472, upload-time = "2025-09-08T23:08:58.18Z" },
+ { url = "https://files.pythonhosted.org/packages/70/d8/78b1bad170f93fcf5e3536e70e8fadac55030002275c9a29e8f5719185de/pyzmq-27.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:19c9468ae0437f8074af379e986c5d3d7d7bfe033506af442e8c879732bedbe0", size = 661401, upload-time = "2025-09-08T23:08:59.802Z" },
+ { url = "https://files.pythonhosted.org/packages/81/d6/4bfbb40c9a0b42fc53c7cf442f6385db70b40f74a783130c5d0a5aa62228/pyzmq-27.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dc5dbf68a7857b59473f7df42650c621d7e8923fb03fa74a526890f4d33cc4d7", size = 575170, upload-time = "2025-09-08T23:09:01.418Z" },
]
[[package]]
@@ -729,6 +1654,29 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/97/30/2f9a5243008f76dfc5dee9a53dfb939d9b31e16ce4bd4f2e628bfc5d89d2/scipy-1.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779", size = 26448374, upload-time = "2025-09-11T17:45:03.45Z" },
]
+[[package]]
+name = "seaborn"
+version = "0.13.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "matplotlib" },
+ { name = "numpy" },
+ { name = "pandas" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" },
+]
+
+[[package]]
+name = "setuptools"
+version = "80.9.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" },
+]
+
[[package]]
name = "six"
version = "1.17.0"
@@ -738,6 +1686,32 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
]
+[[package]]
+name = "stack-data"
+version = "0.6.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "asttokens" },
+ { name = "executing" },
+ { name = "pure-eval" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" },
+]
+
+[[package]]
+name = "sympy"
+version = "1.14.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mpmath" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" },
+]
+
[[package]]
name = "termcolor"
version = "3.1.0"
@@ -765,6 +1739,151 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" },
]
+[[package]]
+name = "torch"
+version = "2.9.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "filelock" },
+ { name = "fsspec" },
+ { name = "jinja2" },
+ { name = "networkx" },
+ { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "setuptools" },
+ { name = "sympy" },
+ { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" },
+ { name = "typing-extensions" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d3/3985739f3b8e88675127bf70f82b3a48ae083e39cda56305dbd90398fec0/torch-2.9.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e5f7af1dc4c0a7c4a260c2534f41ddaf209714f7c89145e644c44712fbd6b642", size = 104107898, upload-time = "2025-10-15T15:46:20.883Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/4b/f4bb2e6c25d0272f798cd6d7a04ed315da76cec68c602d87040c7847287f/torch-2.9.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:01cff95ecd9a212ea2f141db28acccdceb6a4c54f64e6c51091146f5e2a772c6", size = 899738273, upload-time = "2025-10-15T15:50:04.188Z" },
+ { url = "https://files.pythonhosted.org/packages/66/11/c1c5ba6691cda6279087c35bd626536e4fd29521fe740abf5008377a9a02/torch-2.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:4582b162f541651f0cb184d3e291c05c2f556c7117c64a9873e2ee158d40062b", size = 109280887, upload-time = "2025-10-15T15:46:26.228Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/5f/b85bd8c05312d71de9402bf5868d217c38827cfd09d8f8514e5be128a52b/torch-2.9.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:33f58e9a102a91259af289d50525c30323b5c9ae1d31322b6447c0814da68695", size = 74478983, upload-time = "2025-10-15T15:46:39.406Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/1c/90eb13833cdf4969ea9707586d7b57095c3b6e2b223a7256bf111689bcb8/torch-2.9.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c30a17fc83eeab346913e237c64b15b5ba6407fff812f6c541e322e19bc9ea0e", size = 104111330, upload-time = "2025-10-15T15:46:35.238Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/21/2254c54b8d523592c25ef4434769aa23e29b1e6bf5f4c0ad9e27bf442927/torch-2.9.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f25033b8667b57857dfd01458fbf2a9e6a6df1f8def23aef0dc46292f6aa642", size = 899750243, upload-time = "2025-10-15T15:48:57.459Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/a5/5cb94fa4fd1e78223455c23c200f30f6dc10c6d4a2bcc8f6e7f2a2588370/torch-2.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:d037f1b4ffd25013be4a7bf3651a0a910c68554956c7b2c92ebe87c76475dece", size = 109284513, upload-time = "2025-10-15T15:46:45.061Z" },
+ { url = "https://files.pythonhosted.org/packages/66/e8/fc414d8656250ee46120b44836ffbb3266343db424b3e18ca79ebbf69d4f/torch-2.9.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e4e5b5cba837a2a8d1a497ba9a58dae46fa392593eaa13b871c42f71847503a5", size = 74830362, upload-time = "2025-10-15T15:46:48.983Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/5f/9474c98fc5ae0cd04b9466035428cd360e6611a86b8352a0fc2fa504acdc/torch-2.9.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:64693568f5dc4dbd5f880a478b1cea0201cc6b510d91d1bc54fea86ac5d1a637", size = 104144940, upload-time = "2025-10-15T15:47:29.076Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/5a/8e0c1cf57830172c109d4bd6be2708cabeaf550983eee7029291322447a0/torch-2.9.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:f8ed31ddd7d10bfb3fbe0b9fe01b1243577f13d75e6f4a0839a283915ce3791e", size = 899744054, upload-time = "2025-10-15T15:48:29.864Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/28/82c28b30fcb4b7c9cdd995763d18bbb830d6521356712faebbad92ffa61d/torch-2.9.0-cp313-cp313t-win_amd64.whl", hash = "sha256:eff527d4e4846e6f70d2afd8058b73825761203d66576a7e04ea2ecfebcb4ab8", size = 109517546, upload-time = "2025-10-15T15:47:33.395Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/c3/a91f96ec74347fa5fd24453fa514bc61c61ecc79196fa760b012a1873d96/torch-2.9.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:f8877779cf56d1ce431a7636703bdb13307f5960bb1af49716d8b179225e0e6a", size = 74480732, upload-time = "2025-10-15T15:47:38.002Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/73/9f70af34b334a7e0ef496ceec96b7ec767bd778ea35385ce6f77557534d1/torch-2.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7e614fae699838038d888729f82b687c03413c5989ce2a9481f9a7e7a396e0bb", size = 74433037, upload-time = "2025-10-15T15:47:41.894Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/84/37cf88625901934c97109e583ecc21777d21c6f54cda97a7e5bbad1ee2f2/torch-2.9.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:dfb5b8cd310ba3436c7e14e8b7833ef658cf3045e50d2bdaed23c8fc517065eb", size = 104116482, upload-time = "2025-10-15T15:47:46.266Z" },
+ { url = "https://files.pythonhosted.org/packages/56/8e/ca8b17866943a8d4f4664d402ea84210aa274588b4c5d89918f5caa24eec/torch-2.9.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b3d29524993a478e46f5d598b249cd824b7ed98d7fba538bd9c4cde6c803948f", size = 899746916, upload-time = "2025-10-15T15:50:40.294Z" },
+ { url = "https://files.pythonhosted.org/packages/43/65/3b17c0fbbdab6501c5b320a52a648628d0d44e7379f64e27d9eef701b6bf/torch-2.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:71c7578984f5ec0eb645eb4816ac8435fcf3e3e2ae1901bcd2f519a9cafb5125", size = 109275151, upload-time = "2025-10-15T15:49:20.715Z" },
+ { url = "https://files.pythonhosted.org/packages/83/36/74f8c051f785500396e42f93542422422dfd874a174f21f8d955d36e5d64/torch-2.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:71d9309aee457bbe0b164bce2111cd911c4ed4e847e65d5077dbbcd3aba6befc", size = 74823353, upload-time = "2025-10-15T15:49:16.59Z" },
+ { url = "https://files.pythonhosted.org/packages/62/51/dc3b4e2f9ba98ae27238f0153ca098bf9340b2dafcc67fde645d496dfc2a/torch-2.9.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c08fb654d783899e204a32cca758a7ce8a45b2d78eeb89517cc937088316f78e", size = 104140340, upload-time = "2025-10-15T15:50:19.67Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/8d/b00657f8141ac16af7bb6cda2e67de18499a3263b78d516b9a93fcbc98e3/torch-2.9.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ec8feb0099b2daa5728fbc7abb0b05730fd97e0f359ff8bda09865aaa7bd7d4b", size = 899731750, upload-time = "2025-10-15T15:49:36.673Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/29/bd361e0cbb2c79ce6450f42643aaf6919956f89923a50571b0ebfe92d142/torch-2.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:695ba920f234ad4170c9c50e28d56c848432f8f530e6bc7f88fcb15ddf338e75", size = 109503850, upload-time = "2025-10-15T15:50:24.118Z" },
+]
+
+[[package]]
+name = "torchvision"
+version = "0.24.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+ { name = "pillow" },
+ { name = "torch" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/47/ef/81e4e69e02e2c4650b30e8c11c8974f946682a30e0ab7e9803a831beff76/torchvision-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c61d40bcd2e2451e932902a702ad495ba1ec6f279e90b1e15cef2bb55dc911e2", size = 1891726, upload-time = "2025-10-15T15:51:16.977Z" },
+ { url = "https://files.pythonhosted.org/packages/00/7b/e3809b3302caea9a12c13f3adebe4fef127188438e719fd6c8dc93db1da6/torchvision-0.24.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b0531d1483fc322d7da0d83be52f0df860a75114ab87dbeeb9de765feaeda843", size = 2419495, upload-time = "2025-10-15T15:51:11.885Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/e6/7324ead6793075a8c75c56abeed1236d1750de16a5613cfe2ddad164a92a/torchvision-0.24.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:26b9dd9c083f8e5f7ac827de6d5b88c615d9c582dc87666770fbdf16887e4c25", size = 8050480, upload-time = "2025-10-15T15:51:24.012Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/ad/3c56fcd2a0d6e8afa80e115b5ade4302232ec99655220a51d05709819523/torchvision-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:060b7c50ed4b3fb0316b08e2e31bfd874ec2f63ef5ae02f81e54341ca4e88703", size = 4292225, upload-time = "2025-10-15T15:51:27.699Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/b5/b2008e4b77a8d6aada828dd0f6a438d8f94befa23fdd2d62fa0ac6e60113/torchvision-0.24.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:84d79cfc6457310107ce4d712de7a3d388b24484bc9aeded4a76d8f8e3a2813d", size = 1891722, upload-time = "2025-10-15T15:51:28.854Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/02/e2f6b0ff93ca4db5751ac9c5be43f13d5e53d9e9412324f464dca1775027/torchvision-0.24.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:fec12a269cf80f6b0b71471c8d498cd3bdd9d8e892c425bf39fecb604852c3b0", size = 2371478, upload-time = "2025-10-15T15:51:37.842Z" },
+ { url = "https://files.pythonhosted.org/packages/77/85/42e5fc4f716ec7b73cf1f32eeb5c77961be4d4054b26cd6a5ff97f20c966/torchvision-0.24.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:7323a9be5e3da695605753f501cdc87824888c5655d27735cdeaa9986b45884c", size = 8050200, upload-time = "2025-10-15T15:51:46.276Z" },
+ { url = "https://files.pythonhosted.org/packages/93/c2/48cb0b6b26276d2120b1e0dbc877579a748eae02b4091a7522ce54f6d5e1/torchvision-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:08cad8b204196e945f0b2d73adee952d433db1c03645851d52b22a45f1015b13", size = 4309939, upload-time = "2025-10-15T15:51:39.002Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/d7/3dd10830b047eeb46ae6b465474258d7b4fbb7d8872dca69bd42449f5c82/torchvision-0.24.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ab956a6e588623353e0f20d4b03eb1656cb4a3c75ca4dd8b4e32e01bc43271a", size = 2028355, upload-time = "2025-10-15T15:51:22.384Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/cf/2d7e43409089ce7070f5336161f9216d58653ee1cb26bcb5d6c84cc2de36/torchvision-0.24.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:b1b3db80609c32a088554e8e94b4fc31f1033fe5bb4ac0673ec49c3eb03fb4da", size = 2374466, upload-time = "2025-10-15T15:51:35.382Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/30/8f7c328fd7e0a9665da4b6b56b1c627665c18470bfe62f3729ad3eda9aec/torchvision-0.24.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:e6635f100d455c80b43f297df4b8585a76c6a2e114802f6567ddd28d7b5479b0", size = 8217068, upload-time = "2025-10-15T15:51:36.623Z" },
+ { url = "https://files.pythonhosted.org/packages/55/a2/b6f9e40e2904574c80b3bb872c66af20bbd642053e7c8e1b9e99ab396535/torchvision-0.24.0-cp313-cp313t-win_amd64.whl", hash = "sha256:4ce158bbdc3a9086034bced0b5212888bd5b251fee6d08a9eff151d30b4b228a", size = 4273912, upload-time = "2025-10-15T15:51:33.866Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/24/790a39645cc8c71bf442d54a76da9bda5caeb2a44c5f7e02498649cd99d4/torchvision-0.24.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4bdfc85a5ed706421555f32cdc5e3ddb6d40bf65ef03a274ce3c176393e2904b", size = 2028335, upload-time = "2025-10-15T15:51:26.252Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/d7/69479a066ea773653e88eda99031e38681e9094046f87cb957af5036db0e/torchvision-0.24.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:73576a9c4a593223fbae85a64e8bbd77049abd1101893ecf3c5e981284fd58b4", size = 2371609, upload-time = "2025-10-15T15:51:29.859Z" },
+ { url = "https://files.pythonhosted.org/packages/46/64/3c7fdb3771ec992b9445a1f7a969466b23ce2cdb14e09303b3db351a0655/torchvision-0.24.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:dd565b1b06666ff399d0801d4d1824fa570c0167a179ca700a5be232527b3c62", size = 8214918, upload-time = "2025-10-15T15:51:41.465Z" },
+ { url = "https://files.pythonhosted.org/packages/58/51/abc416bc34d574ad479af738e413d9ebf93027ee92d0f4ae38f966b818f7/torchvision-0.24.0-cp314-cp314-win_amd64.whl", hash = "sha256:eb45d12ac48d757738788fd3fb8e88e647d6b2ab2424134ca87556efc72d81b5", size = 4257776, upload-time = "2025-10-15T15:51:42.642Z" },
+ { url = "https://files.pythonhosted.org/packages/08/f7/261d1353c611820541ecd43046b89da3f1ae998dc786e4288b890a009883/torchvision-0.24.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:68120e7e03c31900e499a10bb7fdd63cfd67f0054c9fa108e7e27f9cd372f315", size = 2028359, upload-time = "2025-10-15T15:51:32.119Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/fd/615d8a86db1578345de7fa1edaf476fbcf4f057bf7e4fd898306b620c487/torchvision-0.24.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:64e54494043eecf9f57a9881c6fdea49c62282782e737c002ae8b1639e6ea80e", size = 2374469, upload-time = "2025-10-15T15:51:40.19Z" },
+ { url = "https://files.pythonhosted.org/packages/04/98/bac11e8fdbf00d6c398246ff2781370aa72c99f2ac685c01ce79354c9a32/torchvision-0.24.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:75ef9546323b321a451239d886f0cb528f7e98bb294da47a3200effd4e572064", size = 8217060, upload-time = "2025-10-15T15:51:45.033Z" },
+ { url = "https://files.pythonhosted.org/packages/47/6f/9fba8abc468c904570699eceeb51588f9622172b8fffa4ab11bcf15598c2/torchvision-0.24.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2efb617667950814fc8bb9437e5893861b3616e214285be33cbc364a3f42c599", size = 4358490, upload-time = "2025-10-15T15:51:43.884Z" },
+]
+
+[[package]]
+name = "tornado"
+version = "6.5.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/09/ce/1eb500eae19f4648281bb2186927bb062d2438c2e5093d1360391afd2f90/tornado-6.5.2.tar.gz", hash = "sha256:ab53c8f9a0fa351e2c0741284e06c7a45da86afb544133201c5cc8578eb076a0", size = 510821, upload-time = "2025-08-08T18:27:00.78Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f6/48/6a7529df2c9cc12efd2e8f5dd219516184d703b34c06786809670df5b3bd/tornado-6.5.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:2436822940d37cde62771cff8774f4f00b3c8024fe482e16ca8387b8a2724db6", size = 442563, upload-time = "2025-08-08T18:26:42.945Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/b5/9b575a0ed3e50b00c40b08cbce82eb618229091d09f6d14bce80fc01cb0b/tornado-6.5.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:583a52c7aa94ee046854ba81d9ebb6c81ec0fd30386d96f7640c96dad45a03ef", size = 440729, upload-time = "2025-08-08T18:26:44.473Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/4e/619174f52b120efcf23633c817fd3fed867c30bff785e2cd5a53a70e483c/tornado-6.5.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0fe179f28d597deab2842b86ed4060deec7388f1fd9c1b4a41adf8af058907e", size = 444295, upload-time = "2025-08-08T18:26:46.021Z" },
+ { url = "https://files.pythonhosted.org/packages/95/fa/87b41709552bbd393c85dd18e4e3499dcd8983f66e7972926db8d96aa065/tornado-6.5.2-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b186e85d1e3536d69583d2298423744740986018e393d0321df7340e71898882", size = 443644, upload-time = "2025-08-08T18:26:47.625Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/41/fb15f06e33d7430ca89420283a8762a4e6b8025b800ea51796ab5e6d9559/tornado-6.5.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e792706668c87709709c18b353da1f7662317b563ff69f00bab83595940c7108", size = 443878, upload-time = "2025-08-08T18:26:50.599Z" },
+ { url = "https://files.pythonhosted.org/packages/11/92/fe6d57da897776ad2e01e279170ea8ae726755b045fe5ac73b75357a5a3f/tornado-6.5.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:06ceb1300fd70cb20e43b1ad8aaee0266e69e7ced38fa910ad2e03285009ce7c", size = 444549, upload-time = "2025-08-08T18:26:51.864Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/02/c8f4f6c9204526daf3d760f4aa555a7a33ad0e60843eac025ccfd6ff4a93/tornado-6.5.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:74db443e0f5251be86cbf37929f84d8c20c27a355dd452a5cfa2aada0d001ec4", size = 443973, upload-time = "2025-08-08T18:26:53.625Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/2d/f5f5707b655ce2317190183868cd0f6822a1121b4baeae509ceb9590d0bd/tornado-6.5.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b5e735ab2889d7ed33b32a459cac490eda71a1ba6857b0118de476ab6c366c04", size = 443954, upload-time = "2025-08-08T18:26:55.072Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/59/593bd0f40f7355806bf6573b47b8c22f8e1374c9b6fd03114bd6b7a3dcfd/tornado-6.5.2-cp39-abi3-win32.whl", hash = "sha256:c6f29e94d9b37a95013bb669616352ddb82e3bfe8326fccee50583caebc8a5f0", size = 445023, upload-time = "2025-08-08T18:26:56.677Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/2a/f609b420c2f564a748a2d80ebfb2ee02a73ca80223af712fca591386cafb/tornado-6.5.2-cp39-abi3-win_amd64.whl", hash = "sha256:e56a5af51cc30dd2cae649429af65ca2f6571da29504a07995175df14c18f35f", size = 445427, upload-time = "2025-08-08T18:26:57.91Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/4f/e1f65e8f8c76d73658b33d33b81eed4322fb5085350e4328d5c956f0c8f9/tornado-6.5.2-cp39-abi3-win_arm64.whl", hash = "sha256:d6c33dc3672e3a1f3618eb63b7ef4683a7688e7b9e6e8f0d9aa5726360a004af", size = 444456, upload-time = "2025-08-08T18:26:59.207Z" },
+]
+
+[[package]]
+name = "tqdm"
+version = "4.67.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
+]
+
+[[package]]
+name = "traitlets"
+version = "5.14.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" },
+]
+
+[[package]]
+name = "triton"
+version = "3.5.0"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f5/3a/e991574f3102147b642e49637e0281e9bb7c4ba254edb2bab78247c85e01/triton-3.5.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9e71db82261c4ffa3921cd050cd5faa18322d2d405c30eb56084afaff3b0833", size = 170476535, upload-time = "2025-10-13T16:38:05.18Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/29/10728de8a6e932e517c10773486b8e99f85d1b1d9dd87d9a9616e1fef4a1/triton-3.5.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e6bb9aa5519c084a333acdba443789e50012a4b851cd486c54f0b8dc2a8d3a12", size = 170487289, upload-time = "2025-10-13T16:38:11.662Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/38/db80e48b9220c9bce872b0f616ad0446cdf554a40b85c7865cbca99ab3c2/triton-3.5.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c83f2343e1a220a716c7b3ab9fccfcbe3ad4020d189549200e2d2e8d5868bed9", size = 170577179, upload-time = "2025-10-13T16:38:17.865Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/60/1810655d1d856c9a4fcc90ee8966d85f552d98c53a6589f95ab2cbe27bb8/triton-3.5.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da0fa67ccd76c3dcfb0bffe1b1c57c685136a6bd33d141c24d9655d4185b1289", size = 170487949, upload-time = "2025-10-13T16:38:24.881Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/b7/1dec8433ac604c061173d0589d99217fe7bf90a70bdc375e745d044b8aad/triton-3.5.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:317fe477ea8fd4524a6a8c499fb0a36984a56d0b75bf9c9cb6133a1c56d5a6e7", size = 170580176, upload-time = "2025-10-13T16:38:31.14Z" },
+]
+
+[[package]]
+name = "types-pyyaml"
+version = "6.0.12.20250915"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/7e/69/3c51b36d04da19b92f9e815be12753125bd8bc247ba0470a982e6979e71c/types_pyyaml-6.0.12.20250915.tar.gz", hash = "sha256:0f8b54a528c303f0e6f7165687dd33fafa81c807fcac23f632b63aa624ced1d3", size = 17522, upload-time = "2025-09-15T03:01:00.728Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/bd/e0/1eed384f02555dde685fff1a1ac805c1c7dcb6dd019c916fe659b1c1f9ec/types_pyyaml-6.0.12.20250915-py3-none-any.whl", hash = "sha256:e7d4d9e064e89a3b3cae120b4990cd370874d2bf12fa5f46c97018dd5d3c9ab6", size = 20338, upload-time = "2025-09-15T03:00:59.218Z" },
+]
+
[[package]]
name = "typing-extensions"
version = "4.15.0"
@@ -797,72 +1916,92 @@ wheels = [
[[package]]
name = "virtualenv"
-version = "20.34.0"
+version = "20.35.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
{ name = "filelock" },
{ name = "platformdirs" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/1c/14/37fcdba2808a6c615681cd216fecae00413c9dab44fb2e57805ecf3eaee3/virtualenv-20.34.0.tar.gz", hash = "sha256:44815b2c9dee7ed86e387b842a84f20b93f7f417f95886ca1996a72a4138eb1a", size = 6003808, upload-time = "2025-08-13T14:24:07.464Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a4/d5/b0ccd381d55c8f45d46f77df6ae59fbc23d19e901e2d523395598e5f4c93/virtualenv-20.35.3.tar.gz", hash = "sha256:4f1a845d131133bdff10590489610c98c168ff99dc75d6c96853801f7f67af44", size = 6002907, upload-time = "2025-10-10T21:23:33.178Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/76/06/04c8e804f813cf972e3262f3f8584c232de64f0cde9f703b46cf53a45090/virtualenv-20.34.0-py3-none-any.whl", hash = "sha256:341f5afa7eee943e4984a9207c025feedd768baff6753cd660c857ceb3e36026", size = 5983279, upload-time = "2025-08-13T14:24:05.111Z" },
+ { url = "https://files.pythonhosted.org/packages/27/73/d9a94da0e9d470a543c1b9d3ccbceb0f59455983088e727b8a1824ed90fb/virtualenv-20.35.3-py3-none-any.whl", hash = "sha256:63d106565078d8c8d0b206d48080f938a8b25361e19432d2c9db40d2899c810a", size = 5981061, upload-time = "2025-10-10T21:23:30.433Z" },
]
[[package]]
name = "wcwidth"
-version = "0.2.13"
+version = "0.2.14"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/24/30/6b0809f4510673dc723187aeaf24c7f5459922d01e2f794277a3dfb90345/wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605", size = 102293, upload-time = "2025-09-22T16:29:53.023Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" },
+ { url = "https://files.pythonhosted.org/packages/af/b5/123f13c975e9f27ab9c0770f514345bd406d0e8d3b7a0723af9d43f710af/wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1", size = 37286, upload-time = "2025-09-22T16:29:51.641Z" },
]
[[package]]
name = "wrapt"
-version = "1.17.3"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/95/8f/aeb76c5b46e273670962298c23e7ddde79916cb74db802131d49a85e4b7d/wrapt-1.17.3.tar.gz", hash = "sha256:f66eb08feaa410fe4eebd17f2a2c8e2e46d3476e9f8c783daa8e09e0faa666d0", size = 55547, upload-time = "2025-08-12T05:53:21.714Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/9f/41/cad1aba93e752f1f9268c77270da3c469883d56e2798e7df6240dcb2287b/wrapt-1.17.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ab232e7fdb44cdfbf55fc3afa31bcdb0d8980b9b95c38b6405df2acb672af0e0", size = 53998, upload-time = "2025-08-12T05:51:47.138Z" },
- { url = "https://files.pythonhosted.org/packages/60/f8/096a7cc13097a1869fe44efe68dace40d2a16ecb853141394047f0780b96/wrapt-1.17.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9baa544e6acc91130e926e8c802a17f3b16fbea0fd441b5a60f5cf2cc5c3deba", size = 39020, upload-time = "2025-08-12T05:51:35.906Z" },
- { url = "https://files.pythonhosted.org/packages/33/df/bdf864b8997aab4febb96a9ae5c124f700a5abd9b5e13d2a3214ec4be705/wrapt-1.17.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6b538e31eca1a7ea4605e44f81a48aa24c4632a277431a6ed3f328835901f4fd", size = 39098, upload-time = "2025-08-12T05:51:57.474Z" },
- { url = "https://files.pythonhosted.org/packages/9f/81/5d931d78d0eb732b95dc3ddaeeb71c8bb572fb01356e9133916cd729ecdd/wrapt-1.17.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:042ec3bb8f319c147b1301f2393bc19dba6e176b7da446853406d041c36c7828", size = 88036, upload-time = "2025-08-12T05:52:34.784Z" },
- { url = "https://files.pythonhosted.org/packages/ca/38/2e1785df03b3d72d34fc6252d91d9d12dc27a5c89caef3335a1bbb8908ca/wrapt-1.17.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3af60380ba0b7b5aeb329bc4e402acd25bd877e98b3727b0135cb5c2efdaefe9", size = 88156, upload-time = "2025-08-12T05:52:13.599Z" },
- { url = "https://files.pythonhosted.org/packages/b3/8b/48cdb60fe0603e34e05cffda0b2a4adab81fd43718e11111a4b0100fd7c1/wrapt-1.17.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b02e424deef65c9f7326d8c19220a2c9040c51dc165cddb732f16198c168396", size = 87102, upload-time = "2025-08-12T05:52:14.56Z" },
- { url = "https://files.pythonhosted.org/packages/3c/51/d81abca783b58f40a154f1b2c56db1d2d9e0d04fa2d4224e357529f57a57/wrapt-1.17.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:74afa28374a3c3a11b3b5e5fca0ae03bef8450d6aa3ab3a1e2c30e3a75d023dc", size = 87732, upload-time = "2025-08-12T05:52:36.165Z" },
- { url = "https://files.pythonhosted.org/packages/9e/b1/43b286ca1392a006d5336412d41663eeef1ad57485f3e52c767376ba7e5a/wrapt-1.17.3-cp312-cp312-win32.whl", hash = "sha256:4da9f45279fff3543c371d5ababc57a0384f70be244de7759c85a7f989cb4ebe", size = 36705, upload-time = "2025-08-12T05:53:07.123Z" },
- { url = "https://files.pythonhosted.org/packages/28/de/49493f962bd3c586ab4b88066e967aa2e0703d6ef2c43aa28cb83bf7b507/wrapt-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:e71d5c6ebac14875668a1e90baf2ea0ef5b7ac7918355850c0908ae82bcb297c", size = 38877, upload-time = "2025-08-12T05:53:05.436Z" },
- { url = "https://files.pythonhosted.org/packages/f1/48/0f7102fe9cb1e8a5a77f80d4f0956d62d97034bbe88d33e94699f99d181d/wrapt-1.17.3-cp312-cp312-win_arm64.whl", hash = "sha256:604d076c55e2fdd4c1c03d06dc1a31b95130010517b5019db15365ec4a405fc6", size = 36885, upload-time = "2025-08-12T05:52:54.367Z" },
- { url = "https://files.pythonhosted.org/packages/fc/f6/759ece88472157acb55fc195e5b116e06730f1b651b5b314c66291729193/wrapt-1.17.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a47681378a0439215912ef542c45a783484d4dd82bac412b71e59cf9c0e1cea0", size = 54003, upload-time = "2025-08-12T05:51:48.627Z" },
- { url = "https://files.pythonhosted.org/packages/4f/a9/49940b9dc6d47027dc850c116d79b4155f15c08547d04db0f07121499347/wrapt-1.17.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a30837587c6ee3cd1a4d1c2ec5d24e77984d44e2f34547e2323ddb4e22eb77", size = 39025, upload-time = "2025-08-12T05:51:37.156Z" },
- { url = "https://files.pythonhosted.org/packages/45/35/6a08de0f2c96dcdd7fe464d7420ddb9a7655a6561150e5fc4da9356aeaab/wrapt-1.17.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:16ecf15d6af39246fe33e507105d67e4b81d8f8d2c6598ff7e3ca1b8a37213f7", size = 39108, upload-time = "2025-08-12T05:51:58.425Z" },
- { url = "https://files.pythonhosted.org/packages/0c/37/6faf15cfa41bf1f3dba80cd3f5ccc6622dfccb660ab26ed79f0178c7497f/wrapt-1.17.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6fd1ad24dc235e4ab88cda009e19bf347aabb975e44fd5c2fb22a3f6e4141277", size = 88072, upload-time = "2025-08-12T05:52:37.53Z" },
- { url = "https://files.pythonhosted.org/packages/78/f2/efe19ada4a38e4e15b6dff39c3e3f3f73f5decf901f66e6f72fe79623a06/wrapt-1.17.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ed61b7c2d49cee3c027372df5809a59d60cf1b6c2f81ee980a091f3afed6a2d", size = 88214, upload-time = "2025-08-12T05:52:15.886Z" },
- { url = "https://files.pythonhosted.org/packages/40/90/ca86701e9de1622b16e09689fc24b76f69b06bb0150990f6f4e8b0eeb576/wrapt-1.17.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:423ed5420ad5f5529db9ce89eac09c8a2f97da18eb1c870237e84c5a5c2d60aa", size = 87105, upload-time = "2025-08-12T05:52:17.914Z" },
- { url = "https://files.pythonhosted.org/packages/fd/e0/d10bd257c9a3e15cbf5523025252cc14d77468e8ed644aafb2d6f54cb95d/wrapt-1.17.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e01375f275f010fcbf7f643b4279896d04e571889b8a5b3f848423d91bf07050", size = 87766, upload-time = "2025-08-12T05:52:39.243Z" },
- { url = "https://files.pythonhosted.org/packages/e8/cf/7d848740203c7b4b27eb55dbfede11aca974a51c3d894f6cc4b865f42f58/wrapt-1.17.3-cp313-cp313-win32.whl", hash = "sha256:53e5e39ff71b3fc484df8a522c933ea2b7cdd0d5d15ae82e5b23fde87d44cbd8", size = 36711, upload-time = "2025-08-12T05:53:10.074Z" },
- { url = "https://files.pythonhosted.org/packages/57/54/35a84d0a4d23ea675994104e667ceff49227ce473ba6a59ba2c84f250b74/wrapt-1.17.3-cp313-cp313-win_amd64.whl", hash = "sha256:1f0b2f40cf341ee8cc1a97d51ff50dddb9fcc73241b9143ec74b30fc4f44f6cb", size = 38885, upload-time = "2025-08-12T05:53:08.695Z" },
- { url = "https://files.pythonhosted.org/packages/01/77/66e54407c59d7b02a3c4e0af3783168fff8e5d61def52cda8728439d86bc/wrapt-1.17.3-cp313-cp313-win_arm64.whl", hash = "sha256:7425ac3c54430f5fc5e7b6f41d41e704db073309acfc09305816bc6a0b26bb16", size = 36896, upload-time = "2025-08-12T05:52:55.34Z" },
- { url = "https://files.pythonhosted.org/packages/02/a2/cd864b2a14f20d14f4c496fab97802001560f9f41554eef6df201cd7f76c/wrapt-1.17.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cf30f6e3c077c8e6a9a7809c94551203c8843e74ba0c960f4a98cd80d4665d39", size = 54132, upload-time = "2025-08-12T05:51:49.864Z" },
- { url = "https://files.pythonhosted.org/packages/d5/46/d011725b0c89e853dc44cceb738a307cde5d240d023d6d40a82d1b4e1182/wrapt-1.17.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e228514a06843cae89621384cfe3a80418f3c04aadf8a3b14e46a7be704e4235", size = 39091, upload-time = "2025-08-12T05:51:38.935Z" },
- { url = "https://files.pythonhosted.org/packages/2e/9e/3ad852d77c35aae7ddebdbc3b6d35ec8013af7d7dddad0ad911f3d891dae/wrapt-1.17.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5ea5eb3c0c071862997d6f3e02af1d055f381b1d25b286b9d6644b79db77657c", size = 39172, upload-time = "2025-08-12T05:51:59.365Z" },
- { url = "https://files.pythonhosted.org/packages/c3/f7/c983d2762bcce2326c317c26a6a1e7016f7eb039c27cdf5c4e30f4160f31/wrapt-1.17.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:281262213373b6d5e4bb4353bc36d1ba4084e6d6b5d242863721ef2bf2c2930b", size = 87163, upload-time = "2025-08-12T05:52:40.965Z" },
- { url = "https://files.pythonhosted.org/packages/e4/0f/f673f75d489c7f22d17fe0193e84b41540d962f75fce579cf6873167c29b/wrapt-1.17.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4a8d2b25efb6681ecacad42fca8859f88092d8732b170de6a5dddd80a1c8fa", size = 87963, upload-time = "2025-08-12T05:52:20.326Z" },
- { url = "https://files.pythonhosted.org/packages/df/61/515ad6caca68995da2fac7a6af97faab8f78ebe3bf4f761e1b77efbc47b5/wrapt-1.17.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:373342dd05b1d07d752cecbec0c41817231f29f3a89aa8b8843f7b95992ed0c7", size = 86945, upload-time = "2025-08-12T05:52:21.581Z" },
- { url = "https://files.pythonhosted.org/packages/d3/bd/4e70162ce398462a467bc09e768bee112f1412e563620adc353de9055d33/wrapt-1.17.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d40770d7c0fd5cbed9d84b2c3f2e156431a12c9a37dc6284060fb4bec0b7ffd4", size = 86857, upload-time = "2025-08-12T05:52:43.043Z" },
- { url = "https://files.pythonhosted.org/packages/2b/b8/da8560695e9284810b8d3df8a19396a6e40e7518059584a1a394a2b35e0a/wrapt-1.17.3-cp314-cp314-win32.whl", hash = "sha256:fbd3c8319de8e1dc79d346929cd71d523622da527cca14e0c1d257e31c2b8b10", size = 37178, upload-time = "2025-08-12T05:53:12.605Z" },
- { url = "https://files.pythonhosted.org/packages/db/c8/b71eeb192c440d67a5a0449aaee2310a1a1e8eca41676046f99ed2487e9f/wrapt-1.17.3-cp314-cp314-win_amd64.whl", hash = "sha256:e1a4120ae5705f673727d3253de3ed0e016f7cd78dc463db1b31e2463e1f3cf6", size = 39310, upload-time = "2025-08-12T05:53:11.106Z" },
- { url = "https://files.pythonhosted.org/packages/45/20/2cda20fd4865fa40f86f6c46ed37a2a8356a7a2fde0773269311f2af56c7/wrapt-1.17.3-cp314-cp314-win_arm64.whl", hash = "sha256:507553480670cab08a800b9463bdb881b2edeed77dc677b0a5915e6106e91a58", size = 37266, upload-time = "2025-08-12T05:52:56.531Z" },
- { url = "https://files.pythonhosted.org/packages/77/ed/dd5cf21aec36c80443c6f900449260b80e2a65cf963668eaef3b9accce36/wrapt-1.17.3-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ed7c635ae45cfbc1a7371f708727bf74690daedc49b4dba310590ca0bd28aa8a", size = 56544, upload-time = "2025-08-12T05:51:51.109Z" },
- { url = "https://files.pythonhosted.org/packages/8d/96/450c651cc753877ad100c7949ab4d2e2ecc4d97157e00fa8f45df682456a/wrapt-1.17.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:249f88ed15503f6492a71f01442abddd73856a0032ae860de6d75ca62eed8067", size = 40283, upload-time = "2025-08-12T05:51:39.912Z" },
- { url = "https://files.pythonhosted.org/packages/d1/86/2fcad95994d9b572db57632acb6f900695a648c3e063f2cd344b3f5c5a37/wrapt-1.17.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a03a38adec8066d5a37bea22f2ba6bbf39fcdefbe2d91419ab864c3fb515454", size = 40366, upload-time = "2025-08-12T05:52:00.693Z" },
- { url = "https://files.pythonhosted.org/packages/64/0e/f4472f2fdde2d4617975144311f8800ef73677a159be7fe61fa50997d6c0/wrapt-1.17.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5d4478d72eb61c36e5b446e375bbc49ed002430d17cdec3cecb36993398e1a9e", size = 108571, upload-time = "2025-08-12T05:52:44.521Z" },
- { url = "https://files.pythonhosted.org/packages/cc/01/9b85a99996b0a97c8a17484684f206cbb6ba73c1ce6890ac668bcf3838fb/wrapt-1.17.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:223db574bb38637e8230eb14b185565023ab624474df94d2af18f1cdb625216f", size = 113094, upload-time = "2025-08-12T05:52:22.618Z" },
- { url = "https://files.pythonhosted.org/packages/25/02/78926c1efddcc7b3aa0bc3d6b33a822f7d898059f7cd9ace8c8318e559ef/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e405adefb53a435f01efa7ccdec012c016b5a1d3f35459990afc39b6be4d5056", size = 110659, upload-time = "2025-08-12T05:52:24.057Z" },
- { url = "https://files.pythonhosted.org/packages/dc/ee/c414501ad518ac3e6fe184753632fe5e5ecacdcf0effc23f31c1e4f7bfcf/wrapt-1.17.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:88547535b787a6c9ce4086917b6e1d291aa8ed914fdd3a838b3539dc95c12804", size = 106946, upload-time = "2025-08-12T05:52:45.976Z" },
- { url = "https://files.pythonhosted.org/packages/be/44/a1bd64b723d13bb151d6cc91b986146a1952385e0392a78567e12149c7b4/wrapt-1.17.3-cp314-cp314t-win32.whl", hash = "sha256:41b1d2bc74c2cac6f9074df52b2efbef2b30bdfe5f40cb78f8ca22963bc62977", size = 38717, upload-time = "2025-08-12T05:53:15.214Z" },
- { url = "https://files.pythonhosted.org/packages/79/d9/7cfd5a312760ac4dd8bf0184a6ee9e43c33e47f3dadc303032ce012b8fa3/wrapt-1.17.3-cp314-cp314t-win_amd64.whl", hash = "sha256:73d496de46cd2cdbdbcce4ae4bcdb4afb6a11234a1df9c085249d55166b95116", size = 41334, upload-time = "2025-08-12T05:53:14.178Z" },
- { url = "https://files.pythonhosted.org/packages/46/78/10ad9781128ed2f99dbc474f43283b13fea8ba58723e98844367531c18e9/wrapt-1.17.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f38e60678850c42461d4202739f9bf1e3a737c7ad283638251e79cc49effb6b6", size = 38471, upload-time = "2025-08-12T05:52:57.784Z" },
- { url = "https://files.pythonhosted.org/packages/1f/f6/a933bd70f98e9cf3e08167fc5cd7aaaca49147e48411c0bd5ae701bb2194/wrapt-1.17.3-py3-none-any.whl", hash = "sha256:7171ae35d2c33d326ac19dd8facb1e82e5fd04ef8c6c0e394d7af55a55051c22", size = 23591, upload-time = "2025-08-12T05:53:20.674Z" },
+version = "2.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/49/2a/6de8a50cb435b7f42c46126cf1a54b2aab81784e74c8595c8e025e8f36d3/wrapt-2.0.1.tar.gz", hash = "sha256:9c9c635e78497cacb81e84f8b11b23e0aacac7a136e73b8e5b2109a1d9fc468f", size = 82040, upload-time = "2025-11-07T00:45:33.312Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" },
+ { url = "https://files.pythonhosted.org/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" },
+ { url = "https://files.pythonhosted.org/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" },
+ { url = "https://files.pythonhosted.org/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9", size = 58246, upload-time = "2025-11-07T00:44:03.169Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75", size = 60492, upload-time = "2025-11-07T00:44:01.027Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b", size = 58987, upload-time = "2025-11-07T00:44:02.095Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/fe/41af4c46b5e498c90fc87981ab2972fbd9f0bccda597adb99d3d3441b94b/wrapt-2.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:47b0f8bafe90f7736151f61482c583c86b0693d80f075a58701dd1549b0010a9", size = 78132, upload-time = "2025-11-07T00:44:04.628Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/92/d68895a984a5ebbbfb175512b0c0aad872354a4a2484fbd5552e9f275316/wrapt-2.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cbeb0971e13b4bd81d34169ed57a6dda017328d1a22b62fda45e1d21dd06148f", size = 61211, upload-time = "2025-11-07T00:44:05.626Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/26/ba83dc5ae7cf5aa2b02364a3d9cf74374b86169906a1f3ade9a2d03cf21c/wrapt-2.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb7cffe572ad0a141a7886a1d2efa5bef0bf7fe021deeea76b3ab334d2c38218", size = 61689, upload-time = "2025-11-07T00:44:06.719Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/67/d7a7c276d874e5d26738c22444d466a3a64ed541f6ef35f740dbd865bab4/wrapt-2.0.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8d60527d1ecfc131426b10d93ab5d53e08a09c5fa0175f6b21b3252080c70a9", size = 121502, upload-time = "2025-11-07T00:44:09.557Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/6b/806dbf6dd9579556aab22fc92908a876636e250f063f71548a8660382184/wrapt-2.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c654eafb01afac55246053d67a4b9a984a3567c3808bb7df2f8de1c1caba2e1c", size = 123110, upload-time = "2025-11-07T00:44:10.64Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/08/cdbb965fbe4c02c5233d185d070cabed2ecc1f1e47662854f95d77613f57/wrapt-2.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:98d873ed6c8b4ee2418f7afce666751854d6d03e3c0ec2a399bb039cd2ae89db", size = 117434, upload-time = "2025-11-07T00:44:08.138Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/d1/6aae2ce39db4cb5216302fa2e9577ad74424dfbe315bd6669725569e048c/wrapt-2.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9e850f5b7fc67af856ff054c71690d54fa940c3ef74209ad9f935b4f66a0233", size = 121533, upload-time = "2025-11-07T00:44:12.142Z" },
+ { url = "https://files.pythonhosted.org/packages/79/35/565abf57559fbe0a9155c29879ff43ce8bd28d2ca61033a3a3dd67b70794/wrapt-2.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e505629359cb5f751e16e30cf3f91a1d3ddb4552480c205947da415d597f7ac2", size = 116324, upload-time = "2025-11-07T00:44:13.28Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/e0/53ff5e76587822ee33e560ad55876d858e384158272cd9947abdd4ad42ca/wrapt-2.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2879af909312d0baf35f08edeea918ee3af7ab57c37fe47cb6a373c9f2749c7b", size = 120627, upload-time = "2025-11-07T00:44:14.431Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/7b/38df30fd629fbd7612c407643c63e80e1c60bcc982e30ceeae163a9800e7/wrapt-2.0.1-cp313-cp313-win32.whl", hash = "sha256:d67956c676be5a24102c7407a71f4126d30de2a569a1c7871c9f3cabc94225d7", size = 58252, upload-time = "2025-11-07T00:44:17.814Z" },
+ { url = "https://files.pythonhosted.org/packages/85/64/d3954e836ea67c4d3ad5285e5c8fd9d362fd0a189a2db622df457b0f4f6a/wrapt-2.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:9ca66b38dd642bf90c59b6738af8070747b610115a39af2498535f62b5cdc1c3", size = 60500, upload-time = "2025-11-07T00:44:15.561Z" },
+ { url = "https://files.pythonhosted.org/packages/89/4e/3c8b99ac93527cfab7f116089db120fef16aac96e5f6cdb724ddf286086d/wrapt-2.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:5a4939eae35db6b6cec8e7aa0e833dcca0acad8231672c26c2a9ab7a0f8ac9c8", size = 58993, upload-time = "2025-11-07T00:44:16.65Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/f4/eff2b7d711cae20d220780b9300faa05558660afb93f2ff5db61fe725b9a/wrapt-2.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a52f93d95c8d38fed0669da2ebdb0b0376e895d84596a976c15a9eb45e3eccb3", size = 82028, upload-time = "2025-11-07T00:44:18.944Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/67/cb945563f66fd0f61a999339460d950f4735c69f18f0a87ca586319b1778/wrapt-2.0.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e54bbf554ee29fcceee24fa41c4d091398b911da6e7f5d7bffda963c9aed2e1", size = 62949, upload-time = "2025-11-07T00:44:20.074Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/ca/f63e177f0bbe1e5cf5e8d9b74a286537cd709724384ff20860f8f6065904/wrapt-2.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:908f8c6c71557f4deaa280f55d0728c3bca0960e8c3dd5ceeeafb3c19942719d", size = 63681, upload-time = "2025-11-07T00:44:21.345Z" },
+ { url = "https://files.pythonhosted.org/packages/39/a1/1b88fcd21fd835dca48b556daef750952e917a2794fa20c025489e2e1f0f/wrapt-2.0.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e2f84e9af2060e3904a32cea9bb6db23ce3f91cfd90c6b426757cf7cc01c45c7", size = 152696, upload-time = "2025-11-07T00:44:24.318Z" },
+ { url = "https://files.pythonhosted.org/packages/62/1c/d9185500c1960d9f5f77b9c0b890b7fc62282b53af7ad1b6bd779157f714/wrapt-2.0.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3612dc06b436968dfb9142c62e5dfa9eb5924f91120b3c8ff501ad878f90eb3", size = 158859, upload-time = "2025-11-07T00:44:25.494Z" },
+ { url = "https://files.pythonhosted.org/packages/91/60/5d796ed0f481ec003220c7878a1d6894652efe089853a208ea0838c13086/wrapt-2.0.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d2d947d266d99a1477cd005b23cbd09465276e302515e122df56bb9511aca1b", size = 146068, upload-time = "2025-11-07T00:44:22.81Z" },
+ { url = "https://files.pythonhosted.org/packages/04/f8/75282dd72f102ddbfba137e1e15ecba47b40acff32c08ae97edbf53f469e/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7d539241e87b650cbc4c3ac9f32c8d1ac8a54e510f6dca3f6ab60dcfd48c9b10", size = 155724, upload-time = "2025-11-07T00:44:26.634Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/27/fe39c51d1b344caebb4a6a9372157bdb8d25b194b3561b52c8ffc40ac7d1/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:4811e15d88ee62dbf5c77f2c3ff3932b1e3ac92323ba3912f51fc4016ce81ecf", size = 144413, upload-time = "2025-11-07T00:44:27.939Z" },
+ { url = "https://files.pythonhosted.org/packages/83/2b/9f6b643fe39d4505c7bf926d7c2595b7cb4b607c8c6b500e56c6b36ac238/wrapt-2.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c1c91405fcf1d501fa5d55df21e58ea49e6b879ae829f1039faaf7e5e509b41e", size = 150325, upload-time = "2025-11-07T00:44:29.29Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/b6/20ffcf2558596a7f58a2e69c89597128781f0b88e124bf5a4cadc05b8139/wrapt-2.0.1-cp313-cp313t-win32.whl", hash = "sha256:e76e3f91f864e89db8b8d2a8311d57df93f01ad6bb1e9b9976d1f2e83e18315c", size = 59943, upload-time = "2025-11-07T00:44:33.211Z" },
+ { url = "https://files.pythonhosted.org/packages/87/6a/0e56111cbb3320151eed5d3821ee1373be13e05b376ea0870711f18810c3/wrapt-2.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:83ce30937f0ba0d28818807b303a412440c4b63e39d3d8fc036a94764b728c92", size = 63240, upload-time = "2025-11-07T00:44:30.935Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/54/5ab4c53ea1f7f7e5c3e7c1095db92932cc32fd62359d285486d00c2884c3/wrapt-2.0.1-cp313-cp313t-win_arm64.whl", hash = "sha256:4b55cacc57e1dc2d0991dbe74c6419ffd415fb66474a02335cb10efd1aa3f84f", size = 60416, upload-time = "2025-11-07T00:44:32.002Z" },
+ { url = "https://files.pythonhosted.org/packages/73/81/d08d83c102709258e7730d3cd25befd114c60e43ef3891d7e6877971c514/wrapt-2.0.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5e53b428f65ece6d9dad23cb87e64506392b720a0b45076c05354d27a13351a1", size = 78290, upload-time = "2025-11-07T00:44:34.691Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/14/393afba2abb65677f313aa680ff0981e829626fed39b6a7e3ec807487790/wrapt-2.0.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ad3ee9d0f254851c71780966eb417ef8e72117155cff04821ab9b60549694a55", size = 61255, upload-time = "2025-11-07T00:44:35.762Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/10/a4a1f2fba205a9462e36e708ba37e5ac95f4987a0f1f8fd23f0bf1fc3b0f/wrapt-2.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7b822c61ed04ee6ad64bc90d13368ad6eb094db54883b5dde2182f67a7f22c0", size = 61797, upload-time = "2025-11-07T00:44:37.22Z" },
+ { url = "https://files.pythonhosted.org/packages/12/db/99ba5c37cf1c4fad35349174f1e38bd8d992340afc1ff27f526729b98986/wrapt-2.0.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7164a55f5e83a9a0b031d3ffab4d4e36bbec42e7025db560f225489fa929e509", size = 120470, upload-time = "2025-11-07T00:44:39.425Z" },
+ { url = "https://files.pythonhosted.org/packages/30/3f/a1c8d2411eb826d695fc3395a431757331582907a0ec59afce8fe8712473/wrapt-2.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e60690ba71a57424c8d9ff28f8d006b7ad7772c22a4af432188572cd7fa004a1", size = 122851, upload-time = "2025-11-07T00:44:40.582Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/8d/72c74a63f201768d6a04a8845c7976f86be6f5ff4d74996c272cefc8dafc/wrapt-2.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cd1a4bd9a7a619922a8557e1318232e7269b5fb69d4ba97b04d20450a6bf970", size = 117433, upload-time = "2025-11-07T00:44:38.313Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/5a/df37cf4042cb13b08256f8e27023e2f9b3d471d553376616591bb99bcb31/wrapt-2.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4c2e3d777e38e913b8ce3a6257af72fb608f86a1df471cb1d4339755d0a807c", size = 121280, upload-time = "2025-11-07T00:44:41.69Z" },
+ { url = "https://files.pythonhosted.org/packages/54/34/40d6bc89349f9931e1186ceb3e5fbd61d307fef814f09fbbac98ada6a0c8/wrapt-2.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3d366aa598d69416b5afedf1faa539fac40c1d80a42f6b236c88c73a3c8f2d41", size = 116343, upload-time = "2025-11-07T00:44:43.013Z" },
+ { url = "https://files.pythonhosted.org/packages/70/66/81c3461adece09d20781dee17c2366fdf0cb8754738b521d221ca056d596/wrapt-2.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c235095d6d090aa903f1db61f892fffb779c1eaeb2a50e566b52001f7a0f66ed", size = 119650, upload-time = "2025-11-07T00:44:44.523Z" },
+ { url = "https://files.pythonhosted.org/packages/46/3a/d0146db8be8761a9e388cc9cc1c312b36d583950ec91696f19bbbb44af5a/wrapt-2.0.1-cp314-cp314-win32.whl", hash = "sha256:bfb5539005259f8127ea9c885bdc231978c06b7a980e63a8a61c8c4c979719d0", size = 58701, upload-time = "2025-11-07T00:44:48.277Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/38/5359da9af7d64554be63e9046164bd4d8ff289a2dd365677d25ba3342c08/wrapt-2.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:4ae879acc449caa9ed43fc36ba08392b9412ee67941748d31d94e3cedb36628c", size = 60947, upload-time = "2025-11-07T00:44:46.086Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/3f/96db0619276a833842bf36343685fa04f987dd6e3037f314531a1e00492b/wrapt-2.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:8639b843c9efd84675f1e100ed9e99538ebea7297b62c4b45a7042edb84db03e", size = 59359, upload-time = "2025-11-07T00:44:47.164Z" },
+ { url = "https://files.pythonhosted.org/packages/71/49/5f5d1e867bf2064bf3933bc6cf36ade23505f3902390e175e392173d36a2/wrapt-2.0.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:9219a1d946a9b32bb23ccae66bdb61e35c62773ce7ca6509ceea70f344656b7b", size = 82031, upload-time = "2025-11-07T00:44:49.4Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/89/0009a218d88db66ceb83921e5685e820e2c61b59bbbb1324ba65342668bc/wrapt-2.0.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fa4184e74197af3adad3c889a1af95b53bb0466bced92ea99a0c014e48323eec", size = 62952, upload-time = "2025-11-07T00:44:50.74Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/18/9b968e920dd05d6e44bcc918a046d02afea0fb31b2f1c80ee4020f377cbe/wrapt-2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c5ef2f2b8a53b7caee2f797ef166a390fef73979b15778a4a153e4b5fedce8fa", size = 63688, upload-time = "2025-11-07T00:44:52.248Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/7d/78bdcb75826725885d9ea26c49a03071b10c4c92da93edda612910f150e4/wrapt-2.0.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e042d653a4745be832d5aa190ff80ee4f02c34b21f4b785745eceacd0907b815", size = 152706, upload-time = "2025-11-07T00:44:54.613Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/77/cac1d46f47d32084a703df0d2d29d47e7eb2a7d19fa5cbca0e529ef57659/wrapt-2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2afa23318136709c4b23d87d543b425c399887b4057936cd20386d5b1422b6fa", size = 158866, upload-time = "2025-11-07T00:44:55.79Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/11/b521406daa2421508903bf8d5e8b929216ec2af04839db31c0a2c525eee0/wrapt-2.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c72328f668cf4c503ffcf9434c2b71fdd624345ced7941bc6693e61bbe36bef", size = 146148, upload-time = "2025-11-07T00:44:53.388Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/c0/340b272bed297baa7c9ce0c98ef7017d9c035a17a6a71dce3184b8382da2/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3793ac154afb0e5b45d1233cb94d354ef7a983708cc3bb12563853b1d8d53747", size = 155737, upload-time = "2025-11-07T00:44:56.971Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/93/bfcb1fb2bdf186e9c2883a4d1ab45ab099c79cbf8f4e70ea453811fa3ea7/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fec0d993ecba3991645b4857837277469c8cc4c554a7e24d064d1ca291cfb81f", size = 144451, upload-time = "2025-11-07T00:44:58.515Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/6b/dca504fb18d971139d232652656180e3bd57120e1193d9a5899c3c0b7cdd/wrapt-2.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:949520bccc1fa227274da7d03bf238be15389cd94e32e4297b92337df9b7a349", size = 150353, upload-time = "2025-11-07T00:44:59.753Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/f6/a1de4bd3653afdf91d250ca5c721ee51195df2b61a4603d4b373aa804d1d/wrapt-2.0.1-cp314-cp314t-win32.whl", hash = "sha256:be9e84e91d6497ba62594158d3d31ec0486c60055c49179edc51ee43d095f79c", size = 60609, upload-time = "2025-11-07T00:45:03.315Z" },
+ { url = "https://files.pythonhosted.org/packages/01/3a/07cd60a9d26fe73efead61c7830af975dfdba8537632d410462672e4432b/wrapt-2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:61c4956171c7434634401db448371277d07032a81cc21c599c22953374781395", size = 64038, upload-time = "2025-11-07T00:45:00.948Z" },
+ { url = "https://files.pythonhosted.org/packages/41/99/8a06b8e17dddbf321325ae4eb12465804120f699cd1b8a355718300c62da/wrapt-2.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:35cdbd478607036fee40273be8ed54a451f5f23121bd9d4be515158f9498f7ad", size = 60634, upload-time = "2025-11-07T00:45:02.087Z" },
+ { url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" },
]
diff --git a/validation/cnn/validate.ipynb b/validation/cnn/validate.ipynb
new file mode 100644
index 0000000..ddf7597
--- /dev/null
+++ b/validation/cnn/validate.ipynb
@@ -0,0 +1,74 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "59299e09",
+ "metadata": {},
+ "outputs": [
+ {
+ "ename": "ModuleNotFoundError",
+ "evalue": "No module named 'notebooks.validate'",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
+ "\u001b[31mModuleNotFoundError\u001b[39m Traceback (most recent call last)",
+ "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[1]\u001b[39m\u001b[32m, line 12\u001b[39m\n\u001b[32m 9\u001b[39m \u001b[38;5;66;03m# Force reimport\u001b[39;00m\n\u001b[32m 10\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mimportlib\u001b[39;00m \u001b[38;5;66;03m# noqa: E402\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m12\u001b[39m \u001b[38;5;28;01mimport\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mnotebooks\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mvalidate\u001b[39;00m \u001b[38;5;66;03m# noqa: E402\u001b[39;00m\n\u001b[32m 14\u001b[39m importlib.reload(notebooks.validate)\n\u001b[32m 16\u001b[39m \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mnotebooks\u001b[39;00m\u001b[34;01m.\u001b[39;00m\u001b[34;01mvalidate\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[38;5;28;01mimport\u001b[39;00m main \u001b[38;5;66;03m# noqa: E402\u001b[39;00m\n",
+ "\u001b[31mModuleNotFoundError\u001b[39m: No module named 'notebooks.validate'"
+ ]
+ }
+ ],
+ "source": [
+ "import sys\n",
+ "from pathlib import Path\n",
+ "\n",
+ "# Add project root to path\n",
+ "project_root = Path.cwd().parents[1]\n",
+ "if str(project_root) not in sys.path:\n",
+ " sys.path.insert(0, str(project_root))\n",
+ "\n",
+ "# Force reimport\n",
+ "import importlib # noqa: E402\n",
+ "\n",
+ "import notebooks.cnn_validate.validate # noqa: E402\n",
+ "\n",
+ "importlib.reload(notebooks.validate)\n",
+ "\n",
+ "from notebooks.cnn_validate.validate import main # noqa: E402\n",
+ "\n",
+ "fig = main(\n",
+ " batch_size=32,\n",
+ " patience=10,\n",
+ " n_folds=4,\n",
+ " max_crop_size=128,\n",
+ " min_crop_size=16,\n",
+ " max_samples_per_image=10,\n",
+ " data_dir=project_root / \"data\" / \"sem_images\" / \"raw\",\n",
+ ")\n",
+ "\n",
+ "fig"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "ml-project",
+ "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.12"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/validation/cnn/validate.py b/validation/cnn/validate.py
new file mode 100644
index 0000000..caae203
--- /dev/null
+++ b/validation/cnn/validate.py
@@ -0,0 +1,1077 @@
+"""
+CNN Classifier Validation Module.
+
+Perform k-fold cross-validation with varying training data percentages
+to evaluate the CNN classifier's learning curve.
+"""
+
+import csv
+import random
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Dict, List, Optional, Tuple
+
+import matplotlib.pyplot as plt
+import numpy as np
+import torch
+import torch.nn as nn
+from PIL import Image
+from torch.utils.data import DataLoader, Dataset
+
+from auto_ml.models.cnn.model import CNNClassifier
+
+# ==============================================================================
+# Configuration
+# ==============================================================================
+
+# Project root directory (relative to this file)
+_PROJECT_ROOT = Path(__file__).resolve().parents[1]
+
+
+@dataclass
+class ValidationConfig:
+ """Configuration for validation experiments."""
+
+ # Data paths (relative to project root)
+ data_dir: Path = field(
+ default_factory=lambda: _PROJECT_ROOT / "data/sem_images/raw",
+ )
+ labels_file: str = "Labels.csv"
+
+ # Training percentages to evaluate
+ train_percentages: List[int] = field(
+ default_factory=lambda: [10, 20, 30, 40, 50, 60, 70, 80],
+ )
+
+ # K-fold settings
+ n_folds: int = 5
+
+ # Training settings
+ max_epochs: Optional[int] = None # If None, train until patience runs out
+ patience: int = 5
+ batch_size: int = 32
+ learning_rate: float = 1e-3
+
+ # Random crop settings
+ min_crop_size: int = 32
+ max_crop_size: int = 128 # Reduced default for memory efficiency
+ fixed_crop_size: Optional[int] = None # If set, use fixed size instead of random
+ max_samples_per_image: Optional[int] = (
+ None # If set, cap samples per image to this value
+ )
+
+ # Model settings
+ num_classes: int = 3
+ channels: int = 1
+ base_filters: int = 32
+ dropout: float = 0.5
+
+ # Reproducibility
+ seed: int = 42
+
+ # Device
+ device: str = "cuda" if torch.cuda.is_available() else "cpu"
+
+
+# ==============================================================================
+# Data Classes
+# ==============================================================================
+
+
+@dataclass
+class FoldMetrics:
+ """Store metrics for a single fold."""
+
+ fold: int
+ train_losses: List[float] = field(default_factory=list)
+ val_losses: List[float] = field(default_factory=list)
+ train_accuracies: List[float] = field(default_factory=list)
+ val_accuracies: List[float] = field(default_factory=list)
+ best_val_loss: float = float("inf")
+ best_epoch: int = 0
+ stopped_early: bool = False
+
+
+@dataclass
+class PercentageMetrics:
+ """Store metrics for a training percentage."""
+
+ percentage: int
+ fold_metrics: List[FoldMetrics] = field(default_factory=list)
+
+ @property
+ def mean_best_val_loss(self) -> float:
+ """Calculate mean best validation loss across folds."""
+ return float(np.mean([fm.best_val_loss for fm in self.fold_metrics]))
+
+ @property
+ def std_best_val_loss(self) -> float:
+ """Calculate std of best validation loss across folds."""
+ return float(np.std([fm.best_val_loss for fm in self.fold_metrics]))
+
+ @property
+ def mean_final_val_accuracy(self) -> float:
+ """Calculate mean final validation accuracy across folds."""
+ accuracies = [
+ fm.val_accuracies[fm.best_epoch] if fm.val_accuracies else 0.0
+ for fm in self.fold_metrics
+ ]
+ return float(np.mean(accuracies))
+
+ @property
+ def std_final_val_accuracy(self) -> float:
+ """Calculate std of final validation accuracy across folds."""
+ accuracies = [
+ fm.val_accuracies[fm.best_epoch] if fm.val_accuracies else 0.0
+ for fm in self.fold_metrics
+ ]
+ return float(np.std(accuracies))
+
+ @property
+ def mean_best_train_loss(self) -> float:
+ """Calculate mean training loss at best epoch across folds."""
+ losses = [
+ fm.train_losses[fm.best_epoch] if fm.train_losses else float("inf")
+ for fm in self.fold_metrics
+ ]
+ return float(np.mean(losses))
+
+ @property
+ def std_best_train_loss(self) -> float:
+ """Calculate std of training loss at best epoch across folds."""
+ losses = [
+ fm.train_losses[fm.best_epoch] if fm.train_losses else float("inf")
+ for fm in self.fold_metrics
+ ]
+ return float(np.std(losses))
+
+ @property
+ def mean_final_train_accuracy(self) -> float:
+ """Calculate mean training accuracy at best epoch across folds."""
+ accuracies = [
+ fm.train_accuracies[fm.best_epoch] if fm.train_accuracies else 0.0
+ for fm in self.fold_metrics
+ ]
+ return float(np.mean(accuracies))
+
+ @property
+ def std_final_train_accuracy(self) -> float:
+ """Calculate std of training accuracy at best epoch across folds."""
+ accuracies = [
+ fm.train_accuracies[fm.best_epoch] if fm.train_accuracies else 0.0
+ for fm in self.fold_metrics
+ ]
+ return float(np.std(accuracies))
+
+
+# ==============================================================================
+# Dataset
+# ==============================================================================
+
+
+class SEMDataset(Dataset):
+ """SEM Image Dataset with random square crop and rotation augmentation."""
+
+ def __init__(
+ self,
+ image_paths: List[Path],
+ labels: List[int],
+ min_crop_size: int = 32,
+ max_crop_size: int = 128,
+ fixed_crop_size: Optional[int] = None,
+ augment: bool = True,
+ random_rotation: bool = True,
+ rotation_angles: Tuple[int, ...] = (0, 90, 180, 270),
+ expand_dataset: bool = True,
+ max_samples_per_image: Optional[int] = None,
+ ) -> None:
+ """
+ Initialize the SEM dataset.
+
+ Args:
+ image_paths: List of paths to image files.
+ labels: List of integer labels corresponding to each image.
+ min_crop_size: Minimum size of random square crop.
+ max_crop_size: Maximum size of random square crop.
+ fixed_crop_size: If set, use this fixed size for all crops.
+ augment: Whether to apply random crop augmentation.
+ random_rotation: Whether to apply random rotation augmentation.
+ rotation_angles: Tuple of possible rotation angles in degrees.
+ expand_dataset: Whether to expand dataset size based on possible crops.
+ max_samples_per_image: If set, cap the samples per image to this value.
+
+ """
+ self.image_paths = image_paths
+ self.labels = labels
+ self.min_crop_size = min_crop_size
+ self.max_crop_size = max_crop_size
+ self.fixed_crop_size = fixed_crop_size
+ self.augment = augment
+ self.random_rotation = random_rotation
+ self.rotation_angles = rotation_angles
+ self.expand_dataset = expand_dataset
+ self.max_samples_per_image = max_samples_per_image
+
+ # Calculate samples per image based on possible non-overlapping crops
+ self.samples_per_image = self._calculate_samples_per_image()
+
+ def _calculate_samples_per_image(self) -> int:
+ """
+ Calculate the number of unique samples that can be extracted from each image.
+
+ Estimate based on non-overlapping crops at expected crop size.
+
+ Returns:
+ Number of samples per image.
+
+ """
+ if not self.augment or not self.expand_dataset or not self.image_paths:
+ return 1
+
+ # Get image dimensions from first image (assume all images same size)
+ img = Image.open(self.image_paths[0])
+ h, w = img.height, img.width
+
+ # Expected crop size
+ if self.fixed_crop_size is not None:
+ expected_crop = self.fixed_crop_size
+ else:
+ expected_crop = (self.min_crop_size + self.max_crop_size) // 2
+
+ # Number of non-overlapping crops in each dimension
+ crops_h = max(1, h // expected_crop)
+ crops_w = max(1, w // expected_crop)
+ num_crops = crops_h * crops_w
+
+ # Multiply by number of rotations if rotation is enabled
+ num_rotations = len(self.rotation_angles) if self.random_rotation else 1
+
+ calculated = num_crops * num_rotations
+
+ # Cap at max_samples_per_image if specified
+ if self.max_samples_per_image is not None:
+ return min(calculated, self.max_samples_per_image)
+
+ return calculated
+
+ def __len__(self) -> int:
+ """Return the number of samples (expanded if expand_dataset is True)."""
+ return len(self.image_paths) * self.samples_per_image
+
+ def __getitem__(self, idx: int) -> Tuple[torch.Tensor, int]:
+ """
+ Get a sample.
+
+ Args:
+ idx: Sample index.
+
+ Returns:
+ Tuple of (image tensor, label).
+
+ """
+ # Map expanded index to original image index
+ original_idx = idx % len(self.image_paths)
+
+ # Load image
+ img = Image.open(self.image_paths[original_idx]).convert("L") # Grayscale
+ img_array = np.array(img, dtype=np.float32) / 255.0
+
+ if self.augment:
+ img_array = self._random_square_crop(img_array)
+ if self.random_rotation:
+ img_array = self._random_rotate(img_array)
+ else:
+ # Use deterministic random crop for validation (variable size, reproducible)
+ img_array = self._deterministic_random_crop(img_array, idx)
+
+ # Convert to tensor: (H, W) -> (1, H, W)
+ tensor = torch.from_numpy(img_array).unsqueeze(0)
+
+ return tensor, self.labels[original_idx]
+
+ def _random_rotate(self, img: np.ndarray) -> np.ndarray:
+ """
+ Apply random rotation to the image.
+
+ Args:
+ img: Input image array of shape (H, W).
+
+ Returns:
+ Rotated image array.
+
+ """
+ angle = random.choice(self.rotation_angles)
+ if angle == 0:
+ return img
+ # np.rot90 rotates counter-clockwise, so we adjust for 90-degree increments
+ k = angle // 90
+ return np.ascontiguousarray(np.rot90(img, k=k))
+
+ def _random_square_crop(self, img: np.ndarray) -> np.ndarray:
+ """
+ Extract a random square crop from the image.
+
+ Args:
+ img: Input image array of shape (H, W).
+
+ Returns:
+ Cropped image array.
+
+ """
+ h, w = img.shape
+
+ # Determine crop size (must fit within image)
+ if self.fixed_crop_size is not None:
+ crop_size = min(self.fixed_crop_size, h, w)
+ else:
+ max_possible_size = min(h, w, self.max_crop_size)
+ min_size = min(self.min_crop_size, max_possible_size)
+ crop_size = random.randint(min_size, max_possible_size)
+
+ # Random top-left corner
+ top = random.randint(0, h - crop_size)
+ left = random.randint(0, w - crop_size)
+
+ return img[top : top + crop_size, left : left + crop_size]
+
+ def _deterministic_random_crop(
+ self,
+ img: np.ndarray,
+ idx: int,
+ ) -> np.ndarray:
+ """
+ Extract a deterministic random square crop from the image.
+
+ Use a seed based on the image index for reproducibility while still
+ providing variable crop sizes and positions across the validation set.
+
+ Args:
+ img: Input image array of shape (H, W).
+ idx: Sample index used for deterministic seeding.
+
+ Returns:
+ Cropped image array.
+
+ """
+ h, w = img.shape
+
+ # Create a local random generator seeded by the index for reproducibility
+ rng = random.Random(idx)
+
+ # Determine crop size (must fit within image)
+ if self.fixed_crop_size is not None:
+ crop_size = min(self.fixed_crop_size, h, w)
+ else:
+ max_possible_size = min(h, w, self.max_crop_size)
+ min_size = min(self.min_crop_size, max_possible_size)
+ crop_size = rng.randint(min_size, max_possible_size)
+
+ # Deterministic random position based on index
+ top = rng.randint(0, h - crop_size)
+ left = rng.randint(0, w - crop_size)
+
+ return img[top : top + crop_size, left : left + crop_size]
+
+
+def collate_variable_size(
+ batch: List[Tuple[torch.Tensor, int]],
+) -> Tuple[torch.Tensor, torch.Tensor]:
+ """
+ Collate function for variable-size images.
+
+ Pad all images in the batch to the size of the largest image.
+
+ Args:
+ batch: List of (image, label) tuples.
+
+ Returns:
+ Tuple of (batched images, batched labels).
+
+ """
+ images, labels = zip(*batch)
+
+ # Find max dimensions
+ max_h = max(img.shape[1] for img in images)
+ max_w = max(img.shape[2] for img in images)
+
+ # Pad images to max size
+ padded_images = []
+ for img in images:
+ _, h, w = img.shape
+ pad_h = max_h - h
+ pad_w = max_w - w
+ # Pad on right and bottom
+ padded = torch.nn.functional.pad(img, (0, pad_w, 0, pad_h), value=0)
+ padded_images.append(padded)
+
+ return torch.stack(padded_images), torch.tensor(labels, dtype=torch.long)
+
+
+# ==============================================================================
+# Data Loading
+# ==============================================================================
+
+
+# Valid image extensions
+_IMAGE_EXTENSIONS = {".png", ".jpg", ".jpeg", ".bmp", ".tiff", ".tif", ".gif"}
+
+
+def load_dataset(
+ config: ValidationConfig,
+) -> Tuple[List[Path], List[int], Dict[str, int]]:
+ """
+ Load the dataset from disk.
+
+ Args:
+ config: Validation configuration.
+
+ Returns:
+ Tuple of (image_paths, labels, label_to_idx mapping).
+
+ """
+ labels_path = config.data_dir / config.labels_file
+
+ image_paths = []
+ labels = []
+ label_to_idx: Dict[str, int] = {}
+
+ with open(labels_path, "r") as f:
+ reader = csv.DictReader(f)
+ for row in reader:
+ filename = row["filename"]
+ label_str = row["label"]
+
+ # Skip non-image files
+ if not any(filename.lower().endswith(ext) for ext in _IMAGE_EXTENSIONS):
+ continue
+
+ # Build label mapping
+ if label_str not in label_to_idx:
+ label_to_idx[label_str] = len(label_to_idx)
+
+ # Find image in subdirectory
+ img_path = config.data_dir / label_str / filename
+ if img_path.exists():
+ image_paths.append(img_path)
+ labels.append(label_to_idx[label_str])
+
+ return image_paths, labels, label_to_idx
+
+
+def create_kfold_splits(
+ n_samples: int,
+ n_folds: int,
+ seed: int,
+) -> List[Tuple[List[int], List[int]]]:
+ """
+ Create k-fold cross-validation splits.
+
+ Args:
+ n_samples: Total number of samples.
+ n_folds: Number of folds.
+ seed: Random seed for reproducibility.
+
+ Returns:
+ List of (train_indices, val_indices) tuples for each fold.
+
+ """
+ rng = random.Random(seed)
+ indices = list(range(n_samples))
+ rng.shuffle(indices)
+
+ fold_size = n_samples // n_folds
+ splits = []
+
+ for fold in range(n_folds):
+ val_start = fold * fold_size
+ val_end = val_start + fold_size if fold < n_folds - 1 else n_samples
+
+ val_indices = indices[val_start:val_end]
+ train_indices = indices[:val_start] + indices[val_end:]
+ splits.append((train_indices, val_indices))
+
+ return splits
+
+
+# ==============================================================================
+# Training
+# ==============================================================================
+
+
+def train_one_epoch(
+ model: nn.Module,
+ dataloader: DataLoader,
+ criterion: nn.Module,
+ optimizer: torch.optim.Optimizer,
+ device: str,
+) -> Tuple[float, float]:
+ """
+ Train for one epoch.
+
+ Args:
+ model: The model to train.
+ dataloader: Training data loader.
+ criterion: Loss function.
+ optimizer: Optimizer.
+ device: Device to use.
+
+ Returns:
+ Tuple of (average loss, accuracy).
+
+ """
+ model.train()
+ total_loss = 0.0
+ correct = 0
+ total = 0
+
+ for images, labels in dataloader:
+ images = images.to(device)
+ labels = labels.to(device)
+
+ optimizer.zero_grad()
+ outputs = model(images, return_logits=True)
+ loss = criterion(outputs, labels)
+ loss.backward()
+ optimizer.step()
+
+ total_loss += loss.item() * images.size(0)
+ _, predicted = outputs.max(1)
+ correct += predicted.eq(labels).sum().item()
+ total += labels.size(0)
+
+ return total_loss / total, correct / total
+
+
+def validate(
+ model: nn.Module,
+ dataloader: DataLoader,
+ criterion: nn.Module,
+ device: str,
+) -> Tuple[float, float]:
+ """
+ Validate the model.
+
+ Args:
+ model: The model to validate.
+ dataloader: Validation data loader.
+ criterion: Loss function.
+ device: Device to use.
+
+ Returns:
+ Tuple of (average loss, accuracy).
+
+ """
+ model.eval()
+ total_loss = 0.0
+ correct = 0
+ total = 0
+
+ with torch.no_grad():
+ for images, labels in dataloader:
+ images = images.to(device)
+ labels = labels.to(device)
+
+ outputs = model(images, return_logits=True)
+ loss = criterion(outputs, labels)
+
+ total_loss += loss.item() * images.size(0)
+ _, predicted = outputs.max(1)
+ correct += predicted.eq(labels).sum().item()
+ total += labels.size(0)
+
+ return total_loss / total, correct / total
+
+
+def train_fold(
+ train_paths: List[Path],
+ train_labels: List[int],
+ val_paths: List[Path],
+ val_labels: List[int],
+ fold: int,
+ config: ValidationConfig,
+) -> FoldMetrics:
+ """
+ Train a single fold.
+
+ Args:
+ train_paths: Training image paths.
+ train_labels: Training labels.
+ val_paths: Validation image paths.
+ val_labels: Validation labels.
+ fold: Fold number.
+ config: Validation configuration.
+
+ Returns:
+ FoldMetrics containing training history.
+
+ """
+ # Create datasets
+ train_dataset = SEMDataset(
+ train_paths,
+ train_labels,
+ min_crop_size=config.min_crop_size,
+ max_crop_size=config.max_crop_size,
+ fixed_crop_size=config.fixed_crop_size,
+ augment=True,
+ expand_dataset=True, # Expand training set based on possible crops
+ max_samples_per_image=config.max_samples_per_image,
+ )
+ val_dataset = SEMDataset(
+ val_paths,
+ val_labels,
+ min_crop_size=config.min_crop_size,
+ max_crop_size=config.max_crop_size,
+ fixed_crop_size=config.fixed_crop_size,
+ augment=False, # No augmentation for validation - deterministic evaluation
+ expand_dataset=False, # No expansion for validation
+ )
+
+ # Log dataset sizes
+ print(
+ f" Dataset sizes - Train: {len(train_paths)} images -> "
+ f"{len(train_dataset)} samples ({train_dataset.samples_per_image}x expansion), "
+ f"Val: {len(val_paths)} images -> {len(val_dataset)} samples",
+ )
+ # Log image dimensions for first image
+ from PIL import Image as _PILImage
+
+ _first_img = _PILImage.open(train_paths[0])
+ _expected = (train_dataset.min_crop_size + train_dataset.max_crop_size) // 2
+ print(
+ f" Image dimensions: {_first_img.width}x{_first_img.height}px, "
+ f"Crop range: {train_dataset.min_crop_size}-{train_dataset.max_crop_size}px, "
+ f"Expected crop: {_expected}px",
+ )
+
+ # Create data loaders
+ train_loader = DataLoader(
+ train_dataset,
+ batch_size=config.batch_size,
+ shuffle=True,
+ collate_fn=collate_variable_size,
+ )
+ val_loader = DataLoader(
+ val_dataset,
+ batch_size=config.batch_size,
+ shuffle=False,
+ collate_fn=collate_variable_size,
+ )
+
+ # Create model
+ model = CNNClassifier(
+ num_classes=config.num_classes,
+ channels=config.channels,
+ base_filters=config.base_filters,
+ dropout=config.dropout,
+ ).to(config.device)
+
+ criterion = nn.CrossEntropyLoss()
+ optimizer = torch.optim.Adam(model.parameters(), lr=config.learning_rate)
+
+ # Training loop with early stopping
+ metrics = FoldMetrics(fold=fold)
+ patience_counter = 0
+ epoch = 0
+
+ while True:
+ # Check max_epochs limit
+ if config.max_epochs is not None and epoch >= config.max_epochs:
+ break
+
+ train_loss, train_acc = train_one_epoch(
+ model,
+ train_loader,
+ criterion,
+ optimizer,
+ config.device,
+ )
+ val_loss, val_acc = validate(model, val_loader, criterion, config.device)
+
+ metrics.train_losses.append(train_loss)
+ metrics.val_losses.append(val_loss)
+ metrics.train_accuracies.append(train_acc)
+ metrics.val_accuracies.append(val_acc)
+
+ # Check for improvement
+ improved = val_loss < metrics.best_val_loss
+ if improved:
+ metrics.best_val_loss = val_loss
+ metrics.best_epoch = epoch
+ patience_counter = 0
+ else:
+ patience_counter += 1
+
+ # Log epoch progress
+ max_epochs_str = str(config.max_epochs) if config.max_epochs else "∞"
+ status = (
+ "✓ improved"
+ if improved
+ else f"patience {patience_counter}/{config.patience}"
+ )
+ print(
+ f" Epoch {epoch + 1:3d}/{max_epochs_str} | "
+ f"Train Loss: {train_loss:.4f} | Train Acc: {train_acc * 100:5.1f}% | "
+ f"Val Loss: {val_loss:.4f} | Val Acc: {val_acc * 100:5.1f}% | {status}",
+ )
+
+ # Early stopping
+ if patience_counter >= config.patience:
+ metrics.stopped_early = True
+ break
+
+ epoch += 1
+
+ return metrics
+
+
+# ==============================================================================
+# Main Validation Loop
+# ==============================================================================
+
+
+def run_validation(
+ config: Optional[ValidationConfig] = None,
+) -> Tuple[List[PercentageMetrics], plt.Figure]:
+ """
+ Run the full validation experiment.
+
+ Args:
+ config: Validation configuration. Uses defaults if None.
+
+ Returns:
+ Tuple of (list of PercentageMetrics, matplotlib Figure).
+
+ """
+ if config is None:
+ config = ValidationConfig()
+
+ # Set seed for reproducibility
+ random.seed(config.seed)
+ np.random.seed(config.seed)
+ torch.manual_seed(config.seed)
+ if torch.cuda.is_available():
+ torch.cuda.manual_seed_all(config.seed)
+
+ # Load data
+ print("Loading dataset...")
+ image_paths, labels, label_to_idx = load_dataset(config)
+ print(f"Loaded {len(image_paths)} images with {len(label_to_idx)} classes")
+ print(f"Classes: {label_to_idx}")
+
+ # Create k-fold splits
+ kfold_splits = create_kfold_splits(len(image_paths), config.n_folds, config.seed)
+
+ all_metrics: List[PercentageMetrics] = []
+
+ for percentage in config.train_percentages:
+ print(f"\n{'=' * 60}")
+ print(f"Training with {percentage}% of data")
+ print(f"{'=' * 60}")
+
+ pct_metrics = PercentageMetrics(percentage=percentage)
+
+ for fold, (train_indices, val_indices) in enumerate(kfold_splits):
+ print(f"\n Fold {fold + 1}/{config.n_folds}")
+
+ # Subsample training data by percentage
+ n_train_samples = int(len(train_indices) * percentage / 100)
+ rng = random.Random(config.seed + fold)
+ sampled_train_indices = rng.sample(train_indices, n_train_samples)
+
+ # Get paths and labels for this fold
+ train_paths = [image_paths[i] for i in sampled_train_indices]
+ train_labels_fold = [labels[i] for i in sampled_train_indices]
+ val_paths = [image_paths[i] for i in val_indices]
+ val_labels_fold = [labels[i] for i in val_indices]
+
+ # Train this fold
+ fold_metrics = train_fold(
+ train_paths,
+ train_labels_fold,
+ val_paths,
+ val_labels_fold,
+ fold,
+ config,
+ )
+
+ pct_metrics.fold_metrics.append(fold_metrics)
+
+ print(
+ f" Best val loss: {fold_metrics.best_val_loss:.4f}",
+ f"at epoch {fold_metrics.best_epoch + 1}",
+ )
+ if fold_metrics.stopped_early:
+ print(
+ f" Early stopped after {len(fold_metrics.train_losses)} epochs",
+ )
+
+ all_metrics.append(pct_metrics)
+
+ print(f"\n {percentage}% Summary:")
+ print(
+ " Mean best val loss:",
+ f"{pct_metrics.mean_best_val_loss:.4f} ±",
+ f"{pct_metrics.std_best_val_loss:.4f}",
+ )
+ print(
+ " Mean val accuracy:",
+ f"{pct_metrics.mean_final_val_accuracy:.4f} ±"
+ f"{pct_metrics.std_final_val_accuracy:.4f}",
+ )
+
+ # Generate plots
+ fig = plot_results(all_metrics, config)
+
+ return all_metrics, fig
+
+
+# ==============================================================================
+# Plotting
+# ==============================================================================
+
+
+def plot_results(
+ all_metrics: List[PercentageMetrics],
+ config: ValidationConfig,
+) -> plt.Figure:
+ """
+ Generate visualization of the validation results.
+
+ Args:
+ all_metrics: List of PercentageMetrics from the validation run.
+ config: Validation configuration.
+
+ Returns:
+ Matplotlib Figure with the plots.
+
+ """
+ fig, axes = plt.subplots(2, 2, figsize=(14, 10))
+
+ percentages = [pm.percentage for pm in all_metrics]
+
+ # Plot 1: Mean loss (train & val) vs training percentage
+ ax1 = axes[0, 0]
+ # Validation loss (blue)
+ mean_val_losses = [pm.mean_best_val_loss for pm in all_metrics]
+ std_val_losses = [pm.std_best_val_loss for pm in all_metrics]
+ ax1.errorbar(
+ percentages,
+ mean_val_losses,
+ yerr=std_val_losses,
+ marker="o",
+ color="blue",
+ capsize=5,
+ capthick=2,
+ label="Validation",
+ )
+ # Training loss (red)
+ mean_train_losses = [pm.mean_best_train_loss for pm in all_metrics]
+ std_train_losses = [pm.std_best_train_loss for pm in all_metrics]
+ ax1.errorbar(
+ percentages,
+ mean_train_losses,
+ yerr=std_train_losses,
+ marker="s",
+ color="red",
+ capsize=5,
+ capthick=2,
+ label="Training",
+ )
+ ax1.set_xlabel("Training Data Percentage (%)")
+ ax1.set_ylabel("Loss (at best epoch)")
+ ax1.set_title("Loss vs Training Data Size")
+ ax1.legend()
+ ax1.grid(True, alpha=0.3)
+
+ # Plot 2: Mean accuracy (train & val) vs training percentage
+ ax2 = axes[0, 1]
+ # Validation accuracy (blue)
+ mean_val_accs = [pm.mean_final_val_accuracy * 100 for pm in all_metrics]
+ std_val_accs = [pm.std_final_val_accuracy * 100 for pm in all_metrics]
+ ax2.errorbar(
+ percentages,
+ mean_val_accs,
+ yerr=std_val_accs,
+ marker="o",
+ color="blue",
+ capsize=5,
+ capthick=2,
+ label="Validation",
+ )
+ # Training accuracy (red)
+ mean_train_accs = [pm.mean_final_train_accuracy * 100 for pm in all_metrics]
+ std_train_accs = [pm.std_final_train_accuracy * 100 for pm in all_metrics]
+ ax2.errorbar(
+ percentages,
+ mean_train_accs,
+ yerr=std_train_accs,
+ marker="s",
+ color="red",
+ capsize=5,
+ capthick=2,
+ label="Training",
+ )
+ ax2.set_xlabel("Training Data Percentage (%)")
+ ax2.set_ylabel("Accuracy (%)")
+ ax2.set_title("Accuracy vs Training Data Size")
+ ax2.legend()
+ ax2.grid(True, alpha=0.3)
+
+ # Plot 3: Loss curves for lowest percentage (train & val together)
+ ax3 = axes[1, 0]
+ pm_low = all_metrics[0] # Lowest percentage
+ max_epochs = max(len(fm.train_losses) for fm in pm_low.fold_metrics)
+ avg_train_losses = []
+ avg_val_losses = []
+ for epoch in range(max_epochs):
+ train_losses = [
+ fm.train_losses[epoch]
+ for fm in pm_low.fold_metrics
+ if epoch < len(fm.train_losses)
+ ]
+ val_losses = [
+ fm.val_losses[epoch]
+ for fm in pm_low.fold_metrics
+ if epoch < len(fm.val_losses)
+ ]
+ if train_losses:
+ avg_train_losses.append(np.mean(train_losses))
+ if val_losses:
+ avg_val_losses.append(np.mean(val_losses))
+ ax3.plot(
+ range(1, len(avg_train_losses) + 1),
+ avg_train_losses,
+ color="red",
+ label="Training",
+ )
+ ax3.plot(
+ range(1, len(avg_val_losses) + 1),
+ avg_val_losses,
+ color="blue",
+ label="Validation",
+ )
+ ax3.set_xlabel("Epoch")
+ ax3.set_ylabel("Loss")
+ ax3.set_title(f"Loss Curves ({pm_low.percentage}% Training Data)")
+ ax3.legend()
+ ax3.grid(True, alpha=0.3)
+
+ # Plot 4: Loss curves for highest percentage (train & val together)
+ ax4 = axes[1, 1]
+ pm_high = all_metrics[-1] # Highest percentage
+ max_epochs = max(len(fm.train_losses) for fm in pm_high.fold_metrics)
+ avg_train_losses = []
+ avg_val_losses = []
+ for epoch in range(max_epochs):
+ train_losses = [
+ fm.train_losses[epoch]
+ for fm in pm_high.fold_metrics
+ if epoch < len(fm.train_losses)
+ ]
+ val_losses = [
+ fm.val_losses[epoch]
+ for fm in pm_high.fold_metrics
+ if epoch < len(fm.val_losses)
+ ]
+ if train_losses:
+ avg_train_losses.append(np.mean(train_losses))
+ if val_losses:
+ avg_val_losses.append(np.mean(val_losses))
+ ax4.plot(
+ range(1, len(avg_train_losses) + 1),
+ avg_train_losses,
+ color="red",
+ label="Training",
+ )
+ ax4.plot(
+ range(1, len(avg_val_losses) + 1),
+ avg_val_losses,
+ color="blue",
+ label="Validation",
+ )
+ ax4.set_xlabel("Epoch")
+ ax4.set_ylabel("Loss")
+ ax4.set_title(f"Loss Curves ({pm_high.percentage}% Training Data)")
+ ax4.legend()
+ ax4.grid(True, alpha=0.3)
+
+ plt.tight_layout()
+ return fig
+
+
+# ==============================================================================
+# Entry Point
+# ==============================================================================
+
+
+def main(
+ data_dir: Optional[str] = None,
+ labels_file: str = "Labels.csv",
+ patience: int = 5,
+ n_folds: int = 5,
+ max_epochs: Optional[int] = None,
+ batch_size: int = 32,
+ learning_rate: float = 1e-3,
+ min_crop_size: int = 32,
+ max_crop_size: int = 128,
+ fixed_crop_size: Optional[int] = None,
+ max_samples_per_image: Optional[int] = None,
+ seed: int = 42,
+ save_path: Optional[str] = None,
+) -> plt.Figure:
+ """
+ Run validation and return the results graph.
+
+ Args:
+ data_dir: Path to the dataset directory. If None, uses default location.
+ labels_file: Name of the labels CSV file.
+ patience: Number of epochs without improvement before early stopping.
+ n_folds: Number of folds for cross-validation.
+ max_epochs: Maximum number of epochs per fold. If None, train until patience
+ runs out.
+ batch_size: Batch size for training.
+ learning_rate: Learning rate for optimizer.
+ min_crop_size: Minimum crop size for random cropping.
+ max_crop_size: Maximum crop size for random cropping.
+ fixed_crop_size: If set, use fixed crop size (more memory efficient).
+ max_samples_per_image: If set, cap the samples per image to this value.
+ seed: Random seed for reproducibility.
+ save_path: Optional path to save the figure.
+
+ Returns:
+ Matplotlib Figure with the validation results.
+
+ """
+ # Convert data_dir to Path if provided
+ if data_dir is not None:
+ data_dir_path = Path(data_dir)
+ else:
+ data_dir_path = _PROJECT_ROOT / "data/sem_images/raw"
+
+ config = ValidationConfig(
+ data_dir=data_dir_path,
+ labels_file=labels_file,
+ patience=patience,
+ n_folds=n_folds,
+ max_epochs=max_epochs,
+ batch_size=batch_size,
+ learning_rate=learning_rate,
+ min_crop_size=min_crop_size,
+ max_crop_size=max_crop_size,
+ fixed_crop_size=fixed_crop_size,
+ max_samples_per_image=max_samples_per_image,
+ seed=seed,
+ )
+
+ all_metrics, fig = run_validation(config)
+
+ if save_path:
+ fig.savefig(save_path, dpi=150, bbox_inches="tight")
+ print(f"\nFigure saved to {save_path}")
+
+ return fig
+
+
+if __name__ == "__main__":
+ figure = main()
+ plt.show()