Repository: cadCAD-org/cadCAD (language: python)

Issue:
Results doesn't have the results of last simulation when running 4 parallelized configurations
Hello,

I had an issue trying to execute cadCAD with multiple configurations. It works properly when running up to 3 configs. However, when the number of configs is increased to 4, the last configuration's result doesn't appear on the resulting dataframe, although the configuration count indicates the right number when executing run().

This is the run.py script:

```
# The following imports NEED to be in the exact order
from cadCAD.engine import ExecutionMode, ExecutionContext, Executor

# Simulation configs, input any new simulations here
from covid_19_sir import config
from covid_19_seir import config
from covid_19_seird import config
from covid_19_seir_3778 import config


from cadCAD import configs
import pandas as pd


def run(drop_midsteps: bool=True) -> pd.DataFrame:
    """
    Run all experiments and return their output on the dataset column.
    Each line represents an iteration of the parameter-sweep combinations.
    """
    exec_mode = ExecutionMode()
    multi_mode_ctx = ExecutionContext(context=exec_mode.local_mode)
    run = Executor(exec_context=multi_mode_ctx, configs=configs)
    raw_result, _, _ = run.execute()
    results = pd.DataFrame(raw_result)
    return results
```

The config scripts follow the same pattern:

```
MONTE_CARLO_RUNS = 1 # N monte carlo runs

from cadCAD.configuration.utils import config_sim
from cadCAD.configuration import Experiment
from .model.state_variables import genesis_states
from .model.partial_state_update_block import partial_state_update_block
from .model.sys_params import sys_params as sys_params
from .sim_params import SIMULATION_TIME_STEPS


sim_config = config_sim (
    {
        'N': MONTE_CARLO_RUNS,
        'T': range(SIMULATION_TIME_STEPS), # number of timesteps
        'M': sys_params,
    }
)

exp = Experiment()

exp.append_configs(
    sim_configs=sim_config,
    initial_state=genesis_states,
    partial_state_update_blocks=partial_state_update_block
)
```

If I change the order they are imported, the last config that was imported is still not on the dataframe.

Has anyone had a similar problem?  


Discussion / hints:

Fix patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 9acea6de..5a3da204 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -11,13 +11,27 @@ jobs:
       - python/load-cache
       - python/install-deps
       - python/save-cache
-      - run: pip install cadCAD==0.4.18
+      - run: pip install -r requirements.txt
+      - run: python setup.py sdist bdist_wheel
+      - run: pip install dist/*.whl
       - run:
           command: python testing/tests/param_sweep.py
           name: Parameter sweep
       - run:
           command: python testing/tests/policy_aggregation.py
           name: Policy Aggregation
+      - run:
+          command: python testing/tests/multi_model_sim.py
+          name: Multi Model Simulation
+      - run:
+          command: python testing/tests/timestep1psub0.py
+          name: Timestep equals 1 instead of 0 for 1st PSUB
+      - run:
+          command: python testing/tests/runs_not_zero.py
+          name: Value Error thrown when Runs < 1
+      - run:
+          command: python testing/tests/run1psub0.py
+          name: Run Starts at 1 for PSUB 0
       - run:
           command: python -m unittest discover -s testing/tests -p "*_test.py"
           name: Test Suite
diff --git a/.gitignore b/.gitignore
index b16f0cb2..c4c265bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,35 +1,40 @@
-.idea
-.ipynb_checkpoints
-.DS_Store
-.idea
-.eggs
-.pytest_cache
-client_work
-
-notebooks
+*.idea
+*.vscode
+*.ipynb_checkpoints
+*.DS_Store
+*.eggs
+*.pytest_cache
+*.mypy_cache
+*.csv
 *.egg-info
+*.sqlite3
+*.pyc
+*venv
+*external_data
+*sys_exec.py
+*/__pycache__
+*.DS_Store
+*notes.*
+*announcement.md
+*dist.py
+*/poc
+
 __pycache__
 Pipfile
 Pipfile.lock
+build
 results
-.mypy_cache
-*.csv
-simulations/.ipynb_checkpoints
-simulations/validation/config3.py
-cadCAD.egg-info
+result
+notebooks
+client_work
+monkeytype
+venv
+notes.*
+announcement.md
 
-build
-cadCAD.egg-info
 
-testing/example.py
-testing/example2.py
-testing/multi_config_test.py
-testing/udo.py
-testing/udo_test.py
+simulations/tickets
+simulations/validation
+simulations/poc/local/
 simulations/regression_tests/poc
-
-Simulation.md
-
-monkeytype.sqlite3
-
-result
+simulations/regression_tests/poc_configs
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 59c226e1..76a40daa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,33 +1,267 @@
 # Changelog:
