respec / HSPsquared

Hydrologic Simulation Program Python (HSPsquared)
GNU Affero General Public License v3.0
43 stars 17 forks source link

Data Model for Shared STATE (supports model modularity) RFC #112

Closed rburghol closed 6 months ago

rburghol commented 1 year ago

Currently, hsp2 features domain-specific (i.e. a single RCHERS, PERLND, etc) simulation and data access, with with only the current segment data, i.e. the ts Dict, parameters, and internal calculation state loaded, passed to the functional routine. To support the legacy SPEC-ACTIONS (#90) and potential new enhanced model modularity features, a robust data model is needed to facilitate passing STATE across multiple domains (segments) and amongst the various functions. This issue attempts to outline a proposed data structure schema to facilitate this, including performance considerations, and functional considerations. (see also: #126 )

Goals

Benefits

Draft Data Model

Integer keyed STATE

state_paths = Dict.empty(key_type=types.unicode_type, value_type=types.int64)
state_ix = Dict.empty(key_type=types.int64, value_type=types.float64)
dict_ix = Dict.empty(key_type=types.int64, value_type=types.float64[:,:])
ts_ix = Dict.empty(key_type=types.int64, value_type=types.float64[:])

Concepts

Implementation

Data Structure Option 1 - Character keyed STATE

state_ix = Dict.empty(key_type=types.int64, value_type=types.float64)
dict_ix = Dict.empty(key_type=types.int64, value_type=types.float64[:,:])
ts_ix = Dict.empty(key_type=types.int64, value_type=types.float64[:])

Data Structure Option 2

See above Draft Data Model

rburghol commented 1 year ago

Performance test. Integer-keyed versus character-keyed STATE Dict. Tests performed for a 50-year, 1-hour timestep simulation.

Set Up State and Time Series Dicts

import time
import numpy as np
from numba import njit, jit
from math import sin, cos
from numba import types
from numpy import zeros, any, full, nan, array, int64
from numba.typed import Dict

steps  = 365 * 24 * 50# 50 year hourly simulation
# Character-indexed
ts = Dict.empty(key_type=types.unicode_type, value_type=types.float64[:])
state = Dict.empty(key_type=types.unicode_type, value_type=types.float64)
# set up some base data
ts['/RESULTS/RCHRES_001/SPECL/Qin'] = zeros(steps)
ts['/RESULTS/RCHRES_001/SPECL/Qlocal'] = zeros(steps)
ts['/RESULTS/RCHRES_001/SPECL/Qout'] = zeros(steps)
state['/RESULTS/RCHRES_001/SPECL/Qin'] = 0.0
state['/RESULTS/RCHRES_001/SPECL/Qlocal'] = 0.0
state['/RESULTS/RCHRES_001/SPECL/Qout'] = 0.0

# Integer-indexed
ts_ix = Dict.empty(key_type=types.int64, value_type=types.float64[:])
state_ix = Dict.empty(key_type=types.int64, value_type=types.float64)
# set up some base data
ts_ix[1] = zeros(steps)
ts_ix[2] = zeros(steps)
ts_ix[3] = zeros(steps)
state_ix[1] = 0.0
state_ix[2] = 0.0
state_ix[3] = 0.0

Timeseries Variable Setting Only


@njit
def iterate_specl_ts(ts, step):
    ts['/RESULTS/RCHRES_001/SPECL/Qlocal'][step] = sin(step)
    ts['/RESULTS/RCHRES_001/SPECL/Qin'][step] = abs(cos(5 * step))
    ts['/RESULTS/RCHRES_001/SPECL/Qout'][step] = ts['/RESULTS/RCHRES_001/SPECL/Qlocal'][step] + ts['/RESULTS/RCHRES_001/SPECL/Qin'][step]
    return

# Force a Compile
iterate_specl_ts(ts, 0)

@njit
def iterate_specl_ts_ix(ts_ix, step):
    ts_ix[1][step] = sin(step)
    ts_ix[2][step] = abs(cos(5 * step))
    ts_ix[3][step] = ts_ix[1][step] + ts_ix[2][step]
    return

# Force a Compile
iterate_specl_ts_ix(ts_ix, 0)

@njit
def run_ts(ts, steps):
    for step in range(steps):
        iterate_specl_ts(ts, step)

start = time.time()
run_ts(ts, steps)
end = time.time()
print(end - start, "seconds")
# Run times between 0.1693 and 0.2163 seconds

@njit
def run_ts_ix(ts_ix, steps):
    for step in range(steps):
        iterate_specl_ts_ix(ts_ix, step)

run_ts_ix(ts_ix, 1):

start = time.time()
run_ts_ix(ts_ix, steps)
end = time.time()
print(end - start, "seconds")
# Run times between 0.0866 and 0.1266 seconds

State Variable Setting Only

@njit
def iterate_specl_state(state, step):
    state['/RESULTS/RCHRES_001/SPECL/Qlocal'] = sin(step)
    state['/RESULTS/RCHRES_001/SPECL/Qin'] = abs(cos(5 * step))
    state['/RESULTS/RCHRES_001/SPECL/Qout'] = state['/RESULTS/RCHRES_001/SPECL/Qlocal'] + state['/RESULTS/RCHRES_001/SPECL/Qin']
    return

# Force a Compile
iterate_specl_state(state, 0)

@njit
def iterate_specl_state_ix(state_ix, step):
    state_ix[1] = sin(step)
    state_ix[2] = abs(cos(5 * step))
    state_ix[3] = state_ix[1] + state_ix[2]
    return

# Force a Compile
iterate_specl_state_ix(state_ix, 0)

@njit
def run_state(state, steps):
    for step in range(steps):
        iterate_specl_state(state, step)

run_state(state, 1)

start = time.time()
run_state(state, steps)
end = time.time()
print(end - start, "seconds")
# Run times between 0.1407 and 0.1451 seconds

@njit
def run_state_ix(state_ix, steps):
    for step in range(steps):
        iterate_specl_state_ix(state_ix, step)

run_state_ix(state_ix, 1)

start = time.time()
run_state_ix(state_ix, steps)
end = time.time()
print(end - start, "seconds")
# Run times between 0.0428 and 0.0434 seconds
rburghol commented 6 months ago

Done. Need to move to documentation. See module #126