From 86470e8138fa0f14193112cf1e1b6a1adb2d6710 Mon Sep 17 00:00:00 2001 From: "Joshua E. Jodesty" Date: Tue, 4 Aug 2020 12:07:49 -0400 Subject: [PATCH] 0.4.18 --- .gitignore | 2 + CHANGELOG.md | 3 + README.md | 2 +- cadCAD/configuration/__init__.py | 132 ++-------------- cadCAD/configuration/utils/__init__.py | 8 +- cadCAD/engine/__init__.py | 58 ++++--- cadCAD/engine/execution.py | 116 +++++++------- cadCAD/engine/simulation.py | 147 +++++++++--------- cadCAD/utils/sys_exec.py | 28 ++-- dist/cadCAD-0.4.17-py3-none-any.whl | Bin 67192 -> 0 bytes dist/cadCAD-0.4.17.tar.gz | Bin 34625 -> 0 bytes documentation/Policy_Aggregation.md | 5 +- documentation/README.md | 21 ++- documentation/examples/example_1.py | 1 + .../examples/historical_state_access.py | 7 +- documentation/examples/param_sweep.py | 5 +- documentation/examples/policy_aggregation.py | 5 +- documentation/examples/sys_model_A.py | 7 +- documentation/examples/sys_model_B.py | 7 +- setup.py | 2 +- .../execs/multi_config_dist.py | 55 +++++++ .../execs/multi_config_test.py | 2 +- .../execs/param_sweep_dist.py | 54 +++++++ .../execs/param_sweep_test2.py | 50 ------ .../regression_tests/execs/policy_agg_dist.py | 54 +++++++ .../regression_tests/experiments/__init__.py | 9 +- .../regression_tests/models/config1.py | 7 +- .../regression_tests/models/config2.py | 7 +- .../models/external_dataset.py | 5 +- .../models/historical_state_access.py | 4 +- .../regression_tests/models/param_sweep.py | 94 +++++++++++ .../models/policy_aggregation.py | 11 +- .../regression_tests/models/sweep_config.py | 24 ++- simulations/regression_tests/models/tests.py | 35 ----- simulations/regression_tests/models/udo.py | 12 +- .../models/udo_inter_substep_update.py | 8 +- testing/experiments/__init__.py | 3 +- testing/generic_test.py | 5 +- testing/models/param_sweep.py | 11 +- testing/models/policy_aggregation.py | 6 +- testing/tests/out_check.py | 13 +- testing/tests/param_sweep.py | 86 ---------- testing/tests/param_sweep_test.py | 88 +++++++++++ ...regation.py => policy_aggregation_test.py} | 0 44 files changed, 640 insertions(+), 559 deletions(-) delete mode 100644 dist/cadCAD-0.4.17-py3-none-any.whl delete mode 100644 dist/cadCAD-0.4.17.tar.gz create mode 100644 simulations/regression_tests/execs/multi_config_dist.py create mode 100644 simulations/regression_tests/execs/param_sweep_dist.py delete mode 100644 simulations/regression_tests/execs/param_sweep_test2.py create mode 100644 simulations/regression_tests/execs/policy_agg_dist.py create mode 100644 simulations/regression_tests/models/param_sweep.py delete mode 100644 simulations/regression_tests/models/tests.py delete mode 100644 testing/tests/param_sweep.py create mode 100644 testing/tests/param_sweep_test.py rename testing/tests/{policy_aggregation.py => policy_aggregation_test.py} (100%) diff --git a/.gitignore b/.gitignore index 0fa80837..fd3b7b8f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ .idea .eggs .pytest_cache +client_work + notebooks *.egg-info __pycache__ diff --git a/CHANGELOG.md b/CHANGELOG.md index 96c3fe52..218b4e72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ # Changelog: +### August 4, 2020 +* Experiment Class: Representation of an experiment as one or more configured System Models + ### June 22, 2020 * Bug Fix: Multiprocessing error for Windows diff --git a/README.md b/README.md index 0b5c34ce..90b9ac7d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ / ___/ __` / __ / / / /| | / / / / / /__/ /_/ / /_/ / /___/ ___ |/ /_/ / \___/\__,_/\__,_/\____/_/ |_/_____/ -by cadCAD ver. 0.4.17 +by cadCAD ver. 0.4.18 ====================================== Complex Adaptive Dynamics o i e diff --git a/cadCAD/configuration/__init__.py b/cadCAD/configuration/__init__.py index 7b61cd88..91a550a9 100644 --- a/cadCAD/configuration/__init__.py +++ b/cadCAD/configuration/__init__.py @@ -1,10 +1,8 @@ -from pprint import pprint from typing import Dict, Callable, List, Tuple from pandas.core.frame import DataFrame from collections import deque from copy import deepcopy import pandas as pd -# from time import time from cadCAD import configs from cadCAD.utils import key_filter @@ -13,11 +11,10 @@ class Configuration(object): - def __init__(self, user_id, sim_config={}, initial_state={}, seeds={}, env_processes={}, + def __init__(self, user_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 ) -> None: - # print(exogenous_states) self.sim_config = sim_config self.initial_state = initial_state self.seeds = seeds @@ -28,11 +25,13 @@ def __init__(self, user_id, sim_config={}, initial_state={}, seeds={}, env_proce self.kwargs = kwargs self.user_id = user_id - self.session_id = session_id # eesntially config id + self.session_id = session_id # essentially config id self.simulation_id = simulation_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) @@ -40,12 +39,13 @@ def __init__(self, user_id, sim_config={}, initial_state={}, seeds={}, env_proce class Experiment: def __init__(self): self.exp_id = 0 + 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, user_id='cadCAD_user', - session_id=0, # ToDo: change to string 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 @@ -71,10 +71,11 @@ def append_configs( sim_cnt = 0 new_sim_configs = [] - for t in 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 @@ -92,8 +93,8 @@ def append_configs( sim_cnt += 1 run_id = 0 - print(self.exp_id) for sim_config in new_sim_configs: + subset_id = sim_config['subset_id'] sim_config['N'] = run_id + 1 if max_runs == 1: sim_config['run_id'] = run_id @@ -115,110 +116,17 @@ def append_configs( user_id=user_id, session_id=f"{user_id}={sim_config['simulation_id']}_{sim_config['run_id']}", simulation_id=sim_config['simulation_id'], - # run_id=run_id_config run_id=sim_config['run_id'], + experiment_id=self.exp_id, - exp_window=self.exp_window + exp_window=self.exp_window, + subset_id=subset_id, + subset_window=self.subset_window ) configs.append(config) run_id += 1 - - # print(exp_cnt) self.exp_id += 1 self.exp_window.appendleft(self.exp_id) - # print() - # print(self.exp_id) - - -# def append_configs( -# user_id='cadCAD_user', -# session_id=0, #ToDo: change to string -# 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 -# ) -> None: -# -# try: -# max_runs = sim_configs[0]['N'] -# except KeyError: -# 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 -# -# sim_cnt = 0 -# new_sim_configs = [] -# for t in list(zip(sim_configs, list(range(len(sim_configs))))): -# sim_config = t[0] -# N = sim_config['N'] -# -# if N > 1: -# for n in range(N): -# sim_config['simulation_id'] = simulation_id + sim_cnt -# 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['run_id'] = 0 -# new_sim_configs.append(deepcopy(sim_config)) -# # del sim_config -# -# sim_cnt += 1 -# -# # print(configs) -# run_id = 0 -# # if len(configs) > 0: -# # ds_run_id = configs[-1].__dict__['run_id'] + 1 -# # # print() -# # # print(configs[-1].__dict__['simulation_id']) -# # # print(configs[-1].__dict__['run_id']) -# # # configs[-1].__dict__['run_id'] -# # # print(configs[-1].__dict__['run_id'] + 1) -# # run_id = ds_run_id + 1 -# -# for sim_config in new_sim_configs: -# sim_config['N'] = run_id + 1 -# if max_runs == 1: -# sim_config['run_id'] = run_id -# elif max_runs >= 1: -# if run_id >= max_runs: -# sim_config['N'] = run_id - (max_runs - 1) -# -# config = Configuration( -# sim_config=sim_config, -# initial_state=initial_state, -# seeds=seeds, -# exogenous_states=exogenous_states, -# env_processes=env_processes, -# partial_state_update_blocks=partial_state_update_blocks, -# policy_ops=policy_ops, -# -# # session_id=session_id, -# user_id=user_id, -# session_id=f"{user_id}={sim_config['simulation_id']}_{sim_config['run_id']}", -# simulation_id=sim_config['simulation_id'], -# # run_id=run_id_config -# run_id=sim_config['run_id'] -# ) -# configs.append(config) -# run_id += 1 -# -# # print(configs) -# # print(f'simulation_id: {configs[-1].__dict__["simulation_id"]}') -# # print(f'run_id: {configs[-1].__dict__["run_id"]}') class Identity: @@ -300,7 +208,7 @@ def only_ep_handler(state_dict): return sdf_values, bdf_values if len(partial_state_updates) != 0: - # backwards compatibility # + # backwards compatibility partial_state_updates = sanitize_partial_state_updates(partial_state_updates) bdf = self.create_matrix_field(partial_state_updates, 'policies') @@ -312,13 +220,3 @@ def only_ep_handler(state_dict): zipped_list = list(zip(sdf_values, bdf_values)) return list(map(lambda x: (x[0] + exo_proc, x[1]), zipped_list)) - -# def timing_val(func): -# def wrapper(*arg, **kw): -# '''source: http://www.daniweb.com/code/snippet368.html''' -# t1 = time() -# res = func(*arg, **kw) -# t2 = time() -# print(f"{func.__name__}: {t2-t1 :.5f}") -# return res -# return wrapper diff --git a/cadCAD/configuration/utils/__init__.py b/cadCAD/configuration/utils/__init__.py index d9e9d014..3e85afa6 100644 --- a/cadCAD/configuration/utils/__init__.py +++ b/cadCAD/configuration/utils/__init__.py @@ -1,5 +1,5 @@ -from collections import Counter from datetime import datetime, timedelta +from collections import Counter from copy import deepcopy from functools import reduce from funcy import curry @@ -41,9 +41,9 @@ def configs_as_spec(configs): def configs_as_objs(configs): counted_IDs_configs = configs_as_spec(configs) - new_config = list(map(lambda x: x[1], counted_IDs_configs)) + new_configs = list(map(lambda x: x[1], counted_IDs_configs)) del counted_IDs_configs - return new_config + return new_configs def configs_as_dicts(configs): @@ -79,7 +79,6 @@ def bound_norm_random(rng, low, high): tstep_delta = timedelta(days=0, minutes=0, seconds=30) def time_step(dt_str, dt_format='%Y-%m-%d %H:%M:%S', _timedelta = tstep_delta): - # print(dt_str) dt = datetime.strptime(dt_str, dt_format) t = dt + _timedelta return t.strftime(dt_format) @@ -87,7 +86,6 @@ def time_step(dt_str, dt_format='%Y-%m-%d %H:%M:%S', _timedelta = tstep_delta): ep_t_delta = timedelta(days=0, minutes=0, seconds=1) def ep_time_step(s_condition, dt_str, fromat_str='%Y-%m-%d %H:%M:%S', _timedelta = ep_t_delta): - # print(dt_str) if s_condition: return time_step(dt_str, fromat_str, _timedelta) else: diff --git a/cadCAD/engine/__init__.py b/cadCAD/engine/__init__.py index 8c1e27db..bb8af18b 100644 --- a/cadCAD/engine/__init__.py +++ b/cadCAD/engine/__init__.py @@ -1,15 +1,13 @@ -from pprint import pprint from time import time from typing import Callable, Dict, List, Any, Tuple -# from time import time -# from tqdm import tqdm -from cadCAD import logo + 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 +from cadCAD.configuration.utils import TensorFieldReport, configs_as_objs 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]] @@ -39,10 +37,18 @@ def __init__(self, context=ExecutionMode.local_mode, method=None, additional_obj elif context == 'dist_proc': def distroduce_proc( simulation_execs, var_dict_list, states_lists, configs_structs, env_processes_list, Ts, RunIDs, + ExpIDs, + SubsetIDs, + SubsetWindows, + exec_method, sc, additional_objs=additional_objs ): return method( simulation_execs, var_dict_list, states_lists, configs_structs, env_processes_list, Ts, RunIDs, + ExpIDs, + SubsetIDs, + SubsetWindows, + exec_method, sc, additional_objs ) @@ -52,7 +58,7 @@ def distroduce_proc( class Executor: def __init__(self, exec_context: ExecutionContext, configs: List[Configuration], spark_context=None - ) -> None: + ) -> None: self.sc = spark_context self.SimExecutor = SimExecutor self.exec_method = exec_context.method @@ -65,22 +71,21 @@ def execute(self) -> Tuple[Any, Any, Dict[str, Any]]: sessions = [] var_dict_list, states_lists = [], [] - Ts, Ns, SimIDs, RunIDs, ExpIDs, ExpWindows = [], [], [], [], [], [] + Ts, Ns, SimIDs, RunIDs = [], [], [], [] + ExpIDs, ExpWindows, SubsetIDs, SubsetWindows = [], [], [], [] eps, configs_structs, env_processes_list = [], [], [] partial_state_updates, sim_executors = [], [] config_idx = 0 - print_exec_info(self.exec_context, self.configs) + print_exec_info(self.exec_context, configs_as_objs(self.configs)) -# danlessa_experiments -# for x in tqdm(self.configs, -# desc='Initializing configurations'): t1 = time() for x in self.configs: sessions.append( { 'user_id': x.user_id, 'experiment_id': x.experiment_id, 'session_id': x.session_id, - 'simulation_id': x.simulation_id, 'run_id': x.run_id + 'simulation_id': x.simulation_id, 'run_id': x.run_id, + 'subset_id': x.subset_id, 'subset_window': x.subset_window } ) Ts.append(x.sim_config['T']) @@ -90,6 +95,8 @@ def execute(self) -> Tuple[Any, Any, Dict[str, Any]]: ExpWindows.append(x.exp_window) SimIDs.append(x.simulation_id) RunIDs.append(x.run_id) + SubsetIDs.append(x.subset_id) + SubsetWindows.append(x.subset_window) var_dict_list.append(x.sim_config['M']) states_lists.append([x.initial_state]) @@ -114,7 +121,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): # and (config_amt < remote_threshold): return flat_simulations, tensor_fields, sessions remote_threshold = 100 @@ -124,16 +131,17 @@ 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): # and (config_amt < remote_threshold): return ExecutionMode.multi_mode, parallelize_simulations except AttributeError: if config_amt < 1: - print('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!') + # elif config_amt > remote_threshold: + # print('Remote Threshold is N=100. Use ExecutionMode.dist_proc if N >= 100') final_result = None - original_context = self.exec_context + # original_context = self.exec_context + original_N = len(configs_as_dicts(self.configs)) if self.exec_context != ExecutionMode.distributed: # Consider Legacy Support if self.exec_context != ExecutionMode.local_mode: @@ -142,14 +150,14 @@ def auto_mode_switcher(config_amt): print("Execution Method: " + self.exec_method.__name__) simulations_results = self.exec_method( sim_executors, var_dict_list, states_lists, configs_structs, env_processes_list, Ts, SimIDs, RunIDs, - ExpIDs #Ns + 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__) simulations_results = self.exec_method( sim_executors, var_dict_list, states_lists, configs_structs, env_processes_list, Ts, - SimIDs, RunIDs, ExpIDs, self.sc + SimIDs, RunIDs, ExpIDs, SubsetIDs, SubsetWindows, original_N, self.sc ) final_result = get_final_dist_results(simulations_results, partial_state_updates, eps, sessions) @@ -157,13 +165,3 @@ def auto_mode_switcher(config_amt): print(f"Total execution time: {t2 - t1 :.2f}s") return final_result - -# danlessa_experiments: -> get_final_dist_results -# results = [] -# zipped_results = zip(simulations, partial_state_updates, eps) -# for result, partial_state_updates, ep in tqdm(zipped_results, -# total=len(simulations), -# desc='Flattening results'): -# results.append((flatten(result), create_tensor_field(partial_state_updates, ep))) - -# final_result = results diff --git a/cadCAD/engine/execution.py b/cadCAD/engine/execution.py index 93c933ea..2caa5f0d 100644 --- a/cadCAD/engine/execution.py +++ b/cadCAD/engine/execution.py @@ -1,4 +1,3 @@ -from pprint import pprint from typing import Callable, Dict, List, Any, Tuple from pathos.multiprocessing import ThreadPool as TPool from pathos.multiprocessing import ProcessPool as PPool @@ -13,44 +12,53 @@ def single_proc_exec( - simulation_execs: List[Callable], - var_dict_list: List[VarDictType], - states_lists: List[StatesListsType], - configs_structs: List[ConfigsType], - env_processes_list: List[EnvProcessesType], - Ts: List[range], - SimIDs, - Ns: List[int], - ExpIDs: List[int] - ): + simulation_execs: List[Callable], + var_dict_list: List[VarDictType], + states_lists: List[StatesListsType], + configs_structs: List[ConfigsType], + env_processes_list: List[EnvProcessesType], + Ts: List[range], + SimIDs, + Ns: List[int], + ExpIDs: List[int], + SubsetIDs, + SubsetWindows, + configured_n +): print(f'Execution Mode: single_threaded') - params = [simulation_execs, states_lists, configs_structs, env_processes_list, Ts, SimIDs, Ns] - simulation_exec, states_list, config, env_processes, T, SimID, N = list(map(lambda x: x.pop(), params)) - result = simulation_exec(var_dict_list, states_list, config, env_processes, T, SimID, N) + params = [ + simulation_execs, states_lists, configs_structs, env_processes_list, Ts, SimIDs, Ns, SubsetIDs, SubsetWindows + ] + simulation_exec, states_list, config, env_processes, T, sim_id, N, subset_id, subset_window = list( + map(lambda x: x.pop(), params) + ) + result = simulation_exec( + var_dict_list, states_list, config, env_processes, T, sim_id, N, subset_id, subset_window, configured_n + ) return flatten(result) def parallelize_simulations( - simulation_execs: List[Callable], - var_dict_list: List[VarDictType], - states_lists: List[StatesListsType], - configs_structs: List[ConfigsType], - env_processes_list: List[EnvProcessesType], - Ts: List[range], - SimIDs, - Ns: List[int], - ExpIDs: List[int] - ): - # indexed sim_ids - # SimIDs = list(range(len(SimIDs))) - # print(SimIDs) - # print(list(range(len(Ns)))) - # print(Ns) - # exit() + simulation_execs: List[Callable], + var_dict_list: List[VarDictType], + states_lists: List[StatesListsType], + configs_structs: List[ConfigsType], + env_processes_list: List[EnvProcessesType], + Ts: List[range], + SimIDs, + Ns: List[int], + ExpIDs: List[int], + SubsetIDs, + SubsetWindows, + configured_n +): print(f'Execution Mode: parallelized') params = list( - zip(simulation_execs, var_dict_list, states_lists, configs_structs, env_processes_list, Ts, SimIDs, Ns) + zip( + simulation_execs, var_dict_list, states_lists, configs_structs, env_processes_list, + Ts, SimIDs, Ns, SubsetIDs, SubsetWindows + ) ) len_configs_structs = len(configs_structs) @@ -60,7 +68,6 @@ def parallelize_simulations( highest_divisor = int(len_configs_structs / sim_count) new_configs_structs, new_params = [], [] - print() for count in range(sim_count): if count == 0: new_params.append( @@ -70,7 +77,6 @@ def parallelize_simulations( configs_structs[count: highest_divisor] ) elif count > 0: - # print(params) new_params.append( params[count * highest_divisor: (count + 1) * highest_divisor] ) @@ -78,39 +84,30 @@ def parallelize_simulations( configs_structs[count * highest_divisor: (count + 1) * highest_divisor] ) - print(SimIDs) - print(Ns) - print(ExpIDs) - # pprint(new_configs_structs) - # exit() def threaded_executor(params): - # tp = TPool(len_configs_structs) tp = TPool() if len_configs_structs > 1: - results = tp.map(lambda t: t[0](t[1], t[2], t[3], t[4], t[5], t[6], t[7]), params) + results = tp.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 + ) else: t = params[0] - results = t[0](t[1], t[2], t[3], t[4], t[5], t[6], t[7]) + 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 - # len_new_configs_structs = len(new_configs_structs) - # pp = PPool(len_new_configs_structs) - # len(new_params) - # print('params len: '+ str(len(new_params))) - - # pprint(params) - # print() - pp = PPool() results = flatten(list(pp.map(lambda params: threaded_executor(params), new_params))) pp.close() + pp.join() + pp.clear() + # pp.restart() + return results -remote_threshold = 100 def local_simulations( simulation_execs: List[Callable], var_dict_list: List[VarDictType], @@ -120,19 +117,26 @@ def local_simulations( Ts: List[range], SimIDs, Ns: List[int], - ExpIDs: List[int] + ExpIDs: List[int], + SubsetIDs, + 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: - if len(configs_structs) == 1: + if config_amt == 1: return single_proc_exec( simulation_execs, var_dict_list, states_lists, configs_structs, env_processes_list, Ts, SimIDs, Ns, - ExpIDs + ExpIDs, SubsetIDs, SubsetWindows, configured_n ) - elif len(configs_structs) > 1 and config_amt < remote_threshold: + elif config_amt > 1: # and config_amt < remote_threshold: return parallelize_simulations( simulation_execs, var_dict_list, states_lists, configs_structs, env_processes_list, Ts, SimIDs, Ns, - ExpIDs + ExpIDs, SubsetIDs, SubsetWindows, configured_n ) except ValueError: - print('ValueError: sim_configs\' N must > 0') + raise ValueError("\'sim_configs\' N must > 0") diff --git a/cadCAD/engine/simulation.py b/cadCAD/engine/simulation.py index 3ded8288..b9a57ba8 100644 --- a/cadCAD/engine/simulation.py +++ b/cadCAD/engine/simulation.py @@ -1,14 +1,10 @@ -from pprint import pprint from typing import Any, Callable, Dict, List, Tuple -from pathos.pools import ThreadPool as TPool -from functools import reduce -from types import MappingProxyType from copy import deepcopy from functools import reduce from funcy import curry -from cadCAD.engine.utils import engine_exception from cadCAD.utils import flatten +from cadCAD.engine.utils import engine_exception id_exception: Callable = curry(engine_exception)(KeyError)(KeyError)(None) @@ -26,14 +22,14 @@ def __init__( self.policy_update_exception = policy_update_exception def get_policy_input( - self, - sweep_dict: Dict[str, List[Any]], - sub_step: int, - sL: List[Dict[str, Any]], - s: Dict[str, Any], - funcs: List[Callable], - additional_objs - ) -> Dict[str, Any]: + self, + sweep_dict: Dict[str, List[Any]], + sub_step: int, + sL: List[Dict[str, Any]], + s: Dict[str, Any], + funcs: List[Callable], + additional_objs + ) -> Dict[str, Any]: ops = self.policy_ops @@ -73,11 +69,11 @@ def compose(init_reduction_funct, funct_list, val_list): } def apply_env_proc( - self, - sweep_dict, - env_processes: Dict[str, Callable], - state_dict: Dict[str, Any] - ) -> Dict[str, Any]: + self, + sweep_dict, + env_processes: Dict[str, Callable], + state_dict: Dict[str, Any] + ) -> Dict[str, Any]: def env_composition(target_field, state_dict, target_value): function_type = type(lambda x: x) @@ -105,18 +101,18 @@ def env_composition(target_field, state_dict, target_value): # mech_step def partial_state_update( - self, - sweep_dict: Dict[str, List[Any]], - sub_step: int, - sL, - sH, - state_funcs: List[Callable], - policy_funcs: List[Callable], - env_processes: Dict[str, Callable], - time_step: int, - run: int, - additional_objs - ) -> List[Dict[str, Any]]: + self, + sweep_dict: Dict[str, List[Any]], + sub_step: int, + sL, + sH, + state_funcs: List[Callable], + policy_funcs: List[Callable], + env_processes: Dict[str, Callable], + time_step: int, + run: int, + additional_objs + ) -> List[Dict[str, Any]]: # last_in_obj: Dict[str, Any] = MappingProxyType(sL[-1]) last_in_obj: Dict[str, Any] = deepcopy(sL[-1]) @@ -152,15 +148,15 @@ def transfer_missing_fields(source, destination): # mech_pipeline - state_update_block def state_update_pipeline( - self, - sweep_dict: Dict[str, List[Any]], - simulation_list, - configs: List[Tuple[List[Callable], List[Callable]]], - env_processes: Dict[str, Callable], - time_step: int, - run: int, - additional_objs - ) -> List[Dict[str, Any]]: + self, + sweep_dict: Dict[str, List[Any]], + simulation_list, + configs: List[Tuple[List[Callable], List[Callable]]], + env_processes: Dict[str, Callable], + time_step: int, + run: int, + additional_objs + ) -> List[Dict[str, Any]]: sub_step = 0 states_list_copy: List[Dict[str, Any]] = tuple(simulation_list[-1]) @@ -176,7 +172,8 @@ def state_update_pipeline( sub_step += 1 for [s_conf, p_conf] in configs: states_list: List[Dict[str, Any]] = self.partial_state_update( - sweep_dict, sub_step, states_list, simulation_list, s_conf, p_conf, env_processes, time_step, run, additional_objs + sweep_dict, sub_step, states_list, simulation_list, s_conf, p_conf, env_processes, time_step, run, + additional_objs ) sub_step += 1 @@ -186,15 +183,15 @@ def state_update_pipeline( # state_update_pipeline def run_pipeline( - self, - sweep_dict: Dict[str, List[Any]], - states_list: List[Dict[str, Any]], - configs: List[Tuple[List[Callable], List[Callable]]], - env_processes: Dict[str, Callable], - time_seq: range, - run: int, - additional_objs - ) -> List[List[Dict[str, Any]]]: + self, + sweep_dict: Dict[str, List[Any]], + states_list: List[Dict[str, Any]], + configs: List[Tuple[List[Callable], List[Callable]]], + env_processes: Dict[str, Callable], + time_seq: range, + run: int, + additional_objs + ) -> List[List[Dict[str, Any]]]: time_seq: List[int] = [x + 1 for x in time_seq] simulation_list: List[List[Dict[str, Any]]] = [states_list] @@ -209,39 +206,39 @@ def run_pipeline( return simulation_list def simulation( - self, - sweep_dict: Dict[str, List[Any]], - states_list: List[Dict[str, Any]], - configs, - env_processes: Dict[str, Callable], - time_seq: range, - simulation_id: int, - run: int, - additional_objs=None + self, + sweep_dict: Dict[str, List[Any]], + states_list: List[Dict[str, Any]], + configs, + env_processes: Dict[str, Callable], + time_seq: range, + simulation_id: int, + run: int, + subset_id, + subset_window, + configured_N, + # remote_ind + additional_objs=None ): run += 1 - # print(run) - def execute_run(sweep_dict, states_list, configs, env_processes, time_seq, _run) -> List[Dict[str, Any]]: - # _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 - def generate_init_sys_metrics(genesis_states_list, sim_id, _run): - # pprint(genesis_states_list) - # print() - # for d in genesis_states_list.asDict(): + 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): for D in genesis_states_list: d = deepcopy(D) - # d = D - # print(str(sim_id) + ' ' + ' ' + str(_run)) - d['simulation'], d['run'], d['substep'], d['timestep'] = sim_id, _run, 0, 0 - # d['simulation'], d['run'], d['substep'], d['timestep'] = _run, sim_id, 0, 0 - # print('simulation_id') - # print(simulation_id) + d['simulation'], d['subset'], d['run'], d['substep'], d['timestep'] = \ + sim_id, _subset_id, _run, 0, 0 yield d - # print(tuple(states_list)) - states_list_copy: List[Dict[str, Any]] = list(generate_init_sys_metrics(tuple(states_list), simulation_id, run)) - # simulation_id = + 1 + states_list_copy: List[Dict[str, Any]] = list( + generate_init_sys_metrics(tuple(states_list), simulation_id, subset_id, run, subset_window) + ) first_timestep_per_run: List[Dict[str, Any]] = self.run_pipeline( sweep_dict, states_list_copy, configs, env_processes, time_seq, run, additional_objs @@ -250,8 +247,6 @@ def generate_init_sys_metrics(genesis_states_list, sim_id, _run): return first_timestep_per_run - # print('sim_id: ' + str(simulation_id)) - # print('run_id: ' + str(run)) pipe_run = flatten( [execute_run(sweep_dict, states_list, configs, env_processes, time_seq, run)] ) diff --git a/cadCAD/utils/sys_exec.py b/cadCAD/utils/sys_exec.py index 75222134..7c7559df 100644 --- a/cadCAD/utils/sys_exec.py +++ b/cadCAD/utils/sys_exec.py @@ -1,10 +1,14 @@ 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(): @@ -32,21 +36,27 @@ def to_pandas(rdd: RDD): return pdf_from_rdd -def to_pandas_df(rdd: RDD, init_condition: dict = None): - # Typefull - if init_condition is not None: - return to_spark(rdd, init_condition).toPandas() - # Typeless +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, init_condition: dict = None): - # Typefull - if init_condition is not None: +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) - # Typeless + 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) diff --git a/dist/cadCAD-0.4.17-py3-none-any.whl b/dist/cadCAD-0.4.17-py3-none-any.whl deleted file mode 100644 index 8ba29842ee69f46ded6944f2b46f912995abc99a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67192 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 diff --git a/dist/cadCAD-0.4.17.tar.gz b/dist/cadCAD-0.4.17.tar.gz deleted file mode 100644 index 76a1eabc45d09f36a0689e00c11cba49d3bd7d40..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34625 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 F0|5Qxp$Gr~ diff --git a/documentation/Policy_Aggregation.md b/documentation/Policy_Aggregation.md index 296c707b..3b89e394 100644 --- a/documentation/Policy_Aggregation.md +++ b/documentation/Policy_Aggregation.md @@ -46,9 +46,10 @@ def p2_psu3(_params, step, sH, s, **kwargs): #### Aggregate Policies using functions ```python -from cadCAD.configuration import append_configs +from cadCAD.configuration import Experiment -append_configs( +exp = Experiment() +exp.append_configs( sim_configs=???, initial_state=???, partial_state_update_blocks=???, diff --git a/documentation/README.md b/documentation/README.md index 27f146cb..2dde7432 100644 --- a/documentation/README.md +++ b/documentation/README.md @@ -15,9 +15,10 @@ A Simulation Configuration is comprised of a [System Model](#System-Model) and a `append_configs`, stores a **Simulation Configuration** to be [Executed](/JS4Q9oayQASihxHBJzz4Ug) by cadCAD ```python -from cadCAD.configuration import append_configs +from cadCAD.configuration import Experiment -append_configs( +exp = Experiment() +exp.append_configs( user_id = ..., # OPTIONAL: cadCAD Session User ID initial_state = ..., # System Model partial_state_update_blocks = ..., # System Model @@ -38,8 +39,8 @@ Simulation properties are passed to `append_configs` in the `sim_configs` parame use the `config_sim` function in `cadCAD.configuration.utils` ```python -from cadCAD.configuration import append_configs from cadCAD.configuration.utils import config_sim +from cadCAD.configuration import Experiment sim_config_dict = { "N": ..., @@ -49,7 +50,8 @@ sim_config_dict = { c = config_sim(sim_config_dict) -append_configs( +exp = Experiment() +exp.append_configs( ... sim_configs = c # Simulation Properties ) @@ -100,7 +102,7 @@ State Variables are passed to `append_configs` along with its initial values, as are the names of the variables and the `dict_values` are their initial values. ```python -from cadCAD.configuration import append_configs +from cadCAD.configuration import Experiment genesis_states = { 'state_variable_1': 0, @@ -109,7 +111,8 @@ genesis_states = { 'timestamp': '2019-01-01 00:00:00' } -append_configs( +exp = Experiment() +exp.append_configs( initial_state = genesis_states, ... ) @@ -202,6 +205,8 @@ Partial State Update Blocks are passed to `append_configs` as a List of Python ` state update functions and the values are the functions. ```python +from cadCAD.configuration import Experiment + PSUBs = [ { "policies": { @@ -220,12 +225,12 @@ PSUBs = [ {...} #PSUB_M ] -append_configs( +exp = Experiment() +exp.append_configs( ... partial_state_update_blocks = PSUBs, ... ) - ``` #### Substep diff --git a/documentation/examples/example_1.py b/documentation/examples/example_1.py index c019c49a..ffa21b7f 100644 --- a/documentation/examples/example_1.py +++ b/documentation/examples/example_1.py @@ -2,6 +2,7 @@ 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 diff --git a/documentation/examples/historical_state_access.py b/documentation/examples/historical_state_access.py index 714d7f40..5ca9015b 100644 --- a/documentation/examples/historical_state_access.py +++ b/documentation/examples/historical_state_access.py @@ -1,11 +1,11 @@ import pandas as pd from tabulate import tabulate -from cadCAD.configuration import append_configs + 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'] @@ -87,7 +87,8 @@ def fourth_to_last_x(_g, substep, sH, s, _input): } ) -append_configs( +exp = Experiment() +exp.append_configs( sim_configs=sim_config, initial_state=genesis_states, partial_state_update_blocks=psubs diff --git a/documentation/examples/param_sweep.py b/documentation/examples/param_sweep.py index f664b4d1..9d10830e 100644 --- a/documentation/examples/param_sweep.py +++ b/documentation/examples/param_sweep.py @@ -4,9 +4,9 @@ import pandas as pd from tabulate import tabulate -from cadCAD.configuration import append_configs 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) @@ -92,7 +92,8 @@ def sweeped(_params, step, sH, s, _input): pp.pprint(psu_block) print() -append_configs( +exp = Experiment() +exp.append_configs( sim_configs=sim_config, initial_state=genesis_states, env_processes=env_process, diff --git a/documentation/examples/policy_aggregation.py b/documentation/examples/policy_aggregation.py index 6810793c..2a0edb95 100644 --- a/documentation/examples/policy_aggregation.py +++ b/documentation/examples/policy_aggregation.py @@ -1,9 +1,9 @@ import pandas as pd from tabulate import tabulate -from cadCAD.configuration import append_configs 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 @@ -76,7 +76,8 @@ def policies(_g, step, sH, s, _input): } ) -append_configs( +exp = Experiment() +exp.append_configs( sim_configs=sim_config, initial_state=genesis_states, partial_state_update_blocks=psubs, diff --git a/documentation/examples/sys_model_A.py b/documentation/examples/sys_model_A.py index 5c54dbe1..13ade45a 100644 --- a/documentation/examples/sys_model_A.py +++ b/documentation/examples/sys_model_A.py @@ -1,9 +1,8 @@ import numpy as np from datetime import timedelta - -from cadCAD.configuration import append_configs from cadCAD.configuration.utils import bound_norm_random, config_sim, time_step, env_trigger +from cadCAD.configuration import Experiment seeds = { 'z': np.random.RandomState(1), @@ -94,7 +93,6 @@ def update_timestamp(_g, step, sH, s, _input): # Environment Process -# ToDo: Depreciation Waring for env_proc_trigger convention 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], @@ -150,7 +148,8 @@ def update_timestamp(_g, step, sH, s, _input): } ) -append_configs( +exp = Experiment() +exp.append_configs( sim_configs=sim_config, initial_state=genesis_states, env_processes=env_processes, diff --git a/documentation/examples/sys_model_B.py b/documentation/examples/sys_model_B.py index 2c6ad9e2..b9747d58 100644 --- a/documentation/examples/sys_model_B.py +++ b/documentation/examples/sys_model_B.py @@ -1,8 +1,8 @@ import numpy as np from datetime import timedelta -from cadCAD.configuration import append_configs from cadCAD.configuration.utils import bound_norm_random, config_sim, env_trigger, time_step +from cadCAD.configuration import Experiment seeds = { 'z': np.random.RandomState(1), @@ -88,7 +88,6 @@ def update_timestamp(_g, step, sH, s, _input): # Environment Process -# ToDo: Depreciation Waring for env_proc_trigger convention 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], @@ -131,7 +130,6 @@ def update_timestamp(_g, step, sH, s, _input): } ] - sim_config = config_sim( { "N": 2, @@ -139,7 +137,8 @@ def update_timestamp(_g, step, sH, s, _input): } ) -append_configs( +exp = Experiment() +exp.append_configs( sim_configs=sim_config, initial_state=genesis_states, env_processes=env_processes, diff --git a/setup.py b/setup.py index 41ad37b6..decfa13b 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ """ setup(name='cadCAD', - version='0.4.17', + version='0.4.18', description=short_description, long_description=long_description, url='https://github.com/cadCAD-org/cadCAD', diff --git a/simulations/regression_tests/execs/multi_config_dist.py b/simulations/regression_tests/execs/multi_config_dist.py new file mode 100644 index 00000000..9d690430 --- /dev/null +++ b/simulations/regression_tests/execs/multi_config_dist.py @@ -0,0 +1,55 @@ +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) + +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 810cf8f3..919226f7 100644 --- a/simulations/regression_tests/execs/multi_config_test.py +++ b/simulations/regression_tests/execs/multi_config_test.py @@ -1,6 +1,6 @@ +from tabulate import tabulate 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 config1, config2 diff --git a/simulations/regression_tests/execs/param_sweep_dist.py b/simulations/regression_tests/execs/param_sweep_dist.py new file mode 100644 index 00000000..901e8e71 --- /dev/null +++ b/simulations/regression_tests/execs/param_sweep_dist.py @@ -0,0 +1,54 @@ +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) +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_test2.py b/simulations/regression_tests/execs/param_sweep_test2.py deleted file mode 100644 index 0b1d2cd3..00000000 --- a/simulations/regression_tests/execs/param_sweep_test2.py +++ /dev/null @@ -1,50 +0,0 @@ -from pprint import pprint - -import pandas as pd -from tabulate import tabulate -from cadCAD.engine import ExecutionMode, ExecutionContext, Executor -from cadCAD.configuration.utils import configs_as_objs -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) -# -raw_result, tensor_fields, sessions = run.execute() -result = pd.DataFrame(raw_result) -print(result.head(10)) -# print(tabulate(tensor_fields[0], headers='keys', tablefmt='psql')) -# pprint(sessions) -# print(tabulate(result.head(), headers='keys', tablefmt='psql')) - -a = configs -b = configs_as_objs(configs) - -for config in configs: - print() - # pprint(config.__dict__) - print('simulation_id: '+ str(config.__dict__['simulation_id'])) - print('run_id: '+ str(config.__dict__['run_id'])) - print('N: ' + str(config.__dict__['sim_config']['N'])) - # print('M: ' + str(config.__dict__['sim_config']['M'])) - # print('sim_config:') - # pprint(config.__dict__['sim_config']) - # print() - -# pprint(a) -# print() -# print() -# pprint(b[0].sim_config) -# pprint(b[0]['sim_config']) - -configs -configs = configs_as_objs(configs) - - -# {'run_id': 0, -# 'session_id': 'cadCAD_user=0_0', -# 'simulation_id': 0, -# 'user_id': 'cadCAD_user' -# } \ No newline at end of file diff --git a/simulations/regression_tests/execs/policy_agg_dist.py b/simulations/regression_tests/execs/policy_agg_dist.py new file mode 100644 index 00000000..2c2060b5 --- /dev/null +++ b/simulations/regression_tests/execs/policy_agg_dist.py @@ -0,0 +1,54 @@ +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) + +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/experiments/__init__.py b/simulations/regression_tests/experiments/__init__.py index 947ce42d..6d4cf830 100644 --- a/simulations/regression_tests/experiments/__init__.py +++ b/simulations/regression_tests/experiments/__init__.py @@ -1,3 +1,10 @@ from cadCAD.configuration import Experiment -exp_a = Experiment() \ No newline at end of file +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 bfc80651..80ddc607 100644 --- a/simulations/regression_tests/models/config1.py +++ b/simulations/regression_tests/models/config1.py @@ -2,9 +2,8 @@ from datetime import timedelta from cadCAD import configs -# from cadCAD.configuration import append_configs from cadCAD.configuration.utils import bound_norm_random, config_sim, time_step, env_trigger -from simulations.regression_tests.experiments import exp_a +from simulations.regression_tests.experiments import config1_exp seeds = { 'z': np.random.RandomState(1), @@ -95,7 +94,6 @@ def update_timestamp(_g, step, sL, s, _input, **kwargs): # Environment Process -# ToDo: Depreciation Waring for env_proc_trigger convention 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], @@ -149,8 +147,7 @@ def update_timestamp(_g, step, sL, s, _input, **kwargs): } sim_config = config_sim(sim_config_dict) -# exp_a = Experiment() -exp_a.append_configs( +config1_exp.append_configs( config_list=configs, user_id='user_a', sim_configs=sim_config, diff --git a/simulations/regression_tests/models/config2.py b/simulations/regression_tests/models/config2.py index 2f66794e..392734c4 100644 --- a/simulations/regression_tests/models/config2.py +++ b/simulations/regression_tests/models/config2.py @@ -2,9 +2,8 @@ from datetime import timedelta from cadCAD import configs -# from cadCAD.configuration import append_configs from cadCAD.configuration.utils import bound_norm_random, config_sim, env_trigger, time_step -from simulations.regression_tests.experiments import exp_a +from simulations.regression_tests.experiments import config2_exp seeds = { 'z': np.random.RandomState(1), @@ -90,7 +89,6 @@ def update_timestamp(_g, step, sL, s, _input, **kwargs): # Environment Process -# ToDo: Depreciation Waring for env_proc_trigger convention 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], @@ -140,8 +138,7 @@ def update_timestamp(_g, step, sL, s, _input, **kwargs): sim_config = config_sim(sim_config_dict) -# exp_a = Experiment() -exp_a.append_configs( +config2_exp.append_configs( config_list=configs, user_id='user_b', sim_configs=sim_config, diff --git a/simulations/regression_tests/models/external_dataset.py b/simulations/regression_tests/models/external_dataset.py index a1efc574..4458c084 100644 --- a/simulations/regression_tests/models/external_dataset.py +++ b/simulations/regression_tests/models/external_dataset.py @@ -1,7 +1,7 @@ -from cadCAD.configuration import append_configs 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')) @@ -21,7 +21,6 @@ def p2(_g, substep, sL, s, **kwargs): del result_dict["ds1"], result_dict["ds2"] return {k: list(v.values()).pop() for k, v in result_dict.items()} -# ToDo: SilentDF(df) wont work #integrate_ext_dataset def integrate_ext_dataset(_g, step, sL, s, _input, **kwargs): result_dict = query(s, df).to_dict() @@ -59,7 +58,7 @@ def view_policies(_g, step, sL, s, _input, **kwargs): "T": range(4) }) -append_configs( +ext_ds_exp.append_configs( sim_configs=sim_config, initial_state=state_dict, partial_state_update_blocks=partial_state_update_blocks, diff --git a/simulations/regression_tests/models/historical_state_access.py b/simulations/regression_tests/models/historical_state_access.py index 20987880..c8c3a604 100644 --- a/simulations/regression_tests/models/historical_state_access.py +++ b/simulations/regression_tests/models/historical_state_access.py @@ -1,5 +1,5 @@ -from cadCAD.configuration import append_configs 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'] @@ -84,7 +84,7 @@ def fourth_to_last_x(_g, substep, sH, s, _input, **kwargs): ) -append_configs( +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 new file mode 100644 index 00000000..ed64e142 --- /dev/null +++ b/simulations/regression_tests/models/param_sweep.py @@ -0,0 +1,94 @@ +import pprint +from typing import Dict, List + +# from cadCAD.configuration import append_configs +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) + +def some_function(x): + return x + +# Optional +# dict must contain lists opf 2 distinct lengths +g: Dict[str, List[int]] = { + 'alpha': [1], + 'beta': [2, some_function], + 'gamma': [3, 4], + # 'beta': [1], + # 'gamma': [4], + 'omega': [7] +} + +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) +env_process = {} + + +# ['s1', 's2', 's3', 's4'] +# Policies per Mechanism +def gamma(_g, step, sL, s, **kwargs): + return {'gamma': _g['gamma']} + + +def omega(_g, step, sL, s, **kwargs): + return {'omega': _g['omega']} + + +# Internal States per Mechanism +def alpha(_g, step, sL, s, _input, **kwargs): + return 'alpha', _g['alpha'] + + +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) + + +# Genesis States +genesis_states = { + 'alpha': 0, + 'beta': 0, + 'policies': {}, + 'sweeped': {} +} + +# Environment Process +env_process['sweeped'] = env_timestep_trigger(trigger_field='timestep', trigger_vals=[5], funct_list=[lambda _g, x: _g['beta']]) + + +sim_config = config_sim( + { + "N": 2, + "T": range(2), + "M": g, # Optional + } +) + +# New Convention +partial_state_update_blocks = psub_list(psu_block, psu_steps) +exp_param_sweep.append_configs( + sim_configs=sim_config, + initial_state=genesis_states, + env_processes=env_process, + partial_state_update_blocks=partial_state_update_blocks +) diff --git a/simulations/regression_tests/models/policy_aggregation.py b/simulations/regression_tests/models/policy_aggregation.py index e3035f9a..d499175f 100644 --- a/simulations/regression_tests/models/policy_aggregation.py +++ b/simulations/regression_tests/models/policy_aggregation.py @@ -1,8 +1,8 @@ -from cadCAD.configuration import append_configs 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,10 +74,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) -append_configs( +policy_exp.append_configs( sim_configs=sim_config, initial_state=genesis_states, partial_state_update_blocks=partial_state_update_block, - # ToDo: subsequent functions should include policy dict for access to each policy (i.e shouldnt be a map) - policy_ops=[lambda a, b: a + b, lambda y: y * 2] # Default: lambda a, b: a + b ToDO: reduction function requires high lvl explanation -) \ No newline at end of file + policy_ops=[lambda a, b: a + b, lambda y: y * 2] # Default: lambda a, b: a + b +) diff --git a/simulations/regression_tests/models/sweep_config.py b/simulations/regression_tests/models/sweep_config.py index 35ebf793..38306bc4 100644 --- a/simulations/regression_tests/models/sweep_config.py +++ b/simulations/regression_tests/models/sweep_config.py @@ -2,11 +2,12 @@ from datetime import timedelta import pprint -from cadCAD.configuration import append_configs from cadCAD.configuration.utils import env_trigger, var_substep_trigger, config_sim, time_step, psub_list from typing import Dict, List +from simulations.regression_tests.experiments import sweep_exp + pp = pprint.PrettyPrinter(indent=4) seeds = { @@ -110,12 +111,6 @@ def es4(_g, step, sL, s, _input, **kwargs): for m in ['m1','m2','m3']: psu_block[m]["variables"]['s4'] = var_timestep_trigger(y='s4', f=es4) - -# 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 - # Genesis States genesis_states = { 's1': 0.0, @@ -127,7 +122,6 @@ def es4(_g, step, sL, s, _input, **kwargs): # Environment Process -# ToDo: Validate - make env proc trigger field agnostic 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']]) @@ -135,7 +129,7 @@ def es4(_g, step, sL, s, _input, **kwargs): # config_sim Necessary sim_config = config_sim( { - "N": 3, + "N": 1, "T": range(2), "M": g, # Optional } @@ -143,7 +137,7 @@ def es4(_g, step, sL, s, _input, **kwargs): # New Convention partial_state_update_blocks = psub_list(psu_block, psu_steps) -append_configs( +sweep_exp.append_configs( # user_id='user_a', sim_configs=sim_config, initial_state=genesis_states, @@ -153,8 +147,8 @@ def es4(_g, step, sL, s, _input, **kwargs): ) -print() -print("Policie State Update Block:") -pp.pprint(partial_state_update_blocks) -print() -print() +# print() +# print("Partial State Update Block:") +# pp.pprint(partial_state_update_blocks) +# print() +# print() diff --git a/simulations/regression_tests/models/tests.py b/simulations/regression_tests/models/tests.py deleted file mode 100644 index 424eca78..00000000 --- a/simulations/regression_tests/models/tests.py +++ /dev/null @@ -1,35 +0,0 @@ -import unittest - -import pandas as pd -# from tabulate import tabulate -from cadCAD.engine import ExecutionMode, ExecutionContext, Executor -from cadCAD import configs - -exec_mode = ExecutionMode() -first_config = configs # only contains config1 -single_proc_ctx = ExecutionContext(context=exec_mode.single_mode) -run = Executor(exec_context=single_proc_ctx, configs=first_config) -raw_result, tensor_field = run.execute() -result = pd.DataFrame(raw_result) - -class TestStringMethods(unittest.TestCase): - def __init__(self, result: pd.DataFrame, tensor_field: pd.DataFrame) -> None: - self.result = result - self.tensor_field = tensor_field - - def test_upper(self): - self.assertEqual('foo'.upper(), 'FOO') - - def test_isupper(self): - self.assertTrue('FOO'.isupper()) - self.assertFalse('Foo'.isupper()) - - def test_split(self): - s = 'hello world' - self.assertEqual(s.split(), ['hello', 'world']) - # check that s.split fails when the separator is not a string - with self.assertRaises(TypeError): - s.split(2) - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/simulations/regression_tests/models/udo.py b/simulations/regression_tests/models/udo.py index 44b8bc85..4fe1b5b5 100644 --- a/simulations/regression_tests/models/udo.py +++ b/simulations/regression_tests/models/udo.py @@ -1,13 +1,11 @@ import pandas as pd -# from fn.func import curried from datetime import timedelta import pprint as pp -from cadCAD.utils import SilentDF #, val_switch -from cadCAD.configuration import append_configs +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')) @@ -44,8 +42,6 @@ def read(self, ds_uri): def write(self, ds_uri): pd.to_csv(ds_uri) - # ToDo: Generic update function - pass @@ -59,7 +55,6 @@ def write(self, ds_uri): "T": range(4) }) -# ToDo: DataFrame Column order state_dict = { 'increment': 0, 'state_udo': state_udo, 'state_udo_tracker': 0, @@ -166,10 +161,9 @@ def update_timestamp(_g, step, sL, s, _input, **kwargs): # ) # psu_block[m]["variables"]['timestamp'] = update_timestamp -# ToDo: Bug without specifying parameters # New Convention partial_state_update_blocks = psub_list(psu_block, psu_steps) -append_configs( +udo1_exp.append_configs( sim_configs=sim_config, initial_state=state_dict, partial_state_update_blocks=partial_state_update_blocks diff --git a/simulations/regression_tests/models/udo_inter_substep_update.py b/simulations/regression_tests/models/udo_inter_substep_update.py index e18af0de..583f2386 100644 --- a/simulations/regression_tests/models/udo_inter_substep_update.py +++ b/simulations/regression_tests/models/udo_inter_substep_update.py @@ -4,10 +4,9 @@ from datetime import timedelta from cadCAD.utils import SilentDF #, val_switch -from cadCAD.configuration import append_configs 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')) @@ -43,7 +42,6 @@ def write(self, ds_uri): 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']) @@ -59,7 +57,6 @@ def udo_policyB(_g, step, sL, s, **kwargs): policies = {"p1": udo_policyA, "p2": udo_policyB} -# ToDo: DataFrame Column order state_dict = { 'increment': 0, 'state_udo': state_udo, 'state_udo_tracker_a': 0, 'state_udo_tracker_b': 0, @@ -152,8 +149,7 @@ def apply_incriment_condition(s): "T": range(4) }) -# ToDo: Bug without specifying parameters -append_configs( +udo1_exp.append_configs( sim_configs=sim_config, initial_state=state_dict, seeds={}, diff --git a/testing/experiments/__init__.py b/testing/experiments/__init__.py index a78e6840..fdfe3766 100644 --- a/testing/experiments/__init__.py +++ b/testing/experiments/__init__.py @@ -1,3 +1,4 @@ from cadCAD.configuration import Experiment -exp_param_sweep = Experiment() \ No newline at end of file +exp_param_sweep = Experiment() +exp_policy_agg = Experiment() \ No newline at end of file diff --git a/testing/generic_test.py b/testing/generic_test.py index 7e6e7e56..e3d281b6 100644 --- a/testing/generic_test.py +++ b/testing/generic_test.py @@ -21,7 +21,7 @@ def wrapped_eval(a, b): df[test_name] = df.apply( lambda x: wrapped_eval( x.filter(items=target_cols).to_dict(), - expected_results[(x['simulation'], x['run'], x['timestep'], x['substep'])] + expected_results[(x['subset'], x['run'], x['timestep'], x['substep'])] ), axis=1 ) @@ -39,7 +39,8 @@ def generic_test(self, tested_df, expected_reults, test_name): if erroneous.empty is False: # Or Entire df IS NOT erroneous for index, row in erroneous.iterrows(): - expected = expected_reults[(row['simulation'], row['run'], row['timestep'], row['substep'])] + # expected = expected_reults[(row['simulation'], row['run'], row['timestep'], row['substep'])] + expected = expected_reults[(row['subset'], row['run'], row['timestep'], row['substep'])] unexpected = {f"invalid_{k}": expected[k] for k in expected if k in row and expected[k] != row[k]} for key in unexpected.keys(): diff --git a/testing/models/param_sweep.py b/testing/models/param_sweep.py index b4c1c31c..ed64e142 100644 --- a/testing/models/param_sweep.py +++ b/testing/models/param_sweep.py @@ -16,6 +16,8 @@ def some_function(x): 'alpha': [1], 'beta': [2, some_function], 'gamma': [3, 4], + # 'beta': [1], + # 'gamma': [4], 'omega': [7] } @@ -76,7 +78,7 @@ def sweeped(_g, step, sL, s, _input, **kwargs): sim_config = config_sim( { - "N": 1, + "N": 2, "T": range(2), "M": g, # Optional } @@ -90,10 +92,3 @@ def sweeped(_g, step, sL, s, _input, **kwargs): env_processes=env_process, partial_state_update_blocks=partial_state_update_blocks ) - - -print() -print("Policie State Update Block:") -pp.pprint(partial_state_update_blocks) -print() -print() diff --git a/testing/models/policy_aggregation.py b/testing/models/policy_aggregation.py index 48400e29..2e2277c5 100644 --- a/testing/models/policy_aggregation.py +++ b/testing/models/policy_aggregation.py @@ -1,8 +1,10 @@ -from cadCAD.configuration import append_configs 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): @@ -73,7 +75,7 @@ def policies(_g, step, sH, s, _input, **kwargs): ) -append_configs( +exp_policy_agg.append_configs( sim_configs=sim_config, initial_state=genesis_states, partial_state_update_blocks=partial_state_update_block, diff --git a/testing/tests/out_check.py b/testing/tests/out_check.py index 33c5e3ef..99d5fedb 100644 --- a/testing/tests/out_check.py +++ b/testing/tests/out_check.py @@ -10,10 +10,19 @@ exec_mode = ExecutionMode() exec_ctx = ExecutionContext(context=exec_mode.local_mode) +# exec_ctx = ExecutionContext(context=exec_mode.multi_proc) run = Executor(exec_context=exec_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(tensor_fields[0], headers='keys', tablefmt='psql')) +# pprint(sessions) print(tabulate(result, headers='keys', tablefmt='psql')) + +print() + +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')) \ No newline at end of file diff --git a/testing/tests/param_sweep.py b/testing/tests/param_sweep.py deleted file mode 100644 index 61de0fde..00000000 --- a/testing/tests/param_sweep.py +++ /dev/null @@ -1,86 +0,0 @@ -import unittest -import pandas as pd - -from cadCAD.engine import ExecutionMode, ExecutionContext, Executor -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 - - -exec_mode = ExecutionMode() -exec_ctx = ExecutionContext(context=exec_mode.multi_mode) -run = Executor(exec_context=exec_ctx, configs=configs) - -# sim, run, substep, timestep -def get_expected_results(sim, run, beta, gamma): - return { - (sim, run, 0, 0): {'policies': {}, 'sweeped': {}, 'alpha': 0, 'beta': 0}, - (sim, run, 1, 1): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta}, - (sim, run, 1, 2): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta}, - (sim, run, 1, 3): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta}, - (sim, run, 2, 1): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta}, - (sim, run, 2, 2): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta}, - (sim, run, 2, 3): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta}, - (sim, run, 3, 1): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta}, - (sim, run, 3, 2): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta}, - (sim, run, 3, 3): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta}, - (sim, run, 4, 1): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta}, - (sim, run, 4, 2): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta}, - (sim, run, 4, 3): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': {'beta': beta, 'gamma': gamma}, 'alpha': 1, 'beta': beta}, - (sim, run, 5, 1): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': beta, 'alpha': 1, 'beta': beta}, - (sim, run, 5, 2): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': beta, 'alpha': 1, 'beta': beta}, - (sim, run, 5, 3): {'policies': {'gamma': gamma, 'omega': 7}, 'sweeped': beta, 'alpha': 1, 'beta': beta} - } - - -def generate_expected(sweep_params): - def template(sweep_params): - sim_count = max(len(x) for x in list(sweep_params.values())) - expected_results, expected_results_1, expected_results_2 = {}, {}, {} - for sim in range(sim_count): - expected_results_1a = get_expected_results(sim, 1, 2, 3) - expected_results_1b = get_expected_results(sim, 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(sim, 1, some_function, 4) - expected_results_2b = get_expected_results(sim, 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]]] - - -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/param_sweep_test.py b/testing/tests/param_sweep_test.py new file mode 100644 index 00000000..f908198e --- /dev/null +++ b/testing/tests/param_sweep_test.py @@ -0,0 +1,88 @@ +import unittest +from pprint import pprint + +import pandas as pd + +from cadCAD.engine import ExecutionMode, ExecutionContext, Executor +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 + + +exec_mode = ExecutionMode() +exec_ctx = ExecutionContext(context=exec_mode.multi_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]]] + + +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_test.py similarity index 100% rename from testing/tests/policy_aggregation.py rename to testing/tests/policy_aggregation_test.py