+
+### May 21, 2021
+
+### New Features:
+* **ver. ≥ `0.4.24`:**
+    * ##### [Experiments](documentation#experiments)
+        * ##### [System Model Configurations]
+          * Configurations (`cadCAD.utils.Configuration`'s) as are no longer a part of the `cadCAD` module 
+            (as `cadCAD.configs`) and are now accessed via the `configs` member of `cadCAD.configuration.Experiment`.
+            [Example:](documentation#experiments) `cadCAD.configuration.Experiment().configs`
+        * `cadCAD.configuration.Experiment` is unique representation of an experiment of one or more configured System 
+        Models. An `Experiment`'s `append_model` method stores multiple system model `Configuration`'s for simulation 
+        execution.
+            * The `Experiment`'s `append_model` method requires a `model_id` parameter that is auto-created as `'sys_model_#'`.
+            * **Requirements:** 
+              * Users must use different `model_id`'s when appending multiple System Model Configurations.
+              * Users can no longer use the `config_list` method of `cadCAD.configuration.Experiment`
+            * **Backwards Compatibility:** The `append_model` method of `cadCAD.configuration.Experiment` can also be used as 
+              the `append_configs` method.
+* **Hot-Fixes:** 
+    * [#257](https://github.com/cadCAD-org/cadCAD/issues/257)
+        * The `append_model` method of `cadCAD.configuration.Experiment` will no longer throw mis-leading error message 
+          during simulation execution.
+    * [#242](https://github.com/cadCAD-org/cadCAD/issues/242)
+        * Parallelized simulations re-enabled with the re-inclusion of `ProcessPool`.
+    * [#250](https://github.com/cadCAD-org/cadCAD/issues/250)
+        * First Partial State Update Block at first timestep no longer equals to 0 instead of the expected 1 in simulation 
+          output.
+
+
+### September 22, 2020
+##### [Multi - System Model Execution](https://github.com/cadCAD-org/cadCAD/blob/master/documentation/Simulation_Execution.md#multiple-simulation-execution)
+* **ver. ≥ `0.4.23`:**
+    * **Hot-Fix:** [#203](https://github.com/cadCAD-org/cadCAD/pull/203) (**No Breaking Changes**)
+        * Multi - System Model simulation results will no longer return truncated results (exclude the results of the last 
+        `cadCAD.configuration.Configuration` appended to `cadCAD.configs`).
+        * Issue: [#195](https://github.com/cadCAD-org/cadCAD/issues/195)
+    * Parameter Sweep value `M` (Params) requires up to a maximum of 2 distinct lengths
+
+
 ### August 5, 2020
-* Experiment Class: Representation of an experiment as one or more configured System Models
+##### [Experiments](documentation#experiments)
+* `cadCAD.configuration.Experiment` (Alpha) is in development and needed to be released to support the implementation of 
+web applications and proprietary feature extensions. It is intended to represent a unique identifier of an experiment of 
+one or more configured System Models. For this reason, `append_configs` is a method of 
+`cadCAD.configuration.Experiment`. As of now it does not support multi - system model simulation because configurations 
+are still appended globally despite `append_config` being a method of Experiment.
+
+###### Examples:
+* **ver. ≥ `0.4.22`:**
+    ```python
+    from cadCAD.configuration import Experiment
+    exp = Experiment()
+    exp.append_configs(...)
+    ```
+* **ver. `0.3.1`:** *Deprecated*
+    ```python
+    from cadCAD.configuration import append_configs
+    append_configs(...)
+    ```
+
 
 ### June 22, 2020
 * Bug Fix: Multiprocessing error for Windows
 
+
 ### June 19, 2020
     
 ### New Features:
-##### [Local Execution Mode](documentation/Simulation_Execution.md)
-* Default parallelization of Monte-Carlo / Stochastic simulations
-* **Backwards Compatible** with given legacy modes names
-
-##### [cadCAD Post-Processing Enhancements](https://github.com/cadCAD-org/cadCAD/blob/master/documentation/Simulation_Execution.md#execute-simulation-produce-system-event-dataset)
-* Returns single dataset as three types depending on execution mode:
-    * Local Mode:
-        * 2d List
-* Changes:
-    * Returning a single dataset was originally specified at the project’s inception instead of multiple per simulation
-* [System Configuration Conversions](documentation/System_Configuration.md)
-    * System Configuration as List of Configuration Objects
-    * System Configuration as a Pandas DataFrame
-    * System Configuration as List of Dictionaries
-
-**Backwards compatibility:**
-* Expandable state and policy update parameter space enables changes to the parameter space of updates while 
-supporting backwards compatibility
-* Legacy execution modes supported
+##### [Local Execution Mode (Default)](documentation/Simulation_Execution.md#simulation-execution-modes)
+* Local Execution Mode (Default): Implicit parallelization of Monte-Carlo / Stochastic simulations (Automatically 
+selects Multi-Threaded Mode if simulations are configured for more than a single run)
+    * **Backwards Compatibility:** `cadCAD.engine.ExecutionMode` accepts legacy execution modes from ver. `0.3.1`
+    
+###### Examples:
+* **ver. ≥ `0.4.22`:**
+    ```python
+    from cadCAD.engine import ExecutionMode, ExecutionContext
+    exec_mode = ExecutionMode()
+    local_ctx = ExecutionContext(context=exec_mode.local_mode)
+    ```
+* **ver. `0.3.1`:** 
+    
+    Multi-Threaded:
+    ```python
+    from cadCAD.engine import ExecutionMode, ExecutionContext
+    
+    exec_mode = ExecutionMode()
+    single_ctx = ExecutionContext(context=exec_mode.multi_proc)
+    ```
+    
+    Single-Thread:
+    ```python
+    from cadCAD.engine import ExecutionMode, ExecutionContext
+  
+    exec_mode = ExecutionMode()
+    multi_ctx = ExecutionContext(context=exec_mode.single_proc)
+    ```
+        
+
+##### cadCAD Post-Processing Enhancements / Modifications
+* 	[**Single Result Dataset**](documentation/Simulation_Execution.md#4-execute-simulation--produce-system-event-dataset) 
+as a 2 dimensional `list`
+    * Returns a single dataset instead of multiple datasets per Monte Carlo simulation as in `0.3.1`:
+        * New System Metrics as dataset attributes: 
+            * **Simulation** (Alpha) is a unique identifier being developed to represent Experiments as stated above and 
+            will be renamed accordingly
+                * **Subset** is a unique identifier of Monte-Carlo simulations produced by parameter sweeps
+    * Note: Returning a single dataset was originally specified during the project’s inception instead of multiple per 
+    simulation
+
+###### Examples:
+* **ver. ≥ `0.4.22`:**
+    ```python
+    import pandas as pd
+    from tabulate import tabulate
+    from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
+    import system_model_A, system_model_B
+    from cadCAD import configs
+  
+    exec_mode = ExecutionMode()
+    local_ctx = ExecutionContext(context=exec_mode.local_mode)
+    simulation = Executor(exec_context=local_ctx, configs=configs)
+    raw_result, sys_model, _ = simulation.execute()
+  
+    result = pd.DataFrame(raw_result)
+    print(tabulate(result, headers='keys', tablefmt='psql'))
+    ```
+    Results:
+    ```
+    +----+------------+-----------+----+---------------------+------------+--------+-----+---------+----------+
+    |    | s1         | s2        | s3 | timestamp           | simulation | subset | run | substep | timestep |
+    |----+------------+-----------+----+---------------------+------------+--------+-----+---------+----------|
+    |  0 | 0.0        | 0.0       |  1 | 2018-10-01 15:16:24 |          0 |      0 |   1 |       0 |        0 |
+    |  1 | 1.0        | 4         |  5 | 2018-10-01 15:16:25 |          0 |      0 |   1 |       1 |        1 |
+    |  2 | 2.0        | 6         |  5 | 2018-10-01 15:16:25 |          0 |      0 |   1 |       2 |        1 |
+    |  3 | 3.0        | [ 30 300] |  5 | 2018-10-01 15:16:25 |          0 |      0 |   1 |       3 |        1 |
+    |  4 | 0          | 0         |  1 | 2018-10-01 15:16:24 |          1 |      0 |   1 |       0 |        0 |
+    |  5 | 1          | 0         |  5 | 2018-10-01 15:16:25 |          1 |      0 |   1 |       1 |        1 |
+    |  6 | a          | 0         |  5 | 2018-10-01 15:16:25 |          1 |      0 |   1 |       2 |        1 |
+    |  7 | ['c', 'd'] | [ 30 300] |  5 | 2018-10-01 15:16:25 |          1 |      0 |   1 |       3 |        1 |
+    +----+------------+-----------+----+---------------------+------------+--------+-----+---------+----------+
+    ```
+* **ver. `0.3.1`:**
+    ```python
+    import pandas as pd
+    from tabulate import tabulate
+    from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
+    import system_model_A, system_model_B
+    from cadCAD import configs
+    
+    exec_mode = ExecutionMode()
+    
+    multi_ctx = ExecutionContext(context=exec_mode.multi_proc)
+    simulation = Executor(exec_context=multi_ctx, configs=configs)
+    
+    i = 0
+    config_names = ['sys_model_A', 'sys_model_B']
+    for raw_result, _ in simulation.execute():
+        result = pd.DataFrame(raw_result)
+        print()
+        print(f"{config_names[i]} Result: System Events DataFrame:")
+        print(tabulate(result, headers='keys', tablefmt='psql'))
+        print()
+        i += 1
+    ```
+    Results:
+    ```
+    +----+------------+-----------+----+---------------------+-----+---------+----------+
+    |    | s1         | s2        | s3 | timestamp           | run | substep | timestep |
+    |----+------------+-----------+----+---------------------+-----+---------+----------|
+    |  0 | 0.0        | 0.0       |  1 | 2018-10-01 15:16:24 |   1 |       0 |        0 |
+    |  1 | 1.0        | 4         |  5 | 2018-10-01 15:16:25 |   1 |       1 |        1 |
+    |  2 | 2.0        | 6         |  5 | 2018-10-01 15:16:25 |   1 |       2 |        1 |
+    |  3 | 3.0        | [ 30 300] |  5 | 2018-10-01 15:16:25 |   1 |       3 |        1 |
+    |  4 | 0          | 0         |  1 | 2018-10-01 15:16:24 |   1 |       0 |        0 |
+    |  5 | 1          | 0         |  5 | 2018-10-01 15:16:25 |   1 |       1 |        1 |
+    |  6 | a          | 0         |  5 | 2018-10-01 15:16:25 |   1 |       2 |        1 |
+    |  7 | ['c', 'd'] | [ 30 300] |  5 | 2018-10-01 15:16:25 |   1 |       3 |        1 |
+    +----+------------+-----------+----+---------------------+-----+---------+----------+
+    ```
+    
+* **Flattened Configuration List:** The `cadCAD.configs` (System Model Configuration) `list` has been **temporarily** 
+flattened to contain single run `cadCAD.configuration.Configuration` objects to both fault-tolerant simulation and 
+elastic workloads. This functionality will be restored in a subsequent release by a class that returns 
+`cadCAD.configs`'s original representation in ver. `0.3.1`.
+    * The conversion utilities have been provided to restore its original representation of configurations with 
+    runs >= 1
+        * [System Configuration Conversions](documentation/System_Configuration.md)
+            * Configuration as List of Configuration Objects (as in ver. `0.3.1`) 
+            * New: System Configuration as a Pandas DataFrame
+            * New: System Configuration as List of Dictionaries
+            
+###### Examples:
+* Notes:
+    * `configs` is temporarily returned in a flattened format and reformatted into its intended format 
+    * `Configuration` objects at `0x10790e470` and `0x1143dd630` are reconstituted into objects at `0x10790e7b8` 
+    and `0x116268908` respectively.
+* **ver. ≥ `0.4.22`:** 
+    ```python
+    from pprint import pprint
+    from documentation.examples import sys_model_A, sys_model_B
+    from cadCAD.configuration.utils import configs_as_objs, configs_as_dataframe, configs_as_dicts
+    from cadCAD import configs
+    
+    flattened_configs = configs
+    
+    print('Flattened Format: Temporary')  
+    pprint(flattened_configs)
+    print()
+    
+    print('Intended Format:')
+    intended_configs = configs_as_objs(flattened_configs)
+    pprint(intended_configs)
+    print()
+    ```
+        
+    Result:
+    ```bash
+    Flattened Format: Temporary
+    [<cadCAD.configuration.Configuration object at 0x10790e470>,
+     <cadCAD.configuration.Configuration object at 0x10790e7b8>,
+     <cadCAD.configuration.Configuration object at 0x1143dd630>,
+     <cadCAD.configuration.Configuration object at 0x116268908>]
+    
+    Intended Format:
+    [<cadCAD.configuration.Configuration object at 0x10790e7b8>,
+     <cadCAD.configuration.Configuration object at 0x116268908>]
+    ```
+
+
+##### Expandable state and policy update parameter space: 
+* Enables the development of feature enhancements that involve the use of additional parameters without requiring users 
+to modify their update parameters spaces when upgrading to newer versions. For this reason state / policy update 
+examples in documentation include an additional `**kwargs` parameter.
+    * [State Updates](documentation/README.md#state-update-functions)
+    * [Policy Updates](documentation/README.md#state-update-functions)
+    
+###### Examples:
+* **ver. ≥ `0.4.22`:**
+    ```python
+    def state_update(_params, substep, sH, s, _input, **kwargs):
+        ...
+        return 'state_variable_name', new_value
+
+    def policy(_params, substep, sH, s, **kwargs):
+        ...
+        return {'signal_1': value_1, ..., 'signal_N': value_N}
+    ```
+* **ver. `0.3.1`:**
+
+    ```python
+    def state_update(_params, substep, sH, s, _input):
+        ...
+        return 'state_variable_name', new_value
+
+    def policy(_params, substep, sH, s):
+        ...
+        return {'signal_1': value_1, ..., 'signal_N': value_N}
+    ```
+
 
 ### May 29, 2020
 * Packaging: Add [Nix](https://nixos.org/) derivation and shell for local development and distribution of cadCAD package 
-using Nix. Nix is a powerful package manager for Linux and other Unix systems that makes package management reliable and reproducible, allowing you to share your development and build environments across different machines.
+using Nix. Nix is a powerful package manager for Linux and other Unix systems that makes package management reliable and 
+reproducible, allowing you to share your development and build environments across different machines.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7d1356de..5ef7aa90 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,26 +11,35 @@ Pull Request (PR) presented as "->".
 
 General Template:
 
-`user/branch -> BlockScience/staging`
+`user:branch -> org:staging`
 
 Contributing a new feature:
 
-`user/feature -> BlockScience/staging`
+`user:feature -> org:staging`
 
 Contributing to an existing feature:
 
-`user/feature -> BlockScience/feature`
+`user:feature -> org:feature`
 
 ### General Advise for Forked Repositories:
 1. `git pull fork staging`
 2. `git checkout -b feature` (new feature)
 3. Apply your awesomeness! (Commit Often)
 4. `git push fork feature`
-5. Apply a merge strategy you're comfortable with.
-6. Submit PR from `user:staging` into `BlockScience:staging`
+5. Apply a rebase/merge strategy you're comfortable with (Recommended Below).
+6. Submit PR from `user:staging` into `org:staging`
 7. PR is queued for review
 8. PR Reviewed (Update necessary if rejected)
 9. PR Approved (There may be circumstances delaying the merge.)
-10. Your contribution merged into next feature release on `BlockScience:master`
+10. Your contribution merged into next feature release on `org:master`
+
+### Recommended Strategy: [Rebase](https://git-scm.com/book/en/v2/Git-Branching-Rebasing)
+1. Add cadCAD-org/cadCAD as remote within you forked project locally.
+2. `git checkout remote/master`
+3. `git pull remote master`
+4. `git checkout your_branch`
+5. `git rebase master`
+6. Resolve merge conflicts (while leveraging rebase commands)
+7. `git push fork your_branch`
 
 Thanks! :heart:
diff --git a/README.md b/README.md
index 7fbb7c15..c9ffe1f8 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
  / ___/ __` / __  / /   / /| | / / / /
 / /__/ /_/ / /_/ / /___/ ___ |/ /_/ /
 \___/\__,_/\__,_/\____/_/  |_/_____/
-by cadCAD                  ver. 0.4.21
+by cadCAD                  ver. 0.4.24
 ======================================
        Complex Adaptive Dynamics       
        o       i        e
@@ -21,24 +21,43 @@ through simulation, with support for Monte Carlo methods, A/B testing and parame
 # Getting Started
 
 
-#### Change Log: [ver. 0.4.21](CHANGELOG.md)
+#### Change Log: [ver. 0.4.24](CHANGELOG.md)
 
 [Previous Stable Release (No Longer Supported)](https://github.com/cadCAD-org/cadCAD/tree/b9cc6b2e4af15d6361d60d6ec059246ab8fbf6da)
 
+## 0. Pre-installation Virtual Environments with [`venv`](https://docs.python.org/3/library/venv.html) (Optional):
+If you wish to create an easy to use virtual environment to install cadCAD inside of, please use the built in `venv` package.
+
+***Create** a virtual environment:*
+```bash
+$ python3 -m venv ~/cadcad
+```
+
+***Activate** an existing virtual environment:*
+```bash
+$ source ~/cadcad/bin/activate
+(cadcad) $
+```
+
+***Deactivate** virtual environment:*
+```bash
+(cadcad) $ deactivate
+$
+```
 
 ## 1. Installation: 
 Requires [>= Python 3.6](https://www.python.org/downloads/) 
 
 **Option A: Install Using [pip](https://pypi.org/project/cadCAD/)** 
 ```bash
-pip3 install cadCAD
+$ pip3 install cadCAD
 ```
 
 **Option B:** Build From Source
 ```
-pip3 install -r requirements.txt
-python3 setup.py sdist bdist_wheel
-pip3 install dist/*.whl
+$ pip3 install -r requirements.txt
+$ python3 setup.py sdist bdist_wheel
+$ pip3 install dist/*.whl
 ```
 
 **Option C: Using [Nix](https://nixos.org/nix/)**
diff --git a/cadCAD/__init__.py b/cadCAD/__init__.py
index d9f8feb2..af1b9b97 100644
--- a/cadCAD/__init__.py
+++ b/cadCAD/__init__.py
@@ -1,8 +1,10 @@
 import os, dill
 
 name = "cadCAD"
-configs = []
-global experiment_count
+version = "0.4.24"
+
+sys_job_metrics = None
+remote_dict = {'metrics': sys_job_metrics}
 
 if os.name == 'nt':
     dill.settings['recurse'] = True
diff --git a/cadCAD/configuration/__init__.py b/cadCAD/configuration/__init__.py
index 91a550a9..43ceceff 100644
--- a/cadCAD/configuration/__init__.py
+++ b/cadCAD/configuration/__init__.py
@@ -1,98 +1,122 @@
 from typing import Dict, Callable, List, Tuple
 from pandas.core.frame import DataFrame
+from datetime import datetime
 from collections import deque
 from copy import deepcopy
+from funcy import curry
 import pandas as pd
+import dill
 
-from cadCAD import configs
 from cadCAD.utils import key_filter
-from cadCAD.configuration.utils import exo_update_per_ts
+from cadCAD.configuration.utils import exo_update_per_ts, configs_as_objs
 from cadCAD.configuration.utils.depreciationHandler import sanitize_partial_state_updates, sanitize_config
 
 
-class Configuration(object):
-    def __init__(self, user_id, subset_id, subset_window, sim_config={}, initial_state={}, seeds={}, env_processes={},
+class Configuration():
+    def __init__(self, user_id, model_id, subset_id, subset_window, sim_config={}, initial_state={}, seeds={}, env_processes={},
                  exogenous_states={}, partial_state_update_blocks={}, policy_ops=[lambda a, b: a + b],
-                 session_id=0, simulation_id=0, run_id=1, experiment_id=0, exp_window=deque([0, None], 2), **kwargs
+                 session_id=0, simulation_id=0, run_id=1, experiment_id=0, exp_window=deque([0, None], 2),
+                 exp_creation_ts=None, **kwargs
     ) -> None:
         self.sim_config = sim_config
         self.initial_state = initial_state
         self.seeds = seeds
         self.env_processes = env_processes
         self.exogenous_states = exogenous_states
-        self.partial_state_updates = partial_state_update_blocks
+        self.partial_state_update_blocks = partial_state_update_blocks
         self.policy_ops = policy_ops
         self.kwargs = kwargs
 
+        self.session_id = session_id  # essentially config id
+
+        self.experiment_id = experiment_id
         self.user_id = user_id
-        self.session_id = session_id # essentially config id
+        self.model_id = model_id
+        self.exp_creation_ts = exp_creation_ts
+
+        self.labeled_jobs = {}
         self.simulation_id = simulation_id
+        self.subset_id = subset_id
         self.run_id = run_id
-        self.experiment_id = experiment_id
+
         self.exp_window = exp_window
-        self.subset_id = subset_id
         self.subset_window = subset_window
 
         sanitize_config(self)
 
 
-class Experiment:
+class Experiment(object):
     def __init__(self):
+        self.exp_creation_ts = str(datetime.utcnow())
+        self.configs = []
+        self.ser_configs = []
+        self.sys_configs = []
+
+        self.model_job_map, self.model_job_counts = {}, {}
+        self.model_ser_job_map, self.model_ser_job_counts = {}, {}
+        self.model_ids = list(self.model_job_map.keys())
+        self.model_id_queue = []
+
         self.exp_id = 0
+        self.simulation_id = -1
         self.subset_id = 0
         self.exp_window = deque([self.exp_id, None], 2)
         self.subset_window = deque([self.subset_id, None], 2)
 
-    def append_configs(
+        # self.sys_job_metrics = None
+        # self.remote_dict = {'metrics': self.sys_job_metrics}
+        # Metric counters sent over network within `Configurations`
+
+    def append_model(
             self,
             user_id='cadCAD_user',
+            model_id='sys_model_#',
             sim_configs={}, initial_state={}, seeds={}, raw_exogenous_states={}, env_processes={},
-            partial_state_update_blocks={}, policy_ops=[lambda a, b: a + b], _exo_update_per_ts: bool = True,
-            config_list=configs
+            partial_state_update_blocks={}, policy_ops=[lambda a, b: a + b], _exo_update_per_ts: bool = True, **kwargs
+            # config_list=deepcopy(global_configs)
     ) -> None:
+        _sim_configs = deepcopy(sim_configs)
+        # self.configs = config_list
+        self.simulation_id += 1
 
         try:
-            max_runs = sim_configs[0]['N']
+            max_runs = _sim_configs[0]['N']
         except KeyError:
-            max_runs = sim_configs['N']
+            max_runs = _sim_configs['N']
 
         if _exo_update_per_ts is True:
             exogenous_states = exo_update_per_ts(raw_exogenous_states)
         else:
             exogenous_states = raw_exogenous_states
 
-        if isinstance(sim_configs, dict):
-            sim_configs = [sim_configs]
-
-        simulation_id = 0
-        if len(config_list) > 0:
-            last_config = config_list[-1]
-            simulation_id = last_config.simulation_id + 1
+        if isinstance(_sim_configs, dict):
+            _sim_configs = [_sim_configs]
 
-        sim_cnt = 0
+        sim_cnt_local = 0
         new_sim_configs = []
-        for subset_id, t in enumerate(list(zip(sim_configs, list(range(len(sim_configs)))))):
+        for subset_id, t in enumerate(list(zip(_sim_configs, list(range(len(_sim_configs)))))):
             sim_config = t[0]
             sim_config['subset_id'] = subset_id
             sim_config['subset_window'] = self.subset_window
             N = sim_config['N']
             if N > 1:
                 for n in range(N):
-                    sim_config['simulation_id'] = simulation_id + sim_cnt
+                    sim_config['simulation_id'] = self.simulation_id
                     sim_config['run_id'] = n
                     sim_config['N'] = 1
                     # sim_config['N'] = n + 1
                     new_sim_configs.append(deepcopy(sim_config))
                 del sim_config
             else:
-                sim_config['simulation_id'] = simulation_id
+                sim_config['simulation_id'] = self.simulation_id
                 sim_config['run_id'] = 0
                 new_sim_configs.append(deepcopy(sim_config))
                 # del sim_config
 
-            sim_cnt += 1
+            sim_cnt_local += 1
 
         run_id = 0
+        new_model_ids, new_configs = [], []
         for sim_config in new_sim_configs:
             subset_id = sim_config['subset_id']
             sim_config['N'] = run_id + 1
@@ -104,6 +128,8 @@ def append_configs(
 
             self.exp_window = deepcopy(self.exp_window)
             config = Configuration(
+                exp_creation_ts=self.exp_creation_ts,
+
                 sim_config=sim_config,
                 initial_state=initial_state,
                 seeds=seeds,
@@ -114,19 +140,49 @@ def append_configs(
 
                 # session_id=session_id,
                 user_id=user_id,
+                model_id=model_id,
                 session_id=f"{user_id}={sim_config['simulation_id']}_{sim_config['run_id']}",
-                simulation_id=sim_config['simulation_id'],
-                run_id=sim_config['run_id'],
 
                 experiment_id=self.exp_id,
-                exp_window=self.exp_window,
+                simulation_id=self.simulation_id,
                 subset_id=subset_id,
+                run_id=sim_config['run_id'],
+
+                exp_window=self.exp_window,
                 subset_window=self.subset_window
             )
-            configs.append(config)
+
+            # self.configs.append(config)
+            new_configs.append(config)
+            new_model_ids.append(model_id)
             run_id += 1
+        self.configs += new_configs
+        self.model_id_queue += new_model_ids
         self.exp_id += 1
         self.exp_window.appendleft(self.exp_id)
+        self.sys_configs += configs_as_objs(new_configs)
+
+        unique_new_model_ids = list(set(new_model_ids))
+        new_model_job_list = [(model_id, []) for model_id in unique_new_model_ids]
+        for model_id, v in new_model_job_list:
+            if model_id not in self.model_ids:
+                self.model_job_map[model_id] = v
+                self.model_ids.append(model_id)
+            else:
+                except_str = f"""
+                    Error: Duplicate model_id in Experiment - \'{model_id}\' in {self.model_ids} 
+                    -- Specify unique model_id for each use of `.append_config` per `Experiment()`
+                """
+                raise Exception(except_str)
+
+        for model_id, job in list(zip(new_model_ids, new_configs)):
+            self.model_job_map[model_id].append(job)
+            self.model_ser_job_map[model_id] = []
+
+        self.model_job_counts = dict([(k, len(v)) for k, v in self.model_job_map.items()])
+        self.model_ser_job_counts = self.model_job_counts
+
+    append_configs = append_model
 
 
 class Identity:
diff --git a/cadCAD/configuration/utils/__init__.py b/cadCAD/configuration/utils/__init__.py
index 3e85afa6..5063a007 100644
--- a/cadCAD/configuration/utils/__init__.py
+++ b/cadCAD/configuration/utils/__init__.py
@@ -1,9 +1,10 @@
+import pandas as pd
+import numpy as np
 from datetime import datetime, timedelta
 from collections import Counter
 from copy import deepcopy
 from functools import reduce
 from funcy import curry
-import pandas as pd
 
 from cadCAD.configuration.utils.depreciationHandler import sanitize_partial_state_updates
 from cadCAD.utils import dict_filter, contains_type, flatten_tabulated_dict, tabulate_dict
@@ -27,6 +28,7 @@ def create_tensor_field(self, partial_state_updates, exo_proc, keys=['policies',
 
 def configs_as_spec(configs):
     sim_ids = list(map(lambda x: x.simulation_id, configs))
+    # sim_ids = [config.simulation_id for config in configs]
     sim_id_counts = list(Counter(sim_ids).values())
     IDed_configs = list(zip(sim_ids, configs))
     del sim_ids
@@ -68,6 +70,8 @@ def configs_as_dataframe(configs):
 def state_update(y, x):
     return lambda var_dict, sub_step, sL, s, _input, **kwargs: (y, x)
 
+def policy(y, x):
+    return lambda _g, step, sL, s, **kwargs: {y: x}
 
 def bound_norm_random(rng, low, high):
     res = rng.normal((high+low)/2, (high-low)/6)
@@ -161,7 +165,19 @@ def config_sim(d):
     def process_variables(d):
         return flatten_tabulated_dict(tabulate_dict(d))
 
+    if "N" in d:
+        if d["N"] <= 0:
+            raise ValueError("'N' must be > 0")
+    else:
+        raise KeyError("The 'sim_configs' dictionary must contain the key 'N'")
+
+    if "T" not in d:
+        raise KeyError("The 'sim_configs' dictionary must contain the key 'T'")
+
     if "M" in d:
+        M_lengths = len(list(set({key: len(value) for key, value in d["M"].items()}.values())))
+        if M_lengths > 2:
+            raise Exception('`M` values require up to a maximum of 2 distinct lengths')
         return [{"N": d["N"], "T": d["T"], "M": M} for M in process_variables(d["M"])]
     else:
         d["M"] = [{}]
@@ -224,13 +240,13 @@ def state_sweep_filter(raw_exogenous_states):
 def sweep_partial_states(_type, in_config):
     configs = []
     # filtered_mech_states
-    filtered_partial_states = partial_state_sweep_filter(_type, in_config.partial_state_updates)
+    filtered_partial_states = partial_state_sweep_filter(_type, in_config.partial_state_update_blocks)
     if len(filtered_partial_states) > 0:
         for partial_state, state_dict in filtered_partial_states.items():
             for state, state_funcs in state_dict.items():
                 for f in state_funcs:
                     config = deepcopy(in_config)
-                    config.partial_state_updates[partial_state][_type][state] = f
+                    config.partial_state_update_blocks[partial_state][_type][state] = f
                     configs.append(config)
                     del config
     else:
@@ -258,10 +274,3 @@ def sweep_states(state_type, states, in_config):
         configs = [in_config]
 
     return configs
-
-# @curried
-# def env_proc_trigger(timestep, f, time):
-#     if time == timestep:
-#         return f
-#     else:
-#         return lambda x: x
diff --git a/cadCAD/configuration/utils/depreciationHandler.py b/cadCAD/configuration/utils/depreciationHandler.py
index 2481d9ff..1c24fbff 100644
--- a/cadCAD/configuration/utils/depreciationHandler.py
+++ b/cadCAD/configuration/utils/depreciationHandler.py
@@ -8,7 +8,7 @@ def sanitize_config(config):
         elif key == 'seed':
             config.seeds = value
         elif key == 'mechanisms':
-            config.partial_state_updates = value
+            config.partial_state_update_blocks = value
 
     if config.initial_state == {}:
         raise Exception('The initial conditions of the system have not been set')
diff --git a/cadCAD/engine/__init__.py b/cadCAD/engine/__init__.py
index bb8af18b..57d84bc7 100644
--- a/cadCAD/engine/__init__.py
+++ b/cadCAD/engine/__init__.py
@@ -4,10 +4,9 @@
 from cadCAD.utils import flatten
 from cadCAD.utils.execution import print_exec_info
 from cadCAD.configuration import Configuration, Processor
-from cadCAD.configuration.utils import TensorFieldReport, configs_as_objs
+from cadCAD.configuration.utils import TensorFieldReport, configs_as_objs, configs_as_dicts
 from cadCAD.engine.simulation import Executor as SimExecutor
 from cadCAD.engine.execution import single_proc_exec, parallelize_simulations, local_simulations
-from cadCAD.configuration.utils import configs_as_dicts
 
 VarDictType = Dict[str, List[Any]]
 StatesListsType = List[Dict[str, Any]]
@@ -36,19 +35,19 @@ def __init__(self, context=ExecutionMode.local_mode, method=None, additional_obj
             self.method = parallelize_simulations
         elif context == 'dist_proc':
             def distroduce_proc(
-                    simulation_execs, var_dict_list, states_lists, configs_structs, env_processes_list, Ts, RunIDs,
+                    simulation_execs, var_dict_list, states_lists, configs_structs, env_processes_list, Ts, SimIDs, RunIDs,
                     ExpIDs,
                     SubsetIDs,
                     SubsetWindows,
-                    exec_method,
+                    configured_n, # exec_method,
                     sc, additional_objs=additional_objs
             ):
                 return method(
-                    simulation_execs, var_dict_list, states_lists, configs_structs, env_processes_list, Ts, RunIDs,
+                    simulation_execs, var_dict_list, states_lists, configs_structs, env_processes_list, Ts, SimIDs, RunIDs,
                     ExpIDs,
                     SubsetIDs,
                     SubsetWindows,
-                    exec_method,
+                    configured_n, # exec_method,
                     sc, additional_objs
                 )
 
@@ -57,15 +56,19 @@ def distroduce_proc(
 
 class Executor:
     def __init__(self,
-             exec_context: ExecutionContext, configs: List[Configuration], spark_context=None
+             exec_context: ExecutionContext, configs: List[Configuration], sc=None, empty_return=False
     ) -> None:
-        self.sc = spark_context
+        self.sc = sc
         self.SimExecutor = SimExecutor
         self.exec_method = exec_context.method
         self.exec_context = exec_context.name
         self.configs = configs
+        self.empty_return = empty_return
 
     def execute(self) -> Tuple[Any, Any, Dict[str, Any]]:
+        if self.empty_return is True:
+            return [], [], []
+
         config_proc = Processor()
         create_tensor_field = TensorFieldReport(config_proc).create_tensor_field
 
@@ -77,6 +80,7 @@ def execute(self) -> Tuple[Any, Any, Dict[str, Any]]:
         partial_state_updates, sim_executors = [], []
         config_idx = 0
 
+        # Execution Info
         print_exec_info(self.exec_context, configs_as_objs(self.configs))
 
         t1 = time()
@@ -94,16 +98,16 @@ def execute(self) -> Tuple[Any, Any, Dict[str, Any]]:
             ExpIDs.append(x.experiment_id)
             ExpWindows.append(x.exp_window)
             SimIDs.append(x.simulation_id)
-            RunIDs.append(x.run_id)
             SubsetIDs.append(x.subset_id)
+            RunIDs.append(x.run_id)
             SubsetWindows.append(x.subset_window)
 
             var_dict_list.append(x.sim_config['M'])
             states_lists.append([x.initial_state])
             eps.append(list(x.exogenous_states.values()))
-            configs_structs.append(config_proc.generate_config(x.initial_state, x.partial_state_updates, eps[config_idx]))
+            configs_structs.append(config_proc.generate_config(x.initial_state, x.partial_state_update_blocks, eps[config_idx]))
             env_processes_list.append(x.env_processes)
-            partial_state_updates.append(x.partial_state_updates)
+            partial_state_updates.append(x.partial_state_update_blocks)
             sim_executors.append(SimExecutor(x.policy_ops).simulation)
 
             config_idx += 1
@@ -121,7 +125,7 @@ def get_final_results(simulations, psus, eps, sessions, remote_threshold):
             flat_simulations = flatten(flat_timesteps)
             if config_amt == 1:
                 return simulations, tensor_fields, sessions
-            elif (config_amt > 1): # and (config_amt < remote_threshold):
+            elif config_amt > 1:
                 return flat_simulations, tensor_fields, sessions
 
         remote_threshold = 100
@@ -131,16 +135,13 @@ def auto_mode_switcher(config_amt):
             try:
                 if config_amt == 1:
                     return ExecutionMode.single_mode, single_proc_exec
-                elif (config_amt > 1): # and (config_amt < remote_threshold):
+                elif (config_amt > 1):
                     return ExecutionMode.multi_mode, parallelize_simulations
             except AttributeError:
                 if config_amt < 1:
-                    raise ValueError('N must be > 1!')
-                # elif config_amt > remote_threshold:
-                #     print('Remote Threshold is N=100. Use ExecutionMode.dist_proc if N >= 100')
+                    raise ValueError('N must be >= 1!')
 
         final_result = None
-        # original_context = self.exec_context
         original_N = len(configs_as_dicts(self.configs))
         if self.exec_context != ExecutionMode.distributed:
             # Consider Legacy Support
@@ -152,6 +153,7 @@ def auto_mode_switcher(config_amt):
                 sim_executors, var_dict_list, states_lists, configs_structs, env_processes_list, Ts, SimIDs, RunIDs,
                 ExpIDs, SubsetIDs, SubsetWindows, original_N
             )
+
             final_result = get_final_results(simulations_results, partial_state_updates, eps, sessions, remote_threshold)
         elif self.exec_context == ExecutionMode.distributed:
             print("Execution Method: " + self.exec_method.__name__)
diff --git a/cadCAD/engine/execution.py b/cadCAD/engine/execution.py
index ed43788a..41e86e87 100644
--- a/cadCAD/engine/execution.py
+++ b/cadCAD/engine/execution.py
@@ -1,5 +1,4 @@
 from typing import Callable, Dict, List, Any, Tuple
-from pathos.multiprocessing import ThreadPool as TPool
 from pathos.multiprocessing import ProcessPool as PPool
 from collections import Counter
 
@@ -69,7 +68,7 @@ def parallelize_simulations(
     highest_divisor = int(len_configs_structs / sim_count)
 
     new_configs_structs, new_params = [], []
-    for count in range(sim_count):
+    for count in range(len(params)):
         if count == 0:
             new_params.append(
                 params[count: highest_divisor]
@@ -85,26 +84,21 @@ def parallelize_simulations(
                 configs_structs[count * highest_divisor: (count + 1) * highest_divisor]
             )
 
-
-    def threaded_executor(params):
-        tp = TPool()
+    def process_executor(params):
         if len_configs_structs > 1:
-            results = tp.map(
+            pp = PPool(processes=len_configs_structs)
+            results = pp.map(
                 lambda t: t[0](t[1], t[2], t[3], t[4], t[5], t[6], t[7], t[8], t[9], configured_n), params
             )
+            pp.close()
+            pp.join()
+            pp.clear()
         else:
             t = params[0]
             results = t[0](t[1], t[2], t[3], t[4], t[5], t[6], t[7], t[8], t[9], configured_n)
-
-        tp.close()
         return results
 
-    pp = PPool()
-    results = flatten(list(pp.map(lambda params: threaded_executor(params), new_params)))
-    pp.close()
-    pp.join()
-    pp.clear()
-    # pp.restart()
+    results = flatten(list(map(lambda params: process_executor(params), new_params)))
 
     return results
 
@@ -123,24 +117,19 @@ def local_simulations(
         SubsetWindows,
         configured_n
     ):
-    print(f'SimIDs   : {SimIDs}')
-    print(f'SubsetIDs: {SubsetIDs}')
-    print(f'Ns       : {Ns}')
-    print(f'ExpIDs   : {ExpIDs}')
     config_amt = len(configs_structs)
-    try:
-        _params = None
-        if config_amt == 1:
-            _params = var_dict_list[0]
-            return single_proc_exec(
-                simulation_execs, _params, states_lists, configs_structs, env_processes_list,
-                Ts, SimIDs, Ns, ExpIDs, SubsetIDs, SubsetWindows, configured_n
-            )
-        elif config_amt > 1: # and config_amt < remote_threshold:
-            _params = var_dict_list
-            return parallelize_simulations(
-                simulation_execs, _params, states_lists, configs_structs, env_processes_list,
-                Ts, SimIDs, Ns, ExpIDs, SubsetIDs, SubsetWindows, configured_n
-            )
-    except ValueError:
-        raise ValueError("\'sim_configs\' N must > 0")
+
+    _params = None
+    if config_amt == 1: # and configured_n != 1
+        _params = var_dict_list[0]
+        return single_proc_exec(
+            simulation_execs, _params, states_lists, configs_structs, env_processes_list,
+            Ts, SimIDs, Ns, ExpIDs, SubsetIDs, SubsetWindows, configured_n
+        )
+    elif config_amt > 1: # and configured_n != 1
+        _params = var_dict_list
+        return parallelize_simulations(
+            simulation_execs, _params, states_lists, configs_structs, env_processes_list,
+            Ts, SimIDs, Ns, ExpIDs, SubsetIDs, SubsetWindows, configured_n
+        )
+        # elif config_amt > 1 and configured_n == 1:
diff --git a/cadCAD/engine/simulation.py b/cadCAD/engine/simulation.py
index b9a57ba8..1b1475ec 100644
--- a/cadCAD/engine/simulation.py
+++ b/cadCAD/engine/simulation.py
@@ -3,6 +3,7 @@
 from functools import reduce
 from funcy import curry
 
+from cadCAD import remote_dict
 from cadCAD.utils import flatten
 from cadCAD.engine.utils import engine_exception
 
@@ -199,7 +200,6 @@ def run_pipeline(
             pipe_run: List[Dict[str, Any]] = self.state_update_pipeline(
                 sweep_dict, simulation_list, configs, env_processes, time_step, run, additional_objs
             )
-
             _, *pipe_run = pipe_run
             simulation_list.append(pipe_run)
 
@@ -221,12 +221,12 @@ def simulation(
         additional_objs=None
     ):
         run += 1
-
         subset_window.appendleft(subset_id)
-        latest_subset_id, previous_subset_id = tuple(subset_window)
 
-        if configured_N == 1 and latest_subset_id > previous_subset_id:
-            run -= 1
+        if remote_dict['metrics'] is not None:
+            simulation_id = remote_dict['metrics']['sim_id']
+            subset_id = remote_dict['metrics']['subset_id']
+            run = remote_dict['metrics']['run']
 
         def execute_run(sweep_dict, states_list, configs, env_processes, time_seq, _run) -> List[Dict[str, Any]]:
             def generate_init_sys_metrics(genesis_states_list, sim_id, _subset_id, _run, _subset_window):
diff --git a/cadCAD/engine/utils.py b/cadCAD/engine/utils.py
index ad6f3147..456ebf84 100644
--- a/cadCAD/engine/utils.py
+++ b/cadCAD/engine/utils.py
@@ -30,8 +30,3 @@ def engine_exception(ErrorType, error_message, exception_function, try_function)
     except ErrorType:
         print(error_message)
         return exception_function
-
-
-# @curried
-# def fit_param(param, x):
-#     return x + param
diff --git a/cadCAD/utils/execution.py b/cadCAD/utils/execution.py
index a8d4123e..f165f950 100644
--- a/cadCAD/utils/execution.py
+++ b/cadCAD/utils/execution.py
@@ -1,13 +1,16 @@
-from cadCAD import logo
+from cadCAD import logo, version
 
 
 def print_exec_info(exec_context, configs):
     print(logo)
+    print(f'cadCAD Version: {version}')
     print(f'Execution Mode: {exec_context}')
-    print(f'Configuration Count: {len(configs)}')
+    models = len(configs)
     first_sim = configs[0].sim_config
     n_t = len(first_sim['T'])
     n_m = len(first_sim['M'])
     n_n = first_sim['N']
     n_s = len(configs[0].initial_state)
-    print(f'Dimensions of the first simulation: (Timesteps, Params, Runs, Vars) = ({n_t}, {n_m}, {n_n}, {n_s})')
\ No newline at end of file
+    print(
+        f'Dimensions of the first simulation: (Models, Timesteps, Params, Runs, Vars) = ({models}, {n_t}, {n_m}, {n_n}, {n_s})'
+    )
diff --git a/cadCAD/utils/jupyter.py b/cadCAD/utils/jupyter.py
index 45119c6b..9ff41c7c 100644
--- a/cadCAD/utils/jupyter.py
+++ b/cadCAD/utils/jupyter.py
@@ -1,5 +1,5 @@
 def get_home_dir(user):
     return f"s3://jupyterbackups/jupyter/{user}/"
 
-def set_write_path(_spark_context, user, datafolder_path):
-    return get_home_dir(user) + datafolder_path +f'_{_spark_context.applicationId}'
+def set_write_path(sc, user, datafolder_path):
+    return get_home_dir(user) + datafolder_path +f'_{sc.applicationId}'
diff --git a/cadCAD/utils/sys_exec.py b/cadCAD/utils/sys_exec.py
deleted file mode 100644
index 7c7559df..00000000
--- a/cadCAD/utils/sys_exec.py
+++ /dev/null
@@ -1,66 +0,0 @@
-import warnings
-from pprint import pprint
-
-from pyspark import RDD, Row
-from pyspark.sql import DataFrame, SparkSession
-import pandas as pd
-
-# Distributed
-from tabulate import tabulate
-
-
-def align_type(init_condition: dict):
-    def f(d):
-        for y, x in init_condition.items():
-            d[y] = type(x)(d[y])
-        return Row(**d)
-    return f
-
-### Typefull Conversion: to Spark
-# rdd -> spark
-### Typeless Conversion: to Spark
-# (rdd -> pandas) -> spark
-def to_spark(rdd: RDD, init_condition: dict):
-    type_aligner = align_type(init_condition)
-    rdd: RDD = rdd.map(type_aligner)
-    return rdd.toDF()
-
-### Typefull Conversion: to Pandas
-# (rdd -> spark) -> pandas
-### Typeless Conversion: to Pandas
-# rdd -> pandas
-def to_pandas(rdd: RDD):
-    rdd_result: RDD = rdd.collect()
-    pdf_from_rdd: DataFrame = pd.DataFrame(rdd_result)
-    del rdd_result
-    return pdf_from_rdd
-
-
-def to_pandas_df(rdd: RDD, string_conversion=False, init_condition: dict = None):
-    if init_condition is not None and string_conversion is False:
-        # Typefull
-        return to_spark(rdd=rdd, init_condition=init_condition).toPandas()
-    elif init_condition is None and string_conversion is True:
-        # String
-        return rdd.map(lambda d: Row(**dict([(k, str(v)) for k, v in d.items()]))).toDF()
-    else:
-        # Typeless
-        return to_pandas(rdd)
-
-
-def to_spark_df(rdd: RDD, spark: SparkSession = None, init_condition: dict = None):
-    if init_condition is not None and spark is not None:
-        # Typefull
-        return to_spark(rdd, init_condition)
-    elif spark is None and init_condition is None:
-        # String
-        return rdd.map(lambda d: Row(**dict([(k, str(v)) for k, v in d.items()]))).toDF()
-    else:
-        # Typeless
-        spark.conf.set("spark.sql.execution.arrow.enabled", "true")
-        spark.conf.set("spark.sql.execution.arrow.fallback.enabled", "true")
-        warnings.simplefilter(action='ignore', category=UserWarning)
-        pdf_from_rdd: DataFrame = to_pandas(rdd)
-        result = spark.createDataFrame(pdf_from_rdd)
-        del pdf_from_rdd
-        return result
diff --git a/dist/cadCAD-0.4.21-py3-none-any.whl b/dist/cadCAD-0.4.21-py3-none-any.whl
deleted file mode 100644
index b9848c52..00000000
Binary files a/dist/cadCAD-0.4.21-py3-none-any.whl and /dev/null differ
diff --git a/dist/cadCAD-0.4.21.tar.gz b/dist/cadCAD-0.4.21.tar.gz
deleted file mode 100644
index 8fb863fa..00000000
Binary files a/dist/cadCAD-0.4.21.tar.gz and /dev/null differ
diff --git a/dist/cadCAD-0.4.24-py3-none-any.whl b/dist/cadCAD-0.4.24-py3-none-any.whl
new file mode 100644
index 00000000..f69f6c65
Binary files /dev/null and b/dist/cadCAD-0.4.24-py3-none-any.whl differ
diff --git a/dist/cadCAD-0.4.24.tar.gz b/dist/cadCAD-0.4.24.tar.gz
new file mode 100644
index 00000000..3d401d8c
Binary files /dev/null and b/dist/cadCAD-0.4.24.tar.gz differ
diff --git a/documentation/Policy_Aggregation.md b/documentation/Policy_Aggregation.md
index 3b89e394..8eaf1186 100644
--- a/documentation/Policy_Aggregation.md
+++ b/documentation/Policy_Aggregation.md
@@ -49,7 +49,7 @@ def p2_psu3(_params, step, sH, s, **kwargs):
 from cadCAD.configuration import Experiment
 
 exp = Experiment()
-exp.append_configs(
+exp.append_model(
     sim_configs=???,
     initial_state=???,
     partial_state_update_blocks=???,
diff --git a/documentation/README.md b/documentation/README.md
index d94aed94..c649fea9 100644
--- a/documentation/README.md
+++ b/documentation/README.md
@@ -9,33 +9,36 @@ system is described by a set of [State Variables](#State-Variables). The dynamic
 [Policy Functions](#Policy-Functions) and [State Update Functions](#State-Update-Functions), which are evaluated by 
 cadCAD according to the definitions set by the user in [Partial State Update Blocks](#Partial-State-Update-Blocks).
 
-A Simulation Configuration is comprised of a [System Model](#System-Model) and a set of 
-[Simulation Properties](#Simulation-Properties).
+A Simulation Configuration is comprised of a [System Model](#System-Model) and a set of [Simulation Properties](#Simulation-Properties).
 
-`Experiment`'s `append_configs`, stores a **Simulation Configuration** to be [Executed](Simulation_Execution.md) by cadCAD
+### Experiments
+`cadCAD.configuration.Experiment` is a unique representation of an experiment of one or more configured System Models. 
+The `append_model` method of `Experiment` appends a System Model configurations, each representing a single `run`.
 
 ```python
 from cadCAD.configuration import Experiment
 
 exp = Experiment()
-exp.append_configs(
-    user_id = ..., # OPTIONAL: cadCAD Session User ID
+exp.append_model(
+    model_id = ..., # System Model
     initial_state = ..., # System Model
     partial_state_update_blocks = ..., # System Model
     policy_ops = ..., # System Model
-    sim_configs = ... # Simulation Properties
+    sim_configs = ..., # Simulation Properties
+    user_id = ..., # OPTIONAL: Configuration User ID
 )
 ```
-Parameters:
-* **user_id** : str - OPTIONAL: cadCAD Session User ID
+Parameters: `append_model`
+* **model_id** : str - System Model Identification
 * **initial_state** : _dict_ - [State Variables](#State-Variables) and their initial values
 * **partial_state_update_blocks** : List[dict[dict]] - List of [Partial State Update Blocks](#Partial-State-Update-Blocks)
 * **policy_ops** : List[functions] - See [Policy Aggregation](Policy_Aggregation.md) 
 * **sim_configs** - See [System Model Parameter Sweep](System_Model_Parameter_Sweep.md)
+* **user_id** : str - OPTIONAL: Configuration User ID
 
 ## Simulation Properties
 
-Simulation properties are passed to `append_configs` in the `sim_configs` parameter. To construct this parameter, we 
+Simulation properties are passed to `append_model` in the `sim_configs` parameter. To construct this parameter, we 
 use the `config_sim` function in `cadCAD.configuration.utils`
 
 ```python
@@ -51,7 +54,7 @@ sim_config_dict = {
 c = config_sim(sim_config_dict)
 
 exp = Experiment()
-exp.append_configs(
+exp.append_model(
     ...
     sim_configs = c # Simulation Properties
 )
@@ -98,7 +101,7 @@ absence of any external forces affecting the system. ([source: Wikipedia](https:
 cadCAD can handle state variables of any Python data type, including custom classes. It is up to the user of cadCAD to 
 determine the state variables needed to **sufficiently and accurately** describe the system they are interested in.
 
-State Variables are passed to `append_configs` along with its initial values, as a Python `dict` where the `dict_keys` 
+State Variables are passed to `append_model` along with its initial values, as a Python `dict` where the `dict_keys` 
 are the names of the variables and the `dict_values` are their initial values.
 
 ```python
@@ -112,7 +115,7 @@ genesis_states = {
 }
 
 exp = Experiment()
-exp.append_configs(
+exp.append_model(
     initial_state = genesis_states,
     ...
 )
@@ -200,7 +203,7 @@ impact the State Update Functions and Policy Functions in that PSUB - only those
 
 ![](https://i.imgur.com/9rlX9TG.png)
 
-Partial State Update Blocks are passed to `append_configs` as a List of Python `dicts` where the `dict_keys` are named 
+Partial State Update Blocks are passed to `append_model` as a List of Python `dicts` where the `dict_keys` are named 
 `"policies"` and `"variables"` and the values are also Python `dicts` where the keys are the names of the policy and 
 state update functions and the values are the functions.
 
@@ -226,7 +229,7 @@ PSUBs = [
 ]
 
 exp = Experiment()
-exp.append_configs(
+exp.append_model(
     ...
     partial_state_update_blocks = PSUBs,
     ...
diff --git a/documentation/Simulation_Execution.md b/documentation/Simulation_Execution.md
index b4a92d95..ddfb6115 100644
--- a/documentation/Simulation_Execution.md
+++ b/documentation/Simulation_Execution.md
@@ -8,11 +8,11 @@ Configurations. There are multiple simulation Execution Modes and Execution Cont
 `cadCAD` executes a process per System Model Configuration and a thread per System Simulation.
 #### Class: `cadCAD.engine.ExecutionMode`
 #### Attributes:
-* **Local Mode (Default):** Automatically selects Single Threaded or Multi-Process/Threaded Modes (Example: 
+* **Local Mode (Default):** Automatically selects Single Threaded or Multi-Process Modes (Example: 
 `cadCAD.engine.ExecutionMode().local_mode`).
 * **Single Threaded Mode:** A single threaded Execution Mode for a single System Model Configuration (Example: 
 `cadCAD.engine.ExecutionMode().single_mode`).
-* **Multi-Process/Threaded Mode:** Execution Mode for System Model Simulations which executes Multiple threads within 
+* **Multi-Process Mode:** Execution Mode for System Model Simulations which executes Multiple processes within 
 multiple processes per given System Model Configuration (Example: `cadCAD.engine.ExecutionMode().multi_mode`).
 
 ### 2. *Create Execution Context using Execution Mode*
@@ -25,8 +25,8 @@ local_mode_ctx = ExecutionContext(context=exec_mode.local_mode)
 ### 3. *Create Simulation Executor*
 ```python
 from cadCAD.engine import Executor
-from cadCAD import configs
-simulation = Executor(exec_context=local_mode_ctx, configs=configs)
+from ... import exp # import of an instantiated of an `cadCAD.configuration.Experiment` object 
+simulation = Executor(exec_context=local_mode_ctx, configs=exp.configs)
 ```
 
 ### 4. *Execute Simulation & Produce System Event Dataset*
@@ -57,18 +57,18 @@ simulation_result = pd.DataFrame(raw_system_events)
 
 #### Example Result: System Events DataFrame
 ```
-+----+----------+-------+------------+-----------+------+-----------+
-|    |   subset |   run |   timestep |   substep |   s1 | s2        |
-|----+----------+-------+------------+-----------+------+-----------|
-|  0 |        0 |     1 |          0 |         0 |    0 | 0.0       |
-|  1 |        0 |     1 |          1 |         1 |    1 | 4         |
-|  2 |        0 |     1 |          1 |         2 |    2 | 6         |
-|  3 |        0 |     1 |          1 |         3 |    3 | [ 30 300] |
-|  4 |        0 |     2 |          0 |         0 |    0 | 0.0       |
-|  5 |        0 |     2 |          1 |         1 |    1 | 4         |
-|  6 |        0 |     2 |          1 |         2 |    2 | 6         |
-|  7 |        0 |     2 |          1 |         3 |    3 | [ 30 300] |
-+----+----------+-------+------------+-----------+------+-----------+
++----+------+-----------+------------+--------+-----+---------+----------+
+|    |   s1 | s2        | simulation | subset | run | substep | timestep |
+|----+------+-----------|------------+--------+-----+---------+----------|
+|  0 |    0 | 0.0       |          0 |      0 |   1 |       0 |        0 |
+|  1 |    1 | 4         |          0 |      0 |   1 |       1 |        1 |
+|  2 |    2 | 6         |          0 |      0 |   1 |       2 |        1 |
+|  3 |    3 | [ 30 300] |          0 |      0 |   1 |       3 |        1 |
+|  4 |    0 | 0.0       |          1 |      0 |   1 |       0 |        0 |
+|  5 |    1 | 4         |          1 |      0 |   1 |       1 |        1 |
+|  6 |    2 | 6         |          1 |      0 |   1 |       2 |        1 |
+|  7 |    3 | [ 30 300] |          1 |      0 |   1 |       3 |        1 |
++----+------+-----------+------------+--------+-----+---------+----------+
 ```
 
 ## Execution Examples:
@@ -86,14 +86,14 @@ import pandas as pd
 from tabulate import tabulate
 from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
 from documentation.examples import sys_model_A
-from cadCAD import configs
+from documentation.examples.sys_model_A import exp
 
 exec_mode = ExecutionMode()
 
 # Single Process Execution using a Single System Model Configuration:
 # sys_model_A
 local_mode_ctx = ExecutionContext(context=exec_mode.local_mode)
-sys_model_A_simulation = Executor(exec_context=local_mode_ctx, configs=configs)
+sys_model_A_simulation = Executor(exec_context=local_mode_ctx, configs=exp.configs)
 
 sys_model_A_raw_result, sys_model_A_tensor_field, sessions = sys_model_A_simulation.execute()
 sys_model_A_result = pd.DataFrame(sys_model_A_raw_result)
@@ -119,15 +119,14 @@ import pandas as pd
 from tabulate import tabulate
 
 from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-from documentation.examples import sys_model_A, sys_model_B
-from cadCAD import configs
+from documentation.examples import sys_model_A, sys_model_B, system_model_AB_exp
 
 exec_mode = ExecutionMode()
 
 # # Multiple Processes Execution using Multiple System Model Configurations:
 # # sys_model_A & sys_model_B
 local_mode_ctx = ExecutionContext(context=exec_mode.local_mode)
-sys_model_AB_simulation = Executor(exec_context=local_mode_ctx, configs=configs)
+sys_model_AB_simulation = Executor(exec_context=local_mode_ctx, configs=system_model_AB_exp.configs)
 
 sys_model_AB_raw_result, sys_model_AB_tensor_field, sessions = sys_model_AB_simulation.execute()
 sys_model_AB_result = pd.DataFrame(sys_model_AB_raw_result)
@@ -148,11 +147,11 @@ from tabulate import tabulate
 
 from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
 from documentation.examples import param_sweep
-from cadCAD import configs
+from documentation.examples.param_sweep import exp
 
 exec_mode = ExecutionMode()
 local_mode_ctx = ExecutionContext(context=exec_mode.local_mode)
-run = Executor(exec_context=local_mode_ctx, configs=configs)
+run = Executor(exec_context=local_mode_ctx, configs=exp.configs)
 
 raw_result, tensor_field, sessions = run.execute()
 result = pd.DataFrame(raw_result)
@@ -162,4 +161,4 @@ print(tabulate(tensor_field, headers='keys', tablefmt='psql'))
 print("Output:")
 print(tabulate(result, headers='keys', tablefmt='psql'))
 print()
-```
\ No newline at end of file
+```
diff --git a/documentation/System_Configuration.md b/documentation/System_Configuration.md
index 3bb2bbab..b495ce4a 100644
--- a/documentation/System_Configuration.md
+++ b/documentation/System_Configuration.md
@@ -1,5 +1,18 @@
-# [Display System Model Configurations:](https://github.com/BlockScience/distroduce/blob/master/documentation/dist_exec_doc.ipynb) 
+# Display System Model Configurations:
 
+## Announcement:
+
+See [CHANGELOG](CHANGELOG.md)
+
+The `cadCAD.configuration.Experiment().configs` (System Model Configurations) `list` has been flattened 
+to contain single run `Configuration` objects. This functionality will be restored in a 
+subsequent release by a class that returns the original representation in ver. `0.3.1`.
+* The conversion utilities have been provided to restore its original representation of configurations with 
+runs >= 1
+    * System Configuration Conversions:
+        * Configuration as List of Configuration Objects (as in ver. `0.3.1`) 
+        * New: System Configuration as a Pandas DataFrame
+        * New: System Configuration as List of Dictionaries
 
 ## Conversions
 ##### Note: The following applies as a result of simulation execution
@@ -10,26 +23,53 @@ from cadCAD.configuration.utils import configs_as_objs, configs_as_dataframe, co
 ```
 
 #### System Configurations as List of Configuration Objects
+Example:
+* `configs` is temporarily returned in a flattened format and reformatted into its intended format. 
+* `Configuration` objects at `0x10790e470` and `0x1143dd630` are reconstituted into objects at `0x10790e7b8` 
+and `0x116268908` respectively.
 ```python
-fmt_configs = configs_as_objs(configs)
-pprint(fmt_configs)
+from ... import exp # import of an instantiated `cadCAD.configuration.Experiment` object 
+
+flattened_configs = exp.configs
+         
+print('Flattened Format: Temporary')  
+pprint(flattened_configs)
+print()
+
+print('Intended Format:')
+intended_configs = configs_as_objs(flattened_configs)
+pprint(intended_configs)
+print()
+
+print("Object: cadCAD.configuration.Configuration(...).sim_config")
+pprint(intended_configs[0].sim_config)
 print()
-pprint(fmt_configs[0].sim_config)
 ```
 Return:
 ```
-[<cadCAD.configuration.Configuration object at 0x116a21b90>,
- <cadCAD.configuration.Configuration object at 0x116a21c90>]
+Flattened Format: Temporary
+[<cadCAD.configuration.Configuration object at 0x10790e470>,
+ <cadCAD.configuration.Configuration object at 0x10790e7b8>,
+ <cadCAD.configuration.Configuration object at 0x1143dd630>,
+ <cadCAD.configuration.Configuration object at 0x116268908>]
 
-{'M': {'alpha': 1, 'beta': 2, 'gamma': 3, 'omega': 7},
+Intended Format:
+[<cadCAD.configuration.Configuration object at 0x10790e7b8>,
+ <cadCAD.configuration.Configuration object at 0x116268908>]
+
+Object: cadCAD.configuration.Configuration(...).sim_config
+{'M': [{}],
  'N': 2,
- 'T': range(0, 10),
+ 'T': range(0, 1),
  'run_id': 1,
- 'simulation_id': 0}
+ 'simulation_id': 0,
+ 'subset_id': 0,
+ 'subset_window': deque([0, None], maxlen=2)}
 ```
 
 #### System Configurations as a Pandas DataFrame
 ```python
+flattened_configs = configs
 configs_df = configs_as_dataframe(configs)
 configs_df
 ```
diff --git a/documentation/System_Model_Parameter_Sweep.md b/documentation/System_Model_Parameter_Sweep.md
index 18002c71..fcc48bdc 100644
--- a/documentation/System_Model_Parameter_Sweep.md
+++ b/documentation/System_Model_Parameter_Sweep.md
@@ -3,6 +3,7 @@ System Model Parameter Sweep
 Parametrization of a System Model configuration that produces multiple configurations.
 
 ##### Set Parameters
+*Note:* `params` values require up to a maximum of 2 distinct lengths
 ```python
 params = {
     'alpha': [1],
diff --git a/documentation/cadCAD-v0.4.23-Model-Upgrade-Guide.md b/documentation/cadCAD-v0.4.23-Model-Upgrade-Guide.md
new file mode 100755
index 00000000..9962becf
--- /dev/null
+++ b/documentation/cadCAD-v0.4.23-Model-Upgrade-Guide.md
@@ -0,0 +1,137 @@
+<table>
+    <tr>
+        <th>
+            Feature
+        </th>
+        <th>
+            ver. 0.4.23
+        </th>
+      <th>
+            ver. 0.3.1 (Deprecated)
+        </th>
+    </tr>
+    <tr>
+        <td>
+           <h5>
+                Experiments
+            </h5>
+        </td>
+        <td>
+<pre lang="python">
+from cadCAD.configuration import Experiment
+exp = Experiment()
+exp.append_configs(...) 
+</pre>
+        </td>
+        <td>
+<pre lang="python">
+from cadCAD.configuration import append_configs
+append_configs(…)         
+</pre>
+        </td>
+</tr>
+<tr>
+        <td>
+           <h5>
+                Local Execution Mode
+            </h5>
+        </td>
+        <td>
+<pre lang="python">          
+from cadCAD.engine import ExecutionMode, ExecutionContext
+exec_mode = ExecutionMode()
+local_ctx = ExecutionContext(context=exec_mode.local_mode)         
+</pre>
+        </td>
+        <td>
+         <p>Multi-Threaded</p>
+         <pre lang="python">
+from cadCAD.engine import ExecutionMode, ExecutionContext
+exec_mode = ExecutionMode()
+single_ctx = ExecutionContext(context=exec_mode.multi_proc)
+</pre>
+<p>Single-Threaded</p>
+<pre lang="python">
+from cadCAD.engine import ExecutionMode, ExecutionContext
+exec_mode = ExecutionMode()
+multi_ctx = ExecutionContext(context=exec_mode.single_proc)
+            </pre>
+        </td>
+   </tr>
+ <tr>
+        <td>
+           <h5>
+                cadCAD Post-Processing Enhancements / Modifications
+           </h5>
+        </td>
+        <td>
+            <pre lang="python">
+import pandas as pd
+from tabulate import tabulate
+from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
+import system_model_A, system_model_B
+
+from cadCAD import configs
+exec_mode = ExecutionMode()
+
+local_ctx = ExecutionContext(context=exec_mode.local_mode)
+simulation = Executor(exec_context=local_ctx, configs=configs)
+raw_result, sys_model, _ = simulation.execute()
+result = pd.DataFrame(raw_result)
+print(tabulate(result, headers='keys', tablefmt='psql'))
+</pre>
+        </td>
+        <td>
+         <pre lang="python">
+        
+import pandas as pd
+from tabulate import tabulate
+from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
+import system_model_A, system_model_B
+from cadCAD import configs
+
+exec_mode = ExecutionMode()
+multi_ctx = ExecutionContext(context=exec_mode.multi_proc)
+simulation = Executor(exec_context=multi_ctx, configs=configs)
+i = 0
+config_names = ['sys_model_A', 'sys_model_B']
+for raw_result, _ in simulation.execute():
+result = pd.DataFrame(raw_result)
+print()
+print(f"{config_names[i]} Result: System Events DataFrame:")
+print(tabulate(result, headers='keys', tablefmt='psql'))
+print()
+i += 1
+           
+   </pre>
+        </td>
+   </tr>
+
+ <tr>
+        <td>
+           <h5>
+             Expandable state and policy update parameter
+            </h5>
+        </td>
+        <td>
+            <pre lang="python">
+def state_update(_params, substep, sH, s, _input, **kwargs):
+…
+return 'state_variable_name', new_value
+def policy(_params, substep, sH, s, **kwargs):
+…
+return {'signal_1': value_1, …, 'signal_N': value_N}
+   </pre>
+        </td>
+        <td>
+         <pre lang="python">     
+def state_update(_params, substep, sH, s, _input):
+…
+return 'state_variable_name', new_value
+def policy(_params, substep, sH, s):
+…
+return {'signal_1': value_1, …, 'signal_N': value_N}  
+   </pre>
+        </td>
+   </tr>
+</table>
diff --git a/documentation/examples/__init__.py b/documentation/examples/__init__.py
index e69de29b..50a1788e 100644
--- a/documentation/examples/__init__.py
+++ b/documentation/examples/__init__.py
@@ -0,0 +1,3 @@
+from cadCAD.configuration import Experiment
+
+system_model_AB_exp = Experiment()
diff --git a/documentation/examples/example_1.py b/documentation/examples/example_1.py
index ffa21b7f..e4028666 100644
--- a/documentation/examples/example_1.py
+++ b/documentation/examples/example_1.py
@@ -4,16 +4,15 @@
 from tabulate import tabulate
 
 from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-from documentation.examples import sys_model_A, sys_model_B
-from cadCAD import configs
+from documentation.examples import sys_model_A, sys_model_B, system_model_AB_exp
+from documentation.examples.sys_model_A import exp
 
 exec_mode = ExecutionMode()
 
 # Single Process Execution using a Single System Model Configuration:
 # sys_model_A
-sys_model_A = [configs[0]]
 local_proc_ctx = ExecutionContext(context=exec_mode.local_mode)
-sys_model_A_simulation = Executor(exec_context=local_proc_ctx, configs=sys_model_A)
+sys_model_A_simulation = Executor(exec_context=local_proc_ctx, configs=exp.configs)
 
 sys_model_A_raw_result, sys_model_A_tensor_field, sessions = sys_model_A_simulation.execute()
 sys_model_A_result = pd.DataFrame(sys_model_A_raw_result)
@@ -27,15 +26,14 @@
 # # Multiple Processes Execution using Multiple System Model Configurations:
 # # sys_model_A & sys_model_B
 multi_proc_ctx = ExecutionContext(context=exec_mode.multi_proc)
-sys_model_AB_simulation = Executor(exec_context=multi_proc_ctx, configs=configs)
+sys_model_AB_simulation = Executor(exec_context=multi_proc_ctx, configs=system_model_AB_exp.configs)
 
 sys_model_AB_raw_result, sys_model_AB_tensor_field, sessions = sys_model_AB_simulation.execute()
 print()
-pprint(sys_model_AB_raw_result)
-# sys_model_AB_result = pd.DataFrame(sys_model_AB_raw_result)
+sys_model_AB_result = pd.DataFrame(sys_model_AB_raw_result)
 print()
 print(f"Tensor Field:")
 print(tabulate(sys_model_AB_tensor_field, headers='keys', tablefmt='psql'))
-# print("Result: System Events DataFrame:")
-# print(tabulate(sys_model_AB_result, headers='keys', tablefmt='psql'))
-# print()
+print("Result: System Events DataFrame:")
+print(tabulate(sys_model_AB_result, headers='keys', tablefmt='psql'))
+print()
diff --git a/documentation/examples/historical_state_access.py b/documentation/examples/historical_state_access.py
index 5ca9015b..585d462c 100644
--- a/documentation/examples/historical_state_access.py
+++ b/documentation/examples/historical_state_access.py
@@ -4,7 +4,6 @@
 from cadCAD.configuration.utils import config_sim, access_block
 from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
 from cadCAD.configuration import Experiment
-from cadCAD import configs
 
 policies, variables = {}, {}
 exclusion_list = ['nonexsistant', 'last_x', '2nd_to_last_x', '3rd_to_last_x', '4th_to_last_x']
@@ -88,7 +87,8 @@ def fourth_to_last_x(_g, substep, sH, s, _input):
 )
 
 exp = Experiment()
-exp.append_configs(
+exp.append_model(
+    model_id='hist_state_access',
     sim_configs=sim_config,
     initial_state=genesis_states,
     partial_state_update_blocks=psubs
@@ -96,7 +96,7 @@ def fourth_to_last_x(_g, substep, sH, s, _input):
 
 exec_mode = ExecutionMode()
 local_proc_ctx = ExecutionContext(context=exec_mode.local_mode)
-run = Executor(exec_context=local_proc_ctx, configs=configs)
+run = Executor(exec_context=local_proc_ctx, configs=exp.configs)
 
 raw_result, tensor_field, sessions = run.execute()
 result = pd.DataFrame(raw_result)
@@ -108,4 +108,4 @@ def fourth_to_last_x(_g, substep, sH, s, _input):
 print(tabulate(tensor_field, headers='keys', tablefmt='psql'))
 print("Output:")
 print(tabulate(result, headers='keys', tablefmt='psql'))
-print()
\ No newline at end of file
+print()
diff --git a/documentation/examples/param_sweep.py b/documentation/examples/param_sweep.py
index 9d10830e..61e53a03 100644
--- a/documentation/examples/param_sweep.py
+++ b/documentation/examples/param_sweep.py
@@ -7,7 +7,6 @@
 from cadCAD.configuration.utils import env_trigger, var_substep_trigger, config_sim, psub_list
 from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
 from cadCAD.configuration import Experiment
-from cadCAD import configs
 
 pp = pprint.PrettyPrinter(indent=4)
 
@@ -93,7 +92,8 @@ def sweeped(_params, step, sH, s, _input):
 print()
 
 exp = Experiment()
-exp.append_configs(
+exp.append_model(
+    model_id='param_sweep',
     sim_configs=sim_config,
     initial_state=genesis_states,
     env_processes=env_process,
@@ -102,7 +102,7 @@ def sweeped(_params, step, sH, s, _input):
 
 exec_mode = ExecutionMode()
 local_proc_ctx = ExecutionContext(context=exec_mode.local_mode)
-run = Executor(exec_context=local_proc_ctx, configs=configs)
+run = Executor(exec_context=local_proc_ctx, configs=exp.configs)
 
 raw_result, tensor_field, sessions = run.execute()
 result = pd.DataFrame(raw_result)
@@ -111,4 +111,4 @@ def sweeped(_params, step, sH, s, _input):
 print(tabulate(tensor_field, headers='keys', tablefmt='psql'))
 print("Output:")
 print(tabulate(result, headers='keys', tablefmt='psql'))
-print()
\ No newline at end of file
+print()
diff --git a/documentation/examples/policy_aggregation.py b/documentation/examples/policy_aggregation.py
index 2a0edb95..928872c8 100644
--- a/documentation/examples/policy_aggregation.py
+++ b/documentation/examples/policy_aggregation.py
@@ -4,7 +4,6 @@
 from cadCAD.configuration.utils import config_sim
 from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
 from cadCAD.configuration import Experiment
-from cadCAD import configs
 
 # Policies per Mechanism
 def p1m1(_g, step, sH, s):
@@ -77,7 +76,8 @@ def policies(_g, step, sH, s, _input):
 )
 
 exp = Experiment()
-exp.append_configs(
+exp.append_model(
+    model_id='policy_agg',
     sim_configs=sim_config,
     initial_state=genesis_states,
     partial_state_update_blocks=psubs,
@@ -86,7 +86,7 @@ def policies(_g, step, sH, s, _input):
 
 exec_mode = ExecutionMode()
 local_proc_ctx = ExecutionContext(context=exec_mode.local_mode)
-run = Executor(exec_context=local_proc_ctx, configs=configs)
+run = Executor(exec_context=local_proc_ctx, configs=exp.configs)
 
 raw_result, tensor_field, sessions = run.execute()
 result = pd.DataFrame(raw_result)
diff --git a/documentation/examples/sys_model_A.py b/documentation/examples/sys_model_A.py
index 13ade45a..98d2c5f4 100644
--- a/documentation/examples/sys_model_A.py
+++ b/documentation/examples/sys_model_A.py
@@ -3,6 +3,7 @@
 
 from cadCAD.configuration.utils import bound_norm_random, config_sim, time_step, env_trigger
 from cadCAD.configuration import Experiment
+from documentation.examples import system_model_AB_exp
 
 seeds = {
     'z': np.random.RandomState(1),
@@ -149,10 +150,20 @@ def update_timestamp(_g, step, sH, s, _input):
 )
 
 exp = Experiment()
-exp.append_configs(
+exp.append_model(
+    model_id='sys_model_A',
     sim_configs=sim_config,
     initial_state=genesis_states,
     env_processes=env_processes,
     partial_state_update_blocks=psubs,
     policy_ops=[lambda a, b: a + b]
-)
\ No newline at end of file
+)
+
+system_model_AB_exp.append_model(
+    model_id='sys_model_A',
+    sim_configs=sim_config,
+    initial_state=genesis_states,
+    env_processes=env_processes,
+    partial_state_update_blocks=psubs,
+    policy_ops=[lambda a, b: a + b]
+)
diff --git a/documentation/examples/sys_model_AB_exec.py b/documentation/examples/sys_model_AB_exec.py
index ebfb4cd4..10196bd4 100644
--- a/documentation/examples/sys_model_AB_exec.py
+++ b/documentation/examples/sys_model_AB_exec.py
@@ -1,14 +1,13 @@
 import pandas as pd
 from tabulate import tabulate
 from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-from documentation.examples import sys_model_A, sys_model_B
-from cadCAD import configs
+from documentation.examples import sys_model_A, sys_model_B, system_model_AB_exp
 
 exec_mode = ExecutionMode()
 
 # Multiple Processes Execution using Multiple System Model Configurations:
 local_proc_ctx = ExecutionContext(context=exec_mode.local_mode)
-sys_model_AB_simulation = Executor(exec_context=local_proc_ctx, configs=configs)
+sys_model_AB_simulation = Executor(exec_context=local_proc_ctx, configs=system_model_AB_exp.configs)
 
 i = 0
 config_names = ['sys_model_A', 'sys_model_B']
diff --git a/documentation/examples/sys_model_A_exec.py b/documentation/examples/sys_model_A_exec.py
index a1428b13..1cc04c03 100644
--- a/documentation/examples/sys_model_A_exec.py
+++ b/documentation/examples/sys_model_A_exec.py
@@ -2,13 +2,13 @@
 from tabulate import tabulate
 from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
 from documentation.examples import sys_model_A
-from cadCAD import configs
+from documentation.examples.sys_model_A import exp
 
 exec_mode = ExecutionMode()
 
 # Single Process Execution using a Single System Model Configuration:
 local_proc_ctx = ExecutionContext(context=exec_mode.local_mode)
-sys_model_A_simulation = Executor(exec_context=local_proc_ctx, configs=configs)
+sys_model_A_simulation = Executor(exec_context=local_proc_ctx, configs=exp.configs)
 
 sys_model_A_raw_result, sys_model_A_tensor_field, sessions = sys_model_A_simulation.execute()
 sys_model_A_result = pd.DataFrame(sys_model_A_raw_result)
@@ -17,4 +17,4 @@
 print(tabulate(sys_model_A_tensor_field, headers='keys', tablefmt='psql'))
 print("Result: System Events DataFrame")
 print(tabulate(sys_model_A_result, headers='keys', tablefmt='psql'))
-print()
\ No newline at end of file
+print()
diff --git a/documentation/examples/sys_model_B.py b/documentation/examples/sys_model_B.py
index b9747d58..17a27c3b 100644
--- a/documentation/examples/sys_model_B.py
+++ b/documentation/examples/sys_model_B.py
@@ -3,6 +3,7 @@
 
 from cadCAD.configuration.utils import bound_norm_random, config_sim, env_trigger, time_step
 from cadCAD.configuration import Experiment
+from documentation.examples import system_model_AB_exp
 
 seeds = {
     'z': np.random.RandomState(1),
@@ -138,9 +139,19 @@ def update_timestamp(_g, step, sH, s, _input):
 )
 
 exp = Experiment()
-exp.append_configs(
+exp.append_model(
+    model_id='sys_model_B',
     sim_configs=sim_config,
     initial_state=genesis_states,
     env_processes=env_processes,
     partial_state_update_blocks=psubs
-)
\ No newline at end of file
+)
+
+system_model_AB_exp.append_model(
+    model_id='sys_model_B',
+    sim_configs=sim_config,
+    initial_state=genesis_states,
+    env_processes=env_processes,
+    partial_state_update_blocks=psubs,
+    policy_ops=[lambda a, b: a + b]
+)
diff --git a/documentation/examples/sys_model_B_exec.py b/documentation/examples/sys_model_B_exec.py
index 8019dbb5..ea2211a8 100644
--- a/documentation/examples/sys_model_B_exec.py
+++ b/documentation/examples/sys_model_B_exec.py
@@ -3,14 +3,14 @@
 # The following imports NEED to be in the exact order
 from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
 from documentation.examples import sys_model_B
-from cadCAD import configs
+from documentation.examples.sys_model_B import exp
 
 exec_mode = ExecutionMode()
 
 print("Simulation Execution: Single Configuration")
 print()
 local_proc_ctx = ExecutionContext(context=exec_mode.local_mode)
-run = Executor(exec_context=local_proc_ctx, configs=configs)
+run = Executor(exec_context=local_proc_ctx, configs=exp.configs)
 
 raw_result, tensor_field, sessions = run.execute()
 result = pd.DataFrame(raw_result)
diff --git a/expected_results/param_sweep_4.pkl b/expected_results/param_sweep_4.pkl
new file mode 100644
index 00000000..69be4241
Binary files /dev/null and b/expected_results/param_sweep_4.pkl differ
diff --git a/expected_results/param_sweep_5.pkl b/expected_results/param_sweep_5.pkl
new file mode 100644
index 00000000..5a9ebcff
Binary files /dev/null and b/expected_results/param_sweep_5.pkl differ
diff --git a/expected_results/param_sweep_psub0_4.pkl b/expected_results/param_sweep_psub0_4.pkl
new file mode 100644
index 00000000..e7af2c01
Binary files /dev/null and b/expected_results/param_sweep_psub0_4.pkl differ
diff --git a/expected_results/param_sweep_timestep1_4.pkl b/expected_results/param_sweep_timestep1_4.pkl
new file mode 100644
index 00000000..190a3137
Binary files /dev/null and b/expected_results/param_sweep_timestep1_4.pkl differ
diff --git a/expected_results/policy_agg_4.pkl b/expected_results/policy_agg_4.pkl
new file mode 100644
index 00000000..42ae6bdf
Binary files /dev/null and b/expected_results/policy_agg_4.pkl differ
diff --git a/expected_results/policy_agg_5.pkl b/expected_results/policy_agg_5.pkl
new file mode 100644
index 00000000..142296ff
Binary files /dev/null and b/expected_results/policy_agg_5.pkl differ
diff --git a/requirements.txt b/requirements.txt
index e35949e3..ec3ce453 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,5 +1,21 @@
-wheel
-tabulate
-pytest
-parameterized
-tabulate
+-i https://pypi.org/simple
+
+wheel>=0.35
+fn==0.4.3
+funcy==1.14
+matplotlib==3.3.2
+networkx==2.5
+numba==0.51.2
+numpy==1.19.2
+pandas==1.1.2
+parameterized==0.7.4
+pathos==0.2.6
+plotly==4.10.0
+pyarrow==1.0.1
+pytest==6.0.2
+scikit-learn==0.23.2
+scipy==1.5.2
+seaborn==0.11.0
+tabulate==0.8.7
+xarray==0.16.0
+wheel==0.36.2
\ No newline at end of file
diff --git a/setup.py b/setup.py
index 06381f94..fd6583a4 100644
--- a/setup.py
+++ b/setup.py
@@ -20,8 +20,11 @@
 provided.
 """
 
-setup(name='cadCAD',
-      version='0.4.21',
+name = "cadCAD"
+version = "0.4.24"
+
+setup(name=name,
+      version=version,
       description=short_description,
       long_description=long_description,
       url='https://github.com/cadCAD-org/cadCAD',
@@ -35,5 +38,5 @@
             "fn",
             "funcy"
       ],
-      python_requires='>=3.6'
+      python_requires='>=3.6.13'
 )
diff --git a/simulations/validation/config4.py b/simulations/validation/config4.py
deleted file mode 100644
index f59839ac..00000000
--- a/simulations/validation/config4.py
+++ /dev/null
@@ -1,139 +0,0 @@
-from decimal import Decimal
-import numpy as np
-from datetime import timedelta
-
-from cadCAD.configuration import append_configs
-from cadCAD.configuration.utils import env_proc_trigger, bound_norm_random, ep_time_step, config_sim
-
-
-seeds = {
-    'z': np.random.RandomState(1),
-    'a': np.random.RandomState(2),
-    'b': np.random.RandomState(3),
-    'c': np.random.RandomState(3)
-}
-
-
-# Policies per Mechanism
-def p1m1(_g, step, sL, s):
-    return {'param1': 1}
-def p2m1(_g, step, sL, s):
-    return {'param2': 4}
-
-def p1m2(_g, step, sL, s):
-    return {'param1': 'a', 'param2': 2}
-def p2m2(_g, step, sL, s):
-    return {'param1': 'b', 'param2': 4}
-
-def p1m3(_g, step, sL, s):
-    return {'param1': ['c'], 'param2': np.array([10, 100])}
-def p2m3(_g, step, sL, s):
-    return {'param1': ['d'], 'param2': np.array([20, 200])}
-
-
-# Internal States per Mechanism
-def s1m1(_g, step, sL, s, _input):
-    y = 's1'
-    x = _input['param1']
-    return (y, x)
-def s2m1(_g, step, sL, s, _input):
-    y = 's2'
-    x = _input['param2']
-    return (y, x)
-
-def s1m2(_g, step, sL, s, _input):
-    y = 's1'
-    x = _input['param1']
-    return (y, x)
-def s2m2(_g, step, sL, s, _input):
-    y = 's2'
-    x = _input['param2']
-    return (y, x)
-
-def s1m3(_g, step, sL, s, _input):
-    y = 's1'
-    x = _input['param1']
-    return (y, x)
-def s2m3(_g, step, sL, s, _input):
-    y = 's2'
-    x = _input['param2']
-    return (y, x)
-
-def s1m4(_g, step, sL, s, _input):
-    y = 's1'
-    x = [1]
-    return (y, x)
-
-
-# Exogenous States
-proc_one_coef_A = 0.7
-proc_one_coef_B = 1.3
-
-def es3p1(_g, step, sL, s, _input):
-    y = 's3'
-    x = s['s3'] * bound_norm_random(seeds['a'], proc_one_coef_A, proc_one_coef_B)
-    return (y, x)
-
-def es4p2(_g, step, sL, s, _input):
-    y = 's4'
-    x = s['s4'] * bound_norm_random(seeds['b'], proc_one_coef_A, proc_one_coef_B)
-    return (y, x)
-
-ts_format = '%Y-%m-%d %H:%M:%S'
-t_delta = timedelta(days=0, minutes=0, seconds=1)
-def es5p2(_g, step, sL, s, _input):
-    y = 'timestamp'
-    x = ep_time_step(s, dt_str=s['timestamp'], fromat_str=ts_format, _timedelta=t_delta)
-    return (y, x)
-
-
-# Environment States
-def env_a(x):
-    return 5
-def env_b(x):
-    return 10
-
-
-# Genesis States
-genesis_states = {
-    's1': Decimal(0.0),
-    's2': Decimal(0.0),
-    's3': Decimal(1.0),
-    's4': Decimal(1.0),
-    'timestamp': '2018-10-01 15:16:24'
-}
-
-
-raw_exogenous_states = {
-    "s3": es3p1,
-    "s4": es4p2,
-    "timestamp": es5p2
-}
-
-
-env_processes = {
-    "s3": env_a,
-    "s4": env_proc_trigger('2018-10-01 15:16:25', env_b)
-}
-
-
-partial_state_update_block = [
-]
-
-
-sim_config = config_sim(
-    {
-        "N": 2,
-        "T": range(5),
-    }
-)
-
-
-append_configs(
-    sim_configs=sim_config,
-    initial_state=genesis_states,
-    seeds={},
-    raw_exogenous_states={},
-    env_processes={},
-    partial_state_update_blocks=partial_state_update_block
-)
\ No newline at end of file
diff --git a/simulations/validation/conviction_helpers.py b/simulations/validation/conviction_helpers.py
deleted file mode 100644
index 909d4372..00000000
--- a/simulations/validation/conviction_helpers.py
+++ /dev/null
@@ -1,150 +0,0 @@
-import networkx as nx
-from scipy.stats import expon, gamma
-import numpy as np
-import matplotlib.pyplot as plt
-
-#helper functions
-def get_nodes_by_type(g, node_type_selection):
-    return [node for node in g.nodes if g.nodes[node]['type']== node_type_selection ]
-
-def get_edges_by_type(g, edge_type_selection):
-    return [edge for edge in g.edges if g.edges[edge]['type']== edge_type_selection ]
-
-def total_funds_given_total_supply(total_supply):
-    
-    #can put any bonding curve invariant here for initializatio!
-    total_funds = total_supply
-    
-    return total_funds
-
-#maximum share of funds a proposal can take
-default_beta = .2 #later we should set this to be param so we can sweep it
-# tuning param for the trigger function
-default_rho = .001
-
-def trigger_threshold(requested, funds, supply, beta = default_beta, rho = default_rho):
-    
-    share = requested/funds
-    if share < beta:
-        return rho*supply/(beta-share)**2
-    else: 
-        return np.inf
-
-def initialize_network(n,m, funds_func=total_funds_given_total_supply, trigger_func =trigger_threshold ):
-    network = nx.DiGraph()
-    for i in range(n):
-        network.add_node(i)
-        network.nodes[i]['type']="participant"
-        
-        h_rv = expon.rvs(loc=0.0, scale=1000)
-        network.nodes[i]['holdings'] = h_rv
-        
-        s_rv = np.random.rand() 
-        network.nodes[i]['sentiment'] = s_rv
-    
-    participants = get_nodes_by_type(network, 'participant')
-    initial_supply = np.sum([ network.nodes[i]['holdings'] for i in participants])
-    
-    initial_funds = funds_func(initial_supply)    
-    
-    #generate initial proposals
-    for ind in range(m):
-        j = n+ind
-        network.add_node(j)
-        network.nodes[j]['type']="proposal"
-        network.nodes[j]['conviction']=0
-        network.nodes[j]['status']='candidate'
-        network.nodes[j]['age']=0
-        
-        r_rv = gamma.rvs(3,loc=0.001, scale=10000)
-        network.node[j]['funds_requested'] = r_rv
-        
-        network.nodes[j]['trigger']= trigger_threshold(r_rv, initial_funds, initial_supply)
-        
-        for i in range(n):
-            network.add_edge(i, j)
-            
-            rv = np.random.rand()
-            a_rv = 1-4*(1-rv)*rv #polarized distribution
-            network.edges[(i, j)]['affinity'] = a_rv
-            network.edges[(i,j)]['tokens'] = 0
-            network.edges[(i, j)]['conviction'] = 0
-            
-        proposals = get_nodes_by_type(network, 'proposal')
-        total_requested = np.sum([ network.nodes[i]['funds_requested'] for i in proposals])
-        
-    return network, initial_funds, initial_supply, total_requested
-
-def trigger_sweep(field, trigger_func,xmax=.2,default_alpha=.5):
-    
-    if field == 'token_supply':
-        alpha = default_alpha
-        share_of_funds = np.arange(.001,xmax,.001)
-        total_supply = np.arange(0,10**9, 10**6) 
-        demo_data_XY = np.outer(share_of_funds,total_supply)
-
-        demo_data_Z0=np.empty(demo_data_XY.shape)
-        demo_data_Z1=np.empty(demo_data_XY.shape)
-        demo_data_Z2=np.empty(demo_data_XY.shape)
-        demo_data_Z3=np.empty(demo_data_XY.shape)
-        for sof_ind in range(len(share_of_funds)):
-            sof = share_of_funds[sof_ind]
-            for ts_ind in range(len(total_supply)):
-                ts = total_supply[ts_ind]
-                tc = ts /(1-alpha)
-                trigger = trigger_func(sof, 1, ts)
-                demo_data_Z0[sof_ind,ts_ind] = np.log10(trigger)
-                demo_data_Z1[sof_ind,ts_ind] = trigger
-                demo_data_Z2[sof_ind,ts_ind] = trigger/tc #share of maximum possible conviction
-                demo_data_Z3[sof_ind,ts_ind] = np.log10(trigger/tc)
-        return {'log10_trigger':demo_data_Z0,
-                'trigger':demo_data_Z1,
-                'share_of_max_conv': demo_data_Z2,
-                'log10_share_of_max_conv':demo_data_Z3,
-                'total_supply':total_supply,
-                'share_of_funds':share_of_funds}
-    elif field == 'alpha':
-        alpha = np.arange(.5,1,.01)
-        share_of_funds = np.arange(.001,xmax,.001)
-        total_supply = 10**9
-        demo_data_XY = np.outer(share_of_funds,alpha)
-
-        demo_data_Z4=np.empty(demo_data_XY.shape)
-        demo_data_Z5=np.empty(demo_data_XY.shape)
-        demo_data_Z6=np.empty(demo_data_XY.shape)
-        demo_data_Z7=np.empty(demo_data_XY.shape)
-        for sof_ind in range(len(share_of_funds)):
-            sof = share_of_funds[sof_ind]
-            for a_ind in range(len(alpha)):
-                ts = total_supply
-                a = alpha[a_ind]
-                tc = ts /(1-a)
-                trigger = trigger_func(sof, 1, ts)
-                demo_data_Z4[sof_ind,a_ind] = np.log10(trigger)
-                demo_data_Z5[sof_ind,a_ind] = trigger
-                demo_data_Z6[sof_ind,a_ind] = trigger/tc #share of maximum possible conviction
-                demo_data_Z7[sof_ind,a_ind] = np.log10(trigger/tc)
-        
-        return {'log10_trigger':demo_data_Z4,
-               'trigger':demo_data_Z5,
-               'share_of_max_conv': demo_data_Z6,
-               'log10_share_of_max_conv':demo_data_Z7,
-               'alpha':alpha,
-               'share_of_funds':share_of_funds}
-        
-    else:
-        return "invalid field"
-    
-def trigger_plotter(share_of_funds,Z, color_label,y, ylabel,cmap='jet'):
-    dims = (10, 5)
-    fig, ax = plt.subplots(figsize=dims)
-
-    cf = plt.contourf(share_of_funds, y, Z.T, 100, cmap=cmap)
-    cbar=plt.colorbar(cf)
-    plt.axis([share_of_funds[0], share_of_funds[-1], y[0], y[-1]])
-    #ax.set_xscale('log')
-    plt.ylabel(ylabel)
-    plt.xlabel('Share of Funds Requested')
-    plt.title('Trigger Function Map')
-
-    cbar.ax.set_ylabel(color_label)
\ No newline at end of file
diff --git a/simulations/validation/conviction_system_logic.py b/simulations/validation/conviction_system_logic.py
deleted file mode 100644
index c739b822..00000000
--- a/simulations/validation/conviction_system_logic.py
+++ /dev/null
@@ -1,548 +0,0 @@
-import numpy as np
-
-from cadCAD.configuration.utils import config_sim
-from simulations.validation.conviction_helpers import *
-#import networkx as nx
-from scipy.stats import expon, gamma
-
-
-#functions for partial state update block 1
-
-#Driving processes: arrival of participants, proposals and funds
-##-----------------------------------------
-def gen_new_participant(network, new_participant_holdings):
-    
-    i = len([node for node in network.nodes])
-    
-    network.add_node(i)
-    network.nodes[i]['type']="participant"
-    
-    s_rv = np.random.rand() 
-    network.nodes[i]['sentiment'] = s_rv
-    network.nodes[i]['holdings']=new_participant_holdings
-    
-    for j in get_nodes_by_type(network, 'proposal'):
-        network.add_edge(i, j)
-        
-        rv = np.random.rand()
-        a_rv = 1-4*(1-rv)*rv #polarized distribution
-        network.edges[(i, j)]['affinity'] = a_rv
-        network.edges[(i,j)]['tokens'] = a_rv*network.nodes[i]['holdings']
-        network.edges[(i, j)]['conviction'] = 0
-    
-    return network
-    
-
-scale_factor = 1000
-
-def gen_new_proposal(network, funds, supply, total_funds, trigger_func):
-    j = len([node for node in network.nodes])
-    network.add_node(j)
-    network.nodes[j]['type']="proposal"
-    
-    network.nodes[j]['conviction']=0
-    network.nodes[j]['status']='candidate'
-    network.nodes[j]['age']=0
-    
-    rescale = scale_factor*funds/total_funds
-    r_rv = gamma.rvs(3,loc=0.001, scale=rescale)
-    network.node[j]['funds_requested'] = r_rv
-    
-    network.nodes[j]['trigger']= trigger_func(r_rv, funds, supply)
-    
-    participants = get_nodes_by_type(network, 'participant')
-    proposing_participant = np.random.choice(participants)
-    
-    for i in participants:
-        network.add_edge(i, j)
-        if i==proposing_participant:
-            network.edges[(i, j)]['affinity']=1
-        else:
-            rv = np.random.rand()
-            a_rv = 1-4*(1-rv)*rv #polarized distribution
-            network.edges[(i, j)]['affinity'] = a_rv
-            
-        network.edges[(i, j)]['conviction'] = 0
-        network.edges[(i,j)]['tokens'] = 0
-    return network
-        
-        
-
-def driving_process(params, step, sL, s):
-    
-    #placeholder plumbing for random processes
-    arrival_rate = 10/s['sentiment']
-    rv1 = np.random.rand()
-    new_participant = bool(rv1<1/arrival_rate)
-    if new_participant:
-        h_rv = expon.rvs(loc=0.0, scale=1000)
-        new_participant_holdings = h_rv
-    else:
-        new_participant_holdings = 0
-    
-    network = s['network']
-    affinities = [network.edges[e]['affinity'] for e in network.edges ]
-    median_affinity = np.median(affinities)
-    
-    proposals = get_nodes_by_type(network, 'proposal')
-    fund_requests = [network.nodes[j]['funds_requested'] for j in proposals if network.nodes[j]['status']=='candidate' ]
-    
-    funds = s['funds']
-    total_funds_requested = np.sum(fund_requests)
-    
-    proposal_rate = 10/median_affinity * total_funds_requested/funds
-    rv2 = np.random.rand()
-    new_proposal = bool(rv2<1/proposal_rate)
-    
-    sentiment = s['sentiment']
-    funds = s['funds']
-    scale_factor = 1+4000*sentiment**2
-    
-    #this shouldn't happen but expon is throwing domain errors
-    if scale_factor > 1: 
-        funds_arrival = expon.rvs(loc = 0, scale = scale_factor )
-    else:
-        funds_arrival = 0
-    
-    return({'new_participant':new_participant,
-            'new_participant_holdings':new_participant_holdings,
-            'new_proposal':new_proposal, 
-            'funds_arrival':funds_arrival})
-
-    
-#Mechanisms for updating the state based on driving processes
-##---
-def update_network(params, step, sL, s, _input):
-    
-    print(params)
-    print(type(params))
-    
-    network = s['network']
-    funds = s['funds']
-    supply = s['supply']
-    trigger_func = params['trigger_func']
-
-    new_participant = _input['new_participant'] #T/F
-    new_proposal = _input['new_proposal'] #T/F
-
-    if new_participant:
-        new_participant_holdings = _input['new_participant_holdings']
-        network = gen_new_participant(network, new_participant_holdings)
-    
-    if new_proposal:
-        network= gen_new_proposal(network,funds,supply )
-    
-    #update age of the existing proposals
-    proposals = get_nodes_by_type(network, 'proposal')
-    
-    for j in proposals:
-        network.nodes[j]['age'] =  network.nodes[j]['age']+1
-        if network.nodes[j]['status'] == 'candidate':
-            requested = network.nodes[j]['funds_requested']
-            network.nodes[j]['trigger'] = trigger_func(requested, funds, supply)
-        else:
-            network.nodes[j]['trigger'] = np.nan
-            
-    key = 'network'
-    value = network
-    
-    return (key, value)
-
-def increment_funds(params, step, sL, s, _input):
-    
-    funds = s['funds']
-    funds_arrival = _input['funds_arrival']
-
-    #increment funds
-    funds = funds + funds_arrival
-    
-    key = 'funds'
-    value = funds
-    
-    return (key, value)
-
-def increment_supply(params, step, sL, s, _input):
-    
-    supply = s['supply']
-    supply_arrival = _input['new_participant_holdings']
-
-    #increment funds
-    supply = supply + supply_arrival
-    
-    key = 'supply'
-    value = supply
-    
-    return (key, value)
-
-#functions for partial state update block 2
-
-#Driving processes: completion of previously funded proposals
-##-----------------------------------------
-
-def check_progress(params, step, sL, s):
-    
-    network = s['network']
-    proposals = get_nodes_by_type(network, 'proposal')
-    
-    completed = []
-    for j in proposals:
-        if network.nodes[j]['status'] == 'active':
-            grant_size = network.nodes[j]['funds_requested']
-            base_completion_rate=params['base_completion_rate']
-            likelihood = 1.0/(base_completion_rate+np.log(grant_size))
-            if np.random.rand() < likelihood:
-                completed.append(j)
-    
-    return({'completed':completed})
-
-
-#Mechanisms for updating the state based on check progress
-##---
-def complete_proposal(params, step, sL, s, _input):
-    
-    network = s['network']
-    participants = get_nodes_by_type(network, 'participant')
-    
-    completed = _input['completed']
-    for j in completed:
-        network.nodes[j]['status']='completed'
-        for i in participants:
-            force = network.edges[(i,j)]['affinity']
-            sentiment = network.node[i]['sentiment']
-            network.node[i]['sentiment'] = get_sentimental(sentiment, force, decay=0)
-    
-    key = 'network'
-    value = network
-    
-    return (key, value)
-
-def update_sentiment_on_completion(params, step, sL, s, _input):
-    
-    network = s['network']
-    proposals = get_nodes_by_type(network, 'proposal')
-    completed = _input['completed']
-    
-    grants_outstanding = np.sum([network.nodes[j]['funds_requested'] for j in proposals if network.nodes[j]['status']=='active'])
-    
-    grants_completed = np.sum([network.nodes[j]['funds_requested'] for j in completed])
-    
-    sentiment = s['sentiment']
-    
-    force = grants_completed/grants_outstanding
-    mu = params['sentiment_decay']
-    if (force >=0) and (force <=1):
-        sentiment = get_sentimental(sentiment, force, mu)
-    else:
-        sentiment = get_sentimental(sentiment, 0, mu)
-    
-    
-    key = 'sentiment'
-    value = sentiment
-    
-    return (key, value)
-
-def get_sentimental(sentiment, force, decay=0):
-    mu = decay
-    sentiment = sentiment*(1-mu) + force
-    
-    if sentiment > 1:
-        sentiment = 1
-        
-    return sentiment
-
-#functions for partial state update block 3
-
-#Decision processes: trigger function policy
-##-----------------------------------------
-
-def trigger_function(params, step, sL, s):
-    
-    network = s['network']
-    funds = s['funds']
-    supply = s['supply']
-    proposals = get_nodes_by_type(network, 'proposal')
-    tmin = params['tmin']
-    
-    accepted = []
-    triggers = {}
-    for j in proposals:
-        if network.nodes[j]['status'] == 'candidate':
-            requested = network.nodes[j]['funds_requested']
-            age = network.nodes[j]['age']
-            threshold = trigger_threshold(requested, funds, supply)
-            if age > tmin:
-                conviction = network.nodes[j]['conviction']
-                if conviction >threshold:
-                    accepted.append(j)
-        else:
-            threshold = np.nan
-            
-        triggers[j] = threshold
-                
-        
-                    
-    return({'accepted':accepted, 'triggers':triggers})
-
-def decrement_funds(params, step, sL, s, _input):
-    
-    funds = s['funds']
-    network = s['network']
-    accepted = _input['accepted']
-
-    #decrement funds
-    for j in accepted:
-        funds = funds - network.nodes[j]['funds_requested']
-    
-    key = 'funds'
-    value = funds
-    
-    return (key, value)
-
-def update_proposals(params, step, sL, s, _input):
-    
-    network = s['network']
-    accepted = _input['accepted']
-    triggers = _input['triggers']
-    participants = get_nodes_by_type(network, 'participant')
-    proposals = get_nodes_by_type(network, 'proposals')
-    sensitivity = params['sensitivity']
-    
-    for j in proposals:
-        network.nodes[j]['trigger'] = triggers[j]
-    
-    #bookkeeping conviction and participant sentiment
-    for j in accepted:
-        network.nodes[j]['status']='active'
-        network.nodes[j]['conviction']=np.nan
-        #change status to active
-        for i in participants:
-        
-            #operating on edge = (i,j)
-            #reset tokens assigned to other candidates
-            network.edges[(i,j)]['tokens']=0
-            network.edges[(i,j)]['conviction'] = np.nan
-            
-            #update participants sentiments (positive or negative) 
-            affinities = [network.edges[(i,p)]['affinity'] for p in proposals if not(p in accepted)]
-            if len(affinities)>1:
-                max_affinity = np.max(affinities)
-                force = network.edges[(i,j)]['affinity']-sensitivity*max_affinity
-            else:
-                force = 0
-            
-            #based on what their affinities to the accepted proposals
-            network.nodes[i]['sentiment'] = get_sentimental(network.nodes[i]['sentiment'], force, False)
-            
-    
-    key = 'network'
-    value = network
-    
-    return (key, value)
-
-def update_sentiment_on_release(params, step, sL, s, _input):
-    
-    network = s['network']
-    proposals = get_nodes_by_type(network, 'proposal')
-    accepted = _input['accepted']
-    
-    proposals_outstanding = np.sum([network.nodes[j]['funds_requested'] for j in proposals if network.nodes[j]['status']=='candidate'])
-    
-    proposals_accepted = np.sum([network.nodes[j]['funds_requested'] for j in accepted])
-    
-    sentiment = s['sentiment']
-    force = proposals_accepted/proposals_outstanding
-    if (force >=0) and (force <=1):
-        sentiment = get_sentimental(sentiment, force, False)
-    else:
-        sentiment = get_sentimental(sentiment, 0, False)
-    
-    key = 'sentiment'
-    value = sentiment
-    
-    return (key, value)
-
-def participants_decisions(params, step, sL, s):
-    network = s['network']
-    participants = get_nodes_by_type(network, 'participant')
-    proposals = get_nodes_by_type(network, 'proposal')
-    candidates = [j for j in proposals if network.nodes[j]['status']=='candidate']
-    sensitivity = params['sensitivity']
-    
-    gain = .01
-    delta_holdings={}
-    proposals_supported ={}
-    for i in participants:
-        force = network.nodes[i]['sentiment']-sensitivity
-        delta_holdings[i] = network.nodes[i]['holdings']*gain*force
-        
-        support = []
-        for j in candidates:
-            affinity = network.edges[(i, j)]['affinity']
-            cutoff = sensitivity*np.max([network.edges[(i,p)]['affinity'] for p in candidates])
-            if cutoff <.5:
-                cutoff = .5
-            
-            if affinity > cutoff:
-                support.append(j)
-        
-        proposals_supported[i] = support
-    
-    return({'delta_holdings':delta_holdings, 'proposals_supported':proposals_supported})
-
-def update_tokens(params, step, sL, s, _input):
-    
-    network = s['network']
-    delta_holdings = _input['delta_holdings']
-    proposals = get_nodes_by_type(network, 'proposal')
-    proposals_supported = _input['proposals_supported']
-    participants = get_nodes_by_type(network, 'participant')
-    alpha = params['alpha']
-    
-    for i in participants:
-        network.nodes[i]['holdings'] = network.nodes[i]['holdings']+delta_holdings[i]
-        supported = proposals_supported[i]
-        total_affinity = np.sum([ network.edges[(i, j)]['affinity'] for j in supported])
-        for j in proposals:
-            if j in supported:
-                normalized_affinity = network.edges[(i, j)]['affinity']/total_affinity
-                network.edges[(i, j)]['tokens'] = normalized_affinity*network.nodes[i]['holdings']
-            else:
-                network.edges[(i, j)]['tokens'] = 0
-            
-            prior_conviction = network.edges[(i, j)]['conviction']
-            current_tokens = network.edges[(i, j)]['tokens']
-            network.edges[(i, j)]['conviction'] =current_tokens+alpha*prior_conviction
-    
-    for j in proposals:
-        network.nodes[j]['conviction'] = np.sum([ network.edges[(i, j)]['conviction'] for i in participants])
-    
-    key = 'network'
-    value = network
-    
-    return (key, value)
-
-def update_supply(params, step, sL, s, _input):
-    
-    supply = s['supply']
-    delta_holdings = _input['delta_holdings']
-    delta_supply = np.sum([v for v in delta_holdings.values()])
-    
-    supply = supply + delta_supply
-    
-    key = 'supply'
-    value = supply
-    
-    return (key, value)
-
-# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
-# The Partial State Update Blocks
-partial_state_update_blocks = [
-    {
-        'policies': {
-            #new proposals or new participants
-            'random': driving_process
-        },
-        'variables': {
-            'network': update_network,
-            'funds':increment_funds,
-            'supply':increment_supply
-        }
-    },
-    {
-      'policies': {
-          'completion': check_progress #see if any of the funded proposals completes
-        },
-        'variables': { # The following state variables will be updated simultaneously
-            'sentiment': update_sentiment_on_completion, #note completing decays sentiment, completing bumps it
-            'network': complete_proposal #book-keeping
-        }
-    },
-        {
-      'policies': {
-          'release': trigger_function #check each proposal to see if it passes
-        },
-        'variables': { # The following state variables will be updated simultaneously
-            'funds': decrement_funds, #funds expended
-            'sentiment': update_sentiment_on_release, #releasing funds can bump sentiment
-            'network': update_proposals #reset convictions, and participants sentiments
-                                        #update based on affinities
-        }
-    },
-    {
-        'policies': {
-            'participants_act': participants_decisions, #high sentiment, high affinity =>buy
-                                                        #low sentiment, low affinities => burn
-                                                        #assign tokens to top affinities
-        },
-        'variables': {
-            'supply': update_supply,
-            'network': update_tokens #update everyones holdings
-                                    #and their conviction for each proposal
-        }
-    }
-]
-
-n= 25 #initial participants
-m= 3 #initial proposals
-
-initial_sentiment = .5
-
-network, initial_funds, initial_supply, total_requested = initialize_network(n,m,total_funds_given_total_supply,trigger_threshold)
-
-initial_conditions = {'network':network,
-                      'supply': initial_supply,
-                      'funds':initial_funds,
-                      'sentiment': initial_sentiment}
-
-#power of 1 token forever
-# conviction_capactity = [2]
-# alpha = [1-1/cc for cc in conviction_capactity]
-# print(alpha)
-
-params={
-    'sensitivity': [.75],
-    'tmin': [7], #unit days; minimum periods passed before a proposal can pass
-    'sentiment_decay': [.001], #termed mu in the state update function
-    'alpha': [0.5, 0.9],
-    'base_completion_rate': [10],
-    'trigger_func': [trigger_threshold]
-}
-
-
-# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
-# Settings of general simulation parameters, unrelated to the system itself
-# `T` is a range with the number of discrete units of time the simulation will run for;
-# `N` is the number of times the simulation will be run (Monte Carlo runs)
-time_periods_per_run = 250
-monte_carlo_runs = 1
-
-simulation_parameters = config_sim({
-    'T': range(time_periods_per_run),
-    'N': monte_carlo_runs,
-    'M': params
-})
-
-
-from cadCAD.configuration import append_configs
-
-# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
-# The configurations above are then packaged into a `Configuration` object
-append_configs(
-    initial_state=initial_conditions, #dict containing variable names and initial values
-    partial_state_update_blocks=partial_state_update_blocks, #dict containing state update functions
-    sim_configs=simulation_parameters #dict containing simulation parameters
-)
-
-from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-from cadCAD import configs
-
-exec_mode = ExecutionMode()
-multi_proc_ctx = ExecutionContext(context=exec_mode.multi_proc)
-run = Executor(exec_context=multi_proc_ctx, configs=configs)
-raw_result, tensor = run.execute()
-
-# exec_mode = ExecutionMode()
-# exec_context = ExecutionContext(context=exec_mode.multi_proc)
-# # run = Executor(exec_context=exec_context, configs=configs)
-# executor = Executor(exec_context, configs) # Pass the configuration object inside an array
-# raw_result, tensor = executor.execute() # The `main()` method returns a tuple; its first elements contains the raw results
\ No newline at end of file
diff --git a/simulations/validation/conviction_system_logic_sim.py b/simulations/validation/conviction_system_logic_sim.py
deleted file mode 100644
index 85b62a00..00000000
--- a/simulations/validation/conviction_system_logic_sim.py
+++ /dev/null
@@ -1,555 +0,0 @@
-from pprint import pprint
-
-import numpy as np
-from tabulate import tabulate
-
-from cadCAD.configuration.utils import config_sim
-from simulations.validation.conviction_helpers import *
-#import networkx as nx
-from scipy.stats import expon, gamma
-
-
-#functions for partial state update block 1
-
-#Driving processes: arrival of participants, proposals and funds
-##-----------------------------------------
-
-
-def gen_new_participant(network, new_participant_holdings):
-    
-    i = len([node for node in network.nodes])
-    
-    network.add_node(i)
-    network.nodes[i]['type']="participant"
-    
-    s_rv = np.random.rand() 
-    network.nodes[i]['sentiment'] = s_rv
-    network.nodes[i]['holdings']=new_participant_holdings
-    
-    for j in get_nodes_by_type(network, 'proposal'):
-        network.add_edge(i, j)
-        
-        rv = np.random.rand()
-        a_rv = 1-4*(1-rv)*rv #polarized distribution
-        network.edges[(i, j)]['affinity'] = a_rv
-        network.edges[(i,j)]['tokens'] = a_rv*network.nodes[i]['holdings']
-        network.edges[(i, j)]['conviction'] = 0
-    
-    return network
-    
-
-scale_factor = 1000
-
-def gen_new_proposal(network, funds, supply, trigger_func):
-    j = len([node for node in network.nodes])
-    network.add_node(j)
-    network.nodes[j]['type']="proposal"
-    
-    network.nodes[j]['conviction']=0
-    network.nodes[j]['status']='candidate'
-    network.nodes[j]['age']=0
-    
-    rescale = scale_factor*funds
-    r_rv = gamma.rvs(3,loc=0.001, scale=rescale)
-    network.node[j]['funds_requested'] = r_rv
-    
-    network.nodes[j]['trigger']= trigger_func(r_rv, funds, supply)
-    
-    participants = get_nodes_by_type(network, 'participant')
-    proposing_participant = np.random.choice(participants)
-    
-    for i in participants:
-        network.add_edge(i, j)
-        if i==proposing_participant:
-            network.edges[(i, j)]['affinity']=1
-        else:
-            rv = np.random.rand()
-            a_rv = 1-4*(1-rv)*rv #polarized distribution
-            network.edges[(i, j)]['affinity'] = a_rv
-            
-        network.edges[(i, j)]['conviction'] = 0
-        network.edges[(i,j)]['tokens'] = 0
-    return network
-        
-        
-
-def driving_process(params, step, sL, s):
-    
-    #placeholder plumbing for random processes
-    arrival_rate = 10/s['sentiment']
-    rv1 = np.random.rand()
-    new_participant = bool(rv1<1/arrival_rate)
-    if new_participant:
-        h_rv = expon.rvs(loc=0.0, scale=1000)
-        new_participant_holdings = h_rv
-    else:
-        new_participant_holdings = 0
-    
-    network = s['network']
-    affinities = [network.edges[e]['affinity'] for e in network.edges ]
-    median_affinity = np.median(affinities)
-    
-    proposals = get_nodes_by_type(network, 'proposal')
-    fund_requests = [network.nodes[j]['funds_requested'] for j in proposals if network.nodes[j]['status']=='candidate' ]
-    
-    funds = s['funds']
-    total_funds_requested = np.sum(fund_requests)
-    
-    proposal_rate = 10/median_affinity * total_funds_requested/funds
-    rv2 = np.random.rand()
-    new_proposal = bool(rv2<1/proposal_rate)
-    
-    sentiment = s['sentiment']
-    funds = s['funds']
-    scale_factor = 1+4000*sentiment**2
-    
-    #this shouldn't happen but expon is throwing domain errors
-    if scale_factor > 1: 
-        funds_arrival = expon.rvs(loc = 0, scale = scale_factor )
-    else:
-        funds_arrival = 0
-    
-    return({'new_participant':new_participant,
-            'new_participant_holdings':new_participant_holdings,
-            'new_proposal':new_proposal, 
-            'funds_arrival':funds_arrival})
-
-    
-#Mechanisms for updating the state based on driving processes
-##---
-def update_network(params, step, sL, s, _input):
-
-    network = s['network']
-    funds = s['funds']
-    supply = s['supply']
-    trigger_func = params['trigger_func']
-
-    new_participant = _input['new_participant'] #T/F
-    new_proposal = _input['new_proposal'] #T/F
-
-    if new_participant:
-        new_participant_holdings = _input['new_participant_holdings']
-        network = gen_new_participant(network, new_participant_holdings)
-    
-    if new_proposal:
-        network= gen_new_proposal(network,funds,supply,trigger_func )
-    
-    #update age of the existing proposals
-    proposals = get_nodes_by_type(network, 'proposal')
-    
-    for j in proposals:
-        network.nodes[j]['age'] =  network.nodes[j]['age']+1
-        if network.nodes[j]['status'] == 'candidate':
-            requested = network.nodes[j]['funds_requested']
-            network.nodes[j]['trigger'] = trigger_func(requested, funds, supply)
-        else:
-            network.nodes[j]['trigger'] = np.nan
-            
-    key = 'network'
-    value = network
-    
-    return (key, value)
-
-def increment_funds(params, step, sL, s, _input):
-    
-    funds = s['funds']
-    funds_arrival = _input['funds_arrival']
-
-    #increment funds
-    funds = funds + funds_arrival
-    
-    key = 'funds'
-    value = funds
-    
-    return (key, value)
-
-def increment_supply(params, step, sL, s, _input):
-    
-    supply = s['supply']
-    supply_arrival = _input['new_participant_holdings']
-
-    #increment funds
-    supply = supply + supply_arrival
-    
-    key = 'supply'
-    value = supply
-    
-    return (key, value)
-
-#functions for partial state update block 2
-
-#Driving processes: completion of previously funded proposals
-##-----------------------------------------
-
-def check_progress(params, step, sL, s):
-    
-    network = s['network']
-    proposals = get_nodes_by_type(network, 'proposal')
-    
-    completed = []
-    for j in proposals:
-        if network.nodes[j]['status'] == 'active':
-            grant_size = network.nodes[j]['funds_requested']
-            base_completion_rate=params['base_completion_rate']
-            likelihood = 1.0/(base_completion_rate+np.log(grant_size))
-            if np.random.rand() < likelihood:
-                completed.append(j)
-    
-    return({'completed':completed})
-
-
-#Mechanisms for updating the state based on check progress
-##---
-def complete_proposal(params, step, sL, s, _input):
-    
-    network = s['network']
-    participants = get_nodes_by_type(network, 'participant')
-    
-    completed = _input['completed']
-    for j in completed:
-        network.nodes[j]['status']='completed'
-        for i in participants:
-            force = network.edges[(i,j)]['affinity']
-            sentiment = network.node[i]['sentiment']
-            network.node[i]['sentiment'] = get_sentimental(sentiment, force, decay=0)
-    
-    key = 'network'
-    value = network
-    
-    return (key, value)
-
-def update_sentiment_on_completion(params, step, sL, s, _input):
-    
-    network = s['network']
-    proposals = get_nodes_by_type(network, 'proposal')
-    completed = _input['completed']
-    
-    grants_outstanding = np.sum([network.nodes[j]['funds_requested'] for j in proposals if network.nodes[j]['status']=='active'])
-    
-    grants_completed = np.sum([network.nodes[j]['funds_requested'] for j in completed])
-    
-    sentiment = s['sentiment']
-    
-    force = grants_completed/grants_outstanding
-    mu = params['sentiment_decay']
-    if (force >=0) and (force <=1):
-        sentiment = get_sentimental(sentiment, force, mu)
-    else:
-        sentiment = get_sentimental(sentiment, 0, mu)
-    
-    
-    key = 'sentiment'
-    value = sentiment
-    
-    return (key, value)
-
-def get_sentimental(sentiment, force, decay=0):
-    mu = decay
-    sentiment = sentiment*(1-mu) + force
-    
-    if sentiment > 1:
-        sentiment = 1
-        
-    return sentiment
-
-#functions for partial state update block 3
-
-#Decision processes: trigger function policy
-##-----------------------------------------
-
-def trigger_function(params, step, sL, s):
-    
-    network = s['network']
-    funds = s['funds']
-    supply = s['supply']
-    proposals = get_nodes_by_type(network, 'proposal')
-    tmin = params['tmin']
-    
-    accepted = []
-    triggers = {}
-    for j in proposals:
-        if network.nodes[j]['status'] == 'candidate':
-            requested = network.nodes[j]['funds_requested']
-            age = network.nodes[j]['age']
-            threshold = trigger_threshold(requested, funds, supply)
-            if age > tmin:
-                conviction = network.nodes[j]['conviction']
-                if conviction >threshold:
-                    accepted.append(j)
-        else:
-            threshold = np.nan
-            
-        triggers[j] = threshold
-                
-        
-                    
-    return({'accepted':accepted, 'triggers':triggers})
-
-def decrement_funds(params, step, sL, s, _input):
-    
-    funds = s['funds']
-    network = s['network']
-    accepted = _input['accepted']
-
-    #decrement funds
-    for j in accepted:
-        funds = funds - network.nodes[j]['funds_requested']
-    
-    key = 'funds'
-    value = funds
-    
-    return (key, value)
-
-def update_proposals(params, step, sL, s, _input):
-    
-    network = s['network']
-    accepted = _input['accepted']
-    triggers = _input['triggers']
-    participants = get_nodes_by_type(network, 'participant')
-    proposals = get_nodes_by_type(network, 'proposals')
-    sensitivity = params['sensitivity']
-    
-    for j in proposals:
-        network.nodes[j]['trigger'] = triggers[j]
-    
-    #bookkeeping conviction and participant sentiment
-    for j in accepted:
-        network.nodes[j]['status']='active'
-        network.nodes[j]['conviction']=np.nan
-        #change status to active
-        for i in participants:
-        
-            #operating on edge = (i,j)
-            #reset tokens assigned to other candidates
-            network.edges[(i,j)]['tokens']=0
-            network.edges[(i,j)]['conviction'] = np.nan
-            
-            #update participants sentiments (positive or negative) 
-            affinities = [network.edges[(i,p)]['affinity'] for p in proposals if not(p in accepted)]
-            if len(affinities)>1:
-                max_affinity = np.max(affinities)
-                force = network.edges[(i,j)]['affinity']-sensitivity*max_affinity
-            else:
-                force = 0
-            
-            #based on what their affinities to the accepted proposals
-            network.nodes[i]['sentiment'] = get_sentimental(network.nodes[i]['sentiment'], force, False)
-            
-    
-    key = 'network'
-    value = network
-    
-    return (key, value)
-
-def update_sentiment_on_release(params, step, sL, s, _input):
-    
-    network = s['network']
-    proposals = get_nodes_by_type(network, 'proposal')
-    accepted = _input['accepted']
-    
-    proposals_outstanding = np.sum([network.nodes[j]['funds_requested'] for j in proposals if network.nodes[j]['status']=='candidate'])
-    
-    proposals_accepted = np.sum([network.nodes[j]['funds_requested'] for j in accepted])
-    
-    sentiment = s['sentiment']
-    force = proposals_accepted/proposals_outstanding
-    if (force >=0) and (force <=1):
-        sentiment = get_sentimental(sentiment, force, False)
-    else:
-        sentiment = get_sentimental(sentiment, 0, False)
-    
-    key = 'sentiment'
-    value = sentiment
-    
-    return (key, value)
-
-def participants_decisions(params, step, sL, s):
-    network = s['network']
-    participants = get_nodes_by_type(network, 'participant')
-    proposals = get_nodes_by_type(network, 'proposal')
-    candidates = [j for j in proposals if network.nodes[j]['status']=='candidate']
-    sensitivity = params['sensitivity']
-    
-    gain = .01
-    delta_holdings={}
-    proposals_supported ={}
-    for i in participants:
-        force = network.nodes[i]['sentiment']-sensitivity
-        delta_holdings[i] = network.nodes[i]['holdings']*gain*force
-        
-        support = []
-        for j in candidates:
-            affinity = network.edges[(i, j)]['affinity']
-            cutoff = sensitivity*np.max([network.edges[(i,p)]['affinity'] for p in candidates])
-            if cutoff <.5:
-                cutoff = .5
-            
-            if affinity > cutoff:
-                support.append(j)
-        
-        proposals_supported[i] = support
-    
-    return({'delta_holdings':delta_holdings, 'proposals_supported':proposals_supported})
-
-def update_tokens(params, step, sL, s, _input):
-    
-    network = s['network']
-    delta_holdings = _input['delta_holdings']
-    proposals = get_nodes_by_type(network, 'proposal')
-    proposals_supported = _input['proposals_supported']
-    participants = get_nodes_by_type(network, 'participant')
-    alpha = params['alpha']
-    
-    for i in participants:
-        network.nodes[i]['holdings'] = network.nodes[i]['holdings']+delta_holdings[i]
-        supported = proposals_supported[i]
-        total_affinity = np.sum([ network.edges[(i, j)]['affinity'] for j in supported])
-        for j in proposals:
-            if j in supported:
-                normalized_affinity = network.edges[(i, j)]['affinity']/total_affinity
-                network.edges[(i, j)]['tokens'] = normalized_affinity*network.nodes[i]['holdings']
-            else:
-                network.edges[(i, j)]['tokens'] = 0
-            
-            prior_conviction = network.edges[(i, j)]['conviction']
-            current_tokens = network.edges[(i, j)]['tokens']
-            network.edges[(i, j)]['conviction'] =current_tokens+alpha*prior_conviction
-    
-    for j in proposals:
-        network.nodes[j]['conviction'] = np.sum([ network.edges[(i, j)]['conviction'] for i in participants])
-    
-    key = 'network'
-    value = network
-    
-    return (key, value)
-
-def update_supply(params, step, sL, s, _input):
-    
-    supply = s['supply']
-    delta_holdings = _input['delta_holdings']
-    delta_supply = np.sum([v for v in delta_holdings.values()])
-    
-    supply = supply + delta_supply
-    
-    key = 'supply'
-    value = supply
-    
-    return (key, value)
-
-# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
-# The Partial State Update Blocks
-partial_state_update_blocks = [
-    {
-        'policies': {
-            #new proposals or new participants
-            'random': driving_process
-        },
-        'variables': {
-            'network': update_network,
-            'funds':increment_funds,
-            'supply':increment_supply
-        }
-    },
-    {
-      'policies': {
-          'completion': check_progress #see if any of the funded proposals completes
-        },
-        'variables': { # The following state variables will be updated simultaneously
-            'sentiment': update_sentiment_on_completion, #note completing decays sentiment, completing bumps it
-            'network': complete_proposal #book-keeping
-        }
-    },
-        {
-      'policies': {
-          'release': trigger_function #check each proposal to see if it passes
-        },
-        'variables': { # The following state variables will be updated simultaneously
-            'funds': decrement_funds, #funds expended
-            'sentiment': update_sentiment_on_release, #releasing funds can bump sentiment
-            'network': update_proposals #reset convictions, and participants sentiments
-                                        #update based on affinities
-        }
-    },
-    {
-        'policies': {
-            'participants_act': participants_decisions, #high sentiment, high affinity =>buy
-                                                        #low sentiment, low affinities => burn
-                                                        #assign tokens to top affinities
-        },
-        'variables': {
-            'supply': update_supply,
-            'network': update_tokens #update everyones holdings
-                                    #and their conviction for each proposal
-        }
-    }
-]
-
-n= 25 #initial participants
-m= 3 #initial proposals
-
-initial_sentiment = .5
-
-network, initial_funds, initial_supply, total_requested = initialize_network(n,m,total_funds_given_total_supply,trigger_threshold)
-
-initial_conditions = {'network':network,
-                      'supply': initial_supply,
-                      'funds':initial_funds,
-                      'sentiment': initial_sentiment}
-
-#power of 1 token forever
-# conviction_capactity = [2]
-# alpha = [1-1/cc for cc in conviction_capactity]
-# print(alpha)
-
-params={
-    'sensitivity': [.75],
-    'tmin': [7], #unit days; minimum periods passed before a proposal can pass
-    'sentiment_decay': [.001], #termed mu in the state update function
-    'alpha': [0.5],
-    'base_completion_rate': [10],
-    'trigger_func': [trigger_threshold]
-}
-
-
-# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
-# Settings of general simulation parameters, unrelated to the system itself
-# `T` is a range with the number of discrete units of time the simulation will run for;
-# `N` is the number of times the simulation will be run (Monte Carlo runs)
-time_periods_per_run = 250
-monte_carlo_runs = 1
-
-simulation_parameters = config_sim({
-    'T': range(time_periods_per_run),
-    'N': monte_carlo_runs,
-    'M': params
-})
-
-
-from cadCAD.configuration import append_configs
-
-# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
-# The configurations above are then packaged into a `Configuration` object
-append_configs(
-    initial_state=initial_conditions, #dict containing variable names and initial values
-    partial_state_update_blocks=partial_state_update_blocks, #dict containing state update functions
-    sim_configs=simulation_parameters #dict containing simulation parameters
-)
-
-from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-from cadCAD import configs
-import pandas as pd
-
-exec_mode = ExecutionMode()
-multi_proc_ctx = ExecutionContext(context=exec_mode.multi_proc)
-run = Executor(exec_context=multi_proc_ctx, configs=configs)
-
-i = 0
-for raw_result, tensor_field in run.execute():
-    result = pd.DataFrame(raw_result)
-    print()
-    print(f"Tensor Field: {type(tensor_field)}")
-    print(tabulate(tensor_field, headers='keys', tablefmt='psql'))
-    print(f"Output: {type(result)}")
-    print(tabulate(result, headers='keys', tablefmt='psql'))
-    print()
-    i += 1
diff --git a/simulations/validation/external_ds_write.py b/simulations/validation/external_ds_write.py
deleted file mode 100644
index 05558e38..00000000
--- a/simulations/validation/external_ds_write.py
+++ /dev/null
@@ -1,22 +0,0 @@
-import pandas as pd
-from tabulate import tabulate
-# The following imports NEED to be in the exact order
-from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-# from simulations.validation import config1_test_pipe
-# from simulations.validation import config1
-from simulations.validation import write_simulation
-from cadCAD import configs
-
-exec_mode = ExecutionMode()
-
-first_config = configs # only contains config1
-single_proc_ctx = ExecutionContext(context=exec_mode.single_proc)
-run = Executor(exec_context=single_proc_ctx, configs=first_config)
-
-raw_result, _ = run.main()
-result = pd.DataFrame(raw_result)
-result.to_csv('simulations/external_data/output.csv', index=False)
-
-print("Output:")
-print(tabulate(result, headers='keys', tablefmt='psql'))
-print()
diff --git a/simulations/validation/new_sweep_config.py b/simulations/validation/new_sweep_config.py
deleted file mode 100644
index bd2847b7..00000000
--- a/simulations/validation/new_sweep_config.py
+++ /dev/null
@@ -1,183 +0,0 @@
-from decimal import Decimal
-import numpy as np
-from datetime import timedelta
-import pprint
-
-from cadCAD.configuration import append_configs
-from cadCAD.configuration.utils import env_proc_trigger, ep_time_step, config_sim
-
-from typing import Dict, List
-
-# from cadCAD.utils.sys_config import exo, exo_check
-
-pp = pprint.PrettyPrinter(indent=4)
-
-seeds = {
-    'z': np.random.RandomState(1),
-    'a': np.random.RandomState(2),
-    'b': np.random.RandomState(3),
-    'c': np.random.RandomState(3)
-}
-
-# Optional
-g: Dict[str, List[int]] = {
-    'alpha': [1],
-    'beta': [2, 5],
-    'gamma': [3, 4],
-    'omega': [7]
-}
-
-# Policies per Mechanism
-def p1m1(_g, step, sL, s):
-    return {'param1': 1}
-
-def p2m1(_g, step, sL, s):
-    return {'param2': 4}
-
-def p1m2(_g, step, sL, s):
-    return {'param1': 'a', 'param2': _g['beta']}
-
-def p2m2(_g, step, sL, s):
-    return {'param1': 'b', 'param2': 0}
-
-def p1m3(_g, step, sL, s):
-    return {'param1': np.array([10, 100])}
-
-def p2m3(_g, step, sL, s):
-    return {'param1': np.array([20, 200])}
-
-# Internal States per Mechanism
-def s1m1(_g, step, sL, s, _input):
-    return 's1', 0
-
-def s2m1(_g, step, sL, s, _input):
-    return 's2', _g['beta']
-
-def s1m2(_g, step, sL, s, _input):
-    return 's1', _input['param2']
-
-def s2m2(_g, step, sL, s, _input):
-    return 's2', _input['param2']
-
-def s1m3(_g, step, sL, s, _input):
-    return 's1', 0
-
-def s2m3(_g, step, sL, s, _input):
-    return 's2', 0
-
-
-# Exogenous States
-proc_one_coef_A = 0.7
-proc_one_coef_B = 1.3
-
-
-def es3p1(_g, step, sL, s, _input):
-    return 's3', _g['gamma']
-# @curried
-def es4p2(_g, step, sL, s, _input):
-    return 's4', _g['gamma']
-
-ts_format = '%Y-%m-%d %H:%M:%S'
-t_delta = timedelta(days=0, minutes=0, seconds=1)
-def es5p2(_g, step, sL, s, _input):
-    y = 'timestep'
-    x = ep_time_step(s, dt_str=s['timestep'], fromat_str=ts_format, _timedelta=t_delta)
-    return (y, x)
-
-
-# Environment States
-# @curried
-# def env_a(param, x):
-#     return x + param
-def env_a(x):
-    return x
-def env_b(x):
-    return 10
-
-
-# Genesis States
-genesis_states = {
-    's1': Decimal(0.0),
-    's2': Decimal(0.0),
-    's3': Decimal(1.0),
-    's4': Decimal(1.0),
-#     'timestep': '2018-10-01 15:16:24'
-}
-
-
-# remove `exo_update_per_ts` to update every ts
-raw_exogenous_states = {
-    "s3": es3p1,
-    "s4": es4p2,
-#     "timestep": es5p2
-}
-
-
-# ToDo: make env proc trigger field agnostic
-# ToDo: input json into function renaming __name__
-triggered_env_b = env_proc_trigger(1, env_b)
-env_processes = {
-    "s3": env_a, #sweep(beta, env_a),
-    "s4": triggered_env_b #rename('parameterized', triggered_env_b) #sweep(beta, triggered_env_b)
-}
-# parameterized_env_processes = parameterize_states(env_processes)
-#
-# pp.pprint(parameterized_env_processes)
-# exit()
-
-# ToDo: The number of values entered in sweep should be the # of config objs created,
-# not dependent on the # of times the sweep is applied
-# sweep exo_state func and point to exo-state in every other funtion
-# param sweep on genesis states
-
-partial_state_update_block = {
-    "m1": {
-        "policies": {
-            "b1": p1m1,
-            "b2": p2m1
-        },
-        "variables": {
-            "s1": s1m1,
-            "s2": s2m1
-        }
-    },
-    "m2": {
-        "policies": {
-            "b1": p1m2,
-            "b2": p2m2,
-        },
-        "variables": {
-            "s1": s1m2,
-            "s2": s2m2
-        }
-    },
-    "m3": {
-        "policies": {
-            "b1": p1m3,
-            "b2": p2m3
-        },
-        "variables": {
-            "s1": s1m3,
-            "s2": s2m3
-        }
-    }
-}
-
-# config_sim Necessary
-sim_config = config_sim(
-    {
-        "N": 2,
-        "T": range(5),
-        "M": g # Optional
-    }
-)
-
-# New Convention
-append_configs(
-    sim_configs=sim_config,
-    initial_state=genesis_states,
-    seeds=seeds,
-    raw_exogenous_states={}, #raw_exogenous_states,
-    env_processes={}, #env_processes,
-    partial_state_update_blocks=partial_state_update_block
-)
\ No newline at end of file
diff --git a/simulations/validation/param_sweep_run.py b/simulations/validation/param_sweep_run.py
deleted file mode 100644
index ace80914..00000000
--- a/simulations/validation/param_sweep_run.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import pandas as pd
-from tabulate import tabulate
-# The following imports NEED to be in the exact order
-from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-from simulations.validation import sweep_config
-from cadCAD import configs
-
-exec_mode = ExecutionMode()
-
-print("Simulation Execution: Concurrent Execution")
-multi_proc_ctx = ExecutionContext(context=exec_mode.multi_proc)
-run = Executor(exec_context=multi_proc_ctx, configs=configs)
-
-i = 0
-config_names = ['sweep_config_A', 'sweep_config_B']
-for raw_result, tensor_field in run.execute():
-    result = pd.DataFrame(raw_result)
-    print()
-    print("Tensor Field: " + config_names[i])
-    print(tabulate(tensor_field, headers='keys', tablefmt='psql'))
-    print("Output:")
-    print(tabulate(result, headers='keys', tablefmt='psql'))
-    print()
-    i += 1
diff --git a/simulations/validation/sweep_config.py b/simulations/validation/sweep_config.py
deleted file mode 100644
index 92de2abd..00000000
--- a/simulations/validation/sweep_config.py
+++ /dev/null
@@ -1,181 +0,0 @@
-from decimal import Decimal
-import numpy as np
-from datetime import timedelta
-import pprint
-
-from cadCAD.configuration import append_configs
-from cadCAD.configuration.utils import env_proc_trigger, ep_time_step, config_sim
-
-from typing import Dict, List
-
-pp = pprint.PrettyPrinter(indent=4)
-
-seeds = {
-    'z': np.random.RandomState(1),
-    'a': np.random.RandomState(2),
-    'b': np.random.RandomState(3),
-    'c': np.random.RandomState(3)
-}
-
-# Optional
-g: Dict[str, List[int]] = {
-    'alpha': [1],
-    'beta': [2, 5],
-    'gamma': [3, 4],
-    'omega': [7]
-}
-
-# Policies per Mechanism
-def p1m1(_g, step, sL, s):
-    return {'param1': 1}
-
-def p2m1(_g, step, sL, s):
-    return {'param2': 4}
-
-def p1m2(_g, step, sL, s):
-    return {'param1': 'a', 'param2': _g['beta']}
-
-def p2m2(_g, step, sL, s):
-    return {'param1': 'b', 'param2': 0}
-
-def p1m3(_g, step, sL, s):
-    return {'param1': np.array([10, 100])}
-
-def p2m3(_g, step, sL, s):
-    return {'param1': np.array([20, 200])}
-
-# Internal States per Mechanism
-def s1m1(_g, step, sL, s, _input):
-    return 's1', 0
-
-def s2m1(_g, step, sL, s, _input):
-    return 's2', _g['beta']
-
-def s1m2(_g, step, sL, s, _input):
-    return 's1', _input['param2']
-
-def s2m2(_g, step, sL, s, _input):
-    return 's2', _input['param2']
-
-def s1m3(_g, step, sL, s, _input):
-    return 's1', 0
-
-def s2m3(_g, step, sL, s, _input):
-    return 's2', 0
-
-
-# Exogenous States
-proc_one_coef_A = 0.7
-proc_one_coef_B = 1.3
-
-
-def es3p1(_g, step, sL, s, _input):
-    return 's3', _g['gamma']
-# @curried
-def es4p2(_g, step, sL, s, _input):
-    return 's4', _g['gamma']
-
-ts_format = '%Y-%m-%d %H:%M:%S'
-t_delta = timedelta(days=0, minutes=0, seconds=1)
-def es5p2(_g, step, sL, s, _input):
-    y = 'timestep'
-    x = ep_time_step(s, dt_str=s['timestep'], fromat_str=ts_format, _timedelta=t_delta)
-    return (y, x)
-
-
-# Environment States
-# @curried
-# def env_a(param, x):
-#     return x + param
-def env_a(x):
-    return x
-def env_b(x):
-    return 10
-
-
-# Genesis States
-genesis_states = {
-    's1': Decimal(0.0),
-    's2': Decimal(0.0),
-    's3': Decimal(1.0),
-    's4': Decimal(1.0),
-#     'timestep': '2018-10-01 15:16:24'
-}
-
-
-# remove `exo_update_per_ts` to update every ts
-raw_exogenous_states = {
-    "s3": es3p1,
-    "s4": es4p2,
-#     "timestep": es5p2
-}
-
-
-# ToDo: make env proc trigger field agnostic
-# ToDo: input json into function renaming __name__
-triggered_env_b = env_proc_trigger(1, env_b)
-env_processes = {
-    "s3": env_a, #sweep(beta, env_a),
-    "s4": triggered_env_b #rename('parameterized', triggered_env_b) #sweep(beta, triggered_env_b)
-}
-# parameterized_env_processes = parameterize_states(env_processes)
-#
-# pp.pprint(parameterized_env_processes)
-# exit()
-
-# ToDo: The number of values entered in sweep should be the # of config objs created,
-# not dependent on the # of times the sweep is applied
-# sweep exo_state func and point to exo-state in every other funtion
-# param sweep on genesis states
-
-partial_state_update_block = {
-    "m1": {
-        "policies": {
-            "b1": p1m1,
-            "b2": p2m1
-        },
-        "variables": {
-            "s1": s1m1,
-            "s2": s2m1
-        }
-    },
-    "m2": {
-        "policies": {
-            "b1": p1m2,
-            "b2": p2m2,
-        },
-        "variables": {
-            "s1": s1m2,
-            "s2": s2m2
-        }
-    },
-    "m3": {
-        "policies": {
-            "b1": p1m3,
-            "b2": p2m3
-        },
-        "variables": {
-            "s1": s1m3,
-            "s2": s2m3
-        }
-    }
-}
-
-# config_sim Necessary
-sim_config = config_sim(
-    {
-        "N": 2,
-        "T": range(5),
-        "M": g # Optional
-    }
-)
-
-# New Convention
-append_configs(
-    sim_configs=sim_config,
-    initial_state=genesis_states,
-    seeds=seeds,
-    raw_exogenous_states=raw_exogenous_states,
-    env_processes=env_processes,
-    partial_state_update_blocks=partial_state_update_block
-)
\ No newline at end of file
diff --git a/simulations/validation/write_simulation.py b/simulations/validation/write_simulation.py
deleted file mode 100644
index 859ec225..00000000
--- a/simulations/validation/write_simulation.py
+++ /dev/null
@@ -1,118 +0,0 @@
-from decimal import Decimal
-
-import numpy as np
-
-from cadCAD.configuration import append_configs
-from cadCAD.configuration.utils import bound_norm_random, config_sim
-
-seeds = {
-    'z': np.random.RandomState(1),
-    'a': np.random.RandomState(2),
-    'b': np.random.RandomState(3),
-    'c': np.random.RandomState(3)
-}
-
-
-# Policies per Mechanism
-def p1(_g, step, sL, s):
-    return {'param1': 10}
-def p2(_g, step, sL, s):
-    return {'param1': 10, 'param2': 40}
-
-
-# Internal States per Mechanism
-def s1(_g, step, sL, s, _input):
-    y = 'ds1'
-    x = s['ds1'] + 1
-    return (y, x)
-def s2(_g, step, sL, s, _input):
-    y = 'ds2'
-    x = _input['param2']
-    return (y, x)
-
-
-# Exogenous States
-proc_one_coef_A = 0.7
-proc_one_coef_B = 1.3
-
-def es(_g, step, sL, s, _input):
-    y = 'ds3'
-    x = s['ds3'] * bound_norm_random(seeds['a'], proc_one_coef_A, proc_one_coef_B)
-    return (y, x)
-
-
-# Environment States
-def env_a(x):
-    return 5
-def env_b(x):
-    return 10
-
-
-# Genesis States
-genesis_states = {
-    'ds1': Decimal(0.0),
-    'ds2': Decimal(0.0),
-    'ds3': Decimal(1.0)
-}
-
-
-raw_exogenous_states = {
-    "ds3": es
-}
-
-
-env_processes = {
-    "ds3": env_a
-}
-
-
-partial_state_update_block = {
-    "m1": {
-        "policies": {
-            "p1": p1,
-            "p2": p2
-        },
-        "variables": {
-            "ds1": s1,
-            "ds2": s2
-        }
-    },
-    "m2": {
-        "policies": {
-            "p1": p1,
-            "p2": p2
-        },
-        "variables": {
-            "ds1": s1,
-            "ds2": s2
-        }
-    },
-    "m3": {
-        "policies": {
-            "p1": p1,
-            "p2": p2
-        },
-        "variables": {
-            "ds1": s1,
-            "ds2": s2
-        }
-    }
-}
-
-
-sim_config = config_sim(
-    {
-        "N": 2,
-        "T": range(4),
-    }
-)
-
-append_configs(
-    sim_configs=sim_config,
-    initial_state=genesis_states,
-    seeds=seeds,
-    raw_exogenous_states=raw_exogenous_states,
-    env_processes=env_processes,
-    partial_state_update_blocks=partial_state_update_block,
-    policy_ops=[lambda a, b: a + b]
-)
\ No newline at end of file


Test patch:
diff --git a/simulations/regression_tests/__init__.py b/simulations/regression_tests/__init__.py
index e69de29b..666406b4 100644
--- a/simulations/regression_tests/__init__.py
+++ b/simulations/regression_tests/__init__.py
@@ -0,0 +1,1 @@
+from simulations.regression_tests import experiments
\ No newline at end of file
diff --git a/simulations/regression_tests/execs/config1_test.py b/simulations/regression_tests/execs/config1_test.py
index afa298b0..6a83ec1d 100644
--- a/simulations/regression_tests/execs/config1_test.py
+++ b/simulations/regression_tests/execs/config1_test.py
@@ -4,12 +4,11 @@
 
 from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
 from simulations.regression_tests.models import config1
-from cadCAD import configs
 
 exec_mode = ExecutionMode()
 
 local_proc_ctx = ExecutionContext(context=exec_mode.local_mode)
-run = Executor(exec_context=local_proc_ctx, configs=configs)
+run = Executor(exec_context=local_proc_ctx, configs=config1.exp.configs)
 
 raw_result, tensor_fields, sessions = run.execute()
 result = pd.DataFrame(raw_result)
diff --git a/simulations/regression_tests/execs/config2_test.py b/simulations/regression_tests/execs/config2_test.py
index b63d6556..4cab5bbd 100644
--- a/simulations/regression_tests/execs/config2_test.py
+++ b/simulations/regression_tests/execs/config2_test.py
@@ -4,12 +4,11 @@
 
 from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
 from simulations.regression_tests.models import config2
-from cadCAD import configs
 
 exec_mode = ExecutionMode()
 
 local_proc_ctx = ExecutionContext(context=exec_mode.local_mode)
-run = Executor(exec_context=local_proc_ctx, configs=configs)
+run = Executor(exec_context=local_proc_ctx, configs=config2.exp.configs)
 
 raw_result, tensor_fields, sessions = run.execute()
 result = pd.DataFrame(raw_result)
diff --git a/simulations/regression_tests/execs/external_ds_test.py b/simulations/regression_tests/execs/external_ds_test.py
deleted file mode 100644
index 155cdedd..00000000
--- a/simulations/regression_tests/execs/external_ds_test.py
+++ /dev/null
@@ -1,19 +0,0 @@
-from pprint import pprint
-import pandas as pd
-from tabulate import tabulate
-
-from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-
-from simulations.regression_tests.models import external_dataset
-from cadCAD import configs
-
-exec_mode = ExecutionMode()
-
-local_proc_ctx = ExecutionContext(context=exec_mode.local_mode)
-run = Executor(exec_context=local_proc_ctx, configs=configs)
-
-raw_result, tensor_fields, sessions = run.execute()
-result = pd.DataFrame(raw_result)
-print(tabulate(tensor_fields[0], headers='keys', tablefmt='psql'))
-pprint(sessions)
-print(tabulate(result, headers='keys', tablefmt='psql'))
diff --git a/simulations/regression_tests/execs/historical_state_access_test.py b/simulations/regression_tests/execs/historical_state_access_test.py
deleted file mode 100644
index 0aa8daba..00000000
--- a/simulations/regression_tests/execs/historical_state_access_test.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from pprint import pprint
-import pandas as pd
-from tabulate import tabulate
-
-from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-from simulations.regression_tests.models import historical_state_access
-from cadCAD import configs
-
-exec_mode = ExecutionMode()
-
-local_proc_ctx = ExecutionContext(context=exec_mode.local_mode)
-run = Executor(exec_context=local_proc_ctx, configs=configs)
-
-raw_result, tensor_fields, sessions = run.execute()
-result = pd.DataFrame(raw_result)
-print(tabulate(tensor_fields[0], headers='keys', tablefmt='psql'))
-pprint(sessions)
-print(tabulate(result, headers='keys', tablefmt='psql'))
diff --git a/simulations/regression_tests/execs/multi_config_dist.py b/simulations/regression_tests/execs/multi_config_dist.py
deleted file mode 100644
index 4c8f7661..00000000
--- a/simulations/regression_tests/execs/multi_config_dist.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from pyspark.sql import DataFrame, Row
-from tabulate import tabulate
-from pprint import pprint
-import pandas as pd
-
-from simulations.regression_tests.models import config1, config2
-from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-from cadCAD.configuration.utils import configs_as_dataframe #, configs_as_objs, configs_as_dicts
-from cadCAD.utils.sys_exec import to_spark_df, to_pandas_df
-from cadCAD import configs
-
-from distroduce.engine.execution import transform, distributed_simulations
-from distroduce.session import sc_alt as sc
-
-exec_mode = ExecutionMode()
-distributed_sims = distributed_simulations(transform(publish=False))
-
-distributed_ctx = ExecutionContext(context=exec_mode.distributed, method=distributed_sims)
-run = Executor(exec_context=distributed_ctx, configs=configs, spark_context=sc)
-
-raw_result, tensor_fields, sessions = run.execute()
-
-print(tabulate(tensor_fields[0], headers='keys', tablefmt='psql'))
-print()
-# pprint(sessions)
-# print()
-
-print("Configuration Data:")
-configs_df = configs_as_dataframe(configs)
-print(tabulate(configs_df, headers='keys', tablefmt='psql'))
-print("Tensor Field:")
-print(tabulate(tensor_fields[0], headers='keys', tablefmt='psql'))
-print("Output:")
-
-# RDD:
-print()
-print("RDD:")
-result: list = raw_result.take(5)
-pprint(result[:2])
-# to get all results execute the following
-# result: list = raw_result.collect()
-print()
-
-print("Spark DataFrame:")
-sdf: DataFrame = to_spark_df(raw_result)
-# sdf: DataFrame = to_spark_df(raw_result, spark)
-sdf.show(5)
-print()
-
-# Pandas:
-print()
-print("Pandas DataFrame:")
-# pdf: pd.DataFrame = to_pandas_df(raw_result, config1.genesis_states)
-pdf: pd.DataFrame = to_pandas_df(raw_result)
-print(tabulate(pdf.head(), headers='keys', tablefmt='psql'))
diff --git a/simulations/regression_tests/execs/multi_config_test.py b/simulations/regression_tests/execs/multi_config_test.py
index 919226f7..98979bd6 100644
--- a/simulations/regression_tests/execs/multi_config_test.py
+++ b/simulations/regression_tests/execs/multi_config_test.py
@@ -3,16 +3,16 @@
 import pandas as pd
 
 from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-from simulations.regression_tests.models import config1, config2
-from cadCAD import configs
+from simulations.regression_tests.experiments import multi_exp
+from simulations.regression_tests.models import config_multi_1, config_multi_2
 
 exec_mode = ExecutionMode()
 
 local_proc_ctx = ExecutionContext(context=exec_mode.local_mode)
-run = Executor(exec_context=local_proc_ctx, configs=configs)
+run = Executor(exec_context=local_proc_ctx, configs=multi_exp.configs)
 
 raw_result, tensor_fields, sessions = run.execute()
 result = pd.DataFrame(raw_result)
 print(tabulate(tensor_fields[0], headers='keys', tablefmt='psql'))
-pprint(sessions)
-print(tabulate(result, headers='keys', tablefmt='psql'))
+# pprint(sessions)
+print(tabulate(result, headers='keys', tablefmt='psql'))
\ No newline at end of file
diff --git a/simulations/regression_tests/execs/param_sweep_dist.py b/simulations/regression_tests/execs/param_sweep_dist.py
deleted file mode 100644
index 1defd15e..00000000
--- a/simulations/regression_tests/execs/param_sweep_dist.py
+++ /dev/null
@@ -1,54 +0,0 @@
-from pyspark.sql import DataFrame
-from tabulate import tabulate
-from cadCAD import configs
-from pprint import pprint
-import pandas as pd
-
-from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-from simulations.regression_tests.models import sweep_config
-from cadCAD.utils.sys_exec import to_spark_df, to_pandas_df
-from cadCAD.configuration.utils import configs_as_dataframe #, configs_as_objs, configs_as_dicts
-
-from distroduce.engine.execution import transform, distributed_simulations
-from distroduce.session import sc_alt as sc
-from distroduce.session import spark_alt as spark
-
-
-exec_mode = ExecutionMode()
-distributed_sims = distributed_simulations(transform(publish=False))
-distributed_ctx = ExecutionContext(context=exec_mode.distributed, method=distributed_sims)
-run = Executor(exec_context=distributed_ctx, configs=configs, spark_context=sc)
-
-raw_result, tensor_fields, sessions = run.execute()
-
-print(tabulate(tensor_fields[0], headers='keys', tablefmt='psql'))
-pprint(sessions)
-
-print("Configuration Data:")
-configs_df = configs_as_dataframe(configs)
-print(tabulate(configs_df, headers='keys', tablefmt='psql'))
-print("Tensor Field:")
-print(tabulate(tensor_fields[0], headers='keys', tablefmt='psql'))
-print("Output:")
-
-# RDD:
-print()
-print("RDD:")
-result: list = raw_result.take(5)
-pprint(result[:2])
-# to get all results execute the following
-# result: list = raw_result.collect()
-print()
-
-print("Spark DataFrame:")
-sdf: DataFrame = to_spark_df(raw_result, spark, sweep_config.genesis_states)
-# sdf: DataFrame = to_spark_df(raw_result, spark)
-sdf.show(5)
-print()
-
-# Pandas :
-print()
-print("Pandas DataFrame:")
-pdf: pd.DataFrame = to_pandas_df(raw_result, sweep_config.genesis_states)
-# pdf: pd.DataFrame = to_pandas_df(raw_result)
-print(tabulate(pdf.head(), headers='keys', tablefmt='psql'))
diff --git a/simulations/regression_tests/execs/param_sweep_test.py b/simulations/regression_tests/execs/param_sweep_test.py
index 2da66d1b..9488830a 100644
--- a/simulations/regression_tests/execs/param_sweep_test.py
+++ b/simulations/regression_tests/execs/param_sweep_test.py
@@ -4,15 +4,14 @@
 from tabulate import tabulate
 from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
 from simulations.regression_tests.models import sweep_config
-from cadCAD import configs
 
 exec_mode = ExecutionMode()
 
 local_proc_ctx = ExecutionContext(context=exec_mode.local_mode)
-run = Executor(exec_context=local_proc_ctx, configs=configs)
+run = Executor(exec_context=local_proc_ctx, configs=sweep_config.exp.configs)
 
 raw_result, tensor_fields, sessions = run.execute()
 result = pd.DataFrame(raw_result)
 print(tabulate(tensor_fields[0], headers='keys', tablefmt='psql'))
-pprint(sessions)
+# pprint(sessions)
 print(tabulate(result, headers='keys', tablefmt='psql'))
diff --git a/simulations/regression_tests/execs/policy_agg_dist.py b/simulations/regression_tests/execs/policy_agg_dist.py
deleted file mode 100644
index dd322867..00000000
--- a/simulations/regression_tests/execs/policy_agg_dist.py
+++ /dev/null
@@ -1,54 +0,0 @@
-from pyspark.sql import DataFrame
-from tabulate import tabulate
-from cadCAD import configs
-from pprint import pprint
-import pandas as pd
-
-from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-from simulations.regression_tests.models import policy_aggregation
-from cadCAD.utils.sys_exec import to_spark_df, to_pandas_df
-from cadCAD.configuration.utils import configs_as_dataframe #, configs_as_objs, configs_as_dicts
-
-from distroduce.engine.execution import transform, distributed_simulations
-from distroduce.session import sc_alt as sc
-from distroduce.session import spark_alt as spark
-
-exec_mode = ExecutionMode()
-distributed_sims = distributed_simulations(transform(publish=False))
-
-distributed_ctx = ExecutionContext(context=exec_mode.distributed, method=distributed_sims)
-run = Executor(exec_context=distributed_ctx, configs=configs, spark_context=sc)
-
-raw_result, tensor_fields, sessions = run.execute()
-print(tabulate(tensor_fields[0], headers='keys', tablefmt='psql'))
-pprint(sessions)
-
-print("Configuration Data:")
-configs_df = configs_as_dataframe(configs)
-print(tabulate(configs_df, headers='keys', tablefmt='psql'))
-print("Tensor Field:")
-print(tabulate(tensor_fields[0], headers='keys', tablefmt='psql'))
-print("Output:")
-
-# RDD:
-print()
-print("RDD:")
-result: list = raw_result.take(5)
-pprint(result[:2])
-# to get all results execute the following
-# result: list = raw_result.collect()
-print()
-
-print("Spark DataFrame:")
-sdf: DataFrame = to_spark_df(raw_result, spark, policy_aggregation.genesis_states)
-# sdf: DataFrame = to_spark_df(raw_result, spark)
-sdf.show(5)
-print()
-
-# Pandas :
-print()
-print("Pandas DataFrame:")
-pdf: pd.DataFrame = to_pandas_df(raw_result, policy_aggregation.genesis_states)
-# pdf: pd.DataFrame = to_pandas_df(raw_result)
-print(tabulate(pdf.head(), headers='keys', tablefmt='psql'))
-
diff --git a/simulations/regression_tests/execs/policy_agg_test.py b/simulations/regression_tests/execs/policy_agg_test.py
index 72e87aea..df0aec4b 100644
--- a/simulations/regression_tests/execs/policy_agg_test.py
+++ b/simulations/regression_tests/execs/policy_agg_test.py
@@ -5,12 +5,11 @@
 
 from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
 from simulations.regression_tests.models import policy_aggregation
-from cadCAD import configs
 
 exec_mode = ExecutionMode()
 
 local_proc_ctx = ExecutionContext(context=exec_mode.local_mode)
-run = Executor(exec_context=local_proc_ctx, configs=configs)
+run = Executor(exec_context=local_proc_ctx, configs=policy_aggregation.exp.configs)
 
 raw_result, tensor_fields, sessions = run.execute()
 result = pd.DataFrame(raw_result)
diff --git a/simulations/regression_tests/execs/udo_test.py b/simulations/regression_tests/execs/udo_test.py
deleted file mode 100644
index 7cd46659..00000000
--- a/simulations/regression_tests/execs/udo_test.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from pprint import pprint
-import pandas as pd
-from tabulate import tabulate
-
-from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-from simulations.regression_tests.models import udo
-from cadCAD import configs
-
-exec_mode = ExecutionMode()
-
-local_proc_ctx = ExecutionContext(context=exec_mode.local_mode)
-run = Executor(exec_context=local_proc_ctx, configs=configs)
-
-raw_result, tensor_fields, sessions = run.execute()
-# cols = configs[0].initial_state.keys()
-cols = [
-    'increment',
-    'state_udo_tracker', 'state_udo', 'state_udo_perception_tracker',
-    'udo_policies', 'udo_policy_tracker',
-    'timestamp'
-]
-
-
-result = pd.DataFrame(raw_result)[['run', 'substep', 'timestep'] + cols]
-print(tabulate(tensor_fields[0], headers='keys', tablefmt='psql'))
-pprint(sessions)
-print(tabulate(result, headers='keys', tablefmt='psql'))
diff --git a/simulations/regression_tests/execs/udo_update_test.py b/simulations/regression_tests/execs/udo_update_test.py
deleted file mode 100644
index 2458aad5..00000000
--- a/simulations/regression_tests/execs/udo_update_test.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from pprint import pprint
-import pandas as pd
-from tabulate import tabulate
-
-from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-from simulations.regression_tests.models import udo_inter_substep_update
-from cadCAD import configs
-
-exec_mode = ExecutionMode()
-
-local_proc_ctx = ExecutionContext(context=exec_mode.local_mode)
-run = Executor(exec_context=local_proc_ctx, configs=configs)
-
-raw_result, tensor_fields, sessions = run.execute()
-# cols = configs[0].initial_state.keys()
-cols = [
-    'increment',
-    'state_udo_tracker_a', 'state_udo', 'state_udo_perception_tracker', 'state_udo_tracker_b',
-    'udo_policy_tracker_a', 'udo_policies', 'udo_policy_tracker_b',
-    'timestamp'
-]
-result = pd.DataFrame(raw_result)[['run', 'substep', 'timestep'] + cols]
-print(tabulate(tensor_fields[0], headers='keys', tablefmt='psql'))
-pprint(sessions)
-print(tabulate(result, headers='keys', tablefmt='psql'))
diff --git a/simulations/regression_tests/experiments.py b/simulations/regression_tests/experiments.py
new file mode 100644
index 00000000..87e7a234
--- /dev/null
+++ b/simulations/regression_tests/experiments.py
@@ -0,0 +1,3 @@
+from cadCAD.configuration import Experiment
+
+multi_exp = Experiment()
\ No newline at end of file
diff --git a/simulations/regression_tests/experiments/__init__.py b/simulations/regression_tests/experiments/__init__.py
deleted file mode 100644
index 6d4cf830..00000000
--- a/simulations/regression_tests/experiments/__init__.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from cadCAD.configuration import Experiment
-
-config1_exp = Experiment()
-config2_exp = Experiment()
-ext_ds_exp = Experiment()
-hist_exp = Experiment()
-policy_exp = Experiment()
-sweep_exp = Experiment()
-udo1_exp = Experiment()
-udo2_exp = Experiment()
diff --git a/simulations/regression_tests/models/config1.py b/simulations/regression_tests/models/config1.py
index 80ddc607..f27fddaf 100644
--- a/simulations/regression_tests/models/config1.py
+++ b/simulations/regression_tests/models/config1.py
@@ -1,9 +1,8 @@
 import numpy as np
 from datetime import timedelta
 
-from cadCAD import configs
+from cadCAD.configuration import Experiment
 from cadCAD.configuration.utils import bound_norm_random, config_sim, time_step, env_trigger
-from simulations.regression_tests.experiments import config1_exp
 
 seeds = {
     'z': np.random.RandomState(1),
@@ -147,9 +146,10 @@ def update_timestamp(_g, step, sL, s, _input, **kwargs):
     }
 
 sim_config = config_sim(sim_config_dict)
-config1_exp.append_configs(
-    config_list=configs,
-    user_id='user_a',
+
+exp = Experiment()
+exp.append_model(
+    model_id='sys_model_1',
     sim_configs=sim_config,
     initial_state=genesis_states,
     env_processes=env_processes,
diff --git a/simulations/regression_tests/models/config2.py b/simulations/regression_tests/models/config2.py
index 392734c4..88a8aa4e 100644
--- a/simulations/regression_tests/models/config2.py
+++ b/simulations/regression_tests/models/config2.py
@@ -1,9 +1,8 @@
 import numpy as np
 from datetime import timedelta
 
-from cadCAD import configs
+from cadCAD.configuration import Experiment
 from cadCAD.configuration.utils import bound_norm_random, config_sim, env_trigger, time_step
-from simulations.regression_tests.experiments import config2_exp
 
 seeds = {
     'z': np.random.RandomState(1),
@@ -138,9 +137,10 @@ def update_timestamp(_g, step, sL, s, _input, **kwargs):
 
 
 sim_config = config_sim(sim_config_dict)
-config2_exp.append_configs(
-    config_list=configs,
-    user_id='user_b',
+
+exp = Experiment()
+exp.append_model(
+    model_id='sys_model_1',
     sim_configs=sim_config,
     initial_state=genesis_states,
     env_processes=env_processes,
diff --git a/simulations/regression_tests/models/config_multi_1.py b/simulations/regression_tests/models/config_multi_1.py
new file mode 100644
index 00000000..03274368
--- /dev/null
+++ b/simulations/regression_tests/models/config_multi_1.py
@@ -0,0 +1,172 @@
+from copy import deepcopy
+from pprint import pprint
+
+import numpy as np
+from datetime import timedelta
+
+# from cadCAD import configs
+from cadCAD.configuration import Experiment
+from cadCAD.configuration.utils import bound_norm_random, config_sim, time_step, env_trigger
+from simulations.regression_tests.experiments import multi_exp
+
+seeds = {
+    'z': np.random.RandomState(1),
+    'a': np.random.RandomState(2),
+    'b': np.random.RandomState(3),
+    'c': np.random.RandomState(4)
+}
+
+
+# Policies per Mechanism
+def p1m1(_g, step, sL, s, **kwargs):
+    return {'param1': 1}
+def p2m1(_g, step, sL, s, **kwargs):
+    return {'param1': 1, 'param2': 4}
+
+def p1m2(_g, step, sL, s, **kwargs):
+    return {'param1': 'a', 'param2': 2}
+def p2m2(_g, step, sL, s, **kwargs):
+    return {'param1': 'b', 'param2': 4}
+
+def p1m3(_g, step, sL, s, **kwargs):
+    return {'param1': ['c'], 'param2': np.array([10, 100])}
+def p2m3(_g, step, sL, s, **kwargs):
+    return {'param1': ['d'], 'param2': np.array([20, 200])}
+
+
+# Internal States per Mechanism
+def s1m1(_g, step, sL, s, _input, **kwargs):
+    y = 's1'
+    x = s['s1'] + 1
+    return (y, x)
+def s2m1(_g, step, sL, s, _input, **kwargs):
+    y = 's2'
+    x = _input['param2']
+    return (y, x)
+
+def s1m2(_g, step, sL, s, _input, **kwargs):
+    y = 's1'
+    x = s['s1'] + 1
+    return (y, x)
+def s2m2(_g, step, sL, s, _input, **kwargs):
+    y = 's2'
+    x = _input['param2']
+    return (y, x)
+
+def s1m3(_g, step, sL, s, _input, **kwargs):
+    y = 's1'
+    x = s['s1'] + 1
+    return (y, x)
+def s2m3(_g, step, sL, s, _input, **kwargs):
+    y = 's2'
+    x = _input['param2']
+    return (y, x)
+
+def policies(_g, step, sL, s, _input, **kwargs):
+    y = 'policies'
+    x = _input
+    return (y, x)
+
+
+# Exogenous States
+proc_one_coef_A = 0.7
+proc_one_coef_B = 1.3
+
+def es3(_g, step, sL, s, _input, **kwargs):
+    y = 's3'
+    x = s['s3'] * bound_norm_random(seeds['a'], proc_one_coef_A, proc_one_coef_B)
+    return (y, x)
+
+def es4(_g, step, sL, s, _input, **kwargs):
+    y = 's4'
+    x = s['s4'] * bound_norm_random(seeds['b'], proc_one_coef_A, proc_one_coef_B)
+    return (y, x)
+
+def update_timestamp(_g, step, sL, s, _input, **kwargs):
+    y = 'timestamp'
+    return y, time_step(dt_str=s[y], dt_format='%Y-%m-%d %H:%M:%S', _timedelta=timedelta(days=0, minutes=0, seconds=1))
+
+
+# Genesis States
+genesis_states = {
+    's1': 0.0,
+    's2': 0.0,
+    's3': 1.0,
+    's4': 1.0,
+    'timestamp': '2018-10-01 15:16:24'
+}
+
+
+# Environment Process
+trigger_timestamps = ['2018-10-01 15:16:25', '2018-10-01 15:16:27', '2018-10-01 15:16:29']
+env_processes = {
+    "s3": [lambda _g, x: 5],
+    "s4": env_trigger(3)(trigger_field='timestamp', trigger_vals=trigger_timestamps, funct_list=[lambda _g, x: 10])
+}
+
+
+partial_state_update_block = [
+    {
+        "policies": {
+            "b1": p1m1,
+            "b2": p2m1
+        },
+        "variables": {
+            "s1": s1m1,
+            "s2": s2m1,
+            "s3": es3,
+            "s4": es4,
+            "timestamp": update_timestamp
+        }
+    },
+    {
+        "policies": {
+            "b1": p1m2,
+            "b2": p2m2
+        },
+        "variables": {
+            "s1": s1m2,
+            "s2": s2m2,
+            # "s3": es3p1,
+            # "s4": es4p2,
+        }
+    },
+    {
+        "policies": {
+            "b1": p1m3,
+            "b2": p2m3
+        },
+        "variables": {
+            "s1": s1m3,
+            "s2": s2m3,
+            # "s3": es3p1,
+            # "s4": es4p2,
+        }
+    }
+]
+
+sim_config_dict = {
+        "N": 2,
+        "T": range(5)
+    }
+
+sim_config = config_sim(sim_config_dict)
+
+exp = Experiment()
+exp.append_model(
+    user_id='user_a',
+    sim_configs=sim_config,
+    initial_state=genesis_states,
+    env_processes=env_processes,
+    partial_state_update_blocks=partial_state_update_block,
+    policy_ops=[lambda a, b: a + b]
+)
+
+multi_exp.append_model(
+    model_id='sys_model_1',
+    sim_configs=sim_config,
+    initial_state=genesis_states,
+    env_processes=env_processes,
+    partial_state_update_blocks=partial_state_update_block,
+    policy_ops=[lambda a, b: a + b]
+)
diff --git a/simulations/regression_tests/models/config_multi_2.py b/simulations/regression_tests/models/config_multi_2.py
new file mode 100644
index 00000000..e7f15eed
--- /dev/null
+++ b/simulations/regression_tests/models/config_multi_2.py
@@ -0,0 +1,158 @@
+import numpy as np
+from datetime import timedelta
+
+from cadCAD.configuration import Experiment
+from cadCAD.configuration.utils import bound_norm_random, config_sim, env_trigger, time_step
+from simulations.regression_tests.experiments import multi_exp
+
+seeds = {
+    'z': np.random.RandomState(1),
+    'a': np.random.RandomState(2),
+    'b': np.random.RandomState(3),
+    'c': np.random.RandomState(3)
+}
+
+
+# Policies per Mechanism
+def p1m1(_g, step, sL, s, **kwargs):
+    return {'param1': 1}
+def p2m1(_g, step, sL, s, **kwargs):
+    return {'param2': 4}
+
+def p1m2(_g, step, sL, s, **kwargs):
+    return {'param1': 'a', 'param2': 2}
+def p2m2(_g, step, sL, s, **kwargs):
+    return {'param1': 'b', 'param2': 4}
+
+def p1m3(_g, step, sL, s, **kwargs):
+    return {'param1': ['c'], 'param2': np.array([10, 100])}
+def p2m3(_g, step, sL, s, **kwargs):
+    return {'param1': ['d'], 'param2': np.array([20, 200])}
+
+
+# Internal States per Mechanism
+def s1m1(_g, step, sL, s, _input, **kwargs):
+    y = 's1'
+    x = _input['param1']
+    return (y, x)
+def s2m1(_g, step, sL, s, _input, **kwargs):
+    y = 's2'
+    x = _input['param2']
+    return (y, x)
+
+def s1m2(_g, step, sL, s, _input, **kwargs):
+    y = 's1'
+    x = _input['param1']
+    return (y, x)
+def s2m2(_g, step, sL, s, _input, **kwargs):
+    y = 's2'
+    x = _input['param2']
+    return (y, x)
+
+def s1m3(_g, step, sL, s, _input, **kwargs):
+    y = 's1'
+    x = _input['param1']
+    return (y, x)
+def s2m3(_g, step, sL, s, _input, **kwargs):
+    y = 's2'
+    x = _input['param2']
+    return (y, x)
+
+
+# Exogenous States
+proc_one_coef_A = 0.7
+proc_one_coef_B = 1.3
+
+def es3(_g, step, sL, s, _input, **kwargs):
+    y = 's3'
+    x = s['s3'] * bound_norm_random(seeds['a'], proc_one_coef_A, proc_one_coef_B)
+    return (y, x)
+
+def es4(_g, step, sL, s, _input, **kwargs):
+    y = 's4'
+    x = s['s4'] * bound_norm_random(seeds['b'], proc_one_coef_A, proc_one_coef_B)
+    return (y, x)
+
+def update_timestamp(_g, step, sL, s, _input, **kwargs):
+    y = 'timestamp'
+    return y, time_step(dt_str=s[y], dt_format='%Y-%m-%d %H:%M:%S', _timedelta=timedelta(days=0, minutes=0, seconds=1))
+
+
+# Genesis States
+genesis_states = {
+    's1': 0,
+    's2': 0,
+    's3': 1,
+    's4': 1,
+    'timestamp': '2018-10-01 15:16:24'
+}
+
+
+# Environment Process
+trigger_timestamps = ['2018-10-01 15:16:25', '2018-10-01 15:16:27', '2018-10-01 15:16:29']
+env_processes = {
+    "s3": [lambda _g, x: 5],
+    "s4": env_trigger(3)(trigger_field='timestamp', trigger_vals=trigger_timestamps, funct_list=[lambda _g, x: 10])
+}
+
+partial_state_update_block = {
+    "m1": {
+        "policies": {
+            "b1": p1m1,
+            # "b2": p2m1
+        },
+        "states": {
+            "s1": s1m1,
+            # "s2": s2m1
+            "s3": es3,
+            "s4": es4,
+            "timestep": update_timestamp
+        }
+    },
+    "m2": {
+        "policies": {
+            "b1": p1m2,
+            # "b2": p2m2
+        },
+        "states": {
+            "s1": s1m2,
+            # "s2": s2m2
+        }
+    },
+    "m3": {
+        "policies": {
+            "b1": p1m3,
+            "b2": p2m3
+        },
+        "states": {
+            "s1": s1m3,
+            "s2": s2m3
+        }
+    }
+}
+
+sim_config_dict = {
+        "N": 3,
+        "T": range(5),
+}
+
+
+sim_config = config_sim(sim_config_dict)
+
+exp = Experiment()
+exp.append_model(
+    # config_list=configs,
+    user_id='user_b',
+    sim_configs=sim_config,
+    initial_state=genesis_states,
+    env_processes=env_processes,
+    partial_state_update_blocks=partial_state_update_block
+)
+
+multi_exp.append_model(
+    model_id='sys_model_2',
+    sim_configs=sim_config,
+    initial_state=genesis_states,
+    env_processes=env_processes,
+    partial_state_update_blocks=partial_state_update_block
+)
diff --git a/simulations/regression_tests/models/external_dataset.py b/simulations/regression_tests/models/external_dataset.py
deleted file mode 100644
index 4458c084..00000000
--- a/simulations/regression_tests/models/external_dataset.py
+++ /dev/null
@@ -1,66 +0,0 @@
-from cadCAD.configuration.utils import config_sim
-import pandas as pd
-from cadCAD.utils import SilentDF
-from simulations.regression_tests.experiments import ext_ds_exp
-
-df = SilentDF(pd.read_csv('simulations/external_data/output.csv'))
-
-
-def query(s, df):
-    return df[
-            (df['run'] == s['run']) & (df['substep'] == s['substep']) & (df['timestep'] == s['timestep'])
-        ].drop(columns=['run', 'substep', "timestep"])
-
-def p1(_g, substep, sL, s, **kwargs):
-    result_dict = query(s, df).to_dict()
-    del result_dict["ds3"]
-    return {k: list(v.values()).pop() for k, v in result_dict.items()}
-
-def p2(_g, substep, sL, s, **kwargs):
-    result_dict = query(s, df).to_dict()
-    del result_dict["ds1"], result_dict["ds2"]
-    return {k: list(v.values()).pop() for k, v in result_dict.items()}
-
-#integrate_ext_dataset
-def integrate_ext_dataset(_g, step, sL, s, _input, **kwargs):
-    result_dict = query(s, df).to_dict()
-    return 'external_data', {k: list(v.values()).pop() for k, v in result_dict.items()}
-
-def increment(y, incr_by):
-    return lambda _g, step, sL, s, _input, **kwargs: (y, s[y] + incr_by)
-increment = increment('increment', 1)
-
-def view_policies(_g, step, sL, s, _input, **kwargs):
-    return 'policies', _input
-
-
-external_data = {'ds1': None, 'ds2': None, 'ds3': None}
-state_dict = {
-    'increment': 0,
-    'external_data': external_data,
-    'policies': external_data
-}
-
-
-policies = {"p1": p1, "p2": p2}
-states = {'increment': increment, 'external_data': integrate_ext_dataset, 'policies': view_policies}
-PSUB = {'policies': policies, 'states': states}
-
-# needs M1&2 need behaviors
-partial_state_update_blocks = {
-    'PSUB1': PSUB,
-    'PSUB2': PSUB,
-    'PSUB3': PSUB
-}
-
-sim_config = config_sim({
-    "N": 2,
-    "T": range(4)
-})
-
-ext_ds_exp.append_configs(
-    sim_configs=sim_config,
-    initial_state=state_dict,
-    partial_state_update_blocks=partial_state_update_blocks,
-    policy_ops=[lambda a, b: {**a, **b}]
-)
diff --git a/simulations/regression_tests/models/historical_state_access.py b/simulations/regression_tests/models/historical_state_access.py
deleted file mode 100644
index c8c3a604..00000000
--- a/simulations/regression_tests/models/historical_state_access.py
+++ /dev/null
@@ -1,91 +0,0 @@
-from cadCAD.configuration.utils import config_sim, access_block
-from simulations.regression_tests.experiments import hist_exp
-
-policies, variables = {}, {}
-exclusion_list = ['nonexsistant', 'last_x', '2nd_to_last_x', '3rd_to_last_x', '4th_to_last_x']
-
-# Policies per Mechanism
-
-# WARNING: DO NOT delete elements from sH
-# state_history, target_field, psu_block_offset, exculsion_list
-def last_update(_g, substep, sH, s, **kwargs):
-    return {"last_x": access_block(
-            state_history=sH,
-            target_field="last_x",
-            psu_block_offset=-1,
-            exculsion_list=exclusion_list
-        )
-    }
-policies["last_x"] = last_update
-
-def second2last_update(_g, substep, sH, s, **kwargs):
-    return {"2nd_to_last_x": access_block(sH, "2nd_to_last_x", -2, exclusion_list)}
-policies["2nd_to_last_x"] = second2last_update
-
-
-# Internal States per Mechanism
-
-# WARNING: DO NOT delete elements from sH
-def add(y, x):
-    return lambda _g, substep, sH, s, _input, **kwargs: (y, s[y] + x)
-variables['x'] = add('x', 1)
-
-# last_partial_state_update_block
-def nonexsistant(_g, substep, sH, s, _input, **kwargs):
-    return 'nonexsistant', access_block(sH, "nonexsistant", 0, exclusion_list)
-variables['nonexsistant'] = nonexsistant
-
-# last_partial_state_update_block
-def last_x(_g, substep, sH, s, _input, **kwargs):
-    return 'last_x', _input["last_x"]
-variables['last_x'] = last_x
-
-# 2nd to last partial state update block
-def second_to_last_x(_g, substep, sH, s, _input, **kwargs):
-    return '2nd_to_last_x', _input["2nd_to_last_x"]
-variables['2nd_to_last_x'] = second_to_last_x
-
-# 3rd to last partial state update block
-def third_to_last_x(_g, substep, sH, s, _input, **kwargs):
-    return '3rd_to_last_x', access_block(sH, "3rd_to_last_x", -3, exclusion_list)
-variables['3rd_to_last_x'] = third_to_last_x
-
-# 4th to last partial state update block
-def fourth_to_last_x(_g, substep, sH, s, _input, **kwargs):
-    return '4th_to_last_x', access_block(sH, "4th_to_last_x", -4, exclusion_list)
-variables['4th_to_last_x'] = fourth_to_last_x
-
-
-genesis_states = {
-    'x': 0,
-    'nonexsistant': [],
-    'last_x': [],
-    '2nd_to_last_x': [],
-    '3rd_to_last_x': [],
-    '4th_to_last_x': []
-}
-
-PSUB = {
-    "policies": policies,
-    "variables": variables
-}
-
-partial_state_update_block = {
-    "PSUB1": PSUB,
-    "PSUB2": PSUB,
-    "PSUB3": PSUB
-}
-
-sim_config = config_sim(
-    {
-        "N": 2,
-        "T": range(3),
-    }
-)
-
-
-hist_exp.append_configs(
-    sim_configs=sim_config,
-    initial_state=genesis_states,
-    partial_state_update_blocks=partial_state_update_block
-)
diff --git a/simulations/regression_tests/models/param_sweep.py b/simulations/regression_tests/models/param_sweep.py
index ed64e142..65e3977d 100644
--- a/simulations/regression_tests/models/param_sweep.py
+++ b/simulations/regression_tests/models/param_sweep.py
@@ -1,9 +1,9 @@
 import pprint
-from typing import Dict, List
+from typing import Dict, List, Any
 
 # from cadCAD.configuration import append_configs
+from cadCAD.configuration import Experiment
 from cadCAD.configuration.utils import env_trigger, var_substep_trigger, config_sim, psub_list
-from testing.experiments import exp_param_sweep
 
 pp = pprint.PrettyPrinter(indent=4)
 
@@ -12,12 +12,10 @@ def some_function(x):
 
 # Optional
 # dict must contain lists opf 2 distinct lengths
-g: Dict[str, List[int]] = {
+g: Dict[str, List[Any]] = {
     'alpha': [1],
     'beta': [2, some_function],
     'gamma': [3, 4],
-    # 'beta': [1],
-    # 'gamma': [4],
     'omega': [7]
 }
 
@@ -84,9 +82,11 @@ def sweeped(_g, step, sL, s, _input, **kwargs):
     }
 )
 
-# New Convention
 partial_state_update_blocks = psub_list(psu_block, psu_steps)
-exp_param_sweep.append_configs(
+
+exp = Experiment()
+exp.append_model(
+    model_id='sys_model_1',
     sim_configs=sim_config,
     initial_state=genesis_states,
     env_processes=env_process,
diff --git a/simulations/regression_tests/models/policy_aggregation.py b/simulations/regression_tests/models/policy_aggregation.py
index d499175f..7621f7da 100644
--- a/simulations/regression_tests/models/policy_aggregation.py
+++ b/simulations/regression_tests/models/policy_aggregation.py
@@ -1,8 +1,7 @@
+from cadCAD.configuration import Experiment
 from cadCAD.configuration.utils import config_sim
 
 # Policies per Mechanism
-from simulations.regression_tests.experiments import policy_exp
-
 def p1m1(_g, step, sL, s, **kwargs):
     return {'policy1': 1}
 def p2m1(_g, step, sL, s, **kwargs):
@@ -74,7 +73,9 @@ def policies(_g, step, sH, s, _input, **kwargs):
 
 # Aggregation == Reduce Map / Reduce Map Aggregation
 # using env functions (include in reg test using / for env proc)
-policy_exp.append_configs(
+exp = Experiment()
+exp.append_model(
+    model_id='sys_model_1',
     sim_configs=sim_config,
     initial_state=genesis_states,
     partial_state_update_blocks=partial_state_update_block,
diff --git a/simulations/regression_tests/models/sweep_config.py b/simulations/regression_tests/models/sweep_config.py
index a92205c9..ec5b098f 100644
--- a/simulations/regression_tests/models/sweep_config.py
+++ b/simulations/regression_tests/models/sweep_config.py
@@ -1,28 +1,19 @@
-import numpy as np
-from datetime import timedelta
-import pprint
+from pprint import pprint
+from typing import Dict, List, Any
 
-from cadCAD.configuration.utils import env_trigger, var_substep_trigger, config_sim, time_step, psub_list
+# from cadCAD.configuration import append_configs
+from cadCAD.configuration import Experiment
+from cadCAD.configuration.utils import env_trigger, var_substep_trigger, config_sim, psub_list
 
-from typing import Dict, List
 
-from simulations.regression_tests.experiments import sweep_exp
-
-pp = pprint.PrettyPrinter(indent=4)
-
-seeds = {
-    'z': np.random.RandomState(1),
-    'a': np.random.RandomState(2),
-    'b': np.random.RandomState(3),
-    'c': np.random.RandomState(3)
-}
+def some_function(x):
+    return x
 
 # Optional
-g: Dict[str, List[int]] = {
+# dict must contain lists opf 2 distinct lengths
+g: Dict[str, List[Any]] = {
     'alpha': [1],
-    # 'beta': [2],
-    # 'gamma': [3],
-    'beta': [2, 5],
+    'beta': [2, some_function],
     'gamma': [3, 4],
     'omega': [7]
 }
@@ -32,123 +23,71 @@
 var_timestep_trigger = var_substep_trigger([0, system_substeps])
 env_timestep_trigger = env_trigger(system_substeps)
 env_process = {}
-psu_block = {k: {"policies": {}, "variables": {}} for k in psu_steps}
+
 
 # ['s1', 's2', 's3', 's4']
 # Policies per Mechanism
-def p1m1(_g, step, sL, s, **kwargs):
-    return {'param1': 1}
-psu_block['m1']['policies']['p1'] = p1m1
+def gamma(_g, step, sL, s, **kwargs):
+    return {'gamma': _g['gamma']}
 
-def p2m1(_g, step, sL, s, **kwargs):
-    return {'param2': 4}
-psu_block['m1']['policies']['p2'] = p2m1
 
-def p1m2(_g, step, sL, s, **kwargs):
-    return {'param1': 'a', 'param2': _g['beta']}
-psu_block['m2']['policies']['p1'] = p1m2
+def omega(_g, step, sL, s, **kwargs):
+    return {'omega': _g['omega']}
 
-def p2m2(_g, step, sL, s, **kwargs):
-    return {'param1': 'b', 'param2': 0}
-psu_block['m2']['policies']['p2'] = p2m2
 
-def p1m3(_g, step, sL, s, **kwargs):
-    return {'param1': np.array([10, 100])}
-psu_block['m3']['policies']['p1'] = p1m3
+# Internal States per Mechanism
+def alpha(_g, step, sL, s, _input, **kwargs):
+    return 'alpha', _g['alpha']
 
-def p2m3(_g, step, sL, s, **kwargs):
-    return {'param1': np.array([20, 200])}
-psu_block['m3']['policies']['p2'] = p2m3
 
+def beta(_g, step, sL, s, _input, **kwargs):
+    return 'beta', _g['beta']
+
+
+def policies(_g, step, sL, s, _input, **kwargs):
+    return 'policies', _input
+
+
+def sweeped(_g, step, sL, s, _input, **kwargs):
+    return 'sweeped', {'beta': _g['beta'], 'gamma': _g['gamma']}
+
+psu_block = {k: {"policies": {}, "variables": {}} for k in psu_steps}
+for m in psu_steps:
+    psu_block[m]['policies']['gamma'] = gamma
+    psu_block[m]['policies']['omega'] = omega
+    psu_block[m]["variables"]['alpha'] = alpha
+    psu_block[m]["variables"]['beta'] = beta
+    psu_block[m]['variables']['policies'] = policies
+    psu_block[m]["variables"]['sweeped'] = var_timestep_trigger(y='sweeped', f=sweeped)
 
-# Internal States per Mechanism
-def s1m1(_g, step, sL, s, _input, **kwargs):
-    return 's1', 0
-psu_block['m1']["variables"]['s1'] = s1m1
-
-def s2m1(_g, step, sL, s, _input, **kwargs):
-    return 's2', _g['beta']
-psu_block['m1']["variables"]['s2'] = s2m1
-
-def s1m2(_g, step, sL, s, _input, **kwargs):
-    return 's1', _input['param2']
-psu_block['m2']["variables"]['s1'] = s1m2
-
-def s2m2(_g, step, sL, s, _input, **kwargs):
-    return 's2', _input['param2']
-psu_block['m2']["variables"]['s2'] = s2m2
-
-def s1m3(_g, step, sL, s, _input, **kwargs):
-    return 's1', 0
-psu_block['m3']["variables"]['s1'] = s1m3
-
-def s2m3(_g, step, sL, s, _input, **kwargs):
-    return 's2', 0
-psu_block['m3']["variables"]['s2'] = s2m3
-
-
-# Exogenous States
-def update_timestamp(_g, step, sL, s, _input, **kwargs):
-    y = 'timestamp'
-    return y, time_step(dt_str=s[y], dt_format='%Y-%m-%d %H:%M:%S', _timedelta=timedelta(days=0, minutes=0, seconds=1))
-for m in ['m1','m2','m3']:
-    # psu_block[m]["variables"]['timestamp'] = update_timestamp
-    psu_block[m]["variables"]['timestamp'] = var_timestep_trigger(y='timestamp', f=update_timestamp)
-    # psu_block[m]["variables"]['timestamp'] = var_trigger(
-    #     y='timestamp', f=update_timestamp, pre_conditions={'substep': [0, system_substeps]}, cond_op=lambda a, b: a and b
-    # )
-
-proc_one_coef = 0.7
-def es3(_g, step, sL, s, _input, **kwargs):
-    return 's3', s['s3'] + proc_one_coef
-# use `timestep_trigger` to update every ts
-for m in ['m1','m2','m3']:
-    psu_block[m]["variables"]['s3'] = var_timestep_trigger(y='s3', f=es3)
-
-
-def es4(_g, step, sL, s, _input, **kwargs):
-    return 's4', s['s4'] + _g['gamma']
-for m in ['m1','m2','m3']:
-    psu_block[m]["variables"]['s4'] = var_timestep_trigger(y='s4', f=es4)
 
 # Genesis States
 genesis_states = {
-    's1': 0.0,
-    's2': 0.0,
-    's3': 1.0,
-    's4': 1.0,
-    'timestamp': '2018-10-01 15:16:24'
+    'alpha': 0,
+    'beta': 0,
+    'policies': {},
+    'sweeped': {}
 }
 
-
 # Environment Process
-env_process["s3"] = [lambda _g, x: _g['beta'], lambda _g, x: x + 1]
-env_process["s4"] = env_timestep_trigger(trigger_field='timestep', trigger_vals=[5], funct_list=[lambda _g, x: _g['beta']])
+env_process['sweeped'] = env_timestep_trigger(trigger_field='timestep', trigger_vals=[5], funct_list=[lambda _g, x: _g['beta']])
 
 
-# config_sim Necessary
 sim_config = config_sim(
     {
-        "N": 5,
+        "N": 2,
         "T": range(2),
         "M": g, # Optional
     }
 )
 
-# New Convention
 partial_state_update_blocks = psub_list(psu_block, psu_steps)
-sweep_exp.append_configs(
-    # user_id='user_a',
+
+exp = Experiment()
+exp.append_model(
+    model_id='sys_model_1',
     sim_configs=sim_config,
     initial_state=genesis_states,
-    seeds=seeds,
     env_processes=env_process,
     partial_state_update_blocks=partial_state_update_blocks
 )
-
-
-# print()
-# print("Partial State Update Block:")
-# pp.pprint(partial_state_update_blocks)
-# print()
-# print()
diff --git a/simulations/regression_tests/models/udo.py b/simulations/regression_tests/models/udo.py
deleted file mode 100644
index 4fe1b5b5..00000000
--- a/simulations/regression_tests/models/udo.py
+++ /dev/null
@@ -1,175 +0,0 @@
-import pandas as pd
-from datetime import timedelta
-import pprint as pp
-
-from cadCAD.utils import SilentDF
-from cadCAD.configuration.utils import time_step, config_sim, var_trigger, var_substep_trigger, env_trigger, psub_list
-from cadCAD.configuration.utils.userDefinedObject import udoPipe, UDO
-from simulations.regression_tests.experiments import udo1_exp
-
-DF = SilentDF(pd.read_csv('simulations/external_data/output.csv'))
-
-
-class udoExample(object):
-    def __init__(self, x, dataset=None):
-        self.x = x
-        self.mem_id = str(hex(id(self)))
-        self.ds = dataset # for setting ds initially or querying
-        self.perception = {}
-
-    def anon(self, f):
-        return f(self)
-
-    def updateX(self):
-        self.x += 1
-        return self
-
-    def updateDS(self):
-        self.ds.iloc[0,0] -= 10
-        # pp.pprint(self.ds)
-        return self
-
-    def perceive(self, s):
-        self.perception = self.ds[
-            (self.ds['run'] == s['run']) & (self.ds['substep'] == s['substep']) & (self.ds['timestep'] == s['timestep'])
-        ].drop(columns=['run', 'substep']).to_dict()
-        return self
-
-    def read(self, ds_uri):
-        self.ds = SilentDF(pd.read_csv(ds_uri))
-        return self
-
-    def write(self, ds_uri):
-        pd.to_csv(ds_uri)
-
-    pass
-
-
-state_udo = UDO(udo=udoExample(0, DF), masked_members=['obj', 'perception'])
-policy_udoA = UDO(udo=udoExample(0, DF), masked_members=['obj', 'perception'])
-policy_udoB = UDO(udo=udoExample(0, DF), masked_members=['obj', 'perception'])
-
-
-sim_config = config_sim({
-    "N": 3,
-    "T": range(4)
-})
-
-state_dict = {
-    'increment': 0,
-    'state_udo': state_udo, 'state_udo_tracker': 0,
-    'state_udo_perception_tracker': {"ds1": None, "ds2": None, "ds3": None, "timestep": None},
-    'udo_policies': {'udo_A': policy_udoA, 'udo_B': policy_udoB},
-    'udo_policy_tracker': (0, 0),
-    'timestamp': '2019-01-01 00:00:00'
-}
-
-psu_steps = ['m1', 'm2', 'm3']
-system_substeps = len(psu_steps)
-var_timestep_trigger = var_substep_trigger([0, system_substeps])
-env_timestep_trigger = env_trigger(system_substeps)
-psu_block = {k: {"policies": {}, "variables": {}} for k in psu_steps}
-
-def udo_policyA(_g, step, sL, s, **kwargs):
-    s['udo_policies']['udo_A'].updateX()
-    return {'udo_A': udoPipe(s['udo_policies']['udo_A'])}
-# policies['a'] = udo_policyA
-for m in psu_steps:
-    psu_block[m]['policies']['a'] = udo_policyA
-
-def udo_policyB(_g, step, sL, s, **kwargs):
-    s['udo_policies']['udo_B'].updateX()
-    return {'udo_B': udoPipe(s['udo_policies']['udo_B'])}
-# policies['b'] = udo_policyB
-for m in psu_steps:
-    psu_block[m]['policies']['b'] = udo_policyB
-
-
-# policies = {"p1": udo_policyA, "p2": udo_policyB}
-# policies = {"A": udo_policyA, "B": udo_policyB}
-
-def add(y: str, added_val):
-    return lambda _g, step, sL, s, _input, **kwargs: (y, s[y] + added_val)
-# state_updates['increment'] = add('increment', 1)
-for m in psu_steps:
-    psu_block[m]["variables"]['increment'] = add('increment', 1)
-
-
-# @curried
-def perceive(s, self):
-    self.perception = self.ds[
-        (self.ds['run'] == s['run']) & (self.ds['substep'] == s['substep']) & (self.ds['timestep'] == s['timestep'])
-    ].drop(columns=['run', 'substep']).to_dict()
-    return self
-
-
-def state_udo_update(_g, step, sL, s, _input, **kwargs):
-    y = 'state_udo'
-    # s['hydra_state'].updateX().anon(perceive(s))
-    s['state_udo'].updateX().perceive(s).updateDS()
-    x = udoPipe(s['state_udo'])
-    return y, x
-for m in psu_steps:
-    psu_block[m]["variables"]['state_udo'] = state_udo_update
-
-
-def track(destination, source):
-    return lambda _g, step, sL, s, _input, **kwargs: (destination, s[source].x)
-state_udo_tracker = track('state_udo_tracker', 'state_udo')
-for m in psu_steps:
-    psu_block[m]["variables"]['state_udo_tracker'] = state_udo_tracker
-
-
-def track_state_udo_perception(destination, source):
-    def id(past_perception):
-        if len(past_perception) == 0:
-            return state_dict['state_udo_perception_tracker']
-        else:
-            return past_perception
-    return lambda _g, step, sL, s, _input, **kwargs: (destination, id(s[source].perception))
-state_udo_perception_tracker = track_state_udo_perception('state_udo_perception_tracker', 'state_udo')
-for m in psu_steps:
-    psu_block[m]["variables"]['state_udo_perception_tracker'] = state_udo_perception_tracker
-
-
-def view_udo_policy(_g, step, sL, s, _input, **kwargs):
-    return 'udo_policies', _input
-for m in psu_steps:
-    psu_block[m]["variables"]['udo_policies'] = view_udo_policy
-
-
-def track_udo_policy(destination, source):
-    def val_switch(v):
-        if isinstance(v, pd.DataFrame) is True or isinstance(v, SilentDF) is True:
-            return SilentDF(v)
-        else:
-            return v.x
-    return lambda _g, step, sL, s, _input, **kwargs: (destination, tuple(val_switch(v) for _, v in s[source].items()))
-udo_policy_tracker = track_udo_policy('udo_policy_tracker', 'udo_policies')
-for m in psu_steps:
-    psu_block[m]["variables"]['udo_policy_tracker'] = udo_policy_tracker
-
-
-def update_timestamp(_g, step, sL, s, _input, **kwargs):
-    y = 'timestamp'
-    return y, time_step(dt_str=s[y], dt_format='%Y-%m-%d %H:%M:%S', _timedelta=timedelta(days=0, minutes=0, seconds=1))
-for m in psu_steps:
-    psu_block[m]["variables"]['timestamp'] = var_timestep_trigger(y='timestamp', f=update_timestamp)
-    # psu_block[m]["variables"]['timestamp'] = var_trigger(
-    #     y='timestamp', f=update_timestamp,
-    #     pre_conditions={'substep': [0, system_substeps]}, cond_op=lambda a, b: a and b
-    # )
-    # psu_block[m]["variables"]['timestamp'] = update_timestamp
-
-# New Convention
-partial_state_update_blocks = psub_list(psu_block, psu_steps)
-udo1_exp.append_configs(
-    sim_configs=sim_config,
-    initial_state=state_dict,
-    partial_state_update_blocks=partial_state_update_blocks
-)
-
-print()
-print("State Updates:")
-pp.pprint(partial_state_update_blocks)
-print()
\ No newline at end of file
diff --git a/simulations/regression_tests/models/udo_inter_substep_update.py b/simulations/regression_tests/models/udo_inter_substep_update.py
deleted file mode 100644
index 583f2386..00000000
--- a/simulations/regression_tests/models/udo_inter_substep_update.py
+++ /dev/null
@@ -1,165 +0,0 @@
-import pandas as pd
-import pprint as pp
-# from fn.func import curried
-from datetime import timedelta
-
-from cadCAD.utils import SilentDF #, val_switch
-from cadCAD.configuration.utils import time_step, config_sim
-from cadCAD.configuration.utils.userDefinedObject import udoPipe, UDO
-from simulations.regression_tests.experiments import udo1_exp
-
-DF = SilentDF(pd.read_csv('simulations/external_data/output.csv'))
-
-class udoExample(object):
-    def __init__(self, x, dataset=None):
-        self.x = x
-        self.mem_id = str(hex(id(self)))
-        self.ds = dataset # for setting ds initially or querying
-        self.perception = {}
-
-    def anon(self, f):
-        return f(self)
-
-    def updateX(self):
-        self.x += 1
-        return self
-
-    def perceive(self, s):
-        self.perception = self.ds[
-            (self.ds['run'] == s['run']) & (self.ds['substep'] == s['substep']) & (self.ds['timestep'] == s['timestep'])
-        ].drop(columns=['run', 'substep']).to_dict()
-        return self
-
-    def read(self, ds_uri):
-        self.ds = SilentDF(pd.read_csv(ds_uri))
-        return self
-
-    def write(self, ds_uri):
-        pd.to_csv(ds_uri)
-
-    # Generic update function
-
-    pass
-
-# can be accessed after an update within the same substep and timestep
-state_udo = UDO(udo=udoExample(0, DF), masked_members=['obj', 'perception'])
-policy_udoA = UDO(udo=udoExample(0, DF), masked_members=['obj', 'perception'])
-policy_udoB = UDO(udo=udoExample(0, DF), masked_members=['obj', 'perception'])
-
-def udo_policyA(_g, step, sL, s, **kwargs):
-    s['udo_policies']['udo_A'].updateX()
-    return {'udo_A': udoPipe(s['udo_policies']['udo_A'])}
-
-def udo_policyB(_g, step, sL, s, **kwargs):
-    s['udo_policies']['udo_B'].updateX()
-    return {'udo_B': udoPipe(s['udo_policies']['udo_B'])}
-
-
-policies = {"p1": udo_policyA, "p2": udo_policyB}
-
-state_dict = {
-    'increment': 0,
-    'state_udo': state_udo, 'state_udo_tracker_a': 0, 'state_udo_tracker_b': 0,
-    'state_udo_perception_tracker': {"ds1": None, "ds2": None, "ds3": None, "timestep": None},
-    'udo_policies': {'udo_A': policy_udoA, 'udo_B': policy_udoB},
-    'udo_policy_tracker_a': (0, 0), 'udo_policy_tracker_b': (0, 0),
-    'timestamp': '2019-01-01 00:00:00'
-}
-
-# @curried
-def perceive(s, self):
-    self.perception = self.ds[
-        (self.ds['run'] == s['run']) & (self.ds['substep'] == s['substep']) & (self.ds['timestep'] == s['timestep'])
-    ].drop(columns=['run', 'substep']).to_dict()
-    return self
-
-def view_udo_policy(_g, step, sL, s, _input, **kwargs):
-    return 'udo_policies', _input
-
-def state_udo_update(_g, step, sL, s, _input, **kwargs):
-    y = 'state_udo'
-    # s['hydra_state'].updateX().anon(perceive(s))
-    s['state_udo'].updateX().perceive(s)
-    x = udoPipe(s['state_udo'])
-    return y, x
-
-def increment(y, incr_by):
-    return lambda _g, step, sL, s, _input, **kwargs: (y, s[y] + incr_by)
-
-def track(destination, source):
-    return lambda _g, step, sL, s, _input, **kwargs: (destination, s[source].x)
-
-def track_udo_policy(destination, source):
-    def val_switch(v):
-        if isinstance(v, pd.DataFrame) is True or isinstance(v, SilentDF) is True:
-            return SilentDF(v)
-        else:
-            return v.x
-    return lambda _g, step, sL, s, _input, **kwargs: (destination, tuple(val_switch(v) for _, v in s[source].items()))
-
-def track_state_udo_perception(destination, source):
-    def id(past_perception):
-        if len(past_perception) == 0:
-            return state_dict['state_udo_perception_tracker']
-        else:
-            return past_perception
-    return lambda _g, step, sL, s, _input, **kwargs: (destination, id(s[source].perception))
-
-
-def time_model(y, substeps, time_delta, ts_format='%Y-%m-%d %H:%M:%S'):
-    def apply_incriment_condition(s):
-        if s['substep'] == 0 or s['substep'] == substeps:
-            return y, time_step(dt_str=s[y], dt_format=ts_format, _timedelta=time_delta)
-        else:
-            return y, s[y]
-    return lambda _g, step, sL, s, _input, **kwargs: apply_incriment_condition(s)
-
-
-states = {
-    'increment': increment('increment', 1),
-    'state_udo_tracker_a': track('state_udo_tracker_a', 'state_udo'),
-    'state_udo': state_udo_update,
-    'state_udo_perception_tracker': track_state_udo_perception('state_udo_perception_tracker', 'state_udo'),
-    'state_udo_tracker_b': track('state_udo_tracker_b', 'state_udo'),
-    'udo_policy_tracker_a': track_udo_policy('udo_policy_tracker_a', 'udo_policies'),
-    'udo_policies': view_udo_policy,
-    'udo_policy_tracker_b': track_udo_policy('udo_policy_tracker_b', 'udo_policies')
-}
-
-substeps=3
-update_timestamp = time_model(
-    'timestamp',
-    substeps=3,
-    time_delta=timedelta(days=0, minutes=0, seconds=1),
-    ts_format='%Y-%m-%d %H:%M:%S'
-)
-states['timestamp'] = update_timestamp
-
-PSUB = {
-    'policies': policies,
-    'states': states
-}
-
-# needs M1&2 need behaviors
-partial_state_update_blocks = [PSUB] * substeps
-# pp.pprint(partial_state_update_blocks)
-
-sim_config = config_sim({
-    "N": 2,
-    "T": range(4)
-})
-
-udo1_exp.append_configs(
-    sim_configs=sim_config,
-    initial_state=state_dict,
-    seeds={},
-    raw_exogenous_states={},
-    env_processes={},
-    partial_state_update_blocks=partial_state_update_blocks,
-    # policy_ops=[lambda a, b: {**a, **b}]
-)
-
-print()
-print("State Updates:")
-pp.pprint(partial_state_update_blocks)
-print()
\ No newline at end of file
diff --git a/testing/generic_test.py b/testing/generic_test.py
index e3d281b6..5c35cca6 100644
--- a/testing/generic_test.py
+++ b/testing/generic_test.py
@@ -1,10 +1,7 @@
 import unittest
-from pprint import pprint
-
-from parameterized import parameterized
 from functools import reduce
-
 from tabulate import tabulate
+from parameterized import parameterized
 
 
 def generate_assertions_df(df, expected_results, target_cols, evaluations):
@@ -31,8 +28,6 @@ def wrapped_eval(a, b):
 
 def make_generic_test(params):
     class TestSequence(unittest.TestCase):
-        # pprint(params)
-
         def generic_test(self, tested_df, expected_reults, test_name):
             erroneous = tested_df[(tested_df[test_name] == False)]
             print(tabulate(tested_df, headers='keys', tablefmt='psql'))
diff --git a/testing/models/param_sweep.py b/testing/models/param_sweep.py
index 211142d4..55658c5c 100644
--- a/testing/models/param_sweep.py
+++ b/testing/models/param_sweep.py
@@ -1,9 +1,8 @@
 import pprint
 from typing import Dict, List
 
-# from cadCAD.configuration import append_configs
+from cadCAD.configuration import Experiment
 from cadCAD.configuration.utils import env_trigger, var_substep_trigger, config_sim, psub_list
-from testing.experiments import exp_param_sweep
 
 pp = pprint.PrettyPrinter(indent=4)
 
@@ -54,14 +53,14 @@ def policies(_g, step, sL, s, _input, **kwargs):
 def sweeped(_g, step, sL, s, _input, **kwargs):
     return 'sweeped', {'beta': _g['beta'], 'gamma': _g['gamma']}
 
-psu_block = {k: {"policies": {}, "variables": {}} for k in psu_steps}
+psu_block = {k: {"policies": {}, "states": {}} for k in psu_steps}
 for m in psu_steps:
     psu_block[m]['policies']['gamma'] = gamma
     psu_block[m]['policies']['omega'] = omega
-    psu_block[m]["variables"]['alpha'] = alpha
-    psu_block[m]["variables"]['beta'] = beta
-    psu_block[m]['variables']['policies'] = policies
-    psu_block[m]["variables"]['sweeped'] = var_timestep_trigger(y='sweeped', f=sweeped)
+    psu_block[m]["states"]['alpha'] = alpha
+    psu_block[m]["states"]['beta'] = beta
+    psu_block[m]['states']['policies'] = policies
+    psu_block[m]["states"]['sweeped'] = var_timestep_trigger(y='sweeped', f=sweeped)
 
 
 # Genesis States
@@ -86,7 +85,9 @@ def sweeped(_g, step, sL, s, _input, **kwargs):
 
 # New Convention
 partial_state_update_blocks = psub_list(psu_block, psu_steps)
-exp_param_sweep.append_configs(
+
+exp = Experiment()
+exp.append_model(
     sim_configs=sim_config,
     initial_state=genesis_states,
     env_processes=env_process,
diff --git a/testing/models/policy_aggregation.py b/testing/models/policy_aggregation.py
index 2e2277c5..c21caa4a 100644
--- a/testing/models/policy_aggregation.py
+++ b/testing/models/policy_aggregation.py
@@ -1,10 +1,7 @@
+from cadCAD.configuration import Experiment
 from cadCAD.configuration.utils import config_sim
 
-
 # Policies per Mechanism
-from testing.experiments import exp_policy_agg
-
-
 def p1m1(_g, step, sL, s, **kwargs):
     return {'policy1': 1}
 def p2m1(_g, step, sL, s, **kwargs):
@@ -74,8 +71,8 @@ def policies(_g, step, sH, s, _input, **kwargs):
     }
 )
 
-
-exp_policy_agg.append_configs(
+exp = Experiment()
+exp.append_model(
     sim_configs=sim_config,
     initial_state=genesis_states,
     partial_state_update_blocks=partial_state_update_block,
diff --git a/testing/results_comparison.py b/testing/results_comparison.py
new file mode 100644
index 00000000..22866571
--- /dev/null
+++ b/testing/results_comparison.py
@@ -0,0 +1,95 @@
+import unittest
+import pandas as pd
+from tabulate import tabulate
+from pandas._testing import assert_frame_equal
+
+
+def compare_results(result_diff):
+    class CompareResults(unittest.TestCase):
+        def setUp(self):
+            result_diff.columns
+            self.val_error_status_ind = False not in result_diff.val_error_status.tolist()
+            self.type_error_status_ind = False not in result_diff.type_error_status.tolist()
+            self.df_out = None
+            self.erroneous_indexes = None
+            self.select_result_diff = None
+
+            if (self.val_error_status_ind is False) or (self.type_error_status_ind is False):
+                self.erroneous_indexes = list(result_diff.index[result_diff["val_error_status"] == False])
+                self.select_result_diff = result_diff.iloc[self.erroneous_indexes]
+                self.df_out = tabulate(self.select_result_diff, headers='keys', tablefmt='psql')
+
+        def test_val_error_status(self):
+            print(self.df_out)
+            self.assertEqual(self.val_error_status_ind, True, "Value Error")
+
+        def test_type_error_status(self):
+            print(self.df_out)
+            self.assertEqual(self.type_error_status_ind, True, "Type Error")
+
+    return CompareResults
+
+
+def dataframe_difference(df1, df2):
+    def discrepancies(row):
+        return {
+            col: {'values': list(vals), 'type': [type(v) for v in vals]} for col, vals in row.items()
+            if vals[0] != vals[1]
+        }
+
+    def val_error_status(val):
+        if type(val) is dict and len(val) != 0:
+            return False
+        else:
+            return True
+
+    def type_error_status(val):
+        if type(val) is dict and len(val) != 0:
+            return list(set([v['type'][0] == v['type'][1] for k, v in val.items()]))[0]
+        else:
+            return True
+
+    df1_cols, df2_cols = list(df1.columns), list(df2.columns)
+    if set(df1_cols) == set(df2_cols) and df1.shape == df2.shape:
+        df_equal_ind = True
+        try:
+            assert_frame_equal(df1, df2)
+        except:
+            df_equal_ind = False
+
+        if df_equal_ind is False:
+            data = [list(zip(df1[col], df2[col])) for col in df1_cols]
+
+            df = pd.DataFrame(data).T
+            df.columns = df1_cols
+            df['discrepancies'] = df.apply(discrepancies, axis=1)
+
+            result_df1 = df1.reset_index()
+            discrepancies_df = df[['discrepancies']].reset_index()
+            result_diff = result_df1.merge(discrepancies_df, how='left', on='index')
+            result_diff['val_error_status'] = result_diff['discrepancies'].apply(val_error_status)
+            result_diff['type_error_status'] = result_diff['discrepancies'].apply(type_error_status)
+
+            return result_diff
+        else:
+            df1['discrepancies'] = None
+            df1['val_error_status'] = True
+            df1['type_error_status'] = True
+            return df1
+    else:
+        df1_row_count, df1_col_count = df1.shape
+        df2_row_count, df2_col_count = df2.shape
+        raise Exception(f"""
+        DataFrames have mismatched dimensions or columns:
+
+            Columns:
+                * df1: {df1_cols}
+                * df2: {df2_cols}     
+            Shapes:
+                * df1: 
+                    *    Row Count: {df1_row_count}
+                    * Column Count: {df1_col_count}
+                * df2:
+                    *    Row Count: {df2_row_count}
+                    * Column Count: {df2_col_count}
+        """)
diff --git a/testing/tests/dev/compare_results_dev.py b/testing/tests/dev/compare_results_dev.py
new file mode 100644
index 00000000..b950ae9d
--- /dev/null
+++ b/testing/tests/dev/compare_results_dev.py
@@ -0,0 +1,40 @@
+import unittest
+import pandas as pd
+from copy import deepcopy
+
+from testing.models import param_sweep
+from testing.results_comparison import dataframe_difference, compare_results
+from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
+
+exec_mode = ExecutionMode()
+exec_ctx = ExecutionContext(context=exec_mode.local_mode)
+run = Executor(exec_context=exec_ctx, configs=param_sweep.exp.configs)
+
+raw_result, tensor_fields, sessions = run.execute()
+result = pd.DataFrame(raw_result)
+# print(tabulate(tensor_fields[0], headers='keys', tablefmt='psql'))
+# pprint(sessions)
+# print(tabulate(result, headers='keys', tablefmt='psql'))
+
+
+result_1 = result
+result_2 = deepcopy(result)
+result_df1 = pd.DataFrame({'a': [1, 2], 'b': [3, 5]})
+result_df2 = pd.DataFrame({'a': ['hi', 2], 'b': [3.0, 4.0]})
+
+# print(result_df1.shape)
+# exit()
+
+
+
+equivalent_result_diff = dataframe_difference(result_1, result_2)
+different_result_diff = dataframe_difference(result_df1, result_df2)
+
+class dfCompareTest(compare_results(different_result_diff)):
+    pass
+
+class EquivalentTest(compare_results(equivalent_result_diff)):
+    pass
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/testing/tests/dev/compare_results_old.py b/testing/tests/dev/compare_results_old.py
new file mode 100644
index 00000000..68210670
--- /dev/null
+++ b/testing/tests/dev/compare_results_old.py
@@ -0,0 +1,79 @@
+from copy import deepcopy
+
+import pandas as pd
+import numpy as np
+# import pandasql
+# from tabulate import tabulate
+from tabulate import tabulate
+
+from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
+from testing.models import param_sweep
+
+exec_mode = ExecutionMode()
+exec_ctx = ExecutionContext(context=exec_mode.local_mode)
+run = Executor(exec_context=exec_ctx, configs=param_sweep.exp.configs)
+
+raw_result, tensor_fields, sessions = run.execute()
+result = pd.DataFrame(raw_result)
+# print(tabulate(tensor_fields[0], headers='keys', tablefmt='psql'))
+# pprint(sessions)
+# print(tabulate(result, headers='keys', tablefmt='psql'))
+
+# result_1 = result
+# result_2 = deepcopy(result)
+
+# test_df1 = pd.testing.assert_frame_equal(result_1, result_2)
+# print(tabulate(test_df1, headers='keys', tablefmt='psql'))
+
+result_df = pd.DataFrame({'a': [1, 2], 'b': [3, 5]}).reset_index()
+df2 = pd.DataFrame({'a': [3.1, 2], 'b': [3.0, 4.0]}).reset_index()
+
+# test_df2 = pd.testing.assert_frame_equal(df1, df2)
+# print(tabulate(test_df2, headers='keys', tablefmt='psql'))
+
+def dataframe_difference(df1: pd.DataFrame, df2: pd.DataFrame, which=None):
+    """
+        Find rows which are different between two DataFrames.
+        https://hackersandslackers.com/compare-rows-pandas-dataframes/
+    """
+    comparison_df = df1.merge(
+        df2,
+        indicator=True,
+        how='outer'
+    )
+    if which is None:
+        diff_df = comparison_df[comparison_df['_merge'] != 'both']
+    else:
+        diff_df = comparison_df[comparison_df['_merge'] == which]
+    # diff_df.to_csv('data/diff.csv')
+    return diff_df
+
+
+merge_df = dataframe_difference(result_df, df2)
+cols_no__merge = list(filter(lambda col: '_merge' not in col, merge_df.columns.tolist()))
+cols_no_index = list(filter(lambda col: 'index' not in col, cols_no__merge))
+aggregation = dict((k, 'unique') for k in cols_no_index)
+diff_df = merge_df[cols_no__merge].groupby('index').agg(aggregation)
+# print(tabulate(diff_df, headers='keys', tablefmt='psql'))
+
+
+def discrepancies(row):
+    return dict([
+        (col, list(vals)) for col, vals in row.items()
+            if type(vals) is np.ndarray and len(vals) > 1
+    ])
+
+
+def val_error(val):
+    if type(val) is dict:
+        return False
+    else:
+        return True
+
+
+diff_df['discrepancies'] = diff_df.apply(discrepancies, axis=1)
+discrepancies_df = diff_df[['discrepancies']]
+result_diff = result_df.merge(discrepancies_df, how='left', on='index')
+result_diff['val_error'] = result_diff['discrepancies'].apply(val_error)
+print(tabulate(result_diff, headers='keys', tablefmt='psql'))
+
diff --git a/testing/tests/out_check_dev.py b/testing/tests/dev/out_check_dev.py
similarity index 100%
rename from testing/tests/out_check_dev.py
rename to testing/tests/dev/out_check_dev.py
diff --git a/testing/tests/dev/utils_dev.py b/testing/tests/dev/utils_dev.py
new file mode 100644
index 00000000..63885f81
--- /dev/null
+++ b/testing/tests/dev/utils_dev.py
@@ -0,0 +1,5 @@
+def gen_metric_row(row, cols):
+    return ((row['run'], row['timestep'], row['substep']), {col: row[col] for col in cols})
+
+def gen_metric_dict(df, cols):
+    return dict([gen_metric_row(row, cols) for index, row in df.iterrows()])
diff --git a/testing/tests/expected_results/param_sweep_4.pkl b/testing/tests/expected_results/param_sweep_4.pkl
new file mode 100644
index 00000000..69be4241
Binary files /dev/null and b/testing/tests/expected_results/param_sweep_4.pkl differ
diff --git a/testing/tests/expected_results/param_sweep_5.pkl b/testing/tests/expected_results/param_sweep_5.pkl
new file mode 100644
index 00000000..5a9ebcff
Binary files /dev/null and b/testing/tests/expected_results/param_sweep_5.pkl differ
diff --git a/testing/tests/expected_results/param_sweep_psub0_4.pkl b/testing/tests/expected_results/param_sweep_psub0_4.pkl
new file mode 100644
index 00000000..e7af2c01
Binary files /dev/null and b/testing/tests/expected_results/param_sweep_psub0_4.pkl differ
diff --git a/testing/tests/expected_results/param_sweep_timestep1_4.pkl b/testing/tests/expected_results/param_sweep_timestep1_4.pkl
new file mode 100644
index 00000000..190a3137
Binary files /dev/null and b/testing/tests/expected_results/param_sweep_timestep1_4.pkl differ
diff --git a/testing/tests/expected_results/policy_agg_4.pkl b/testing/tests/expected_results/policy_agg_4.pkl
new file mode 100644
index 00000000..42ae6bdf
Binary files /dev/null and b/testing/tests/expected_results/policy_agg_4.pkl differ
diff --git a/testing/tests/expected_results/policy_agg_5.pkl b/testing/tests/expected_results/policy_agg_5.pkl
new file mode 100644
index 00000000..142296ff
Binary files /dev/null and b/testing/tests/expected_results/policy_agg_5.pkl differ
diff --git a/testing/tests/external_dataset_dev.py b/testing/tests/external_dataset_dev.py
deleted file mode 100644
index 8ef1a6fe..00000000
--- a/testing/tests/external_dataset_dev.py
+++ /dev/null
@@ -1,107 +0,0 @@
-import unittest
-import pandas as pd
-
-from cadCAD import configs
-from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-from testing.generic_test import make_generic_test
-
-exec_mode = ExecutionMode()
-
-first_config = configs
-exec_ctx = ExecutionContext(context=exec_mode.local_mode)
-run = Executor(exec_context=exec_ctx, configs=first_config)
-
-raw_result, tensor_field, sessions = run.execute()
-result = pd.DataFrame(raw_result)
-
-
-def get_expected_results(run):
-    return {
-        (0, run, 0, 0): {
-            'external_data': {'ds1': None, 'ds2': None, 'ds3': None},
-            'increment': 0,
-            'policies': {'ds1': None, 'ds2': None, 'ds3': None}
-        },
-        (0, run, 1, 1): {
-            'external_data': {'ds1': 0, 'ds2': 0, 'ds3': 1},
-             'increment': 1,
-             'policies': {'ds1': 0, 'ds2': 0, 'ds3': 1}
-        },
-        (0, run, 1, 2): {
-            'external_data': {'ds1': 1, 'ds2': 40, 'ds3': 5},
-             'increment': 2,
-             'policies': {'ds1': 1, 'ds2': 40, 'ds3': 5}
-        },
-        (0, run, 1, 3): {
-            'external_data': {'ds1': 2, 'ds2': 40, 'ds3': 5},
-             'increment': 3,
-             'policies': {'ds1': 2, 'ds2': 40, 'ds3': 5}
-        },
-        (0, run, 2, 1): {
-            'external_data': {'ds1': 3, 'ds2': 40, 'ds3': 5},
-             'increment': 4,
-             'policies': {'ds1': 3, 'ds2': 40, 'ds3': 5}
-        },
-        (0, run, 2, 2): {
-            'external_data': {'ds1': 4, 'ds2': 40, 'ds3': 5},
-             'increment': 5,
-             'policies': {'ds1': 4, 'ds2': 40, 'ds3': 5}
-        },
-        (0, run, 2, 3): {
-            'external_data': {'ds1': 5, 'ds2': 40, 'ds3': 5},
-            'increment': 6,
-            'policies': {'ds1': 5, 'ds2': 40, 'ds3': 5}
-        },
-        (0, run, 3, 1): {
-            'external_data': {'ds1': 6, 'ds2': 40, 'ds3': 5},
-            'increment': 7,
-            'policies': {'ds1': 6, 'ds2': 40, 'ds3': 5}
-        },
-        (0, run, 3, 2): {
-            'external_data': {'ds1': 7, 'ds2': 40, 'ds3': 5},
-            'increment': 8,
-            'policies': {'ds1': 7, 'ds2': 40, 'ds3': 5}
-        },
-        (0, run, 3, 3): {
-            'external_data': {'ds1': 8, 'ds2': 40, 'ds3': 5},
-            'increment': 9,
-            'policies': {'ds1': 8, 'ds2': 40, 'ds3': 5}
-        },
-        (0, run, 4, 1): {
-            'external_data': {'ds1': 9, 'ds2': 40, 'ds3': 5},
-            'increment': 10,
-            'policies': {'ds1': 9, 'ds2': 40, 'ds3': 5}
-        },
-        (0, run, 4, 2): {
-            'external_data': {'ds1': 10, 'ds2': 40, 'ds3': 5},
-            'increment': 11,
-            'policies': {'ds1': 10, 'ds2': 40, 'ds3': 5}
-        },
-        (0, run, 4, 3): {
-            'external_data': {'ds1': 11, 'ds2': 40, 'ds3': 5},
-            'increment': 12,
-            'policies': {'ds1': 11, 'ds2': 40, 'ds3': 5}
-        }
-    }
-
-
-expected_results = {}
-expected_results_1 = get_expected_results(1)
-expected_results_2 = get_expected_results(2)
-expected_results.update(expected_results_1)
-expected_results.update(expected_results_2)
-
-
-def row(a, b):
-    return a == b
-
-
-params = [["external_dataset", result, expected_results, ['increment', 'external_data', 'policies'], [row]]]
-
-
-class GenericTest(make_generic_test(params)):
-    pass
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/testing/tests/historical_state_access_dev.py b/testing/tests/historical_state_access_dev.py
deleted file mode 100644
index 3aa0dfe2..00000000
--- a/testing/tests/historical_state_access_dev.py
+++ /dev/null
@@ -1,122 +0,0 @@
-import unittest
-import pandas as pd
-
-from cadCAD import configs
-from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-from testing.generic_test import make_generic_test
-
-exec_mode = ExecutionMode()
-exec_ctx = ExecutionContext(context=exec_mode.single_mode)
-run = Executor(exec_context=exec_ctx, configs=configs)
-
-raw_result, tensor_field, _ = run.execute()
-result = pd.DataFrame(raw_result)
-expected_results = {
-    (0, 1, 0, 0): {'x': 0, 'nonexsistant': [], 'last_x': [], '2nd_to_last_x': [], '3rd_to_last_x': [], '4th_to_last_x': []},
-    (0, 1, 1, 1): {'x': 1,
-                'nonexsistant': [],
-                'last_x': [{'simulation': 0, 'x': 0, 'run': 1, 'substep': 0, 'timestep': 0}],
-                '2nd_to_last_x': [],
-                '3rd_to_last_x': [],
-                '4th_to_last_x': []},
-    (0, 1, 1, 2): {'x': 2,
-                'nonexsistant': [],
-                'last_x': [{'simulation': 0, 'x': 0, 'run': 1, 'substep': 0, 'timestep': 0}],
-                '2nd_to_last_x': [],
-                '3rd_to_last_x': [],
-                '4th_to_last_x': []},
-    (0, 1, 1, 3): {'x': 3,
-                'nonexsistant': [],
-                'last_x': [{'simulation': 0, 'x': 0, 'run': 1, 'substep': 0, 'timestep': 0}],
-                '2nd_to_last_x': [],
-                '3rd_to_last_x': [],
-                '4th_to_last_x': []},
-    (0, 1, 2, 1): {'x': 4,
-                'nonexsistant': [],
-                'last_x': [
-                    {'simulation': 0, 'x': 1, 'run': 1, 'substep': 1, 'timestep': 1},
-                    {'simulation': 0, 'x': 2, 'run': 1, 'substep': 2, 'timestep': 1},
-                    {'simulation': 0, 'x': 3, 'run': 1, 'substep': 3, 'timestep': 1}
-                ],
-                '2nd_to_last_x': [{'simulation': 0, 'x': 0, 'run': 1, 'substep': 0, 'timestep': 0}],
-                '3rd_to_last_x': [],
-                '4th_to_last_x': []},
-    (0, 1, 2, 2): {'x': 5,
-                'nonexsistant': [],
-                'last_x': [
-                    {'simulation': 0, 'x': 1, 'run': 1, 'substep': 1, 'timestep': 1},
-                    {'simulation': 0, 'x': 2, 'run': 1, 'substep': 2, 'timestep': 1},
-                    {'simulation': 0, 'x': 3, 'run': 1, 'substep': 3, 'timestep': 1}
-                ],
-                '2nd_to_last_x': [{'simulation': 0, 'x': 0, 'run': 1, 'substep': 0, 'timestep': 0}],
-                '3rd_to_last_x': [],
-                '4th_to_last_x': []},
-    (0, 1, 2, 3): {'x': 6,
-                'nonexsistant': [],
-                'last_x': [
-                    {'simulation': 0, 'x': 1, 'run': 1, 'substep': 1, 'timestep': 1},
-                    {'simulation': 0, 'x': 2, 'run': 1, 'substep': 2, 'timestep': 1},
-                    {'simulation': 0, 'x': 3, 'run': 1, 'substep': 3, 'timestep': 1}
-                ],
-                '2nd_to_last_x': [{'simulation': 0, 'x': 0, 'run': 1, 'substep': 0, 'timestep': 0}],
-                '3rd_to_last_x': [],
-                '4th_to_last_x': []},
-    (0, 1, 3, 1): {'x': 7,
-                'nonexsistant': [],
-                'last_x': [
-                    {'simulation': 0, 'x': 4, 'run': 1, 'substep': 1, 'timestep': 2},
-                    {'simulation': 0, 'x': 5, 'run': 1, 'substep': 2, 'timestep': 2},
-                    {'simulation': 0, 'x': 6, 'run': 1, 'substep': 3, 'timestep': 2}
-                ],
-                '2nd_to_last_x': [
-                    {'simulation': 0, 'x': 1, 'run': 1, 'substep': 1, 'timestep': 1},
-                    {'simulation': 0, 'x': 2, 'run': 1, 'substep': 2, 'timestep': 1},
-                    {'simulation': 0, 'x': 3, 'run': 1, 'substep': 3, 'timestep': 1}
-                ],
-                '3rd_to_last_x': [{'simulation': 0, 'x': 0, 'run': 1, 'substep': 0, 'timestep': 0}],
-                '4th_to_last_x': []},
-    (0, 1, 3, 2): {'x': 8,
-                'nonexsistant': [],
-                'last_x': [
-                    {'simulation': 0, 'x': 4, 'run': 1, 'substep': 1, 'timestep': 2},
-                    {'simulation': 0, 'x': 5, 'run': 1, 'substep': 2, 'timestep': 2},
-                    {'simulation': 0, 'x': 6, 'run': 1, 'substep': 3, 'timestep': 2}
-                ],
-                '2nd_to_last_x': [
-                    {'simulation': 0, 'x': 1, 'run': 1, 'substep': 1, 'timestep': 1},
-                    {'simulation': 0, 'x': 2, 'run': 1, 'substep': 2, 'timestep': 1},
-                    {'simulation': 0, 'x': 3, 'run': 1, 'substep': 3, 'timestep': 1}
-                ],
-                '3rd_to_last_x': [{'simulation': 0, 'x': 0, 'run': 1, 'substep': 0, 'timestep': 0}],
-                '4th_to_last_x': []},
-    (0, 1, 3, 3): {'x': 9,
-                'nonexsistant': [],
-                'last_x': [
-                    {'simulation': 0, 'x': 4, 'run': 1, 'substep': 1, 'timestep': 2},
-                    {'simulation': 0, 'x': 5, 'run': 1, 'substep': 2, 'timestep': 2},
-                    {'simulation': 0, 'x': 6, 'run': 1, 'substep': 3, 'timestep': 2}
-                ],
-                '2nd_to_last_x': [
-                    {'simulation': 0, 'x': 1, 'run': 1, 'substep': 1, 'timestep': 1},
-                    {'simulation': 0, 'x': 2, 'run': 1, 'substep': 2, 'timestep': 1},
-                    {'simulation': 0, 'x': 3, 'run': 1, 'substep': 3, 'timestep': 1}
-                ],
-                '3rd_to_last_x': [{'simulation': 0, 'x': 0, 'run': 1, 'substep': 0, 'timestep': 0}],
-                '4th_to_last_x': []}
-}
-
-
-def row(a, b):
-    return a == b
-params = [
-    ["historical_state_access", result, expected_results,
-           ['x', 'nonexsistant', 'last_x', '2nd_to_last_x', '3rd_to_last_x', '4th_to_last_x'], [row]]
-    ]
-
-
-class GenericTest(make_generic_test(params)):
-    pass
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/testing/tests/multi_model_sim.py b/testing/tests/multi_model_sim.py
new file mode 100644
index 00000000..f8d79c22
--- /dev/null
+++ b/testing/tests/multi_model_sim.py
@@ -0,0 +1,93 @@
+import unittest, pandas as pd
+from cadCAD.configuration import Experiment
+from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
+from testing.models import param_sweep, policy_aggregation
+
+exp = Experiment()
+sys_model_A_id = "sys_model_A"
+exp.append_model(
+    model_id=sys_model_A_id,
+    sim_configs=param_sweep.sim_config,
+    initial_state=param_sweep.genesis_states,
+    env_processes=param_sweep.env_process,
+    partial_state_update_blocks=param_sweep.partial_state_update_blocks
+)
+sys_model_B_id = "sys_model_B"
+exp.append_model(
+    model_id=sys_model_B_id,
+    sim_configs=param_sweep.sim_config,
+    initial_state=param_sweep.genesis_states,
+    env_processes=param_sweep.env_process,
+    partial_state_update_blocks=param_sweep.partial_state_update_blocks
+)
+sys_model_C_id = "sys_model_C"
+exp.append_model(
+    model_id=sys_model_C_id,
+    sim_configs=policy_aggregation.sim_config,
+    initial_state=policy_aggregation.genesis_states,
+    partial_state_update_blocks=policy_aggregation.partial_state_update_block,
+    policy_ops=[lambda a, b: a + b, lambda y: y * 2] # Default: lambda a, b: a + b
+)
+
+simulation = 3
+model_A_sweeps = len(param_sweep.sim_config)
+model_B_sweeps = len(param_sweep.sim_config)
+model_C_sweeps = 1
+# total_sweeps = model_A_sweeps + model_B_sweeps
+
+model_A_runs = param_sweep.sim_config[0]['N']
+model_B_runs = param_sweep.sim_config[0]['N']
+model_C_runs = policy_aggregation.sim_config['N']
+# total_runs = model_A_runs + model_B_runs
+
+model_A_timesteps = len(param_sweep.sim_config[0]['T'])
+model_B_timesteps = len(param_sweep.sim_config[0]['T'])
+model_C_timesteps = len(policy_aggregation.sim_config['T'])
+
+model_A_substeps = len(param_sweep.partial_state_update_blocks)
+model_B_substeps = len(param_sweep.partial_state_update_blocks)
+model_C_substeps = len(policy_aggregation.partial_state_update_block)
+# total_substeps = model_A_substeps + model_B_substeps
+
+model_A_init_rows = model_A_runs * model_A_sweeps
+model_B_init_rows = model_B_runs * model_B_sweeps
+model_C_init_rows = model_C_runs * 1
+model_A_rows = model_A_init_rows + (model_A_sweeps * (model_A_runs * model_A_timesteps * model_A_substeps))
+model_B_rows = model_B_init_rows + (model_B_sweeps * (model_B_runs * model_B_timesteps * model_B_substeps))
+model_C_rows = model_C_init_rows + (model_C_sweeps * (model_C_runs * model_C_timesteps * model_C_substeps))
+
+
+exec_mode = ExecutionMode()
+local_mode_ctx = ExecutionContext(context=exec_mode.local_mode)
+simulation = Executor(exec_context=local_mode_ctx, configs=exp.configs)
+raw_results, _, _ = simulation.execute()
+
+results_df = pd.DataFrame(raw_results)
+param_sweep_df = pd.read_pickle("expected_results/param_sweep_4.pkl")
+policy_agg_df = pd.read_pickle("expected_results/policy_agg_4.pkl")
+param_sweep_df_rows = len(param_sweep_df.index)
+policy_agg_df_rows = len(policy_agg_df.index)
+
+expected_rows = param_sweep_df_rows + param_sweep_df_rows + policy_agg_df_rows
+expected_rows_from_api = model_A_rows + model_B_rows + model_C_rows
+result_rows = len(results_df.index)
+
+
+class MultiModelTest(unittest.TestCase):
+    def test_row_count(self):
+        equal_row_count = expected_rows == expected_rows_from_api == result_rows
+        self.assertEqual(equal_row_count, True, "Row Count Mismatch between Expected and Multi-Model simulation results")
+    def test_row_count_from_api(self):
+        self.assertEqual(expected_rows == expected_rows_from_api, True, "API not producing Expected simulation results")
+    def test_row_count_from_results(self):
+        self.assertEqual(expected_rows == result_rows, True, "Engine not producing Expected simulation results")
+    def test_row_count_from_sys_model_A(self):
+        self.assertEqual(model_A_rows == param_sweep_df_rows, True, f"{sys_model_A_id}: Row Count Mismatch with Expected results")
+    def test_row_count_from_sys_model_B(self):
+        self.assertEqual(model_B_rows == param_sweep_df_rows, True, f"{sys_model_B_id}: Row Count Mismatch with Expected results")
+    def test_row_count_from_sys_model_C(self):
+        self.assertEqual(model_C_rows == policy_agg_df_rows, True, f"{sys_model_C_id}: Row Count Mismatch with Expected results")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/testing/tests/param_sweep.py b/testing/tests/param_sweep.py
index 91d15538..84af144d 100644
--- a/testing/tests/param_sweep.py
+++ b/testing/tests/param_sweep.py
@@ -1,86 +1,21 @@
-import unittest
-from pprint import pprint
-
-import pandas as pd
-
-from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
+import unittest, pandas as pd
+from tabulate import tabulate
 from testing.models import param_sweep
-from cadCAD import configs
-
-from testing.generic_test import make_generic_test
-from testing.models.param_sweep import some_function, g as sweep_params
-
+from testing.results_comparison import dataframe_difference, compare_results
+from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
 
 exec_mode = ExecutionMode()
 exec_ctx = ExecutionContext(context=exec_mode.local_mode)
-run = Executor(exec_context=exec_ctx, configs=configs)
-
-# sim, run, substep, timestep
-def get_expected_results(subset, run, beta, gamma):
-    return {
-        (subset, run, 0, 0): {'policies': {}, 'sweeped': {}, 'alpha': 0, 'beta': 0},
-        (subset, run, 1, 1): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 1, 2): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 1, 3): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 2, 1): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 2, 2): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 2, 3): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 3, 1): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 3, 2): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 3, 3): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 4, 1): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 4, 2): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 4, 3): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 5, 1): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': beta, 'alpha': 1, 'beta': beta},
-        (subset, run, 5, 2): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': beta, 'alpha': 1, 'beta': beta},
-        (subset, run, 5, 3): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': beta, 'alpha': 1, 'beta': beta}
-    }
-
-
-def generate_expected(sweep_params):
-    def template(sweep_params):
-        subset_count = max(len(x) for x in list(sweep_params.values()))
-        expected_results, expected_results_1, expected_results_2 = {}, {}, {}
-        for subset in range(subset_count):
-            expected_results_1a = get_expected_results(subset, 1, 2, 3)
-            expected_results_1b = get_expected_results(subset, 2, 2, 3)
-            expected_results_1.update(expected_results_1a)
-            expected_results_1.update(expected_results_1b)
-
-            expected_results_2 = {}
-            expected_results_2a = get_expected_results(subset, 1, some_function, 4)
-            expected_results_2b = get_expected_results(subset, 2, some_function, 4)
-            expected_results_2.update(expected_results_2a)
-            expected_results_2.update(expected_results_2b)
-
-            expected_results.update(expected_results_1)
-            expected_results.update(expected_results_2)
-
-            yield expected_results
-
-    merged_expected = list(template(sweep_params))
-    result = {}
-    for d in merged_expected:
-        result.update(d)
-
-    return result
-
-
-def row(a, b):
-    return a == b
-
-
-def create_test_params(feature, fields):
-    raw_result, tensor_fields, sessions = run.execute()
-    df = pd.DataFrame(raw_result)
-    expected = generate_expected(sweep_params)
-    return [[feature, df, expected, fields, [row]]]
-
+run = Executor(exec_context=exec_ctx, configs=param_sweep.exp.configs)
+raw_result, _, _ = run.execute()
 
-params = list(create_test_params("param_sweep", ['alpha', 'beta', 'policies', 'sweeped']))
+result_df = pd.DataFrame(raw_result)
+expected_df = pd.read_pickle("expected_results/param_sweep_4.pkl")
+result_diff = dataframe_difference(result_df, expected_df)
+print(tabulate(result_diff, headers='keys', tablefmt='psql'))
 
 
-class GenericTest(make_generic_test(params)):
+class ParamSweepTest(compare_results(result_diff)):
     pass
 
 
diff --git a/testing/tests/param_sweep_dist_dev.py b/testing/tests/param_sweep_dist_dev.py
deleted file mode 100644
index 9bb65027..00000000
--- a/testing/tests/param_sweep_dist_dev.py
+++ /dev/null
@@ -1,90 +0,0 @@
-import unittest
-import pandas as pd
-
-from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-from cadCAD import configs
-from cadCAD.utils.sys_exec import to_pandas_df
-
-from testing.models import param_sweep
-from testing.generic_test import make_generic_test
-from testing.models.param_sweep import some_function, g as sweep_params
-
-from distroduce.engine.execution import distributed_simulations, transform
-from distroduce.session import sc_alt as sc
-
-exec_mode = ExecutionMode()
-distributed_sims = distributed_simulations(transform)
-distributed_ctx = ExecutionContext(context=exec_mode.distributed, method=distributed_sims)
-run = Executor(exec_context=distributed_ctx, configs=configs, spark_context=sc)
-
-# sim, run, substep, timestep
-def get_expected_results(subset, run, beta, gamma):
-    return {
-        (subset, run, 0, 0): {'policies': {}, 'sweeped': {}, 'alpha': 0, 'beta': 0},
-        (subset, run, 1, 1): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 1, 2): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 1, 3): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 2, 1): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 2, 2): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 2, 3): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 3, 1): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 3, 2): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 3, 3): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 4, 1): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 4, 2): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 4, 3): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta},
-        (subset, run, 5, 1): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': beta, 'alpha': 1, 'beta': beta},
-        (subset, run, 5, 2): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': beta, 'alpha': 1, 'beta': beta},
-        (subset, run, 5, 3): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': beta, 'alpha': 1, 'beta': beta}
-    }
-
-
-def generate_expected(sweep_params):
-    def template(sweep_params):
-        subset_count = max(len(x) for x in list(sweep_params.values()))
-        expected_results, expected_results_1, expected_results_2 = {}, {}, {}
-        for subset in range(subset_count):
-            expected_results_1a = get_expected_results(subset, 1, 2, 3)
-            expected_results_1b = get_expected_results(subset, 2, 2, 3)
-            expected_results_1.update(expected_results_1a)
-            expected_results_1.update(expected_results_1b)
-
-            expected_results_2 = {}
-            expected_results_2a = get_expected_results(subset, 1, some_function, 4)
-            expected_results_2b = get_expected_results(subset, 2, some_function, 4)
-            expected_results_2.update(expected_results_2a)
-            expected_results_2.update(expected_results_2b)
-
-            expected_results.update(expected_results_1)
-            expected_results.update(expected_results_2)
-
-            yield expected_results
-
-    merged_expected = list(template(sweep_params))
-    result = {}
-    for d in merged_expected:
-        result.update(d)
-
-    return result
-
-
-def row(a, b):
-    return a == b
-
-
-def create_test_params(feature, fields):
-    raw_result, tensor_fields, sessions = run.execute()
-    df: pd.DataFrame = to_pandas_df(raw_result)
-    expected = generate_expected(sweep_params)
-    return [[feature, df, expected, fields, [row]]]
-
-
-params = list(create_test_params("param_sweep", ['alpha', 'beta', 'policies', 'sweeped']))
-
-
-class GenericTest(make_generic_test(params)):
-    pass
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/testing/tests/policy_aggregation.py b/testing/tests/policy_aggregation.py
index 0e229f32..30b2b439 100644
--- a/testing/tests/policy_aggregation.py
+++ b/testing/tests/policy_aggregation.py
@@ -1,40 +1,21 @@
-import unittest
-import pandas as pd
-
+import unittest, pandas as pd
+from tabulate import tabulate
+from testing.models import policy_aggregation as policy_agg
+from testing.results_comparison import dataframe_difference, compare_results
 from cadCAD.engine import ExecutionMode, ExecutionContext, Executor
-from testing.generic_test import make_generic_test
-from testing.models import policy_aggregation
-from cadCAD import configs
 
 exec_mode = ExecutionMode()
 exec_ctx = ExecutionContext(context=exec_mode.local_mode)
-run = Executor(exec_context=exec_ctx, configs=configs)
-
-raw_result, tensor_field, _ = run.execute()
-result = pd.DataFrame(raw_result)
-
-expected_results = {
-    (0, 1, 0, 0): {'policies': {}, 's1': 0},
-    (0, 1, 1, 1): {'policies': {'policy1': 2, 'policy2': 4}, 's1': 1}, # 'policy1': 2
-    (0, 1, 1, 2): {'policies': {'policy1': 8, 'policy2': 8}, 's1': 2},
-    (0, 1, 1, 3): {'policies': {'policy1': 4, 'policy2': 8, 'policy3': 12}, 's1': 3},
-    (0, 1, 2, 1): {'policies': {'policy1': 2, 'policy2': 4}, 's1': 4},
-    (0, 1, 2, 2): {'policies': {'policy1': 8, 'policy2': 8}, 's1': 5},
-    (0, 1, 2, 3): {'policies': {'policy1': 4, 'policy2': 8, 'policy3': 12}, 's1': 6},
-    (0, 1, 3, 1): {'policies': {'policy1': 2, 'policy2': 4}, 's1': 7},
-    (0, 1, 3, 2): {'policies': {'policy1': 8, 'policy2': 8}, 's1': 8},
-    (0, 1, 3, 3): {'policies': {'policy1': 4, 'policy2': 8, 'policy3': 12}, 's1': 9}
-}
-
-
-def row(a, b):
-    return a == b
-
+run = Executor(exec_context=exec_ctx, configs=policy_agg.exp.configs)
+raw_result, _, _ = run.execute()
 
-params = [["policy_aggregation", result, expected_results, ['policies', 's1'], [row]]]
+result_df = pd.DataFrame(raw_result)
+expected_df = pd.read_pickle("expected_results/policy_agg_4.pkl")
+result_diff = dataframe_difference(result_df, expected_df)
+print(tabulate(result_diff, headers='keys', tablefmt='psql'))
 
 
-class GenericTest(make_generic_test(params)):
+class PolicyAggTest(compare_results(result_diff)):
     pass
 
 
diff --git a/testing/tests/run1psub0.py b/testing/tests/run1psub0.py
new file mode 100644
index 00000000..bb62b322
--- /dev/null
+++ b/testing/tests/run1psub0.py
@@ -0,0 +1,17 @@
+import unittest, pandas as pd
+from testing.results_comparison import dataframe_difference, compare_results
+
+expected_df = pd.read_pickle("expected_results/param_sweep_psub0_4.pkl")
+param_sweep_df = pd.read_pickle("expected_results/param_sweep_4.pkl")
+result_df = param_sweep_df[
+    (param_sweep_df.index > 0) & (param_sweep_df['subset'] < 1) & (param_sweep_df['run'] == 1)
+]
+result_diff = dataframe_difference(result_df, expected_df)
+
+
+class run1psub0Test(compare_results(result_diff)):
+    pass
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/testing/tests/runs_not_zero.py b/testing/tests/runs_not_zero.py
new file mode 100644
index 00000000..972bc340
--- /dev/null
+++ b/testing/tests/runs_not_zero.py
@@ -0,0 +1,22 @@
+import unittest
+from cadCAD.configuration.utils import config_sim
+
+val_error_indicator = False
+try:
+    sim_config = config_sim(
+        {
+            "N": 0,
+            "T": range(5)
+        }
+    )
+except ValueError:
+    val_error_indicator = True
+
+
+class RunExceptionTest(unittest.TestCase):
+    def test_multi_model(self):
+        self.assertEqual(val_error_indicator, True, "ValueError raised when runs (N) < 1")
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/testing/tests/timestep1psub0.py b/testing/tests/timestep1psub0.py
new file mode 100644
index 00000000..64f20eb9
--- /dev/null
+++ b/testing/tests/timestep1psub0.py
@@ -0,0 +1,20 @@
+import unittest, pandas as pd
+from testing.results_comparison import dataframe_difference, compare_results
+
+expected_df = pd.read_pickle("expected_results/param_sweep_timestep1_4.pkl")
+param_sweep_df = pd.read_pickle("expected_results/param_sweep_4.pkl")
+result_df = param_sweep_df[
+    (param_sweep_df.index > 0) &
+    (param_sweep_df['subset'] < 1) &
+    (param_sweep_df['timestep'] < 2) &
+    (param_sweep_df['run'] == 1)
+]
+result_diff = dataframe_difference(result_df, expected_df)
+
+
+class timestep1psub0Test(compare_results(result_diff)):
+    pass
+
+
+if __name__ == '__main__':
+    unittest.main()