Skip to content

Commit

Permalink
Update mutability tests (#56)
Browse files Browse the repository at this point in the history
* Update core

* Update substeps

* Update benchmark

* Update version

* Add deepcopy

* Update benchmark
  • Loading branch information
BenSchZA authored Sep 3, 2022
1 parent 08a884d commit 744b5f4
Show file tree
Hide file tree
Showing 7 changed files with 97 additions and 21 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.11.0] - 2022-09-03
### Changed
- Adapted core deepcopy processes to be more efficient and avoid unintended mutation of state between policy and state update functions

## [0.10.1] - 2022-09-02
### Changed
- Updated to use "radCAD" logging instance
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -505,8 +505,8 @@ See [benchmarks](benchmarks/)
### Time Profiling

```bash
poetry run python3 -m pytest run benchmarks/benchmark_radcad.py
poetry run python3 -m pytest run benchmarks/benchmark_single_process.py
poetry run python3 -m pytest benchmarks/benchmark_radcad.py
poetry run python3 -m pytest benchmarks/benchmark_single_process.py
```

### Memory Profiling
Expand Down
11 changes: 7 additions & 4 deletions benchmarks/benchmark_cadcad_comparison.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
import tests.test_cases.predator_prey_model as benchmark_model


TEST_ITERATIONS = 1
TEST_ROUNDS = 3

initial_state = benchmark_model.initial_state
state_update_blocks = benchmark_model.state_update_blocks
params = benchmark_model.params
Expand All @@ -21,7 +24,7 @@
model = Model(initial_state=initial_state, state_update_blocks=state_update_blocks, params=params)
simulation_radcad = Simulation(model=model, timesteps=TIMESTEPS, runs=RUNS)
experiment = Experiment(simulation_radcad)
experiment.engine = Engine(backend=Backend.PATHOS)
experiment.engine = Engine(backend=Backend.SINGLE_PROCESS)

