Repository: JacksonBurns/fastprop (language: python)

Issue:
Next Pre-Release TODO
 - [x] Fix the `'squared' is deprecated in version 1.4 and will be removed in 1.6. To calculate the root mean squared error, use the function'root_mean_squared_error'.` warning
 - [x] Add the DOI to the printed citation request and a `CITATION.cff` to the repo


Discussion / hints:

Fix patch:
diff --git a/CITATION.cff b/CITATION.cff
new file mode 100644
index 0000000..dcf41e1
--- /dev/null
+++ b/CITATION.cff
@@ -0,0 +1,26 @@
+# This CITATION.cff file was generated with cffinit.
+# Visit https://bit.ly/cffinit to generate yours today!
+
+cff-version: 1.2.0
+title: fastprop
+message: >-
+  If you use this software in published work, please cite it
+  using the metadata in this file.
+type: software
+authors:
+  - given-names: Jackson
+    family-names: Burns
+    orcid: 'https://orcid.org/0000-0002-0657-9426'
+  - given-names: William
+    family-names: Green
+    orcid: 'https://orcid.org/0000-0003-2603-9694'
+identifiers:
+  - type: doi
+    value: 10.48550/arXiv.2404.02058
+  - type: url
+    value: 'https://arxiv.org/abs/2404.02058'
+repository-code: 'https://github.com/JacksonBurns/fastprop'
+abstract: >-
+  fast, accurate, scalable, and interpretable machine
+  learning with molecular descriptors
+license: MIT
diff --git a/README.md b/README.md
index 8400a86..aeda408 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,8 @@
 <p align="center">  
-  <img alt="fastproplogo" height="400" src="https://raw.githubusercontent.com/JacksonBurns/fastprop/main/fastprop_logo.png">
+  <img alt="fastprop Logo" height="400" src="https://raw.githubusercontent.com/JacksonBurns/fastprop/main/fastprop_logo.png">
 </p>
-<h2 align="center">Fast Molecular Property Prediction with <a href="https://github.com/JacksonBurns/mordred-community">mordredcommunity</a></h2>
+<h2 align="center">Molecular Property Prediction with <a href="https://github.com/JacksonBurns/mordred-community">mordredcommunity</a></h2>
+<h3 align="center">Fast, Scalable, and <500 LOC</h3>
  
 <p align="center">
   <img alt="GitHub Repo Stars" src="https://img.shields.io/github/stars/JacksonBurns/fastprop?style=social">
@@ -10,20 +11,22 @@
   <img alt="PyPI - License" src="https://img.shields.io/github/license/JacksonBurns/fastprop">
 </p>
 
-# Announcement - Open Beta!
-`fastprop` is currently in the version 4 open beta (1.0.0b4)!
+# Announcements
+## alphaXiv
+The `fastprop` paper is freely available online at [arxiv.org/abs/2404.02058](https://arxiv.org/abs/2404.02058) and we are conducting open source peer review on [alphaXiv](https://alphaxiv.org/abs/2404.02058) - comments are appreciated!
+The source for the paper is stored in this repository under the `paper` directory.
+
+## Initial Release :tada:
+`fastprop` version 1 is officially released, meaning the API is now stable and ready for production use!
 Please try `fastprop` on your datasets and let us know what you think.
 Feature requests and bug reports are **very** appreciated!
 
-Check out the demo notebook for quick intro to `fastprop` via Google Colab - runs in your browser, GPU included, no install required!
-
-<a target="_blank" href="https://colab.research.google.com/github/JacksonBurns/fastprop/blob/main/fastprop_demo.ipynb">
-  <img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/>
-</a>
-
 # Installing `fastprop`
 `fastprop` supports Mac, Windows, and Linux on Python versions 3.8 to 3.12.
-Installing from `pip` or `conda` is the best way to get `fastprop`, but if you need to check out a specific GitHub branch or you want to contribute to `fastprop` a source installation is recommended.
+Installing from `pip` is the best way to get `fastprop`, but if you need to check out a specific GitHub branch or you want to contribute to `fastprop` a source installation is recommended.
+Pending interest from users, a `conda` package will be added.
+
+Check out the demo notebook for quick intro to `fastprop` via Google Colab - runs in your browser, GPU included, no install required: [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/JacksonBurns/fastprop/blob/main/fastprop_demo.ipynb)
 
 ## `pip` [recommended]
 `fastprop` is available via PyPI with `pip install fastprop`.
@@ -32,12 +35,9 @@ To make extending `fastprop` easier and keep the installation size down, depende
 They can be installed with `pip install fastprop[hopt]`, `pip install fastprop[shap]`, or `pip install fastprop[shap,hopt]` to install them both.
 If you want to use `fastprop` but not write new code on top of it, you may want to install these now - you can always do so later, however, and `fastprop` will remind you.
 
-## `conda` - _coming soon!_
-~~`fastprop` is available from `conda-forge` with `conda install -c conda-forge fastprop`.~~
-
 ## Source
 To install `fastprop` from GitHub directly you can:
- 1. Run `pip install https://github.com/JacksonBurns/fastprop.git@main` to install from the `main` branch (or specify any other branch you like)
+ 1. Run `pip install https://github.com/JacksonBurns/fastprop.git@main` to install from the `main` branch (or specify any other branch you like).
  2. Clone the repository with `git clone https://github.com/JacksonBurns/fastprop.git`, navigate to `fastprop` with `cd fastprop`, and run `pip install .`
 
 To contribute to `fastprop` please follow [this tutorial](https://opensource.com/article/19/7/create-pull-request-github) (or something similar) to set up a forked version of `fastprop` and open a pull request (similar to above option 2).
@@ -49,15 +49,11 @@ See [Developing `fastprop`](#developing-fastprop) for more details.
 By passing in a list of SMILES strings, `fastprop` will automatically generate and cache a set of molecular descriptors using [`mordredcommunity`](https://github.com/JacksonBurns/mordred-community) and train an FNN to predict the corresponding properties.
 See the `examples` and `benchmarks` directories to see how to run training - the rest of this documentation will focus on how you can run, configure, and customize `fastprop`.
 
-## Paper
-An academic paper has been prepared which describes the `fastprop` approach and walks through the `benchmarks` in greater detail.
-See the `paper` directory for more information.
-
 ## `fastprop` Framework
 There are four distinct steps in `fastprop` that define its framework:
  1. Featurization - transform the input molecules (as SMILES strings) into an array of molecular descriptors which are saved
  2. Preprocessing - clean the descriptors by removing or imputing missing values then rescaling the remainder
- 3. Training - send the processed input to the neural network, which is a sample FNN (sequential fully-connected layers with an activation function between)
+ 3. Training - send the processed input to the neural network, which is a simple FNN (sequential fully-connected layers with an activation function between)
  4. Prediction - save the trained model for future use
 
 ## Configurable Parameters
@@ -68,15 +64,12 @@ There are four distinct steps in `fastprop` that define its framework:
 
     _and_
     - Which `mordred` descriptors to calculate: 'all' or 'optimized' (a smaller set of descriptors; faster, but less accurate).
-    - Enable/Disable caching of calculated descriptors: `fastprop` will by default cache calculated descriptors based on the input filename and warn the user when it loads descriptors from the file rather than calculating on the fly
+    - Enable/Disable caching of calculated descriptors: `fastprop` will by default cache calculated descriptors based on the input filename and warn the user when it loads descriptors from the file rather than calculating on-the-fly
 
     _or_
     - Load precomputed descriptors: filepath to where descriptors are already cached either manually or by `fastprop`
  2. Preprocessing
-    - Enable/Disable dropping of zero-variance parameters (disabled by default; faster, but often less accurate)
-
-    ~~- Enable/Disable dropping of co-linear descriptors (disabled by default; faster, decreased accuracy)~~ _WIP_
-    - _not configurable_: `fastprop` will always rescale input features, drop columns with no values, and impute missing values with the per-feature mean
+    - _not configurable_: `fastprop` will always rescale input features, set invariant and missing features to zero, and impute missing values with the per-feature mean
  3. Training
     - Number of Repeats: How many times to split/train/test on the dataset (increments random seed by 1 each time).
 
@@ -91,25 +84,14 @@ There are four distinct steps in `fastprop` that define its framework:
     - Output Directory
     - Learning rate
     - Batch size
-
-    ~~- Checkpoint file to resume from (optional)~~ _WIP_
     - Problem type (one of: regression, binary, multiclass (start labels from 0), multilabel)
  4. Prediction
-    - Input SMILES: either a single SMILES or a CSV file
+    - Input SMILES: either a single SMILES or file of SMILES strings on individual lines
     - Output format: filepath to write the results or nothing, defaults to stdout
-    - Checkpoints directory: directory where previously trained model(s) are
-
-## Relationship to Chemprop
-In addition to having a similar name, `fastprop` and Chemprop do a similar things: map chemical structures to their corresponding properties in a user-friendly way using machine learning.
-I ([@JacksonBurns](https://github.com/jacksonburns)) am also a developer of Chemprop so some code is inevitably shared between the two (`fastprop`->Chemprop and vice versa).
-
-`fastprop` _feels_ a lot like Chemprop but without a lot of the clutter.
-The `fast` in `fastprop` (both in usage and execution time) comes from the basic architecture, the use of caching, and the reduced configurability of `fastprop` (i.e. I hope you like MSE loss for regression tasks, because that's the only training metric `fastprop` will use).
 
 # Using `fastprop`
 `fastprop` can be run from the command line or as a Python module.
 Regardless of the method of use the parameters described in [Configurable Parameters](#configurable-parameters) can be modified.
-Some system-specific configuration options can be specified in a `.fastpropconfig` file - see the [example file](https://github.com/JacksonBurns/fastprop/blob/main/.fastpropconfig).
 
 ## Command Line
 After installation, `fastprop` is accessible from the command line via `fastprop subcommand`, where `subcommand` is either `train`, `predict`, or `shap`.
@@ -131,28 +113,63 @@ See `fastprop train --help` or `fastprop predict --help` for more information.
 `fastprop shap` and `fastprop predict` have only a couple arguments and so do not use configuration files.
 
 ## Python Module
+
+### Example
+Here's an example of training `fastprop` as a Python module on the `Arockiaraj` Polycyclic Aromatic Hydrocarbon dataset, pulled largely from `fastprop/cli/train.py`.
+With `fastprop` installed you can copy and run this script as-is!
+
+```python
+import pandas as pd
+import torch
+
+from fastprop.data import (
+    clean_dataset,
+    fastpropDataLoader,
+    fastpropDataset,
+    split,
+    standard_scale,
+)
+from fastprop.defaults import DESCRIPTOR_SET_LOOKUP, _init_loggers, init_logger
+from fastprop.descriptors import get_descriptors
+from fastprop.io import load_saved_descriptors, read_input_csv
+from fastprop.model import fastprop, train_and_test
+
+# prepare the dataset
+targets, smiles = read_input_csv("https://raw.githubusercontent.com/JacksonBurns/fastprop/main/benchmarks/pah/arockiaraj_pah_data.csv")
+targets, rdkit_mols = clean_dataset(targets, smiles)
+descriptors = get_descriptors(".", DESCRIPTOR_SET_LOOKUP["all"], rdkit_mols)
+descriptors = descriptors.to_numpy(dtype=float)
+descriptors = torch.tensor(descriptors, dtype=torch.float32)
+targets = torch.tensor(targets, dtype=torch.float32)
+# feature scaling
+train_indexes, val_indexes, test_indexes = split(smiles)
+descriptors[train_indexes], feature_means, feature_vars = standard_scale(descriptors[train_indexes])
+descriptors[val_indexes] = standard_scale(descriptors[val_indexes], feature_means, feature_vars)
+descriptors[test_indexes] = standard_scale(descriptors[test_indexes], feature_means, feature_vars)
+
+# initialize dataloaders and model, then train
+train_dataloader = fastpropDataLoader(fastpropDataset(descriptors[train_indexes], targets[train_indexes]), shuffle=True)
+val_dataloader = fastpropDataLoader(fastpropDataset(descriptors[val_indexes], targets[val_indexes]))
+test_dataloader = fastpropDataLoader(fastpropDataset(descriptors[test_indexes], targets[test_indexes]))
+model = fastprop(feature_means, feature_vars)
+test_results, validation_results = train_and_test(".", model, train_dataloader, val_dataloader, test_dataloader)
+
+```
+
+### Package Structure
 This section documents where the various modules and functions used in `fastprop` are located.
 Check each file listed for more information, as each contains additional inline documentation useful for development as a Python module.
 To use the core `fastprop` model and dataloaders in your own work, consider looking at `shap.py` or `train.py` which show how to import and instantiate the relevant classes.
 
-### `fastprop`
+#### `fastprop`
  - `defaults`: contains the function `init_logger` used to initialize loggers in different submodules, as well as the default configuration for training.
- - `fastprop_core`: the model itself and convenience functions.
- - `hopt`: hyperparameter optimization using Optuna and Ray\[tune\], used by the CLI.
- - `train`: performs model training, used by the CLI.
- - `predict`: loads models from their checkpoint and config files and runs inference, used by the CLI.
- - `shap`: performs SHAP analysis on a previously trained model, used by the CLI.
- - `preprocessing`: performs all of the preprocessing described above.
-
-### `fastprop.utils`
- - `calculate_descriptors`: wraps the `mordredcommunity` descriptor calculator.
- - `linear_baseline`: trains a basic linear model on the same inputs to `fastprop` to act as a baseline.
- - `descriptor_lists`: hardcoded lists of all of the descriptors implemented in `mordredcommunity`.
- - `select_descriptors`: the script to retrieve the `mordredcommunity` modules based on the strings in the above file (`mordredcommunity` has a weird interface; thus, it is wrapped).
- - `load_data`: short wrappers to `pandas` CSV loading utility, but specialized for the output from `mordredcommunity` and `fastprop`.
- - `validate_config`: _WIP_ validate the input from the command line.
-
-### `fastprop.cli`
+ - `model`: the model itself and a convenience function for training.
+ - `metrics`: wraps a number of common loss and score functions.
+ - `descriptors`: functions for calculating descriptors.
+ - `data`: functions for cleaning and scaling data.
+ - `io`: functions for loading data from files.
+
+#### `fastprop.cli`
 `fastprop_cli`` contains all the CLI code which is likely not useful in use from a script.
 If you wish to extend the CLI, check the inline documentation there.
 
@@ -160,6 +177,13 @@ If you wish to extend the CLI, check the inline documentation there.
 The `benchmarks` directory contains the scripts needed to perform the studies (see `benchmarks/README.md` for more detail, they are a great way to learn how to use `fastprop`).
 To just see the results, checkout [`paper/paper.pdf`](https://github.com/JacksonBurns/fastprop/blob/main/paper/paper.pdf) (or `paper/paper.md` for the plain text version).
 
+# Relationship to Chemprop
+In addition to having a similar name, `fastprop` and Chemprop do a similar things: map chemical structures to their corresponding properties in a user-friendly way using machine learning.
+I ([@JacksonBurns](https://github.com/jacksonburns)) am also a developer of Chemprop so some code is inevitably shared between the two (`fastprop` to Chemprop and vice versa).
+
+`fastprop` _feels_ a lot like Chemprop but without a lot of the clutter.
+The `fast` in `fastprop` (both in usage and execution time) comes from the basic architecture, the use of caching, and the reduced configurability of `fastprop` (i.e. I hope you like MSE loss for regression tasks, because that's the only training metric `fastprop` will use via the CLI).
+
 # Developing `fastprop`
 Bug reports, feature requests, and pull requests are welcome and encouraged!
 Follow [this tutorial from GitHub](https://docs.github.com/en/get-started/exploring-projects-on-github/contributing-to-a-project) to get started.
diff --git a/benchmarks/h2s/parse.py b/benchmarks/h2s/parse.py
index ce6033c..6db490e 100644
--- a/benchmarks/h2s/parse.py
+++ b/benchmarks/h2s/parse.py
@@ -1,12 +1,15 @@
-import pandas as pd
-import numpy as np
-
-from fastprop.utils import calculate_mordred_desciptors, ALL_2D, mordred_descriptors_from_strings
+import itertools
 
-from rdkit import Chem
+import numpy as np
+import pandas as pd
 from py2opsin import py2opsin
+from rdkit import Chem
 
-import itertools
+from fastprop.utils import (
+    ALL_2D,
+    calculate_mordred_desciptors,
+    mordred_descriptors_from_strings,
+)
 
 study_data = pd.read_excel("1-s2.0-S0167732217350985-mmc1.xlsx")
 
diff --git a/benchmarks/hiv/hiv.yml b/benchmarks/hiv/hiv.yml
index 0cff52b..6e7a8f0 100644
--- a/benchmarks/hiv/hiv.yml
+++ b/benchmarks/hiv/hiv.yml
@@ -33,7 +33,7 @@ random_seed: 765309408
 # featurization
 input_file: hiv/benchmark_data.csv
 smiles_column: smiles
-descriptors: all
+descriptor_set: all
 precomputed: hiv/precomputed.csv
 
 # training
diff --git a/benchmarks/pah/pah.yml b/benchmarks/pah/pah.yml
index 920b972..055e370 100644
--- a/benchmarks/pah/pah.yml
+++ b/benchmarks/pah/pah.yml
@@ -11,26 +11,20 @@
 
 # generic args
 output_directory: pah
-random_seed: 55
+random_seed: 562
 problem_type: regression
 
 # featurization
 input_file: pah/arockiaraj_pah_data.csv
 target_columns: log_p
 smiles_column: smiles
-descriptors: all
-
-# preprocessing
-zero_variance_drop: False
-colinear_drop: False
+descriptor_set: all
 
 # training
-hidden_size: 2900
-fnn_layers: 5
-number_repeats: 8
-number_epochs: 100
+number_repeats: 3
+number_epochs: 20
 batch_size: 64
-patience: 10
+patience: 5
 train_size: 0.8
 val_size: 0.1
 test_size: 0.1
diff --git a/benchmarks/quantumscents/quantumscents.py b/benchmarks/quantumscents/quantumscents.py
index 922280b..6a70aba 100644
--- a/benchmarks/quantumscents/quantumscents.py
+++ b/benchmarks/quantumscents/quantumscents.py
@@ -15,17 +15,22 @@
 import os
 import pickle as pkl
 
-import psutil
-from fastprop.utils import mordred_descriptors_from_strings, calculate_mordred_desciptors, load_saved_desc
-from fastprop.fastprop_core import train_and_test, fastprop
-from fastprop.preprocessing import preprocess
-from rdkit import Chem
-from rdkit.Chem import rdDetermineBonds
 import pandas as pd
+import psutil
 
 # the QuantumScents Zenodo includes a number of utilities for loading the dataset
 # two of which are imported here.
-from dataset.loader import QS_DATA, CID_TO_SMILES
+from dataset.loader import CID_TO_SMILES, QS_DATA
+from rdkit import Chem
+from rdkit.Chem import rdDetermineBonds
+
+from fastprop.fastprop_core import fastprop, train_and_test
+from fastprop.preprocessing import preprocess
+from fastprop.utils import (
+    calculate_mordred_desciptors,
+    load_saved_desc,
+    mordred_descriptors_from_strings,
+)
 
 # this file is a list of list of list - the outer list is len=number of folds, then the sublist is len=3
 # i.e. train/val/test and then then innermost lists contain the indexes for train/val/test
diff --git a/benchmarks/quantumscents/quantumscents.yml b/benchmarks/quantumscents/quantumscents.yml
index bc3a2e2..7cc0476 100644
--- a/benchmarks/quantumscents/quantumscents.yml
+++ b/benchmarks/quantumscents/quantumscents.yml
@@ -26,7 +26,7 @@ smiles_column: smiles
 target_columns: alcoholic aldehydic alliaceous almond animal anisic apple apricot aromatic balsamic banana beefy berry black_currant brandy bread brothy burnt buttery cabbage camphoreous caramellic catty chamomile cheesy cherry chicken chocolate cinnamon citrus cocoa coconut coffee cognac coumarinic creamy cucumber dairy dry earthy ethereal fatty fermented fishy floral fresh fruity garlic gasoline grape grapefruit grassy green hay hazelnut herbal honey horseradish jasmine ketonic leafy leathery lemon malty meaty medicinal melon metallic milky mint mushroom musk musty nutty odorless oily onion orange orris peach pear phenolic pine pineapple plum popcorn potato pungent radish ripe roasted rose rum savory sharp smoky solvent sour spicy strawberry sulfurous sweet tea tobacco tomato tropical vanilla vegetable violet warm waxy winey woody
 
 # featurization
-descriptors: all
+descriptor_set: all
 
 # training
 number_repeats: 2
diff --git a/examples/example_fastprop_train_config.yml b/examples/example_fastprop_train_config.yml
index 1f3cd66..081d56e 100644
--- a/examples/example_fastprop_train_config.yml
+++ b/examples/example_fastprop_train_config.yml
@@ -25,7 +25,7 @@ optimize: False # True
 # Featurization
 #
 # Which set of descriptors to calculate (either all or optimized)
-descriptors: all
+descriptor_set: all
 # Allow caching of descriptors
 enable_cache: True
 #
@@ -34,15 +34,6 @@ enable_cache: True
 # Provide your own descriptors or those calculated by mordredcommunity from its CLI
 precomputed: /path/to/precomputed/descriptors.csv
 
-# Preprocessing
-#
-# Rescale descriptors from their initial range to 0->1
-rescaling: True
-# Drop descriptors which are invariant across the entire dataset
-zero_variance_drop: False
-# Drop descriptors which are colinear
-colinear_drop: False
-
 # Training
 #
 # Number of layers in the FNN
@@ -59,10 +50,7 @@ number_epochs: 1000
 train_size: 0.8
 val_size: 0.05
 test_size: 0.15
-# Checkpoint from a previous run from which to restart (optional)
-checkpoint: /path/to/checkpoint.ckpt
-# Algorithm for data sampling, i.e. random, see all available samplers
-# here: https://github.com/JacksonBurns/astartes?tab=readme-ov-file#implemented-sampling-algorithms
+# Algorithm for data sampling, either scaffold or random
 sampler: random
 # number of epochs to wait before stopping early - avoid overfitting
 patience: 5
diff --git a/examples/oom_training.py b/examples/oom_training.py
index 978275b..f8d79a4 100644
--- a/examples/oom_training.py
+++ b/examples/oom_training.py
@@ -1,6 +1,8 @@
 """
 oom_training.py
 
+This was created with fastprop beta version 4, but could easily be adapted to later versions.
+
 This script demonstrates how to run fastprop on a dataset which is too large to fit in memory
 (GPU or main memory) and instead must be dynamically loaded during training, i.e. "Out-of-memory"
 (OOM) training.
diff --git a/fastprop/.fastpropconfig_template b/fastprop/.fastpropconfig_template
deleted file mode 100644
index 6d079a4..0000000
--- a/fastprop/.fastpropconfig_template
+++ /dev/null
@@ -1,12 +0,0 @@
-# .fastpropconfig_template - template file for fastprop configuration
-#
-# copy this file in the same directory you found it and name it .fastpropconfig_template
-# i.e. drop the _template part and fastprop will read it when running
-#
-# number of models to run concurrently during hyperparameter optimization
-# by default set to 4, which will allow each model to use 20% of the GPU
-# try and set this as high as possible without causing a crash
-models_per_gpu: 4
-# total number of optimization trials - generally the default of 64 is
-# enough to improve the model; more will be time-consuming
-num_hopt_trials: 64
diff --git a/fastprop/__init__.py b/fastprop/__init__.py
index e13df6e..54734b3 100644
--- a/fastprop/__init__.py
+++ b/fastprop/__init__.py
@@ -1,8 +1,3 @@
 from .defaults import DEFAULT_TRAINING_CONFIG
-from .hopt import hopt_fastprop
-from .predict import predict_fastprop
-from .preprocessing import preprocess
-from .shap import shap_fastprop
-from .train import train_fastprop
 
-__all__ = ["DEFAULT_TRAINING_CONFIG", "hopt_fastprop", "predict_fastprop", "preprocess", "shap_fastprop", "train_fastprop"]
+__all__ = ["DEFAULT_TRAINING_CONFIG"]
diff --git a/fastprop/cli/__init__.py b/fastprop/cli/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/fastprop/cli/fastprop_cli.py b/fastprop/cli/base.py
similarity index 73%
rename from fastprop/cli/fastprop_cli.py
rename to fastprop/cli/base.py
index eb638eb..ad9c6b2 100644
--- a/fastprop/cli/fastprop_cli.py
+++ b/fastprop/cli/base.py
@@ -6,15 +6,12 @@
 
 import yaml
 
-from fastprop import (
-    DEFAULT_TRAINING_CONFIG,
-    hopt_fastprop,
-    predict_fastprop,
-    shap_fastprop,
-    train_fastprop,
-)
+from fastprop import DEFAULT_TRAINING_CONFIG
 from fastprop.defaults import init_logger
-from fastprop.utils import validate_config
+
+from .predict import predict_fastprop
+from .shap import shap_fastprop
+from .train import train_fastprop
 
 logger = init_logger(__name__)
 
@@ -34,15 +31,10 @@ def main():
     train_subparser.add_argument("-if", "--input-file", help="csv of SMILES and targets")
     train_subparser.add_argument("-tc", "--target-columns", nargs="+", help="column name(s) for target(s)")
     train_subparser.add_argument("-sc", "--smiles-column", help="column name for SMILES")
-    train_subparser.add_argument("-d", "--descriptors", help="descriptors to calculate (one of all, optimized, smallest, or search)")
+    train_subparser.add_argument("-d", "--descriptors", help="descriptors to calculate (one of all, optimized, or debug)")
     train_subparser.add_argument("-ec", "--enable-cache", type=bool, help="allow saving and loading of cached descriptors")
     train_subparser.add_argument("-p", "--precomputed", help="precomputed descriptors from fastprop or mordred")
 
-    # preprocessing
-    train_subparser.add_argument("-r", "--rescaling", type=bool, help="rescale descriptors between 0 and 1 (default to True)")
-    train_subparser.add_argument("-zvd", "--zero-variance-drop", type=bool, help="drop zero variance descriptors (defaults to True)")
-    train_subparser.add_argument("-cd", "--colinear-drop", type=bool, help="drop colinear descriptors (defaults to False)")
-
     # training
     train_subparser.add_argument("-op", "--optimize", action="store_true", help="run hyperparameter optimization", default=False)
     train_subparser.add_argument("-fl", "--fnn-layers", type=int, help="number of fnn layers")
@@ -58,22 +50,28 @@ def main():
     train_subparser.add_argument("-rs", "--random-seed", type=int, help="random seed for sampling and pytorch seed")
     train_subparser.add_argument("-pc", "--patience", type=int, help="number of epochs to wait before early stopping")
 
+    # inference
     predict_subparser = subparsers.add_parser("predict")
     predict_subparser.add_argument("checkpoints_dir", help="directory of checkpoint file(s) for predictions")
     input_group = predict_subparser.add_mutually_exclusive_group()
-    input_group.add_argument("-s", "--smiles", nargs="+", help="SMILES string for prediction")
-    input_group.add_argument("-i", "--input-file", help="file containing SMILES strings")
+    input_group.add_argument("-ss", "--smiles-strings", nargs="+", type=str, help="SMILES string(s) for prediction")
+    input_group.add_argument("-sf", "--smiles-file", help="file containing SMILES strings only")
+    input_group = predict_subparser.add_mutually_exclusive_group(required=True)
+    input_group.add_argument("-ds", "--descriptor-set", help="descriptors to calculate (one of all, optimized, or debug)")
+    input_group.add_argument("-pd", "--precomputed-descriptors", help="precomputed descriptors")
     predict_subparser.add_argument("-o", "--output", required=False, help="output file for predictions (defaults to stdout)")
 
+    # feature importance
     shap_subparser = subparsers.add_parser("shap")
     shap_subparser.add_argument("checkpoints_dir", help="directory of checkpoint file(s) for SHAP analysis")
-    shap_subparser.add_argument("input_file", help="csv of SMILES and targets used during training")
+    shap_subparser.add_argument("cached_descriptors", help="csv of calculated descriptors cached by fastprop")
+    shap_subparser.add_argument("descriptor_set", help="descriptors in the cache file (one of all, optimized, or debug)")
     shap_subparser.add_argument(
         "-it",
         "--importance-threshold",
         default=0.75,
         type=float,
-        help="[0-1] hide features below (most important feature) x this",
+        help="[0-1] include top fraction of features, default 0.75",
     )
 
     # no args provided - print the help text
@@ -92,10 +90,6 @@ def main():
     args.pop("version")
     if subcommand == "train":
         training_default = dict(DEFAULT_TRAINING_CONFIG)
-        # exit with help if no args given
-        if not sum(map(lambda i: i is not None, args.values())):
-            train_subparser.print_help()
-            exit(0)
         optim_requested = args.pop("optimize")
         if args["config_file"] is not None:
             if sum(map(lambda i: i is not None, args.values())) > 1:
@@ -109,27 +103,22 @@ def main():
 
         optim_requested = training_default.pop("optimize") or optim_requested
         logger.info(f"Training Parameters:\n{yaml.dump(training_default, sort_keys=False)}")
-        # validate this dictionary, i.e. layer counts are positive, etc.
-        # cannot specify both precomputed and descriptors or enable/cache
-        validate_config(training_default)
         if optim_requested:
-            training_default.pop("fnn_layers")
-            training_default.pop("hidden_size")
-            if any((args.get("fnn_layers") is not None, args.get("hidden_size") is not None)):
-                logger.warning("Hidden Size/FNN Layers specified with optimize and are ignored.")
-            hopt_fastprop(**training_default)
+            if args.get("fnn_layers", None) is not None:
+                logger.warning("--fnn-layers specified with --optimize - ignored.")
+            if args.get("hidden_size", None) is not None:
+                logger.warning("--hidden-size specified with --optimize - ignored.")
+            train_fastprop(**training_default, hopt=True)
         else:
             train_fastprop(**training_default)
     elif subcommand == "shap":
         shap_fastprop(**args)
     elif subcommand == "predict":
-        if args["smiles"] is None and args["input_file"] is None:
-            raise parser.error("One of -i/--input-file or -s/--smiles must be provided.")
         logger.info(f"Predict Parameters:\n {yaml.dump(args, sort_keys=False)}")
         predict_fastprop(**args)
     else:
         logger.critical(f"Unrecognized subcommand '{subcommand}', printing help and exiting.")
         parser.print_help()
         sys.exit(0)
-    logger.info("If you use fastprop in published work, please cite: ...WIP...")
+    logger.info("If you use fastprop in published work, please cite https://arxiv.org/abs/2404.02058")
     logger.info("Total elapsed time: " + str(datetime.timedelta(seconds=perf_counter() - cli_start)))
diff --git a/fastprop/cli/predict.py b/fastprop/cli/predict.py
new file mode 100644
index 0000000..d416d51
--- /dev/null
+++ b/fastprop/cli/predict.py
@@ -0,0 +1,67 @@
+import os
+from typing import List, Optional
+
+import numpy as np
+import pandas as pd
+import torch
+from pytorch_lightning import Trainer
+from torch.utils.data import TensorDataset
+
+from fastprop.data import clean_dataset, fastpropDataLoader
+from fastprop.defaults import DESCRIPTOR_SET_LOOKUP, init_logger
+from fastprop.descriptors import get_descriptors
+from fastprop.io import load_saved_descriptors
+from fastprop.model import fastprop
+
+logger = init_logger(__name__)
+
+
+def predict_fastprop(
+    checkpoints_dir: str,
+    smiles_strings: List[str],
+    descriptor_set: str,
+    smiles_file: Optional[str] = None,
+    precomputed_descriptors: Optional[np.ndarray] = None,
+    output: Optional[str] = None,
+):
+    if smiles_file is not None:
+        if smiles_strings:
+            raise RuntimeError("Specify either smiles_strings or smiles_file, not both.")
+        smiles_strings = [s.strip() for s in open(smiles_file, "r").readlines()]
+
+    # load the models
+    if precomputed_descriptors is None:
+        _, rdkit_mols = clean_dataset(np.zeros((1, len(smiles_strings))), np.array(smiles_strings))
+        descs = get_descriptors(cache_filepath=False, descriptors=DESCRIPTOR_SET_LOOKUP[descriptor_set], rdkit_mols=rdkit_mols)
+        descs = descs.to_numpy(dtype=float)
+    else:
+        descs = load_saved_descriptors(precomputed_descriptors)
+
+    all_models = []
+    for checkpoint in os.listdir(checkpoints_dir):
+        model = fastprop.load_from_checkpoint(os.path.join(checkpoints_dir, checkpoint))
+        all_models.append(model)
+
+    descs = torch.tensor(descs, dtype=torch.float32)
+    predict_dataloader = fastpropDataLoader(TensorDataset(descs))
+    # run inference
+    # axis: contents
+    # 0: smiles
+    # 1: predictions
+    # 2: per-model
+    trainer = Trainer(logger=False)
+    all_predictions = np.stack([torch.vstack(trainer.predict(model, predict_dataloader)).numpy(force=True) for model in all_models], axis=2)
+    perf = np.mean(all_predictions, axis=2)
+    err = np.std(all_predictions, axis=2)
+    # interleave the columns of these arrays, thanks stackoverflow.com/a/75519265
+    res = np.empty((len(perf), perf.shape[1] * 2), dtype=perf.dtype)
+    res[:, 0::2] = perf
+    res[:, 1::2] = err
+    column_names = []
+    for target in [f"task_{i}" for i in range(all_predictions.shape[1])]:
+        column_names.extend([target, target + "_stdev"])
+    out = pd.DataFrame(res, columns=column_names, index=smiles_strings)
+    if output is None:
+        print("\n", out)
+    else:
+        out.to_csv(output)
diff --git a/fastprop/shap.py b/fastprop/cli/shap.py
similarity index 54%
rename from fastprop/shap.py
rename to fastprop/cli/shap.py
index 47683c1..b08133a 100644
--- a/fastprop/shap.py
+++ b/fastprop/cli/shap.py
@@ -1,16 +1,14 @@
 import os
-import pickle
 
 import matplotlib.pyplot as plt
 import numpy as np
 import torch
-import yaml
 from tqdm import tqdm
 
-from fastprop.defaults import init_logger
-from fastprop.fastprop_core import fastprop
-from fastprop.utils import calculate_mordred_desciptors, load_from_csv
-from fastprop.utils.select_descriptors import mordred_descriptors_from_strings
+from fastprop.data import standard_scale
+from fastprop.defaults import DESCRIPTOR_SET_LOOKUP, init_logger
+from fastprop.io import load_saved_descriptors
+from fastprop.model import fastprop
 
 shap = None
 try:
@@ -22,54 +20,36 @@
 logger = init_logger(__name__)
 
 
-def shap_fastprop(checkpoints_dir, input_file, importance_threshold=0.75):
+def shap_fastprop(
+    checkpoints_dir: str,
+    cached_descriptors: str,
+    descriptor_set: str,
+    importance_threshold: float = 0.75,
+):
     if shap is None:
         raise RuntimeError("Unable to import shap dependencies, please install fastprop[shap]. Original error: " + str(shape_error))
-    # get the configuration file with some run metadata
-    checkpoint_dir_contents = os.listdir(checkpoints_dir)
-    with open(os.path.join(checkpoints_dir, "fastprop_config.yml")) as file:
-        config_dict = yaml.safe_load(file)
+    # load the models
+    all_models = []
+    for checkpoint in os.listdir(checkpoints_dir):
+        model = fastprop.load_from_checkpoint(os.path.join(checkpoints_dir, checkpoint))
+        all_models.append(model)
 
-    # calculate the features in the same way the model did when training,
     # downsampling to the shap suggested size where required
     # (also see https://github.com/shap/shap/issues/955 for an explanation of
     # Why the thresholds are the way they are)
-    targets, mols, smiles = load_from_csv(input_file, config_dict["smiles"], config_dict["targets"])
-    num_mols = len(mols)
+    descs = load_saved_descriptors(cached_descriptors)
+    num_mols = descs.shape[0]
     threshold_1 = 200
     threshold_2 = 2000
     if num_mols > threshold_1:
         rng = np.random.default_rng(42)
         target = threshold_1 if num_mols < threshold_2 else threshold_2
         logger.info(f"Randomly downsampling to {target} molecules.")
-        mols = rng.choice(mols, size=target, replace=False)
-    descs = calculate_mordred_desciptors(
-        mordred_descriptors_from_strings(config_dict["descriptors"]),
-        mols,
-        n_procs=0,  # ignored for "strategy='low-memory'"
-        strategy="low-memory",
-    )
+        idxs = rng.choice(num_mols, size=target, replace=False)
+        descs = descs[idxs]
 
-    # load each of the models in the group
-    all_models = []
-    for checkpoint in checkpoint_dir_contents:
-        if not checkpoint.endswith(".ckpt"):
-            continue
-        model = fastprop.load_from_checkpoint(
-            os.path.join(checkpoints_dir, checkpoint),
-            cleaned_data=None,
-            targets=None,
-            smiles=None,
-        )
-        try:
-            with open(os.path.join(checkpoints_dir, f"repetition_{checkpoint.split('-')[1]}_scalers.yml")) as file:
-                scalers = yaml.safe_load(file)
-        except FileNotFoundError:
-            logger.error(f"checkpoints directory is missing 'repetition_{checkpoint.split('-')[1]}_scalers.yml'. Re-execute training.")
-        model.target_scaler = pickle.loads(scalers["target_scaler"])
-        model.mean_imputer = pickle.loads(scalers["mean_imputer"])
-        model.feature_scaler = pickle.loads(scalers["feature_scaler"])
-        all_models.append(model)
+    descs = torch.tensor(descs, dtype=torch.float32, device=all_models[0].device)
+    X = standard_scale(descs, all_models[0].feature_means, all_models[0].feature_vars)
 
     # we will use half of the data for 'integrating', and the other half for getting shap values
     halfway_idx = descs.shape[0] // 2
@@ -81,7 +61,6 @@ def shap_fastprop(checkpoints_dir, input_file, importance_threshold=0.75):
     per_model_shap = []
     for model in tqdm(all_models, desc="Calculating SHAP values for each model"):
         # now scale and send the data to whatever device lightning put the model on
-        X = torch.tensor(model.feature_scaler.transform(model.mean_imputer.transform(descs)), dtype=torch.float32).to(model.device)
         e = shap.DeepExplainer(model, X[:halfway_idx])
         model_shap_values = e.shap_values(X[halfway_idx:])
         # returns a list for multi-target problems, cast for uniformity
@@ -89,14 +68,13 @@ def shap_fastprop(checkpoints_dir, input_file, importance_threshold=0.75):
             model_shap_values = [model_shap_values]
         per_model_shap.append(model_shap_values)
     all_shap_values = np.mean(np.array(per_model_shap), axis=0)
-    # TODO: write these to a file, sorted by highest absolute SHAP on first target.
 
     # for each target, create a plot of the most important features
-    for i, target_name in enumerate(config_dict["targets"]):
+    for i, target_name in enumerate(f"task_{i}" for i in range(all_models[0].readout.out_features)):
         shap_values = all_shap_values[i]
         # include features until the shap value is half the highest, aka half as important
         avg_shaps = np.mean(np.abs(shap_values), axis=0)
-        avg_shaps, names, keep_idxs = zip(*sorted(zip(avg_shaps, config_dict["descriptors"], list(range(len(avg_shaps)))), reverse=True))
+        avg_shaps, names, keep_idxs = zip(*sorted(zip(avg_shaps, DESCRIPTOR_SET_LOOKUP[descriptor_set], list(range(len(avg_shaps)))), reverse=True))
         include_idx = 0
         for val in avg_shaps[1:]:
             if val > avg_shaps[0] * importance_threshold:
@@ -105,7 +83,7 @@ def shap_fastprop(checkpoints_dir, input_file, importance_threshold=0.75):
                 break
         explanation = shap.Explanation(values=shap_values[:, keep_idxs[:include_idx]], feature_names=names[0:include_idx])
         plt.cla()
-        axes = shap.plots.beeswarm(explanation, max_display=include_idx + 1, color_bar=False, color="shap_red", show=False)
+        _ = shap.plots.beeswarm(explanation, max_display=include_idx + 1, color_bar=False, color="shap_red", show=False)
         out_fname = target_name + "_feature_importance_beeswarm.png"
         if os.path.exists(out_fname):
             logger.warning(f"Output file exists! {__name__} will overwrite '{out_fname}'.")
diff --git a/fastprop/cli/train.py b/fastprop/cli/train.py
new file mode 100644
index 0000000..e7d775f
--- /dev/null
+++ b/fastprop/cli/train.py
@@ -0,0 +1,348 @@
+import datetime
+import logging
+import os
+from pathlib import Path
+from typing import Dict, List
+
+import pandas as pd
+import torch
+from lightning.pytorch import seed_everything
+from scipy.stats import ttest_ind
+
+from fastprop.data import (
+    clean_dataset,
+    fastpropDataLoader,
+    fastpropDataset,
+    split,
+    standard_scale,
+)
+from fastprop.defaults import DESCRIPTOR_SET_LOOKUP, _init_loggers, init_logger
+from fastprop.descriptors import get_descriptors
+from fastprop.io import load_saved_descriptors, read_input_csv
+from fastprop.model import fastprop, train_and_test
+
+logger = init_logger(__name__)
+
+
+tune, OptunaSearch = None, None
+try:
+    import ray
+    from ray import tune
+    from ray.train.torch import enable_reproducibility
+    from ray.tune.search.optuna import OptunaSearch
+except ImportError as ie:
+    hopt_error = ie
+
+logger = init_logger(__name__)
+
+
+MODELS_PER_GPU, NUM_HOPT_TRIALS = 4, 16
+
+
+def train_fastprop(
+    output_directory: str,
+    input_file: str,
+    smiles_column: str,
+    target_columns: List[str],
+    descriptor_set: str,
+    enable_cache: bool,
+    precomputed: str,
+    fnn_layers: int,
+    hidden_size: int,
+    learning_rate: float,
+    batch_size: int,
+    number_epochs: int,
+    number_repeats: int,
+    problem_type: str,
+    train_size: float,
+    val_size: float,
+    test_size: float,
+    sampler: str,
+    random_seed: int,
+    patience: int,
+    hopt: bool = False,
+):
+    if hopt and (tune is None or OptunaSearch is None):
+        raise RuntimeError(
+            "Unable to import hyperparameter optimization dependencies, please install fastprop[hopt].\nOriginal error: " + str(hopt_error)
+        )
+    # setup logging and output directories
+    output_subdirectory = os.path.join(output_directory, f"fastprop_{int(datetime.datetime.utcnow().timestamp())}")
+    os.makedirs(output_directory, exist_ok=True)
+    os.makedirs(output_subdirectory, exist_ok=True)
+    os.makedirs(os.path.join(output_subdirectory, "checkpoints"), exist_ok=True)
+    _init_loggers(output_subdirectory)
+    logging.getLogger("pytorch_lightning").setLevel(logging.INFO)
+    seed_everything(random_seed)
+
+    # load the data
+    targets, smiles = read_input_csv(input_file, smiles_column, target_columns)
+    if precomputed:
+        descriptors = load_saved_descriptors(precomputed)
+    else:
+        cache_file = os.path.join(
+            output_directory, "cached_" + Path(input_file).stem + "_" + descriptor_set + "_" + str(int(os.stat(input_file).st_ctime)) + ".csv"
+        )
+        if os.path.exists(cache_file) and enable_cache:
+            logger.info(f"Found cached descriptor data at {cache_file}, loading instead of recalculating.")
+            descriptors = load_saved_descriptors(cache_file)
+        else:
+            targets, rdkit_mols = clean_dataset(targets, smiles)
+            descriptors = get_descriptors(enable_cache and cache_file, DESCRIPTOR_SET_LOOKUP[descriptor_set], rdkit_mols)
+            descriptors = descriptors.to_numpy(dtype=float)
+
+    descriptors = torch.tensor(descriptors, dtype=torch.float32)
+    targets = torch.tensor(targets, dtype=torch.float32)
+
+    if problem_type == "multiclass" and not (targets.size(dim=1) > 1):
+        logger.info("One-hot encoding targets.")
+        targets = torch.nn.functional.one_hot(targets.long()).squeeze()
+
+    input_size = descriptors.shape[1]
+    readout_size = targets.shape[1]
+    n_tasks = 1 if problem_type in {"binary", "multiclass"} else readout_size
+
+    logger.info(f"Run 'tensorboard --logdir {os.path.join(output_directory, 'tensorboard_logs')}' to track training progress.")
+    if not hopt:
+        return _replicates(
+            number_repeats,
+            smiles,
+            train_size,
+            val_size,
+            test_size,
+            descriptors,
+            targets,
+            problem_type,
+            random_seed,
+            sampler,
+            input_size,
+            hidden_size,
+            readout_size,
+            n_tasks,
+            learning_rate,
+            fnn_layers,
+            batch_size,
+            number_epochs,
+            patience,
+            target_columns,
+            output_subdirectory,
+        )
+    else:
+        # driver code of optimization
+        search_space = {
+            "hidden_size": tune.choice(range(100, 3001, 100)),
+            "fnn_layers": tune.choice(range(1, 5, 1)),
+        }
+        algo = OptunaSearch()
+        smiles_ref = ray.put(smiles)
+        targets_ref = ray.put(targets)
+        descriptors_ref = ray.put(descriptors)
+        metric = fastprop.get_metric(problem_type)
+        tuner = tune.Tuner(
+            tune.with_resources(
+                lambda trial: _hopt_objective(
+                    trial,
+                    smiles_ref,
+                    targets_ref,
+                    descriptors_ref,
+                    number_repeats,
+                    train_size,
+                    val_size,
+                    test_size,
+                    problem_type,
+                    random_seed,
+                    sampler,
+                    input_size,
+                    hidden_size,
+                    readout_size,
+                    n_tasks,
+                    learning_rate,
+                    fnn_layers,
+                    batch_size,
+                    number_epochs,
+                    patience,
+                    target_columns,
+                    output_subdirectory,
+                ),
+                # run n_parallel models at the same time (leave 20% for system)
+                # don't specify cpus, and just let pl figure it out
+                resources={"gpu": (1 - 0.20) / MODELS_PER_GPU},
+            ),
+            tune_config=tune.TuneConfig(
+                metric=metric,
+                mode="min",
+                search_alg=algo,
+                max_concurrent_trials=MODELS_PER_GPU,
+                num_samples=NUM_HOPT_TRIALS,
+            ),
+            param_space=search_space,
+        )
+        results = tuner.fit()
+        best = results.get_best_result().config
+        logger.info(f"Best hyperparameters identified: {', '.join([key + ': ' + str(val) for key, val in best.items()])}")
+        return best
+
+
+def _replicates(
+    number_repeats,
+    smiles,
+    train_size,
+    val_size,
+    test_size,
+    descriptors,
+    targets,
+    problem_type,
+    random_seed,
+    sampler,
+    input_size,
+    hidden_size,
+    readout_size,
+    n_tasks,
+    learning_rate,
+    fnn_layers,
+    batch_size,
+    number_epochs,
+    patience,
+    target_columns,
+    output_subdirectory,
+):
+    all_test_results, all_validation_results = [], []
+    for replicate_number in range(number_repeats):
+        logger.info(f"Training model {replicate_number+1} of {number_repeats} ({random_seed=})")
+
+        # prepare the dataloaders
+        train_indexes, val_indexes, test_indexes = split(smiles, random_seed, train_size, val_size, test_size, sampler)
+        descriptors[train_indexes], feature_means, feature_vars = standard_scale(descriptors[train_indexes])
+        descriptors[val_indexes] = standard_scale(descriptors[val_indexes], feature_means, feature_vars)
+        descriptors[test_indexes] = standard_scale(descriptors[test_indexes], feature_means, feature_vars)
+
+        if problem_type == "regression":
+            targets[train_indexes], target_means, target_vars = standard_scale(targets[train_indexes, :])
+            targets[val_indexes] = standard_scale(targets[val_indexes, :], target_means, target_vars)
+            targets[test_indexes] = standard_scale(targets[test_indexes, :], target_means, target_vars)
+        else:
+            target_means = None
+            target_vars = None
+
+        train_dataloader = fastpropDataLoader(
+            fastpropDataset(descriptors[train_indexes], targets[train_indexes]),
+            shuffle=True,
+            batch_size=batch_size,
+        )
+        val_dataloader = fastpropDataLoader(
+            fastpropDataset(descriptors[val_indexes], targets[val_indexes]),
+            batch_size=batch_size,
+        )
+        test_dataloader = fastpropDataLoader(
+            fastpropDataset(descriptors[test_indexes], targets[test_indexes]),
+            batch_size=batch_size,
+        )
+
+        # initialize the model and train/test
+        model = fastprop(
+            input_size,
+            hidden_size,
+            readout_size,
+            n_tasks,
+            learning_rate,
+            fnn_layers,
+            problem_type,
+            target_columns,
+            feature_means,
+            feature_vars,
+            target_means,
+            target_vars,
+        )
+        logger.info("Model architecture:\n{%s}", str(model))
+        test_results, validation_results = train_and_test(
+            output_subdirectory,
+            model,
+            train_dataloader,
+            val_dataloader,
+            test_dataloader,
+            number_epochs,
+            patience,
+        )
+        all_test_results.append(test_results[0])
+        all_validation_results.append(validation_results[0])
+
+        random_seed += 1
+        # ensure that the model is re-instantiated
+        del model
+
+    validation_results_df = pd.DataFrame.from_records(all_validation_results)
+    logger.info("Displaying validation results:\n%s", validation_results_df.describe().transpose().to_string())
+    test_results_df = pd.DataFrame.from_records(all_test_results)
+    logger.info("Displaying testing results:\n%s", test_results_df.describe().transpose().to_string())
+
+    if number_repeats > 1:
+        metric = fastprop.get_metric(problem_type)
+        ttest_result = ttest_ind(
+            test_results_df[f"test_{metric}_loss"].to_numpy(),
+            validation_results_df[f"validation_{metric}_loss"].to_numpy(),
+        )
+        if (p := ttest_result.pvalue) < 0.05:
+            logger.warn(
+                "Detected possible over/underfitting! 2-sided T-test between validation and testing"
+                f" {metric} yielded {p=:.3f}<0.05. Consider changing patience."
+            )
+        else:
+            logger.info(f"2-sided T-test between validation and testing {metric} yielded p value of {p=:.3f}>0.05.")
+    else:
+        logger.info("fastprop is unable to generate statistics to check for overfitting, consider increasing 'num_repeats' to at least 2.")
+    return test_results_df
+
+
+def _hopt_objective(
+    trial,
+    smiles_ref,
+    targets_ref,
+    descriptors_ref,
+    number_repeats,
+    train_size,
+    val_size,
+    test_size,
+    problem_type,
+    random_seed,
+    sampler,
+    input_size,
+    hidden_size,
+    readout_size,
+    n_tasks,
+    learning_rate,
+    fnn_layers,
+    batch_size,
+    number_epochs,
+    patience,
+    target_columns,
+    output_subdirectory,
+) -> Dict[str, float]:
+    descriptors = ray.get(descriptors_ref)
+    targets = ray.get(targets_ref)
+    smiles = ray.get(smiles_ref)
+    enable_reproducibility(random_seed)
+    test_results_df = _replicates(
+        number_repeats,
+        smiles,
+        train_size,
+        val_size,
+        test_size,
+        descriptors,
+        targets,
+        problem_type,
+        random_seed,
+        sampler,
+        input_size,
+        trial["hidden_size"],
+        readout_size,
+        n_tasks,
+        learning_rate,
+        trial["fnn_layers"],
+        batch_size,
+        number_epochs,
+        patience,
+        target_columns,
+        output_subdirectory,
+    )
+    metric = fastprop.get_metric(problem_type)
+    return {metric: test_results_df.describe().at["mean", f"test_{metric}_loss"]}
diff --git a/fastprop/data.py b/fastprop/data.py
new file mode 100644
index 0000000..b33da09
--- /dev/null
+++ b/fastprop/data.py
@@ -0,0 +1,197 @@
+from typing import List, Literal, Optional, Tuple
+
+import numpy as np
+import torch
+from astartes import train_val_test_split
+from astartes.molecules import train_val_test_split_molecules
+from rdkit import Chem
+from torch.utils.data import DataLoader as TorchDataloader
+from torch.utils.data import Dataset as TorchDataset
+
+from fastprop.defaults import init_logger
+
+logger = init_logger(__name__)
+
+
+def split(
+    smiles: List[str],
+    random_seed: int = 42,
+    train_size: float = 0.8,
+    val_size: float = 0.1,
+    test_size: float = 0.1,
+    sampler: Literal["random", "scaffold"] = "random",
+) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
+    """Split a dataset into training, validation, and testing subsets and return the indices for each.
+
+    Args:
+        smiles (list[str]): SMILES strings to split.
+        random_seed (int, optional): Seed for splitting. Defaults to 42.
+        train_size (float, optional): Fraction of data for training. Defaults to 0.8.
+        val_size (float, optional): Fraction of data for validation. Defaults to 0.1.
+        test_size (float, optional): Fraction of data for testing. Defaults to 0.1.
+        sampler (Literal["random", "scaffold"], optional): Type of sampler from astartes. Defaults to "random".
+
+    Raises:
+        TypeError: Unsupported sampler requested
+
+    Returns:
+        tuple[np.ndarray, np.ndarray, np.ndarray]: Indices for training, validation, and testing.
+    """
+    split_kwargs = dict(
+        train_size=train_size,
+        val_size=val_size,
+        test_size=test_size,
+        sampler=sampler,
+        random_state=random_seed,
+        return_indices=True,
+    )
+    if sampler == "random":
+        (
+            *_,
+            train_idxs,
+            val_idxs,
+            test_idxs,
+        ) = train_val_test_split(np.arange(len(smiles)), **split_kwargs)
+    elif sampler == "scaffold":
+        (
+            *_,
+            train_idxs,
+            val_idxs,
+            test_idxs,
+        ) = train_val_test_split_molecules(smiles, **split_kwargs)
+    else:
+        raise TypeError(f"Unknown sampler {sampler=}.")
+    return train_idxs, val_idxs, test_idxs
+
+
+def standard_scale(data: torch.Tensor, means: Optional[torch.Tensor] = None, variances: Optional[torch.Tensor] = None):
+    """Applies standard scaling, i.e. Z-Score normalization.
+
+    Args:
+        data (torch.Tensor): Data to be scaled.
+        means (torch.Tensor, optional): Precomputed means. Defaults to None and are calculated from data.
+        variances (torch.Tensor, optional): Precomputed means. Defaults to None and are calculated from data.
+
+    Returns:
+        torch.Tensor: scaled data and its means and variances if they were not provided in call.
+    """
+    return_stats = False
+    if means is None:
+        return_stats = True
+        means = data.nanmean(dim=0)
+    # treat missing features' means as zero
+    torch.nan_to_num_(means)
+
+    # set missing rows with their column's average value
+    data = data.where(~data.isnan(), means)
+    if variances is None:
+        variances = data.var(dim=0)
+
+    # center around zero and apply unit variance
+    data = (data - means) / variances.sqrt()
+
+    # the above will result in features with no variance going to nan,
+    # -inf, or inf (if they were zero or slightly below/above it) so
+    # we reset them to zero
+    torch.nan_to_num_(data, posinf=0.0, neginf=0.0)
+    if return_stats:
+        return data, means, variances
+    else:
+        return data
+
+
+def inverse_standard_scale(data: torch.Tensor, means: torch.Tensor, variances: torch.Tensor) -> torch.Tensor:
+    """Undo standard scaling.
+
+    Args:
+        data (torch.Tensor): Input data.
+        means (torch.Tensor): Precomputed means which were used for scaling.
+        variances (torch.Tensor): Precomputed variances which were used for scaling.
+
+    Returns:
+        torch.Tensor: Unscaled data.
+    """
+    return data * variances.sqrt() + means
+
+
+def clean_dataset(targets: np.ndarray, smiles: np.ndarray):
+    """Removes targets with missing values and SMILES which cannot be converted to molecules.
+
+    Args:
+        targets (np.ndarray): Targets corresponding to mols.
+        smiles (np.ndarray): SMILES corresponding to the targets.
+
+    Returns:
+        tuple[np.ndarray, np.ndarray]: Valid targets and RDKit Molecules.
+    """
+    rdkit_mols = np.array(list(Chem.MolFromSmiles(i) for i in smiles))
+    starting_length = len(rdkit_mols)
+
+    # remove dataset entries where the molecule could not be built
+    error_mols_idxs = np.where(rdkit_mols == None)[0]  # noqa: E711
+    for idx in error_mols_idxs:
+        logger.warn(
+            f"Unable to create RDKit molecule from SMILES string {smiles[idx]} (index {idx})."
+            " Both the molecule and the target will be removed from the data."
+        )
+    if len(error_mols_idxs) > 0:
+        # specify axis=0 to avoid changing dimensions (flattening)
+        logger.warn(f"Removed {len(error_mols_idxs)} entries from the dataset ({100*len(error_mols_idxs)/starting_length:.2f}% of the data).")
+        targets = np.delete(targets, error_mols_idxs, axis=0)
+        rdkit_mols = np.delete(rdkit_mols, error_mols_idxs, axis=0)
+        smiles = np.delete(smiles, error_mols_idxs)
+
+    # also remove dataset entries where the target is missing
+    # TODO: weight masking instead of removal?
+    error_target_idxs = [idx for idx, arr in enumerate(targets) if any(np.isnan(arr))]
+    for idx in error_target_idxs:
+        logger.warn(
+            f"Missing target value (target={targets[idx]}) for SMILES {smiles[idx]}. Both the molecule and the target will be removed from the data."
+        )
+    if len(error_target_idxs) > 0:
+        # specify axis=0 to avoid changing dimensions (flattening)
+        logger.warn(f"Removed {len(error_target_idxs)} entries from the dataset ({100*len(error_target_idxs)/starting_length:.2f}% of the data).")
+        targets = np.delete(targets, error_target_idxs, axis=0)
+        rdkit_mols = np.delete(rdkit_mols, error_target_idxs, axis=0)
+        smiles = np.delete(smiles, error_target_idxs)
+    return targets, rdkit_mols
+
+
+# wrap the basic pytorch Dataset and Dataloader to set some arguments in a convenient way
+
+
+class fastpropDataset(TorchDataset):
+    def __init__(
+        self,
+        features: torch.Tensor,
+        targets: torch.Tensor,
+    ):
+        self.features = features
+        self.length = len(targets)
+        self.targets = targets
+
+    def __len__(self):
+        return self.length
+
+    def __getitem__(self, index):
+        return self.features[index], self.targets[index]
+
+
+class fastpropDataLoader(TorchDataloader):
+    def __init__(
+        self,
+        dataset: fastpropDataset,
+        batch_size: int = 128,
+        num_workers: int = 1,
+        persistent_workers: bool = True,
+        shuffle=False,
+        **torch_kwargs,
+    ):
+        super().__init__(
+            dataset,
+            batch_size,
+            shuffle=shuffle,
+            persistent_workers=persistent_workers,
+            num_workers=num_workers,
+            **torch_kwargs,
+        )
diff --git a/fastprop/defaults.py b/fastprop/defaults.py
index d2d0c04..1b97f32 100644
--- a/fastprop/defaults.py
+++ b/fastprop/defaults.py
@@ -1,7 +1,10 @@
 import logging
 import os
+import warnings
 from types import MappingProxyType
 
+warnings.filterwarnings(action="ignore", message=".*does not have many workers.*")
+
 _logging_format = dict(
     format="[%(asctime)s %(name)s] %(levelname)s: %(message)s",
     datefmt="%m/%d/%Y %I:%M:%S %p",
@@ -9,14 +12,27 @@
 )
 
 
-def _init_loggers(outpath):
+def _init_loggers(outpath: str):
+    """Sets up a duplicate logging output to a text file in the given directory.
+
+    Args:
+        outpath (str): Destination directory for log file.
+    """
     logging.basicConfig(
         **_logging_format,
         handlers=[logging.FileHandler(os.path.join(outpath, "fastprop_log.txt")), logging.StreamHandler()],
     )
 
 
-def init_logger(module_name):
+def init_logger(module_name: str) -> logging.Logger:
+    """Returns a logger instance at appropriate level given with the given name.
+
+    Args:
+        module_name (str): Module name.
+
+    Returns:
+        logging.logger: Logger instance.
+    """
     logger = logging.getLogger(module_name)
     logger.setLevel(logging.DEBUG)
     return logger
@@ -25,25 +41,2610 @@ def init_logger(module_name):
 # immutable default settings
 DEFAULT_TRAINING_CONFIG = MappingProxyType(
     dict(
-        descriptors="optimized",
+        descriptor_set="all",
         enable_cache=True,
         precomputed=None,
-        zero_variance_drop=False,
-        colinear_drop=False,
         fnn_layers=2,
         learning_rate=0.0001,
         batch_size=2048,
-        number_epochs=100,
+        number_epochs=30,
         number_repeats=1,
         problem_type="regression",
-        checkpoint=None,
         train_size=0.8,
         val_size=0.1,
         test_size=0.1,
         sampler="random",
-        random_seed=0,
+        random_seed=42,
         hidden_size=1800,
-        optimize=False,
+        hopt=False,
         patience=5,
     )
 )
+
+
+# Complete list of 2D descriptors implemented in Mordred as well as a subset
+# of those descriptors which are often useful for fastprop after preprocessing
+
+ALL_2D = [
+    "ABC",
+    "ABCGG",
+    "nAcid",
+    "nBase",
+    "SpAbs_A",
+    "SpMax_A",
+    "SpDiam_A",
+    "SpAD_A",
+    "SpMAD_A",
+    "LogEE_A",
+    "VE1_A",
+    "VE2_A",
+    "VE3_A",
+    "VR1_A",
+    "VR2_A",
+    "VR3_A",
+    "nAromAtom",
+    "nAromBond",
+    "nAtom",
+    "nHeavyAtom",
+    "nSpiro",
+    "nBridgehead",
+    "nHetero",
+    "nH",
+    "nB",
+    "nC",
+    "nN",
+    "nO",
+    "nS",
+    "nP",
+    "nF",
+    "nCl",
+    "nBr",
+    "nI",
+    "nX",
+    "ATS0dv",
+    "ATS1dv",
+    "ATS2dv",
+    "ATS3dv",
+    "ATS4dv",
+    "ATS5dv",
+    "ATS6dv",
+    "ATS7dv",
+    "ATS8dv",
+    "ATS0d",
+    "ATS1d",
+    "ATS2d",
+    "ATS3d",
+    "ATS4d",
+    "ATS5d",
+    "ATS6d",
+    "ATS7d",
+    "ATS8d",
+    "ATS0s",
+    "ATS1s",
+    "ATS2s",
+    "ATS3s",
+    "ATS4s",
+    "ATS5s",
+    "ATS6s",
+    "ATS7s",
+    "ATS8s",
+    "ATS0Z",
+    "ATS1Z",
+    "ATS2Z",
+    "ATS3Z",
+    "ATS4Z",
+    "ATS5Z",
+    "ATS6Z",
+    "ATS7Z",
+    "ATS8Z",
+    "ATS0m",
+    "ATS1m",
+    "ATS2m",
+    "ATS3m",
+    "ATS4m",
+    "ATS5m",
+    "ATS6m",
+    "ATS7m",
+    "ATS8m",
+    "ATS0v",
+    "ATS1v",
+    "ATS2v",
+    "ATS3v",
+    "ATS4v",
+    "ATS5v",
+    "ATS6v",
+    "ATS7v",
+    "ATS8v",
+    "ATS0se",
+    "ATS1se",
+    "ATS2se",
+    "ATS3se",
+    "ATS4se",
+    "ATS5se",
+    "ATS6se",
+    "ATS7se",
+    "ATS8se",
+    "ATS0pe",
+    "ATS1pe",
+    "ATS2pe",
+    "ATS3pe",
+    "ATS4pe",
+    "ATS5pe",
+    "ATS6pe",
+    "ATS7pe",
+    "ATS8pe",
+    "ATS0are",
+    "ATS1are",
+    "ATS2are",
+    "ATS3are",
+    "ATS4are",
+    "ATS5are",
+    "ATS6are",
+    "ATS7are",
+    "ATS8are",
+    "ATS0p",
+    "ATS1p",
+    "ATS2p",
+    "ATS3p",
+    "ATS4p",
+    "ATS5p",
+    "ATS6p",
+    "ATS7p",
+    "ATS8p",
+    "ATS0i",
+    "ATS1i",
+    "ATS2i",
+    "ATS3i",
+    "ATS4i",
+    "ATS5i",
+    "ATS6i",
+    "ATS7i",
+    "ATS8i",
+    "AATS0dv",
+    "AATS1dv",
+    "AATS2dv",
+    "AATS3dv",
+    "AATS4dv",
+    "AATS5dv",
+    "AATS6dv",
+    "AATS7dv",
+    "AATS8dv",
+    "AATS0d",
+    "AATS1d",
+    "AATS2d",
+    "AATS3d",
+    "AATS4d",
+    "AATS5d",
+    "AATS6d",
+    "AATS7d",
+    "AATS8d",
+    "AATS0s",
+    "AATS1s",
+    "AATS2s",
+    "AATS3s",
+    "AATS4s",
+    "AATS5s",
+    "AATS6s",
+    "AATS7s",
+    "AATS8s",
+    "AATS0Z",
+    "AATS1Z",
+    "AATS2Z",
+    "AATS3Z",
+    "AATS4Z",
+    "AATS5Z",
+    "AATS6Z",
+    "AATS7Z",
+    "AATS8Z",
+    "AATS0m",
+    "AATS1m",
+    "AATS2m",
+    "AATS3m",
+    "AATS4m",
+    "AATS5m",
+    "AATS6m",
+    "AATS7m",
+    "AATS8m",
+    "AATS0v",
+    "AATS1v",
+    "AATS2v",
+    "AATS3v",
+    "AATS4v",
+    "AATS5v",
+    "AATS6v",
+    "AATS7v",
+    "AATS8v",
+    "AATS0se",
+    "AATS1se",
+    "AATS2se",
+    "AATS3se",
+    "AATS4se",
+    "AATS5se",
+    "AATS6se",
+    "AATS7se",
+    "AATS8se",
+    "AATS0pe",
+    "AATS1pe",
+    "AATS2pe",
+    "AATS3pe",
+    "AATS4pe",
+    "AATS5pe",
+    "AATS6pe",
+    "AATS7pe",
+    "AATS8pe",
+    "AATS0are",
+    "AATS1are",
+    "AATS2are",
+    "AATS3are",
+    "AATS4are",
+    "AATS5are",
+    "AATS6are",
+    "AATS7are",
+    "AATS8are",
+    "AATS0p",
+    "AATS1p",
+    "AATS2p",
+    "AATS3p",
+    "AATS4p",
+    "AATS5p",
+    "AATS6p",
+    "AATS7p",
+    "AATS8p",
+    "AATS0i",
+    "AATS1i",
+    "AATS2i",
+    "AATS3i",
+    "AATS4i",
+    "AATS5i",
+    "AATS6i",
+    "AATS7i",
+    "AATS8i",
+    "ATSC0c",
+    "ATSC1c",
+    "ATSC2c",
+    "ATSC3c",
+    "ATSC4c",
+    "ATSC5c",
+    "ATSC6c",
+    "ATSC7c",
+    "ATSC8c",
+    "ATSC0dv",
+    "ATSC1dv",
+    "ATSC2dv",
+    "ATSC3dv",
+    "ATSC4dv",
+    "ATSC5dv",
+    "ATSC6dv",
+    "ATSC7dv",
+    "ATSC8dv",
+    "ATSC0d",
+    "ATSC1d",
+    "ATSC2d",
+    "ATSC3d",
+    "ATSC4d",
+    "ATSC5d",
+    "ATSC6d",
+    "ATSC7d",
+    "ATSC8d",
+    "ATSC0s",
+    "ATSC1s",
+    "ATSC2s",
+    "ATSC3s",
+    "ATSC4s",
+    "ATSC5s",
+    "ATSC6s",
+    "ATSC7s",
+    "ATSC8s",
+    "ATSC0Z",
+    "ATSC1Z",
+    "ATSC2Z",
+    "ATSC3Z",
+    "ATSC4Z",
+    "ATSC5Z",
+    "ATSC6Z",
+    "ATSC7Z",
+    "ATSC8Z",
+    "ATSC0m",
+    "ATSC1m",
+    "ATSC2m",
+    "ATSC3m",
+    "ATSC4m",
+    "ATSC5m",
+    "ATSC6m",
+    "ATSC7m",
+    "ATSC8m",
+    "ATSC0v",
+    "ATSC1v",
+    "ATSC2v",
+    "ATSC3v",
+    "ATSC4v",
+    "ATSC5v",
+    "ATSC6v",
+    "ATSC7v",
+    "ATSC8v",
+    "ATSC0se",
+    "ATSC1se",
+    "ATSC2se",
+    "ATSC3se",
+    "ATSC4se",
+    "ATSC5se",
+    "ATSC6se",
+    "ATSC7se",
+    "ATSC8se",
+    "ATSC0pe",
+    "ATSC1pe",
+    "ATSC2pe",
+    "ATSC3pe",
+    "ATSC4pe",
+    "ATSC5pe",
+    "ATSC6pe",
+    "ATSC7pe",
+    "ATSC8pe",
+    "ATSC0are",
+    "ATSC1are",
+    "ATSC2are",
+    "ATSC3are",
+    "ATSC4are",
+    "ATSC5are",
+    "ATSC6are",
+    "ATSC7are",
+    "ATSC8are",
+    "ATSC0p",
+    "ATSC1p",
+    "ATSC2p",
+    "ATSC3p",
+    "ATSC4p",
+    "ATSC5p",
+    "ATSC6p",
+    "ATSC7p",
+    "ATSC8p",
+    "ATSC0i",
+    "ATSC1i",
+    "ATSC2i",
+    "ATSC3i",
+    "ATSC4i",
+    "ATSC5i",
+    "ATSC6i",
+    "ATSC7i",
+    "ATSC8i",
+    "AATSC0c",
+    "AATSC1c",
+    "AATSC2c",
+    "AATSC3c",
+    "AATSC4c",
+    "AATSC5c",
+    "AATSC6c",
+    "AATSC7c",
+    "AATSC8c",
+    "AATSC0dv",
+    "AATSC1dv",
+    "AATSC2dv",
+    "AATSC3dv",
+    "AATSC4dv",
+    "AATSC5dv",
+    "AATSC6dv",
+    "AATSC7dv",
+    "AATSC8dv",
+    "AATSC0d",
+    "AATSC1d",
+    "AATSC2d",
+    "AATSC3d",
+    "AATSC4d",
+    "AATSC5d",
+    "AATSC6d",
+    "AATSC7d",
+    "AATSC8d",
+    "AATSC0s",
+    "AATSC1s",
+    "AATSC2s",
+    "AATSC3s",
+    "AATSC4s",
+    "AATSC5s",
+    "AATSC6s",
+    "AATSC7s",
+    "AATSC8s",
+    "AATSC0Z",
+    "AATSC1Z",
+    "AATSC2Z",
+    "AATSC3Z",
+    "AATSC4Z",
+    "AATSC5Z",
+    "AATSC6Z",
+    "AATSC7Z",
+    "AATSC8Z",
+    "AATSC0m",
+    "AATSC1m",
+    "AATSC2m",
+    "AATSC3m",
+    "AATSC4m",
+    "AATSC5m",
+    "AATSC6m",
+    "AATSC7m",
+    "AATSC8m",
+    "AATSC0v",
+    "AATSC1v",
+    "AATSC2v",
+    "AATSC3v",
+    "AATSC4v",
+    "AATSC5v",
+    "AATSC6v",
+    "AATSC7v",
+    "AATSC8v",
+    "AATSC0se",
+    "AATSC1se",
+    "AATSC2se",
+    "AATSC3se",
+    "AATSC4se",
+    "AATSC5se",
+    "AATSC6se",
+    "AATSC7se",
+    "AATSC8se",
+    "AATSC0pe",
+    "AATSC1pe",
+    "AATSC2pe",
+    "AATSC3pe",
+    "AATSC4pe",
+    "AATSC5pe",
+    "AATSC6pe",
+    "AATSC7pe",
+    "AATSC8pe",
+    "AATSC0are",
+    "AATSC1are",
+    "AATSC2are",
+    "AATSC3are",
+    "AATSC4are",
+    "AATSC5are",
+    "AATSC6are",
+    "AATSC7are",
+    "AATSC8are",
+    "AATSC0p",
+    "AATSC1p",
+    "AATSC2p",
+    "AATSC3p",
+    "AATSC4p",
+    "AATSC5p",
+    "AATSC6p",
+    "AATSC7p",
+    "AATSC8p",
+    "AATSC0i",
+    "AATSC1i",
+    "AATSC2i",
+    "AATSC3i",
+    "AATSC4i",
+    "AATSC5i",
+    "AATSC6i",
+    "AATSC7i",
+    "AATSC8i",
+    "MATS1c",
+    "MATS2c",
+    "MATS3c",
+    "MATS4c",
+    "MATS5c",
+    "MATS6c",
+    "MATS7c",
+    "MATS8c",
+    "MATS1dv",
+    "MATS2dv",
+    "MATS3dv",
+    "MATS4dv",
+    "MATS5dv",
+    "MATS6dv",
+    "MATS7dv",
+    "MATS8dv",
+    "MATS1d",
+    "MATS2d",
+    "MATS3d",
+    "MATS4d",
+    "MATS5d",
+    "MATS6d",
+    "MATS7d",
+    "MATS8d",
+    "MATS1s",
+    "MATS2s",
+    "MATS3s",
+    "MATS4s",
+    "MATS5s",
+    "MATS6s",
+    "MATS7s",
+    "MATS8s",
+    "MATS1Z",
+    "MATS2Z",
+    "MATS3Z",
+    "MATS4Z",
+    "MATS5Z",
+    "MATS6Z",
+    "MATS7Z",
+    "MATS8Z",
+    "MATS1m",
+    "MATS2m",
+    "MATS3m",
+    "MATS4m",
+    "MATS5m",
+    "MATS6m",
+    "MATS7m",
+    "MATS8m",
+    "MATS1v",
+    "MATS2v",
+    "MATS3v",
+    "MATS4v",
+    "MATS5v",
+    "MATS6v",
+    "MATS7v",
+    "MATS8v",
+    "MATS1se",
+    "MATS2se",
+    "MATS3se",
+    "MATS4se",
+    "MATS5se",
+    "MATS6se",
+    "MATS7se",
+    "MATS8se",
+    "MATS1pe",
+    "MATS2pe",
+    "MATS3pe",
+    "MATS4pe",
+    "MATS5pe",
+    "MATS6pe",
+    "MATS7pe",
+    "MATS8pe",
+    "MATS1are",
+    "MATS2are",
+    "MATS3are",
+    "MATS4are",
+    "MATS5are",
+    "MATS6are",
+    "MATS7are",
+    "MATS8are",
+    "MATS1p",
+    "MATS2p",
+    "MATS3p",
+    "MATS4p",
+    "MATS5p",
+    "MATS6p",
+    "MATS7p",
+    "MATS8p",
+    "MATS1i",
+    "MATS2i",
+    "MATS3i",
+    "MATS4i",
+    "MATS5i",
+    "MATS6i",
+    "MATS7i",
+    "MATS8i",
+    "GATS1c",
+    "GATS2c",
+    "GATS3c",
+    "GATS4c",
+    "GATS5c",
+    "GATS6c",
+    "GATS7c",
+    "GATS8c",
+    "GATS1dv",
+    "GATS2dv",
+    "GATS3dv",
+    "GATS4dv",
+    "GATS5dv",
+    "GATS6dv",
+    "GATS7dv",
+    "GATS8dv",
+    "GATS1d",
+    "GATS2d",
+    "GATS3d",
+    "GATS4d",
+    "GATS5d",
+    "GATS6d",
+    "GATS7d",
+    "GATS8d",
+    "GATS1s",
+    "GATS2s",
+    "GATS3s",
+    "GATS4s",
+    "GATS5s",
+    "GATS6s",
+    "GATS7s",
+    "GATS8s",
+    "GATS1Z",
+    "GATS2Z",
+    "GATS3Z",
+    "GATS4Z",
+    "GATS5Z",
+    "GATS6Z",
+    "GATS7Z",
+    "GATS8Z",
+    "GATS1m",
+    "GATS2m",
+    "GATS3m",
+    "GATS4m",
+    "GATS5m",
+    "GATS6m",
+    "GATS7m",
+    "GATS8m",
+    "GATS1v",
+    "GATS2v",
+    "GATS3v",
+    "GATS4v",
+    "GATS5v",
+    "GATS6v",
+    "GATS7v",
+    "GATS8v",
+    "GATS1se",
+    "GATS2se",
+    "GATS3se",
+    "GATS4se",
+    "GATS5se",
+    "GATS6se",
+    "GATS7se",
+    "GATS8se",
+    "GATS1pe",
+    "GATS2pe",
+    "GATS3pe",
+    "GATS4pe",
+    "GATS5pe",
+    "GATS6pe",
+    "GATS7pe",
+    "GATS8pe",
+    "GATS1are",
+    "GATS2are",
+    "GATS3are",
+    "GATS4are",
+    "GATS5are",
+    "GATS6are",
+    "GATS7are",
+    "GATS8are",
+    "GATS1p",
+    "GATS2p",
+    "GATS3p",
+    "GATS4p",
+    "GATS5p",
+    "GATS6p",
+    "GATS7p",
+    "GATS8p",
+    "GATS1i",
+    "GATS2i",
+    "GATS3i",
+    "GATS4i",
+    "GATS5i",
+    "GATS6i",
+    "GATS7i",
+    "GATS8i",
+    "BCUTc-1h",
+    "BCUTc-1l",
+    "BCUTdv-1h",
+    "BCUTdv-1l",
+    "BCUTd-1h",
+    "BCUTd-1l",
+    "BCUTs-1h",
+    "BCUTs-1l",
+    "BCUTZ-1h",
+    "BCUTZ-1l",
+    "BCUTm-1h",
+    "BCUTm-1l",
+    "BCUTv-1h",
+    "BCUTv-1l",
+    "BCUTse-1h",
+    "BCUTse-1l",
+    "BCUTpe-1h",
+    "BCUTpe-1l",
+    "BCUTare-1h",
+    "BCUTare-1l",
+    "BCUTp-1h",
+    "BCUTp-1l",
+    "BCUTi-1h",
+    "BCUTi-1l",
+    "BalabanJ",
+    "SpAbs_DzZ",
+    "SpMax_DzZ",
+    "SpDiam_DzZ",
+    "SpAD_DzZ",
+    "SpMAD_DzZ",
+    "LogEE_DzZ",
+    "SM1_DzZ",
+    "VE1_DzZ",
+    "VE2_DzZ",
+    "VE3_DzZ",
+    "VR1_DzZ",
+    "VR2_DzZ",
+    "VR3_DzZ",
+    "SpAbs_Dzm",
+    "SpMax_Dzm",
+    "SpDiam_Dzm",
+    "SpAD_Dzm",
+    "SpMAD_Dzm",
+    "LogEE_Dzm",
+    "SM1_Dzm",
+    "VE1_Dzm",
+    "VE2_Dzm",
+    "VE3_Dzm",
+    "VR1_Dzm",
+    "VR2_Dzm",
+    "VR3_Dzm",
+    "SpAbs_Dzv",
+    "SpMax_Dzv",
+    "SpDiam_Dzv",
+    "SpAD_Dzv",
+    "SpMAD_Dzv",
+    "LogEE_Dzv",
+    "SM1_Dzv",
+    "VE1_Dzv",
+    "VE2_Dzv",
+    "VE3_Dzv",
+    "VR1_Dzv",
+    "VR2_Dzv",
+    "VR3_Dzv",
+    "SpAbs_Dzse",
+    "SpMax_Dzse",
+    "SpDiam_Dzse",
+    "SpAD_Dzse",
+    "SpMAD_Dzse",
+    "LogEE_Dzse",
+    "SM1_Dzse",
+    "VE1_Dzse",
+    "VE2_Dzse",
+    "VE3_Dzse",
+    "VR1_Dzse",
+    "VR2_Dzse",
+    "VR3_Dzse",
+    "SpAbs_Dzpe",
+    "SpMax_Dzpe",
+    "SpDiam_Dzpe",
+    "SpAD_Dzpe",
+    "SpMAD_Dzpe",
+    "LogEE_Dzpe",
+    "SM1_Dzpe",
+    "VE1_Dzpe",
+    "VE2_Dzpe",
+    "VE3_Dzpe",
+    "VR1_Dzpe",
+    "VR2_Dzpe",
+    "VR3_Dzpe",
+    "SpAbs_Dzare",
+    "SpMax_Dzare",
+    "SpDiam_Dzare",
+    "SpAD_Dzare",
+    "SpMAD_Dzare",
+    "LogEE_Dzare",
+    "SM1_Dzare",
+    "VE1_Dzare",
+    "VE2_Dzare",
+    "VE3_Dzare",
+    "VR1_Dzare",
+    "VR2_Dzare",
+    "VR3_Dzare",
+    "SpAbs_Dzp",
+    "SpMax_Dzp",
+    "SpDiam_Dzp",
+    "SpAD_Dzp",
+    "SpMAD_Dzp",
+    "LogEE_Dzp",
+    "SM1_Dzp",
+    "VE1_Dzp",
+    "VE2_Dzp",
+    "VE3_Dzp",
+    "VR1_Dzp",
+    "VR2_Dzp",
+    "VR3_Dzp",
+    "SpAbs_Dzi",
+    "SpMax_Dzi",
+    "SpDiam_Dzi",
+    "SpAD_Dzi",
+    "SpMAD_Dzi",
+    "LogEE_Dzi",
+    "SM1_Dzi",
+    "VE1_Dzi",
+    "VE2_Dzi",
+    "VE3_Dzi",
+    "VR1_Dzi",
+    "VR2_Dzi",
+    "VR3_Dzi",
+    "BertzCT",
+    "nBonds",
+    "nBondsO",
+    "nBondsS",
+    "nBondsD",
+    "nBondsT",
+    "nBondsA",
+    "nBondsM",
+    "nBondsKS",
+    "nBondsKD",
+    "RNCG",
+    "RPCG",
+    "C1SP1",
+    "C2SP1",
+    "C1SP2",
+    "C2SP2",
+    "C3SP2",
+    "C1SP3",
+    "C2SP3",
+    "C3SP3",
+    "C4SP3",
+    "HybRatio",
+    "FCSP3",
+    "Xch-3d",
+    "Xch-4d",
+    "Xch-5d",
+    "Xch-6d",
+    "Xch-7d",
+    "Xch-3dv",
+    "Xch-4dv",
+    "Xch-5dv",
+    "Xch-6dv",
+    "Xch-7dv",
+    "Xc-3d",
+    "Xc-4d",
+    "Xc-5d",
+    "Xc-6d",
+    "Xc-3dv",
+    "Xc-4dv",
+    "Xc-5dv",
+    "Xc-6dv",
+    "Xpc-4d",
+    "Xpc-5d",
+    "Xpc-6d",
+    "Xpc-4dv",
+    "Xpc-5dv",
+    "Xpc-6dv",
+    "Xp-0d",
+    "Xp-1d",
+    "Xp-2d",
+    "Xp-3d",
+    "Xp-4d",
+    "Xp-5d",
+    "Xp-6d",
+    "Xp-7d",
+    "AXp-0d",
+    "AXp-1d",
+    "AXp-2d",
+    "AXp-3d",
+    "AXp-4d",
+    "AXp-5d",
+    "AXp-6d",
+    "AXp-7d",
+    "Xp-0dv",
+    "Xp-1dv",
+    "Xp-2dv",
+    "Xp-3dv",
+    "Xp-4dv",
+    "Xp-5dv",
+    "Xp-6dv",
+    "Xp-7dv",
+    "AXp-0dv",
+    "AXp-1dv",
+    "AXp-2dv",
+    "AXp-3dv",
+    "AXp-4dv",
+    "AXp-5dv",
+    "AXp-6dv",
+    "AXp-7dv",
+    "SZ",
+    "Sm",
+    "Sv",
+    "Sse",
+    "Spe",
+    "Sare",
+    "Sp",
+    "Si",
+    "MZ",
+    "Mm",
+    "Mv",
+    "Mse",
+    "Mpe",
+    "Mare",
+    "Mp",
+    "Mi",
+    "SpAbs_Dt",
+    "SpMax_Dt",
+    "SpDiam_Dt",
+    "SpAD_Dt",
+    "SpMAD_Dt",
+    "LogEE_Dt",
+    "SM1_Dt",
+    "VE1_Dt",
+    "VE2_Dt",
+    "VE3_Dt",
+    "VR1_Dt",
+    "VR2_Dt",
+    "VR3_Dt",
+    "DetourIndex",
+    "SpAbs_D",
+    "SpMax_D",
+    "SpDiam_D",
+    "SpAD_D",
+    "SpMAD_D",
+    "LogEE_D",
+    "VE1_D",
+    "VE2_D",
+    "VE3_D",
+    "VR1_D",
+    "VR2_D",
+    "VR3_D",
+    "NsLi",
+    "NssBe",
+    "NssssBe",
+    "NssBH",
+    "NsssB",
+    "NssssB",
+    "NsCH3",
+    "NdCH2",
+    "NssCH2",
+    "NtCH",
+    "NdsCH",
+    "NaaCH",
+    "NsssCH",
+    "NddC",
+    "NtsC",
+    "NdssC",
+    "NaasC",
+    "NaaaC",
+    "NssssC",
+    "NsNH3",
+    "NsNH2",
+    "NssNH2",
+    "NdNH",
+    "NssNH",
+    "NaaNH",
+    "NtN",
+    "NsssNH",
+    "NdsN",
+    "NaaN",
+    "NsssN",
+    "NddsN",
+    "NaasN",
+    "NssssN",
+    "NsOH",
+    "NdO",
+    "NssO",
+    "NaaO",
+    "NsF",
+    "NsSiH3",
+    "NssSiH2",
+    "NsssSiH",
+    "NssssSi",
+    "NsPH2",
+    "NssPH",
+    "NsssP",
+    "NdsssP",
+    "NsssssP",
+    "NsSH",
+    "NdS",
+    "NssS",
+    "NaaS",
+    "NdssS",
+    "NddssS",
+    "NsCl",
+    "NsGeH3",
+    "NssGeH2",
+    "NsssGeH",
+    "NssssGe",
+    "NsAsH2",
+    "NssAsH",
+    "NsssAs",
+    "NsssdAs",
+    "NsssssAs",
+    "NsSeH",
+    "NdSe",
+    "NssSe",
+    "NaaSe",
+    "NdssSe",
+    "NddssSe",
+    "NsBr",
+    "NsSnH3",
+    "NssSnH2",
+    "NsssSnH",
+    "NssssSn",
+    "NsI",
+    "NsPbH3",
+    "NssPbH2",
+    "NsssPbH",
+    "NssssPb",
+    "SsLi",
+    "SssBe",
+    "SssssBe",
+    "SssBH",
+    "SsssB",
+    "SssssB",
+    "SsCH3",
+    "SdCH2",
+    "SssCH2",
+    "StCH",
+    "SdsCH",
+    "SaaCH",
+    "SsssCH",
+    "SddC",
+    "StsC",
+    "SdssC",
+    "SaasC",
+    "SaaaC",
+    "SssssC",
+    "SsNH3",
+    "SsNH2",
+    "SssNH2",
+    "SdNH",
+    "SssNH",
+    "SaaNH",
+    "StN",
+    "SsssNH",
+    "SdsN",
+    "SaaN",
+    "SsssN",
+    "SddsN",
+    "SaasN",
+    "SssssN",
+    "SsOH",
+    "SdO",
+    "SssO",
+    "SaaO",
+    "SsF",
+    "SsSiH3",
+    "SssSiH2",
+    "SsssSiH",
+    "SssssSi",
+    "SsPH2",
+    "SssPH",
+    "SsssP",
+    "SdsssP",
+    "SsssssP",
+    "SsSH",
+    "SdS",
+    "SssS",
+    "SaaS",
+    "SdssS",
+    "SddssS",
+    "SsCl",
+    "SsGeH3",
+    "SssGeH2",
+    "SsssGeH",
+    "SssssGe",
+    "SsAsH2",
+    "SssAsH",
+    "SsssAs",
+    "SsssdAs",
+    "SsssssAs",
+    "SsSeH",
+    "SdSe",
+    "SssSe",
+    "SaaSe",
+    "SdssSe",
+    "SddssSe",
+    "SsBr",
+    "SsSnH3",
+    "SssSnH2",
+    "SsssSnH",
+    "SssssSn",
+    "SsI",
+    "SsPbH3",
+    "SssPbH2",
+    "SsssPbH",
+    "SssssPb",
+    "MAXsLi",
+    "MAXssBe",
+    "MAXssssBe",
+    "MAXssBH",
+    "MAXsssB",
+    "MAXssssB",
+    "MAXsCH3",
+    "MAXdCH2",
+    "MAXssCH2",
+    "MAXtCH",
+    "MAXdsCH",
+    "MAXaaCH",
+    "MAXsssCH",
+    "MAXddC",
+    "MAXtsC",
+    "MAXdssC",
+    "MAXaasC",
+    "MAXaaaC",
+    "MAXssssC",
+    "MAXsNH3",
+    "MAXsNH2",
+    "MAXssNH2",
+    "MAXdNH",
+    "MAXssNH",
+    "MAXaaNH",
+    "MAXtN",
+    "MAXsssNH",
+    "MAXdsN",
+    "MAXaaN",
+    "MAXsssN",
+    "MAXddsN",
+    "MAXaasN",
+    "MAXssssN",
+    "MAXsOH",
+    "MAXdO",
+    "MAXssO",
+    "MAXaaO",
+    "MAXsF",
+    "MAXsSiH3",
+    "MAXssSiH2",
+    "MAXsssSiH",
+    "MAXssssSi",
+    "MAXsPH2",
+    "MAXssPH",
+    "MAXsssP",
+    "MAXdsssP",
+    "MAXsssssP",
+    "MAXsSH",
+    "MAXdS",
+    "MAXssS",
+    "MAXaaS",
+    "MAXdssS",
+    "MAXddssS",
+    "MAXsCl",
+    "MAXsGeH3",
+    "MAXssGeH2",
+    "MAXsssGeH",
+    "MAXssssGe",
+    "MAXsAsH2",
+    "MAXssAsH",
+    "MAXsssAs",
+    "MAXsssdAs",
+    "MAXsssssAs",
+    "MAXsSeH",
+    "MAXdSe",
+    "MAXssSe",
+    "MAXaaSe",
+    "MAXdssSe",
+    "MAXddssSe",
+    "MAXsBr",
+    "MAXsSnH3",
+    "MAXssSnH2",
+    "MAXsssSnH",
+    "MAXssssSn",
+    "MAXsI",
+    "MAXsPbH3",
+    "MAXssPbH2",
+    "MAXsssPbH",
+    "MAXssssPb",
+    "MINsLi",
+    "MINssBe",
+    "MINssssBe",
+    "MINssBH",
+    "MINsssB",
+    "MINssssB",
+    "MINsCH3",
+    "MINdCH2",
+    "MINssCH2",
+    "MINtCH",
+    "MINdsCH",
+    "MINaaCH",
+    "MINsssCH",
+    "MINddC",
+    "MINtsC",
+    "MINdssC",
+    "MINaasC",
+    "MINaaaC",
+    "MINssssC",
+    "MINsNH3",
+    "MINsNH2",
+    "MINssNH2",
+    "MINdNH",
+    "MINssNH",
+    "MINaaNH",
+    "MINtN",
+    "MINsssNH",
+    "MINdsN",
+    "MINaaN",
+    "MINsssN",
+    "MINddsN",
+    "MINaasN",
+    "MINssssN",
+    "MINsOH",
+    "MINdO",
+    "MINssO",
+    "MINaaO",
+    "MINsF",
+    "MINsSiH3",
+    "MINssSiH2",
+    "MINsssSiH",
+    "MINssssSi",
+    "MINsPH2",
+    "MINssPH",
+    "MINsssP",
+    "MINdsssP",
+    "MINsssssP",
+    "MINsSH",
+    "MINdS",
+    "MINssS",
+    "MINaaS",
+    "MINdssS",
+    "MINddssS",
+    "MINsCl",
+    "MINsGeH3",
+    "MINssGeH2",
+    "MINsssGeH",
+    "MINssssGe",
+    "MINsAsH2",
+    "MINssAsH",
+    "MINsssAs",
+    "MINsssdAs",
+    "MINsssssAs",
+    "MINsSeH",
+    "MINdSe",
+    "MINssSe",
+    "MINaaSe",
+    "MINdssSe",
+    "MINddssSe",
+    "MINsBr",
+    "MINsSnH3",
+    "MINssSnH2",
+    "MINsssSnH",
+    "MINssssSn",
+    "MINsI",
+    "MINsPbH3",
+    "MINssPbH2",
+    "MINsssPbH",
+    "MINssssPb",
+    "ECIndex",
+    "ETA_alpha",
+    "AETA_alpha",
+    "ETA_shape_p",
+    "ETA_shape_y",
+    "ETA_shape_x",
+    "ETA_beta",
+    "AETA_beta",
+    "ETA_beta_s",
+    "AETA_beta_s",
+    "ETA_beta_ns",
+    "AETA_beta_ns",
+    "ETA_beta_ns_d",
+    "AETA_beta_ns_d",
+    "ETA_eta",
+    "AETA_eta",
+    "ETA_eta_L",
+    "AETA_eta_L",
+    "ETA_eta_R",
+    "AETA_eta_R",
+    "ETA_eta_RL",
+    "AETA_eta_RL",
+    "ETA_eta_F",
+    "AETA_eta_F",
+    "ETA_eta_FL",
+    "AETA_eta_FL",
+    "ETA_eta_B",
+    "AETA_eta_B",
+    "ETA_eta_BR",
+    "AETA_eta_BR",
+    "ETA_dAlpha_A",
+    "ETA_dAlpha_B",
+    "ETA_epsilon_1",
+    "ETA_epsilon_2",
+    "ETA_epsilon_3",
+    "ETA_epsilon_4",
+    "ETA_epsilon_5",
+    "ETA_dEpsilon_A",
+    "ETA_dEpsilon_B",
+    "ETA_dEpsilon_C",
+    "ETA_dEpsilon_D",
+    "ETA_dBeta",
+    "AETA_dBeta",
+    "ETA_psi_1",
+    "ETA_dPsi_A",
+    "ETA_dPsi_B",
+    "fragCpx",
+    "fMF",
+    "nHBAcc",
+    "nHBDon",
+    "IC0",
+    "IC1",
+    "IC2",
+    "IC3",
+    "IC4",
+    "IC5",
+    "TIC0",
+    "TIC1",
+    "TIC2",
+    "TIC3",
+    "TIC4",
+    "TIC5",
+    "SIC0",
+    "SIC1",
+    "SIC2",
+    "SIC3",
+    "SIC4",
+    "SIC5",
+    "BIC0",
+    "BIC1",
+    "BIC2",
+    "BIC3",
+    "BIC4",
+    "BIC5",
+    "CIC0",
+    "CIC1",
+    "CIC2",
+    "CIC3",
+    "CIC4",
+    "CIC5",
+    "MIC0",
+    "MIC1",
+    "MIC2",
+    "MIC3",
+    "MIC4",
+    "MIC5",
+    "ZMIC0",
+    "ZMIC1",
+    "ZMIC2",
+    "ZMIC3",
+    "ZMIC4",
+    "ZMIC5",
+    "Kier1",
+    "Kier2",
+    "Kier3",
+    "Lipinski",
+    "GhoseFilter",
+    "FilterItLogS",
+    "VMcGowan",
+    "LabuteASA",
+    "PEOE_VSA1",
+    "PEOE_VSA2",
+    "PEOE_VSA3",
+    "PEOE_VSA4",
+    "PEOE_VSA5",
+    "PEOE_VSA6",
+    "PEOE_VSA7",
+    "PEOE_VSA8",
+    "PEOE_VSA9",
+    "PEOE_VSA10",
+    "PEOE_VSA11",
+    "PEOE_VSA12",
+    "PEOE_VSA13",
+    "SMR_VSA1",
+    "SMR_VSA2",
+    "SMR_VSA3",
+    "SMR_VSA4",
+    "SMR_VSA5",
+    "SMR_VSA6",
+    "SMR_VSA7",
+    "SMR_VSA8",
+    "SMR_VSA9",
+    "SlogP_VSA1",
+    "SlogP_VSA2",
+    "SlogP_VSA3",
+    "SlogP_VSA4",
+    "SlogP_VSA5",
+    "SlogP_VSA6",
+    "SlogP_VSA7",
+    "SlogP_VSA8",
+    "SlogP_VSA9",
+    "SlogP_VSA10",
+    "SlogP_VSA11",
+    "EState_VSA1",
+    "EState_VSA2",
+    "EState_VSA3",
+    "EState_VSA4",
+    "EState_VSA5",
+    "EState_VSA6",
+    "EState_VSA7",
+    "EState_VSA8",
+    "EState_VSA9",
+    "EState_VSA10",
+    "VSA_EState1",
+    "VSA_EState2",
+    "VSA_EState3",
+    "VSA_EState4",
+    "VSA_EState5",
+    "VSA_EState6",
+    "VSA_EState7",
+    "VSA_EState8",
+    "VSA_EState9",
+    "MDEC-11",
+    "MDEC-12",
+    "MDEC-13",
+    "MDEC-14",
+    "MDEC-22",
+    "MDEC-23",
+    "MDEC-24",
+    "MDEC-33",
+    "MDEC-34",
+    "MDEC-44",
+    "MDEO-11",
+    "MDEO-12",
+    "MDEO-22",
+    "MDEN-11",
+    "MDEN-12",
+    "MDEN-13",
+    "MDEN-22",
+    "MDEN-23",
+    "MDEN-33",
+    "MID",
+    "AMID",
+    "MID_h",
+    "AMID_h",
+    "MID_C",
+    "AMID_C",
+    "MID_N",
+    "AMID_N",
+    "MID_O",
+    "AMID_O",
+    "MID_X",
+    "AMID_X",
+    "MPC2",
+    "MPC3",
+    "MPC4",
+    "MPC5",
+    "MPC6",
+    "MPC7",
+    "MPC8",
+    "MPC9",
+    "MPC10",
+    "TMPC10",
+    "piPC1",
+    "piPC2",
+    "piPC3",
+    "piPC4",
+    "piPC5",
+    "piPC6",
+    "piPC7",
+    "piPC8",
+    "piPC9",
+    "piPC10",
+    "TpiPC10",
+    "apol",
+    "bpol",
+    "nRing",
+    "n3Ring",
+    "n4Ring",
+    "n5Ring",
+    "n6Ring",
+    "n7Ring",
+    "n8Ring",
+    "n9Ring",
+    "n10Ring",
+    "n11Ring",
+    "n12Ring",
+    "nG12Ring",
+    "nHRing",
+    "n3HRing",
+    "n4HRing",
+    "n5HRing",
+    "n6HRing",
+    "n7HRing",
+    "n8HRing",
+    "n9HRing",
+    "n10HRing",
+    "n11HRing",
+    "n12HRing",
+    "nG12HRing",
+    "naRing",
+    "n3aRing",
+    "n4aRing",
+    "n5aRing",
+    "n6aRing",
+    "n7aRing",
+    "n8aRing",
+    "n9aRing",
+    "n10aRing",
+    "n11aRing",
+    "n12aRing",
+    "nG12aRing",
+    "naHRing",
+    "n3aHRing",
+    "n4aHRing",
+    "n5aHRing",
+    "n6aHRing",
+    "n7aHRing",
+    "n8aHRing",
+    "n9aHRing",
+    "n10aHRing",
+    "n11aHRing",
+    "n12aHRing",
+    "nG12aHRing",
+    "nARing",
+    "n3ARing",
+    "n4ARing",
+    "n5ARing",
+    "n6ARing",
+    "n7ARing",
+    "n8ARing",
+    "n9ARing",
+    "n10ARing",
+    "n11ARing",
+    "n12ARing",
+    "nG12ARing",
+    "nAHRing",
+    "n3AHRing",
+    "n4AHRing",
+    "n5AHRing",
+    "n6AHRing",
+    "n7AHRing",
+    "n8AHRing",
+    "n9AHRing",
+    "n10AHRing",
+    "n11AHRing",
+    "n12AHRing",
+    "nG12AHRing",
+    "nFRing",
+    "n4FRing",
+    "n5FRing",
+    "n6FRing",
+    "n7FRing",
+    "n8FRing",
+    "n9FRing",
+    "n10FRing",
+    "n11FRing",
+    "n12FRing",
+    "nG12FRing",
+    "nFHRing",
+    "n4FHRing",
+    "n5FHRing",
+    "n6FHRing",
+    "n7FHRing",
+    "n8FHRing",
+    "n9FHRing",
+    "n10FHRing",
+    "n11FHRing",
+    "n12FHRing",
+    "nG12FHRing",
+    "nFaRing",
+    "n4FaRing",
+    "n5FaRing",
+    "n6FaRing",
+    "n7FaRing",
+    "n8FaRing",
+    "n9FaRing",
+    "n10FaRing",
+    "n11FaRing",
+    "n12FaRing",
+    "nG12FaRing",
+    "nFaHRing",
+    "n4FaHRing",
+    "n5FaHRing",
+    "n6FaHRing",
+    "n7FaHRing",
+    "n8FaHRing",
+    "n9FaHRing",
+    "n10FaHRing",
+    "n11FaHRing",
+    "n12FaHRing",
+    "nG12FaHRing",
+    "nFARing",
+    "n4FARing",
+    "n5FARing",
+    "n6FARing",
+    "n7FARing",
+    "n8FARing",
+    "n9FARing",
+    "n10FARing",
+    "n11FARing",
+    "n12FARing",
+    "nG12FARing",
+    "nFAHRing",
+    "n4FAHRing",
+    "n5FAHRing",
+    "n6FAHRing",
+    "n7FAHRing",
+    "n8FAHRing",
+    "n9FAHRing",
+    "n10FAHRing",
+    "n11FAHRing",
+    "n12FAHRing",
+    "nG12FAHRing",
+    "nRot",
+    "RotRatio",
+    "SLogP",
+    "SMR",
+    "TopoPSA(NO)",
+    "TopoPSA",
+    "GGI1",
+    "GGI2",
+    "GGI3",
+    "GGI4",
+    "GGI5",
+    "GGI6",
+    "GGI7",
+    "GGI8",
+    "GGI9",
+    "GGI10",
+    "JGI1",
+    "JGI2",
+    "JGI3",
+    "JGI4",
+    "JGI5",
+    "JGI6",
+    "JGI7",
+    "JGI8",
+    "JGI9",
+    "JGI10",
+    "JGT10",
+    "Diameter",
+    "Radius",
+    "TopoShapeIndex",
+    "PetitjeanIndex",
+    "Vabc",
+    "VAdjMat",
+    "MWC01",
+    "MWC02",
+    "MWC03",
+    "MWC04",
+    "MWC05",
+    "MWC06",
+    "MWC07",
+    "MWC08",
+    "MWC09",
+    "MWC10",
+    "TMWC10",
+    "SRW02",
+    "SRW03",
+    "SRW04",
+    "SRW05",
+    "SRW06",
+    "SRW07",
+    "SRW08",
+    "SRW09",
+    "SRW10",
+    "TSRW10",
+    "MW",
+    "AMW",
+    "WPath",
+    "WPol",
+    "Zagreb1",
+    "Zagreb2",
+    "mZagreb1",
+    "mZagreb2",
+]
+
+SUBSET_947 = [
+    "ABC",
+    "ABCGG",
+    "nBase",
+    "SpAbs_A",
+    "SpMax_A",
+    "SpDiam_A",
+    "SpAD_A",
+    "SpMAD_A",
+    "LogEE_A",
+    "VE1_A",
+    "VE2_A",
+    "VE3_A",
+    "VR1_A",
+    "VR2_A",
+    "VR3_A",
+    "nAromAtom",
+    "nAromBond",
+    "nAtom",
+    "nHeavyAtom",
+    "nHetero",
+    "nH",
+    "nC",
+    "nN",
+    "nO",
+    "ATS0dv",
+    "ATS1dv",
+    "ATS2dv",
+    "ATS3dv",
+    "ATS0d",
+    "ATS1d",
+    "ATS2d",
+    "ATS3d",
+    "ATS4d",
+    "ATS5d",
+    "ATS0s",
+    "ATS1s",
+    "ATS2s",
+    "ATS3s",
+    "ATS4s",
+    "ATS5s",
+    "ATS0Z",
+    "ATS1Z",
+    "ATS2Z",
+    "ATS3Z",
+    "ATS4Z",
+    "ATS5Z",
+    "ATS0m",
+    "ATS1m",
+    "ATS2m",
+    "ATS3m",
+    "ATS4m",
+    "ATS5m",
+    "ATS0v",
+    "ATS1v",
+    "ATS2v",
+    "ATS3v",
+    "ATS4v",
+    "ATS5v",
+    "ATS0se",
+    "ATS1se",
+    "ATS2se",
+    "ATS3se",
+    "ATS4se",
+    "ATS5se",
+    "ATS0pe",
+    "ATS1pe",
+    "ATS2pe",
+    "ATS3pe",
+    "ATS4pe",
+    "ATS5pe",
+    "ATS0are",
+    "ATS1are",
+    "ATS2are",
+    "ATS3are",
+    "ATS4are",
+    "ATS5are",
+    "ATS0p",
+    "ATS1p",
+    "ATS2p",
+    "ATS3p",
+    "ATS4p",
+    "ATS5p",
+    "ATS0i",
+    "ATS1i",
+    "ATS2i",
+    "ATS3i",
+    "ATS4i",
+    "ATS5i",
+    "AATS0dv",
+    "AATS1dv",
+    "AATS2dv",
+    "AATS3dv",
+    "AATS0d",
+    "AATS1d",
+    "AATS2d",
+    "AATS3d",
+    "AATS4d",
+    "AATS0s",
+    "AATS1s",
+    "AATS2s",
+    "AATS3s",
+    "AATS4s",
+    "AATS0Z",
+    "AATS1Z",
+    "AATS2Z",
+    "AATS3Z",
+    "AATS4Z",
+    "AATS0m",
+    "AATS1m",
+    "AATS2m",
+    "AATS3m",
+    "AATS4m",
+    "AATS0v",
+    "AATS1v",
+    "AATS2v",
+    "AATS3v",
+    "AATS4v",
+    "AATS5v",
+    "AATS0se",
+    "AATS1se",
+    "AATS2se",
+    "AATS3se",
+    "AATS4se",
+    "AATS5se",
+    "AATS0pe",
+    "AATS1pe",
+    "AATS2pe",
+    "AATS3pe",
+    "AATS4pe",
+    "AATS5pe",
+    "AATS0are",
+    "AATS1are",
+    "AATS2are",
+    "AATS3are",
+    "AATS4are",
+    "AATS5are",
+    "AATS0p",
+    "AATS1p",
+    "AATS2p",
+    "AATS3p",
+    "AATS4p",
+    "AATS5p",
+    "AATS0i",
+    "AATS1i",
+    "AATS2i",
+    "AATS3i",
+    "AATS4i",
+    "AATS5i",
+    "ATSC0c",
+    "ATSC1c",
+    "ATSC2c",
+    "ATSC3c",
+    "ATSC4c",
+    "ATSC5c",
+    "ATSC0dv",
+    "ATSC1dv",
+    "ATSC2dv",
+    "ATSC3dv",
+    "ATSC4dv",
+    "ATSC5dv",
+    "ATSC0d",
+    "ATSC1d",
+    "ATSC2d",
+    "ATSC3d",
+    "ATSC4d",
+    "ATSC5d",
+    "ATSC0s",
+    "ATSC1s",
+    "ATSC2s",
+    "ATSC3s",
+    "ATSC4s",
+    "ATSC5s",
+    "ATSC0Z",
+    "ATSC1Z",
+    "ATSC2Z",
+    "ATSC3Z",
+    "ATSC4Z",
+    "ATSC5Z",
+    "ATSC0m",
+    "ATSC1m",
+    "ATSC2m",
+    "ATSC3m",
+    "ATSC4m",
+    "ATSC5m",
+    "ATSC0v",
+    "ATSC1v",
+    "ATSC2v",
+    "ATSC3v",
+    "ATSC4v",
+    "ATSC5v",
+    "ATSC0se",
+    "ATSC1se",
+    "ATSC2se",
+    "ATSC3se",
+    "ATSC4se",
+    "ATSC5se",
+    "ATSC0pe",
+    "ATSC1pe",
+    "ATSC2pe",
+    "ATSC3pe",
+    "ATSC4pe",
+    "ATSC5pe",
+    "ATSC0are",
+    "ATSC1are",
+    "ATSC2are",
+    "ATSC3are",
+    "ATSC4are",
+    "ATSC5are",
+    "ATSC0p",
+    "ATSC1p",
+    "ATSC2p",
+    "ATSC3p",
+    "ATSC4p",
+    "ATSC5p",
+    "ATSC0i",
+    "ATSC1i",
+    "ATSC2i",
+    "ATSC3i",
+    "ATSC4i",
+    "ATSC5i",
+    "AATSC0c",
+    "AATSC1c",
+    "AATSC2c",
+    "AATSC3c",
+    "AATSC4c",
+    "AATSC5c",
+    "AATSC0dv",
+    "AATSC1dv",
+    "AATSC2dv",
+    "AATSC3dv",
+    "AATSC4dv",
+    "AATSC5dv",
+    "AATSC0d",
+    "AATSC1d",
+    "AATSC2d",
+    "AATSC3d",
+    "AATSC4d",
+    "AATSC5d",
+    "AATSC0s",
+    "AATSC1s",
+    "AATSC2s",
+    "AATSC3s",
+    "AATSC4s",
+    "AATSC5s",
+    "AATSC0Z",
+    "AATSC1Z",
+    "AATSC2Z",
+    "AATSC3Z",
+    "AATSC4Z",
+    "AATSC5Z",
+    "AATSC0m",
+    "AATSC1m",
+    "AATSC2m",
+    "AATSC3m",
+    "AATSC4m",
+    "AATSC5m",
+    "AATSC0v",
+    "AATSC1v",
+    "AATSC2v",
+    "AATSC3v",
+    "AATSC4v",
+    "AATSC5v",
+    "AATSC0se",
+    "AATSC1se",
+    "AATSC2se",
+    "AATSC3se",
+    "AATSC4se",
+    "AATSC5se",
+    "AATSC0pe",
+    "AATSC1pe",
+    "AATSC2pe",
+    "AATSC3pe",
+    "AATSC4pe",
+    "AATSC5pe",
+    "AATSC0are",
+    "AATSC1are",
+    "AATSC2are",
+    "AATSC3are",
+    "AATSC4are",
+    "AATSC5are",
+    "AATSC0p",
+    "AATSC1p",
+    "AATSC2p",
+    "AATSC3p",
+    "AATSC4p",
+    "AATSC5p",
+    "AATSC0i",
+    "AATSC1i",
+    "AATSC2i",
+    "AATSC3i",
+    "AATSC4i",
+    "AATSC5i",
+    "MATS1c",
+    "MATS2c",
+    "MATS3c",
+    "MATS4c",
+    "MATS5c",
+    "MATS1dv",
+    "MATS2dv",
+    "MATS3dv",
+    "MATS4dv",
+    "MATS5dv",
+    "MATS1d",
+    "MATS2d",
+    "MATS3d",
+    "MATS4d",
+    "MATS5d",
+    "MATS1s",
+    "MATS2s",
+    "MATS3s",
+    "MATS4s",
+    "MATS5s",
+    "MATS1Z",
+    "MATS2Z",
+    "MATS3Z",
+    "MATS4Z",
+    "MATS5Z",
+    "MATS1m",
+    "MATS2m",
+    "MATS3m",
+    "MATS4m",
+    "MATS5m",
+    "MATS1v",
+    "MATS2v",
+    "MATS3v",
+    "MATS4v",
+    "MATS5v",
+    "MATS1se",
+    "MATS2se",
+    "MATS3se",
+    "MATS4se",
+    "MATS5se",
+    "MATS1pe",
+    "MATS2pe",
+    "MATS3pe",
+    "MATS4pe",
+    "MATS5pe",
+    "MATS1are",
+    "MATS2are",
+    "MATS3are",
+    "MATS4are",
+    "MATS5are",
+    "MATS1p",
+    "MATS2p",
+    "MATS3p",
+    "MATS4p",
+    "MATS5p",
+    "MATS1i",
+    "MATS2i",
+    "MATS3i",
+    "MATS4i",
+    "MATS5i",
+    "GATS1c",
+    "GATS2c",
+    "GATS3c",
+    "GATS4c",
+    "GATS5c",
+    "GATS1dv",
+    "GATS2dv",
+    "GATS3dv",
+    "GATS4dv",
+    "GATS1d",
+    "GATS2d",
+    "GATS3d",
+    "GATS4d",
+    "GATS1s",
+    "GATS2s",
+    "GATS3s",
+    "GATS4s",
+    "GATS1Z",
+    "GATS2Z",
+    "GATS3Z",
+    "GATS4Z",
+    "GATS1m",
+    "GATS2m",
+    "GATS3m",
+    "GATS4m",
+    "GATS1v",
+    "GATS2v",
+    "GATS3v",
+    "GATS4v",
+    "GATS1se",
+    "GATS2se",
+    "GATS3se",
+    "GATS4se",
+    "GATS1pe",
+    "GATS2pe",
+    "GATS3pe",
+    "GATS4pe",
+    "GATS1are",
+    "GATS2are",
+    "GATS3are",
+    "GATS4are",
+    "GATS1p",
+    "GATS2p",
+    "GATS3p",
+    "GATS4p",
+    "GATS1i",
+    "GATS2i",
+    "GATS3i",
+    "GATS4i",
+    "BCUTc-1h",
+    "BCUTc-1l",
+    "BCUTdv-1h",
+    "BCUTdv-1l",
+    "BCUTd-1h",
+    "BCUTd-1l",
+    "BCUTs-1h",
+    "BCUTs-1l",
+    "BCUTZ-1h",
+    "BCUTZ-1l",
+    "BCUTm-1h",
+    "BCUTm-1l",
+    "BCUTv-1h",
+    "BCUTv-1l",
+    "BCUTse-1h",
+    "BCUTse-1l",
+    "BCUTpe-1h",
+    "BCUTpe-1l",
+    "BCUTare-1h",
+    "BCUTare-1l",
+    "BCUTp-1h",
+    "BCUTp-1l",
+    "BCUTi-1h",
+    "BCUTi-1l",
+    "BalabanJ",
+    "SpAbs_DzZ",
+    "SpMax_DzZ",
+    "SpDiam_DzZ",
+    "SpAD_DzZ",
+    "SpMAD_DzZ",
+    "LogEE_DzZ",
+    "SM1_DzZ",
+    "VE1_DzZ",
+    "VE2_DzZ",
+    "VE3_DzZ",
+    "VR1_DzZ",
+    "VR2_DzZ",
+    "VR3_DzZ",
+    "SpAbs_Dzm",
+    "SpMax_Dzm",
+    "SpDiam_Dzm",
+    "SpAD_Dzm",
+    "SpMAD_Dzm",
+    "LogEE_Dzm",
+    "SM1_Dzm",
+    "VE1_Dzm",
+    "VE2_Dzm",
+    "VE3_Dzm",
+    "VR1_Dzm",
+    "VR2_Dzm",
+    "VR3_Dzm",
+    "SpAbs_Dzv",
+    "SpMax_Dzv",
+    "SpDiam_Dzv",
+    "SpAD_Dzv",
+    "SpMAD_Dzv",
+    "LogEE_Dzv",
+    "SM1_Dzv",
+    "VE1_Dzv",
+    "VE2_Dzv",
+    "VE3_Dzv",
+    "VR1_Dzv",
+    "VR2_Dzv",
+    "VR3_Dzv",
+    "SpAbs_Dzse",
+    "SpMax_Dzse",
+    "SpDiam_Dzse",
+    "SpAD_Dzse",
+    "SpMAD_Dzse",
+    "LogEE_Dzse",
+    "SM1_Dzse",
+    "VE1_Dzse",
+    "VE2_Dzse",
+    "VE3_Dzse",
+    "VR1_Dzse",
+    "VR2_Dzse",
+    "VR3_Dzse",
+    "SpAbs_Dzpe",
+    "SpMax_Dzpe",
+    "SpDiam_Dzpe",
+    "SpAD_Dzpe",
+    "SpMAD_Dzpe",
+    "LogEE_Dzpe",
+    "SM1_Dzpe",
+    "VE1_Dzpe",
+    "VE2_Dzpe",
+    "VE3_Dzpe",
+    "VR1_Dzpe",
+    "VR2_Dzpe",
+    "VR3_Dzpe",
+    "SpAbs_Dzare",
+    "SpMax_Dzare",
+    "SpDiam_Dzare",
+    "SpAD_Dzare",
+    "SpMAD_Dzare",
+    "LogEE_Dzare",
+    "SM1_Dzare",
+    "VE1_Dzare",
+    "VE2_Dzare",
+    "VE3_Dzare",
+    "VR1_Dzare",
+    "VR2_Dzare",
+    "VR3_Dzare",
+    "SpAbs_Dzp",
+    "SpMax_Dzp",
+    "SpDiam_Dzp",
+    "SpAD_Dzp",
+    "SpMAD_Dzp",
+    "LogEE_Dzp",
+    "SM1_Dzp",
+    "VE1_Dzp",
+    "VE2_Dzp",
+    "VE3_Dzp",
+    "VR1_Dzp",
+    "VR2_Dzp",
+    "VR3_Dzp",
+    "SpAbs_Dzi",
+    "SpMax_Dzi",
+    "SpDiam_Dzi",
+    "SpAD_Dzi",
+    "SpMAD_Dzi",
+    "LogEE_Dzi",
+    "SM1_Dzi",
+    "VE1_Dzi",
+    "VE2_Dzi",
+    "VE3_Dzi",
+    "VR1_Dzi",
+    "VR2_Dzi",
+    "VR3_Dzi",
+    "BertzCT",
+    "nBonds",
+    "nBondsO",
+    "nBondsS",
+    "nBondsD",
+    "nBondsT",
+    "nBondsA",
+    "nBondsM",
+    "nBondsKS",
+    "nBondsKD",
+    "RNCG",
+    "RPCG",
+    "C1SP1",
+    "C2SP1",
+    "C1SP2",
+    "C2SP2",
+    "C1SP3",
+    "C2SP3",
+    "C3SP3",
+    "C4SP3",
+    "HybRatio",
+    "FCSP3",
+    "Xch-3d",
+    "Xch-4d",
+    "Xch-5d",
+    "Xch-3dv",
+    "Xch-4dv",
+    "Xch-5dv",
+    "Xc-3d",
+    "Xc-4d",
+    "Xc-3dv",
+    "Xc-4dv",
+    "Xpc-4d",
+    "Xpc-4dv",
+    "Xp-0d",
+    "Xp-1d",
+    "Xp-2d",
+    "Xp-3d",
+    "Xp-4d",
+    "AXp-0d",
+    "AXp-1d",
+    "AXp-2d",
+    "AXp-3d",
+    "AXp-4d",
+    "Xp-0dv",
+    "Xp-1dv",
+    "Xp-2dv",
+    "Xp-3dv",
+    "Xp-4dv",
+    "AXp-0dv",
+    "AXp-1dv",
+    "AXp-2dv",
+    "AXp-3dv",
+    "AXp-4dv",
+    "SZ",
+    "Sm",
+    "Sv",
+    "Sse",
+    "Spe",
+    "Sare",
+    "Sp",
+    "Si",
+    "MZ",
+    "Mm",
+    "Mv",
+    "Mse",
+    "Mpe",
+    "Mare",
+    "Mp",
+    "Mi",
+    "SpAbs_Dt",
+    "SpMax_Dt",
+    "SpDiam_Dt",
+    "SpAD_Dt",
+    "SpMAD_Dt",
+    "LogEE_Dt",
+    "VE1_Dt",
+    "VE2_Dt",
+    "VE3_Dt",
+    "VR1_Dt",
+    "VR2_Dt",
+    "VR3_Dt",
+    "DetourIndex",
+    "SpAbs_D",
+    "SpMax_D",
+    "SpDiam_D",
+    "SpAD_D",
+    "SpMAD_D",
+    "LogEE_D",
+    "VE1_D",
+    "VE2_D",
+    "VE3_D",
+    "VR1_D",
+    "VR2_D",
+    "VR3_D",
+    "NsCH3",
+    "NdCH2",
+    "NssCH2",
+    "NtCH",
+    "NdsCH",
+    "NaaCH",
+    "NsssCH",
+    "NtsC",
+    "NdssC",
+    "NssssC",
+    "NsNH2",
+    "NdNH",
+    "NssNH",
+    "NaaNH",
+    "NtN",
+    "NdsN",
+    "NaaN",
+    "NsssN",
+    "NsOH",
+    "NdO",
+    "NssO",
+    "NaaO",
+    "SsCH3",
+    "SdCH2",
+    "SssCH2",
+    "StCH",
+    "SdsCH",
+    "SaaCH",
+    "SsssCH",
+    "StsC",
+    "SdssC",
+    "SssssC",
+    "SsNH2",
+    "SdNH",
+    "SssNH",
+    "SaaNH",
+    "StN",
+    "SdsN",
+    "SaaN",
+    "SsssN",
+    "SsOH",
+    "SdO",
+    "SssO",
+    "SaaO",
+    "MAXsCH3",
+    "MAXssCH2",
+    "MAXtCH",
+    "MAXdsCH",
+    "MAXaaCH",
+    "MAXsssCH",
+    "MAXtsC",
+    "MAXdssC",
+    "MAXssssC",
+    "MAXsNH2",
+    "MAXdNH",
+    "MAXssNH",
+    "MAXaaNH",
+    "MAXtN",
+    "MAXaaN",
+    "MAXsssN",
+    "MAXsOH",
+    "MAXdO",
+    "MAXssO",
+    "MAXaaO",
+    "MINsCH3",
+    "MINssCH2",
+    "MINtCH",
+    "MINdsCH",
+    "MINaaCH",
+    "MINsssCH",
+    "MINtsC",
+    "MINdssC",
+    "MINssssC",
+    "MINsNH2",
+    "MINdNH",
+    "MINssNH",
+    "MINaaNH",
+    "MINtN",
+    "MINaaN",
+    "MINsssN",
+    "MINsOH",
+    "MINdO",
+    "MINssO",
+    "MINaaO",
+    "ECIndex",
+    "ETA_alpha",
+    "AETA_alpha",
+    "ETA_shape_p",
+    "ETA_shape_y",
+    "ETA_shape_x",
+    "ETA_beta",
+    "AETA_beta",
+    "ETA_beta_s",
+    "AETA_beta_s",
+    "ETA_beta_ns",
+    "AETA_beta_ns",
+    "ETA_eta",
+    "AETA_eta",
+    "ETA_eta_L",
+    "AETA_eta_L",
+    "ETA_eta_R",
+    "AETA_eta_R",
+    "ETA_eta_RL",
+    "AETA_eta_RL",
+    "ETA_eta_F",
+    "AETA_eta_F",
+    "ETA_eta_FL",
+    "AETA_eta_FL",
+    "ETA_eta_B",
+    "AETA_eta_B",
+    "ETA_eta_BR",
+    "AETA_eta_BR",
+    "ETA_dAlpha_B",
+    "ETA_epsilon_1",
+    "ETA_epsilon_2",
+    "ETA_epsilon_3",
+    "ETA_epsilon_4",
+    "ETA_epsilon_5",
+    "ETA_dEpsilon_A",
+    "ETA_dEpsilon_B",
+    "ETA_dEpsilon_C",
+    "ETA_dEpsilon_D",
+    "ETA_dBeta",
+    "AETA_dBeta",
+    "ETA_psi_1",
+    "ETA_dPsi_A",
+    "ETA_dPsi_B",
+    "fragCpx",
+    "fMF",
+    "nHBAcc",
+    "nHBDon",
+    "IC0",
+    "IC1",
+    "IC2",
+    "IC3",
+    "IC4",
+    "IC5",
+    "TIC0",
+    "TIC1",
+    "TIC2",
+    "TIC3",
+    "TIC4",
+    "TIC5",
+    "SIC0",
+    "SIC1",
+    "SIC2",
+    "SIC3",
+    "SIC4",
+    "SIC5",
+    "BIC0",
+    "BIC1",
+    "BIC2",
+    "BIC3",
+    "BIC4",
+    "BIC5",
+    "CIC0",
+    "CIC1",
+    "CIC2",
+    "CIC3",
+    "CIC4",
+    "CIC5",
+    "MIC0",
+    "MIC1",
+    "MIC2",
+    "MIC3",
+    "MIC4",
+    "MIC5",
+    "ZMIC0",
+    "ZMIC1",
+    "ZMIC2",
+    "ZMIC3",
+    "ZMIC4",
+    "ZMIC5",
+    "Kier1",
+    "Kier2",
+    "Kier3",
+    "FilterItLogS",
+    "VMcGowan",
+    "LabuteASA",
+    "PEOE_VSA1",
+    "PEOE_VSA2",
+    "PEOE_VSA3",
+    "PEOE_VSA4",
+    "PEOE_VSA5",
+    "PEOE_VSA6",
+    "PEOE_VSA7",
+    "PEOE_VSA8",
+    "PEOE_VSA9",
+    "PEOE_VSA10",
+    "PEOE_VSA11",
+    "PEOE_VSA12",
+    "PEOE_VSA13",
+    "SMR_VSA1",
+    "SMR_VSA2",
+    "SMR_VSA3",
+    "SMR_VSA4",
+    "SMR_VSA5",
+    "SMR_VSA6",
+    "SMR_VSA7",
+    "SMR_VSA9",
+    "SlogP_VSA1",
+    "SlogP_VSA2",
+    "SlogP_VSA3",
+    "SlogP_VSA4",
+    "SlogP_VSA5",
+    "SlogP_VSA6",
+    "SlogP_VSA10",
+    "EState_VSA1",
+    "EState_VSA2",
+    "EState_VSA3",
+    "EState_VSA4",
+    "EState_VSA5",
+    "EState_VSA6",
+    "EState_VSA7",
+    "EState_VSA8",
+    "EState_VSA9",
+    "EState_VSA10",
+    "VSA_EState1",
+    "VSA_EState2",
+    "VSA_EState3",
+    "VSA_EState4",
+    "VSA_EState5",
+    "VSA_EState6",
+    "VSA_EState7",
+    "VSA_EState8",
+    "VSA_EState9",
+    "MDEC-11",
+    "MDEC-12",
+    "MDEC-13",
+    "MDEC-14",
+    "MDEC-22",
+    "MDEC-23",
+    "MDEC-24",
+    "MDEO-12",
+    "MDEN-11",
+    "MID",
+    "AMID",
+    "MID_h",
+    "AMID_h",
+    "MID_C",
+    "AMID_C",
+    "MID_N",
+    "AMID_N",
+    "MID_O",
+    "AMID_O",
+    "MPC2",
+    "MPC3",
+    "MPC4",
+    "TMPC10",
+    "piPC1",
+    "piPC2",
+    "piPC3",
+    "piPC4",
+    "TpiPC10",
+    "apol",
+    "bpol",
+    "nRing",
+    "n3Ring",
+    "n4Ring",
+    "n5Ring",
+    "nHRing",
+    "n3HRing",
+    "n4HRing",
+    "n5HRing",
+    "naRing",
+    "n5aRing",
+    "naHRing",
+    "n5aHRing",
+    "nARing",
+    "n3ARing",
+    "n4ARing",
+    "nAHRing",
+    "n3AHRing",
+    "n4AHRing",
+    "nRot",
+    "RotRatio",
+    "SLogP",
+    "SMR",
+    "TopoPSA(NO)",
+    "TopoPSA",
+    "GGI1",
+    "GGI2",
+    "GGI3",
+    "JGI1",
+    "JGI2",
+    "JGI3",
+    "JGT10",
+    "Diameter",
+    "Radius",
+    "TopoShapeIndex",
+    "PetitjeanIndex",
+    "Vabc",
+    "VAdjMat",
+    "MWC01",
+    "MWC02",
+    "MWC03",
+    "MWC04",
+    "MWC05",
+    "MWC06",
+    "MWC07",
+    "MWC08",
+    "MWC09",
+    "MWC10",
+    "TMWC10",
+    "SRW02",
+    "SRW03",
+    "SRW04",
+    "SRW05",
+    "SRW06",
+    "SRW07",
+    "SRW08",
+    "SRW09",
+    "SRW10",
+    "TSRW10",
+    "MW",
+    "AMW",
+    "WPath",
+    "WPol",
+    "Zagreb1",
+    "Zagreb2",
+    "mZagreb1",
+    "mZagreb2",
+]
+
+DEBUG = [
+    "MATS8pe",
+    "MATS1are",
+    "MATS2are",
+    "MATS3are",
+    "MATS4are",
+    "MATS5are",
+    "MATS6are",
+    "MATS7are",
+    "MATS8are",
+]
+
+DESCRIPTOR_SET_LOOKUP = dict(
+    optimized=SUBSET_947,
+    all=ALL_2D,
+    debug=DEBUG,
+)
diff --git a/fastprop/descriptors.py b/fastprop/descriptors.py
new file mode 100644
index 0000000..3154988
--- /dev/null
+++ b/fastprop/descriptors.py
@@ -0,0 +1,76 @@
+import datetime
+from importlib.metadata import version
+from time import perf_counter
+from typing import List, Union
+
+import numpy as np
+import pandas as pd
+import psutil
+from mordred import Calculator, Descriptor, descriptors, get_descriptors_in_module
+from packaging.version import Version
+from rdkit.Chem import rdchem
+
+from fastprop.defaults import init_logger
+
+logger = init_logger(__name__)
+
+
+def _descriptor_names_to_mordred_class(string_list: List[str] = [], include_3d: bool = False):
+    """Wraps a weird quirk of the mordred calculator library - descriptors must be passed
+    as their corresponding class, not their name string.
+
+    Args:
+        string_list (list[str], optional): Names of descriptors, leave empty to get all.
+        include_3d (bool, optional): Set True to ignore descriptors needing 3D coordinates. Defaults to False.
+    """
+    out = []
+    string_set = set(string_list)
+    if (input_length := len(string_list)) != (set_length := len(string_set)):
+        raise RuntimeError(f"Input contains {input_length - set_length} duplicate entires.")
+    __version__ = Version(version("mordredcommunity"))
+    for mdl in descriptors.all:
+        for Desc in get_descriptors_in_module(mdl, submodule=False):
+            for desc in Desc.preset(__version__):
+                if not string_list or desc.__str__() in string_list:
+                    if desc.require_3D and not include_3d:
+                        continue
+                    out.append(desc)
+    return out
+
+
+def _mols_to_desciptors(descriptors: List[Descriptor], rdkit_mols: List[rdchem.Mol]) -> np.ndarray:
+    """Wraps the mordred descriptor calculator.
+
+    Args:
+        descriptors (Mordred descriptor instances): Descriptors to calculate.
+        rdkit_mols (list[rdkit mols]): List of RDKit molecules.
+
+    Returns:
+        np.array: Calculated descriptors.
+    """
+    start = perf_counter()
+    mordred_calc = Calculator(descriptors)
+    logger.info("Calculating descriptors")
+    mordred_descs = np.array(list(mordred_calc.map(rdkit_mols, nproc=psutil.cpu_count(logical=True), quiet=False)))
+    logger.info(f"Descriptor calculation complete, elapsed time: {str(datetime.timedelta(seconds=perf_counter() - start))}")
+    return mordred_descs
+
+
+def get_descriptors(cache_filepath: Union[str, bool], descriptors: List[str], rdkit_mols: List[rdchem.Mol]) -> pd.DataFrame:
+    """Calculates requested descriptors for the given molecules, optionally writing to a cache file.
+
+    Args:
+        cache_filepath (str | bool): Filepath for cache, False to not cache.
+        descriptors (list[str]): Names of mordred-community descriptors to calculate.
+        rdkit_mols (list[rdchem.Mol]): Molecules.
+
+    Returns:
+        pd.DataFrame: Calculated descriptors.
+    """
+    d2c = _descriptor_names_to_mordred_class(descriptors)
+    out = pd.DataFrame(data=_mols_to_desciptors(d2c, rdkit_mols), columns=descriptors)
+    out = out.apply(pd.to_numeric, errors="coerce")
+    if cache_filepath:
+        out.to_csv(cache_filepath)
+        logger.info(f"Cached descriptors to {cache_filepath}")
+    return out
diff --git a/fastprop/fastprop_core.py b/fastprop/fastprop_core.py
deleted file mode 100644
index ebd1d36..0000000
--- a/fastprop/fastprop_core.py
+++ /dev/null
@@ -1,637 +0,0 @@
-"""
-fastprop - molecular property prediction with simple descriptors
-
-This file contains the core data handling and model definition functionality
-for the fastprop framework.
-"""
-
-import datetime
-import os
-import pickle
-import warnings
-from time import perf_counter
-from types import SimpleNamespace
-from typing import OrderedDict
-
-import numpy as np
-import pandas as pd
-import pytorch_lightning as pl
-import torch
-import yaml
-from astartes import train_val_test_split
-from astartes.molecules import train_val_test_split_molecules
-from pytorch_lightning.callbacks import ModelCheckpoint
-from pytorch_lightning.callbacks.early_stopping import EarlyStopping
-from pytorch_lightning.loggers import TensorBoardLogger
-from scipy.stats import ttest_ind
-from sklearn.impute import SimpleImputer
-from sklearn.metrics import mean_absolute_percentage_error as mape
-from sklearn.metrics import mean_squared_error as l2_error
-from sklearn.preprocessing import OneHotEncoder, StandardScaler
-from torch import distributed
-from torch.utils.data import Dataset as TorchDataset
-from torchmetrics.functional.classification import (
-    auroc,
-    binary_accuracy,
-    f1_score,
-    multiclass_accuracy,
-    multiclass_auroc,
-    multilabel_auroc,
-)
-from torchmetrics.functional.regression import r2_score
-
-from fastprop.defaults import init_logger
-
-logger = init_logger(__name__)
-
-warnings.filterwarnings(action="ignore", message=".*does not have many workers which may be a bottleneck.*")
-
-# device configuration
-DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
-NUM_WORKERS = 1
-# controls level of output
-NUM_VALIDATION_CHECKS = 20
-
-
-def _mock_inverse_transform(x):
-    return x
-
-
-class ArbitraryDataset(TorchDataset):
-    """Basic PyTorch dataset class.
-
-    Args:
-        TorchDataset (pytorch.Dataset): PyTorch's dataset class.
-    """
-
-    def __init__(self, data, targets):
-        """Initialize a basic torch-compatible data
-
-        Args:
-            data (torch.tensor): features
-            targets (torch.tensor): targets
-        """
-        self.data = data
-        self.length = len(targets)
-        self.targets = targets
-
-    def __len__(self):
-        return self.length
-
-    def __getitem__(self, index):
-        return self.data[index], self.targets[index]
-
-
-class fastprop(pl.LightningModule):
-    """Core fastprop LightningModule
-
-    Args:
-        pl (pl.LightningModule): Parent Lightning Module class.
-    """
-
-    def __init__(
-        self,
-        num_epochs,
-        input_size,
-        hidden_size,
-        readout_size,
-        learning_rate,
-        fnn_layers,
-        problem_type,
-        cleaned_data,
-        targets,
-        target_names,
-        batch_size,
-        random_seed,
-        train_size,
-        val_size,
-        test_size,
-        sampler,
-        smiles,
-        verbose=True,
-    ):
-        """Core fastprop model.
-
-        Args:
-            num_epochs (int): Maximum allowed number of training epochs.
-            input_size (int): Number of input neurons.
-            hidden_size (int): Number of neurons in the hidden layers.
-            readout_size (int): Number of targets to readout.
-            learning_rate (float): Learning rate for Adam.
-            fnn_layers (int): Number of layers in the FNN.
-            problem_type (str): Problem type, i.e. regression, multiclass, multilabel, or binary.
-            cleaned_data (numpy.ndarray): Descriptors with no missing values.
-            targets (numpy.ndarray): Scaled targets in the same order as the descriptors.
-            target_names (sequence): Sequence of names for the targets.
-            batch_size (int): Number of molecules per training batch in SGD.
-            random_seed (int): Seed for splitting.
-            train_size (float): Fraction of data for training, nonzero.
-            val_size (float): Fraction of data for validation, nonzero.
-            test_size (float): Fraction of data for test, nonzero.
-            sampler (string): Sampling approach passed to astartes, i.e. random, scaffold
-            smiles (sequence): Sequence of SMILES strings corresponding to the features.
-            verbose (bool, optional): Print extra information. Defaults to True.
-        """
-        super().__init__()
-        # used for data preparation and training
-        self.random_seed = random_seed
-        self.problem_type = problem_type
-        self.num_classes = readout_size if self.problem_type == "multiclass" else None
-
-        # used only for training
-        self.training_metric, self.overfitting_metric = fastprop.get_metrics(problem_type)
-        self.verbose = verbose
-        self.num_epochs = num_epochs
-        self.learning_rate = learning_rate
-        # fully-connected nn
-        layers = OrderedDict(
-            [
-                ("lin1", torch.nn.Linear(input_size, hidden_size)),
-                ("act1", torch.nn.ReLU()),
-            ]
-        )
-        for i in range(fnn_layers - 1):
-            layers[f"lin{i+2}"] = torch.nn.Linear(hidden_size, hidden_size)
-            layers[f"act{i+2}"] = torch.nn.ReLU()
-        self.fnn = torch.nn.Sequential(layers)
-        self.readout = torch.nn.Linear(hidden_size, readout_size)
-
-        # used only for data preparation
-        self.data = cleaned_data
-        self.targets = targets
-        self.batch_size = batch_size
-        self.sampler = sampler
-        self.smiles = smiles
-        self.train_size, self.val_size, self.test_size = train_size, val_size, test_size
-        self.train_idxs, self.val_idxs, self.test_idxs = None, None, None
-        # either using folds or random sampling, both of these will be incremented by the training loop
-        self.fold_number = 0
-        self.target_names = target_names
-
-        # add derived attributes to the hparams attribute manually so that they are saved with checkpoints
-        self.hparams["num_classes"] = self.num_classes
-        self.save_hyperparameters(ignore=("cleaned_data", "targets", "smiles"))
-
-    def _split(self):
-        """Sets self.*_idxs for training, validation, and test"""
-        logger.info(f"Sampling dataset with {self.sampler} sampler.")
-        split_kwargs = dict(
-            train_size=self.train_size,
-            val_size=self.val_size,
-            test_size=self.test_size,
-            sampler=self.sampler,
-            random_state=self.random_seed,
-            return_indices=True,
-        )
-        if self.sampler != "scaffold":
-            (
-                *_,
-                self.train_idxs,
-                self.val_idxs,
-                self.test_idxs,
-            ) = train_val_test_split(
-                self.data,
-                # flatten 1D targets
-                self.targets.flatten() if self.targets.shape[1] == 1 else self.targets,
-                **split_kwargs,
-            )
-        else:
-            (
-                *_,
-                self.train_idxs,
-                self.val_idxs,
-                self.test_idxs,
-            ) = train_val_test_split_molecules(self.smiles, **split_kwargs)
-
-    def setup(self, stage=None):
-        """Split and rescale the data."""
-        if stage == "fit":
-            self._split()
-            logger.info("Imputing and rescaling input features.")
-            # it is possible that the randomly chosen training set can have all entries missing a descriptor
-            # even if other members of the dataset are valued! They will be zeroed out
-            self.mean_imputer = SimpleImputer(missing_values=np.nan, strategy="mean", keep_empty_features=True)
-            self.mean_imputer.fit(self.data[self.train_idxs, :])
-            self.data = self.mean_imputer.transform(self.data)
-
-            # scale each column to unit variance
-            self.feature_scaler = StandardScaler()
-            self.feature_scaler.fit(self.data[self.train_idxs, :])
-            self.data = self.feature_scaler.transform(self.data)
-
-            # mock the scaler object for classification tasks
-            self.target_scaler = SimpleNamespace(n_features_in_=self.targets.shape[1], inverse_transform=_mock_inverse_transform)
-            if self.problem_type == "regression":
-                logger.info("Rescaling targets.")
-                self.target_scaler = StandardScaler()
-                self.target_scaler.fit(self.targets[self.train_idxs, :])
-                self.targets = self.target_scaler.transform(self.targets)
-            elif self.problem_type == "multiclass":
-                logger.info("One-hot encoding target values.")
-                self.target_scaler = OneHotEncoder(sparse_output=False)
-                self.targets = self.target_scaler.fit_transform(self.targets)
-
-            # finally, cast everything as needed
-            self.data = torch.tensor(self.data, dtype=torch.float32)
-            self.targets = torch.tensor(self.targets, dtype=torch.float32)
-
-    def _init_dataloader(self, shuffle, idxs):
-        """Helper method to initialize dataloaders.
-
-        Args:
-            shuffle (bool): Passed to torch's DataLoader
-            idxs (np.array): Indexes of overall dataset to include in dataloader.
-
-        Returns:
-            torch.utils.data.Dataloader: Dataloader instance.
-        """
-        return torch.utils.data.DataLoader(
-            ArbitraryDataset(
-                [self.data[i] for i in idxs],
-                [self.targets[i] for i in idxs],
-            ),
-            batch_size=self.batch_size,
-            shuffle=shuffle,
-            num_workers=NUM_WORKERS,
-            persistent_workers=True,
-        )
-
-    def train_dataloader(self):
-        return self._init_dataloader(True, self.train_idxs)
-
-    def val_dataloader(self):
-        return self._init_dataloader(False, self.val_idxs)
-
-    def test_dataloader(self):
-        return self._init_dataloader(False, self.test_idxs)
-
-    def get_metrics(problem_type):
-        """Get the metrics for training and early stopping based on the problem type.
-
-        Args:
-            problem_type (str): Regression, multilabel, multiclass, or binary.
-
-        Raises:
-            RuntimeError: Unsupported problem types
-
-        Returns:
-            str: names for the two metrics
-        """
-        if problem_type == "regression":
-            return "mse", "rmse"
-        elif problem_type == "multilabel":
-            return "bce", "auroc"
-        elif problem_type == "multiclass":
-            return "kldiv", "auroc"
-        elif problem_type == "binary":
-            return "bce", "accuracy"
-        else:
-            raise RuntimeError(f"Unsupported problem type '{problem_type}'!")
-
-    def forward(self, x):
-        x = self.fnn.forward(x)
-        x = self.readout(x)
-        return x
-
-    def log(self, name, value, **kwargs):
-        if in_distributed := distributed.is_initialized():
-            if not isinstance(value, torch.Tensor):
-                value = torch.tensor(value)
-            value = value.to(self.device)
-        return super().log(name, value, sync_dist=in_distributed, **kwargs)
-
-    def configure_optimizers(self):
-        optimizer = torch.optim.Adam(self.parameters(), lr=self.learning_rate)
-        return {"optimizer": optimizer}
-
-    def training_step(self, batch, batch_idx):
-        loss = self._machine_loss(batch, reduction="mean")
-        self.log(f"train_{self.training_metric}_loss", loss)
-        return loss
-
-    def validation_step(self, batch, batch_idx):
-        loss, _, y, y_hat = self._machine_loss(batch, return_all=True)
-        self.log(f"validation_{self.training_metric}_loss", loss)
-        self._human_loss(y_hat.detach().cpu(), y.detach().cpu(), "validation")
-        return loss
-
-    def test_step(self, batch, batch_idx):
-        loss, _, y, y_hat = self._machine_loss(batch, return_all=True)
-        self.log(f"test_{self.training_metric}_loss", loss)
-        self._human_loss(y_hat.detach().cpu(), y.detach().cpu(), "test")
-        return loss
-
-    def predict_step(self, X):
-        # calls forward, but applies the appropriate transforms and activations
-        X = self.mean_imputer.transform(X)
-        X = self.feature_scaler.transform(X)
-        X = torch.tensor(X, dtype=torch.float32, device=self.device)
-        with torch.inference_mode():
-            logits = self.forward(X)
-        if self.problem_type == "regression":
-            return self.target_scaler.inverse_transform(logits.detach().cpu())
-        elif self.problem_type in {"multilabel", "binary"}:
-            return torch.sigmoid(logits).detach().cpu()
-        elif self.problem_type == "multiclass":
-            return torch.nn.functional.softmax(logits, dim=1).detach().cpu()
-
-    def _machine_loss(self, batch, reduction="mean", return_all=False):
-        # reports the rescaled loss directly on the logits for computational efficiency
-        x, y = batch
-        y_hat = self.forward(x)
-        if self.problem_type == "regression":
-            loss = torch.nn.functional.mse_loss(y_hat, y, reduction=reduction)
-        elif self.problem_type in {"multilabel", "binary"}:
-            loss = torch.nn.functional.binary_cross_entropy_with_logits(y_hat, y, reduction=reduction)
-        else:
-            y_pred = torch.softmax(y_hat, dim=1)
-            loss = torch.nn.functional.kl_div(y_pred.log(), y, reduction="batchmean")
-        if not return_all:
-            return loss
-        else:
-            return loss, x, y, y_hat
-
-    def _human_loss(self, pred, real, name):
-        n_tasks = len(self.target_names)
-        # outputs the performance in more human-interpretable manner
-        # expensive so we don't do this during training
-        if self.problem_type == "regression":
-            rescaled_pred = self.target_scaler.inverse_transform(pred)
-            rescaled_truth = self.target_scaler.inverse_transform(real)
-            # report r2
-            if n_tasks == 1:
-                r2 = r2_score(torch.tensor(rescaled_pred), torch.tensor(rescaled_truth))
-                self.log(f"{name}_r2", r2)
-
-            # mean absolute percentage error
-            per_task_mape = np.abs(mape(rescaled_truth, rescaled_pred, multioutput="raw_values"))
-            if n_tasks == 1:
-                self.log(f"{name}_mape", np.mean(per_task_mape))
-            else:
-                self.log(f"{name}_mean_mape", np.mean(per_task_mape))
-                for target, value in zip(self.target_names, per_task_mape):
-                    self.log(f"{name}_mape_output_{target}", value)
-            # same, but weighted
-            # sklearn asks for an array of weights, but it actually just passes through to np.average which
-            # accepts weights of the same shape as the inputs
-            per_task_wmape = np.abs(mape(rescaled_truth, rescaled_pred, multioutput="raw_values", sample_weight=rescaled_truth))
-            if n_tasks == 1:
-                self.log(f"{name}_wmape", np.mean(per_task_wmape))
-            else:
-                self.log(f"{name}_mean_wmape", np.mean(per_task_wmape))
-                for target, value in zip(self.target_names, per_task_wmape):
-                    self.log(f"{name}_wmape_output_{target}", value)
-
-            # mean absolute error
-            all_loss = torch.nn.functional.l1_loss(torch.tensor(rescaled_pred), torch.tensor(rescaled_truth), reduction="none")
-            per_task_loss = all_loss.numpy().mean(axis=0)
-            if n_tasks == 1:
-                self.log(f"{name}_l1", np.mean(per_task_loss))
-                self.log(f"{name}_mdae", np.median(all_loss))
-            else:
-                self.log(f"{name}_l1_avg", np.mean(per_task_loss))
-                for target, value in zip(self.target_names, per_task_loss):
-                    self.log(f"{name}_l1_output_{target}", value)
-
-            # rmse
-            per_task_loss = l2_error(rescaled_truth, rescaled_pred, multioutput="raw_values", squared=False)
-            if n_tasks == 1:
-                self.log(f"{name}_rmse", np.mean(per_task_loss))
-            else:
-                self.log(f"{name}_rmse_avg", np.mean(per_task_loss))
-                for target, value in zip(self.target_names, per_task_loss):
-                    self.log(f"{name}_rmse_output_{target}", value)
-        else:
-            if self.problem_type == "multilabel":
-                pred_prob = torch.sigmoid(pred)
-                auroc_score = multilabel_auroc(pred_prob, real.int(), n_tasks, "macro")
-                self.log(f"{name}_auroc", auroc_score)
-            elif self.problem_type == "multiclass":
-                pred_prob = torch.nn.functional.softmax(pred, dim=1)
-                real_class = torch.tensor(self.target_scaler.inverse_transform(real.int()), dtype=torch.long).squeeze()
-                score = multiclass_auroc(pred, real_class, self.num_classes)
-                self.log(f"{name}_auroc", score)
-                score = multiclass_accuracy(pred, real_class, self.num_classes)
-                self.log(f"{name}_accuracy", score)
-            elif self.problem_type == "binary":
-                pred_prob = torch.sigmoid(pred)
-                acc = binary_accuracy(pred_prob, real)
-                self.log(f"{name}_accuracy", acc)
-                f1 = f1_score(pred_prob, real, "binary")
-                self.log(f"{name}_f1", f1)
-                auroc_score = auroc(pred_prob, real.int(), task="binary")
-                self.log(f"{name}_auroc", auroc_score)
-
-    def on_train_epoch_end(self) -> None:
-        # print progress every NUM_VALIDATION_CHECKS epochs, unless the total number of epochs is tiny (<NUM_VALIDATION_CHECKS)
-        if self.num_epochs > NUM_VALIDATION_CHECKS and (self.trainer.current_epoch + 1) % (self.num_epochs // NUM_VALIDATION_CHECKS) == 0:
-            if self.verbose:
-                logger.info(f"Epoch [{self.trainer.current_epoch + 1}/{self.num_epochs}]")
-
-
-def train_and_test(
-    outdir,
-    lightning_module,
-    patience=5,
-    verbose=True,
-    no_logs=False,
-    enable_checkpoints=True,
-):
-    """Run the lightning trainer loop on a given model.
-
-    Args:
-        outdir (str): Output directory for log files.
-        lightning_module (LightingModule): fastprop model architecture itself.
-        patience (int, optional): Maximum number of epochs to wait before stopping early. Defaults to 5.
-        verbose (bool, optional): Set to false for less output. Defaults to True.
-        no_logs (bool, optional): Set to true to disable logs. Defaults to False.
-        enable_checkpoints (bool, optional): Set to false to disable checkpoint writing. Defaults to True.
-
-    Returns:
-        list[dict{metric: score}]: Output of lightning model.test and model.validate
-    """
-    if not no_logs:
-        try:
-            repetition_number = len(os.listdir(os.path.join(outdir, "tensorboard_logs"))) + 1
-        except FileNotFoundError:
-            repetition_number = 1
-        tensorboard_logger = TensorBoardLogger(outdir, name="tensorboard_logs", version=f"repetition_{repetition_number}", default_hp_metric=False)
-
-    callbacks = [
-        EarlyStopping(
-            monitor=f"validation_{lightning_module.training_metric}_loss",
-            mode="min",
-            verbose=False,
-            patience=patience,
-        )
-    ]
-    if enable_checkpoints:
-        callbacks.append(
-            ModelCheckpoint(
-                monitor=f"validation_{lightning_module.training_metric}_loss",
-                dirpath=os.path.join(outdir, "checkpoints"),
-                filename=f"repetition-{repetition_number}" + "-{epoch:02d}-{val_loss:.2f}",
-                save_top_k=1,
-                mode="min",
-            )
-        )
-
-    trainer = pl.Trainer(
-        max_epochs=lightning_module.num_epochs,
-        accelerator=DEVICE,
-        enable_progress_bar=False,
-        enable_model_summary=verbose,
-        logger=False if no_logs else tensorboard_logger,
-        log_every_n_steps=0 if no_logs else 1,
-        enable_checkpointing=enable_checkpoints,
-        check_val_every_n_epoch=1,
-        callbacks=callbacks,
-        deterministic=True,
-    )
-
-    t1_start = perf_counter()
-    trainer.fit(lightning_module)
-    t1_stop = perf_counter()
-    if verbose:
-        logger.info("Elapsed time during training: " + str(datetime.timedelta(seconds=t1_stop - t1_start)))
-    validation_results = trainer.validate(lightning_module, verbose=False)
-    test_results = trainer.test(lightning_module, verbose=False)
-    if verbose:
-        validation_results_df = pd.DataFrame.from_records(validation_results, index=("value",))
-        logger.info("Displaying validation results for repetition %d:\n%s", repetition_number, validation_results_df.transpose().to_string())
-        test_results_df = pd.DataFrame.from_records(test_results, index=("value",))
-        logger.info("Displaying validation results for repetition %d:\n%s", repetition_number, test_results_df.transpose().to_string())
-    return test_results, validation_results
-
-
-def _training_loop(
-    number_repeats,
-    number_epochs,
-    input_size,
-    hidden_size,
-    readout_size,
-    learning_rate,
-    fnn_layers,
-    output_directory,
-    patience,
-    problem_type,
-    train_size,
-    val_size,
-    test_size,
-    sampler,
-    smiles,
-    cleaned_data,
-    targets,
-    target_names,
-    batch_size,
-    random_seed,
-    hopt=False,
-):
-    """Outer training loop to perform repeats.
-
-    Args:
-        number_repeats (int): Number of repetitions.
-        number_epochs (int): Maximum allowed number of training epochs.
-        input_size (int): Number of input neurons.
-        hidden_size (int): Number of neurons in the hidden layers.
-        readout_size (int): Number of targets to readout.
-        learning_rate (float): Learning rate for Adam.
-        fnn_layers (int): Number of layers in the FNN.
-        output_directory (str): Destination directory for file output.
-        patience (int, optional): Maximum number of epochs to wait before stopping early. Defaults to 5.
-        problem_type (str): Problem type, i.e. regression, multiclass, multilabel, or binary.
-        train_size (float): Fraction of data for training, nonzero.
-        val_size (float): Fraction of data for validation, nonzero.
-        test_size (float): Fraction of data for test, nonzero.
-        sampler (string): Sampling approach passed to astartes, i.e. random, scaffold
-        smiles (sequence): Sequence of SMILES strings corresponding to the features.
-        cleaned_data (numpy.ndarray): Descriptors with no missing values.
-        targets (numpy.ndarray): Scaled targets in the same order as the descriptors.
-        target_names (sequence): Sequence of names for the targets.
-        batch_size (int): Number of molecules per training batch in SGD.
-        random_seed (int): Seed for splitting.
-        hopt (bool, optional): Disables checkpointing and printing when True. Defaults to False.
-
-    Returns:
-        _type_: _description_
-    """
-    if not hopt:
-        logger.info(f"Run 'tensorboard --logdir {os.path.join(output_directory, 'tensorboard_logs')}' to track training progress.")
-    # hopt disables some printing from fastprop, as well as the training loop, disables logging, and disables writing checkpoints
-    all_test_results, all_validation_results = [], []
-    for i in range(number_repeats):
-        # reinitialize model
-        lightning_module = fastprop(
-            number_epochs,
-            input_size,
-            hidden_size,
-            readout_size,
-            learning_rate,
-            fnn_layers,
-            problem_type,
-            cleaned_data,
-            targets,
-            target_names,
-            batch_size,
-            random_seed,
-            train_size,
-            val_size,
-            test_size,
-            sampler,
-            smiles,
-            verbose=not hopt,
-        )
-        logger.info(f"Training model {i+1} of {number_repeats}")
-        test_results, validation_results = train_and_test(
-            output_directory,
-            lightning_module,
-            patience=patience,
-            verbose=not hopt,
-            no_logs=hopt,
-            enable_checkpoints=not hopt,
-        )
-        all_test_results.append(test_results[0])
-        all_validation_results.append(validation_results[0])
-        # save the scalers for later use
-        if not hopt:
-            with open(os.path.join(output_directory, "checkpoints", f"repetition_{i+1}_scalers.yml"), "w") as file:
-                file.write(
-                    yaml.dump(
-                        dict(
-                            target_scaler=pickle.dumps(lightning_module.target_scaler),
-                            mean_imputer=pickle.dumps(lightning_module.mean_imputer),
-                            feature_scaler=pickle.dumps(lightning_module.feature_scaler),
-                        ),
-                        sort_keys=False,
-                    )
-                )
-
-        # resample for repeat trials
-        if i + 1 < number_repeats:
-            random_seed += 1
-            # ensure that the model is re-initialized on the next iteration
-            del lightning_module
-
-    validation_results_df = pd.DataFrame.from_records(all_validation_results)
-    logger.info("Displaying validation results:\n%s", validation_results_df.describe().transpose().to_string())
-    test_results_df = pd.DataFrame.from_records(all_test_results)
-    logger.info("Displaying testing results:\n%s", test_results_df.describe().transpose().to_string())
-    if number_repeats > 1:
-        # regression results are reported per-task, so we might need to average a differently-named
-        # column
-        n_tasks = len(lightning_module.target_names)
-        addendum = "_avg" if (problem_type == "regression" and n_tasks > 1) else ""
-        ttest_result = ttest_ind(
-            test_results_df[f"test_{lightning_module.overfitting_metric}" + addendum].to_numpy(),
-            validation_results_df[f"validation_{lightning_module.overfitting_metric}" + addendum].to_numpy(),
-        )
-        if (p := ttest_result.pvalue) < 0.05:
-            logger.warn(
-                "Detected possible over/underfitting! 2-sided T-test between validation and testing"
-                f" {lightning_module.overfitting_metric} yielded {p=:.3f}<0.05. Consider changing patience."
-            )
-        else:
-            logger.info(f"2-sided T-test between validation and testing {lightning_module.overfitting_metric} yielded p value of {p=:.3f}>0.05.")
-    else:
-        logger.info("fastprop is unable to generate statistics to check for overfitting, consider increasing 'num_repeats' to at least 2.")
-    return test_results_df, validation_results_df
diff --git a/fastprop/hopt.py b/fastprop/hopt.py
deleted file mode 100644
index ba73984..0000000
--- a/fastprop/hopt.py
+++ /dev/null
@@ -1,186 +0,0 @@
-"""
-hopt.py
-
-Implements parallel hyperparameter optimization for fastprop using ray[tune] and Optuna.
-"""
-
-import logging
-import os
-
-import numpy as np
-import yaml
-
-from fastprop.defaults import init_logger
-from fastprop.fastprop_core import _training_loop, fastprop
-from fastprop.preprocessing import preprocess
-from fastprop.utils import _get_descs, load_from_csv
-
-tune, OptunaSearch = None, None
-try:
-    import ray
-    from ray import tune
-    from ray.train.torch import enable_reproducibility
-    from ray.tune.search.optuna import OptunaSearch
-except ImportError as ie:
-    hopt_error = ie
-
-logger = init_logger(__name__)
-
-
-CONFIG_FNAME = ".fastpropconfig"
-_fpath = os.path.join(os.path.dirname(__file__), CONFIG_FNAME)
-MODELS_PER_GPU, NUM_HOPT_TRIALS = 4, 64
-if os.path.exists(_fpath):
-    with open(_fpath) as file:
-        cfg = yaml.safe_load(file)
-        MODELS_PER_GPU = cfg.get("models_per_gpu", MODELS_PER_GPU)
-        NUM_HOPT_TRIALS = cfg.get("num_hopt_trials", NUM_HOPT_TRIALS)
-
-
-def hopt_fastprop(
-    output_directory,
-    input_file,
-    smiles_column,
-    target_columns,
-    descriptors="optimized",
-    enable_cache=True,
-    precomputed=None,
-    zero_variance_drop=True,
-    colinear_drop=False,
-    learning_rate=0.0001,
-    batch_size=2048,
-    number_epochs=1000,
-    number_repeats=1,
-    problem_type="regression",
-    checkpoint=None,
-    train_size=0.8,
-    val_size=0.1,
-    test_size=0.1,
-    sampler="random",
-    random_seed=0,
-    patience=5,
-    n_trials=NUM_HOPT_TRIALS,
-    n_parallel=MODELS_PER_GPU,
-):
-    if tune is None or OptunaSearch is None:
-        raise RuntimeError(
-            "Unable to import hyperparameter optimization dependencies, please install fastprop[hopt].\nOriginal error: " + str(hopt_error)
-        )
-    logging.getLogger("pytorch_lightning").setLevel(logging.WARNING)
-    if not os.path.exists(output_directory):
-        os.mkdir(output_directory)
-    targets, mols, smiles = load_from_csv(input_file, smiles_column, target_columns)
-    descs = _get_descs(precomputed, input_file, output_directory, descriptors, enable_cache, mols, as_df=True)
-
-    logger.info("Preprocessing features")
-    X = preprocess(descs, zero_variance_drop, colinear_drop).to_numpy()
-
-    input_size = X.shape[1]
-    readout_size = targets.shape[1] if problem_type != "multiclass" else (np.max(targets[:, 1]) + 1)
-
-    X_ref = ray.put(X)
-    targets_ref = ray.put(targets)
-    smiles_ref = ray.put(smiles)
-
-    _, metric = fastprop.get_metrics(problem_type)
-
-    # driver code of optimization
-    search_space = {
-        "hidden_size": tune.choice(range(100, 3001, 100)),
-        "fnn_layers": tune.choice(range(1, 6, 1)),
-    }
-    algo = OptunaSearch()
-    tuner = tune.Tuner(
-        tune.with_resources(
-            lambda trial: objective(
-                trial,
-                X_ref,
-                targets_ref,
-                smiles_ref,
-                input_size,
-                readout_size,
-                number_epochs,
-                learning_rate,
-                number_repeats,
-                patience,
-                problem_type,
-                train_size,
-                val_size,
-                test_size,
-                sampler,
-                target_columns,
-                batch_size,
-                random_seed,
-            ),
-            # run n_parallel models at the same time (leave 20% for system)
-            # don't specify cpus, and just let pl figure it out
-            resources={"gpu": (1 - 0.20) / n_parallel},
-        ),
-        tune_config=tune.TuneConfig(
-            metric=metric,
-            mode="min" if problem_type == "regression" else "max",
-            search_alg=algo,
-            max_concurrent_trials=n_parallel,
-            num_samples=n_trials,
-        ),
-        param_space=search_space,
-    )
-
-    results = tuner.fit()
-    best = results.get_best_result().config
-    logger.info(f"Best hyperparameters identified: {', '.join([key + ': ' + str(val) for key, val in best.items()])}")
-    return best
-
-
-def objective(
-    trial,
-    X_ref,
-    targets_ref,
-    smiles_ref,
-    input_size,
-    readout_size,
-    number_epochs,
-    learning_rate,
-    number_repeats,
-    patience,
-    problem_type,
-    train_size,
-    val_size,
-    test_size,
-    sampler,
-    target_columns,
-    batch_size,
-    random_seed,
-) -> float:
-    X = ray.get(X_ref)
-    targets = ray.get(targets_ref)
-    smiles = ray.get(smiles_ref)
-    enable_reproducibility(random_seed)
-    results_df, _ = _training_loop(
-        number_repeats,
-        number_epochs,
-        input_size,
-        trial["hidden_size"],
-        readout_size,
-        learning_rate,
-        trial["fnn_layers"],
-        None,  # output_directory ignored during hopt
-        patience,
-        problem_type,
-        train_size,
-        val_size,
-        test_size,
-        sampler,
-        smiles,
-        X,
-        targets,
-        target_columns,
-        batch_size,
-        random_seed,
-        hopt=True,
-    )
-    _, metric = fastprop.get_metrics(problem_type)
-    if readout_size == 1 or problem_type != "regression":
-        return {metric: results_df.describe().at["mean", f"test_{metric}"]}
-    else:
-        return {metric: results_df.describe().at["mean", f"test_{metric}_avg"]}
diff --git a/fastprop/io.py b/fastprop/io.py
new file mode 100644
index 0000000..3b61aa1
--- /dev/null
+++ b/fastprop/io.py
@@ -0,0 +1,51 @@
+from typing import List, Tuple
+
+import numpy as np
+import pandas as pd
+
+from fastprop.defaults import init_logger
+
+logger = init_logger(__name__)
+
+
+def read_input_csv(filepath: str, smiles_column: str, target_columns: List[str]) -> Tuple[np.ndarray, np.ndarray]:
+    """Read a CSV file provided by the user.
+
+    Args:
+        filepath (str): Filepath.
+        smiles_column (str): Column holding SMILES strings.
+        target_columns (list[str]): Column(s) with target values.
+
+    Raises:
+        RuntimeError: Missing columns name(s).
+
+    Returns:
+        tuple[np.ndarray, np.ndarray]: Targets and SMILES strings.
+    """
+    src = pd.read_csv(filepath)
+    try:
+        targets = pd.concat([src.pop(x) for x in target_columns], axis=1).to_numpy()
+        smiles = src.pop(smiles_column).to_numpy()
+    except KeyError as ke:
+        raise RuntimeError(f"Unable to find column(s) inside file {filepath}. Check spelling and file contents.") from ke
+
+    # keep everything 2D
+    if len(target_columns) == 1:
+        targets = targets.reshape(-1, 1)
+
+    return targets, smiles
+
+
+def load_saved_descriptors(fpath: str) -> np.ndarray:
+    """Loads cached descriptors as calculated previously, forcing missing to nan.
+
+    Args:
+        fpath (str): Filepath to CSV.
+
+    Returns:
+        np.ndarray: Loaded descriptors.
+    """
+    d = pd.read_csv(fpath, low_memory=False)
+    d = d.apply(pd.to_numeric, errors="coerce")
+    descs = d[d.columns[1:]].to_numpy(dtype=float)
+    return descs
diff --git a/fastprop/metrics.py b/fastprop/metrics.py
new file mode 100644
index 0000000..300922e
--- /dev/null
+++ b/fastprop/metrics.py
@@ -0,0 +1,102 @@
+"""
+This file wraps a number of loss functions and score functions so that
+they can be called with a unified interface after being packed into a
+dictionary.
+
+Names of each function indicate its contents, and all of them call out
+to external libraries with thorough documentation.
+"""
+
+import torch
+from sklearn.metrics import mean_absolute_percentage_error as mape
+from torchmetrics.functional.classification import (
+    accuracy,
+    auroc,
+    average_precision,
+    f1_score,
+)
+from torchmetrics.functional.regression import r2_score as tm_r2_score
+
+
+def r2_score(truth: torch.Tensor, prediction: torch.Tensor, ignored: None, multitask: bool = False):
+    return tm_r2_score(prediction, truth, multioutput="raw_values" if multitask else "uniform_average")
+
+
+def mean_absolute_percentage_error_score(truth: torch.Tensor, prediction: torch.Tensor, ignored: None, multitask: bool = False):
+    return mape(truth.numpy(force=True), prediction.numpy(force=True), multioutput="raw_values" if multitask else "uniform_average")
+
+
+def weighted_mean_absolute_percentage_error_score(truth: torch.Tensor, prediction: torch.Tensor, ignored: None, multitask: bool = False):
+    return mape(
+        truth.numpy(force=True),
+        prediction.numpy(force=True),
+        multioutput="raw_values" if multitask else "uniform_average",
+        sample_weight=truth.numpy(force=True),
+    )
+
+
+def mean_absolute_error_score(truth: torch.Tensor, prediction: torch.Tensor, ignored: None, multitask: bool = False):
+    return torch.nn.functional.l1_loss(prediction, truth, reduction="none" if multitask else "mean")
+
+
+def mean_squared_error_loss(truth: torch.Tensor, prediction: torch.Tensor, ignored: None, multitask: bool = False):
+    return torch.nn.functional.mse_loss(prediction, truth, reduction="none" if multitask else "mean")
+
+
+def root_mean_squared_error_loss(truth: torch.Tensor, prediction: torch.Tensor, ignored: None, multitask: bool = False):
+    return torch.sqrt(mean_squared_error_loss(truth, prediction, multitask))
+
+
+def binary_accuracy_score(truth: torch.Tensor, prediction: torch.Tensor, ignored: None, multitask: bool = False):
+    return accuracy(prediction, truth, task="binary")
+
+
+def binary_f1_score(truth: torch.Tensor, prediction: torch.Tensor, ignored: None, multitask: bool = False):
+    return f1_score(prediction, truth, task="binary")
+
+
+def binary_auroc(truth: torch.Tensor, prediction: torch.Tensor, ignored: None, multitask: bool = False):
+    return auroc(prediction, truth.int(), task="binary")
+
+
+def binary_average_precision(truth: torch.Tensor, prediction: torch.Tensor, ignored: None, multitask: bool = False):
+    return average_precision(prediction, truth.int(), task="binary")
+
+
+def multilabel_auroc(truth: torch.Tensor, prediction: torch.Tensor, num_labels: int):
+    return auroc(prediction, truth.int(), task="multilabel", num_labels=num_labels)
+
+
+def multilabel_average_precision(truth: torch.Tensor, prediction: torch.Tensor, num_labels: int):
+    return average_precision(prediction, truth.int(), task="multilabel", num_labels=num_labels, average="micro")
+
+
+def multilabel_f1_score(truth: torch.Tensor, prediction: torch.Tensor, num_labels: int):
+    return f1_score(prediction, truth.int(), task="multilabel", num_labels=num_labels, average="micro")
+
+
+def multiclass_auroc(truth: torch.Tensor, prediction: torch.Tensor, num_classes: int):
+    return auroc(prediction, torch.argmax(truth, dim=1), task="multiclass", num_classes=num_classes)
+
+
+SCORE_LOOKUP = {
+    "regression": (
+        r2_score,
+        mean_absolute_percentage_error_score,
+        weighted_mean_absolute_percentage_error_score,
+        mean_absolute_error_score,
+        root_mean_squared_error_loss,
+    ),
+    "binary": (
+        binary_accuracy_score,
+        binary_f1_score,
+        binary_auroc,
+        binary_average_precision,
+    ),
+    "multiclass": (multiclass_auroc,),
+    "multilabel": (
+        multilabel_auroc,
+        multilabel_average_precision,
+        multilabel_f1_score,
+    ),
+}
diff --git a/fastprop/model.py b/fastprop/model.py
new file mode 100644
index 0000000..8e2e1b5
--- /dev/null
+++ b/fastprop/model.py
@@ -0,0 +1,289 @@
+import datetime
+import glob
+import os
+from time import perf_counter
+from typing import List, Literal, Optional, OrderedDict, Tuple
+
+import pandas as pd
+import pytorch_lightning as pl
+import torch
+from pytorch_lightning.callbacks import ModelCheckpoint
+from pytorch_lightning.callbacks.early_stopping import EarlyStopping
+from pytorch_lightning.loggers import TensorBoardLogger
+from torch import distributed
+
+from fastprop.data import fastpropDataLoader, inverse_standard_scale, standard_scale
+from fastprop.defaults import init_logger
+from fastprop.metrics import SCORE_LOOKUP
+
+logger = init_logger(__name__)
+
+
+class fastprop(pl.LightningModule):
+    def __init__(
+        self,
+        input_size: int = 1613,
+        hidden_size: int = 1800,
+        readout_size: int = 1,
+        num_tasks: int = 1,
+        learning_rate: float = 0.001,
+        fnn_layers: int = 2,
+        problem_type: Literal["regression", "binary", "multiclass", "multilabel"] = "regression",
+        target_names: List[str] = [],
+        feature_means: Optional[torch.Tensor] = None,
+        feature_vars: Optional[torch.Tensor] = None,
+        target_means: Optional[torch.Tensor] = None,
+        target_vars: Optional[torch.Tensor] = None,
+    ):
+        """fastprop descriptor-based regression FNN.
+
+        Args:
+            input_size (int, optional): Number of features. Defaults to 1613.
+            hidden_size (int, optional): Height of hidden layers. Defaults to 1800.
+            readout_size (int, optional): Number of output neurons. Defaults to 1.
+            num_tasks (int, optional): Number of distinct tasks. Defaults to 1.
+            learning_rate (float, optional): Learning rate for SGD. Defaults to 0.001.
+            fnn_layers (int, optional): Number of hidden layers. Defaults to 2.
+            problem_type (Literal["regression", "binary", "multiclass", "multilabel"], optional): Type of training task. Defaults to "regression".
+            target_names (list[str], optional): Names for targets in dataset, blank for simple integer names. Defaults to [].
+            feature_means (Optional[torch.Tensor], optional): Means for scaling features in regression. Defaults to None.
+            feature_vars (Optional[torch.Tensor], optional): Variances for scaling features in regression. Defaults to None.
+            target_means (Optional[torch.Tensor], optional): Means for scaling targets during inference. Defaults to None.
+            target_vars (Optional[torch.Tensor], optional): Means for scaling targets during inference. Defaults to None.
+        """
+        super().__init__()
+        self.n_tasks = num_tasks
+        self.register_buffer("feature_means", feature_means)
+        self.register_buffer("feature_vars", feature_vars)
+        self.register_buffer("target_means", target_means)
+        self.register_buffer("target_vars", target_vars)
+        self.problem_type = problem_type
+        self.training_metric = fastprop.get_metric(problem_type)
+        self.learning_rate = learning_rate
+        self.target_names = target_names
+
+        # fully-connected nn
+        layers = OrderedDict(
+            [
+                ("lin1", torch.nn.Linear(input_size, hidden_size)),
+                ("act1", torch.nn.ReLU()),
+            ]
+        )
+        for i in range(fnn_layers - 1):
+            layers[f"lin{i+2}"] = torch.nn.Linear(hidden_size, hidden_size)
+            layers[f"act{i+2}"] = torch.nn.ReLU()
+        self.fnn = torch.nn.Sequential(layers)
+        self.readout = torch.nn.Linear(hidden_size, readout_size)
+
+        self.save_hyperparameters()
+
+    def configure_optimizers(self):
+        """See https://lightning.ai/docs/pytorch/stable/common/optimization.html
+
+        Returns:
+            dict: Optimizer name and instance.
+        """
+        return {"optimizer": torch.optim.Adam(self.parameters(), lr=self.learning_rate)}
+
+    def setup(self, stage=None):
+        """Fill the target names if none were provided
+
+        Args:
+            stage (str, optional): Step of pipeline. Defaults to None.
+        """
+        if stage == "fit":
+            if len(self.target_names) == 0:
+                self.target_names = [f"task_{i}" for i in range(self.n_tasks)]
+
+    def get_metric(problem_type: str):
+        """Get the metric for training and early stopping based on the problem type.
+
+        Args:
+            problem_type (str): Regression, multilabel, multiclass, or binary.
+
+        Raises:
+            RuntimeError: Unsupported problem types
+
+        Returns:
+            str: names for the two metrics
+        """
+        if problem_type == "regression":
+            return "mse"
+        elif problem_type in {"multilabel", "binary"}:
+            return "bce"
+        elif problem_type == "multiclass":
+            return "kldiv"
+        else:
+            raise RuntimeError(f"Unsupported problem type '{problem_type}'!")
+
+    def forward(self, x):
+        """Returns the logits (i.e. without activation or scaling) for a given batch of features.
+
+        Args:
+            x (torch.Tensor): Input features.
+
+        Returns:
+            torch.Tensor: Logits.
+        """
+        x = self.fnn.forward(x)
+        x = self.readout(x)
+        return x
+
+    def log(self, name, value, **kwargs):
+        """Wrap the parent PyTorch Lightning log function to automatically detect DDP."""
+        return super().log(name, value, sync_dist=distributed.is_initialized(), **kwargs)
+
+    def training_step(self, batch, batch_idx):
+        loss, _ = self._machine_loss(batch)
+        self.log(f"train_{self.training_metric}_loss", loss)
+        return loss
+
+    def validation_step(self, batch, batch_idx):
+        loss, y_hat = self._machine_loss(batch)
+        self.log(f"validation_{self.training_metric}_loss", loss)
+        self._human_loss(y_hat, batch, "validation")
+        return loss
+
+    def test_step(self, batch, batch_idx):
+        loss, y_hat = self._machine_loss(batch)
+        self.log(f"test_{self.training_metric}_loss", loss)
+        self._human_loss(y_hat, batch, "test")
+        return loss
+
+    def predict_step(self, batch: Tuple[torch.Tensor]):
+        """Applies feature scaling and appropriate activation function to a Tensor of descriptors.
+
+        Args:
+            batch (tuple[torch.Tensor]): Unscaled descriptors.
+
+        Returns:
+            torch.Tensor: Predictions.
+        """
+        descriptors = batch[0]
+        if self.feature_means is not None and self.feature_vars is not None:
+            descriptors = standard_scale(descriptors, self.feature_means, self.feature_vars)
+        with torch.inference_mode():
+            logits = self.forward(descriptors)
+        if self.problem_type == "regression":
+            logits = inverse_standard_scale(logits, self.target_means, self.target_vars)
+            return logits
+        elif self.problem_type in {"multilabel", "binary"}:
+            return torch.sigmoid(logits)
+        elif self.problem_type == "multiclass":
+            return torch.nn.functional.softmax(logits, dim=1)
+
+    def _machine_loss(self, batch):
+        """Reports the loss without undoing scaling for greater efficiency."""
+        x, y = batch
+        y_hat = self.forward(x)
+        if self.problem_type == "regression":
+            loss = torch.nn.functional.mse_loss(y_hat, y, reduction="mean")
+        elif self.problem_type in {"multilabel", "binary"}:
+            loss = torch.nn.functional.binary_cross_entropy_with_logits(y_hat, y, reduction="mean")
+        else:
+            y_pred = torch.softmax(y_hat, dim=1)
+            loss = torch.nn.functional.kl_div(y_pred.log(), y.float(), reduction="batchmean")
+        return loss, y_hat
+
+    def _human_loss(self, pred, batch, name):
+        """Reports human-interpretable loss metrics.
+
+        Args:
+            pred (torch.Tensor): Network logits.
+            batch (tuple[torch.Tensor, torch.Tensor]): Inputs and targets
+            name (str): Name under which to log the results.
+        """
+        truth = batch[1]
+        if self.problem_type == "regression" and self.target_means is not None and self.target_vars is not None:
+            pred = inverse_standard_scale(pred, self.target_means, self.target_vars)
+            truth = inverse_standard_scale(truth, self.target_means, self.target_vars)
+        # get probability from logits
+        elif self.problem_type in {"binary", "multilabel"}:
+            pred = torch.sigmoid(pred)
+        else:
+            pred = torch.nn.functional.softmax(pred, dim=1)
+
+        for metric in SCORE_LOOKUP[self.problem_type]:
+            self.log(f"{name}_{metric.__name__}", metric(truth, pred, self.readout.out_features))
+            if "multi" not in self.problem_type and self.n_tasks > 1:
+                per_task_metric = metric(truth, pred, True)
+                for target, value in zip(self.target_names, per_task_metric):
+                    self.log(f"{name}_{target}_{metric.__name__}", value)
+
+
+def train_and_test(
+    output_directory: str,
+    fastprop_model: fastprop,
+    train_dataloader: fastpropDataLoader,
+    val_dataloader: fastpropDataLoader,
+    test_dataloader: fastpropDataLoader,
+    number_epochs: int = 30,
+    patience: int = 5,
+):
+    """Run a single train/validate and test iteration.
+
+    Args:
+        output_directory (str): Filepath to write logs and checkpoints.
+        fastprop_model (fastprop): fastprop LightningModule instance.
+        train_dataloader (fastpropDataLoader): Training data.
+        val_dataloader (fastpropDataLoader): Validation data.
+        test_dataloader (fastpropDataLoader): Testing data.
+        number_epochs (int, optional): Maximum number of epochs for training. Defaults to 30.
+        patience (int, optional): Number of epochs for early stopping. Defaults to 5.
+
+    Returns:
+        _type_: _description_
+    """
+    try:
+        repetition_number = len(os.listdir(os.path.join(output_directory, "tensorboard_logs"))) + 1
+    except FileNotFoundError:
+        repetition_number = 1
+    tensorboard_logger = TensorBoardLogger(
+        output_directory,
+        name="tensorboard_logs",
+        version=f"repetition_{repetition_number}",
+        default_hp_metric=False,
+    )
+
+    callbacks = [
+        EarlyStopping(
+            monitor=f"validation_{fastprop_model.training_metric}_loss",
+            mode="min",
+            verbose=False,
+            patience=patience,
+        ),
+        ModelCheckpoint(
+            monitor=f"validation_{fastprop_model.training_metric}_loss",
+            dirpath=os.path.join(output_directory, "checkpoints"),
+            filename=f"repetition-{repetition_number}" + "-{epoch:02d}-{val_loss:.2f}",
+            save_top_k=1,
+            mode="min",
+        ),
+    ]
+
+    trainer = pl.Trainer(
+        max_epochs=number_epochs,
+        enable_progress_bar=True,
+        enable_model_summary=True,
+        logger=tensorboard_logger,
+        log_every_n_steps=1,
+        enable_checkpointing=True,
+        check_val_every_n_epoch=1,
+        callbacks=callbacks,
+    )
+
+    t1_start = perf_counter()
+    trainer.fit(fastprop_model, train_dataloader, val_dataloader)
+    t1_stop = perf_counter()
+    logger.info("Elapsed time during training: " + str(datetime.timedelta(seconds=t1_stop - t1_start)))
+    checkpoints_list = glob.glob(os.path.join(output_directory, "checkpoints", "*.ckpt"))
+    latest_file = max(checkpoints_list, key=os.path.getctime)
+    logger.info(f"Reloading best model from checkpoint file: {latest_file}")
+    fastprop_model = fastprop.load_from_checkpoint(latest_file)
+    validation_results = trainer.validate(fastprop_model, val_dataloader, verbose=False)
+    test_results = trainer.test(fastprop_model, test_dataloader, verbose=False)
+    validation_results_df = pd.DataFrame.from_records(validation_results, index=("value",))
+    logger.info("Displaying validation results for repetition %d:\n%s", repetition_number, validation_results_df.transpose().to_string())
+    test_results_df = pd.DataFrame.from_records(test_results, index=("value",))
+    logger.info("Displaying validation results for repetition %d:\n%s", repetition_number, test_results_df.transpose().to_string())
+    return test_results, validation_results
diff --git a/fastprop/predict.py b/fastprop/predict.py
deleted file mode 100644
index 403a807..0000000
--- a/fastprop/predict.py
+++ /dev/null
@@ -1,82 +0,0 @@
-import os
-import pickle
-
-import numpy as np
-import pandas as pd
-import yaml
-from rdkit import Chem
-
-from fastprop.defaults import init_logger
-from fastprop.utils import calculate_mordred_desciptors
-from fastprop.utils.select_descriptors import mordred_descriptors_from_strings
-
-from .fastprop_core import fastprop
-
-logger = init_logger(__name__)
-
-
-def _safe_yaml_open(dir, fname):
-    try:
-        with open(os.path.join(dir, fname)) as file:
-            return yaml.safe_load(file)
-    except FileNotFoundError:
-        logger.error(f"checkpoints directory is missing '{fname}'. Re-execute training.")
-
-
-def predict_fastprop(checkpoints_dir, smiles, input_file, output=None):
-    """Prediction CLI.
-
-    Loads a model and runs inference on the input.
-
-    Args:
-        checkpoints_dir (str): 'checkpoints' directory from a previous fastprop train.
-        smiles (str or list[str]): SMILES strings for prediction.
-        input_file (str): Input file containing only SMILES strings for prediction.
-        output (str or None): Either save to a file or just print result.
-    """
-    if input_file:
-        raise NotImplementedError("TODO: please pass as command line options, loading from file is a WIP")
-    if type(smiles) is str:
-        smiles = [smiles]
-    checkpoint_dir_contents = os.listdir(checkpoints_dir)
-    config_dict = _safe_yaml_open(checkpoints_dir, "fastprop_config.yml")
-    descs = calculate_mordred_desciptors(
-        mordred_descriptors_from_strings(config_dict["descriptors"]),
-        [Chem.MolFromSmiles(i) for i in smiles],
-        n_procs=0,  # ignored for "strategy='low-memory'"
-        strategy="low-memory",
-    )
-    all_models = []
-    for checkpoint in checkpoint_dir_contents:
-        if not checkpoint.endswith(".ckpt"):
-            continue
-        model = fastprop.load_from_checkpoint(
-            os.path.join(checkpoints_dir, checkpoint),
-            cleaned_data=None,
-            targets=None,
-            smiles=None,
-        )
-        scalers = _safe_yaml_open(checkpoints_dir, f"repetition_{checkpoint.split('-')[1]}_scalers.yml")
-        model.target_scaler = pickle.loads(scalers["target_scaler"])
-        model.mean_imputer = pickle.loads(scalers["mean_imputer"])
-        model.feature_scaler = pickle.loads(scalers["feature_scaler"])
-        all_models.append(model)
-    # axis: contents
-    # 0: smiles
-    # 1: predictions
-    # 2: per-model
-    all_predictions = np.stack([model.predict_step(descs) for model in all_models], axis=2)
-    perf = np.mean(all_predictions, axis=2)
-    err = np.std(all_predictions, axis=2)
-    # interleave the columns of these arrays, thanks stackoverflow.com/a/75519265
-    res = np.empty((len(perf), perf.shape[1] * 2), dtype=perf.dtype)
-    res[:, 0::2] = perf
-    res[:, 1::2] = err
-    column_names = []
-    for target in config_dict["targets"]:
-        column_names.extend([target, target + "_stdev"])
-    out = pd.DataFrame(res, columns=column_names, index=smiles)
-    if output is None:
-        print("\n", out)
-    else:
-        out.to_csv(output)
diff --git a/fastprop/preprocessing.py b/fastprop/preprocessing.py
deleted file mode 100644
index 1429d4f..0000000
--- a/fastprop/preprocessing.py
+++ /dev/null
@@ -1,34 +0,0 @@
-# given a filepath and some target columns,
-# retrieve the data as numpy arrays
-
-
-import pandas as pd
-from sklearn.feature_selection import VarianceThreshold
-
-from fastprop.defaults import init_logger
-
-logger = init_logger(__name__)
-
-
-def preprocess(descriptors, zero_variance_drop=False, colinear_drop=False):
-    # drop missing features - if this is direct output from mordred, missing values are
-    # strings describing why they are missing. If this is output from fastprop.utils.load_daved_desc
-    # the missing descriptors are nan. To deal with the former case, force all str->nan
-    descriptors: pd.DataFrame
-    descriptors = descriptors.apply(pd.to_numeric, errors="coerce")
-    descriptors = descriptors.dropna(axis=1, how="all")
-
-    if zero_variance_drop:
-        # drop invariant features
-        var_scaler = VarianceThreshold(threshold=0).set_output(transform="pandas")
-        # exclude from the returned feature scalers - the columns which should be dropped are done
-        # automatically by not calculating them in the first place during prediction.
-        descriptors = var_scaler.fit_transform(descriptors)
-        logger.info(f"size after invariant feature removal: {descriptors.shape}")
-
-    if colinear_drop:
-        raise NotImplementedError("TODO")
-
-    logger.info(f"Size after preprocessing: {descriptors.shape}")
-
-    return descriptors
diff --git a/fastprop/train.py b/fastprop/train.py
deleted file mode 100644
index f39b8c7..0000000
--- a/fastprop/train.py
+++ /dev/null
@@ -1,99 +0,0 @@
-import datetime
-import logging
-import os
-
-import numpy as np
-import yaml
-from lightning.pytorch import seed_everything
-
-from fastprop import fastprop_core
-from fastprop.defaults import _init_loggers, init_logger
-from fastprop.preprocessing import preprocess
-
-# choose the descriptor set absed on the args
-from fastprop.utils import _get_descs, load_from_csv
-
-logger = init_logger(__name__)
-
-
-def train_fastprop(
-    output_directory,
-    input_file,
-    smiles_column,
-    target_columns,
-    descriptors="optimized",
-    enable_cache=True,
-    precomputed=None,
-    zero_variance_drop=False,
-    colinear_drop=False,
-    fnn_layers=2,
-    hidden_size=1800,
-    learning_rate=0.0001,
-    batch_size=2048,
-    number_epochs=100,
-    number_repeats=1,
-    problem_type="regression",
-    checkpoint=None,
-    train_size=0.8,
-    val_size=0.1,
-    test_size=0.1,
-    sampler="random",
-    random_seed=0,
-    patience=5,
-):
-    """Driver function for automatic training.
-
-    See the fastprop documentation or CLI --help for details on each argument.
-    """
-    if checkpoint is not None:
-        raise RuntimeError("TODO: Restarting from checkpoint not currently supported. Exiting.")
-    # make output directories
-    if not os.path.exists(output_directory):
-        os.mkdir(output_directory)
-    output_subdirectory = os.path.join(output_directory, f"fastprop_{int(datetime.datetime.utcnow().timestamp())}")
-    os.mkdir(output_subdirectory)
-    os.mkdir(os.path.join(output_subdirectory, "checkpoints"))
-    _init_loggers(output_subdirectory)
-    logging.getLogger("pytorch_lightning").setLevel(logging.INFO)
-    seed_everything(random_seed)
-    targets, mols, smiles = load_from_csv(input_file, smiles_column, target_columns)
-    descs = _get_descs(precomputed, input_file, output_directory, descriptors, enable_cache, mols, as_df=True)
-
-    logger.info("Preprocessing features")
-    X = preprocess(descs, zero_variance_drop, colinear_drop)
-
-    # write information needed for feature importance, prediction, etc. into the checkpoints directory for later use
-    with open(os.path.join(output_subdirectory, "checkpoints", "fastprop_config.yml"), "w") as file:
-        file.write(
-            yaml.dump(
-                dict(smiles=smiles_column, targets=target_columns, descriptors=X.columns.to_list()),
-                sort_keys=False,
-            )
-        )
-
-    input_size = X.shape[1]
-    readout_size = targets.shape[1] if problem_type != "multiclass" else int(np.max(targets) + 1)
-
-    return fastprop_core._training_loop(
-        number_repeats,
-        number_epochs,
-        input_size,
-        hidden_size,
-        readout_size,
-        learning_rate,
-        fnn_layers,
-        output_subdirectory,
-        patience,
-        problem_type,
-        train_size,
-        val_size,
-        test_size,
-        sampler,
-        smiles,
-        X.to_numpy(),
-        targets,
-        target_columns,
-        batch_size,
-        random_seed,
-        hopt=False,
-    )
diff --git a/fastprop/utils/__init__.py b/fastprop/utils/__init__.py
deleted file mode 100644
index f468dd6..0000000
--- a/fastprop/utils/__init__.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from .calculate_descriptors import _get_descs, calculate_mordred_desciptors
-from .descriptor_lists import ALL_2D, SUBSET_947, descriptors_lookup
-from .load_data import load_from_csv, load_saved_desc
-from .select_descriptors import mordred_descriptors_from_strings
-from .validate_config import validate_config
-
-__all__ = [
-    "_get_descs",
-    "calculate_mordred_desciptors",
-    "ALL_2D",
-    "SUBSET_947",
-    "descriptors_lookup",
-    "load_from_csv",
-    "load_saved_desc",
-    "mordred_descriptors_from_strings",
-    "validate_config",
-]
diff --git a/fastprop/utils/calculate_descriptors.py b/fastprop/utils/calculate_descriptors.py
deleted file mode 100644
index 28882f0..0000000
--- a/fastprop/utils/calculate_descriptors.py
+++ /dev/null
@@ -1,111 +0,0 @@
-import datetime
-import os
-from multiprocessing import Pool
-from pathlib import Path
-from time import perf_counter
-from typing import Literal
-
-import numpy as np
-import pandas as pd
-import psutil
-from mordred import Calculator
-
-from fastprop.defaults import init_logger
-
-from .descriptor_lists import descriptors_lookup
-from .load_data import load_saved_desc
-from .select_descriptors import mordred_descriptors_from_strings
-
-logger = init_logger(__name__)
-
-
-# mordred tried to avoid instantiating multiple Calculator classes, which makes
-# the parallelism slower but more memory efficient. We do this manually:
-def _f(in_tuple):
-    mordred_calc = Calculator(in_tuple[2], ignore_3D=in_tuple[3])
-    mordred_descs = np.array(list(mordred_calc.map(in_tuple[0], nproc=1, quiet=in_tuple[1])))
-    return mordred_descs
-
-
-def calculate_mordred_desciptors(descriptors, rdkit_mols, n_procs: int = 2, strategy: Literal["fast", "low-memory"] = "fast", ignore_3d=True):
-    """Wraps the mordred descriptor calculator.
-
-    Args:
-        descriptors (Mordred descriptor instances): Descriptors to calculate
-        rdkit_mols (list[rdkit mols]): List of RDKit molecules.
-        n_procs (int): Number of parallel processes. Defaults to 2.
-        strategy (Literal["fast", "low-memory", optional): Parallelization strategy. Defaults to "fast".
-        ignore_3d (bool, optional): Include 3D descriptors, if in given list. Defaults to True.
-
-    Raises:
-        RuntimeError: Invalid choice of parallel strategy.
-
-    Returns:
-        np.array: Calculated descriptors.
-    """
-    start = perf_counter()
-    # descriptors should be a list of mordred descriptors classes
-    if strategy not in {"fast", "low-memory"}:
-        raise RuntimeError(f"Strategy {strategy} not supported, only 'fast' and 'low-memory'.")
-
-    mordred_descs = None
-    logger.info(f"Calculating descriptors using {strategy=}")
-    if strategy == "fast":
-        # higher level parallelism - uses more memory
-        # TODO: subdivide batches further to avoid large communication bottleneck after all descriptors are calculated
-        batches = np.array_split(rdkit_mols, n_procs)
-        # let the root process show a progress bar, since array split will make
-        # that one the largest
-        # convert to starmap to avoid having to duplicate the list of descriptor classes - too much communication (?)
-        to_procs = [(batch, bool(i), descriptors, ignore_3d) for i, batch in enumerate(batches)]
-        with Pool(n_procs) as p:
-            mordred_descs = np.vstack(p.map(_f, to_procs, 1))
-    else:
-        # mordred parallelism
-        mordred_calc = Calculator(descriptors, ignore_3D=ignore_3d)
-        mordred_descs = np.array(list(mordred_calc.map(rdkit_mols, nproc=psutil.cpu_count(logical=True), quiet=False)))
-    logger.info(f"Descriptor calculation complete, elapsed time: {str(datetime.timedelta(seconds=perf_counter() - start))}")
-    return mordred_descs
-
-
-def _get_descs(precomputed, input_file, output_directory, descriptors, enable_cache, mols, as_df=False):
-    """Loads descriptors according to the user-specified configuration.
-
-    This is a 'hidden' function since the caching logic is specific to fastprop.
-
-    Args:
-        precomputed (str): Use precomputed descriptors if str is.
-        input_file (str): Filepath of input data.
-        output_directory (str): Destination directory for caching.
-        descriptors (list): fastprop set of descriptors to calculate.
-        enable_cache (bool): Allow/disallow caching mechanism.
-        mols (list): RDKit molecules.
-        as_df (bool): Set to true to return the result as a pandas DataFrame. Defaults to False.
-    """
-    descs = None
-    if precomputed:
-        del mols
-        logger.info(f"Loading precomputed descriptors from {precomputed}.")
-        descs = load_saved_desc(precomputed)
-    else:
-        in_name = Path(input_file).stem
-        # cached descriptors, which contains (1) cached (2) source filename (3) types of descriptors (4) timestamp when file was last touched
-        cache_file = os.path.join(output_directory, "cached_" + in_name + "_" + descriptors + "_" + str(int(os.stat(input_file).st_ctime)) + ".csv")
-
-        if os.path.exists(cache_file) and enable_cache:
-            logger.info(f"Found cached descriptor data at {cache_file}, loading instead of recalculating.")
-            descs = load_saved_desc(cache_file)
-        else:
-            d2c = mordred_descriptors_from_strings(descriptors_lookup[descriptors])
-            # use all the cpus available
-            descs = calculate_mordred_desciptors(d2c, mols, psutil.cpu_count(logical=False), "fast")
-            # cache these
-            if enable_cache:
-                d = pd.DataFrame(descs)
-                d.to_csv(cache_file)
-                logger.info(f"Cached descriptors to {cache_file}.")
-    if as_df:
-        if precomputed:  # specifying descriptors' names not required
-            return pd.DataFrame(data=descs)
-        return pd.DataFrame(data=descs, columns=descriptors_lookup[descriptors])
-    return descs
diff --git a/fastprop/utils/descriptor_lists.py b/fastprop/utils/descriptor_lists.py
deleted file mode 100644
index e344cc7..0000000
--- a/fastprop/utils/descriptor_lists.py
+++ /dev/null
@@ -1,2574 +0,0 @@
-# Complete list of 2D descriptors implemented in Mordred as well as a subset
-# of those descriptors which are often useful for fastprop after preprocessing
-
-ALL_2D = [
-    "ABC",
-    "ABCGG",
-    "nAcid",
-    "nBase",
-    "SpAbs_A",
-    "SpMax_A",
-    "SpDiam_A",
-    "SpAD_A",
-    "SpMAD_A",
-    "LogEE_A",
-    "VE1_A",
-    "VE2_A",
-    "VE3_A",
-    "VR1_A",
-    "VR2_A",
-    "VR3_A",
-    "nAromAtom",
-    "nAromBond",
-    "nAtom",
-    "nHeavyAtom",
-    "nSpiro",
-    "nBridgehead",
-    "nHetero",
-    "nH",
-    "nB",
-    "nC",
-    "nN",
-    "nO",
-    "nS",
-    "nP",
-    "nF",
-    "nCl",
-    "nBr",
-    "nI",
-    "nX",
-    "ATS0dv",
-    "ATS1dv",
-    "ATS2dv",
-    "ATS3dv",
-    "ATS4dv",
-    "ATS5dv",
-    "ATS6dv",
-    "ATS7dv",
-    "ATS8dv",
-    "ATS0d",
-    "ATS1d",
-    "ATS2d",
-    "ATS3d",
-    "ATS4d",
-    "ATS5d",
-    "ATS6d",
-    "ATS7d",
-    "ATS8d",
-    "ATS0s",
-    "ATS1s",
-    "ATS2s",
-    "ATS3s",
-    "ATS4s",
-    "ATS5s",
-    "ATS6s",
-    "ATS7s",
-    "ATS8s",
-    "ATS0Z",
-    "ATS1Z",
-    "ATS2Z",
-    "ATS3Z",
-    "ATS4Z",
-    "ATS5Z",
-    "ATS6Z",
-    "ATS7Z",
-    "ATS8Z",
-    "ATS0m",
-    "ATS1m",
-    "ATS2m",
-    "ATS3m",
-    "ATS4m",
-    "ATS5m",
-    "ATS6m",
-    "ATS7m",
-    "ATS8m",
-    "ATS0v",
-    "ATS1v",
-    "ATS2v",
-    "ATS3v",
-    "ATS4v",
-    "ATS5v",
-    "ATS6v",
-    "ATS7v",
-    "ATS8v",
-    "ATS0se",
-    "ATS1se",
-    "ATS2se",
-    "ATS3se",
-    "ATS4se",
-    "ATS5se",
-    "ATS6se",
-    "ATS7se",
-    "ATS8se",
-    "ATS0pe",
-    "ATS1pe",
-    "ATS2pe",
-    "ATS3pe",
-    "ATS4pe",
-    "ATS5pe",
-    "ATS6pe",
-    "ATS7pe",
-    "ATS8pe",
-    "ATS0are",
-    "ATS1are",
-    "ATS2are",
-    "ATS3are",
-    "ATS4are",
-    "ATS5are",
-    "ATS6are",
-    "ATS7are",
-    "ATS8are",
-    "ATS0p",
-    "ATS1p",
-    "ATS2p",
-    "ATS3p",
-    "ATS4p",
-    "ATS5p",
-    "ATS6p",
-    "ATS7p",
-    "ATS8p",
-    "ATS0i",
-    "ATS1i",
-    "ATS2i",
-    "ATS3i",
-    "ATS4i",
-    "ATS5i",
-    "ATS6i",
-    "ATS7i",
-    "ATS8i",
-    "AATS0dv",
-    "AATS1dv",
-    "AATS2dv",
-    "AATS3dv",
-    "AATS4dv",
-    "AATS5dv",
-    "AATS6dv",
-    "AATS7dv",
-    "AATS8dv",
-    "AATS0d",
-    "AATS1d",
-    "AATS2d",
-    "AATS3d",
-    "AATS4d",
-    "AATS5d",
-    "AATS6d",
-    "AATS7d",
-    "AATS8d",
-    "AATS0s",
-    "AATS1s",
-    "AATS2s",
-    "AATS3s",
-    "AATS4s",
-    "AATS5s",
-    "AATS6s",
-    "AATS7s",
-    "AATS8s",
-    "AATS0Z",
-    "AATS1Z",
-    "AATS2Z",
-    "AATS3Z",
-    "AATS4Z",
-    "AATS5Z",
-    "AATS6Z",
-    "AATS7Z",
-    "AATS8Z",
-    "AATS0m",
-    "AATS1m",
-    "AATS2m",
-    "AATS3m",
-    "AATS4m",
-    "AATS5m",
-    "AATS6m",
-    "AATS7m",
-    "AATS8m",
-    "AATS0v",
-    "AATS1v",
-    "AATS2v",
-    "AATS3v",
-    "AATS4v",
-    "AATS5v",
-    "AATS6v",
-    "AATS7v",
-    "AATS8v",
-    "AATS0se",
-    "AATS1se",
-    "AATS2se",
-    "AATS3se",
-    "AATS4se",
-    "AATS5se",
-    "AATS6se",
-    "AATS7se",
-    "AATS8se",
-    "AATS0pe",
-    "AATS1pe",
-    "AATS2pe",
-    "AATS3pe",
-    "AATS4pe",
-    "AATS5pe",
-    "AATS6pe",
-    "AATS7pe",
-    "AATS8pe",
-    "AATS0are",
-    "AATS1are",
-    "AATS2are",
-    "AATS3are",
-    "AATS4are",
-    "AATS5are",
-    "AATS6are",
-    "AATS7are",
-    "AATS8are",
-    "AATS0p",
-    "AATS1p",
-    "AATS2p",
-    "AATS3p",
-    "AATS4p",
-    "AATS5p",
-    "AATS6p",
-    "AATS7p",
-    "AATS8p",
-    "AATS0i",
-    "AATS1i",
-    "AATS2i",
-    "AATS3i",
-    "AATS4i",
-    "AATS5i",
-    "AATS6i",
-    "AATS7i",
-    "AATS8i",
-    "ATSC0c",
-    "ATSC1c",
-    "ATSC2c",
-    "ATSC3c",
-    "ATSC4c",
-    "ATSC5c",
-    "ATSC6c",
-    "ATSC7c",
-    "ATSC8c",
-    "ATSC0dv",
-    "ATSC1dv",
-    "ATSC2dv",
-    "ATSC3dv",
-    "ATSC4dv",
-    "ATSC5dv",
-    "ATSC6dv",
-    "ATSC7dv",
-    "ATSC8dv",
-    "ATSC0d",
-    "ATSC1d",
-    "ATSC2d",
-    "ATSC3d",
-    "ATSC4d",
-    "ATSC5d",
-    "ATSC6d",
-    "ATSC7d",
-    "ATSC8d",
-    "ATSC0s",
-    "ATSC1s",
-    "ATSC2s",
-    "ATSC3s",
-    "ATSC4s",
-    "ATSC5s",
-    "ATSC6s",
-    "ATSC7s",
-    "ATSC8s",
-    "ATSC0Z",
-    "ATSC1Z",
-    "ATSC2Z",
-    "ATSC3Z",
-    "ATSC4Z",
-    "ATSC5Z",
-    "ATSC6Z",
-    "ATSC7Z",
-    "ATSC8Z",
-    "ATSC0m",
-    "ATSC1m",
-    "ATSC2m",
-    "ATSC3m",
-    "ATSC4m",
-    "ATSC5m",
-    "ATSC6m",
-    "ATSC7m",
-    "ATSC8m",
-    "ATSC0v",
-    "ATSC1v",
-    "ATSC2v",
-    "ATSC3v",
-    "ATSC4v",
-    "ATSC5v",
-    "ATSC6v",
-    "ATSC7v",
-    "ATSC8v",
-    "ATSC0se",
-    "ATSC1se",
-    "ATSC2se",
-    "ATSC3se",
-    "ATSC4se",
-    "ATSC5se",
-    "ATSC6se",
-    "ATSC7se",
-    "ATSC8se",
-    "ATSC0pe",
-    "ATSC1pe",
-    "ATSC2pe",
-    "ATSC3pe",
-    "ATSC4pe",
-    "ATSC5pe",
-    "ATSC6pe",
-    "ATSC7pe",
-    "ATSC8pe",
-    "ATSC0are",
-    "ATSC1are",
-    "ATSC2are",
-    "ATSC3are",
-    "ATSC4are",
-    "ATSC5are",
-    "ATSC6are",
-    "ATSC7are",
-    "ATSC8are",
-    "ATSC0p",
-    "ATSC1p",
-    "ATSC2p",
-    "ATSC3p",
-    "ATSC4p",
-    "ATSC5p",
-    "ATSC6p",
-    "ATSC7p",
-    "ATSC8p",
-    "ATSC0i",
-    "ATSC1i",
-    "ATSC2i",
-    "ATSC3i",
-    "ATSC4i",
-    "ATSC5i",
-    "ATSC6i",
-    "ATSC7i",
-    "ATSC8i",
-    "AATSC0c",
-    "AATSC1c",
-    "AATSC2c",
-    "AATSC3c",
-    "AATSC4c",
-    "AATSC5c",
-    "AATSC6c",
-    "AATSC7c",
-    "AATSC8c",
-    "AATSC0dv",
-    "AATSC1dv",
-    "AATSC2dv",
-    "AATSC3dv",
-    "AATSC4dv",
-    "AATSC5dv",
-    "AATSC6dv",
-    "AATSC7dv",
-    "AATSC8dv",
-    "AATSC0d",
-    "AATSC1d",
-    "AATSC2d",
-    "AATSC3d",
-    "AATSC4d",
-    "AATSC5d",
-    "AATSC6d",
-    "AATSC7d",
-    "AATSC8d",
-    "AATSC0s",
-    "AATSC1s",
-    "AATSC2s",
-    "AATSC3s",
-    "AATSC4s",
-    "AATSC5s",
-    "AATSC6s",
-    "AATSC7s",
-    "AATSC8s",
-    "AATSC0Z",
-    "AATSC1Z",
-    "AATSC2Z",
-    "AATSC3Z",
-    "AATSC4Z",
-    "AATSC5Z",
-    "AATSC6Z",
-    "AATSC7Z",
-    "AATSC8Z",
-    "AATSC0m",
-    "AATSC1m",
-    "AATSC2m",
-    "AATSC3m",
-    "AATSC4m",
-    "AATSC5m",
-    "AATSC6m",
-    "AATSC7m",
-    "AATSC8m",
-    "AATSC0v",
-    "AATSC1v",
-    "AATSC2v",
-    "AATSC3v",
-    "AATSC4v",
-    "AATSC5v",
-    "AATSC6v",
-    "AATSC7v",
-    "AATSC8v",
-    "AATSC0se",
-    "AATSC1se",
-    "AATSC2se",
-    "AATSC3se",
-    "AATSC4se",
-    "AATSC5se",
-    "AATSC6se",
-    "AATSC7se",
-    "AATSC8se",
-    "AATSC0pe",
-    "AATSC1pe",
-    "AATSC2pe",
-    "AATSC3pe",
-    "AATSC4pe",
-    "AATSC5pe",
-    "AATSC6pe",
-    "AATSC7pe",
-    "AATSC8pe",
-    "AATSC0are",
-    "AATSC1are",
-    "AATSC2are",
-    "AATSC3are",
-    "AATSC4are",
-    "AATSC5are",
-    "AATSC6are",
-    "AATSC7are",
-    "AATSC8are",
-    "AATSC0p",
-    "AATSC1p",
-    "AATSC2p",
-    "AATSC3p",
-    "AATSC4p",
-    "AATSC5p",
-    "AATSC6p",
-    "AATSC7p",
-    "AATSC8p",
-    "AATSC0i",
-    "AATSC1i",
-    "AATSC2i",
-    "AATSC3i",
-    "AATSC4i",
-    "AATSC5i",
-    "AATSC6i",
-    "AATSC7i",
-    "AATSC8i",
-    "MATS1c",
-    "MATS2c",
-    "MATS3c",
-    "MATS4c",
-    "MATS5c",
-    "MATS6c",
-    "MATS7c",
-    "MATS8c",
-    "MATS1dv",
-    "MATS2dv",
-    "MATS3dv",
-    "MATS4dv",
-    "MATS5dv",
-    "MATS6dv",
-    "MATS7dv",
-    "MATS8dv",
-    "MATS1d",
-    "MATS2d",
-    "MATS3d",
-    "MATS4d",
-    "MATS5d",
-    "MATS6d",
-    "MATS7d",
-    "MATS8d",
-    "MATS1s",
-    "MATS2s",
-    "MATS3s",
-    "MATS4s",
-    "MATS5s",
-    "MATS6s",
-    "MATS7s",
-    "MATS8s",
-    "MATS1Z",
-    "MATS2Z",
-    "MATS3Z",
-    "MATS4Z",
-    "MATS5Z",
-    "MATS6Z",
-    "MATS7Z",
-    "MATS8Z",
-    "MATS1m",
-    "MATS2m",
-    "MATS3m",
-    "MATS4m",
-    "MATS5m",
-    "MATS6m",
-    "MATS7m",
-    "MATS8m",
-    "MATS1v",
-    "MATS2v",
-    "MATS3v",
-    "MATS4v",
-    "MATS5v",
-    "MATS6v",
-    "MATS7v",
-    "MATS8v",
-    "MATS1se",
-    "MATS2se",
-    "MATS3se",
-    "MATS4se",
-    "MATS5se",
-    "MATS6se",
-    "MATS7se",
-    "MATS8se",
-    "MATS1pe",
-    "MATS2pe",
-    "MATS3pe",
-    "MATS4pe",
-    "MATS5pe",
-    "MATS6pe",
-    "MATS7pe",
-    "MATS8pe",
-    "MATS1are",
-    "MATS2are",
-    "MATS3are",
-    "MATS4are",
-    "MATS5are",
-    "MATS6are",
-    "MATS7are",
-    "MATS8are",
-    "MATS1p",
-    "MATS2p",
-    "MATS3p",
-    "MATS4p",
-    "MATS5p",
-    "MATS6p",
-    "MATS7p",
-    "MATS8p",
-    "MATS1i",
-    "MATS2i",
-    "MATS3i",
-    "MATS4i",
-    "MATS5i",
-    "MATS6i",
-    "MATS7i",
-    "MATS8i",
-    "GATS1c",
-    "GATS2c",
-    "GATS3c",
-    "GATS4c",
-    "GATS5c",
-    "GATS6c",
-    "GATS7c",
-    "GATS8c",
-    "GATS1dv",
-    "GATS2dv",
-    "GATS3dv",
-    "GATS4dv",
-    "GATS5dv",
-    "GATS6dv",
-    "GATS7dv",
-    "GATS8dv",
-    "GATS1d",
-    "GATS2d",
-    "GATS3d",
-    "GATS4d",
-    "GATS5d",
-    "GATS6d",
-    "GATS7d",
-    "GATS8d",
-    "GATS1s",
-    "GATS2s",
-    "GATS3s",
-    "GATS4s",
-    "GATS5s",
-    "GATS6s",
-    "GATS7s",
-    "GATS8s",
-    "GATS1Z",
-    "GATS2Z",
-    "GATS3Z",
-    "GATS4Z",
-    "GATS5Z",
-    "GATS6Z",
-    "GATS7Z",
-    "GATS8Z",
-    "GATS1m",
-    "GATS2m",
-    "GATS3m",
-    "GATS4m",
-    "GATS5m",
-    "GATS6m",
-    "GATS7m",
-    "GATS8m",
-    "GATS1v",
-    "GATS2v",
-    "GATS3v",
-    "GATS4v",
-    "GATS5v",
-    "GATS6v",
-    "GATS7v",
-    "GATS8v",
-    "GATS1se",
-    "GATS2se",
-    "GATS3se",
-    "GATS4se",
-    "GATS5se",
-    "GATS6se",
-    "GATS7se",
-    "GATS8se",
-    "GATS1pe",
-    "GATS2pe",
-    "GATS3pe",
-    "GATS4pe",
-    "GATS5pe",
-    "GATS6pe",
-    "GATS7pe",
-    "GATS8pe",
-    "GATS1are",
-    "GATS2are",
-    "GATS3are",
-    "GATS4are",
-    "GATS5are",
-    "GATS6are",
-    "GATS7are",
-    "GATS8are",
-    "GATS1p",
-    "GATS2p",
-    "GATS3p",
-    "GATS4p",
-    "GATS5p",
-    "GATS6p",
-    "GATS7p",
-    "GATS8p",
-    "GATS1i",
-    "GATS2i",
-    "GATS3i",
-    "GATS4i",
-    "GATS5i",
-    "GATS6i",
-    "GATS7i",
-    "GATS8i",
-    "BCUTc-1h",
-    "BCUTc-1l",
-    "BCUTdv-1h",
-    "BCUTdv-1l",
-    "BCUTd-1h",
-    "BCUTd-1l",
-    "BCUTs-1h",
-    "BCUTs-1l",
-    "BCUTZ-1h",
-    "BCUTZ-1l",
-    "BCUTm-1h",
-    "BCUTm-1l",
-    "BCUTv-1h",
-    "BCUTv-1l",
-    "BCUTse-1h",
-    "BCUTse-1l",
-    "BCUTpe-1h",
-    "BCUTpe-1l",
-    "BCUTare-1h",
-    "BCUTare-1l",
-    "BCUTp-1h",
-    "BCUTp-1l",
-    "BCUTi-1h",
-    "BCUTi-1l",
-    "BalabanJ",
-    "SpAbs_DzZ",
-    "SpMax_DzZ",
-    "SpDiam_DzZ",
-    "SpAD_DzZ",
-    "SpMAD_DzZ",
-    "LogEE_DzZ",
-    "SM1_DzZ",
-    "VE1_DzZ",
-    "VE2_DzZ",
-    "VE3_DzZ",
-    "VR1_DzZ",
-    "VR2_DzZ",
-    "VR3_DzZ",
-    "SpAbs_Dzm",
-    "SpMax_Dzm",
-    "SpDiam_Dzm",
-    "SpAD_Dzm",
-    "SpMAD_Dzm",
-    "LogEE_Dzm",
-    "SM1_Dzm",
-    "VE1_Dzm",
-    "VE2_Dzm",
-    "VE3_Dzm",
-    "VR1_Dzm",
-    "VR2_Dzm",
-    "VR3_Dzm",
-    "SpAbs_Dzv",
-    "SpMax_Dzv",
-    "SpDiam_Dzv",
-    "SpAD_Dzv",
-    "SpMAD_Dzv",
-    "LogEE_Dzv",
-    "SM1_Dzv",
-    "VE1_Dzv",
-    "VE2_Dzv",
-    "VE3_Dzv",
-    "VR1_Dzv",
-    "VR2_Dzv",
-    "VR3_Dzv",
-    "SpAbs_Dzse",
-    "SpMax_Dzse",
-    "SpDiam_Dzse",
-    "SpAD_Dzse",
-    "SpMAD_Dzse",
-    "LogEE_Dzse",
-    "SM1_Dzse",
-    "VE1_Dzse",
-    "VE2_Dzse",
-    "VE3_Dzse",
-    "VR1_Dzse",
-    "VR2_Dzse",
-    "VR3_Dzse",
-    "SpAbs_Dzpe",
-    "SpMax_Dzpe",
-    "SpDiam_Dzpe",
-    "SpAD_Dzpe",
-    "SpMAD_Dzpe",
-    "LogEE_Dzpe",
-    "SM1_Dzpe",
-    "VE1_Dzpe",
-    "VE2_Dzpe",
-    "VE3_Dzpe",
-    "VR1_Dzpe",
-    "VR2_Dzpe",
-    "VR3_Dzpe",
-    "SpAbs_Dzare",
-    "SpMax_Dzare",
-    "SpDiam_Dzare",
-    "SpAD_Dzare",
-    "SpMAD_Dzare",
-    "LogEE_Dzare",
-    "SM1_Dzare",
-    "VE1_Dzare",
-    "VE2_Dzare",
-    "VE3_Dzare",
-    "VR1_Dzare",
-    "VR2_Dzare",
-    "VR3_Dzare",
-    "SpAbs_Dzp",
-    "SpMax_Dzp",
-    "SpDiam_Dzp",
-    "SpAD_Dzp",
-    "SpMAD_Dzp",
-    "LogEE_Dzp",
-    "SM1_Dzp",
-    "VE1_Dzp",
-    "VE2_Dzp",
-    "VE3_Dzp",
-    "VR1_Dzp",
-    "VR2_Dzp",
-    "VR3_Dzp",
-    "SpAbs_Dzi",
-    "SpMax_Dzi",
-    "SpDiam_Dzi",
-    "SpAD_Dzi",
-    "SpMAD_Dzi",
-    "LogEE_Dzi",
-    "SM1_Dzi",
-    "VE1_Dzi",
-    "VE2_Dzi",
-    "VE3_Dzi",
-    "VR1_Dzi",
-    "VR2_Dzi",
-    "VR3_Dzi",
-    "BertzCT",
-    "nBonds",
-    "nBondsO",
-    "nBondsS",
-    "nBondsD",
-    "nBondsT",
-    "nBondsA",
-    "nBondsM",
-    "nBondsKS",
-    "nBondsKD",
-    "RNCG",
-    "RPCG",
-    "C1SP1",
-    "C2SP1",
-    "C1SP2",
-    "C2SP2",
-    "C3SP2",
-    "C1SP3",
-    "C2SP3",
-    "C3SP3",
-    "C4SP3",
-    "HybRatio",
-    "FCSP3",
-    "Xch-3d",
-    "Xch-4d",
-    "Xch-5d",
-    "Xch-6d",
-    "Xch-7d",
-    "Xch-3dv",
-    "Xch-4dv",
-    "Xch-5dv",
-    "Xch-6dv",
-    "Xch-7dv",
-    "Xc-3d",
-    "Xc-4d",
-    "Xc-5d",
-    "Xc-6d",
-    "Xc-3dv",
-    "Xc-4dv",
-    "Xc-5dv",
-    "Xc-6dv",
-    "Xpc-4d",
-    "Xpc-5d",
-    "Xpc-6d",
-    "Xpc-4dv",
-    "Xpc-5dv",
-    "Xpc-6dv",
-    "Xp-0d",
-    "Xp-1d",
-    "Xp-2d",
-    "Xp-3d",
-    "Xp-4d",
-    "Xp-5d",
-    "Xp-6d",
-    "Xp-7d",
-    "AXp-0d",
-    "AXp-1d",
-    "AXp-2d",
-    "AXp-3d",
-    "AXp-4d",
-    "AXp-5d",
-    "AXp-6d",
-    "AXp-7d",
-    "Xp-0dv",
-    "Xp-1dv",
-    "Xp-2dv",
-    "Xp-3dv",
-    "Xp-4dv",
-    "Xp-5dv",
-    "Xp-6dv",
-    "Xp-7dv",
-    "AXp-0dv",
-    "AXp-1dv",
-    "AXp-2dv",
-    "AXp-3dv",
-    "AXp-4dv",
-    "AXp-5dv",
-    "AXp-6dv",
-    "AXp-7dv",
-    "SZ",
-    "Sm",
-    "Sv",
-    "Sse",
-    "Spe",
-    "Sare",
-    "Sp",
-    "Si",
-    "MZ",
-    "Mm",
-    "Mv",
-    "Mse",
-    "Mpe",
-    "Mare",
-    "Mp",
-    "Mi",
-    "SpAbs_Dt",
-    "SpMax_Dt",
-    "SpDiam_Dt",
-    "SpAD_Dt",
-    "SpMAD_Dt",
-    "LogEE_Dt",
-    "SM1_Dt",
-    "VE1_Dt",
-    "VE2_Dt",
-    "VE3_Dt",
-    "VR1_Dt",
-    "VR2_Dt",
-    "VR3_Dt",
-    "DetourIndex",
-    "SpAbs_D",
-    "SpMax_D",
-    "SpDiam_D",
-    "SpAD_D",
-    "SpMAD_D",
-    "LogEE_D",
-    "VE1_D",
-    "VE2_D",
-    "VE3_D",
-    "VR1_D",
-    "VR2_D",
-    "VR3_D",
-    "NsLi",
-    "NssBe",
-    "NssssBe",
-    "NssBH",
-    "NsssB",
-    "NssssB",
-    "NsCH3",
-    "NdCH2",
-    "NssCH2",
-    "NtCH",
-    "NdsCH",
-    "NaaCH",
-    "NsssCH",
-    "NddC",
-    "NtsC",
-    "NdssC",
-    "NaasC",
-    "NaaaC",
-    "NssssC",
-    "NsNH3",
-    "NsNH2",
-    "NssNH2",
-    "NdNH",
-    "NssNH",
-    "NaaNH",
-    "NtN",
-    "NsssNH",
-    "NdsN",
-    "NaaN",
-    "NsssN",
-    "NddsN",
-    "NaasN",
-    "NssssN",
-    "NsOH",
-    "NdO",
-    "NssO",
-    "NaaO",
-    "NsF",
-    "NsSiH3",
-    "NssSiH2",
-    "NsssSiH",
-    "NssssSi",
-    "NsPH2",
-    "NssPH",
-    "NsssP",
-    "NdsssP",
-    "NsssssP",
-    "NsSH",
-    "NdS",
-    "NssS",
-    "NaaS",
-    "NdssS",
-    "NddssS",
-    "NsCl",
-    "NsGeH3",
-    "NssGeH2",
-    "NsssGeH",
-    "NssssGe",
-    "NsAsH2",
-    "NssAsH",
-    "NsssAs",
-    "NsssdAs",
-    "NsssssAs",
-    "NsSeH",
-    "NdSe",
-    "NssSe",
-    "NaaSe",
-    "NdssSe",
-    "NddssSe",
-    "NsBr",
-    "NsSnH3",
-    "NssSnH2",
-    "NsssSnH",
-    "NssssSn",
-    "NsI",
-    "NsPbH3",
-    "NssPbH2",
-    "NsssPbH",
-    "NssssPb",
-    "SsLi",
-    "SssBe",
-    "SssssBe",
-    "SssBH",
-    "SsssB",
-    "SssssB",
-    "SsCH3",
-    "SdCH2",
-    "SssCH2",
-    "StCH",
-    "SdsCH",
-    "SaaCH",
-    "SsssCH",
-    "SddC",
-    "StsC",
-    "SdssC",
-    "SaasC",
-    "SaaaC",
-    "SssssC",
-    "SsNH3",
-    "SsNH2",
-    "SssNH2",
-    "SdNH",
-    "SssNH",
-    "SaaNH",
-    "StN",
-    "SsssNH",
-    "SdsN",
-    "SaaN",
-    "SsssN",
-    "SddsN",
-    "SaasN",
-    "SssssN",
-    "SsOH",
-    "SdO",
-    "SssO",
-    "SaaO",
-    "SsF",
-    "SsSiH3",
-    "SssSiH2",
-    "SsssSiH",
-    "SssssSi",
-    "SsPH2",
-    "SssPH",
-    "SsssP",
-    "SdsssP",
-    "SsssssP",
-    "SsSH",
-    "SdS",
-    "SssS",
-    "SaaS",
-    "SdssS",
-    "SddssS",
-    "SsCl",
-    "SsGeH3",
-    "SssGeH2",
-    "SsssGeH",
-    "SssssGe",
-    "SsAsH2",
-    "SssAsH",
-    "SsssAs",
-    "SsssdAs",
-    "SsssssAs",
-    "SsSeH",
-    "SdSe",
-    "SssSe",
-    "SaaSe",
-    "SdssSe",
-    "SddssSe",
-    "SsBr",
-    "SsSnH3",
-    "SssSnH2",
-    "SsssSnH",
-    "SssssSn",
-    "SsI",
-    "SsPbH3",
-    "SssPbH2",
-    "SsssPbH",
-    "SssssPb",
-    "MAXsLi",
-    "MAXssBe",
-    "MAXssssBe",
-    "MAXssBH",
-    "MAXsssB",
-    "MAXssssB",
-    "MAXsCH3",
-    "MAXdCH2",
-    "MAXssCH2",
-    "MAXtCH",
-    "MAXdsCH",
-    "MAXaaCH",
-    "MAXsssCH",
-    "MAXddC",
-    "MAXtsC",
-    "MAXdssC",
-    "MAXaasC",
-    "MAXaaaC",
-    "MAXssssC",
-    "MAXsNH3",
-    "MAXsNH2",
-    "MAXssNH2",
-    "MAXdNH",
-    "MAXssNH",
-    "MAXaaNH",
-    "MAXtN",
-    "MAXsssNH",
-    "MAXdsN",
-    "MAXaaN",
-    "MAXsssN",
-    "MAXddsN",
-    "MAXaasN",
-    "MAXssssN",
-    "MAXsOH",
-    "MAXdO",
-    "MAXssO",
-    "MAXaaO",
-    "MAXsF",
-    "MAXsSiH3",
-    "MAXssSiH2",
-    "MAXsssSiH",
-    "MAXssssSi",
-    "MAXsPH2",
-    "MAXssPH",
-    "MAXsssP",
-    "MAXdsssP",
-    "MAXsssssP",
-    "MAXsSH",
-    "MAXdS",
-    "MAXssS",
-    "MAXaaS",
-    "MAXdssS",
-    "MAXddssS",
-    "MAXsCl",
-    "MAXsGeH3",
-    "MAXssGeH2",
-    "MAXsssGeH",
-    "MAXssssGe",
-    "MAXsAsH2",
-    "MAXssAsH",
-    "MAXsssAs",
-    "MAXsssdAs",
-    "MAXsssssAs",
-    "MAXsSeH",
-    "MAXdSe",
-    "MAXssSe",
-    "MAXaaSe",
-    "MAXdssSe",
-    "MAXddssSe",
-    "MAXsBr",
-    "MAXsSnH3",
-    "MAXssSnH2",
-    "MAXsssSnH",
-    "MAXssssSn",
-    "MAXsI",
-    "MAXsPbH3",
-    "MAXssPbH2",
-    "MAXsssPbH",
-    "MAXssssPb",
-    "MINsLi",
-    "MINssBe",
-    "MINssssBe",
-    "MINssBH",
-    "MINsssB",
-    "MINssssB",
-    "MINsCH3",
-    "MINdCH2",
-    "MINssCH2",
-    "MINtCH",
-    "MINdsCH",
-    "MINaaCH",
-    "MINsssCH",
-    "MINddC",
-    "MINtsC",
-    "MINdssC",
-    "MINaasC",
-    "MINaaaC",
-    "MINssssC",
-    "MINsNH3",
-    "MINsNH2",
-    "MINssNH2",
-    "MINdNH",
-    "MINssNH",
-    "MINaaNH",
-    "MINtN",
-    "MINsssNH",
-    "MINdsN",
-    "MINaaN",
-    "MINsssN",
-    "MINddsN",
-    "MINaasN",
-    "MINssssN",
-    "MINsOH",
-    "MINdO",
-    "MINssO",
-    "MINaaO",
-    "MINsF",
-    "MINsSiH3",
-    "MINssSiH2",
-    "MINsssSiH",
-    "MINssssSi",
-    "MINsPH2",
-    "MINssPH",
-    "MINsssP",
-    "MINdsssP",
-    "MINsssssP",
-    "MINsSH",
-    "MINdS",
-    "MINssS",
-    "MINaaS",
-    "MINdssS",
-    "MINddssS",
-    "MINsCl",
-    "MINsGeH3",
-    "MINssGeH2",
-    "MINsssGeH",
-    "MINssssGe",
-    "MINsAsH2",
-    "MINssAsH",
-    "MINsssAs",
-    "MINsssdAs",
-    "MINsssssAs",
-    "MINsSeH",
-    "MINdSe",
-    "MINssSe",
-    "MINaaSe",
-    "MINdssSe",
-    "MINddssSe",
-    "MINsBr",
-    "MINsSnH3",
-    "MINssSnH2",
-    "MINsssSnH",
-    "MINssssSn",
-    "MINsI",
-    "MINsPbH3",
-    "MINssPbH2",
-    "MINsssPbH",
-    "MINssssPb",
-    "ECIndex",
-    "ETA_alpha",
-    "AETA_alpha",
-    "ETA_shape_p",
-    "ETA_shape_y",
-    "ETA_shape_x",
-    "ETA_beta",
-    "AETA_beta",
-    "ETA_beta_s",
-    "AETA_beta_s",
-    "ETA_beta_ns",
-    "AETA_beta_ns",
-    "ETA_beta_ns_d",
-    "AETA_beta_ns_d",
-    "ETA_eta",
-    "AETA_eta",
-    "ETA_eta_L",
-    "AETA_eta_L",
-    "ETA_eta_R",
-    "AETA_eta_R",
-    "ETA_eta_RL",
-    "AETA_eta_RL",
-    "ETA_eta_F",
-    "AETA_eta_F",
-    "ETA_eta_FL",
-    "AETA_eta_FL",
-    "ETA_eta_B",
-    "AETA_eta_B",
-    "ETA_eta_BR",
-    "AETA_eta_BR",
-    "ETA_dAlpha_A",
-    "ETA_dAlpha_B",
-    "ETA_epsilon_1",
-    "ETA_epsilon_2",
-    "ETA_epsilon_3",
-    "ETA_epsilon_4",
-    "ETA_epsilon_5",
-    "ETA_dEpsilon_A",
-    "ETA_dEpsilon_B",
-    "ETA_dEpsilon_C",
-    "ETA_dEpsilon_D",
-    "ETA_dBeta",
-    "AETA_dBeta",
-    "ETA_psi_1",
-    "ETA_dPsi_A",
-    "ETA_dPsi_B",
-    "fragCpx",
-    "fMF",
-    "nHBAcc",
-    "nHBDon",
-    "IC0",
-    "IC1",
-    "IC2",
-    "IC3",
-    "IC4",
-    "IC5",
-    "TIC0",
-    "TIC1",
-    "TIC2",
-    "TIC3",
-    "TIC4",
-    "TIC5",
-    "SIC0",
-    "SIC1",
-    "SIC2",
-    "SIC3",
-    "SIC4",
-    "SIC5",
-    "BIC0",
-    "BIC1",
-    "BIC2",
-    "BIC3",
-    "BIC4",
-    "BIC5",
-    "CIC0",
-    "CIC1",
-    "CIC2",
-    "CIC3",
-    "CIC4",
-    "CIC5",
-    "MIC0",
-    "MIC1",
-    "MIC2",
-    "MIC3",
-    "MIC4",
-    "MIC5",
-    "ZMIC0",
-    "ZMIC1",
-    "ZMIC2",
-    "ZMIC3",
-    "ZMIC4",
-    "ZMIC5",
-    "Kier1",
-    "Kier2",
-    "Kier3",
-    "Lipinski",
-    "GhoseFilter",
-    "FilterItLogS",
-    "VMcGowan",
-    "LabuteASA",
-    "PEOE_VSA1",
-    "PEOE_VSA2",
-    "PEOE_VSA3",
-    "PEOE_VSA4",
-    "PEOE_VSA5",
-    "PEOE_VSA6",
-    "PEOE_VSA7",
-    "PEOE_VSA8",
-    "PEOE_VSA9",
-    "PEOE_VSA10",
-    "PEOE_VSA11",
-    "PEOE_VSA12",
-    "PEOE_VSA13",
-    "SMR_VSA1",
-    "SMR_VSA2",
-    "SMR_VSA3",
-    "SMR_VSA4",
-    "SMR_VSA5",
-    "SMR_VSA6",
-    "SMR_VSA7",
-    "SMR_VSA8",
-    "SMR_VSA9",
-    "SlogP_VSA1",
-    "SlogP_VSA2",
-    "SlogP_VSA3",
-    "SlogP_VSA4",
-    "SlogP_VSA5",
-    "SlogP_VSA6",
-    "SlogP_VSA7",
-    "SlogP_VSA8",
-    "SlogP_VSA9",
-    "SlogP_VSA10",
-    "SlogP_VSA11",
-    "EState_VSA1",
-    "EState_VSA2",
-    "EState_VSA3",
-    "EState_VSA4",
-    "EState_VSA5",
-    "EState_VSA6",
-    "EState_VSA7",
-    "EState_VSA8",
-    "EState_VSA9",
-    "EState_VSA10",
-    "VSA_EState1",
-    "VSA_EState2",
-    "VSA_EState3",
-    "VSA_EState4",
-    "VSA_EState5",
-    "VSA_EState6",
-    "VSA_EState7",
-    "VSA_EState8",
-    "VSA_EState9",
-    "MDEC-11",
-    "MDEC-12",
-    "MDEC-13",
-    "MDEC-14",
-    "MDEC-22",
-    "MDEC-23",
-    "MDEC-24",
-    "MDEC-33",
-    "MDEC-34",
-    "MDEC-44",
-    "MDEO-11",
-    "MDEO-12",
-    "MDEO-22",
-    "MDEN-11",
-    "MDEN-12",
-    "MDEN-13",
-    "MDEN-22",
-    "MDEN-23",
-    "MDEN-33",
-    "MID",
-    "AMID",
-    "MID_h",
-    "AMID_h",
-    "MID_C",
-    "AMID_C",
-    "MID_N",
-    "AMID_N",
-    "MID_O",
-    "AMID_O",
-    "MID_X",
-    "AMID_X",
-    "MPC2",
-    "MPC3",
-    "MPC4",
-    "MPC5",
-    "MPC6",
-    "MPC7",
-    "MPC8",
-    "MPC9",
-    "MPC10",
-    "TMPC10",
-    "piPC1",
-    "piPC2",
-    "piPC3",
-    "piPC4",
-    "piPC5",
-    "piPC6",
-    "piPC7",
-    "piPC8",
-    "piPC9",
-    "piPC10",
-    "TpiPC10",
-    "apol",
-    "bpol",
-    "nRing",
-    "n3Ring",
-    "n4Ring",
-    "n5Ring",
-    "n6Ring",
-    "n7Ring",
-    "n8Ring",
-    "n9Ring",
-    "n10Ring",
-    "n11Ring",
-    "n12Ring",
-    "nG12Ring",
-    "nHRing",
-    "n3HRing",
-    "n4HRing",
-    "n5HRing",
-    "n6HRing",
-    "n7HRing",
-    "n8HRing",
-    "n9HRing",
-    "n10HRing",
-    "n11HRing",
-    "n12HRing",
-    "nG12HRing",
-    "naRing",
-    "n3aRing",
-    "n4aRing",
-    "n5aRing",
-    "n6aRing",
-    "n7aRing",
-    "n8aRing",
-    "n9aRing",
-    "n10aRing",
-    "n11aRing",
-    "n12aRing",
-    "nG12aRing",
-    "naHRing",
-    "n3aHRing",
-    "n4aHRing",
-    "n5aHRing",
-    "n6aHRing",
-    "n7aHRing",
-    "n8aHRing",
-    "n9aHRing",
-    "n10aHRing",
-    "n11aHRing",
-    "n12aHRing",
-    "nG12aHRing",
-    "nARing",
-    "n3ARing",
-    "n4ARing",
-    "n5ARing",
-    "n6ARing",
-    "n7ARing",
-    "n8ARing",
-    "n9ARing",
-    "n10ARing",
-    "n11ARing",
-    "n12ARing",
-    "nG12ARing",
-    "nAHRing",
-    "n3AHRing",
-    "n4AHRing",
-    "n5AHRing",
-    "n6AHRing",
-    "n7AHRing",
-    "n8AHRing",
-    "n9AHRing",
-    "n10AHRing",
-    "n11AHRing",
-    "n12AHRing",
-    "nG12AHRing",
-    "nFRing",
-    "n4FRing",
-    "n5FRing",
-    "n6FRing",
-    "n7FRing",
-    "n8FRing",
-    "n9FRing",
-    "n10FRing",
-    "n11FRing",
-    "n12FRing",
-    "nG12FRing",
-    "nFHRing",
-    "n4FHRing",
-    "n5FHRing",
-    "n6FHRing",
-    "n7FHRing",
-    "n8FHRing",
-    "n9FHRing",
-    "n10FHRing",
-    "n11FHRing",
-    "n12FHRing",
-    "nG12FHRing",
-    "nFaRing",
-    "n4FaRing",
-    "n5FaRing",
-    "n6FaRing",
-    "n7FaRing",
-    "n8FaRing",
-    "n9FaRing",
-    "n10FaRing",
-    "n11FaRing",
-    "n12FaRing",
-    "nG12FaRing",
-    "nFaHRing",
-    "n4FaHRing",
-    "n5FaHRing",
-    "n6FaHRing",
-    "n7FaHRing",
-    "n8FaHRing",
-    "n9FaHRing",
-    "n10FaHRing",
-    "n11FaHRing",
-    "n12FaHRing",
-    "nG12FaHRing",
-    "nFARing",
-    "n4FARing",
-    "n5FARing",
-    "n6FARing",
-    "n7FARing",
-    "n8FARing",
-    "n9FARing",
-    "n10FARing",
-    "n11FARing",
-    "n12FARing",
-    "nG12FARing",
-    "nFAHRing",
-    "n4FAHRing",
-    "n5FAHRing",
-    "n6FAHRing",
-    "n7FAHRing",
-    "n8FAHRing",
-    "n9FAHRing",
-    "n10FAHRing",
-    "n11FAHRing",
-    "n12FAHRing",
-    "nG12FAHRing",
-    "nRot",
-    "RotRatio",
-    "SLogP",
-    "SMR",
-    "TopoPSA(NO)",
-    "TopoPSA",
-    "GGI1",
-    "GGI2",
-    "GGI3",
-    "GGI4",
-    "GGI5",
-    "GGI6",
-    "GGI7",
-    "GGI8",
-    "GGI9",
-    "GGI10",
-    "JGI1",
-    "JGI2",
-    "JGI3",
-    "JGI4",
-    "JGI5",
-    "JGI6",
-    "JGI7",
-    "JGI8",
-    "JGI9",
-    "JGI10",
-    "JGT10",
-    "Diameter",
-    "Radius",
-    "TopoShapeIndex",
-    "PetitjeanIndex",
-    "Vabc",
-    "VAdjMat",
-    "MWC01",
-    "MWC02",
-    "MWC03",
-    "MWC04",
-    "MWC05",
-    "MWC06",
-    "MWC07",
-    "MWC08",
-    "MWC09",
-    "MWC10",
-    "TMWC10",
-    "SRW02",
-    "SRW03",
-    "SRW04",
-    "SRW05",
-    "SRW06",
-    "SRW07",
-    "SRW08",
-    "SRW09",
-    "SRW10",
-    "TSRW10",
-    "MW",
-    "AMW",
-    "WPath",
-    "WPol",
-    "Zagreb1",
-    "Zagreb2",
-    "mZagreb1",
-    "mZagreb2",
-]
-
-
-SUBSET_947 = [
-    "ABC",
-    "ABCGG",
-    "nBase",
-    "SpAbs_A",
-    "SpMax_A",
-    "SpDiam_A",
-    "SpAD_A",
-    "SpMAD_A",
-    "LogEE_A",
-    "VE1_A",
-    "VE2_A",
-    "VE3_A",
-    "VR1_A",
-    "VR2_A",
-    "VR3_A",
-    "nAromAtom",
-    "nAromBond",
-    "nAtom",
-    "nHeavyAtom",
-    "nHetero",
-    "nH",
-    "nC",
-    "nN",
-    "nO",
-    "ATS0dv",
-    "ATS1dv",
-    "ATS2dv",
-    "ATS3dv",
-    "ATS0d",
-    "ATS1d",
-    "ATS2d",
-    "ATS3d",
-    "ATS4d",
-    "ATS5d",
-    "ATS0s",
-    "ATS1s",
-    "ATS2s",
-    "ATS3s",
-    "ATS4s",
-    "ATS5s",
-    "ATS0Z",
-    "ATS1Z",
-    "ATS2Z",
-    "ATS3Z",
-    "ATS4Z",
-    "ATS5Z",
-    "ATS0m",
-    "ATS1m",
-    "ATS2m",
-    "ATS3m",
-    "ATS4m",
-    "ATS5m",
-    "ATS0v",
-    "ATS1v",
-    "ATS2v",
-    "ATS3v",
-    "ATS4v",
-    "ATS5v",
-    "ATS0se",
-    "ATS1se",
-    "ATS2se",
-    "ATS3se",
-    "ATS4se",
-    "ATS5se",
-    "ATS0pe",
-    "ATS1pe",
-    "ATS2pe",
-    "ATS3pe",
-    "ATS4pe",
-    "ATS5pe",
-    "ATS0are",
-    "ATS1are",
-    "ATS2are",
-    "ATS3are",
-    "ATS4are",
-    "ATS5are",
-    "ATS0p",
-    "ATS1p",
-    "ATS2p",
-    "ATS3p",
-    "ATS4p",
-    "ATS5p",
-    "ATS0i",
-    "ATS1i",
-    "ATS2i",
-    "ATS3i",
-    "ATS4i",
-    "ATS5i",
-    "AATS0dv",
-    "AATS1dv",
-    "AATS2dv",
-    "AATS3dv",
-    "AATS0d",
-    "AATS1d",
-    "AATS2d",
-    "AATS3d",
-    "AATS4d",
-    "AATS0s",
-    "AATS1s",
-    "AATS2s",
-    "AATS3s",
-    "AATS4s",
-    "AATS0Z",
-    "AATS1Z",
-    "AATS2Z",
-    "AATS3Z",
-    "AATS4Z",
-    "AATS0m",
-    "AATS1m",
-    "AATS2m",
-    "AATS3m",
-    "AATS4m",
-    "AATS0v",
-    "AATS1v",
-    "AATS2v",
-    "AATS3v",
-    "AATS4v",
-    "AATS5v",
-    "AATS0se",
-    "AATS1se",
-    "AATS2se",
-    "AATS3se",
-    "AATS4se",
-    "AATS5se",
-    "AATS0pe",
-    "AATS1pe",
-    "AATS2pe",
-    "AATS3pe",
-    "AATS4pe",
-    "AATS5pe",
-    "AATS0are",
-    "AATS1are",
-    "AATS2are",
-    "AATS3are",
-    "AATS4are",
-    "AATS5are",
-    "AATS0p",
-    "AATS1p",
-    "AATS2p",
-    "AATS3p",
-    "AATS4p",
-    "AATS5p",
-    "AATS0i",
-    "AATS1i",
-    "AATS2i",
-    "AATS3i",
-    "AATS4i",
-    "AATS5i",
-    "ATSC0c",
-    "ATSC1c",
-    "ATSC2c",
-    "ATSC3c",
-    "ATSC4c",
-    "ATSC5c",
-    "ATSC0dv",
-    "ATSC1dv",
-    "ATSC2dv",
-    "ATSC3dv",
-    "ATSC4dv",
-    "ATSC5dv",
-    "ATSC0d",
-    "ATSC1d",
-    "ATSC2d",
-    "ATSC3d",
-    "ATSC4d",
-    "ATSC5d",
-    "ATSC0s",
-    "ATSC1s",
-    "ATSC2s",
-    "ATSC3s",
-    "ATSC4s",
-    "ATSC5s",
-    "ATSC0Z",
-    "ATSC1Z",
-    "ATSC2Z",
-    "ATSC3Z",
-    "ATSC4Z",
-    "ATSC5Z",
-    "ATSC0m",
-    "ATSC1m",
-    "ATSC2m",
-    "ATSC3m",
-    "ATSC4m",
-    "ATSC5m",
-    "ATSC0v",
-    "ATSC1v",
-    "ATSC2v",
-    "ATSC3v",
-    "ATSC4v",
-    "ATSC5v",
-    "ATSC0se",
-    "ATSC1se",
-    "ATSC2se",
-    "ATSC3se",
-    "ATSC4se",
-    "ATSC5se",
-    "ATSC0pe",
-    "ATSC1pe",
-    "ATSC2pe",
-    "ATSC3pe",
-    "ATSC4pe",
-    "ATSC5pe",
-    "ATSC0are",
-    "ATSC1are",
-    "ATSC2are",
-    "ATSC3are",
-    "ATSC4are",
-    "ATSC5are",
-    "ATSC0p",
-    "ATSC1p",
-    "ATSC2p",
-    "ATSC3p",
-    "ATSC4p",
-    "ATSC5p",
-    "ATSC0i",
-    "ATSC1i",
-    "ATSC2i",
-    "ATSC3i",
-    "ATSC4i",
-    "ATSC5i",
-    "AATSC0c",
-    "AATSC1c",
-    "AATSC2c",
-    "AATSC3c",
-    "AATSC4c",
-    "AATSC5c",
-    "AATSC0dv",
-    "AATSC1dv",
-    "AATSC2dv",
-    "AATSC3dv",
-    "AATSC4dv",
-    "AATSC5dv",
-    "AATSC0d",
-    "AATSC1d",
-    "AATSC2d",
-    "AATSC3d",
-    "AATSC4d",
-    "AATSC5d",
-    "AATSC0s",
-    "AATSC1s",
-    "AATSC2s",
-    "AATSC3s",
-    "AATSC4s",
-    "AATSC5s",
-    "AATSC0Z",
-    "AATSC1Z",
-    "AATSC2Z",
-    "AATSC3Z",
-    "AATSC4Z",
-    "AATSC5Z",
-    "AATSC0m",
-    "AATSC1m",
-    "AATSC2m",
-    "AATSC3m",
-    "AATSC4m",
-    "AATSC5m",
-    "AATSC0v",
-    "AATSC1v",
-    "AATSC2v",
-    "AATSC3v",
-    "AATSC4v",
-    "AATSC5v",
-    "AATSC0se",
-    "AATSC1se",
-    "AATSC2se",
-    "AATSC3se",
-    "AATSC4se",
-    "AATSC5se",
-    "AATSC0pe",
-    "AATSC1pe",
-    "AATSC2pe",
-    "AATSC3pe",
-    "AATSC4pe",
-    "AATSC5pe",
-    "AATSC0are",
-    "AATSC1are",
-    "AATSC2are",
-    "AATSC3are",
-    "AATSC4are",
-    "AATSC5are",
-    "AATSC0p",
-    "AATSC1p",
-    "AATSC2p",
-    "AATSC3p",
-    "AATSC4p",
-    "AATSC5p",
-    "AATSC0i",
-    "AATSC1i",
-    "AATSC2i",
-    "AATSC3i",
-    "AATSC4i",
-    "AATSC5i",
-    "MATS1c",
-    "MATS2c",
-    "MATS3c",
-    "MATS4c",
-    "MATS5c",
-    "MATS1dv",
-    "MATS2dv",
-    "MATS3dv",
-    "MATS4dv",
-    "MATS5dv",
-    "MATS1d",
-    "MATS2d",
-    "MATS3d",
-    "MATS4d",
-    "MATS5d",
-    "MATS1s",
-    "MATS2s",
-    "MATS3s",
-    "MATS4s",
-    "MATS5s",
-    "MATS1Z",
-    "MATS2Z",
-    "MATS3Z",
-    "MATS4Z",
-    "MATS5Z",
-    "MATS1m",
-    "MATS2m",
-    "MATS3m",
-    "MATS4m",
-    "MATS5m",
-    "MATS1v",
-    "MATS2v",
-    "MATS3v",
-    "MATS4v",
-    "MATS5v",
-    "MATS1se",
-    "MATS2se",
-    "MATS3se",
-    "MATS4se",
-    "MATS5se",
-    "MATS1pe",
-    "MATS2pe",
-    "MATS3pe",
-    "MATS4pe",
-    "MATS5pe",
-    "MATS1are",
-    "MATS2are",
-    "MATS3are",
-    "MATS4are",
-    "MATS5are",
-    "MATS1p",
-    "MATS2p",
-    "MATS3p",
-    "MATS4p",
-    "MATS5p",
-    "MATS1i",
-    "MATS2i",
-    "MATS3i",
-    "MATS4i",
-    "MATS5i",
-    "GATS1c",
-    "GATS2c",
-    "GATS3c",
-    "GATS4c",
-    "GATS5c",
-    "GATS1dv",
-    "GATS2dv",
-    "GATS3dv",
-    "GATS4dv",
-    "GATS1d",
-    "GATS2d",
-    "GATS3d",
-    "GATS4d",
-    "GATS1s",
-    "GATS2s",
-    "GATS3s",
-    "GATS4s",
-    "GATS1Z",
-    "GATS2Z",
-    "GATS3Z",
-    "GATS4Z",
-    "GATS1m",
-    "GATS2m",
-    "GATS3m",
-    "GATS4m",
-    "GATS1v",
-    "GATS2v",
-    "GATS3v",
-    "GATS4v",
-    "GATS1se",
-    "GATS2se",
-    "GATS3se",
-    "GATS4se",
-    "GATS1pe",
-    "GATS2pe",
-    "GATS3pe",
-    "GATS4pe",
-    "GATS1are",
-    "GATS2are",
-    "GATS3are",
-    "GATS4are",
-    "GATS1p",
-    "GATS2p",
-    "GATS3p",
-    "GATS4p",
-    "GATS1i",
-    "GATS2i",
-    "GATS3i",
-    "GATS4i",
-    "BCUTc-1h",
-    "BCUTc-1l",
-    "BCUTdv-1h",
-    "BCUTdv-1l",
-    "BCUTd-1h",
-    "BCUTd-1l",
-    "BCUTs-1h",
-    "BCUTs-1l",
-    "BCUTZ-1h",
-    "BCUTZ-1l",
-    "BCUTm-1h",
-    "BCUTm-1l",
-    "BCUTv-1h",
-    "BCUTv-1l",
-    "BCUTse-1h",
-    "BCUTse-1l",
-    "BCUTpe-1h",
-    "BCUTpe-1l",
-    "BCUTare-1h",
-    "BCUTare-1l",
-    "BCUTp-1h",
-    "BCUTp-1l",
-    "BCUTi-1h",
-    "BCUTi-1l",
-    "BalabanJ",
-    "SpAbs_DzZ",
-    "SpMax_DzZ",
-    "SpDiam_DzZ",
-    "SpAD_DzZ",
-    "SpMAD_DzZ",
-    "LogEE_DzZ",
-    "SM1_DzZ",
-    "VE1_DzZ",
-    "VE2_DzZ",
-    "VE3_DzZ",
-    "VR1_DzZ",
-    "VR2_DzZ",
-    "VR3_DzZ",
-    "SpAbs_Dzm",
-    "SpMax_Dzm",
-    "SpDiam_Dzm",
-    "SpAD_Dzm",
-    "SpMAD_Dzm",
-    "LogEE_Dzm",
-    "SM1_Dzm",
-    "VE1_Dzm",
-    "VE2_Dzm",
-    "VE3_Dzm",
-    "VR1_Dzm",
-    "VR2_Dzm",
-    "VR3_Dzm",
-    "SpAbs_Dzv",
-    "SpMax_Dzv",
-    "SpDiam_Dzv",
-    "SpAD_Dzv",
-    "SpMAD_Dzv",
-    "LogEE_Dzv",
-    "SM1_Dzv",
-    "VE1_Dzv",
-    "VE2_Dzv",
-    "VE3_Dzv",
-    "VR1_Dzv",
-    "VR2_Dzv",
-    "VR3_Dzv",
-    "SpAbs_Dzse",
-    "SpMax_Dzse",
-    "SpDiam_Dzse",
-    "SpAD_Dzse",
-    "SpMAD_Dzse",
-    "LogEE_Dzse",
-    "SM1_Dzse",
-    "VE1_Dzse",
-    "VE2_Dzse",
-    "VE3_Dzse",
-    "VR1_Dzse",
-    "VR2_Dzse",
-    "VR3_Dzse",
-    "SpAbs_Dzpe",
-    "SpMax_Dzpe",
-    "SpDiam_Dzpe",
-    "SpAD_Dzpe",
-    "SpMAD_Dzpe",
-    "LogEE_Dzpe",
-    "SM1_Dzpe",
-    "VE1_Dzpe",
-    "VE2_Dzpe",
-    "VE3_Dzpe",
-    "VR1_Dzpe",
-    "VR2_Dzpe",
-    "VR3_Dzpe",
-    "SpAbs_Dzare",
-    "SpMax_Dzare",
-    "SpDiam_Dzare",
-    "SpAD_Dzare",
-    "SpMAD_Dzare",
-    "LogEE_Dzare",
-    "SM1_Dzare",
-    "VE1_Dzare",
-    "VE2_Dzare",
-    "VE3_Dzare",
-    "VR1_Dzare",
-    "VR2_Dzare",
-    "VR3_Dzare",
-    "SpAbs_Dzp",
-    "SpMax_Dzp",
-    "SpDiam_Dzp",
-    "SpAD_Dzp",
-    "SpMAD_Dzp",
-    "LogEE_Dzp",
-    "SM1_Dzp",
-    "VE1_Dzp",
-    "VE2_Dzp",
-    "VE3_Dzp",
-    "VR1_Dzp",
-    "VR2_Dzp",
-    "VR3_Dzp",
-    "SpAbs_Dzi",
-    "SpMax_Dzi",
-    "SpDiam_Dzi",
-    "SpAD_Dzi",
-    "SpMAD_Dzi",
-    "LogEE_Dzi",
-    "SM1_Dzi",
-    "VE1_Dzi",
-    "VE2_Dzi",
-    "VE3_Dzi",
-    "VR1_Dzi",
-    "VR2_Dzi",
-    "VR3_Dzi",
-    "BertzCT",
-    "nBonds",
-    "nBondsO",
-    "nBondsS",
-    "nBondsD",
-    "nBondsT",
-    "nBondsA",
-    "nBondsM",
-    "nBondsKS",
-    "nBondsKD",
-    "RNCG",
-    "RPCG",
-    "C1SP1",
-    "C2SP1",
-    "C1SP2",
-    "C2SP2",
-    "C1SP3",
-    "C2SP3",
-    "C3SP3",
-    "C4SP3",
-    "HybRatio",
-    "FCSP3",
-    "Xch-3d",
-    "Xch-4d",
-    "Xch-5d",
-    "Xch-3dv",
-    "Xch-4dv",
-    "Xch-5dv",
-    "Xc-3d",
-    "Xc-4d",
-    "Xc-3dv",
-    "Xc-4dv",
-    "Xpc-4d",
-    "Xpc-4dv",
-    "Xp-0d",
-    "Xp-1d",
-    "Xp-2d",
-    "Xp-3d",
-    "Xp-4d",
-    "AXp-0d",
-    "AXp-1d",
-    "AXp-2d",
-    "AXp-3d",
-    "AXp-4d",
-    "Xp-0dv",
-    "Xp-1dv",
-    "Xp-2dv",
-    "Xp-3dv",
-    "Xp-4dv",
-    "AXp-0dv",
-    "AXp-1dv",
-    "AXp-2dv",
-    "AXp-3dv",
-    "AXp-4dv",
-    "SZ",
-    "Sm",
-    "Sv",
-    "Sse",
-    "Spe",
-    "Sare",
-    "Sp",
-    "Si",
-    "MZ",
-    "Mm",
-    "Mv",
-    "Mse",
-    "Mpe",
-    "Mare",
-    "Mp",
-    "Mi",
-    "SpAbs_Dt",
-    "SpMax_Dt",
-    "SpDiam_Dt",
-    "SpAD_Dt",
-    "SpMAD_Dt",
-    "LogEE_Dt",
-    "VE1_Dt",
-    "VE2_Dt",
-    "VE3_Dt",
-    "VR1_Dt",
-    "VR2_Dt",
-    "VR3_Dt",
-    "DetourIndex",
-    "SpAbs_D",
-    "SpMax_D",
-    "SpDiam_D",
-    "SpAD_D",
-    "SpMAD_D",
-    "LogEE_D",
-    "VE1_D",
-    "VE2_D",
-    "VE3_D",
-    "VR1_D",
-    "VR2_D",
-    "VR3_D",
-    "NsCH3",
-    "NdCH2",
-    "NssCH2",
-    "NtCH",
-    "NdsCH",
-    "NaaCH",
-    "NsssCH",
-    "NtsC",
-    "NdssC",
-    "NssssC",
-    "NsNH2",
-    "NdNH",
-    "NssNH",
-    "NaaNH",
-    "NtN",
-    "NdsN",
-    "NaaN",
-    "NsssN",
-    "NsOH",
-    "NdO",
-    "NssO",
-    "NaaO",
-    "SsCH3",
-    "SdCH2",
-    "SssCH2",
-    "StCH",
-    "SdsCH",
-    "SaaCH",
-    "SsssCH",
-    "StsC",
-    "SdssC",
-    "SssssC",
-    "SsNH2",
-    "SdNH",
-    "SssNH",
-    "SaaNH",
-    "StN",
-    "SdsN",
-    "SaaN",
-    "SsssN",
-    "SsOH",
-    "SdO",
-    "SssO",
-    "SaaO",
-    "MAXsCH3",
-    "MAXssCH2",
-    "MAXtCH",
-    "MAXdsCH",
-    "MAXaaCH",
-    "MAXsssCH",
-    "MAXtsC",
-    "MAXdssC",
-    "MAXssssC",
-    "MAXsNH2",
-    "MAXdNH",
-    "MAXssNH",
-    "MAXaaNH",
-    "MAXtN",
-    "MAXaaN",
-    "MAXsssN",
-    "MAXsOH",
-    "MAXdO",
-    "MAXssO",
-    "MAXaaO",
-    "MINsCH3",
-    "MINssCH2",
-    "MINtCH",
-    "MINdsCH",
-    "MINaaCH",
-    "MINsssCH",
-    "MINtsC",
-    "MINdssC",
-    "MINssssC",
-    "MINsNH2",
-    "MINdNH",
-    "MINssNH",
-    "MINaaNH",
-    "MINtN",
-    "MINaaN",
-    "MINsssN",
-    "MINsOH",
-    "MINdO",
-    "MINssO",
-    "MINaaO",
-    "ECIndex",
-    "ETA_alpha",
-    "AETA_alpha",
-    "ETA_shape_p",
-    "ETA_shape_y",
-    "ETA_shape_x",
-    "ETA_beta",
-    "AETA_beta",
-    "ETA_beta_s",
-    "AETA_beta_s",
-    "ETA_beta_ns",
-    "AETA_beta_ns",
-    "ETA_eta",
-    "AETA_eta",
-    "ETA_eta_L",
-    "AETA_eta_L",
-    "ETA_eta_R",
-    "AETA_eta_R",
-    "ETA_eta_RL",
-    "AETA_eta_RL",
-    "ETA_eta_F",
-    "AETA_eta_F",
-    "ETA_eta_FL",
-    "AETA_eta_FL",
-    "ETA_eta_B",
-    "AETA_eta_B",
-    "ETA_eta_BR",
-    "AETA_eta_BR",
-    "ETA_dAlpha_B",
-    "ETA_epsilon_1",
-    "ETA_epsilon_2",
-    "ETA_epsilon_3",
-    "ETA_epsilon_4",
-    "ETA_epsilon_5",
-    "ETA_dEpsilon_A",
-    "ETA_dEpsilon_B",
-    "ETA_dEpsilon_C",
-    "ETA_dEpsilon_D",
-    "ETA_dBeta",
-    "AETA_dBeta",
-    "ETA_psi_1",
-    "ETA_dPsi_A",
-    "ETA_dPsi_B",
-    "fragCpx",
-    "fMF",
-    "nHBAcc",
-    "nHBDon",
-    "IC0",
-    "IC1",
-    "IC2",
-    "IC3",
-    "IC4",
-    "IC5",
-    "TIC0",
-    "TIC1",
-    "TIC2",
-    "TIC3",
-    "TIC4",
-    "TIC5",
-    "SIC0",
-    "SIC1",
-    "SIC2",
-    "SIC3",
-    "SIC4",
-    "SIC5",
-    "BIC0",
-    "BIC1",
-    "BIC2",
-    "BIC3",
-    "BIC4",
-    "BIC5",
-    "CIC0",
-    "CIC1",
-    "CIC2",
-    "CIC3",
-    "CIC4",
-    "CIC5",
-    "MIC0",
-    "MIC1",
-    "MIC2",
-    "MIC3",
-    "MIC4",
-    "MIC5",
-    "ZMIC0",
-    "ZMIC1",
-    "ZMIC2",
-    "ZMIC3",
-    "ZMIC4",
-    "ZMIC5",
-    "Kier1",
-    "Kier2",
-    "Kier3",
-    "FilterItLogS",
-    "VMcGowan",
-    "LabuteASA",
-    "PEOE_VSA1",
-    "PEOE_VSA2",
-    "PEOE_VSA3",
-    "PEOE_VSA4",
-    "PEOE_VSA5",
-    "PEOE_VSA6",
-    "PEOE_VSA7",
-    "PEOE_VSA8",
-    "PEOE_VSA9",
-    "PEOE_VSA10",
-    "PEOE_VSA11",
-    "PEOE_VSA12",
-    "PEOE_VSA13",
-    "SMR_VSA1",
-    "SMR_VSA2",
-    "SMR_VSA3",
-    "SMR_VSA4",
-    "SMR_VSA5",
-    "SMR_VSA6",
-    "SMR_VSA7",
-    "SMR_VSA9",
-    "SlogP_VSA1",
-    "SlogP_VSA2",
-    "SlogP_VSA3",
-    "SlogP_VSA4",
-    "SlogP_VSA5",
-    "SlogP_VSA6",
-    "SlogP_VSA10",
-    "EState_VSA1",
-    "EState_VSA2",
-    "EState_VSA3",
-    "EState_VSA4",
-    "EState_VSA5",
-    "EState_VSA6",
-    "EState_VSA7",
-    "EState_VSA8",
-    "EState_VSA9",
-    "EState_VSA10",
-    "VSA_EState1",
-    "VSA_EState2",
-    "VSA_EState3",
-    "VSA_EState4",
-    "VSA_EState5",
-    "VSA_EState6",
-    "VSA_EState7",
-    "VSA_EState8",
-    "VSA_EState9",
-    "MDEC-11",
-    "MDEC-12",
-    "MDEC-13",
-    "MDEC-14",
-    "MDEC-22",
-    "MDEC-23",
-    "MDEC-24",
-    "MDEO-12",
-    "MDEN-11",
-    "MID",
-    "AMID",
-    "MID_h",
-    "AMID_h",
-    "MID_C",
-    "AMID_C",
-    "MID_N",
-    "AMID_N",
-    "MID_O",
-    "AMID_O",
-    "MPC2",
-    "MPC3",
-    "MPC4",
-    "TMPC10",
-    "piPC1",
-    "piPC2",
-    "piPC3",
-    "piPC4",
-    "TpiPC10",
-    "apol",
-    "bpol",
-    "nRing",
-    "n3Ring",
-    "n4Ring",
-    "n5Ring",
-    "nHRing",
-    "n3HRing",
-    "n4HRing",
-    "n5HRing",
-    "naRing",
-    "n5aRing",
-    "naHRing",
-    "n5aHRing",
-    "nARing",
-    "n3ARing",
-    "n4ARing",
-    "nAHRing",
-    "n3AHRing",
-    "n4AHRing",
-    "nRot",
-    "RotRatio",
-    "SLogP",
-    "SMR",
-    "TopoPSA(NO)",
-    "TopoPSA",
-    "GGI1",
-    "GGI2",
-    "GGI3",
-    "JGI1",
-    "JGI2",
-    "JGI3",
-    "JGT10",
-    "Diameter",
-    "Radius",
-    "TopoShapeIndex",
-    "PetitjeanIndex",
-    "Vabc",
-    "VAdjMat",
-    "MWC01",
-    "MWC02",
-    "MWC03",
-    "MWC04",
-    "MWC05",
-    "MWC06",
-    "MWC07",
-    "MWC08",
-    "MWC09",
-    "MWC10",
-    "TMWC10",
-    "SRW02",
-    "SRW03",
-    "SRW04",
-    "SRW05",
-    "SRW06",
-    "SRW07",
-    "SRW08",
-    "SRW09",
-    "SRW10",
-    "TSRW10",
-    "MW",
-    "AMW",
-    "WPath",
-    "WPol",
-    "Zagreb1",
-    "Zagreb2",
-    "mZagreb1",
-    "mZagreb2",
-]
-
-descriptors_lookup = dict(
-    optimized=SUBSET_947,
-    all=ALL_2D,
-)
diff --git a/fastprop/utils/load_data.py b/fastprop/utils/load_data.py
deleted file mode 100644
index 3a6ebfc..0000000
--- a/fastprop/utils/load_data.py
+++ /dev/null
@@ -1,59 +0,0 @@
-import numpy as np
-import pandas as pd
-from rdkit import Chem
-
-from fastprop.defaults import init_logger
-
-logger = init_logger(__name__)
-
-
-def load_from_csv(fpath, smiles_column, target_columns):
-    # load a file of smiles and targets, returning rdkit mols and targets
-    src = pd.read_csv(fpath)
-    try:
-        targets = src[target_columns].to_numpy()
-    except KeyError as ke:
-        raise RuntimeError(f"Unable to find column(s) {', '.join(target_columns)} inside file {fpath}. Check spelling and file contents.") from ke
-    # keep everything 2D
-    if len(target_columns) == 1:
-        targets = targets.reshape(-1, 1)
-    smiles: np.ndarray = src[smiles_column].to_numpy()
-    logger.info("Generating RDKit molecules from SMILES.")
-    rdkit_mols = np.array(list(Chem.MolFromSmiles(i) for i in smiles))
-
-    # remove dataset entries where the molecule could not be built
-    error_mols_idxs = np.where(rdkit_mols == None)[0]  # noqa: E711
-    for idx in error_mols_idxs:
-        logger.warn(
-            f"Unable to create RDKit molecule from SMILES string {smiles[idx]} (index {idx})."
-            " Both the molecule and the target will be removed from the data."
-        )
-    if len(error_mols_idxs) > 0:
-        # specify axis=0 to avoid changing dimensions (flattening)
-        logger.warn(f"Removed {len(error_mols_idxs)} entries from the dataset ({100*len(error_mols_idxs)/len(src.index):.2f}% of the data).")
-        targets = np.delete(targets, error_mols_idxs, axis=0)
-        rdkit_mols = np.delete(rdkit_mols, error_mols_idxs, axis=0)
-        smiles = np.delete(smiles, error_mols_idxs)
-
-    # also remove dataset entries where the target is missing
-    # TODO: weight masking instead of removal?
-    error_target_idxs = [idx for idx, arr in enumerate(targets) if any(np.isnan(arr))]
-    for idx in error_target_idxs:
-        logger.warn(
-            f"Missing target value (target={targets[idx]}) for SMILES {smiles[idx]}. Both the molecule and the target will be removed from the data."
-        )
-    if len(error_target_idxs) > 0:
-        # specify axis=0 to avoid changing dimensions (flattening)
-        logger.warn(f"Removed {len(error_target_idxs)} entries from the dataset ({100*len(error_target_idxs)/len(src.index):.2f}% of the data).")
-        targets = np.delete(targets, error_target_idxs, axis=0)
-        rdkit_mols = np.delete(rdkit_mols, error_target_idxs, axis=0)
-        smiles = np.delete(smiles, error_target_idxs)
-    return targets, rdkit_mols, smiles
-
-
-def load_saved_desc(fpath):
-    # loads descriptors previously saved by fastprop, forces any non-numeric values (missing, strings, etc) to be nan.
-    d = pd.read_csv(fpath, low_memory=False)
-    d = d.apply(pd.to_numeric, errors="coerce")
-    descs = d[d.columns[1:]].to_numpy(dtype=float)
-    return descs
diff --git a/fastprop/utils/select_descriptors.py b/fastprop/utils/select_descriptors.py
deleted file mode 100644
index 7a50507..0000000
--- a/fastprop/utils/select_descriptors.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from importlib.metadata import version
-
-from mordred import descriptors, get_descriptors_in_module
-from packaging.version import Version
-
-
-def mordred_descriptors_from_strings(string_list, include_3d=False):
-    """
-    returns mordred descriptor classes based on their string name
-    if string_list is empty, returns all
-    include_3d will include or exclude descriptors that require 3d info
-    """
-    out = []
-    string_set = set(string_list)
-    if (input_length := len(string_list)) != (set_length := len(string_set)):
-        raise RuntimeError(f"Input contains {input_length - set_length} duplicate entires.")
-    __version__ = Version(version("mordredcommunity"))
-    for mdl in descriptors.all:
-        for Desc in get_descriptors_in_module(mdl, submodule=False):
-            for desc in Desc.preset(__version__):
-                if not string_list or desc.__str__() in string_list:
-                    if desc.require_3D and not include_3d:
-                        continue
-                    out.append(desc)
-    return out
diff --git a/fastprop/utils/validate_config.py b/fastprop/utils/validate_config.py
deleted file mode 100644
index ee80e4b..0000000
--- a/fastprop/utils/validate_config.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from fastprop.defaults import init_logger
-
-logger = init_logger(__name__)
-
-
-def validate_config(config: dict):
-    logger.warn("TODO: Input validation has not been implemented! Skipping.")
diff --git a/fastprop_demo.ipynb b/fastprop_demo.ipynb
index dbbd91b..dfa9d8b 100644
--- a/fastprop_demo.ipynb
+++ b/fastprop_demo.ipynb
@@ -30,7 +30,7 @@
     },
     {
       "cell_type": "code",
-      "execution_count": 12,
+      "execution_count": 1,
       "metadata": {
         "id": "8KjnfwtAdibb"
       },
@@ -64,13 +64,13 @@
     },
     {
       "cell_type": "code",
-      "execution_count": 13,
+      "execution_count": 2,
       "metadata": {
         "colab": {
           "base_uri": "https://localhost:8080/"
         },
         "id": "3aChOAnkj2Jj",
-        "outputId": "a4c83eb1-6897-44f0-ff7f-223bdf924092"
+        "outputId": "032eb7c4-ad60-49e1-b57c-9b405f4bef6b"
       },
       "outputs": [
         {
@@ -105,13 +105,13 @@
     },
     {
       "cell_type": "code",
-      "execution_count": 14,
+      "execution_count": 3,
       "metadata": {
         "colab": {
           "base_uri": "https://localhost:8080/"
         },
         "id": "3i0FRyQRku_U",
-        "outputId": "03a71a9d-9e19-4ee6-d8f8-ac854f3bfbd4"
+        "outputId": "d7eaec18-651e-4032-dc7e-e05cfd3f5ec5"
       },
       "outputs": [
         {
@@ -142,13 +142,13 @@
     },
     {
       "cell_type": "code",
-      "execution_count": 15,
+      "execution_count": 4,
       "metadata": {
         "colab": {
           "base_uri": "https://localhost:8080/"
         },
         "id": "QQwy57tAlMdJ",
-        "outputId": "6a8a8e14-5501-4002-b8f3-e22af3741158"
+        "outputId": "dc70c8b4-2344-4790-b6d8-720fb70611f5"
       },
       "outputs": [
         {
@@ -166,28 +166,24 @@
             "# Additional Comments:\n",
             "# \n",
             "\n",
-            "\n",
             "# generic args\n",
             "output_directory: pah\n",
-            "random_seed: 55\n",
+            "random_seed: 562\n",
             "problem_type: regression\n",
             "\n",
             "# featurization\n",
             "input_file: pah/arockiaraj_pah_data.csv\n",
             "target_columns: log_p\n",
             "smiles_column: smiles\n",
-            "descriptors: all\n",
-            "\n",
-            "# preprocessing\n",
-            "rescaling: True\n",
-            "zero_variance_drop: False\n",
-            "colinear_drop: False\n",
+            "descriptor_set: all\n",
             "\n",
             "# training\n",
-            "number_repeats: 4\n",
-            "number_epochs: 100\n",
+            "hidden_size: 128\n",
+            "fnn_layers: 2\n",
+            "number_repeats: 1\n",
+            "number_epochs: 20\n",
             "batch_size: 64\n",
-            "patience: 15\n",
+            "patience: 5\n",
             "train_size: 0.8\n",
             "val_size: 0.1\n",
             "test_size: 0.1\n",
@@ -215,338 +211,208 @@
     },
     {
       "cell_type": "code",
-      "execution_count": 17,
+      "execution_count": 5,
       "metadata": {
         "colab": {
           "base_uri": "https://localhost:8080/"
         },
         "id": "bk3lj-86f4xM",
-        "outputId": "3a18a34e-9cef-475d-c5db-4d50cfff74f0"
+        "outputId": "9e036256-50f2-409c-cc24-19df5a3ec0e6"
       },
       "outputs": [
         {
           "output_type": "stream",
           "name": "stdout",
           "text": [
-            "[02/08/2024 06:12:35 PM numexpr.utils] INFO: NumExpr defaulting to 2 threads.\n",
-            "[02/08/2024 06:12:43 PM fastprop.cli.fastprop_cli] INFO: Training Parameters:\n",
-            " descriptors: all\n",
-            "enable_cache: true\n",
-            "precomputed: null\n",
-            "rescaling: true\n",
-            "zero_variance_drop: false\n",
-            "colinear_drop: false\n",
-            "fnn_layers: 2\n",
-            "learning_rate: 0.0001\n",
-            "batch_size: 64\n",
-            "number_epochs: 100\n",
-            "number_repeats: 4\n",
-            "problem_type: regression\n",
-            "checkpoint: null\n",
-            "train_size: 0.8\n",
-            "val_size: 0.1\n",
-            "test_size: 0.1\n",
-            "sampler: random\n",
-            "random_seed: 55\n",
-            "hidden_size: 1800\n",
-            "patience: 15\n",
-            "output_directory: pah\n",
-            "input_file: pah/arockiaraj_pah_data.csv\n",
-            "target_columns:\n",
-            "- log_p\n",
-            "smiles_column: smiles\n",
+            "Seed set to 562\n",
+            "[04/19/2024 07:32:02 PM fastprop.descriptors] INFO: Calculating descriptors\n",
+            "/usr/lib/python3.10/multiprocessing/popen_fork.py:66: RuntimeWarning: os.fork() was called. os.fork() is incompatible with multithreaded code, and JAX is multithreaded, so this will likely lead to a deadlock.\n",
+            "  self.pid = os.fork()\n",
+            "100% 55/55 [01:53<00:00,  2.06s/it]\n",
+            "[04/19/2024 07:33:56 PM fastprop.descriptors] INFO: Descriptor calculation complete, elapsed time: 0:01:53.434715\n",
+            "[04/19/2024 07:33:56 PM fastprop.descriptors] INFO: Cached descriptors to pah/cached_arockiaraj_pah_data_all_1713554970.csv\n",
+            "[04/19/2024 07:33:56 PM fastprop.cli.train] INFO: Run 'tensorboard --logdir pah/tensorboard_logs' to track training progress.\n",
+            "[04/19/2024 07:33:56 PM fastprop.cli.train] INFO: Training model 1 of 1 (random_seed=562)\n",
+            "/usr/local/lib/python3.10/dist-packages/astartes/main.py:325: ImperfectSplittingWarning: Actual train/test split differs from requested size. Requested validation size of 0.10, got 0.11. Requested test size of 0.10, got 0.11. \n",
+            "  warn(\n",
+            "[04/19/2024 07:33:56 PM fastprop.cli.train] INFO: Model architecture:\n",
+            "{fastprop(\n",
+            "  (fnn): Sequential(\n",
+            "    (lin1): Linear(in_features=1613, out_features=128, bias=True)\n",
+            "    (act1): ReLU()\n",
+            "    (lin2): Linear(in_features=128, out_features=128, bias=True)\n",
+            "    (act2): ReLU()\n",
+            "  )\n",
+            "  (readout): Linear(in_features=128, out_features=1, bias=True)\n",
+            ")}\n",
+            "GPU available: True (cuda), used: True\n",
+            "TPU available: False, using: 0 TPU cores\n",
+            "IPU available: False, using: 0 IPUs\n",
+            "HPU available: False, using: 0 HPUs\n",
+            "2024-04-19 19:33:58.989888: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:9261] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered\n",
+            "2024-04-19 19:33:58.989958: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:607] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered\n",
+            "2024-04-19 19:33:59.075069: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1515] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered\n",
+            "2024-04-19 19:34:00.210568: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT\n",
+            "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n",
             "\n",
-            "[02/08/2024 06:12:43 PM fastprop.utils.validate_config] WARNING: TODO: Input validation has not been implemented! Skipping.\n",
-            "[02/08/2024 06:12:43 PM fastprop.utils.load_data] INFO: Generating RDKit molecules from SMILES.\n",
-            "[02/08/2024 06:12:43 PM fastprop.utils.calculate_descriptors] INFO: Calculating descriptors using strategy='fast'\n",
-            "100% 55/55 [02:08<00:00,  2.34s/it]\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.calculate_descriptors] INFO: Cached descriptors to pah/cached_arockiaraj_pah_data_all_1707415931.csv.\n",
-            "[02/08/2024 06:14:53 PM fastprop.train] INFO: Preprocessing data...\n",
-            "[02/08/2024 06:14:53 PM fastprop.preprocessing] INFO: size after clean (impute missing, scale to unit variance): (55, 1443)\n",
-            "[02/08/2024 06:14:53 PM fastprop.train] INFO: ...done.\n",
-            "Actual train/test split differs from requested size. Requested validation size of 0.10, got 0.11. Requested test size of 0.10, got 0.11. \n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: Training ElasticNet(random_state=55) model repetition 0 as baseline...\n",
-            "Objective did not converge. You might want to increase the number of iterations, check the scale of the features or consider increasing regularisation. Duality gap: 1.042e-02, tolerance: 5.201e-03\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: ...done.\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: Baseline linear model statistics for validation:\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: RMSE: 0.5056\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: MAE: 0.3970\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: wMAPE: 0.0534\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: MAPE: 0.0590\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: Baseline linear model statistics for test:\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: RMSE: 0.4935\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: MAE: 0.4221\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: wMAPE: 0.0567\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: MAPE: 0.0628\n",
-            "Actual train/test split differs from requested size. Requested validation size of 0.10, got 0.11. Requested test size of 0.10, got 0.11. \n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: Training ElasticNet(random_state=56) model repetition 1 as baseline...\n",
-            "Objective did not converge. You might want to increase the number of iterations, check the scale of the features or consider increasing regularisation. Duality gap: 8.608e-03, tolerance: 4.688e-03\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: ...done.\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: Baseline linear model statistics for validation:\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: RMSE: 0.4986\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: MAE: 0.4200\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: wMAPE: 0.0549\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: MAPE: 0.0572\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: Baseline linear model statistics for test:\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: RMSE: 1.2014\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: MAE: 1.0411\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: wMAPE: 0.1354\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: MAPE: 0.1893\n",
-            "Actual train/test split differs from requested size. Requested validation size of 0.10, got 0.11. Requested test size of 0.10, got 0.11. \n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: Training ElasticNet(random_state=57) model repetition 2 as baseline...\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: ...done.\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: Baseline linear model statistics for validation:\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: RMSE: 1.4454\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: MAE: 1.0396\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: wMAPE: 0.1502\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: MAPE: 0.2351\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: Baseline linear model statistics for test:\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: RMSE: 3.4093\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: MAE: 2.5943\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: wMAPE: 0.2616\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: MAPE: 0.2283\n",
-            "Actual train/test split differs from requested size. Requested validation size of 0.10, got 0.11. Requested test size of 0.10, got 0.11. \n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: Training ElasticNet(random_state=58) model repetition 3 as baseline...\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: ...done.\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: Baseline linear model statistics for validation:\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: RMSE: 3.5556\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: MAE: 2.3275\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: wMAPE: 0.2667\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: MAPE: 0.2197\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: Baseline linear model statistics for test:\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: RMSE: 0.6020\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: MAE: 0.3933\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: wMAPE: 0.0485\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: MAPE: 0.0442\n",
-            "[02/08/2024 06:14:53 PM fastprop.utils.linear_baseline] INFO: Displaying summary baseline results:\n",
-            "                  count      mean       std       min       25%       50%       75%       max\n",
-            "validation-l2       4.0  1.501311  1.439907  0.498605  0.503870  0.975523  1.972964  3.555591\n",
-            "validation-l1       4.0  1.046043  0.904695  0.397039  0.414253  0.729797  1.361586  2.327539\n",
-            "validation-wmape    4.0  0.131330  0.100993  0.053423  0.054553  0.102581  0.179358  0.266736\n",
-            "validation-mape     4.0  0.142748  0.097961  0.057226  0.058522  0.139309  0.223535  0.235147\n",
-            "test-l2             4.0  1.426579  1.357993  0.493530  0.574917  0.901729  1.753390  3.409329\n",
-            "test-l1             4.0  1.112703  1.031931  0.393323  0.414918  0.731601  1.429386  2.594288\n",
-            "test-wmape          4.0  0.125553  0.098766  0.048518  0.054691  0.096065  0.166928  0.261565\n",
-            "test-mape           4.0  0.131143  0.091373  0.044172  0.058172  0.126048  0.199018  0.228305\n",
-            "[02/08/2024 06:14:54 PM fastprop.fastprop_core] INFO: Training model 1 of 4\n",
-            "[02/08/2024 06:14:54 PM pytorch_lightning.utilities.rank_zero] INFO: GPU available: True (cuda), used: True\n",
-            "[02/08/2024 06:14:54 PM pytorch_lightning.utilities.rank_zero] INFO: TPU available: False, using: 0 TPU cores\n",
-            "[02/08/2024 06:14:54 PM pytorch_lightning.utilities.rank_zero] INFO: IPU available: False, using: 0 IPUs\n",
-            "[02/08/2024 06:14:54 PM pytorch_lightning.utilities.rank_zero] INFO: HPU available: False, using: 0 HPUs\n",
-            "2024-02-08 18:14:54.687764: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:9261] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered\n",
-            "2024-02-08 18:14:54.687818: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:607] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered\n",
-            "2024-02-08 18:14:54.689122: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1515] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered\n",
-            "2024-02-08 18:14:56.092846: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT\n",
-            "[02/08/2024 06:14:56 PM fastprop.fastprop_core] INFO: Sampling dataset with random sampler.\n",
-            "Actual train/test split differs from requested size. Requested validation size of 0.10, got 0.11. Requested test size of 0.10, got 0.11. \n",
-            "Checkpoint directory /content/fastprop/benchmarks/pah/fastprop_1707415963/checkpoints exists and is not empty.\n",
-            "[02/08/2024 06:14:56 PM pytorch_lightning.accelerators.cuda] INFO: LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n",
-            "[02/08/2024 06:14:57 PM pytorch_lightning.callbacks.model_summary] INFO: \n",
             "  | Name    | Type       | Params\n",
             "---------------------------------------\n",
-            "0 | fnn     | Sequential | 5.8 M \n",
-            "1 | readout | Linear     | 1.8 K \n",
+            "0 | fnn     | Sequential | 223 K \n",
+            "1 | readout | Linear     | 129   \n",
             "---------------------------------------\n",
-            "5.8 M     Trainable params\n",
+            "223 K     Trainable params\n",
             "0         Non-trainable params\n",
-            "5.8 M     Total params\n",
-            "23.371    Total estimated model params size (MB)\n",
-            "[02/08/2024 06:14:58 PM fastprop.fastprop_core] INFO: Epoch [5/100]\n",
-            "[02/08/2024 06:14:58 PM fastprop.fastprop_core] INFO: Epoch [10/100]\n",
-            "[02/08/2024 06:14:58 PM fastprop.fastprop_core] INFO: Epoch [15/100]\n",
-            "[02/08/2024 06:14:58 PM fastprop.fastprop_core] INFO: Epoch [20/100]\n",
-            "[02/08/2024 06:14:58 PM fastprop.fastprop_core] INFO: Elapsed time during training: 0:00:04.340715\n",
-            "[02/08/2024 06:14:58 PM fastprop.fastprop_core] INFO: Sampling dataset with random sampler.\n",
-            "Actual train/test split differs from requested size. Requested validation size of 0.10, got 0.11. Requested test size of 0.10, got 0.11. \n",
-            "[02/08/2024 06:14:58 PM pytorch_lightning.accelerators.cuda] INFO: LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n",
-            "[02/08/2024 06:14:58 PM fastprop.fastprop_core] INFO: Sampling dataset with random sampler.\n",
-            "Actual train/test split differs from requested size. Requested validation size of 0.10, got 0.11. Requested test size of 0.10, got 0.11. \n",
-            "[02/08/2024 06:14:58 PM pytorch_lightning.accelerators.cuda] INFO: LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n",
-            "[02/08/2024 06:14:58 PM fastprop.fastprop_core] INFO: Displaying validation results for repetition 1:\n",
-            "                        value\n",
-            "validation_mse_loss  0.005667\n",
-            "validation_r2        0.973674\n",
-            "validation_mape      0.019301\n",
-            "validation_wmape     0.018532\n",
-            "validation_l1        0.137731\n",
-            "validation_mdae      0.098858\n",
-            "validation_rmse      0.161215\n",
-            "[02/08/2024 06:14:58 PM fastprop.fastprop_core] INFO: Displaying validation results for repetition 1:\n",
-            "                  value\n",
-            "test_mse_loss  0.007560\n",
-            "test_r2        0.967383\n",
-            "test_mape      0.021820\n",
-            "test_wmape     0.020520\n",
-            "test_l1        0.152637\n",
-            "test_mdae      0.156447\n",
-            "test_rmse      0.186213\n",
-            "[02/08/2024 06:14:59 PM fastprop.fastprop_core] INFO: Training model 2 of 4\n",
-            "[02/08/2024 06:14:59 PM pytorch_lightning.utilities.rank_zero] INFO: GPU available: True (cuda), used: True\n",
-            "[02/08/2024 06:14:59 PM pytorch_lightning.utilities.rank_zero] INFO: TPU available: False, using: 0 TPU cores\n",
-            "[02/08/2024 06:14:59 PM pytorch_lightning.utilities.rank_zero] INFO: IPU available: False, using: 0 IPUs\n",
-            "[02/08/2024 06:14:59 PM pytorch_lightning.utilities.rank_zero] INFO: HPU available: False, using: 0 HPUs\n",
-            "[02/08/2024 06:14:59 PM fastprop.fastprop_core] INFO: Sampling dataset with random sampler.\n",
-            "Actual train/test split differs from requested size. Requested validation size of 0.10, got 0.11. Requested test size of 0.10, got 0.11. \n",
-            "Checkpoint directory /content/fastprop/benchmarks/pah/fastprop_1707415963/checkpoints exists and is not empty.\n",
-            "[02/08/2024 06:14:59 PM pytorch_lightning.accelerators.cuda] INFO: LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n",
-            "[02/08/2024 06:14:59 PM pytorch_lightning.callbacks.model_summary] INFO: \n",
-            "  | Name    | Type       | Params\n",
-            "---------------------------------------\n",
-            "0 | fnn     | Sequential | 5.8 M \n",
-            "1 | readout | Linear     | 1.8 K \n",
-            "---------------------------------------\n",
-            "5.8 M     Trainable params\n",
-            "0         Non-trainable params\n",
-            "5.8 M     Total params\n",
-            "23.371    Total estimated model params size (MB)\n",
-            "[02/08/2024 06:14:59 PM fastprop.fastprop_core] INFO: Epoch [5/100]\n",
-            "[02/08/2024 06:15:00 PM fastprop.fastprop_core] INFO: Epoch [10/100]\n",
-            "[02/08/2024 06:15:00 PM fastprop.fastprop_core] INFO: Epoch [15/100]\n",
-            "[02/08/2024 06:15:00 PM fastprop.fastprop_core] INFO: Epoch [20/100]\n",
-            "[02/08/2024 06:15:00 PM fastprop.fastprop_core] INFO: Elapsed time during training: 0:00:01.609038\n",
-            "[02/08/2024 06:15:00 PM fastprop.fastprop_core] INFO: Sampling dataset with random sampler.\n",
-            "Actual train/test split differs from requested size. Requested validation size of 0.10, got 0.11. Requested test size of 0.10, got 0.11. \n",
-            "[02/08/2024 06:15:00 PM pytorch_lightning.accelerators.cuda] INFO: LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n",
-            "[02/08/2024 06:15:00 PM fastprop.fastprop_core] INFO: Sampling dataset with random sampler.\n",
-            "Actual train/test split differs from requested size. Requested validation size of 0.10, got 0.11. Requested test size of 0.10, got 0.11. \n",
-            "[02/08/2024 06:15:00 PM pytorch_lightning.accelerators.cuda] INFO: LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n",
-            "[02/08/2024 06:15:00 PM fastprop.fastprop_core] INFO: Displaying validation results for repetition 2:\n",
-            "                        value\n",
-            "validation_mse_loss  0.007016\n",
-            "validation_r2        0.962000\n",
-            "validation_mape      0.022652\n",
-            "validation_wmape     0.022227\n",
-            "validation_l1        0.169949\n",
-            "validation_mdae      0.140838\n",
-            "validation_rmse      0.179385\n",
-            "[02/08/2024 06:15:00 PM fastprop.fastprop_core] INFO: Displaying validation results for repetition 2:\n",
-            "                  value\n",
-            "test_mse_loss  0.019230\n",
-            "test_r2        0.983737\n",
-            "test_mape      0.046429\n",
-            "test_wmape     0.032332\n",
-            "test_l1        0.248633\n",
-            "test_mdae      0.191107\n",
-            "test_rmse      0.296987\n",
-            "[02/08/2024 06:15:00 PM fastprop.fastprop_core] INFO: Training model 3 of 4\n",
-            "[02/08/2024 06:15:01 PM pytorch_lightning.utilities.rank_zero] INFO: GPU available: True (cuda), used: True\n",
-            "[02/08/2024 06:15:01 PM pytorch_lightning.utilities.rank_zero] INFO: TPU available: False, using: 0 TPU cores\n",
-            "[02/08/2024 06:15:01 PM pytorch_lightning.utilities.rank_zero] INFO: IPU available: False, using: 0 IPUs\n",
-            "[02/08/2024 06:15:01 PM pytorch_lightning.utilities.rank_zero] INFO: HPU available: False, using: 0 HPUs\n",
-            "[02/08/2024 06:15:01 PM fastprop.fastprop_core] INFO: Sampling dataset with random sampler.\n",
-            "Actual train/test split differs from requested size. Requested validation size of 0.10, got 0.11. Requested test size of 0.10, got 0.11. \n",
-            "Checkpoint directory /content/fastprop/benchmarks/pah/fastprop_1707415963/checkpoints exists and is not empty.\n",
-            "[02/08/2024 06:15:01 PM pytorch_lightning.accelerators.cuda] INFO: LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n",
-            "[02/08/2024 06:15:01 PM pytorch_lightning.callbacks.model_summary] INFO: \n",
-            "  | Name    | Type       | Params\n",
-            "---------------------------------------\n",
-            "0 | fnn     | Sequential | 5.8 M \n",
-            "1 | readout | Linear     | 1.8 K \n",
-            "---------------------------------------\n",
-            "5.8 M     Trainable params\n",
-            "0         Non-trainable params\n",
-            "5.8 M     Total params\n",
-            "23.371    Total estimated model params size (MB)\n",
-            "[02/08/2024 06:15:01 PM fastprop.fastprop_core] INFO: Epoch [5/100]\n",
-            "[02/08/2024 06:15:01 PM fastprop.fastprop_core] INFO: Epoch [10/100]\n",
-            "[02/08/2024 06:15:04 PM fastprop.fastprop_core] INFO: Epoch [15/100]\n",
-            "[02/08/2024 06:15:04 PM fastprop.fastprop_core] INFO: Epoch [20/100]\n",
-            "[02/08/2024 06:15:04 PM fastprop.fastprop_core] INFO: Epoch [25/100]\n",
-            "[02/08/2024 06:15:04 PM fastprop.fastprop_core] INFO: Elapsed time during training: 0:00:03.391862\n",
-            "[02/08/2024 06:15:04 PM fastprop.fastprop_core] INFO: Sampling dataset with random sampler.\n",
-            "Actual train/test split differs from requested size. Requested validation size of 0.10, got 0.11. Requested test size of 0.10, got 0.11. \n",
-            "[02/08/2024 06:15:04 PM pytorch_lightning.accelerators.cuda] INFO: LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n",
-            "[02/08/2024 06:15:04 PM fastprop.fastprop_core] INFO: Sampling dataset with random sampler.\n",
-            "Actual train/test split differs from requested size. Requested validation size of 0.10, got 0.11. Requested test size of 0.10, got 0.11. \n",
-            "[02/08/2024 06:15:04 PM pytorch_lightning.accelerators.cuda] INFO: LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n",
-            "[02/08/2024 06:15:04 PM fastprop.fastprop_core] INFO: Displaying validation results for repetition 3:\n",
-            "                        value\n",
-            "validation_mse_loss  0.012857\n",
-            "validation_r2        0.985313\n",
-            "validation_mape      0.028447\n",
-            "validation_wmape     0.024040\n",
-            "validation_l1        0.166356\n",
-            "validation_mdae      0.123438\n",
-            "validation_rmse      0.242837\n",
-            "[02/08/2024 06:15:04 PM fastprop.fastprop_core] INFO: Displaying validation results for repetition 3:\n",
-            "                  value\n",
-            "test_mse_loss  0.055089\n",
-            "test_r2        0.977502\n",
-            "test_mape      0.023053\n",
-            "test_wmape     0.030364\n",
-            "test_l1        0.301158\n",
-            "test_mdae      0.129379\n",
-            "test_rmse      0.502663\n",
-            "[02/08/2024 06:15:04 PM fastprop.fastprop_core] INFO: Training model 4 of 4\n",
-            "[02/08/2024 06:15:04 PM pytorch_lightning.utilities.rank_zero] INFO: GPU available: True (cuda), used: True\n",
-            "[02/08/2024 06:15:04 PM pytorch_lightning.utilities.rank_zero] INFO: TPU available: False, using: 0 TPU cores\n",
-            "[02/08/2024 06:15:04 PM pytorch_lightning.utilities.rank_zero] INFO: IPU available: False, using: 0 IPUs\n",
-            "[02/08/2024 06:15:04 PM pytorch_lightning.utilities.rank_zero] INFO: HPU available: False, using: 0 HPUs\n",
-            "[02/08/2024 06:15:04 PM fastprop.fastprop_core] INFO: Sampling dataset with random sampler.\n",
-            "Actual train/test split differs from requested size. Requested validation size of 0.10, got 0.11. Requested test size of 0.10, got 0.11. \n",
-            "Checkpoint directory /content/fastprop/benchmarks/pah/fastprop_1707415963/checkpoints exists and is not empty.\n",
-            "[02/08/2024 06:15:04 PM pytorch_lightning.accelerators.cuda] INFO: LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n",
-            "[02/08/2024 06:15:04 PM pytorch_lightning.callbacks.model_summary] INFO: \n",
-            "  | Name    | Type       | Params\n",
-            "---------------------------------------\n",
-            "0 | fnn     | Sequential | 5.8 M \n",
-            "1 | readout | Linear     | 1.8 K \n",
-            "---------------------------------------\n",
-            "5.8 M     Trainable params\n",
-            "0         Non-trainable params\n",
-            "5.8 M     Total params\n",
-            "23.371    Total estimated model params size (MB)\n",
-            "[02/08/2024 06:15:05 PM fastprop.fastprop_core] INFO: Epoch [5/100]\n",
-            "[02/08/2024 06:15:06 PM fastprop.fastprop_core] INFO: Epoch [10/100]\n",
-            "[02/08/2024 06:15:06 PM fastprop.fastprop_core] INFO: Epoch [15/100]\n",
-            "[02/08/2024 06:15:07 PM fastprop.fastprop_core] INFO: Epoch [20/100]\n",
-            "[02/08/2024 06:15:07 PM fastprop.fastprop_core] INFO: Epoch [25/100]\n",
-            "[02/08/2024 06:15:07 PM fastprop.fastprop_core] INFO: Epoch [30/100]\n",
-            "[02/08/2024 06:15:07 PM fastprop.fastprop_core] INFO: Elapsed time during training: 0:00:02.603157\n",
-            "[02/08/2024 06:15:07 PM fastprop.fastprop_core] INFO: Sampling dataset with random sampler.\n",
-            "Actual train/test split differs from requested size. Requested validation size of 0.10, got 0.11. Requested test size of 0.10, got 0.11. \n",
-            "[02/08/2024 06:15:07 PM pytorch_lightning.accelerators.cuda] INFO: LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n",
-            "[02/08/2024 06:15:07 PM fastprop.fastprop_core] INFO: Sampling dataset with random sampler.\n",
-            "Actual train/test split differs from requested size. Requested validation size of 0.10, got 0.11. Requested test size of 0.10, got 0.11. \n",
-            "[02/08/2024 06:15:07 PM pytorch_lightning.accelerators.cuda] INFO: LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n",
-            "[02/08/2024 06:15:07 PM fastprop.fastprop_core] INFO: Displaying validation results for repetition 4:\n",
-            "                        value\n",
-            "validation_mse_loss  0.008233\n",
-            "validation_r2        0.997224\n",
-            "validation_mape      0.020658\n",
-            "validation_wmape     0.018859\n",
-            "validation_l1        0.164559\n",
-            "validation_mdae      0.198991\n",
-            "validation_rmse      0.194318\n",
-            "[02/08/2024 06:15:07 PM fastprop.fastprop_core] INFO: Displaying validation results for repetition 4:\n",
-            "                  value\n",
-            "test_mse_loss  0.009988\n",
-            "test_r2        0.933815\n",
-            "test_mape      0.022602\n",
-            "test_wmape     0.021500\n",
-            "test_l1        0.174293\n",
-            "test_mdae      0.135692\n",
-            "test_rmse      0.214039\n",
-            "[02/08/2024 06:15:07 PM fastprop.fastprop_core] INFO: Displaying validation results:\n",
-            "                     count      mean       std       min       25%       50%       75%       max\n",
-            "validation_mse_loss    4.0  0.008443  0.003124  0.005667  0.006679  0.007624  0.009389  0.012857\n",
-            "validation_r2          4.0  0.979553  0.015145  0.962000  0.970756  0.979493  0.988290  0.997224\n",
-            "validation_mape        4.0  0.022765  0.004030  0.019301  0.020319  0.021655  0.024101  0.028447\n",
-            "validation_wmape       4.0  0.020914  0.002670  0.018532  0.018777  0.020543  0.022680  0.024040\n",
-            "validation_l1          4.0  0.159649  0.014783  0.137731  0.157852  0.165458  0.167254  0.169949\n",
-            "validation_mdae        4.0  0.140531  0.042609  0.098858  0.117293  0.132138  0.155376  0.198991\n",
-            "validation_rmse        4.0  0.194439  0.034990  0.161215  0.174843  0.186852  0.206448  0.242837\n",
-            "[02/08/2024 06:15:07 PM fastprop.fastprop_core] INFO: Displaying testing results:\n",
-            "               count      mean       std       min       25%       50%       75%       max\n",
-            "test_mse_loss    4.0  0.022967  0.021997  0.007560  0.009381  0.014609  0.028195  0.055089\n",
-            "test_r2          4.0  0.965609  0.022242  0.933815  0.958991  0.972443  0.979061  0.983737\n",
-            "test_mape        4.0  0.028476  0.011980  0.021820  0.022406  0.022828  0.028897  0.046429\n",
-            "test_wmape       4.0  0.026179  0.006036  0.020520  0.021255  0.025932  0.030856  0.032332\n",
-            "test_l1          4.0  0.219180  0.068388  0.152637  0.168879  0.211463  0.261764  0.301158\n",
-            "test_mdae        4.0  0.153156  0.027817  0.129379  0.134114  0.146069  0.165112  0.191107\n",
-            "test_rmse        4.0  0.299975  0.143083  0.186213  0.207083  0.255513  0.348406  0.502663\n",
-            "[02/08/2024 06:15:07 PM fastprop.fastprop_core] INFO: 2-sided T-test between validation and testing rmse yielded p value of p=0.202>0.05.\n",
-            "[02/08/2024 06:15:07 PM fastprop.cli.fastprop_cli] INFO: If you use fastprop in published work, please cite: ...WIP...\n"
+            "223 K     Total params\n",
+            "0.893     Total estimated model params size (MB)\n",
+            "/usr/lib/python3.10/multiprocessing/popen_fork.py:66: RuntimeWarning: os.fork() was called. os.fork() is incompatible with multithreaded code, and JAX is multithreaded, so this will likely lead to a deadlock.\n",
+            "  self.pid = os.fork()\n",
+            "Epoch 0: 100% 1/1 [00:00<00:00,  3.50it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 151.81it/s]\u001b[A\n",
+            "Epoch 1: 100% 1/1 [00:00<00:00, 118.19it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 218.28it/s]\u001b[A\n",
+            "Epoch 2: 100% 1/1 [00:00<00:00, 122.53it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 204.76it/s]\u001b[A\n",
+            "Epoch 3: 100% 1/1 [00:00<00:00, 134.31it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 221.45it/s]\u001b[A\n",
+            "Epoch 4: 100% 1/1 [00:00<00:00, 117.04it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 216.32it/s]\u001b[A\n",
+            "Epoch 5: 100% 1/1 [00:00<00:00, 125.51it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 215.30it/s]\u001b[A\n",
+            "Epoch 6: 100% 1/1 [00:00<00:00, 143.27it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 229.79it/s]\u001b[A\n",
+            "Epoch 7: 100% 1/1 [00:00<00:00, 128.88it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 147.69it/s]\u001b[A\n",
+            "Epoch 8: 100% 1/1 [00:00<00:00, 112.67it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 210.47it/s]\u001b[A\n",
+            "Epoch 9: 100% 1/1 [00:00<00:00, 128.20it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 215.21it/s]\u001b[A\n",
+            "Epoch 10: 100% 1/1 [00:00<00:00, 144.00it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 209.48it/s]\u001b[A\n",
+            "Epoch 11: 100% 1/1 [00:00<00:00, 126.67it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 220.99it/s]\u001b[A\n",
+            "Epoch 12: 100% 1/1 [00:00<00:00, 95.25it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 186.23it/s]\u001b[A\n",
+            "Epoch 13: 100% 1/1 [00:00<00:00, 143.68it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 192.18it/s]\u001b[A\n",
+            "Epoch 14: 100% 1/1 [00:00<00:00, 130.44it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 231.64it/s]\u001b[A\n",
+            "Epoch 15: 100% 1/1 [00:00<00:00, 137.17it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 252.20it/s]\u001b[A\n",
+            "Epoch 16: 100% 1/1 [00:00<00:00, 149.65it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 233.04it/s]\u001b[A\n",
+            "Epoch 17: 100% 1/1 [00:00<00:00, 125.75it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 247.10it/s]\u001b[A\n",
+            "Epoch 18: 100% 1/1 [00:00<00:00, 149.13it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 252.99it/s]\u001b[A\n",
+            "Epoch 19: 100% 1/1 [00:00<00:00, 130.93it/s, v_num=on_1]\n",
+            "Validation: |          | 0/? [00:00<?, ?it/s]\u001b[A\n",
+            "Validation:   0% 0/1 [00:00<?, ?it/s]        \u001b[A\n",
+            "Validation DataLoader 0:   0% 0/1 [00:00<?, ?it/s]\u001b[A\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 215.98it/s]\u001b[A\n",
+            "Epoch 19: 100% 1/1 [00:00<00:00, 50.87it/s, v_num=on_1]`Trainer.fit` stopped: `max_epochs=20` reached.\n",
+            "Epoch 19: 100% 1/1 [00:00<00:00, 32.33it/s, v_num=on_1]\n",
+            "[04/19/2024 07:34:03 PM fastprop.model] INFO: Elapsed time during training: 0:00:06.392899\n",
+            "[04/19/2024 07:34:03 PM fastprop.model] INFO: Reloading best model from checkpoint file: pah/fastprop_1713555122/checkpoints/repetition-1-epoch=19-val_loss=0.00.ckpt\n",
+            "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n",
+            "/usr/lib/python3.10/multiprocessing/popen_fork.py:66: RuntimeWarning: os.fork() was called. os.fork() is incompatible with multithreaded code, and JAX is multithreaded, so this will likely lead to a deadlock.\n",
+            "  self.pid = os.fork()\n",
+            "Validation DataLoader 0: 100% 1/1 [00:00<00:00, 50.70it/s]\n",
+            "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n",
+            "/usr/lib/python3.10/multiprocessing/popen_fork.py:66: RuntimeWarning: os.fork() was called. os.fork() is incompatible with multithreaded code, and JAX is multithreaded, so this will likely lead to a deadlock.\n",
+            "  self.pid = os.fork()\n",
+            "Testing DataLoader 0: 100% 1/1 [00:00<00:00, 49.66it/s]\n",
+            "[04/19/2024 07:34:03 PM fastprop.model] INFO: Displaying validation results for repetition 1:\n",
+            "                                                             value\n",
+            "validation_mse_loss                                       0.039866\n",
+            "validation_r2_score                                       0.872485\n",
+            "validation_mean_absolute_percentage_error_score           0.044972\n",
+            "validation_weighted_mean_absolute_percentage_error_score  0.048963\n",
+            "validation_mean_absolute_error_score                      0.358413\n",
+            "validation_root_mean_squared_error_loss                   0.463780\n",
+            "[04/19/2024 07:34:03 PM fastprop.model] INFO: Displaying validation results for repetition 1:\n",
+            "                                                       value\n",
+            "test_mse_loss                                       0.079504\n",
+            "test_r2_score                                       0.690164\n",
+            "test_mean_absolute_percentage_error_score           0.079382\n",
+            "test_weighted_mean_absolute_percentage_error_score  0.077446\n",
+            "test_mean_absolute_error_score                      0.570651\n",
+            "test_root_mean_squared_error_loss                   0.654945\n",
+            "[04/19/2024 07:34:03 PM fastprop.cli.train] INFO: Displaying validation results:\n",
+            "                                                          count      mean  std       min       25%       50%       75%       max\n",
+            "validation_mse_loss                                         1.0  0.039866  NaN  0.039866  0.039866  0.039866  0.039866  0.039866\n",
+            "validation_r2_score                                         1.0  0.872485  NaN  0.872485  0.872485  0.872485  0.872485  0.872485\n",
+            "validation_mean_absolute_percentage_error_score             1.0  0.044972  NaN  0.044972  0.044972  0.044972  0.044972  0.044972\n",
+            "validation_weighted_mean_absolute_percentage_error_score    1.0  0.048963  NaN  0.048963  0.048963  0.048963  0.048963  0.048963\n",
+            "validation_mean_absolute_error_score                        1.0  0.358413  NaN  0.358413  0.358413  0.358413  0.358413  0.358413\n",
+            "validation_root_mean_squared_error_loss                     1.0  0.463780  NaN  0.463780  0.463780  0.463780  0.463780  0.463780\n",
+            "[04/19/2024 07:34:03 PM fastprop.cli.train] INFO: Displaying testing results:\n",
+            "                                                    count      mean  std       min       25%       50%       75%       max\n",
+            "test_mse_loss                                         1.0  0.079504  NaN  0.079504  0.079504  0.079504  0.079504  0.079504\n",
+            "test_r2_score                                         1.0  0.690164  NaN  0.690164  0.690164  0.690164  0.690164  0.690164\n",
+            "test_mean_absolute_percentage_error_score             1.0  0.079382  NaN  0.079382  0.079382  0.079382  0.079382  0.079382\n",
+            "test_weighted_mean_absolute_percentage_error_score    1.0  0.077446  NaN  0.077446  0.077446  0.077446  0.077446  0.077446\n",
+            "test_mean_absolute_error_score                        1.0  0.570651  NaN  0.570651  0.570651  0.570651  0.570651  0.570651\n",
+            "test_root_mean_squared_error_loss                     1.0  0.654945  NaN  0.654945  0.654945  0.654945  0.654945  0.654945\n",
+            "[04/19/2024 07:34:03 PM fastprop.cli.train] INFO: fastprop is unable to generate statistics to check for overfitting, consider increasing 'num_repeats' to at least 2.\n",
+            "[04/19/2024 07:34:03 PM fastprop.cli.base] INFO: If you use fastprop in published work, please cite https://arxiv.org/abs/2404.02058\n",
+            "[04/19/2024 07:34:03 PM fastprop.cli.base] INFO: Total elapsed time: 0:02:00.910982\n"
           ]
         }
       ],
@@ -566,13 +432,13 @@
     },
     {
       "cell_type": "code",
-      "execution_count": 18,
+      "execution_count": 6,
       "metadata": {
         "colab": {
           "base_uri": "https://localhost:8080/"
         },
         "id": "zjJbks6-g7ZV",
-        "outputId": "0123092c-eaf8-478b-f8cf-62d35c98aba7"
+        "outputId": "5c423ad7-8a73-4753-dedf-16652f67100d"
       },
       "outputs": [
         {
@@ -581,53 +447,21 @@
           "text": [
             "\u001b[01;34mpah\u001b[0m\n",
             "├── \u001b[00marockiaraj_pah_data.csv\u001b[0m\n",
-            "├── \u001b[00mcached_arockiaraj_pah_data_all_1707415931.csv\u001b[0m\n",
+            "├── \u001b[00mcached_arockiaraj_pah_data_all_1713554970.csv\u001b[0m\n",
             "├── \u001b[00mchemprop_pah.sh\u001b[0m\n",
-            "├── \u001b[01;34mfastprop_1707415963\u001b[0m\n",
+            "├── \u001b[01;34mfastprop_1713555122\u001b[0m\n",
             "│   ├── \u001b[01;34mcheckpoints\u001b[0m\n",
-            "│   │   ├── \u001b[00mfastprop_config.yml\u001b[0m\n",
-            "│   │   ├── \u001b[00mrepetition-1-epoch=04-val_loss=0.00.ckpt\u001b[0m\n",
-            "│   │   ├── \u001b[00mrepetition-2-epoch=05-val_loss=0.00.ckpt\u001b[0m\n",
-            "│   │   ├── \u001b[00mrepetition-3-epoch=12-val_loss=0.00.ckpt\u001b[0m\n",
-            "│   │   └── \u001b[00mrepetition-4-epoch=14-val_loss=0.00.ckpt\u001b[0m\n",
-            "│   ├── \u001b[01;34mcsv_logs\u001b[0m\n",
-            "│   │   ├── \u001b[01;34mrepetition_1\u001b[0m\n",
-            "│   │   │   ├── \u001b[00mhparams.yaml\u001b[0m\n",
-            "│   │   │   └── \u001b[00mmetrics.csv\u001b[0m\n",
-            "│   │   ├── \u001b[01;34mrepetition_2\u001b[0m\n",
-            "│   │   │   ├── \u001b[00mhparams.yaml\u001b[0m\n",
-            "│   │   │   └── \u001b[00mmetrics.csv\u001b[0m\n",
-            "│   │   ├── \u001b[01;34mrepetition_3\u001b[0m\n",
-            "│   │   │   ├── \u001b[00mhparams.yaml\u001b[0m\n",
-            "│   │   │   └── \u001b[00mmetrics.csv\u001b[0m\n",
-            "│   │   └── \u001b[01;34mrepetition_4\u001b[0m\n",
-            "│   │       ├── \u001b[00mhparams.yaml\u001b[0m\n",
-            "│   │       └── \u001b[00mmetrics.csv\u001b[0m\n",
+            "│   │   └── \u001b[00mrepetition-1-epoch=19-val_loss=0.00.ckpt\u001b[0m\n",
             "│   ├── \u001b[00mfastprop_log.txt\u001b[0m\n",
             "│   └── \u001b[01;34mtensorboard_logs\u001b[0m\n",
-            "│       ├── \u001b[01;34mrepetition_1\u001b[0m\n",
-            "│       │   ├── \u001b[00mevents.out.tfevents.1707416096.67acd398d733.13637.0\u001b[0m\n",
-            "│       │   ├── \u001b[00mevents.out.tfevents.1707416098.67acd398d733.13637.1\u001b[0m\n",
-            "│       │   ├── \u001b[00mevents.out.tfevents.1707416098.67acd398d733.13637.2\u001b[0m\n",
-            "│       │   └── \u001b[00mhparams.yaml\u001b[0m\n",
-            "│       ├── \u001b[01;34mrepetition_2\u001b[0m\n",
-            "│       │   ├── \u001b[00mevents.out.tfevents.1707416099.67acd398d733.13637.3\u001b[0m\n",
-            "│       │   ├── \u001b[00mevents.out.tfevents.1707416100.67acd398d733.13637.4\u001b[0m\n",
-            "│       │   ├── \u001b[00mevents.out.tfevents.1707416100.67acd398d733.13637.5\u001b[0m\n",
-            "│       │   └── \u001b[00mhparams.yaml\u001b[0m\n",
-            "│       ├── \u001b[01;34mrepetition_3\u001b[0m\n",
-            "│       │   ├── \u001b[00mevents.out.tfevents.1707416101.67acd398d733.13637.6\u001b[0m\n",
-            "│       │   ├── \u001b[00mevents.out.tfevents.1707416104.67acd398d733.13637.7\u001b[0m\n",
-            "│       │   ├── \u001b[00mevents.out.tfevents.1707416104.67acd398d733.13637.8\u001b[0m\n",
-            "│       │   └── \u001b[00mhparams.yaml\u001b[0m\n",
-            "│       └── \u001b[01;34mrepetition_4\u001b[0m\n",
-            "│           ├── \u001b[00mevents.out.tfevents.1707416104.67acd398d733.13637.9\u001b[0m\n",
-            "│           ├── \u001b[00mevents.out.tfevents.1707416107.67acd398d733.13637.10\u001b[0m\n",
-            "│           ├── \u001b[00mevents.out.tfevents.1707416107.67acd398d733.13637.11\u001b[0m\n",
+            "│       └── \u001b[01;34mrepetition_1\u001b[0m\n",
+            "│           ├── \u001b[00mevents.out.tfevents.1713555240.1fb33fc37668.3606.0\u001b[0m\n",
+            "│           ├── \u001b[00mevents.out.tfevents.1713555243.1fb33fc37668.3606.1\u001b[0m\n",
+            "│           ├── \u001b[00mevents.out.tfevents.1713555243.1fb33fc37668.3606.2\u001b[0m\n",
             "│           └── \u001b[00mhparams.yaml\u001b[0m\n",
             "└── \u001b[00mpah.yml\u001b[0m\n",
             "\n",
-            "12 directories, 34 files\n"
+            "4 directories, 10 files\n"
           ]
         }
       ],
@@ -656,24 +490,22 @@
     },
     {
       "cell_type": "code",
-      "execution_count": 19,
+      "execution_count": 7,
       "metadata": {
         "colab": {
           "base_uri": "https://localhost:8080/"
         },
         "id": "bVwwAvcWmsGH",
-        "outputId": "3005655f-fddb-4c49-d8c4-17482d8b308f"
+        "outputId": "2d0013b2-8681-4724-a0ff-176f5a880d80"
       },
       "outputs": [
         {
           "output_type": "stream",
           "name": "stdout",
           "text": [
-            "[02/08/2024 06:15:10 PM numexpr.utils] INFO: NumExpr defaulting to 2 threads.\n",
             "usage: fastprop train [-h] [-od OUTPUT_DIRECTORY] [-if INPUT_FILE]\n",
             "                      [-tc TARGET_COLUMNS [TARGET_COLUMNS ...]] [-sc SMILES_COLUMN]\n",
-            "                      [-d DESCRIPTORS] [-ec ENABLE_CACHE] [-p PRECOMPUTED] [-r RESCALING]\n",
-            "                      [-zvd ZERO_VARIANCE_DROP] [-cd COLINEAR_DROP] [-op] [-fl FNN_LAYERS]\n",
+            "                      [-d DESCRIPTORS] [-ec ENABLE_CACHE] [-p PRECOMPUTED] [-op] [-fl FNN_LAYERS]\n",
             "                      [-lr LEARNING_RATE] [-bs BATCH_SIZE] [-ne NUMBER_EPOCHS]\n",
             "                      [-nr NUMBER_REPEATS] [-pt PROBLEM_TYPE] [-ns TRAIN_SIZE] [-vs VAL_SIZE]\n",
             "                      [-ts TEST_SIZE] [-s SAMPLER] [-rs RANDOM_SEED] [-pc PATIENCE]\n",
@@ -693,17 +525,11 @@
             "  -sc SMILES_COLUMN, --smiles-column SMILES_COLUMN\n",
             "                        column name for SMILES\n",
             "  -d DESCRIPTORS, --descriptors DESCRIPTORS\n",
-            "                        descriptors to calculate (one of all, optimized, smallest, or search)\n",
+            "                        descriptors to calculate (one of all, optimized, or debug)\n",
             "  -ec ENABLE_CACHE, --enable-cache ENABLE_CACHE\n",
             "                        allow saving and loading of cached descriptors\n",
             "  -p PRECOMPUTED, --precomputed PRECOMPUTED\n",
             "                        precomputed descriptors from fastprop or mordred\n",
-            "  -r RESCALING, --rescaling RESCALING\n",
-            "                        rescale descriptors between 0 and 1 (default to True)\n",
-            "  -zvd ZERO_VARIANCE_DROP, --zero-variance-drop ZERO_VARIANCE_DROP\n",
-            "                        drop zero variance descriptors (defaults to True)\n",
-            "  -cd COLINEAR_DROP, --colinear-drop COLINEAR_DROP\n",
-            "                        drop colinear descriptors (defaults to False)\n",
             "  -op, --optimize       run hyperparameter optimization\n",
             "  -fl FNN_LAYERS, --fnn-layers FNN_LAYERS\n",
             "                        number of fnn layers\n",
@@ -748,31 +574,36 @@
     },
     {
       "cell_type": "code",
-      "execution_count": 20,
+      "execution_count": 8,
       "metadata": {
         "id": "J_WsdsDGmf8j",
         "colab": {
           "base_uri": "https://localhost:8080/"
         },
-        "outputId": "62086417-0fa3-4500-ebbd-196553957d80"
+        "outputId": "3b965048-3356-4670-eeb7-660cc62ba9b0"
       },
       "outputs": [
         {
           "output_type": "stream",
           "name": "stdout",
           "text": [
-            "[02/08/2024 06:15:19 PM numexpr.utils] INFO: NumExpr defaulting to 2 threads.\n",
-            "usage: fastprop predict [-h] [-s SMILES [SMILES ...] | -i INPUT_FILE] [-o OUTPUT] checkpoints_dir\n",
+            "usage: fastprop predict [-h] [-ss SMILES_STRINGS [SMILES_STRINGS ...] | -sf SMILES_FILE]\n",
+            "                        (-ds DESCRIPTOR_SET | -pd PRECOMPUTED_DESCRIPTORS) [-o OUTPUT]\n",
+            "                        checkpoints_dir\n",
             "\n",
             "positional arguments:\n",
             "  checkpoints_dir       directory of checkpoint file(s) for predictions\n",
             "\n",
             "options:\n",
             "  -h, --help            show this help message and exit\n",
-            "  -s SMILES [SMILES ...], --smiles SMILES [SMILES ...]\n",
-            "                        SMILES string for prediction\n",
-            "  -i INPUT_FILE, --input-file INPUT_FILE\n",
-            "                        file containing SMILES strings\n",
+            "  -ss SMILES_STRINGS [SMILES_STRINGS ...], --smiles-strings SMILES_STRINGS [SMILES_STRINGS ...]\n",
+            "                        SMILES string(s) for prediction\n",
+            "  -sf SMILES_FILE, --smiles-file SMILES_FILE\n",
+            "                        file containing SMILES strings only\n",
+            "  -ds DESCRIPTOR_SET, --descriptor-set DESCRIPTOR_SET\n",
+            "                        descriptors to calculate (one of all, optimized, or debug)\n",
+            "  -pd PRECOMPUTED_DESCRIPTORS, --precomputed-descriptors PRECOMPUTED_DESCRIPTORS\n",
+            "                        precomputed descriptors\n",
             "  -o OUTPUT, --output OUTPUT\n",
             "                        output file for predictions (defaults to stdout)\n"
           ]
@@ -795,38 +626,36 @@
     },
     {
       "cell_type": "code",
-      "execution_count": 21,
+      "execution_count": 11,
       "metadata": {
         "id": "WALs2qaAm4Fi",
         "colab": {
           "base_uri": "https://localhost:8080/"
         },
-        "outputId": "9c3c21b4-3c6e-405a-9e73-616e2e277d27"
+        "outputId": "4c0f4e9a-9588-4af1-f0de-4007e806a4f1"
       },
       "outputs": [
         {
           "output_type": "stream",
           "name": "stdout",
           "text": [
-            "[02/08/2024 06:15:28 PM numexpr.utils] INFO: NumExpr defaulting to 2 threads.\n",
-            "[02/08/2024 06:15:35 PM fastprop.cli.fastprop_cli] INFO: Predict Parameters:\n",
-            " checkpoints_dir: pah/fastprop_1707415963/checkpoints\n",
-            "smiles:\n",
-            "- c1c2ccccc2ccc1\n",
-            "input_file: null\n",
-            "output: null\n",
-            "\n",
-            "[02/08/2024 06:15:36 PM fastprop.utils.calculate_descriptors] INFO: Calculating descriptors using strategy='low-memory'\n",
-            "100% 1/1 [00:00<00:00,  6.86it/s]\n",
+            "/usr/lib/python3.10/multiprocessing/popen_fork.py:66: RuntimeWarning: os.fork() was called. os.fork() is incompatible with multithreaded code, and JAX is multithreaded, so this will likely lead to a deadlock.\n",
+            "  self.pid = os.fork()\n",
+            "100% 1/1 [00:00<00:00,  6.88it/s]\n",
+            "GPU available: True (cuda), used: True\n",
+            "TPU available: False, using: 0 TPU cores\n",
+            "IPU available: False, using: 0 IPUs\n",
+            "HPU available: False, using: 0 HPUs\n",
+            "LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]\n",
+            "Predicting DataLoader 0: 100% 1/1 [00:00<00:00,  7.95it/s]\n",
             "\n",
-            "                   log_p  log_p_stdev\n",
-            "c1c2ccccc2ccc1  2.77289     0.651922\n",
-            "[02/08/2024 06:15:37 PM fastprop.cli.fastprop_cli] INFO: If you use fastprop in published work, please cite: ...WIP...\n"
+            "                  task_0  task_0_stdev\n",
+            "c1c2ccccc2ccc1  1.74838           0.0\n"
           ]
         }
       ],
       "source": [
-        "!fastprop predict pah/fastprop_1707415963/checkpoints --smiles c1c2ccccc2ccc1"
+        "!fastprop predict pah/fastprop_1713555122/checkpoints --smiles-strings c1c2ccccc2ccc1 -ds all"
       ]
     },
     {
@@ -849,25 +678,26 @@
           "base_uri": "https://localhost:8080/"
         },
         "id": "roET_L4P87ck",
-        "outputId": "97d3877f-6462-46e6-c9d1-7ba5a14878a0"
+        "outputId": "e23bd9dc-1c2f-4047-d6c5-b10f88fdf38f"
       },
-      "execution_count": 23,
+      "execution_count": 12,
       "outputs": [
         {
           "output_type": "stream",
           "name": "stdout",
           "text": [
-            "[02/08/2024 06:21:45 PM numexpr.utils] INFO: NumExpr defaulting to 2 threads.\n",
-            "usage: fastprop shap [-h] [-it IMPORTANCE_THRESHOLD] checkpoints_dir input_file\n",
+            "usage: fastprop shap [-h] [-it IMPORTANCE_THRESHOLD]\n",
+            "                     checkpoints_dir cached_descriptors descriptor_set\n",
             "\n",
             "positional arguments:\n",
             "  checkpoints_dir       directory of checkpoint file(s) for SHAP analysis\n",
-            "  input_file            csv of SMILES and targets used during training\n",
+            "  cached_descriptors    csv of calculated descriptors cached by fastprop\n",
+            "  descriptor_set        descriptors in the cache file (one of all, optimized, or debug)\n",
             "\n",
             "options:\n",
             "  -h, --help            show this help message and exit\n",
             "  -it IMPORTANCE_THRESHOLD, --importance-threshold IMPORTANCE_THRESHOLD\n",
-            "                        [0-1] hide features below (most important feature) x this\n"
+            "                        [0-1] include top fraction of features, default 0.75\n"
           ]
         }
       ]
@@ -887,29 +717,22 @@
     {
       "cell_type": "code",
       "source": [
-        "!fastprop shap pah/fastprop_1707415963/checkpoints pah/arockiaraj_pah_data.csv -it 0.90"
+        "!fastprop shap pah/fastprop_1713555122/checkpoints pah/cached_arockiaraj_pah_data_all_1713554970.csv all -it 0.90"
       ],
       "metadata": {
         "colab": {
           "base_uri": "https://localhost:8080/"
         },
         "id": "5B3XrFXZ9Fvm",
-        "outputId": "703d6182-6de9-4bb4-ecbe-66503a410be4"
+        "outputId": "95f71d00-c396-4c2d-e823-013f86291ded"
       },
-      "execution_count": 29,
+      "execution_count": 17,
       "outputs": [
         {
           "output_type": "stream",
           "name": "stdout",
           "text": [
-            "[02/08/2024 06:29:19 PM numexpr.utils] INFO: NumExpr defaulting to 2 threads.\n",
-            "[02/08/2024 06:29:26 PM fastprop.utils.load_data] INFO: Generating RDKit molecules from SMILES.\n",
-            "[02/08/2024 06:29:26 PM fastprop.utils.calculate_descriptors] INFO: Calculating descriptors using strategy='low-memory'\n",
-            "100% 55/55 [01:53<00:00,  2.07s/it]\n",
-            "Calculating SHAP values for each model: 100% 4/4 [00:00<00:00,  7.86it/s]\n",
-            "[02/08/2024 06:31:22 PM fastprop.shap] WARNING: Output file exists! fastprop.shap will overwrite 'log_p_feature_importance_beeswarm.png'.\n",
-            "[02/08/2024 06:31:22 PM fastprop.shap] INFO: Visit the mordred-community docs to lookup descriptors: https://jacksonburns.github.io/mordred-community/descriptors.html\n",
-            "[02/08/2024 06:31:22 PM fastprop.cli.fastprop_cli] INFO: If you use fastprop in published work, please cite: ...WIP...\n"
+            "\rCalculating SHAP values for each model:   0% 0/1 [00:00<?, ?it/s]\rCalculating SHAP values for each model: 100% 1/1 [00:00<00:00,  5.02it/s]\rCalculating SHAP values for each model: 100% 1/1 [00:00<00:00,  5.02it/s]\n"
           ]
         }
       ]
@@ -918,28 +741,28 @@
       "cell_type": "code",
       "source": [
         "from IPython.display import Image\n",
-        "Image('log_p_feature_importance_beeswarm.png')"
+        "Image('task_0_feature_importance_beeswarm.png')"
       ],
       "metadata": {
         "colab": {
           "base_uri": "https://localhost:8080/",
-          "height": 403
+          "height": 434
         },
         "id": "Ny89BTOu98VX",
-        "outputId": "b1c30daa-39c9-40ba-e762-7f0c268cfcad"
+        "outputId": "a0718374-8a37-40bc-82d9-7ab184d5ca5d"
       },
-      "execution_count": 30,
+      "execution_count": 18,
       "outputs": [
         {
           "output_type": "execute_result",
           "data": {
-            "image/png": "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zfaEhAR5eXlp+vTpVtsNw9AHH3ygtm3bKjAwUMHBwbrnnntsbuV0JDMzU6tXr9Z9991nc0XL3nM4ubfNmTNHt99+uwICAtSiRQt98cUXkqRDhw6pV69eqly5smrUqKHx48crIyPDbj+PHz+uBx54QFWqVFHlypU1cOBAHT9+3KpsVlaWXnnlFXXp0kW1a9eWn5+f6tWrpyeffFKXL1+226+VK1eqW7duqlq1qgIDA3X77bdr/PjxSk9P14cffmi54jpq1CjLbaeuXP1ISEjQsGHDdPPNN8vf31+33Xabnn/+eV27ds2q3PTp0+Xl5aUff/xRzz//vEJCQuTv769WrVpp7dq1+bYj/e85qk2bNumll15SaGioKlasqIiICMXHx0uStm7dqk6dOikoKEh16tTR3//+d7t1rV69WnfffbeCgoIUHBysu+++W1FRUXbLLliwQE2aNJG/v7/CwsI0e/Zsh7e+JSUl6dlnn1VYWJj8/f1Vq1YtPfzwwzbHsKBcHWdnz0V6eXlp5MiRkrLP2wYNGkjK/uVQzjHP+azl/nx98sknatmypQICAlSvXj1Nnz5dmZmZVnW7+jl15Vzz8vJSz549FRsbq5SUlAKOFFA2cKULAArgtttu048//qjPP/9cgwYNcmmfGzduOHxmKy0tzeF+ixYtUoMGDdS5c2d5eXmpdevWWrx4sR5//HGX4/3pp58kSTVr1nRY5o9//KMmTpyojz/+WP3797d6b9OmTTp79qzltkUp+4esxMREDR8+XCEhITp79qwWLlyoe++9V3FxcU5vgXTHsGHD9Mknn2jw4MEaNWqU0tLS9K9//Us9evTQ559/bhNzXnv37lVKSoratWtXoHbff/99/fbbb3r88ccVEBCgd955RwMHDtR//vMfjRkzRg8//LAGDBigDRs26N1339VNN92kF154waqO1NRUdevWTREREZo5c6Z++uknzZkzR/Hx8dq3b58lSU9PT9c///lPPfjgg3rggQcUFBSkb775RosWLdJXX31lc3voX//6V/3jH/9Qs2bNNHHiRNWpU0c///yzVq5cqZdeekldunTR888/r3/84x8aO3as5ZjcfPPNTvt88uRJtWvXTklJSXrqqafUqFEjbdmyRTNnztSOHTu0adMm+fhY/+gwYsQI+fr6asqUKUpPT9fs2bM1YMAAHT161O4P7fY899xzunHjhp555hmlp6frjTfe0P3336+PP/5Yo0eP1tixYzV06FD9+9//1t/+9jc1aNDA6qrunDlz9PTTT6tJkyb629/+Jin7PB0wYIDmzZunsWPHWsrOnj1bEydOVKtWrfSPf/xD165d0+uvv66bbrrJJq6kpCR17NhRp06d0mOPPabmzZvr3LlzmjNnjiIiIrRnzx6Fhoa61MfCjnN+mjZtqrfeeksTJ07UwIEDLd9PwcHBVuWio6N1/PhxPf3006pdu7aio6M1Y8YMnTx5UkuWLClwX1w91zp06KB58+bpq6++Uq9evQrcDlDqGQAAl3399deGr6+vIclo1KiRMWrUKGPOnDnGDz/8YLd8aGioISnfPxcvXrTa7+zZs4a3t7cxbdo0y7bZs2cbkuy2Jcm4//77jYsXLxoXL140jh49arz55puGr6+vUaVKFePChQtO+zV48GDD39/fSExMtNr+6KOPGj4+Plb7p6Sk2Ox//vx5o0aNGkbv3r2tto8YMcLI+19N165djdDQUJs6Tpw4YUiy6vPnn39uSDLmzZtnVTYjI8No27atUb9+fSMrK8tp3xYvXmxIMqKiomzei4uLMyQZS5YssdlWt25d48qVK5btBw4cMCQZXl5exsqVK63qadOmjVG7dm2bfkoynnnmGavtOX0aN26cZVtWVpZx7do1m/gWLlxoSDI+++wzy7Zdu3YZkox77rnH+P33363KZ2VlWcbDXt/y88gjjxiSjDVr1lhtnzJliiHJWLhwoWXbtGnTDElG3759rY7B7t27DUnGc889l297S5YsMSQZrVu3NtLS0izbo6KiDEmGj4+P8c0331i2p6WlGbVr1zbat29v2ZaYmGgEBQUZt912m5GUlGTZnpSUZDRs2NAIDg42fvvtN8MwDOO3334zAgMDjaZNmxqpqamWsqdPnzaCgoIMSUZcXJxl+/jx442AgABj//79VnEnJCQYlSpVMkaMGGHZVpDxLsg42/sM5ZBkFYO9z1De9ypUqGDs3bvXsj0rK8sYMGCAIcnYuXOnZXtBPqeu9H379u2GJOP11193WAYoy7i9EAAKoEOHDtq7d69GjBihpKQkLVmyRE899ZSaNWumLl262L3lqH79+tq4caPdP/fff7/ddj788ENlZWVp+PDhlm1Dhw6Vr6+vFi9ebHefDRs2qFatWqpVq5YaN26sSZMmqVmzZtqwYYPd3+LnNmLECKWlpVndvpiSkqJVq1apV69eVvsHBQVZlbl8+bK8vb0VERGhXbt2OW2noJYtW6ZKlSppwIABunTpkuXPlStX1K9fPyUkJFiu5jly8eJFSVL16tUL1PbIkSNVpUoVy+uWLVuqcuXKqlu3rs1Vzk6dOun8+fN2b5167rnnrF4PHDhQt99+u9WkHl5eXqpYsaKk7CujV65c0aVLl9S9e3dJshrXf/3rX5KkmTNn2jyPlnNrlzuysrIUHR2t1q1b2zz79pe//EUVKlTQqlWrbPZ75plnrNq86667FBwcnO9xye3JJ5+0upKXc7UkIiJC4eHhlu1+fn5q166dVd0bN25Uamqqxo8fr8qVK1u2V65cWePHj1dKSoq+/PJLSdmfkWvXrunpp59WYGCgpWxISIiGDh1qFZNhGPrXv/6lLl266JZbbrE6/4KCgtS+fXtt2LDB5T7mcHeci0qPHj3Upk0by2svLy9NnTpVkkxtt0aNGpKkX3/91bQ2AE/G7YUAUEAtWrSwPAN08uRJbd26VQsXLtT27dv1wAMP2NwKFhQUpPvuu89uXcuWLbPZZhiGFi9erJYtWyorK8vqeay7775bS5cu1cyZM21uP4qIiNDLL78sSfL391doaKjq1avnUp9yEquPP/5YTzzxhKTsZ4ZSU1OtEj9J+vnnn/XXv/5V69ev15UrV6zeK+o1uQ4fPqzk5GSnt8VduHBBjRs3dvh+TkxGAaerbtiwoc22atWq6dZbb7W7XZIuX75sdTtX1apV7T7n17RpU61evVqpqamWJPbf//633njjDe3bt8/m+bDffvvN8u+ffvpJXl5eDp8rdNfFixeVkpKi5s2b27xXvXp11alTx+4vFeyNU40aNRw+i2ZP3jpyxjPnGaW87+Wu+8SJE5JkN+6cbTlx5/zdpEkTm7LNmjWzen3x4kVdvnzZ8ssMeypUKPjvrt0d56LStGlTm205fTez3ZzPn6es2wcUN5IuACiE0NBQDR8+XMOGDVPnzp21Y8cO7d69W506dXK7zq1bt+rnn3+WJDVq1MhumS+++EIDBgyw2lazZk2HyV1+fHx89Mgjj2j27Nk6duyYwsLC9PHHH6tatWpWz0ylpKSoS5cuSk1N1YQJE9SiRQtVqlRJFSpU0MyZM7V58+Z823L0Q1feB/ml7B/UatWqpeXLlzusz9k6aJIsPzAXdL0yb2/vAm2XCp7Y5fj888/1hz/8Qe3atdPbb7+tW2+9VQEBAbpx44Z69eqlrKwsq/KFuaJV1ByNR0HGwp2xNltO/Pfdd5+effbZEoujIJ8XT2435/PnKIEFyjqSLgAoAl5eXoqIiNCOHTt09uzZQtW1ePFi+fv76+OPP7b7m/Rx48Zp0aJFNklXYY0YMUKzZ8/Wxx9/rDFjxmjLli0aO3as/P39LWU2bdqkX375RYsXL9aoUaOs9s87iYQj1atX1969e2222/ste6NGjXT06FG1b9/eZkIAV+UkZQW53a2oXLlyRefPn7e52nX48GHddNNNlqtcS5cuVUBAgOLi4qxuezty5IhNnY0bN9a6det04MABp5ODFDQpq1WrlipVqqTvv//e5r3ffvtN586d88j1vnKukn3//fe69957rd774YcfrMrk/H3kyBGHZXPUqlVLVatW1dWrV93+ZYY9BR3nnNtiExMTrW6Rtfd5ceWYHz582GZb3nHKadfVz6kr7eZcsc/vlyRAWcUzXQBQABs3brT7m97ff//d8nxH3tuUCiIpKUkrVqzQ/fffr4ceekiDBw+2+dO/f3+tW7dO586dc7sde+688061bNlSy5Yt09KlS5WVlaURI0ZYlcm58pD3KsaGDRtcfp6rcePGSk5O1u7duy3bsrKy9NZbb9mUHT58uLKysvSXv/zFbl0XLlzIt73WrVurcuXKlinIi9usWbOsXq9atUo//vijVdLs7e0tLy8vqytahmFYbhfN7ZFHHpEkPf/880pPT7d5P+fY5CSprl7hq1Chgvr166d9+/YpNjbWpg9ZWVkaOHCgS3UVpx49eigoKEjvvvuukpOTLduTk5P17rvvKjg4WD169LCUrVixot5//32rqdnPnDljczW1QoUKGjp0qHbv3q0VK1bYbdud55MKOs45t87mPJeW44033rCp25VjvnHjRn377beW14Zh6LXXXpMkq3OyIJ9TV9qNj4+Xj4+P7r77bodlgLKMK10AUAATJ07U5cuX1b9/f7Vo0UKBgYE6ffq0li9frqNHj2r48OFq0aKF2/V/8skn+v333/Xggw86LPPggw/qww8/1EcffWQzSUNhjRgxQpMnT9arr76qxo0bq3379lbvd+rUSbVr19bkyZOVkJCgkJAQ7d+/X0uXLlWLFi106NChfNsYO3as3njjDQ0cOFDPPPOM/Pz8tGLFCrvJbM408e+9956+/fZbRUZGqmbNmjpz5ox27typY8eO5fscire3twYNGqTVq1crLS3N6sqd2WrWrKnPP/9cv/zyi7p162aZMv7mm2+2Wo9s8ODBWrlypbp3767hw4crIyNDq1evtlmzSZLatWunZ599Vq+++qratGmjP/zhD6pdu7ZOnDihFStWaPfu3apataqaNWumSpUqac6cOQoMDFTVqlV10003WSbnsOcf//iHNm7cqAEDBuipp55SWFiYtm3bps8++0xdunSxScI9QdWqVfXaa6/p6aefVkREhGXdqg8//FDHjh3TvHnzLBOiVKtWTX//+981ZcoUdezYUcOHD9e1a9c0d+5cNWrUSPv27bOq+5VXXtGOHTv00EMP6aGHHlL79u3l5+enkydPau3atWrbtq3VGm+uKsg4P/zww3r++ec1duxYHTlyRNWrV1dsbKzdZShq1KihsLAwffrpp7rtttt08803KygoSP369bOUadWqlbp3766nn35aderUUVRUlL788ksNGzZMHTp0sJQryOc0v3PNMAzFxsaqV69ebl+xBkq9EpkzEQBKqfXr1xtPPfWU0bJlS6NGjRqGt7e3Ub16daNbt27GokWLjBs3bliVDw0NNZo3b+6wvpzpoHOmjA8PDzd8fHxspm7P7fr160alSpWMxo0bW7bpv1N3F9b58+cNHx8fQ5Lx8ssv2y1z4MABo2fPnkbVqlWN4OBgo2vXrsa2bdvsTm3taLrrNWvWGK1atTL8/PyMOnXqGFOnTjWOHDnicLrrjz/+2OjUqZNRqVIlw9/f3wgNDTUGDhxofPrppy71K2ea9RUrVlhtdzZlvL3pr0NDQ42uXbvabM+ZPv3EiROWbTlTbv/8889G//79jUqVKhnBwcFG//79jZ9++smmjvnz5xtNmzY1/P39jdq1axtjxowxLl++bDMteI7ly5cbHTt2NIKDg43AwEDj9ttvN5555hmrqdfXrFljtG7d2vD39zck2Y09r+PHjxuPPvqoUatWLcPX19do0KCB8Ze//MVqinVHfc5vnPLKmTI+9zTtORz129E59fnnnxsdOnQwAgMDjcDAQKNDhw7GqlWr7LY7d+5co3Hjxoafn59x2223GW+99ZZlaYG8saSmphovvfSScccddxgBAQFGcHCw0aRJE+Pxxx834uPjLeUKOkW/q+NsGIYRHx9vdOzY0fD39zdq1KhhjBkzxvjtt9/sjtGuXbuMjh07GoGBgYYky7Tvuad6X758udGiRQvDz8/PCAkJMV588UUjPT3dpt2CfE6dnWtbtmwxJBlffPGFS2MDlEVehuHmU78AAJQivXr1UmpqqrZv314s7XXr1k0JCQlKSEgolvYAZxISEtSgQQNNmzbN6iprcRg4cKBOnz6tb775xmMmgAGKG890AQDKhTfeeEM7d+50a20lAO7Zt2+foqKi9MYbb5BwoVzjmS4AQLnQvHlz06fZBmCtdevWNkseAOURV7oAAAAAwEQ80wUAAAAAJuJKFwAAAACYiKQLAAAAAExE0gUAAAAAJiLpAgAAAAATkXQBAAAAgIlIugAAAADARCRdAAAAAGAiki4AAAAAMBFJFwAAAACYiKQLAAAAAExE0gUAAAAAJiLpAgAAAAATkXQBAAAAgIlIugAAAADARCRdAAAAAGAiki4AAAAAMBFJFwAAAACYiKQLAAAAAExE0gUAAAAAJiLpAgAAAAATkXQBAAAAgIlIugAAAADARCRdAAAAAGAiki4AAAAAMBFJFwAAAACYiKQLAAAAAExE0gUAAAAAJiLpAgAAAAATkXQBAAAAgIlIugAAAADARCRdAAAAAGAiki4AAAAAMBFJFwAAAACYiKQLAAAAAExE0gUAAAAAJiLpAgAAAAATkXQBAAAAgIlIugAAAADARCRdAAAAAGAiki4AAAAAMBFJFwAAAACYiKQLAAAAAExE0gUAAAAAJiLpAgAAAAATkXQBAAAAgIlIugAAAADARCRdAAAAAGAiki4AAAAAMNH/A0fZE2XzGhM0AAAAAElFTkSuQmCC\n",
+            "image/png": "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oYceUsWKFfNcH1BqZN5/pTjssVIY7cmpjrlfSQ/Pt+13I0kWi1TG4tn9b/LTR0+Mx9yvpPHzpct/98nXR3p7tPtysvY/L/nt7fP1kQxdqSczd8fy0h9vXQ/F7boHAC8y5eIGERERCgoK0q233ur4g3/16tWyWt38J5QLe1CyYsUK5TYdau/evYqPj1fXrl1VoUKFArU9J7t27dLkyZOVlJSkkSNH6vHHH9egQYOUkJDgCNAk6cSJE7rvvvu0YcMG9erVS1OmTFHv3r21du1ajRo1SsnJyR5vG1CinU5w3nQyLUOauMiWbtb25FSH/VjmP/oN48prT7QnP330xHicTpD+tcA54MiwShMXupbjrv95yW9vX4bVfdCT3bHc+uOt66G4XfcA4GWmC3xSU1MVHR2tbt26qWzZspKkPn366OTJk9q2bVu+y7vnnnsUHBys119/XX379tXUqVP18ccf68CBAy55Dx06JElq0KDB1XUiG5s3b5bVatU777yje++9VwMHDtQDDzygefPmqVOnTo58L7/8sjIyMvTRRx/pX//6lwYNGqQJEyZozpw5On78uD766COvtK+gzp49q9TUVMfr5ORkp7lOaWlpio+PdzonLi4ux9cnTpxwClSpgzpyrMPd/iup6YrftLto+rE3Jts9XwqjjovfH8x575tMeXOsI4excltHdmVm8/M5u3lPjnWkpKRcGat9R6T0y276keFaZ0778aRmKG3XIcfL5ORkXdh+IPfxyk2mvl/VWGXz2t3PI23noWzLNeX7nDqogzquuo6SznSrukVHR2vq1Kl67733HCu4ZWRk6Pbbb1erVq300ksvOeWPiorS9OnT9dxzz6lfv35uyzx+/Lg+/PBDbdmyRSdPnnSk16tXT88995waNWokSVq4cKHmzJmjqVOnauDAgQVqv32Oj7tV3ebNm6d58+bpySef1MCBA+Xr6/qkYnJysrp166Z+/fq5PFInSaNGjVL58uW1aNGiArUPMKXTCVLN0c5/BAb4SX/NL5rlhgujPTnVYRiux7K62vbkp4/5yJvjqm41HnANfgJ8pb8WOJfjrr6C5s+rnMbTW9dDcbvuAcDLTHfHJyIiQhUqVNA111yj2NhYxcbGKi4uTu3atdOWLVt0/vz5fJdZvXp1TZkyRatXr1Z0dLRefPFFde7cWYcOHdKkSZOUkGB7LMC+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AAB3TidcCXok29eJi7jzAwAlFIEPAADu7DtyJeixS02X9h8tmvYAAK4KgQ8AAO40r2V7vC2zAD9bOgCgxCHwAQCUDvldpKBKeducnoC/g58AX+n5f0p7Y6QDsbay7F/tZbIQAgAUWyxuAAAwv6tZpMBq2L6mX5b+vVTKsLrm8feVhrSXlm1jIQQAKKa44wMAMLeCLlJgPy/9su211XAf9NjL/HgrCyEAQDFG4AMAMLeCLlLg7rz8YCEEAChWSvWjblOmTNGGDRv00Ucf6cYbb3SbxzAMDRgwQImJiYqOjtadd96puLi4bMucMWOGevfuLUnavHmzNm3apH379unkyZMKCQlR3bp1de+996pDhw4FanN4eLjje4vForJly6pChQqqX7++unTpottuu02BgYFO5xw/flxRUVHq2rVrtv0EANOyL1KQOYjJyyIF7s7LDxZCAIBipVQHPgMGDNCGDRsUFRWVbUCwc+dOHT9+XIMHD3YEFNdee63Gjx/vNn+LFi0c37/wwgsKDg7WzTffrFq1aikhIUFRUVGaMGGCHnroIY0aNapA7W7QoIHuvfdeSdKlS5d04sQJbd++XTNmzNCiRYv08ssvq0GDBo78x48f1/z581W9enUCHwClj32RgomLbHdhAvykN+6XKofm7zxfH0kWKeOya94AP+mOdtIX2/NXBwCg0JTqwKddu3a69tpr9eWXX2rixIny8/NzyRMZGSnJFiTZBQcHO+7q5OT555/XP/7xD6e0u+++W0OHDtX8+fN15513KjQ0//8pXnPNNS71jxs3TuvXr9fUqVM1YcIEffbZZwUqG7hqpxNsjwg1r2X7wxF5V5Rj58m67WVVryAdP3elTG/2z152oxruj4/tKXVpLK3eJfVpLTW6Lm/lju0pNb1eWrJJGtZVqhgirf5Rat9AOnleOn9Bslik0LK28qcOkT79Vrq+itQvPJfCM7V9yy+277s0tn3lPQQAHleqAx8fHx/169dPCxYs0ObNm9W9e3en48nJydq4caNuuOEGNWnSJN/lZw16JCkwMFCdO3fWRx99pCNHjqhZs2YFbn9W3bt312+//aZFixbps88+0wMPPKCoqChNnz5dkjR9+nTH961atdK8efM8Vjcg6epWzirtinLsPFl35rLsvL3iWaY6ff191ejOBjrQ5frs2/X0x3mv/57XbYsWSNK8dZKPxbbIga+PZEi6nGmxA18f2zH7KnBlfKR3Rudcz9yvpIfnX1k0wcdiC6QuW3kPAYCHlfrFDfr16yeLxaKoqCiXY2vXrlVqaqrT3R5JslqtOn/+vNt/hmHkWuepU6ckSRUrVvRMJzIZOHCgJOnbb7+VJLVs2VIjR46UJA0aNEgzZszQjBkzdP/993u8bpRyBV05C0U7dp6sO2tZdt5c8SxLnZa0DLX/9KACk9Kyb1de6z8QeyXosbMHNRlW56DHnmbN9H/AZaut3uzqsbcr80pxVuNKubyHAMCjSn3gU6NGDYWHh2v79u06c+aM07GoqCj5+fm5PFYWExOj7t27u/2XkJDzf1C//fabNm7cqJYtW6pGjWweybgK1atXV3BwsI4eta0kVLNmTbVt21aS1Lx5c/Xu3Vu9e/dWu3btPF53QZ09e1apqamO18nJyUpKSnK8TktLU3x8vNM5WReYyPr6xIkTTkEodRRCHTmsnFWi+lEUdeQwdl7vRzZ1X/j+oGf6kZ2C1pG1H3tjXOr0zbCq4rEkpaSk2MYqh/HNsY5Vu/LWl5ykZSh+0273deRlvFLTdXHHr8X32qUO6qCOUlVHSWcx8nKLwuSio6M1depUPfzwwxoxYoQkW3AzZMgQ3XrrrXrppZccee13iJ5++mm3ZbVu3Vq+vu6fIDx37pxGjBih8+fPa8mSJapVK/+r/YSHh6tTp06aPXt2tnl69+6t+Ph4ff/995JsCzQ8+OCDeu6559SvX7981wnkyekEqeZo15Wz/prPBO/cFOXYebJud2Vlx1P9c1Nnhq+PPnqpq4b+a6xt7mZB+3ggVmo88era5+8rHVvgvp68jBfvIQDwmFJ/x0eSbrnlFpUrV87pcbeIiAhJUv/+/V3yBwYGqm3btm7/ZRf0JCQkaPz48Tpz5oxeffXVAgU9eZWSkqKQkBCvlQ+4ZV8BK+DvRUJY1SrvinLsPFl31rLsAvykoZ29078sdRoBftp2d0Olhvhn36681t/oOlu7M7P8/dW3zN+rvGXiW8Y2R8fx2sdWb3b12NvlW+ZKmo/lSrm8hwDAo0r14gZ2AQEB6tWrlz7//HPt3btXTZs21Zo1a3Tttdeqffv2V11+QkKCxo0bp5iYGM2aNcvtogeecvz4caWkpKh58+ZeqwPI1tie0uB2tk0bm13PilT5UZRj58m6M5dVLUyKO3+lzNkJ3ulfpjozGlbXgVVf5Nyu/NT/0SNS/WrSfz63zb8xZAtOZt4jDb/FtZ+StPUXSRapc6Pc67G3K/M5Eu8hAPACAp+/DRgwQJ9//rmioqKUmJio+Ph43X///fLxubqbYvag5/Dhw3rllVc8EkjlZOXKlZKkjh07OtIsFks2uQEvqFJe6ua51QpLlaIcO0/WnbmszMtGe7N/9rLT0/PWrrw6nSDNXO68aIHVsK0MN/wW9/0cnM/f81XKu57DewgAPI7A528NGzZUgwYNtG7dOp06dUoWi8XtY275kZiYqPHjx+vPP//UK6+84hSMeMP69eu1ZMkSValSRXfddZcjPSgoSJJyXXgBAJBFdgsQpGXY7soQoABAiUHgk8mAAQP0yiuv6LvvvlPr1q1Vs2ZNt/lSUlK0Zs0at8fq1aunBg0aSJLGjx+vgwcPqmfPnkpMTHQ5p3nz5tnWkZNTp045ykpNTdWJEye0fft2/fzzz7ruuuv0yiuvqFy5co78derUUXBwsJYtW6bAwECVK1dOFStW9OojdwBgCs1r2RYoyBr8+PvajgEASgxWdcskMTFRt99+u1JTUzV9+nT16dPHJU+/fv1yXNrv/vvv17hx4yTZVmDLSUFWWctaZtmyZVWhQgXVr19fXbp0Uc+ePRUYGOhy3jfffKM5c+bo8OHDSktLYwNTAKaUnp6uxYsXS5JGjhxpW9Xtas39Snp4gZRx2fba10d6O5eNSQEAxQ6BDwDANLwS+Ei2pa0//VaqdY3UtzWLDgBACcSjbgAA5GTuV9KEhbbH3fx9bUtQc7cHAEocAp9i4MyZM7nmCQkJcfsIGwDAi04nXAl6JNvXiYtsS1Bz1wcAShQCn2KgV69eueYpyHwgAMBVcreqW2o6K7oBQAlE4FMMvPPOO7nmueGGGwqhJQAAJ+5WdQvwY0U3ACiBCHyKgbZt2xZ1EwAA7lQpb5vTM3GR7U5PgJ/0xv1S5dCibhkAIJ8IfAAAyMnYnrY5PfuPSs2uZ24PAJRQBD4AAOSmSnnm9ABACedT1A0AAAAAAG8j8AEAAABgegQ+AAAAAEyPwAcAAACA6RH4AAAAADA9Ah8AAAAApkfgAwAAAMD0CHwAAAAAmB6BDwAAAADTI/ABAAAAYHoEPgAAAABMj8AHAAAAgOkR+AAAAAAwPQIfAAAAAKZH4AMAAADA9Ah8AAAAAJgegQ8AAAAA0yPwAQAAAGB6BD4AAAAATI/ABwAAAIDpEfgAAAAAMD0CHwAAAACmR+ADAAAAwPQIfAAAAACYHoEPAAAAANMj8AEAAABgegQ+AAAAAEyPwAcAAACA6fkWdQNQuhmGoaSkpKJuBgCTSE9P18WLFyVJiYmJ8vPzK+IWAYB5lCtXThaLpaibUWAWwzCMom4ESq/ExESVL1++qJsBAACAXCQkJCg0NLSom1FgBD4oUiXpjk9ycrL69Omj1atXKyQkpKibU2IwbgXH2BUM41ZwjF3BMG4Fx9gVTFGNW0m/48OjbihSFoulxHxy4OPjozJlyig0NJRfzvnAuBUcY1cwjFvBMXYFw7gVHGNXMIxbwbC4AQAAAADTI/ABAAAAYHoEPkAe+fv7a/To0fL39y/qppQojFvBMXYFw7gVHGNXMIxbwTF2BcO4FQyLGwAAAAAwPe74AAAAADA9Ah8AAAAApkfgAwAAAMD02McH+NuWLVs0Z84cHTlyRFWrVtWIESPUv3//XM9LTk7Wa6+9pk2bNikjI0Pt2rXTE088ocqVKzvyrFq1SsuWLdPRo0d16dIlVa1aVb1799awYcNMMTHRm2P3xRdfaOPGjTp06JAuXbqkOnXqaMSIEeratasXe1Q4vDluv/zyiz7//HPt379fR44cUceOHTV79mwv9sbzYmJi9PLLL2vfvn0KDg5W7969NW7cOPn5+eV4nmEY+uCDD/T555/r/PnzatCggSZPnqxmzZo55Tt9+rRefvllff/99/L19dUtt9yiRx55xBR7Ynhz7M6dO6eFCxdq//79+u233+Tr66utW7d6u0uFwpvj9v3332vlypX66aefdPbsWVWvXl39+vXT0KFD5etb8v8c8+bY/fzzz3rnnXf0xx9/KDExURUrVlTbtm310EMPqUqVKt7umld5+/ecndVq1bBhw3Tw4EG9+OKL6t69uze6U+xxxweQtGfPHj3++ONq1qyZ3nzzTfXo0UP/+c9/tH79+lzP/fe//63vv/9e//73v/Wf//xHR44c0YQJE5SRkeHIk5CQoPbt2+vZZ5/Vm2++qf79+2vx4sV65ZVXvNmtQuHtsVu0aJGqVaumJ598Ui+99JLq16+vxx57TKtWrfJmt7zO2+O2d+9e7d69Ww0bNlTVqlW92RWvSExM1IMPPqiMjAy98sorGjdunFasWKHXXnst13M/+OADzZ07V0OHDtXrr7+uypUr6+GHH9Zff/3lyJORkaGHH35YR48e1fPPP68nn3xS27dv19SpU73ZrULh7bE7deqU1q5dq4oVK6pRo0be7Eqh8va4LV++XBcuXNDYsWP1xhtvqE+fPpo7d67++9//erNbhcLbY5eUlKTatWvr0Ucf1VtvvaUxY8bohx9+0L/+9S+lpaV5s2te5e1xy2z58uU6ffq0p7tQ8hgAjPHjxxsjR450SnvqqaeMIUOG5Hje3r17jdatWxvbtm1zpB0+fNgIDw831q5dm+O5b7/9ttGhQwcjIyOj4A0vBrw9dufOnXM5d9y4ccZdd911dQ0vYt4et8uXLzu+Hz16tDFx4kTPNLyQLFq0yOjUqZNx/vx5R9oXX3xhtGnTxjh16lS25126dMno0qWL8fbbbzvS0tLSjL59+xozZ850pH355ZdGeHi4cfjwYUfatm3bjNatWxv79+/3bGcKmbfHLvO19d577xmdOnXycA+KhrfHzd3vsoULFxrh4eFuj5Uk3h47d+zv1z179lx9B4pIYY3buXPnjG7duhkRERFG69atjXXr1nm2IyUId3xQ6qWlpWnnzp0ut31vu+02HT58WMePH8/23O+++07lypVT27ZtHWm1a9dWgwYN9O233+ZYb/ny5ZWRkSGjBK8oXxhjFxYW5nLujTfeqDNnzlx9B4pIYYybj0/J/vX+3XffqU2bNipfvrwjrUePHrJardq+fXu25+3bt08pKSlOY+vn56dbbrnFaXy+++471a9fX7Vr13aktW3bVuXLl8/1vVvceXvsSvq1lR1vj1t2v8sMwyjRv88k74+dO/bxTE9Pv7rGF6HCGre3335brVu3Vnh4uGc7UAKZ87cXkA9//fWXMjIynP4AkqQ6depIsj1/m52YmBjVqlVLFovF5Vx352VkZOjSpUvavXu3/ve//+nOO+8s0c92F+bYZbZnzx6XOkuSohq3kiQmJsZlfMqVK6fKlSvnOj6S3I7tiRMndOnSJUe+WrVqOeWxWCyqVatWiR9Hb4+dWRXFuO3Zs0f+/v6qXr16AVtdPBTW2F2+fFnp6emKiYnRm2++qYYNG+qmm266+g4UkcIYt59++knR0dGaNGmSZxpdwpXcv7gAD0lMTJRk+2WTWWhoqNPx7M7Nep69rKzn2Seh2/Xt21eTJ08ucLuLg8Iau8yio6O1b98+vfrqqwVpcrFQFONW0hS0n4mJifL391dAQIDLeYZhKCkpSYGBgUpKSnJbfmhoaIkfR2+PnVkV9rgdPXpUn3zyie644w4FBQVdfQeKUGGN3ZgxY7R3715JUuPGjfXGG2+U6A8PvT1uVqtVL7/8su69915Vr149x6cJSouSe7UAOUhOTs7TowM1atQohNbY+Pr6asmSJUpLS9Mvv/yihQsXavr06Zo+fXqhtSEviuPY2f3++++aOXOm+vXrV+xWdSvO4wageElOTtbjjz+u6tWra9y4cUXdnBLjmWeeUXJysmJjY/XBBx9o3LhxWrhwoSlWYvSGlStXKj4+XiNGjCjqphQbBD4wpfXr1+v555/PNd+yZcscn7InJyc7HbN/2mI/7k5oaKhOnjzpkp6UlOT2vMaNG0uSbrrpJlWvXl2PPfaY7r77bkd6cVBcxy4uLk4TJkxQkyZN9PTTT+favsJWXMetpAoNDXUZHyn3foaGhiotLU2pqalOn4YmJSXJYrE4Pl0tV66c2/ITExN17bXXeqAHRcfbY2dWhTVu6enpevzxx5WUlKRFixapbNmynutEESmssbM/2tW0aVO1adNG/fr104oVK3Tfffd5piOFzJvjduHCBb3zzjsaN26c0tPTlZ6erpSUFEnSpUuXlJycXCoDRgIfmNLAgQM1cODAPOVNS0uTr6+vYmJi1L59e0d6ds/QZla7dm3t2LFDhmE4zbmIiYlRvXr1cqzXvgxsbGxssQp8iuPYnT9/Xg8//LAqVKigl19+uVg+2lAcx60kq127tssz7va7armNjyQdOXJEDRo0cKTHxMSoatWqjsdmateurUOHDjmdaxiGjhw54rRwREnk7bEzq8IYN6vVqqlTp+rAgQNasGBBiVxq3p2iuOYqVaqka665RrGxsVfT9CLlzXE7fvy4EhISNHPmTM2cOdPp/GnTpqlSpUr66quvPNWVEoPFDVDq+fv7Kzw8XBs2bHBKX7dunerUqZPjpNMOHTooMTFRO3bscKQdOXJEv/76qzp27JhjvXv27JFUsh99Koyxu3DhgmOPmjfffNMUn1AV1TVXknTo0EE7duxQUlKSI239+vXy8fFxmiuXVfPmzRUcHOy0H1JGRoa+/vprp/Hp0KGDfv/9dx09etSRtmPHDiUkJJT4cfT22JlVYYzbSy+9pK1bt2rWrFmm+qCiKK65EydOKC4urkT/H+rNcatUqZLee+89p3/2PaPGjBmjl19+2Uu9Kt6K38emQBF44IEHNHbsWMduxrt27VJ0dLTLpyRt27ZVnz599Oyzz0qy/fJp3769ZsyYoUceeUT+/v569913Vb9+fd1yyy2O80aPHq2uXbuqTp068vHx0U8//aQPP/xQHTp0UNOmTQu1r57m7bF7/PHH9euvv+rZZ59VXFyc4uLiHMey26G6JPD2uJ07d067du2SZLtjdvHiRcd/kp06dSr2n97fcccd+vTTT/Xoo4/q/vvv16lTp/TGG29o8ODBTju1P/TQQ4qLi9PKlSslSQEBARo5cqTmzZunChUqqF69evr888+VkJCge++913Fe9+7dtXjxYj3xxBMaP368Ll26pNmzZ6tTp04l/j3p7bGT5LiWDh8+LKvV6njdpEkTVatWrXA66mHeHrdFixbpiy++0H333Sd/f3/t37/fcaxOnTol+kMdb4/dCy+8oLCwMDVu3FghISGKiYnRRx99pEqVKmnAgAGF3V2P8ea4BQQEuCxfbV/coG7dumrRokXhdLKYsRgleRMRwIM2b96sOXPm6MiRI6patapGjBjh8gs1PDxcffv21bRp0xxpycnJeu211/T111/r8uXLatu2rZ544gmnX1qvvfaatm3bpri4OPn6+qp69erq06eP7rzzTvn7+xdWF73Gm2OX074DO3fu9HhfCpM3x23nzp168MEH3dYbGRlZIpbPPXz4sF555RXt3btXwcHB6tOnj8aNGyc/Pz9HnjFjxiguLk5RUVGONMMw9P7772vZsmU6d+6cGjRooMmTJ6t58+ZO5Z86dUqvvPKKvv/+e5UpU0a33HKLJk+eXKL/ALXz9thl97587rnn1K9fP+90qhB4c9zGjBmjH3/80W297733XonfY8WbYxcREaEVK1boyJEjSktLU9WqVdW+fXuNHDlSlSpVKtR+epq336uZHT9+XP3793d84FYaEfgAAAAAMD3m+AAAAAAwPQIfAAAAAKZH4AMAAADA9Ah8AAAAAJgegQ8AAAAA0yPwAQAAAGB6BD4AAAAATI/ABwAAAIDpEfgAgImNGDFCFoulqJshSfrpp5/k6+urdevWOdI2bdoki8Wi999/v+gahmLh/fffl8Vi0aZNmwp0PteSe3v27JGPj482b95c1E0BihyBD4AS588//9SYMWPUsGFDBQUFqUKFCmrUqJGGDx+ur7/+2ilv7dq11bRp02zLsgcGZ86ccXv8wIEDslgsslgs2rp1a7bl2PPY/wUGBqp+/fqaPHmyzp49W7COmszkyZPVsWNH9ejRo6ibUihiYmI0bdo07dmzp6ibgkJy/vx5TZs2rcDBW0HldK3ddNNNGjhwoB599FEZhlGo7QKKG9+ibgAA5MfOnTt18803y8/PT8OGDVOTJk108eJF/f7771q7dq3KlSunW265xWP1LVy4UOXKlVPZsmW1aNEide7cOdu8N910kx599FFJ0tmzZ7VmzRq9/vrrWrdunXbt2iV/f3+Ptauk2bZtm9atW6eVK1c6pXfp0kUXL16Un59f0TTMi2JiYjR9+nTVrl1bN910U1E3B4Xg/Pnzmj59uiSpa9euhVZvbtfapEmTdPPNN2vNmjXq06dPobULKG4IfACUKNOnT9eFCxe0Z88etWjRwuX4iRMnPFZXenq6PvzwQ915550qX7685s2bpzfffFPlypVzm79GjRq69957Ha8nTJigfv36adWqVYqIiNCdd97psbaVNO+++64qV66s3r17O6X7+PgoMDCwiFoFlA6dO3dW7dq19d577xH4oFTjUTcAJcrvv/+uSpUquQ16JKlq1aoeqysqKkqnTp3S8OHDNWLECKWkpOjTTz/NVxk9e/aUJB06dCjbPHPmzJHFYlFkZKTLMavVqpo1azp9irt27Vrdfffdqlu3rsqWLauwsDDddttteX6Gv2vXrqpdu7ZLekxMjCwWi6ZNm+aUbhiG5syZo9atWysoKEghISG65ZZbXB4rzE5GRoZWrlyp7t27u9zZcTcvI3Pau+++qxtvvFGBgYFq1qyZVq1aJUnav3+/evXqpdDQUFWqVEkTJkxQenq6237++eefGjBggMqXL6/Q0FANGjRIf/75p1Neq9Wq//73v+rSpYuqVq0qf39/XX/99XrooYcUHx/vtl9ffPGFunbtqrCwMAUFBenGG2/UhAkTlJaWpvfff99x53HkyJGORyDzchcgJiZG9913n6699loFBATohhtu0FNPPaULFy445Zs2bZosFot+/fVXPfXUU6pZs6YCAgLUokULrVmzJtd6pCvzajZs2KAZM2aoVq1aKlu2rNq2bavt27dLkjZv3qxOnTopODhY1apV03/+8x+3Za1cuVIdO3ZUcHCwQkJC1LFjR0VERLjNO3/+fDVs2FABAQGqV6+eZs+ene1jWAkJCZoyZYrq1aungIAAValSRf/85z9dfob5lddxzmmenMVi0YgRIyTZrts6depIsn1AY/+Z299rmd9f//vf/9S8eXMFBgbq+uuv17Rp05SRkeFUdl7fp3m51iwWi3r27Kno6GglJyfnc6QA8+COD4AS5YYbbtCvv/6q5cuXa/DgwXk65/Lly9nO4UlNTc32vIULF6pOnTrq3LmzLBaLWrZsqUWLFumBBx7Ic3t///13SVLlypWzzfN///d/euSRR7RkyRL179/f6diGDRt07NgxxyN0ku0PnbNnz2rYsGGqWbOmjh07pgULFujWW2/V119/nePjeAVx33336X//+5+GDBmikSNHKjU1VR999JF69Oih5cuXu7Q5q127dik5OVlt2rTJV73vvPOOzp07pwceeECBgYF68803NWjQIH3++ecaPXq0/vnPf2rgwIFau3at3nrrLV1zzTWaOnWqUxkpKSnq2rWr2rZtq5kzZ+r333/Xu+++q+3bt2v37t2OQDktLU2vvPKK7rjjDg0YMEDBwcH64YcftHDhQn3zzTcujyo+/fTTeuGFF9S4cWM98sgjqlatmv744w998cUXmjFjhrp06aKnnnpKL7zwgsaMGeP4mVx77bU59vnIkSNq06aNEhISNG7cONWvX1+bNm3SzJkz9e2332rDhg3y9XX+r3v48OHy8/PTY489prS0NM2ePVsDBw7Ub7/95vYPZ3eefPJJXb58WRMnTlRaWppmzZql2267TUuWLNGoUaM0ZswY3XPPPfrss8/07LPPqk6dOk53N999912NHz9eDRs21LPPPivJdp0OHDhQc+fO1ZgxYxx5Z8+erUceeUQtWrTQCy+8oAsXLujVV1/VNddc49KuhIQEdejQQUePHtX999+vJk2aKC4uTu+++67atm2rnTt3qlatWnnq49WOc24aNWqk119/XY888ogGDRrk+P0UEhLilC8yMlJ//vmnxo8fr6pVqyoyMlLTp0/XkSNHtHjx4nz3Ja/XWvv27TV37lx988036tWrV77rAUzBAIAS5LvvvjP8/PwMSUb9+vWNkSNHGu+++67xyy+/uM1fq1YtQ1Ku/06fPu103rFjx4wyZcoYzz33nCNt9uzZhiS3dUkybrvtNuP06dPG6dOnjd9++8147bXXDD8/P6N8+fLGyZMnc+zXkCFDjICAAOPs2bNO6ffee6/h6+vrdH5ycrLL+SdOnDAqVapk3H777U7pw4cPN7L+qr/55puNWrVquZRx+PBhQ5JTn5cvX25IMubOneuUNz093WjdurVRu3Ztw2q15ti3RYsWGZKMiIgIl2Nff/21IclYvHixS1r16tWN8+fPO9L37t1rSDIsFovxxRdfOJXTqlUro2rVqi79lGRMnDjRKd3ep7FjxzrSrFarceHCBZf2LViwwJBkfPrpp46077//3pBk3HLLLcbFixed8lutVsd4uOtbboYOHWpIMlavXu2U/thjjxmSjAULFjjSnnvuOUOS0adPH6efwY4dOwxJxpNPPplrfYsXLzYkGS1btjRSU1Md6REREYYkw9fX1/jhhx8c6ampqUbVqlWNdu3aOdLOnj1rBAcHGzfccIORkJDgSE9ISDDq1q1rhISEGOfOnTMMwzDOnTtnBAUFGY0aNTJSUlIceWNjY43g4GBDkvH111870idMmGAEBgYae/bscWp3TEyMUa5cOWP48OGOtPyMd37G2d17yE6SUxvcvYeyHvPx8TF27drlSLdarcbAgQMNSca2bdsc6fl5n+al71u3bjUkGa+++mq2eQCz41E3ACVK+/bttWvXLg0fPlwJCQlavHixxo0bp8aNG6tLly5uH3+pXbu21q1b5/bfbbfd5rae999/X1arVcOGDXOk3XPPPfLz89OiRYvcnrN27VpVqVJFVapUUYMGDTR58mQ1btxYa9eudftpdmbDhw9Xamqq06N0ycnJWrFihXr16uV0fnBwsFOe+Ph4lSlTRm3bttX333+fYz35tXTpUpUrV04DBw7UmTNnHP/Onz+vfv36KSYmxnFXKzunT5+WJFWsWDFfdY8YMULly5d3vG7evLlCQ0NVvXp1l7t9nTp10okTJ9w+xvPkk086vR40aJBuvPFGp4UWLBaLypYtK8l2h/D8+fM6c+aMunXrJklO4/rRRx9JkmbOnOkyP8n+mFFBWK1WRUZGqmXLli5zof7973/Lx8dHK1ascDlv4sSJTnX+4x//UEhISK4/l8weeughpzta9rsGbdu2VXh4uCPd399fbdq0cSp73bp1SklJ0YQJExQaGupIDw0N1YQJE5ScnKz169dLsr1HLly4oPHjxysoKMiRt2bNmrrnnnuc2mQYhj766CN16dJFNWrUcLr+goOD1a5dO61duzbPfbQr6Dh7So8ePdSqVSvHa4vFoieeeEKSvFpvpUqVJEmnTp3yWh1AccejbgBKnGbNmjnmhBw5ckSbN2/WggULtHXrVg0YMMDlsaTg4GB1797dbVlLly51STMMQ4sWLVLz5s1ltVqd5ud07NhRH374oWbOnOnyKEzbtm31/PPPS5ICAgJUq1YtXX/99Xnqkz24WbJkiR588EFJtjkkKSkpTsGXJP3xxx96+umn9dVXX+n8+fNOxzy9Z8+BAweUlJSU4yNaJ0+eVIMGDbI9bm+Tkc+ldOvWreuSVqFCBV133XVu0yUpPj7e6dGisLAwt/O+GjVqpJUrVyolJcURSH722WeaNWuWdu/e7TJf6Ny5c47vf//9d1kslmznmRXU6dOnlZycrCZNmrgcq1ixoqpVq+Y2sHc3TpUqVcp2bpI7Wcuwj6d9zkrWY5nLPnz4sCS5bbc9zd5u+9eGDRu65G3cuLHT69OnTys+Pt7xgYI7Pj75//y2oOPsKY0aNXJJs/fdm/Xa33/FZV8voCgQ+AAo0WrVqqVhw4bpvvvuU+fOnfXtt99qx44d6tSpU4HL3Lx5s/744w9JUv369d3mWbVqlQYOHOiUVrly5WwDrNz4+vpq6NChmj17tg4dOqR69eppyZIlqlChgtMcmuTkZHXp0kUpKSmaNGmSmjVrpnLlysnHx0czZ87Uxo0bc60ruz98sk6ulmx/LFWpUkUff/xxtuXltE+SJMcfrfndz6hMmTL5SpfyH1zZLV++XHfffbfatGmjN954Q9ddd50CAwN1+fJl9erVS1ar1Sn/1dzZ8bTsxiM/Y1GQsfY2e/u7d++uKVOmFFk78vN+Kc712t9/2QWRQGlA4APAFCwWi9q2batvv/1Wx44du6qyFi1apICAAC1ZssTtJ8pjx47VwoULXQKfqzV8+HDNnj1bS5Ys0ejRo7Vp0yaNGTNGAQEBjjwbNmzQ8ePHtWjRIo0cOdLp/KwT+7NTsWJF7dq1yyXd3afN9evX12+//aZ27dq5TNLOK3tglJ9Hrzzl/PnzOnHihMtdnwMHDuiaa65x3O358MMPFRgYqK+//trpEayDBw+6lNmgQQN9+eWX2rt3b44LNuQ3MKpSpYrKlSunn3/+2eXYuXPnFBcXVyz3A7LfLfr555916623Oh375ZdfnPLYvx48eDDbvHZVqlRRWFiYEhMTC/yBgjv5HWf7I5pnz551elzT3fslLz/zAwcOuKRlHSd7vXl9n+alXvud69w+qADMjDk+AEqUdevWuf3E8+LFi47n/bM+MpMfCQkJWrZsmW677TbdddddGjJkiMu//v3768svv1RcXFyB63HnpptuUvPmzbV06VJ9+OGHslqtGj58uFMe+yfwWT/NX7t2bZ7n9zRo0EBJSUnasWOHI81qter11193yTts2DBZrVb9+9//dlvWyZMnc62vZcuWCg0NdSyPXNhefPFFp9crVqzQr7/+6hS4lilTRhaLxenOjmEYjkcXMxs6dKgk6amnnlJaWprLcfvPxh4o5vVOl4+Pj/r166fdu3crOjrapQ9Wq1WDBg3KU1mFqUePHgoODtZbb72lpKQkR3pSUpLeeusthYSEqEePHo68ZcuW1TvvvOO0bPRff/3lclfRx8dH99xzj3bs2KFly5a5rbsg81XyO872xzjt85TsZs2a5VJ2Xn7m69at048//uh4bRiGXn75ZUlyuibz8z7NS73bt2+Xr6+vOnbsmG0ewOy44wOgRHnkkUcUHx+v/v37q1mzZgoKClJsbKw+/vhj/fbbbxo2bJiaNWtW4PL/97//6eLFi7rjjjuyzXPHHXfo/fff1wcffOAycf5qDR8+XI8++qheeuklNWjQQO3atXM63qlTJ1WtWlWPPvqoYmJiVLNmTe3Zs0cffvihmjVrpv379+dax5gxYzRr1iwNGjRIEydOlL+/v5YtW+Y2oLQvYf3222/rxx9/VN++fVW5cmX99ddf2rZtmw4dOpTrvIQyZcpo8ODBWrlypVJTU53uYHlb5cqVtXz5ch0/flxdu3Z1LGd97bXXOu1XNGTIEH3xxRfq1q2bhg0bpvT0dK1cudJlTxdJatOmjaZMmaKXXnpJrVq10t13362qVavq8OHDWrZsmXbs2KGwsDA1btxY5cqV07vvvqugoCCFhYXpmmuucSyY4M4LL7ygdevWaeDAgRo3bpzq1aunLVu26NNPP1WXLl1cAuHiICwsTC+//LLGjx+vtm3bOva1ef/993Xo0CHNnTvXsUhFhQoV9J///EePPfaYOnTooGHDhunChQt67733VL9+fe3evdup7P/+97/69ttvddddd+muu+5Su3bt5O/vryNHjmjNmjVq3bq10x5QeZWfcf7nP/+pp556SmPGjNHBgwdVsWJFRUdHu10iv1KlSqpXr54++eQT3XDDDbr22msVHBysfv36OfK0aNFC3bp10/jx41WtWjVFRERo/fr1uu+++9S+fXtHvvy8T3O71gzDUHR0tHr16lXgO7eAKRTJWnIAUEBfffWVMW7cOKN58+ZGpUqVjDJlyhgVK1Y0unbtaixcuNC4fPmyU/5atWoZTZo0ybY8+1K19uWsw8PDDV9fX5dlpTO7dOmSUa5cOaNBgwaONP29rPDVOnHihOHr62tIMp5//nm3efbu3Wv07NnTCAsLM0JCQoybb77Z2LJli9tld7Nbinf16tVGixYtDH9/f6NatWrGE088YRw8eDDbpXiXLFlidOrUyShXrpwREBBg1KpVyxg0aJDxySef5Klf9iWgly1b5pSe03LW7pbmrVWrlnHzzTe7pNuXdj58+LAjzb4c8B9//GH079/fKFeunBESEmL079/f+P33313KmDdvntGoUSMjICDAqFq1qjF69GgjPj7eZcliu48//tjo0KGDERISYgQFBRk33nijMXHiRKdloVevXm20bNnSCAgIMCS5bXtWf/75p3HvvfcaVapUMfz8/Iw6deoY//73v52Wf86uz7mNU1b25awzLyFtl12/s7umli9fbrRv394ICgoygoKCjPbt2xsrVqxwW+97771nNGjQwPD39zduuOEG4/XXX3cse561LSkpKcaMGTOMpk2bGoGBgUZISIjRsGFD44EHHjC2b9/uyJff5cPzOs6GYRjbt283OnToYAQEBBiVKlUyRo8ebZw7d87tGH3//fdGhw4djKCgIEOSY0nqzMtQf/zxx0azZs0Mf39/o2bNmsYzzzxjpKWludSbn/dpTtfapk2bDEnGqlWr8jQ2gFlZDKOAM0EBAMiHXr16KSUlRVu3bi2U+rp27aqYmBjFxMQUSn1ATmJiYlSnTh0999xzTncbC8OgQYMUGxurH374odgsygEUBeb4AAAKxaxZs7Rt27YC7b0CoGB2796tiIgIzZo1i6AHpR5zfAAAhaJJkyZeXwIYgLOWLVu6LMcOlFbc8QEAAABgeszxAQAAAGB63PEBAAAAYHoEPgAAAABMj8AHAAAAgOkR+AAAAAAwPQIfAAAAAKZH4AMAAADA9Ah8AAAAAJgegQ8AAAAA0yPwAQAAAGB6BD4AAAAATI/ABwAAAIDpEfgAAAAAMD0CHwAAAACmR+ADAAAAwPQIfAAAAACYHoEPAAAAANMj8AEAAABgegQ+AAAAAEyPwAcAAACA6RH4AAAAADA9Ah8AAAAApkfgAwAAAMD0CHwAAAAAmB6BDwAAAADTI/ABAAAAYHoEPgAAAABMj8AHAAAAgOkR+AAAAAAwPQIfAAAAAKZH4AMAAADA9Ah8AAAAAJgegQ8AAAAA0yPwAQAAAGB6BD4AAAAATI/ABwAAAIDpEfgAAAAAMD0CHwAAAACmR+ADAAAAwPQIfAAAAACYHoEPAAAAANMj8AEAAABgegQ+AAAAAEyPwAcAAACA6RH4AAAAADA9Ah8AAAAApkfgAwAAAMD0CHwAAAAAmB6BDwAAAADT+3/RJGhxVnhLaAAAAABJRU5ErkJggg==\n",
             "text/plain": [
               "<IPython.core.display.Image object>"
             ]
           },
           "metadata": {},
-          "execution_count": 30
+          "execution_count": 18
         }
       ]
     },
diff --git a/pyproject.toml b/pyproject.toml
index 61379b0..c00f798 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "fastprop"
-version = "1.0.0b4"
+version = "1.0.0"
 authors = [
     { name = "Jackson Burns" },
 ]
@@ -15,12 +15,12 @@ classifiers = [
 ]
 urls = { Homepage = "https://github.com/JacksonBurns/fastprop" }
 requires-python = ">=3.8"
-dependencies = ["pyyaml", "lightning", "mordredcommunity", "astartes[molecules]", "tensorboard", "psutil"]
+dependencies = ["pyyaml", "lightning", "torch>=1.13", "mordredcommunity", "astartes[molecules]", "tensorboard", "psutil"]
 
 [project.optional-dependencies]
-dev = ["black", "isort", "pytest"]
+dev = ["black", "isort", "pytest", "pytest-cov"]
 hopt = ["ray[train]", "optuna"]
-shap = ["shap", "matplotlib"]
+shap = ["shap<0.45", "matplotlib"]
 bmark = ["py2opsin"]
 
 [project.readme]
@@ -28,7 +28,7 @@ file = "README.md"
 content-type = "text/markdown"
 
 [project.scripts]
-fastprop = "fastprop.cli.fastprop_cli:main"
+fastprop = "fastprop.cli.base:main"
 
 [tool.isort]
 profile = "black"
@@ -36,6 +36,15 @@ profile = "black"
 [tool.black]
 line-length = 150
 
+[tool.autopep8]
+max_line_length = 150
+in-place = true
+recursive = true
+aggressive = 3
+
+[tool.pytest.ini_options]
+addopts = "--cov fastprop"
+
 [tool.setuptools]
 include-package-data = true
 


Test patch:
diff --git a/test/test_fastprop.py b/test/test_fastprop.py
index 35552ce..6bf9225 100644
--- a/test/test_fastprop.py
+++ b/test/test_fastprop.py
@@ -5,7 +5,8 @@
 
 import yaml
 
-from fastprop import train_fastprop
+from fastprop import DEFAULT_TRAINING_CONFIG
+from fastprop.cli.train import train_fastprop
 
 
 class Test_fastprop(unittest.TestCase):
@@ -21,13 +22,15 @@ def setUpClass(cls):
 
     def test_pah(self):
         """Run the PAH benchmark."""
+        train_args = dict(DEFAULT_TRAINING_CONFIG)
         with open(self.config_file, "r") as f:
             fastprop_args = yaml.safe_load(f)
             fastprop_args["target_columns"] = fastprop_args["target_columns"].split(" ")
             fastprop_args["output_directory"] = self.temp_dirname
             fastprop_args["input_file"] = os.path.join(self.benchmark_dir, "pah", "arockiaraj_pah_data.csv")
-            res, _ = train_fastprop(**fastprop_args)
-            assert res.describe().loc["mean", "test_r2"] > 0.95
+            train_args.update(fastprop_args)
+            res = train_fastprop(**train_args)
+            assert res.describe().loc["mean", "test_r2_score"] > 0.90
 
     @classmethod
     def tearDownClass(cls):