c = config_sim({
"N": RUNS,
Expand All @@ -37,14 +40,14 @@
)

exec_mode = ExecutionMode()
local_mode_ctx = ExecutionContext(context=exec_mode.local_mode)
local_mode_ctx = ExecutionContext(context=exec_mode.single_mode)
simulation_cadcad = Executor(exec_context=local_mode_ctx, configs=exp.configs)

def test_benchmark_radcad(benchmark):
benchmark.pedantic(radcad_simulation, iterations=1, rounds=3)
benchmark.pedantic(radcad_simulation, iterations=TEST_ITERATIONS, rounds=TEST_ROUNDS)

def test_benchmark_cadcad(benchmark):
benchmark.pedantic(cadcad_simulation, iterations=1, rounds=3)
benchmark.pedantic(cadcad_simulation, iterations=TEST_ITERATIONS, rounds=TEST_ROUNDS)

def radcad_simulation():
data_radcad = experiment.run()
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "radcad"
version = "0.10.1"
version = "0.11.0"
description = "A Python package for dynamical systems modelling & simulation, inspired by and compatible with cadCAD"
authors = ["CADLabs <[email protected]>"]
packages = [
Expand Down
2 changes: 1 addition & 1 deletion radcad/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "0.10.1"
__version__ = "0.11.0"

from radcad.wrappers import Context, Model, Simulation, Experiment
from radcad.engine import Engine
Expand Down
39 changes: 26 additions & 13 deletions radcad/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,15 @@ def default_deepcopy_method(obj):
return pickle.loads(pickle.dumps(obj=obj, protocol=-1))


def _update_state(initial_state, params, substep, result, substate, signals, state_update_tuple):
def _update_state(initial_state, params, substep, result, substate, signals, deepcopy, deepcopy_method, state_update_tuple):
_substate = deepcopy_method(substate) if deepcopy else substate.copy()
_signals = deepcopy_method(signals) if deepcopy else signals.copy()

state, function = state_update_tuple
if not state in initial_state:
raise KeyError(f"Invalid state key {state} in partial state update block")
state_key, state_value = function(
params, substep, result, substate, signals
params, substep, result, _substate, _signals
)
if not state_key in initial_state:
raise KeyError(
Expand Down Expand Up @@ -71,24 +74,29 @@ def _single_run(
substate: dict = (
previous_state.copy() if substep == 0 else substeps[substep - 1].copy()
)

# Create two independent deepcopies to ensure a policy function
# can't mutate the state passed to the state update functions
policy_substate_copy = deepcopy_method(substate) if deepcopy else substate.copy()
state_update_substate_copy = deepcopy_method(substate) if deepcopy else substate.copy()

substate["substep"] = substep + 1

signals: dict = reduce_signals(
params, substep, result, policy_substate_copy, psu, deepcopy
params, substep, result, substate, psu, deepcopy, deepcopy_method
)

updated_state = map(
partial(_update_state, initial_state, params, substep, result, state_update_substate_copy, signals),
partial(
_update_state,
initial_state,
params,
substep,
result,
substate,
signals,
deepcopy,
deepcopy_method,
),
psu["variables"].items()
)

substate.update(updated_state)
substate["timestep"] = (previous_state["timestep"] + 1) if timestep == 0 else timestep + 1
substate["substep"] = substep + 1
substeps.append(substate)

substeps = [substate] if not substeps else substeps
Expand Down Expand Up @@ -193,14 +201,19 @@ def _add_signals(acc, a: Dict[str, any]):

def reduce_signals(params: dict, substep: int, result: list, substate: dict, psu: dict, deepcopy: bool=True, deepcopy_method: Callable=default_deepcopy_method):
policy_results: List[Dict[str, any]] = list(
map(lambda function: function(params, substep, result, substate), psu["policies"].values())
map(lambda function: function(
params,
substep,
result,
deepcopy_method(substate) if deepcopy else substate.copy()
), psu["policies"].values())
)

result: dict = {}
result_length = len(policy_results)
if result_length == 0:
return result
elif result_length == 1:
return deepcopy_method(policy_results[0]) if deepcopy else policy_results[0].copy()
return policy_results[0]
else:
return reduce(_add_signals, policy_results, result)
56 changes: 56 additions & 0 deletions tests/test_mutability.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,59 @@ def test_state_mutation_no_deepcopy():
assert not 1 in df.iloc[10]['a']
assert not 1 in df.iloc[0]['b']
assert not 1 in df.iloc[10]['b']

def policy_1_mutate_state(params, substep, state_history, previous_state):
a = previous_state['a']
assert not 1 in a
a.append(1)
return {'signal': {'x': 1}}

def policy_2_mutate_state(params, substep, state_history, previous_state):
a = previous_state['a']
assert not 1 in a
a.append(1)
return {}

def update_a_mutate_signal(params, substep, state_history, previous_state, policy_input):
signal = policy_input['signal']
assert signal['x'] == 1
signal['x'] = 0
return 'a', previous_state['a']

def update_b_mutate_signal(params, substep, state_history, previous_state, policy_input):
signal = policy_input['signal']
assert signal['x'] == 1
signal['x'] = 0
return 'b', previous_state['b']

def test_policy_state_mutation():
initial_state = {
'a': [0],
'b': [0],
}

state_update_blocks = [
{
'policies': {
'p1': policy_1_mutate_state,
'p2': policy_2_mutate_state,
},
'variables': {
'a': update_a_mutate_signal,
'b': update_b_mutate_signal,
}
},
]

model = Model(initial_state=initial_state, state_update_blocks=state_update_blocks, params={})
simulation = Simulation(model=model, timesteps=10)
experiment = Experiment(simulation)
experiment.engine = Engine(backend=Backend.SINGLE_PROCESS, deepcopy=True)

result = experiment.run()
df = pd.DataFrame(result)

assert not 1 in df.iloc[0]['a']
assert not 1 in df.iloc[10]['a']
assert not 1 in df.iloc[0]['b']
assert not 1 in df.iloc[10]['b']

0 comments on commit 744b5f4

Please sign in to comment.