diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a4b9c958..86ec78ea 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,7 +59,7 @@ repos: # hooks: # - id: setup-cfg-fmt - repo: https://github.com/psf/black-pre-commit-mirror - rev: 26.1.0 + rev: 26.3.1 hooks: - id: black language_version: python3.13 @@ -76,6 +76,7 @@ repos: rev: v1.7.7 hooks: - id: docformatter + language_version: python3.13 args: - --in-place - --wrap-summaries diff --git a/docs/source/background/limitations.rst b/docs/source/background/limitations.rst index ea87341a..5723cea4 100644 --- a/docs/source/background/limitations.rst +++ b/docs/source/background/limitations.rst @@ -9,7 +9,7 @@ However, there are limitations to what kind of features can be implemented and h Below we discuss what kind of models `dcegm` is designed for and importantly what limitations to be aware of when implementing a model. -What can dcegm do? +What can `dcegm` do? --------------------- The package follows Ishakov et al. (2017) and implements the discrete-continuous endogenous grid method (DC-EGM) for solving dynamic stochastic optimization problems with both discrete and continuous choices. Our code originated as a Python replication of their Matlab code and has since been extended to include additional features and improvements (such as used in Iskhakov and Keane (2021)). @@ -25,7 +25,6 @@ What can `dcegm` not be used for? --------------------------------- - Purely discrete choice models (see e.g. Keane and Wolpin; 1997) -- Models with more than two continuous state variables. - Models with more than one continuous state variable with a normally distributed idiosyncratic shock. diff --git a/docs/source/background/sparsity_conditions.rst b/docs/source/background/sparsity_conditions.rst index dad0d010..224a1984 100644 --- a/docs/source/background/sparsity_conditions.rst +++ b/docs/source/background/sparsity_conditions.rst @@ -15,6 +15,8 @@ To ensure that only **feasible** and **logically consistent** states are evaluat 1. **Filters** out invalid state combinations that violate model logic (e.g., working while dead). 2. **Returns** cleaned or adjusted versions of the state variables to account for absorbing states (e.g., setting `job_offer = 0` when dead). +Note that sparsity conditions can only be defined for discrete state variables. + Basic Structure of a Sparsity Condition --------------------------------------- diff --git a/docs/source/background/two_occupation_model.ipynb b/docs/source/background/two_occupation_model.ipynb new file mode 100644 index 00000000..5f6ac68d --- /dev/null +++ b/docs/source/background/two_occupation_model.ipynb @@ -0,0 +1,884 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "927a5769", + "metadata": {}, + "outputs": [], + "source": [ + "import jax\n", + "import jax.numpy as jnp\n", + "import dcegm\n", + "import matplotlib.pyplot as plt\n", + "\n", + "jax.config.update(\"jax_enable_x64\", True)" + ] + }, + { + "cell_type": "markdown", + "id": "c7e30319", + "metadata": {}, + "source": [ + "## Model with Multiple Deterministic (Continuous or Discrete) States" + ] + }, + { + "cell_type": "markdown", + "id": "7b5772b5", + "metadata": {}, + "source": [ + "`dcegm` can handle multiple deterministic states variables which can be discrete as well as continuous. In this tutorial we outline how to specify such state variables using the example of experience stocks. Experience stocks can be specified as a discrete state variable but become computationally expensive quickly due to the **curse of dimensionality**. It is thus advisable to handle them as a continuous state variable instead. \n", + "\n", + "This notebook develops a minimal five-period model with **two deterministic experience stocks** — one per occupation — alongside stochastic wealth. We first show how to specify these using only a discrete implementation in `dcegm` and then explain how to implement them as continuous state variables instead.\n", + "\n", + "The model extends the single-experience minimal example by adding a second occupation track. The agent chooses among three discrete alternatives in each working period. We compare an implementation with discrete versus continuous experience stocks.\n", + "\n", + "| Choice | Label | Interpretation |\n", + "|:---:|:---:|:---|\n", + "| 0 | Occupation 0 | Red Occupation; income proportional to red experience $x_0$ |\n", + "| 1 | Occupation 1 | Green Occupation; income proportional to green experience $x_1$ |\n", + "| 2 | No work | Leisure / unemployment / retirement ; no income, no experience gained |\n", + "\n", + "Last period is mandatory retirement (forced choice 2), with pension income depending on accumulated experience. For simplicity, we do not include any stochastic state variables such as long-term care dependency risk." + ] + }, + { + "cell_type": "markdown", + "id": "999eb2ff", + "metadata": {}, + "source": [ + "### Model Setup\n", + "\n", + "We can first implement the model setup which is applicable to both a discrete and continuous implementation of the experience stocks." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "184964df", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'interest_rate': 0.02,\n", + " 'max_wealth': 50,\n", + " 'wage_constant': 3,\n", + " 'wage_exp_green': 0.5,\n", + " 'wage_exp_red': 0.8,\n", + " 'income_shock_std': 1,\n", + " 'income_shock_mean': 0,\n", + " 'taste_shock_scale': 1,\n", + " 'discount_factor': 0.95,\n", + " 'rho': 0.9,\n", + " 'delta': 1.5,\n", + " 'beta_green': 0.2,\n", + " 'beta_red': 0.1}" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "params = {}\n", + "params[\"interest_rate\"] = 0.02\n", + "params[\"max_wealth\"] = 50\n", + "params[\"wage_constant\"] = 3\n", + "params[\"wage_exp_green\"] = 0.5\n", + "params[\"wage_exp_red\"] = 0.8\n", + "params[\"income_shock_std\"] = 1\n", + "params[\"income_shock_mean\"] = 0\n", + "params[\"taste_shock_scale\"] = 1\n", + "params[\"discount_factor\"] = 0.95\n", + "params[\"rho\"] = 0.9\n", + "params[\"delta\"] = 1.5\n", + "params[\"beta_green\"] = 0.2\n", + "params[\"beta_red\"] = 0.1\n", + "params" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f0404218", + "metadata": {}, + "outputs": [], + "source": [ + "# Utility functions\n", + "def flow_util(consumption, choice, params):\n", + " rho = params[\"rho\"]\n", + " beta_green = params[\"beta_green\"]\n", + " beta_red = params[\"beta_red\"]\n", + " disutility = beta_red * (choice == 0) + beta_green * (choice == 1)\n", + " u = consumption ** (1 - rho) / (1 - rho) - disutility\n", + " return u\n", + "\n", + "\n", + "def marginal_utility(consumption, params):\n", + " rho = params[\"rho\"]\n", + " u_prime = consumption ** (-rho)\n", + " return u_prime\n", + "\n", + "\n", + "def inverse_marginal_utility(marginal_utility, params):\n", + " rho = params[\"rho\"]\n", + " return marginal_utility ** (-1 / rho)\n", + "\n", + "\n", + "utility_functions = {\n", + " \"utility\": flow_util,\n", + " \"inverse_marginal_utility\": inverse_marginal_utility,\n", + " \"marginal_utility\": marginal_utility,\n", + "}\n", + "\n", + "# Final period utility functions.\n", + "\n", + "\n", + "def final_period_utility(wealth: float, choice: int, params):\n", + " return flow_util(wealth, choice, params)\n", + "\n", + "\n", + "def marginal_final(wealth, choice, params):\n", + " return marginal_utility(wealth, params)\n", + "\n", + "\n", + "utility_functions_final_period = {\n", + " \"utility\": final_period_utility,\n", + " \"marginal_utility\": marginal_final,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d6585a94", + "metadata": {}, + "outputs": [], + "source": [ + "# Define state specific choice set.\n", + "def state_specific_choice_set(\n", + " period,\n", + " lagged_choice,\n", + " model_specs,\n", + "):\n", + " \"\"\"State specific choice set limits which choices are available to agent given the state.\"\"\"\n", + " # Once the agent choses retirement, she can only choose retirement thereafter.\n", + " # Hence, retirement is an absorbing state.\n", + " if lagged_choice == 2:\n", + " choice_set = [2]\n", + " elif period == 4:\n", + " choice_set = [2]\n", + " else:\n", + " choice_set = model_specs[\"choices\"]\n", + "\n", + " return choice_set" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "36c36ece", + "metadata": {}, + "outputs": [], + "source": [ + "# Model specifications.\n", + "model_specs = {\n", + " \"choices\": [0, 1, 2],\n", + "}" + ] + }, + { + "cell_type": "markdown", + "id": "5f7f280b", + "metadata": {}, + "source": [ + "### Discrete Experience Stocks\n", + "\n", + "We first implement a model if discrete experience stocks for our `red` and `green` occupation. To do so, the following objects need to be specified to suit the discrete deterministic state variables:\n", + "\n", + "- `model_config`: Needs the nested dict in `deterministic_states` with grid of (maximum) experience.\n", + "- `state_space_functions`\n", + " - `next_period_deterministic_state`: Function which describes the evolution of deterministic discrete state variables.\n", + " - `sparsity_conditions`: Exclude impossible state from state space.\n", + "- `budget_constraint`: Determines disposable resources for agent." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "bf9f7f62", + "metadata": {}, + "outputs": [], + "source": [ + "model_config = {\n", + " \"n_periods\": 5,\n", + " \"choices\": [0, 1, 2],\n", + " \"continuous_states\": {\n", + " \"assets_end_of_period\": jnp.linspace(0, 50, 100),\n", + " \"assets_begin_of_period\": jnp.linspace(0, 50, 100),\n", + " },\n", + " \"deterministic_states\": {\n", + " \"exp_green\": jnp.arange(0, 7, dtype=int),\n", + " \"exp_red\": jnp.arange(0, 7, dtype=int),\n", + " },\n", + " \"n_quad_points\": 5,\n", + " \"upper_envelope\": {\"method\": \"druedahl_jorgensen\"},\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "f1e9b301", + "metadata": {}, + "outputs": [], + "source": [ + "def next_period_deterministic_state(\n", + " period,\n", + " choice,\n", + " lagged_choice,\n", + " exp_green,\n", + " exp_red,\n", + "):\n", + " next_exp_green = exp_green + (choice == 1)\n", + " next_exp_red = exp_red + (choice == 0)\n", + " return {\n", + " \"period\": period + 1,\n", + " \"exp_green\": next_exp_green,\n", + " \"exp_red\": next_exp_red,\n", + " \"lagged_choice\": choice,\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "2ce93afa", + "metadata": {}, + "outputs": [], + "source": [ + "def sparsity_condition(\n", + " period,\n", + " lagged_choice,\n", + " exp_green,\n", + " exp_red,\n", + "):\n", + " \"\"\"Define sparsity condition to rule out state space points that are not feasible given the model structure.\"\"\"\n", + " # Experience cannot exceed the period\n", + " if (exp_green + exp_red) > period:\n", + " return False\n", + " # In later periods, if retired, shouldn't have accumulated experience before retirement\n", + " else:\n", + " return True\n", + "\n", + "\n", + "# Define dict of state space functions to pass to setuo_model.\n", + "state_space_functions_discrete_exp = {\n", + " \"state_specific_choice_set\": state_specific_choice_set,\n", + " \"next_period_deterministic_state\": next_period_deterministic_state,\n", + " \"sparsity_condition\": sparsity_condition,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "6e6cad62", + "metadata": {}, + "outputs": [], + "source": [ + "def budget_constraint_discrete_exp(\n", + " lagged_choice,\n", + " exp_green,\n", + " exp_red,\n", + " asset_end_of_previous_period,\n", + " income_shock_previous_period,\n", + " params,\n", + "):\n", + " \"\"\"Budget constraint that determines the resource available to the agent in the next period given the state and choice in the previous period.\"\"\"\n", + " interest_factor = 1 + params[\"interest_rate\"]\n", + " # Wage depends on accumulated experience in each occupation, retirees/unemployed only get wage constant.\n", + " wage = (\n", + " params[\"wage_constant\"]\n", + " + params[\"wage_exp_green\"] * exp_green * (lagged_choice == 1)\n", + " + params[\"wage_exp_red\"] * exp_red * (lagged_choice == 0)\n", + " )\n", + " resource = (\n", + " interest_factor * asset_end_of_previous_period\n", + " + (wage + income_shock_previous_period) * (lagged_choice != 2)\n", + " + (wage + income_shock_previous_period) * 0.5 * (lagged_choice == 2)\n", + " )\n", + " return jnp.maximum(resource, 0.5)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "4a706fee", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting state space creation\n", + "State space created.\n", + "\n", + "Starting state-choice space creation and child state mapping.\n", + "State, state-choice and child state mapping created.\n", + "\n", + "Start creating batches for the model.\n", + "The batch size of the backwards induction is 42\n", + "The batch size of the backwards induction is 41\n", + "The batch size of the backwards induction is 40\n", + "The batch size of the backwards induction is 39\n", + "The batch size of the backwards induction is 38\n", + "The batch size of the backwards induction is 37\n", + "The batch size of the backwards induction is 36\n", + "The batch size of the backwards induction is 35\n", + "The batch size of the backwards induction is 34\n", + "The batch size of the backwards induction is 33\n", + "The batch size of the backwards induction is 32\n", + "The batch size of the backwards induction is 31\n", + "The batch size of the backwards induction is 30\n", + "The batch size of the backwards induction is 29\n", + "The batch size of the backwards induction is 28\n", + "The batch size of the backwards induction is 27\n", + "The batch size of the backwards induction is 26\n", + "The batch size of the backwards induction is 25\n", + "The batch size of the backwards induction is 24\n", + "The batch size of the backwards induction is 23\n", + "The batch size of the backwards induction is 22\n", + "The batch size of the backwards induction is 21\n", + "Model setup complete.\n", + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/maxblesch/Uni/dcegm/dcegm/src/dcegm/pre_processing/model_structure/state_choice_space.py:295: UserWarning: \n", + "\n", + "\n", + "\n", + " Some states are not child states of any state-choice combination or stochastic transition. Please revisit the sparsity condition. \n", + " \n", + "An example of a state that is not a child state is: \n", + " \n", + "{'period': np.uint8(1), 'lagged_choice': np.uint8(0), 'exp_green': np.uint8(0), 'exp_red': np.uint8(0), 'dummy_stochastic': np.uint8(0)} \n", + " \n", + "\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "model = dcegm.setup_model(\n", + " model_config=model_config,\n", + " model_specs=model_specs,\n", + " utility_functions=utility_functions,\n", + " utility_functions_final_period=utility_functions_final_period,\n", + " state_space_functions=state_space_functions_discrete_exp,\n", + " stochastic_states_transitions={},\n", + " budget_constraint=budget_constraint_discrete_exp,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "f4dbc792", + "metadata": {}, + "source": [ + "The model creation gives a warning. This is because we did not exclude in the sparisity condition all the states, which can not be reached from an initial state in period 0. Below we provide a sparsity condition which would exclude them. However, this could of course be on purpose, because there might people starting later into the model for example or you want to have extra states for the solver to compute more states at the same time. For more on this, please read the information provided about batches. In principal, you should think very carefully about your state space and in the most perfect scenario, don't get any warnings." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "497e792f", + "metadata": {}, + "outputs": [], + "source": [ + "# def sparsity_condition(\n", + "# period,\n", + "# lagged_choice,\n", + "# exp_green,\n", + "# exp_red,\n", + "# ):\n", + "# \"\"\"Define sparsity condition to rule out state space points that are not feasible\n", + "# given the model structure.\"\"\"\n", + "# # Experience cannot exceed the period\n", + "# if (exp_green + exp_red) > period:\n", + "# return False\n", + "# # If retired, experience sum can not be the same as period\n", + "# elif (lagged_choice == 2) and ((exp_green + exp_red) == period) & (period > 0):\n", + "# return False\n", + "# # # As retirement is absorbing and there is no non-employment, experience sum must be at least one less than period in later periods.\n", + "# elif (lagged_choice != 2) and ((exp_green + exp_red) < period):\n", + "# return False\n", + "# # In later periods, if last period chosen an occupation experience must be positive\n", + "# elif (lagged_choice == 1) and (exp_green == 0) and (period > 0):\n", + "# return False\n", + "# # In later periods, if last period choosen an occupation experience must be positive\n", + "# elif (lagged_choice == 0) and (exp_red == 0) and (period > 0):\n", + "# return False\n", + "# else:\n", + "# return True\n", + "\n", + "# # Define dict of state space functions to pass to setuo_model.\n", + "# state_space_functions_discrete_exp = {\n", + "# \"state_specific_choice_set\": state_specific_choice_set,\n", + "# \"next_period_deterministic_state\": next_period_deterministic_state,\n", + "# \"sparsity_condition\": sparsity_condition,\n", + "# }\n", + "\n", + "# model = dcegm.setup_model(\n", + "# model_config=model_config,\n", + "# model_specs=model_specs,\n", + "# utility_functions=utility_functions,\n", + "# utility_functions_final_period=utility_functions_final_period,\n", + "# state_space_functions=state_space_functions_discrete_exp,\n", + "# stochastic_states_transitions={},\n", + "# budget_constraint=budget_constraint_discrete_exp,\n", + "# )" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "e9f384c2", + "metadata": {}, + "outputs": [], + "source": [ + "solved_model = model.solve(params)\n", + "policy_function = solved_model.policy" + ] + }, + { + "cell_type": "markdown", + "id": "880ae5ce", + "metadata": {}, + "source": [ + "### Continuous Experience Stocks\n", + "\n", + "For continuous experience stocks we need to change the model specification slightly. Note that we need to select a different solution method for the upper envelope if the deterministic continuous state space is multidimensional compared to when we only have one deterministic continuous state variable (i.e. one experience stock)." + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "19388b7c", + "metadata": {}, + "outputs": [], + "source": [ + "model_config_cont_exp = {\n", + " \"n_periods\": 5,\n", + " \"choices\": [0, 1, 2],\n", + " \"continuous_states\": {\n", + " \"assets_end_of_period\": jnp.linspace(0, 50, 100),\n", + " \"assets_begin_of_period\": jnp.linspace(0, 50, 100),\n", + " \"exp_green\": jnp.arange(0, 7, dtype=float),\n", + " \"exp_red\": jnp.arange(0, 7, dtype=float),\n", + " },\n", + " \"n_quad_points\": 5,\n", + " \"upper_envelope\": {\"method\": \"druedahl_jorgensen\"},\n", + "}\n", + "\n", + "# The following model config would be used if there is only one deterministic continuous state variable.\n", + "# model_config_cont_exp = {\n", + "# \"n_periods\": 5,\n", + "# \"choices\": [0, 1, 2],\n", + "# \"upper_envelope\": {\n", + "# \"method\": \"fues\",\n", + "# \"tuning_params\": {\n", + "# \"fues_n_points_to_scan\": 10, # this is set as default if missing\n", + "# \"fues_jump_threshold\": 2, # this is set as default if missing\n", + "# },\n", + "# \"continuous_states\": {\n", + "# \"assets_end_of_period\": jnp.linspace(0, 50, 100),\n", + "# \"exp\": jnp.arange(0, 1, 5, dtype=float), },\n", + "# \"n_quad_points\": 5,\n", + "# }" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "9ddd9f80", + "metadata": {}, + "outputs": [], + "source": [ + "def next_period_continuous_state(\n", + " lagged_choice,\n", + " period,\n", + " exp_green,\n", + " exp_red,\n", + "):\n", + " return {\n", + " \"exp_red\": exp_red + (lagged_choice == 0),\n", + " \"exp_green\": exp_green + (lagged_choice == 1),\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "96f10e06", + "metadata": {}, + "outputs": [], + "source": [ + "state_space_functions_cont_exp = {\n", + " \"state_specific_choice_set\": state_specific_choice_set,\n", + " # \"next_period_deterministic_state\": next_period_deterministic_state_cont,\n", + " \"next_period_continuous_state\": next_period_continuous_state,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "dce00ada", + "metadata": {}, + "outputs": [], + "source": [ + "def budget_constraint_cont_exp(\n", + " period,\n", + " lagged_choice,\n", + " exp_green,\n", + " exp_red,\n", + " asset_end_of_previous_period,\n", + " income_shock_previous_period,\n", + " params,\n", + "):\n", + " \"\"\"Budget constraint for two continuous experience variables.\"\"\"\n", + " # Scale experience by period to retrieve years of experience, then calculate wage and resources as before.\n", + " interest_factor = 1 + params[\"interest_rate\"]\n", + " wage = (\n", + " params[\"wage_constant\"]\n", + " + params[\"wage_exp_green\"] * exp_green * (lagged_choice == 1)\n", + " + params[\"wage_exp_red\"] * exp_red * (lagged_choice == 0)\n", + " )\n", + " resource = (\n", + " interest_factor * asset_end_of_previous_period\n", + " + (wage + income_shock_previous_period) * (lagged_choice != 2)\n", + " + (wage + income_shock_previous_period) * 0.5 * (lagged_choice == 2)\n", + " )\n", + " return jnp.maximum(resource, 0.5)" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "67eaebb7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Update function for state space not given. Assume states only change with an increase of the period and lagged choice.\n", + "Sparsity condition not provided. Assume all states are valid.\n", + "Starting state space creation\n", + "State space created.\n", + "\n", + "Starting state-choice space creation and child state mapping.\n", + "State, state-choice and child state mapping created.\n", + "\n", + "Start creating batches for the model.\n", + "The batch size of the backwards induction is 7\n", + "Model setup complete.\n", + "\n" + ] + } + ], + "source": [ + "model_cont_exp = dcegm.setup_model(\n", + " model_config=model_config_cont_exp,\n", + " model_specs=model_specs,\n", + " utility_functions=utility_functions,\n", + " utility_functions_final_period=utility_functions_final_period,\n", + " state_space_functions=state_space_functions_cont_exp,\n", + " stochastic_states_transitions={},\n", + " budget_constraint=budget_constraint_cont_exp,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "fb984f18", + "metadata": {}, + "outputs": [], + "source": [ + "solved_model_cont_exp = model_cont_exp.solve(params)\n", + "# policy_function_cont_exp = solved_model_cont_exp.policy" + ] + }, + { + "cell_type": "markdown", + "id": "bd92bc85", + "metadata": {}, + "source": [ + "### Simulation and Model Comparison\n", + "\n", + "Lastly, we can now simulate our two model implementations and compare the resulting choice probabilities by experience profiles." + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "9b25015c", + "metadata": {}, + "outputs": [], + "source": [ + "n_agents = 100\n", + "states_initial = {\n", + " \"n_agents\": n_agents,\n", + " \"assets_begin_of_period\": jnp.zeros(n_agents),\n", + " \"exp_green\": jnp.zeros(n_agents),\n", + " \"exp_red\": jnp.zeros(n_agents),\n", + " \"lagged_choice\": jnp.zeros(n_agents),\n", + " \"period\": jnp.zeros(n_agents, dtype=int),\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "701ac13d", + "metadata": {}, + "outputs": [], + "source": [ + "simulate = model.get_solve_and_simulate_func(states_initial=states_initial, seed=99)\n", + "df = simulate(params)" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "02cdaf9a", + "metadata": {}, + "outputs": [], + "source": [ + "simulate_cont_exp = model_cont_exp.get_solve_and_simulate_func(\n", + " states_initial=states_initial, seed=99\n", + ")\n", + "\n", + "df_cont_exp = simulate_cont_exp(params)" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "ecdedc0a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 22, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABCMAAAHUCAYAAAAA4OLOAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAR11JREFUeJzt3Qm4VWW5OPAPQQYHMCecAHGMIidwQCWnwtC8eusmZYkDXMVZMUvklkMmDmloCmkOZDlwTS0tUmkQUbSUNL3qNWdQQUSTwQEE9v951//u0z777APnwDnrcM7+/Z5ni3udtfdew7fWete7vqFdoVAoJAAAAICcrJHXDwEAAAAEyQgAAAAgV5IRAAAAQK4kIwAAAIBcSUYAAAAAuZKMAAAAAHIlGQEAAADkSjICAAAAyJVkBAAAAJAryYh6PP300+mYY45JvXv3Tp07d07rrLNO2mWXXdKll16a3nvvvZr5ttxyy/TlL3+5yXbIa6+9ltq1a5cmTJiQ8rTvvvtmv1t8denSJe24445p7NixadmyZU32O7Fe8f1PPPFEk33neeedl33n3LlzG7Se8SoVn43vKHrwwQezafFv0aRJk2rNUyrKwNFHH51WV8XtU3yttdZaaYsttkgHHnhg+slPfpIWLFhQ5zOxPrFeq6PnnnsuW6c4VppS8dir71Xf/l+dtJblXJEPPvggXXLJJdk5qGvXrmnddddNW2+9dTr88MPTlClTmr0srMo5BhpCjCHGaCsxRtHUqVOzc/Tmm2+eOnbsmLp165b23HPPNH78+Oyc3lyWdx1YnWOZ5lCMX+t75X1v0VrugZrDu+++m0aNGpU+85nPpLXXXjs7Hj796U+nI488Mjv/F02bNi0rv++//36zLk8cC3Evuzrq0NILsDr62c9+lk488cS0/fbbp7POOisrSJ988kl2A/3Tn/40Pfroo+nuu+9ult/edNNNs++PwDtvW221Vbrllluy/58zZ062rmeccUaaNWtWdmPQFowbN26F80TSKfZB7PfSQOGaa66pGCxEWYgbptXdfffdl50MFy9enN566630xz/+MX3nO99Jl112Wbr33nuzG7+i733ve+m0005Lq6MIPM4///wsqdQcQcYpp5ySjjjiiDrTI4Gzuoty2xqWc3mWLl2aBg0alJ555pns/Lvbbrtl01988cWsnEbAu88+++RSFqA5iDHEGG0txjj33HPTBRdckCUffvCDH2Qx7Icfflhzo/WPf/wj/fjHP26W317edWB1jmWa00UXXZT222+/OtNb4t6itdwDNaWFCxemPfbYI/s34piIrz/66KPsOLjrrrvSU089lXbYYYds3mnTpmXlN5IF6623XqpGkhFl4iA44YQT0he/+MX061//OnXq1KnmbzHtzDPPzG7qmkv8XhTglhC1IUp/e/DgwVkW7+qrr04XXnhhWnPNNet8plAopI8//jj7bGtQevGvT1z0G7MPdt5559Qa9OvXL2244YY177/+9a+nk08+Obux+7d/+7fsJFks73leCOLmc8mSJbWOtZbUs2fPFjsGV0bpMdialrs+Dz30UHZxvvHGG7PaaUVRkyfKa1PW1IK8iTHEGG0txrjjjjuyRMSwYcOyRFs82S6NI+OhR5T7ltDab2pX1rbbbtuq4oHSOLA1LffyjomXXnop/elPf6qTFBo5cqQ4poxmGhWyiXEive666yreHEXVs7hxKxcJiniiHjcEcQMfgXS5//mf/0mHHnpo+tSnPpU1/dhpp53Sz3/+8wZVUfrf//3f9I1vfCN17949W664YRo6dGhatGhRzTyzZ89Oxx9/fPZkNJYzmphEti0O8JURyYe4gY3s9jvvvJNNi2WLG4KoNdGnT59sWYrr8PDDD6cDDjggq1IdTQEiQ/673/2u4nf/85//zG401l9//az60iGHHJJeeeWVWvNMnjw5216xPrG9ttlmm2z96qsqPXPmzPSVr3wlu9BHDYBvfetbNcu9vGYa5cqbaUS2Mp5YFNe/+CpWCaxUhXL+/Pnp29/+drYPYl9EtcXTTz+9TlXFOGHtvvvu2fLGNovaKccee2zKS2RrR48enWbMmJEmTpy43KqNDVnWqGYWCbv4W5SNjTfeOB100EFZ+S0t39HcKRJcsX1ivj//+c/Z36P2URxfUS5in0cQ9t///d813x/Hxde+9rXs/+MEX6nq4R/+8IesHEY5iOXca6+9slogTSWe0Md3F5ejKC467du3z57ElDfjiidbkQWPdYptc9VVV9X53oaWmeUdg5WaaTTkvFDcLz/60Y/SFVdckc0T1fkGDBiQHnvssTrL+pe//CU7ZjfYYINsnSLgi2Ut305RwyTKQCxjLGvxOFpR1cbiE5JK1lhjjQaXhTgPRxmPZYwy9e///u/p+eefX6n1KRdlOvZlHBNRkyw8+eST2f4urvNmm22WDj744PTGG2+scL2pDmKMfxFjtI0YIxIREdfGda00EVEUMWHUdiuK5HlUXy9d9pNOOqlONfXi9XN58fWKrgOVYpniNfQXv/hFdl2K7RLXid/+9rcNauJRbLZXqqHrVF9TyvJ9HHF3cf8Wr1/9+/dPt912W2oKEa/H8Re/Uakp9Q033FBne1177bVpu+22y65t8WDv9ttvr/O9jYk3KsWB9d0DNSSeKMbusY0iro3rb8RqX/jCF9ILL7xQZ1mjXEWsWDw24jvHjBlTa54VxaSrGsdEWTjrrLOy/4/tUCy/xfuPePgS2ynKfTGmjnu/SjFFQ9an3COPPJI9pIzjrHjuiFg27pMiHopjLu43v/rVr2ZlstkUqLFkyZLCWmutVdh9990bvFV69epV2GKLLQqf+cxnCjfffHPh/vvvL3zta18rxKadMmVKzXz/+7//W1h33XULW2+9dTbf7373u8I3vvGNbL5LLrmkZr5XX301m3bTTTfVTHvqqacK66yzTmHLLbcs/PSnPy388Y9/LPzyl78sHH744YX58+dn88yaNavQo0ePbHmuvfbawh/+8IfCD37wg0KnTp0KRx999ArXY5999il89rOfrTN9l112KXTo0KHw4YcfZu9j2TbffPPCDjvsULj11lsLf/rTnwr/8z//U3jwwQcLa665ZqFfv36FiRMnFn79618XBg0aVGjXrl3h9ttvr/m+WK/4jljWY489tvD73/++cN111xU23njjbNo///nPmnnHjx9fGDNmTOGee+7JtuXPf/7zwo477ljYfvvtC4sXL66Z79xzz82+M9b9rLPOyvbBFVdcUVh77bULO++8c615Yz3jVSo+G99R9Oc//zmbFv+Gl156qfAf//Ef2bRHH3205vXxxx/XlIGjjjqq5vMffPBBYaeddipsuOGG2XLEvrjyyisL3bp1K+y///6FZcuWZfNNmzYt2z5f//rXC5MmTcq2ZWyfI488stCUitvnnXfeqfj3KJvx92HDhtVMi/WJ9SpqyLJGWYwyFNv9ggsuyPbDnXfeWTjttNOy+UvLd5Sh/fbbr/CrX/2q8MADD2TTY56OHTsWBg4cmJWh++67Lyu7pcfDnDlzChdddFE27ZprrqnZFzE9/OIXv8iW87DDDivcddddhXvvvbfw5S9/udC+fftsPyxPcdniePzkk0/qvEpFmY55Y78Wj7/u3btnZSvOI0WxDWNde/bsWbjxxhuzbffNb34z++xll13W6DKzvGOwUllu6HmhuO5xjvnSl76UHb/x+tznPlf41Kc+VXj//fdr5o39Esd6/P6ECROy3491i7JR9Oyzz2bLHp+P813s4zPPPLOwxhprFM4777wV7of4/u222y47z7311lsV51tRWSj+Lc6zcb6N5dhqq62y5frHP/7RqPUpP4bifBfb5dBDD832XVi4cGFhgw02KPTv37/w3//939k5K8rxiBEjCs8999xy15nqIMYQY7S1GCPOz7HcQ4YMadD8sWwHHnhgFld+73vfy64NP/rRj2riteI6NzS+XtF1oDyWCcVr3W677Zadq2Pb7LvvvtkyvfzyyzXzVfps6fVgZdap/Bpduq6l+/j444/P7kdi/0Y5+e1vf1u4+OKLCz/5yU+Wu32LZSuuPSuKY+L7Yt7f/OY32fuII+I3v/Wtb9XZXhFHxH647bbbspg84oSYfscdd6x0vFEpDqx0D9TQeKK47rFvI86K634sb8Rf2267ba3Y7Prrr8+OjdjvEUfFso4bN65w4okn1szTkJi0Pg8//HA236677lq4++67C3Pnzq0438yZMwunnHJKNm/ErMXyO2/evOzvxx13XPa3k08+Ofv9uAfcaKONsu1cGtM3ZH2ifEWZLIp1in1zwgkn1Gyb2P6dO3cufPGLX8xiwIh1brnlluycUXp/1tQkI0rMnj072+mlQeiKxEEXO+7111+vmfbRRx8V1l9//exkUhTfGTt9xowZtT4/ePDg7OAvBvuVDsS4uKy33no1J9dK4rciYVG6HCFOiPF9cTA3JBlRPGHFBebss8/OPhsn/6J4HyeF9957r9bn99hjjyyhsGDBgpppUbj79u2bXUyKF8diMuLf//3fa33+kUceyaZfeOGFFZcvPh/LFetXevIsvTCcccYZtT4TB1BMjxuaVUlGhJNOOqnWxWd5F5FIoMRJ8vHHH681X5xw4zviwle6b0pv9JrDipIRUV7j71EW67sIN2RZIwER80yePLneeYrlO5JypUmi8OlPfzq7cJdfMCOZsOmmmxaWLl2avY+LX/n+KQZocdwdcsghtabH5yKJFYHH8hSXrb7X1KlTa80fJ/C4UMWFI47RKP/lN86x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yaqSjACgVVu8eHGaNWtW+vDDD1NbttZaa6VNN900dezYsaUXBQCox8KFC9Mbb7yRPShpy51TbrHFFmmdddZZpe+RjACg1Vq2bFl69dVXs+z8Zpttlt2ot7URJSKYiYTLO++8k63rtttum9ZYQytLAFgda0REIiIeIGy00UZtLiYpxiURk8R6RkyyKjUkJCMAaLXiJj0SEjGedVz426ouXbpkVSJff/31bJ07d+7c0osEAJSJphlxsx6JiLh2t1UbbbRReu2117L1XZVkhEcrALR61VBToBrWEQDagrZYI6I51k9kAwAAAORKMgIAAADIlWQEAFUr2jtGVcOnnnpqlb5nyy23TGPHjm2y5QIAqstrVRiTNDoZ8dBDD6VDDjkk67U8Ntavf/3rFX5mypQpqV+/flmHW1tttVX66U9/urLLCwCrnccffzwdd9xxLb0YVUdMAgCtNyZpdDLigw8+SDvuuGO6+uqrGzR/DEN20EEHpYEDB6Ynn3wynXPOOenUU09Nd95558osLwCslr1Kt+XRPFZXYhIAaL0xSaOTEYMHD04XXnhh+spXvtKg+aMWRM+ePbOqIn369EnDhw9Pxx57bPrRj360MssLAI0Ww39ecsklaZtttkmdOnXKrks//OEPa/7+yiuvpP322y+7eEfC/dFHH631+Uigf/azn80+G9UfL7/88uVWiXz//fezpxLdu3fPagX27ds3/fa3v635+7Rp09LnP//5bNivGJY0kvRxY03jiEkAaG3EJDn2GREB3aBBg2pNO/DAA9MTTzyRjUtayaJFi9L8+fNrvQBgZY0aNSpLRnzve99Lzz33XLr11luzREHR6NGj07e//e2sneZ2222XvvGNb6QlS5Zkf5s+fXo6/PDD09e//vX0zDPPpPPOOy/7ngkTJtQbZMRNciQcfvnLX2a/d/HFF9eMwx3fEdfBSOo//fTTaeLEienhhx9OJ598sh3czMQkALQ0Mcm/dEjNbPbs2bUCvhDvI8ibO3du2nTTTet8ZsyYMen8889Pedny7N+l1ui1iw9OrY1tbXu31bLdWst3a9/Wm6/bPp2338ZpcZf5qV2HjyvO+8HCBWnslVemUT+4NO18wKEp6h903XKDtNuWfdPzM2dk8wwZdmLqseNeKb7hiBPOTL/61YD0u4f/lnpvs136/oUXp9322icdeswp2d93+cJh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pS5durT04rU555xzTjYkcI8ePdKCBQuytoMPPvhguu+++1p60dqcKM/lfZ+svfbaaYMNNtAnCm2CuCQ/4pL8iEvyJS7Jz7pVFJdIRjSDIUOGpHfffTddcMEFadasWVmhmTRpUurVq1dz/FxVe+KJJ9J+++1XK+AK0bZ+woQJLbhkbU9xqNp999231vSbbropHX300S20VG3X22+/nSU14xwSCZ8YXzoSEV/84hdbetGAVkZckh9xSX7EJfkSl9Ac2hWifg0AAABATvQZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAay0o48+Oh122GGrtAUffPDB1K5du/T+++/bEwCAmASqRIeWXgCg9bryyitToVBo6cUAAKqcmARaH8kIoNGWLl2a1Wbo1q2brQcAtBgxCbRemmlAFdh3333TySefnL3WW2+9tMEGG6T/+q//qqnVsHjx4vSd73wnbb755mnttddOu+++e9Z8omjChAnZ537729+mz3zmM6lTp07p9ddfr9NMY9GiRenUU09NG2+8cercuXPae++90+OPP15rWSZNmpS222671KVLl7Tffvul1157LcctAQC0JDEJUCQZAVXi5z//eerQoUP6y1/+kq666qr04x//OF1//fXZ34455pj0yCOPpNtvvz09/fTT6Wtf+1r60pe+lF588cWaz3/44YdpzJgx2WeeffbZLOFQLhIad955Z/Zbf/vb39I222yTDjzwwPTee+9lf585c2b6yle+kg466KD01FNPpeHDh6ezzz47x60AALQ0MQmQKQBt3j777FPo06dPYdmyZTXTvvvd72bTXnrppUK7du0Kb775Zq3PHHDAAYVRo0Zl/3/TTTdFFYrCU089VWueo446qnDooYdm/79w4cLCmmuuWbjllltq/r548eLCZpttVrj00kuz9/F9lZYjvvuf//xnM609ALC6EJMARfqMgCqxxx57ZP08FA0YMCBdfvnl6Yknnsiaa0TTiVLR5CKacxR17Ngx7bDDDvV+/8svv5w++eSTtNdee9VMW3PNNdNuu+2Wnn/++ex9/FtpOQCA6iEmAYJkBJDat2+fpk+fnv1bap111qn5/+jjoTSJUK7Y/0T5PDG9OM3IGwDA8ohJoHroMwKqxGOPPVbn/bbbbpt23nnnrCfqOXPmZH08lL422WSTBn9/zB+1Jx5++OGaaVFTImpe9OnTJ3sfnV9WWg4AoHqISYAgGQFVIjqPHDlyZHrhhRfSbbfdln7yk5+k0047LWue8c1vfjMNHTo03XXXXenVV1/NRsC45JJLspEvGipG4TjhhBPSWWedle6777703HPPpf/8z//MOr4cNmxYNs+IESOy5hzF5bj11luzkToAgOohJgGCZhpQJSLZ8NFHH2V9OEQVyFNOOSUdd9xx2d9uuummdOGFF6Yzzzwzvfnmm1lfEdGXQ4x60RgXX3xxWrZsWTryyCPTggULUv/+/dP999+fPvWpT2V/79mzZzbaxhlnnJHGjRuXLctFF12Ujj322GZZZwBg9SMmAUK76MXSpoC2Lcb03mmnndLYsWNbelEAgComJgGKNNMAAAAAciUZAQAAAORKMw0AAAAgV2pGAAAAALmSjAAAAAByJRkBAAAA5EoyAgAAAMiVZAQAAACQK8kIAAAAIFeSEQAAAECuJCMAAACAlKf/B5M+yV0GWX8BAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(1, 2, figsize=(13, 5))\n", + "df.groupby(\"period\").choice.value_counts(normalize=True).unstack().plot(\n", + " stacked=True,\n", + " kind=\"bar\",\n", + " rot=0,\n", + " title=\"Choice Probabilities - Discrete Experience Stocks\",\n", + " ax=ax[0],\n", + ")\n", + "\n", + "df_cont_exp.groupby(\"period\").choice.value_counts(normalize=True).unstack().plot(\n", + " stacked=True,\n", + " kind=\"bar\",\n", + " rot=0,\n", + " title=\"Choice Probabilities - Continuous Experience Stocks\",\n", + " ax=ax[1],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "f9614d23", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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1WKfKMtddtml7hH1xx9/vCjv8jpHPBO1yCr35WyVaBeWLFlSWn311Uu77LJLs6c57rjjSrELnHjiiaW77rqrdPXVV5c22GCD0oABA0ovv/xy3Xh77bVXab311ittscUWxTiTJk0qffWrXy2m/clPflI33pgxY0qdO3cunXvuuaU//OEPxTwvu+yy0nnnnVc3zpe//OXSwIEDl1mWmKZyd4z3Me5uu+1Wuu2220q333576f3vf39p3XXXLY0aNap04IEHln7zm9+UbrzxxlKfPn1K2267bendd99dZrn79+9fuuKKK0p33313aeTIkcV8TzjhhGKc//znP8VyxrCjjz66NHXq1OL1t7/9rfj8+uuvLz6bPXt23XwvuOCCYthhhx1W+u1vf1v66U9/Wtp0001LvXv3Lv31r39tsK7dunUrDR48uHTRRReVfv/735e++c1vljp16lT61re+VVqVysv5yCOPVP383//+d1E2++yzT6PbPNaxR48epX333bf0q1/9qnTvvfcW23b48OGl119/vRjnnXfeKX30ox8tdenSpXTaaaeV7rzzztIdd9xROuuss0o333xz3bxivu973/uKMrnppptKf/zjH0t/+ctfSjNmzCi20wc/+MFiu02cOLH0ta99rbTaaqvV7Sfz58+vW59zzjmnrkzmzJlTfB7ziu26xx57lG699dai/I488shi/JhueWK82H9jXSpf9fefBx54oFjP2NfCm2++Wdp6661LW221VbE9W7KfhaVLl5b222+/0hprrFGUfxxH1157bbGdYr5vvfVW3bix3/fr16/Yr3784x+X7rnnntLDDz9c91nsW2WxXB/+8IdL66+/fumSSy4p9rPLL7+82M4f+9jHGqxTLNMmm2xS2nnnnUu/+MUvivLbe++9i/X8+9//XjfetGnTSmuuuWYxbhzzcTz//Oc/L33hC18oLViwoBjnpZdeKn4rYnl++MMfFt/77W9/u9S9e/eiPJoS22/ttdcu9e3bt5i2/vFV3/L2hdg/47OhQ4cW+2zsDzvssEOxf9x///0tWp/KY+jJJ58s1m/IkCENfg+33HLL0uabb1762c9+Vpo8eXJpwoQJxT4cZQTtlRij48YYbV32yzsPVIsf47y00UYbFefW2H5RNp///OeL8eJ3u6zatI2VS3PXqfIcXX9d49WSmLmaWKZYjrFj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6a233mrX2yUCiOhObc0112zrRQEAOnhcsvrqq6d+/fqlbt26rfA8JDcA4P+8++67afbs2cUdhP79+xcn2PbYY0jcAXr55ZfTP//5z7TFFluowQEANagjxCWlUqlI4ERcEusacclqq61Y6xmSGwDwf+LkGoFE9KUedxDasw022CA9++yz6Z133pHcAIAa1FHikp49exaPyz733HPFOvfo0WOF5qNBUQCoPDmu4B2DnLS3Oz8A0F51hLhktVWwju1/KwEAAADtmuQGAAAAkDXJDQBYRaINi3jcY9q0aSs1n0022SRddtllygUAEJc0kwZFAaDGPPLII2mNNdZo68UAAEi5xCWSGwBQgz2ZAADUgg0yiUs8lgIALRTdso0dOzZtvvnmqXv37mnjjTdO3/3ud+s+/8c//pE++tGPFt22fehDH0pTp05tMP2ECRPSBz7wgWLaeATl4osvbvKxlDfeeCMdd9xxqU+fPkX3aNtss036zW9+U/f5lClT0p577ll0pRbdxY0cOTK9+eabyhUAOgBxyf+S3ACAFho9enSR3PjGN76RnnrqqXTTTTcViYeys88+O5122mlF2xvvf//702GHHZaWLFlSfPbYY4+lL3zhC+nQQw9NTz75ZDrvvPOK+dxwww2NBiz7779/kcD4+c9/Xnzf9773vdS5c+fi85jHsGHD0mc/+9k0ffr0dOutt6YHHnggnXjiicoVADoAccn/KXVA8+fPL8Wqx/8AUPb222+XnnrqqeL/xixYsKDUvXv30jXXXLPMZ7Nnzy7OL9dee23dsBkzZhTDZs6cWbw//PDDS/vuu2+D6b7+9a+Xtt5667r3AwcOLF166aXF33fffXdptdVWK82aNavq8gwfPrx03HHHNRh2//33F9M0tR5Nravz5HvL9gagGnFJy86Tam4AQAvMnDkzLVq0KO2zzz6NjrPtttvW/d2vX7/i/3nz5tVN/5GPfKTB+PH+mWeeSUuXLl1mXlH7Y6ONNipqgFQTNUGi1seaa65Z94qaHFHjY/bs2coWANoxccn/p0FRAGiBaNdiebp27Vr3d3QNGyLZEEqlUt2wshi2ot8X8z3++OOLdjYqRVsgAED7JS75/yQ3AKAFtthiiyKQ+MMf/pCOOeaYFm+7rbfeumgTo75oTyNqZpTb0aisBfLPf/4z/fWvf61ae2P77bdPM2bMKBo3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AEDWJDcAAACArEluAAAAAFmT3AAAAACyJrkBAAAAZE1yAwAAAMia5AYAAACQNckNAAAAIGuSGwAAAEDWJDcAAACArEluAAAAAFmT3AAAAACyJrkBAAAAZE1yAwAAAMia5AYAAACQNckNAAAAIGuSGwAAAEDWJDcAAACArEluAAAAAFmT3AAAAACyJrkBAAAAZE1yAwAAAMia5AYAAACQNckNAAAAIGuSGwAAAEDWJDcAAACArEluAAAAAFmT3AAAAACyJrkBAAAAZE1yAwAAAMia5AYAAACQNckNAAAAIGuSGwAAAEDWJDcAAACArEluAAAAAFmT3AAAAACyJrkBAAAAZE1yAwAAAMia5AYAAACQNckNAAAAIGuSGwAAAEDWJDcAAACArEluAAAAAFmT3AAAAACyJrkBAAAAZE1yAwAAAMia5AYAAACQNckNAAAAIGuSGwAAAEDWJDcAAACArEluAAAAAFmT3AAAAACy9p4kN8aPH58GDRqUevTokXbYYYd0//33Nzn+5MmTi/Fi/E033TRdffXVDT6/5ppr0h577JHWWWed4vXxj388Pfzww628FgBA7sQkANA+tXpy49Zbb02nnHJKOvvss9Pjjz9eJCX233//9Pzzz1cdf/bs2emAAw4oxovxzzrrrDRy5Mg0YcKEunHuvffedNhhh6V77rknTZ06NW288cZp6NCh6YUXXmjt1QEAMiUmAYD2q1OpVCq15hfssssuafvtt09XXXVV3bDBgwengw46KI0ZM2aZ8c8444x0xx13pJkzZ9YNGzFiRHriiSeKREY1S5cuLWpwjBs3Ln3pS19a7jItWLAg9e7dO82fPz/16tVrhdcNANqj9nqerMWYpD1vbwBYFZp7nmzVmhuLFy9Ojz32WFGror54P2XKlKrTRLBQOf6wYcPSo48+mt55552q07z11lvFZ+uuu27VzxctWlRskPovAKDjqJWYJIhLAGDVa9XkxiuvvFLcwejTp0+D4fF+7ty5VaeJ4dXGX7JkSTG/as4888z0vve9r2h7o5q4GxOZnvJrwIABK7xOAEB+aiUmCeISAMi0QdFOnTo1eB9PwlQOW9741YaHCy+8MN18883ptttuKxogrWb06NFFFZbya86cOSu4JgBAzto6JgniEgBY9bqkVrT++uunzp07L3NHZN68ecvcCSnr27dv1fG7dOmS1ltvvQbDL7roonTBBRek3//+92nbbbdtdDm6d+9evACAjqlWYpIgLgGAzGpudOvWrejSddKkSQ2Gx/vddtut6jRDhgxZZvyJEyemHXfcMXXt2rVu2Pe///307W9/O911113FZwAAYhIA6Jha/bGUU089NV177bXpxz/+cdHa+KhRo4puYKO18XLVzPqticfw5557rpguxo/prrvuunTaaac1qPZ5zjnnFJ9tsskmxV2VeP373/9u7dUBADIlJgGA9qtVH0sJhxxySHr11VfT+eefn1566aW0zTbbpDvvvDMNHDiw+DyGRbKjbNCgQcXnkQS58sorU//+/dMVV1yRDj744Lpxxo8fX7R6/rnPfa7Bd5177rnpvPPOa+1VAgAyJCYBgParU6ncMlYHoj95AHCerBXiEgBY+fPke9JbCgAAAEBrkdwAAAAAsia5AQAAAGRNcgMAAADImuQGAAAAkDXJDQAAACBrkhsAAABA1iQ3AAAAgKxJbgAAAABZk9wAAAAAsia5AQAAAGRNcgMAAADImuQGAAAAkDXJDQAAACBrkhsAAABA1iQ3AAAAgKxJbgAAAABZk9wAAAAAsia5AQAAAGRNcgMAAADImuQGAAAAkDXJDQAAACBrkhsAAABA1iQ3AAAAgKxJbgAAAABZk9wAAAAAsia5AQAAAGRNcgMAAADImuQGAAAAkDXJDQAAACBrkhsAAABA1iQ3AAAAgKxJbgAAAABZk9wAAAAAsia5AQAAAGRNcgMAAADImuQGAAAAkDXJDQAAACBrkhsAAABA1iQ3AAAAgKxJbgAAAABZk9wAAAAAsia5AQAAAGRNcgMAAADImuQGAAAAkDXJDQAAACBrkhsAAABA1iQ3AAAAgKxJbgAAAABZk9wAAAAAsia5AQAAAGRNcgMAAADImuQGAAAAkDXJDQAAACBrkhsAAABA1iQ3AAAAgKxJbgAAAABZk9wAAAAAsia5AQAAAGTtPUlujB8/Pg0aNCj16NEj7bDDDun+++9vcvzJkycX48X4m266abr66quXGWfChAlp6623Tt27dy/+v/3221txDQCA9kBMAgDtU6snN2699dZ0yimnpLPPPjs9/vjjaY899kj7779/ev7556uOP3v27HTAAQcU48X4Z511Vho5cmSRzCibOnVqOuSQQ9Lw4cPTE088Ufz/hS98IT300EOtvToA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am5MnTq1eBSlnNgIu+66azFsypQpVaeZPXt2mjt3bho6dGjdsO7du6e99tqrwTRvvfVWOuyww4raHVGzY3kWLVqUFixY0OAFAHQMtRSTBHEJAGSU3IiAYMMNN1xmeAyLzxqbJkRNjfriff1pRo0alXbbbbd04IEHNmtZxowZU9fuR7wGDBjQwrUBAHJVSzFJEJcAQA0kN6Kxz2jnoqnXo48+WoxbrT2MePRkee1kVH5ef5o77rgj/fGPfyweX2mu0aNHp/nz59e95syZ0+xpAYDalGNMEsQlAFADbW6ceOKJ6dBDD21ynE022SRNnz49/etf/1rms5dffnmZuyBl5eqccUekfjsa8+bNq5smgoi///3vae21124w7cEHH5z22GOPdO+99y4z36hGGi8AoP3IMSYJ4hIAqIHkxvrrr1+8licaDo1aEg8//HDaeeedi2EPPfRQMSyqb1YzaNCgIpiYNGlS2m677YphixcvTpMnT05jx44t3p955pnpmGOOaTDdBz/4wXTppZemT33qUy1dHQAgU2ISAKDVe0sZPHhw2m+//dKxxx6bfvjDHxbDjjvuuPTJT36yQU8pW221VfHs6Wc+85mimucpp5ySLrjggrTFFlsUr/h79dVXL7qVDZH8qNZg18Ybb1wkRwAAxCQA0LG0WnIj3HjjjWnkyJF1LY1/+tOfLloTry+6hY3aHGWnn356evvtt9NXv/rV9Prrrxctm0+cODGttdZarbmoAEA7JiYBgPatUylaxupgoivY6DUlkiq9evVq68UBgJriPGl7A0BucUmrdQULAAAA8F6Q3AAAAACyJrkBAAAAZE1yAwAAAMia5AYAAACQNckNAAAAIGuSGwAAAEDWJDcAAACArEluAAAAAFmT3AAAAACyJrkBAAAAZE1yAwAAAMia5AYAAACQNckNAAAAIGuSGwAAAEDWJDcAAACArEluAAAAAFmT3AAAAACyJrkBAAAAZE1yAwAAAMia5AYAAACQNckNAAAAIGuSGwAAAEDWJDcAAACArEluAAAAAFmT3AAAAACyJrkBAAAAZE1yAwAAAMia5AYAAACQNckNAAAAIGuSGwAAAEDWJDcAAACArEluAAAAAFmT3AAAAACyJrkBAAAAZE1yAwAAAMia5AYAAACQNckNAAAAIGuSGwAAAEDWJDcAAACArEluAAAAAFmT3AAA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u3TvvfeWXnrppbrXW2+9VTdOTuWzIutTy+UzevTo0n333VeaPXt2afr06aWzzjqrtNpqq5UmTpyYXdmsyPrUctk0prJV8tzKiFVDXFK7x66YpHbLJohLavvcJy6p3bLJIS6R3GiBK6+8sjRw4MBSt27dSttvv32Drh+//OUvFwVbX1z8bLfddsX4m2yySemqq64q5bo+Y8eOLW222WalHj16FN0x7b777qXf/va3pVpR7s6x8hXrkWP5tHR9arl8qq1HvK6//vq6cXIqnxVZn1oun6OOOqrud2CDDTYo7bPPPnWJgNzKZkXWp5bLprlBRG5lxKojLqnNY1dMUtu/q+KS2j73iUtqt2xyiEs6xT+rti4IAAAAwHtHmxsAAABA1iQ3AAAAgKxJbgAAAABZk9wAAAAAsia5AQAAAGRNcgMAAADImuQGAAAAkDXJDQAAACBrkhtAzTjyyCPTQQcdtFLzuPfee1OnTp3SG2+8scqWCwDoeMQlkJcubb0AAGWXX355KpVKNggA0ObEJZAXyQ2gzS1durSobdG7d++2XhQAoIMTl0CePJYCtNjee++dTjzxxOK19tprp/XWWy+dc845dbUuFi9enE4//fT0vve9L62xxhppl112KR4XKbvhhhuK6X7zm9+krbfeOnXv3j0999xzy1T/XLRoURo5cmTacMMNU48ePdLuu++eHnnkkQbLcuedd6b3v//9qWfPnumjH/1oevbZZ5UoAHQg4hIgSG4AK+QnP/lJ6tKlS3rooYfSFVdckS699NJ07bXXFp/913/9V/rTn/6UbrnlljR9+vT0+c9/Pu23337pmWeeqZv+rbfeSmPGjCmmmTFjRpHAqBQJkgkTJhTf9ec//zltvvnmadiwYem1114rPp8zZ0767Gc/mw444IA0bdq0dMwxx6QzzzxTiQJAByMuAeJOK0CL7LXXXqXBgweX3n333bphZ5xxRjHsb3/7W6lTp06lF154ocE0++yzT2n06NHF39dff31U8ShNmzatwThf/vKXSwceeGDx97///e9S165dSzfeeGPd54sXLy7179+/dOGFFxbvY37VliPm/frrrytVAOgAxCVA0OYGsEJ23XXXop2MsiFDhqSLL744Pfroo8XjKfGoSH3xiEk8vlLWrVu3tO222zY6/7///e/pnXfeSR/5yEfqhnXt2jXtvPPOaebMmcX7+L/acgAAHYu4BJDcAFa5zp07p8cee6z4v74111yz7u9oI6N+UqJSuf2OynFieHmYnlUAAHEJELS5AayQBx98cJn3W2yxRdpuu+2KVsbnzZtXtJFR/9W3b99mzz/Gj9odDzzwQN2wqMkRNUMGDx5cvI/GSKstBwDQsYhLAMkNYIVEY56nnnpqmjVrVrr55pvTD37wg3TyyScXj6McccQR6Utf+lK67bbb0uzZs4seTsaOHVv0bNJc0cvKV77ylfT1r3893XXXXempp55Kxx57bNEQ6dFHH12MM2LEiOLxlfJy3HTTTUVPLABAxyIuATyWAqyQSF68/fbbRRsY8fjJSSedlI477rjis+uvvz595zvfSV/72tfSCy+8ULS1EW1hRK8mLfG9730vvfvuu2n48OFp4cKFaccdd0x33313WmeddYrPN95446I3lVGjRqXx48cXy3LBBReko446SqkCQAciLgE6RauiNgPQ0v7kP/zhD6fLLrvMhgMA2pS4BAgeSwEAAACyJrkBAAAAZM1jKQAAAEDW1NwAAAAAsia5AQAAAGRNcgMAAADImuQGAAAAkDXJDQAAACBrkhsAAABA1iQ3AAAAgKxJbgAAAAApZ/8PtxTEjiIx3dMAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(1, 2, figsize=(13, 5))\n", + "df.groupby([\"period\", \"choice\"]).consumption.mean().unstack().fillna(0).plot(\n", + " rot=0, title=\"Consumption - Discrete Experience Stocks\", ax=ax[0]\n", + ")\n", + "\n", + "df.groupby([\"period\", \"choice\"]).consumption.mean().unstack().fillna(0).plot(\n", + " rot=0, title=\"Consumption - Continuous Experience Stocks\", ax=ax[1]\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "8a10b970", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 24, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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+1TbrZTFfS31u9/9Lg0YMCDb72J7XIyVVtUnn3ySxaabbbZZlbKM47jBBhtk5Ve8jblYXnEcItaI+CTa8+gBE23eqsQb5eLAcrdp1DWeiLKNHjr33Xdf1ismtiHeE72Aq3v77bfTiSeemB37iBUjto1tjWuHorrEpOXE61GXVhTHRF2KfQkRpxbrb5R50eOPP56Ve9SpKKcor7/85S8rtT/Vxf5FnBnfq+jdFKJMi+splnVce8X1WJMqUGfjx48vRJE99dRThU8//bTKY8mSJVWWHTp0aKFTp06Fp59+Ovv7r3/9a6Fdu3aFc889t3KZN954I1tfr169CrvuumvhzjvvLPzud78rfOlLXyp07NixMHny5MplX3jhhUK3bt0KX/ziFwu33HJL4YEHHiicfvrp2TovuOCCyuX+9re/ZevcaKONCt/85jcLd999d+HPf/5zYe7cuZWvxf+LHn744Ww7d9ttt8KECRMK9913X+GYY47Jlov9rb7vm266aeGUU04p3H///YUbbrih8JnPfKaw5557Vtn3H/3oR9myhxxySLY/sa2XX3559nzR//zP/xQqKioKBx98cOGuu+4q3HPPPYWvfOUrhfbt2xceeuih5R6HfffdN1vuo48+qvexi3I58cQTC/fee2/h97//fXbcfvKTn2Tbctxxx2VlFdszcODAwhprrJGVe2OW1aoq1qFf/OIXtS6z0047FTp37pzV01CuHmy55ZaFzTbbLDsujz76aFYXo36VLlOX47r77rsX1llnnaxOX3311dn7Y31xrLbbbrtC9+7ds/fEMb7yyiuzOr3XXnsVli1blr3/ySefLKy22mqFAw44IPt3PIrHYObMmdl6e/fuXbjuuuuydfz4xz/O9i2Ow4rEtn3hC1+o8d2Nx9KlSyuXmzNnTmHjjTcuDBgwoLB48eLsuaOPPjrbrueee65yufPPPz8rj9ieM888MzvOsW9Rb7bffvvK94a61rH4nPjuf/azny2MHj06O2/EeouvxWeVOuGEE7Ll41hFfbztttsKn//85ws9evQozJo1q8q+r7vuuoXNN9+8cO211xYefPDBwve///1s+3/9619XLjd//vysjGK7Lrroouyzoy6cdtppWf0PUVb7779/tsyFF16YrSvqd3y3ttpqq8LChQuXexz22WefQocOHbLye/bZZ2ucO4uWVxf+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9NNPVykZ4acVABqVX/GVIwA0F+KSVddQPV0lIwAAAIBcSUYAAAAAuZKMAAAAAJp3MuKxxx5LBx10UDYyetwr8sc//nGF73n00UdTv379sgG3Nt1003Tttdeu7PYCAIhJAKCtJSM++uijtO2226ZrrrmmTsvHNGQHHHBA2m233dK0adPSOeeck0499dR05513rsz2AkCTiVGjaT7EJAC0VZ+2gpik3smIwYMHp4svvjgdcsghdVo+ekFssskmacyYMalv377p+OOPT8cdd1y69NJLV2Z7AWhlc1VfcsklWa+5mL4ykt2///3vs+f32WeftP/++2f/Dh988EHWnowaNSr7+5FHHsl66P3lL3/J3he97wYMGJCef/75On/+r371q8qpR7/+9a+nyy+/PK299tqVr19wwQVpu+22SzfddFO2jZ07d862Z968eenEE09M66+/furatWvaa6+90j//+c8q677nnnuq9Aq88MIL05IlSypfj22/4YYbss+Nz4+5uu++++4GKNW2Q0wCQEMRk9yQe0zS6GNGPPnkk2nQoEFVnttvv/3SlClTas3mLFq0KM2fP7/KA4DW59xzz03jx49P48aNSy+88EIaPnx4+u53v5vdEvjrX/86/eMf/0hXXXVVtuzQoUNTjx49sgRBqTPPPDNLcD/99NNZcuCrX/1qnX4teOKJJ7J1nnbaaenZZ59N++67b/rJT35SY7nXXnst/fa3v8169MVy4cADD0yzZs1KEydOTFOnTk077LBD2nvvvdP777+fvX7//fdn+xE9AV988cV03XXXpZtvvrnG+iNBcdhhh6Xnnnsu60V4xBFHVK6DhicmAaA2YpILc49JOjR2dYxgLYLHUvF3/Do0Z86ctOGGG9Z4z+jRo7MALS+fLF2YWqIu7VdPLY2yVt6ttW631PrdlGUdXeyjJ8LDDz+cBg4cmD0XPQgef/zx7OL9tttuy/5/5JFHpnfffTfraRC3+3Xs2DEtKyzNHuFH5/0o7b3PXtm/x998U9qkV+905113psMOO3S5n3/V1Vel/Qfvn0acPjz7e7PNP5eemPxE+suf/1K57kJhWVq8eHH69S03p/XWWy977q8PP5T1vpj17sysp0S45Bc/z8ZQ+u3vfptOPPGE9JOfXJzOOuuH6cijvpu9/tk+vdOFF12Yzj7r7HT++edXbsMxxxyTvv3tb2f//ulPf5quvvrqLAETPUJoeGKSxuO8na+WWN4tsY1sqWUdlHd+MUkQkzTTZESxK2qpYpfb6s8XjRw5Mo0YMaLy7+gZEd1oAWg9osfAJ598kvVIKBUX/9tvv33270MPPTT94Q9/yJLU0Xtiiy22qLGegQN3qvz3Ouusk7bccsv0r5deWuHnv/LyK+ngg79W5bkdv/SlLBlRqnfv3pWJiPDM1GfShx9+mNbrvn6V5T7++OP079dfz/49deoz6emnp6Sf/nR05etLly7N9nfhwoVZF8iwzTbbVL6+xhprpLXWWivNnj17hdvOyhOTAFCdmCQ1SUzS6MmIDTbYIPslolTsVIcOHdK6665b9j3xS1Px1yYAWqdly5Zl/48xHzbaaKMqrxXbgLhwj9sg2rdvn1599dU6r7u2ZHf1xHhtF6alokGuvt3Rq+/hv/21xrLF8SZimQsuOD99/ZCvV3m9XUX7bAyJouIvKqXbXSwXGp6YBIByxCSpSWKSRk9GRDeX6MZS6oEHHkj9+/evscMAtB1bbbVVlnSYPn162n333csuc/rpp6d27dqle++9N7t/McZqiMEiSz311N+zgS3Df//73/TKK6+kLT//+RV+/paf3zL94+mnqzw3ZerUFb5v+x22z5LskVT/7Gc/W3aZHXbYPr388itps802q5GMoOmISQAoR0zSNOqdjIiuqTGYV+nUnTGgV3SNjWAwbrF4++230y233JK9HoODxTSgcdvFCSeckA0edeONN6bbb7+9YfcEgBYluv+dccYZ2aCVkXnfdddds9vyJk+enNZcc83UvXv3bBaLaDdigMizzz47HX300dnASt3W7lq5not/fHFad911svGIzj33R9n7qt9+Uc7JJ/8g7bH7numKy69IXznoK+nhh/+W7rv3vhX2qohZPuLWkEO+/o00+mc/zW4Leeedd9K9E+9NXzv4a1my/dwfnZu+etDXUq9eG6dvHvrNLKHy3HPPpxf+94VsRioahpgEgKaOST7zmc9UrkdM0sizacQsGHEvb/F+3kgyxL/PO++87O+ZM2dmv3IV9enTJxttPKZgi+nRfvzjH2cjo3/jG9+o70cD0MpEmxDtR4wJEdM/x2xL0ZsuehwMGTIkmzkjGv0QAz/27NkzS3KX+unon6Thw0akL/XfMc2aOTP98U9/SJ06dVrhZ++yyy5p7Lix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rbeu8v71118/denSpcbzAABiEgBoG+qdjDj88MPT3Llz00UXXZRmzpyZJRUmTpyYevfunb0ez0VyAgCgMYlJAKDlqijEAA7NXEztGbNqxGCWcetHQ/tk6cLUEnVpv3pqaZS18m6tdbul1m9l3TrKu7HbSfIr65Z4HgnOJcp7RdTtfClv5d0SYpKVmk0DAAAAYGVJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAAKD5JyPGjh2b+vTpk7p06ZL69euXJk2aVOuyd911V9p3333Teuutl7p27ZoGDhyY7r///lXZZgAAMQkAtKVkxIQJE9KwYcPSqFGj0rRp09Juu+2WBg8enKZPn152+cceeyxLRkycODFNnTo17bnnnumggw7K3gsAsLLEJADQclUUCoVCfd4wYMCAtMMOO6Rx48ZVPte3b9908MEHp9GjR9dpHV/4whfS4Ycfns4777yyry9atCh7FM2fPz/16tUrzZs3L+td0dA+WbowtURd2q+eWhplrbxba91uqfVbWbeO8o52slu3bo3WTjZXYpLmw7lEebfGNjKo28q7tdbvLs0gJqlXz4jFixdnvRsGDRpU5fn4e/LkyXVax7Jly9KCBQvSOuusU+sykdSIHSg+IhEBACAmAYDWoV7JiDlz5qSlS5emHj16VHk+/p41a1ad1nHZZZeljz76KB122GG1LjNy5Mgsk1J8zJgxoz6bCQC0cmISAGjZOqzMmyoqKqr8HXd6VH+unNtvvz1dcMEF6U9/+lNaf/31a12uc+fO2QMAQEwCAG08GdG9e/fUvn37Gr0gZs+eXaO3RLlBpoYMGZJ+97vfpX322WflthYAQEwCAG3rNo1OnTplU3k++OCDVZ6Pv3feeefl9og45phj0m233ZYOPPDAld9aAAAxCQC0vds0RowYkY488sjUv3//NHDgwHT99ddn03oOHTq0cryHt99+O91yyy2ViYijjjoqXXnllWmnnXaq7FWx2mqrZYNTAgCsDDEJALShZERMyTl37tx00UUXpZkzZ6att946TZw4MfXu3Tt7PZ6L5ETRddddl5YsWZJ+8IMfZI+io48+Ot18880NtR8AQBsjJgGAlquiEKNPNnONPX96S5wXtqXOe6yslXdrrdsttX4r69ZR3o3dTpJfWbfE80hwLlHeK6Ju50t5K++WEJPUa8wIAAAAgFUlGQEAAADkSjICAAAAyJVkBAAAAJAryQgAAAAgV5IRAAAAQK4kIwAAAIBcSUYAAAAAuZKMAAAAAHIlGQEAAADkSjICAAAAyJVkBAAAAJAryQgAAAAgV5IRAAAAQK4kIwAAAIBcSUYAAAAAuZKMAAAAAHIlGQEAAADkSjICAAAAyJVkBAAAAJAryQgAAAAgV5IRAAAAQK4kIwAAAIBcSUYAAAAAuZKMAAAAAHIlGQEAAADkSjICAAAAyJVkBAAAAJAryQgAAAAgV5IRAAAAQK4kIwAAAIBcSUYAAAAAuZKMAAAAAHIlGQEAAADkSjICAAAAyJVkBAAAAJAryQgAAAAgV5IRAAAAQK4kIwAAAIBcSUYAAAAAuZKMAAAAAHIlGQEAAADkSjICAAAAyJVkBAAAAJAryQgAAAAgV5IRAAAAQK4kIwAAAIBcSUYAAAAAuZKMAAAAAHIlGQEAAADkSjICAAAAyJVkBAAAAJAryQgAAAAgV5IRAAAAQK4kIwAAAIBcSUYAAAAAuZKMAAAAAHIlGQEAAADkSjICAAAAyJVkBAAAAJAryQgAAAAgV5IRAAAAQPNPRowdOzb16dMndenSJfXr1y9NmjRpucs/+uij2XKx/Kabbpquvfbald1eAAAxCQC0tWTEhAkT0rBhw9KoUaPStGnT0m677ZYGDx6cpk+fXnb5N954Ix1wwAHZcrH8Oeeck0499dR05513NsT2AwBtlJgEAFquikKhUKjPGwYMGJB22GGHNG7cuMrn+vbtmw4++OA0evToGsufddZZ6e67704vvfRS5XNDhw5N//znP9OTTz5Zp8+cP39+6tatW5o3b17q2rVramifLF2YWqIu7VdPLY2yVt6ttW631PqtrFtHeTd2O9lciUmaD+cS5d0a28igbivv1lq/uzSDmKRDfVa8ePHiNHXq1HT22WdXeX7QoEFp8uTJZd8TCYd4vdR+++2XbrzxxvTpp5+mjh071njPokWLskdR7EhxxxpDS6w8YXH7JamlUdbKu7XW7ZZav5V16yjvYvtYz98XWjQxSfPiXKK8W2MbGdRt5d1a6/fiZhCT1CsZMWfOnLR06dLUo0ePKs/H37NmzSr7nni+3PJLlizJ1rfhhhvWeE/0sLjwwgtrPN+rV6/6bC4AtCkLFizIfo1oC8QkANCyY5J6JSOKKioqqvwdWY/qz61o+XLPF40cOTKNGDGi8u9ly5al999/P6277rrL/ZzmJrJCkUCZMWNGm+o22xSUtfJurdRt5V0X0a5Go9+zZ8/U1ohJ6sa5JF/KW1m3Vuq28m7ImKReyYju3bun9u3b1+gFMXv27Bq9H4o22GCDsst36NAhSy6U07lz5+xRau21104tVSQiJCOUdWukbivr1qol1u220iOiSEzSdup2S6a8lXVrpW4r74aISeo1m0anTp2yKToffPDBKs/H3zvvvHPZ9wwcOLDG8g888EDq379/2fEiAADEJADQutV7as+4feKGG25IN910UzZDxvDhw7NpPWOGjOItFkcddVTl8vH8m2++mb0vlo/3xeCVZ5xxRsPuCQDQpohJAKDlqveYEYcffniaO3duuuiii9LMmTPT1ltvnSZOnJh69+6dvR7PRXKiqE+fPtnrkbT45S9/md07ctVVV6VvfOMbqbWLW03OP//8GrecoKxbOnVbWbdW6nbLIiapO3U7X8pbWbdW6rbybkgVhbY0DxgAAADQ8m7TAAAAAFgVkhEAAABAriQjAAAAgFxJRgAAAAC5koxoJGPHjs1mEunSpUvq169fmjRpUmN9VJv22GOPpYMOOiibpaWioiL98Y9/bOpNarVGjx6dvvSlL6W11lorrb/++unggw9OL7/8clNvVqs1bty4tM0226SuXbtmj4EDB6Z77723qTerzdT1OJ8MGzasqTcFGoy4JB/ikvyIS/IlLmk6o1txXCIZ0QgmTJiQVZZRo0aladOmpd122y0NHjy4ypSnNIyPPvoobbvttumaa65RpI3s0UcfTT/4wQ/SU089lR588MG0ZMmSNGjQoOwY0PA23njj9LOf/SxNmTIle+y1117pa1/7WnrhhRcUdyN6+umn0/XXX58lgqC1EJfkR1ySH3FJvsQlTePpVh6XmNqzEQwYMCDtsMMOWQaxqG/fvtkvyZHZonFExvAPf/hDVs40vvfeey/rIRHBwJe//GVFnoN11lkn/eIXv0hDhgxR3o3gww8/zM7d8QvyxRdfnLbbbrs0ZswYZU2LJy5pGuKSfIlL8icuaVwftoG4RM+IBrZ48eI0derU7BfjUvH35MmTG/rjoMnMmzevsiGicS1dujTdcccd2S9ucbsGjSN6/hx44IFpn332UcS0GuIS2gpxSX7EJfn4QRuISzo09Qa0NnPmzMm+oD169KjyfPw9a9asJtsuaEiFQiGNGDEi7brrrmnrrbdWuI3k+eefz5IPn3zySVpzzTWznj9bbbWV8m4Ekex55plnsu6Q0JqIS2gLxCX5EJfk5442EpdIRjRi17zqJ8nqz0FLdfLJJ6fnnnsuPf744029Ka3alltumZ599tn0wQcfpDvvvDMdffTR2W0xEhINa8aMGem0005LDzzwQDboMLRG4hJaM3FJPsQl+ZjRhuISyYgG1r1799S+ffsavSBmz55do7cEtESnnHJKuvvuu7MRw2MwIxpPp06d0mabbZb9u3///ll2/Morr0zXXXedYm9AcWtdnKNj5qOi6OEWdTwGx120aFF2XoeWSFxCaycuyY+4JB9T21BcYsyIRviSRsWJ2QZKxd8777xzQ38c5CZ698QvD3fddVd6+OGHs6lryf8YRANEw9p7772zrqfRC6X4iOTPEUcckf27tTT4tE3iElorcUnTE5c0jr3bUFyiZ0QjiHvpjzzyyKzSxP3eMR1LTOs5dOjQxvi41NZHmX3ttdcq/37jjTeyL2kMqrjJJps06ba1xkF0brvttvSnP/0prbXWWpW9f7p165ZWW221pt68Vuecc87JpgTu1atXWrBgQXbv4COPPJLuu+++pt60Vifqc/WxT9ZYY4207rrrGhOFVkFckh9xSX7EJfkSl+RnrTYUl0hGNILDDz88zZ07N1100UVp5syZWaWZOHFi6t27d2N8XJs2ZcqUtOeee1YJuELcW3/zzTc34Za1PsWpavfYY48qz48fPz4dc8wxTbRVrde7776bJTXjHBIJn5hfOhIR++67b1NvGtDCiEvyIy7Jj7gkX+ISGkNFIfrXAAAAAOTEmBEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhHASjvmmGPSwQcfvEol+Mgjj6SKior0wQcfOBIAgJgE2ogOTb0BQMt15ZVXpkKh0NSbAQC0cWISaHkkI4B6W7p0adaboVu3bkoPAGgyYhJoudymAW3AHnvskU4++eTssfbaa6d11103nXvuuZW9GhYvXpx++MMfpo022iitscYaacCAAdntE0U333xz9r4///nPaauttkqdO3dOb775Zo3bNBYtWpROPfXUtP7666cuXbqkXXfdNT399NNVtmXixIlpiy22SKuttlrac88903/+858cSwIAaEpiEqBIMgLaiF//+tepQ4cO6e9//3u66qqr0hVXXJFuuOGG7LVjjz02PfHEE+mOO+5Izz33XDr00EPT/vvvn1599dXK9y9cuDCNHj06e88LL7yQJRyqi4TGnXfemX3WM888kzbbbLO03377pffffz97fcaMGemQQw5JBxxwQHr22WfT8ccfn84+++wcSwEAaGpiEiBTAFq93XffvdC3b9/CsmXLKp8766yzsudee+21QkVFReHtt9+u8p699967MHLkyOzf48ePjy4UhWeffbbKMkcffXTha1/7WvbvDz/8sNCxY8fCrbfeWvn64sWLCz179ixccskl2d+xvnLbEev+73//20h7DwA0F2ISoMiYEdBG7LTTTtk4D0UDBw5Ml112WZoyZUp2u0bcOlEqbrmI2zmKOnXqlLbZZpta1//666+nTz/9NO2yyy6Vz3Xs2DHtuOOO6aWXXsr+jv+X2w4AoO0QkwBBMgJI7du3T1OnTs3+X2rNNdes/HeM8VCaRKiuOP5E9WXi+eJzZt4AAJZHTAJthzEjoI146qmnavy9+eabp+233z4biXr27NnZGA+ljw022KDO64/lo/fE448/Xvlc9JSInhd9+/bN/o7BL8ttBwDQdohJgCAZAW1EDB45YsSI9PLLL6fbb789XX311em0007Lbs844ogj0lFHHZXuuuuu9MYbb2QzYPz85z/PZr6oq5iF43vf+14688wz03333ZdefPHFdMIJJ2QDXw4ZMiRbZujQodntHMXtuO2227KZOgCAtkNMAgS3aUAbEcmGjz/+OBvDIbpAnnLKKenEE0/MXhs/fny6+OKL0+mnn57efvvtbKyIGMshZr2oj5/97Gdp2bJl6cgjj0wLFixI/fv3T/fff3/6zGc+k72+ySabZLNtDB8+PI0dOzbblp/+9KfpuOOOa5R9BgCaHzEJECpiFEtFAa1bzOm93XbbpTFjxjT1pgAAbZiYBChymwYAAACQK8kIAAAAIFdu0wAAAABypWcEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAABIefp/mgsUaTL6JjYAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(1, 2, figsize=(13, 5))\n", + "df.groupby(\"period\").exp_green.value_counts(normalize=True).unstack().plot(\n", + " stacked=True,\n", + " kind=\"bar\",\n", + " rot=0,\n", + " title=\"Experience Green - Discrete Experience Stocks\",\n", + " ax=ax[0],\n", + " cmap=\"Greens\",\n", + ")\n", + "\n", + "df_cont_exp.groupby(\"period\").exp_green.value_counts(normalize=True).unstack().plot(\n", + " stacked=True,\n", + " kind=\"bar\",\n", + " rot=0,\n", + " title=\"Experience Green - Continuous Experience Stocks\",\n", + " ax=ax[1],\n", + " cmap=\"Greens\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "c37f18d4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABCMAAAHUCAYAAAAA4OLOAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAASY1JREFUeJzt3QmcVWXdOPCHHTcwIXBDwlzCSE1IBCV3DMw0TW1zRYvcAlwSMU0zUSvDJVBzy1KjUsuMTKpXRVETxPRVKzUTNBDBBBQDgfv//E7vnf+dy8wwM8yc2b7fz+cq98y5957z3Oec87u/8yztCoVCIQEAAADkpH1eHwQAAAAQJCMAAACAXElGAAAAALmSjAAAAAByJRkBAAAA5EoyAgAAAMiVZAQAAACQK8kIAAAAIFeSEQAAAECuJCMawa233pratWtX7ePBBx9MzVlsX0vYznX55z//Wanc27dvnz7wgQ+k/fffPz3wwAON8lnx3TekD33oQ5W2v3v37ql///7p2GOPrXYfYt1vfetbqTm644470qRJkxr8fWN/azrm4vtpCeeM5r6dtfHCCy+kY445Jm277bapa9euqWfPnmm33XZLp512Wlq6dGmj14WqjqFPf/rTjf45kBcxRvPQGmKMsGLFinTttdemvfbaK9v+zp07p6222iodddRR6aGHHkqNqabrQHOOZRrD8ccfX2Mc09wV47DW4Iknnkif/exn0zbbbJO6dOmSevfunYYMGZLOPPPMSutNnjy5UY7JclGuEUO1Vh2begNas1tuuSV95CMfWWv5TjvtlJqz+OHw2GOPNfvtrK3TTz89ffGLX0yrV69Of/3rX9NFF12URo4cmf70pz+lT37yk6m523PPPdP3vve97N/vvPNO+tvf/pZ+9rOfpYMOOigdccQR6c4770ydOnWqWD++u6233jo1RxF4/O///m8aM2ZMo7z//fffnyVsym2xxRapOTv44IOz7625b+e6zJkzJ6uvkTC74IILskTAokWL0l/+8peszp511lmpW7duudQFaO3EGM1DS44x4vz8qU99Kj3zzDPpxBNPTGeffXbabLPN0uuvv55+/etfZ4mV2bNnp1122aVRPr+m60BzjmUaywYbbJDVm5bopJNOyupSS/fb3/42feYzn0n77LNPuuKKK7K4bP78+WnWrFlZHPP973+/UjIibrhEIon6k4xoRAMGDEiDBg1KLcX777+fZd/ix8Iee+yRWovIbBb3J34obb/99mnvvfdON910U7MPFMKmm25a6fs44IAD0qmnnpploSPoOf/889Pll19e8fc8v7vly5enDTfcMDUXAwcOzC4MLcV7772XtR744Ac/mD1aurjDFXcHo1XVJptsUrH8c5/7XPr2t7+dCoVCk24ftCZijOahJccY0coyksW///3v03777Vfpb5///OfTuHHjstYSTaE1xaG1FdfPlrbfxTgwEketIXkUCYh+/fplx0THjh0rHQ/xNxqebhpNKDJs8eM/mseVuvDCC1OHDh3S9OnTKzXPi4PgO9/5Tnbhix8wkej44x//uNb7vvjii1mWvlevXlnzorhL+cMf/rDKrhg/+clPsmZH0SQv1n3ppZeq7aYRWcHIFkbWPD7/4x//ePr5z39eZfPR//mf/0lf+9rXsh+GPXr0SIcffnj617/+VWVWPJo+bbzxxtlj1113zS7gpf7whz9k2flIksQJLy72Ve13bRUTRG+88Ual5QsWLEhf/epXs5NpNFOMk1H82F+1alWl9WI/ovli/NiKu/BHH3109tq8RTLiox/9aFZ//vOf/1TbtDEuFHFHOvYnvrf4/qIMokVFebO0Qw45JPu+Yr0Pf/jDle5WFJvgPfXUU9mPywhQYp0QPzIjQxzfX2T242+xzj/+8Y+K10eWOTLOr776apVND1euXJkuueSSrDVR1MX4cX7CCSekN998s8HK7LLLLssu9r/5zW8qLY+sdtStZ599NntePAZ++tOfZsHY5ptvnu1XBJhx979cXY6NaL4bd6Bi/+Izo4lsdd00alP3i9/Lc889l77whS9kdTKaFMZnLFmypNK6a9asSddcc03F91RMdN17772V1ps6dWp2XG600UbZcRmtcKra73KLFy/OtjVeU5Xi972uuvDWW2+lU045JTsvxbEYXT4mTJiQlVV99qdc1NUIMuJcWzRlypTs7l9sexzbUQ/PO++8de4zNFdiDDFGTaLFw+9+97s0atSotRIRRZ/4xCeymLMoWjEceuih2TU+rnVx7v3xj39c6TXF62fEGHHe3nLLLbPrQtxIiZadReu6DpTHMnWJL6vr4hGt9crvYtdmn6q7RlcVL8e1MroGFmPw2P9o/fjaa6+lhjB69OhsO+P7K70WRqwQ1/64i1+6zfFbImKpiE/imh5xXmlsVp94o6o4sLpuGrWJJ+I7ib/Fb5BoVRT/7tOnT/b7pPy6H88vvvji7LdNlEPUgX333TfNnDmzYp3axKQ1xTFRt0oTEUURP5bWpYi7oitTse7GsqK5c+emL3/5y5V+i0Wriviu6ro/5WL/Ij6JVtE/+tGPsmXxvhFD77jjjhXx0M4775yuuuqq1OwVaHC33HJL3P4rPP7444X333+/0mPVqlWV1h09enShc+fOhSeffDJ7/sc//rHQvn37wvnnn1+xziuvvJK9X58+fQp77bVX4a677ir84he/KHziE58odOrUqTBz5syKdZ977rlC9+7dCx/72McKt912W+GBBx4onHnmmdl7futb36pY73/+53+y99xqq60Kn/vc5wr33ntv4b777issXry44m/x/6I//elP2XYOGzasMHXq1ML9999fOP7447P1Yn/L933bbbctnH766YXf//73hRtvvLHwgQ98oLDvvvtW2vdvfvOb2bqHH354tj+xrVdeeWW2vOgnP/lJoV27doXDDjuscPfddxd+85vfFD796U8XOnToUPjDH/5Q4/dQLLfvfve7lZb/7//+b7Y8tq9o/vz5Wfn27du3cP3112fv/e1vf7vQpUuXbD+Lli9fXujfv39Wxtdcc022f2eccUZhm222WassGkJsz8EHH1zt388999zsc2fMmFGxLJ5feOGFFc+/+tWvFjbccMOsbOM7je/5sssuy7a/KL7PqEs777xz4dZbb82+75tvvrnw+c9/vmKdeM9479imb3zjG4Xp06cXfvWrX2V/O/nkk7PXR12L97rjjjsKH/nIRwq9e/cuLFiwoKJu7rnnnoXNN9+88Nhjj1U8wurVqwuf+tSnChtttFHhoosuyt476k3Uz5122ikr95oUty0+q6Zjbs2aNYWRI0dm9fGf//xntiz2M14bn1dUPAaiThx66KFZvfvpT39a2G677QrdunUrvPzyy/U+NmKfvvKVrxR+97vfFX75y19m21f8W9TZutb94r7vuOOOhQsuuCAru/iuo+6ecMIJlcrpmGOOyd7zpJNOKvz617/OtuE73/lO4aqrrqpYJ57HOieeeGJWV+KzhwwZkn038R3W5JJLLsm25Qtf+ELhwQcfrPZ7q6kuvPfee1k9jM/73ve+l50X4pzQsWPH7Lur6/6UHkPx/Ucd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zZkzJ5133nnpjDPOSHfddVd9thcAmq3Vq1dns3+QDzEJALTcOKTOyYgRI0akSy65JB1++OG1Wj9aQWyzzTZp0qRJqX///umkk05KJ554Yvre975Xn+0FgLXmu77iiiuylncxBWYkzH/5y19myw844ID0qU99Kvt3ePvtt7Nr0oQJE7LnDz74YNbK77e//W32umjBN3jw4PTss8/WqUnjfffdl3baaafUpUuXiuk3zznnnLTVVluljTbaKHvP+Kzy18a2xJSln/3sZ9PixYt9s3UkJgGgqYlDmvGYEY899lgaPnx4pWUHHXRQmjVrVjYvaVVWrFiRli5dWukBAFU5//zz0y233JKmTJmSnnvuuTR27Nj05S9/OetW+OMf/zj9+c9/TldffXW27ujRo1Pv3r3Tt771rUrvcfbZZ2dJ8ieffDL16tUrfeYzn6n2GlVu+fLlaeLEienGG2/MPj9ef8IJJ6RHH300/exnP0vPPPNMOvLII7OkyIsvvpi95oknnsgS86ecckp6+umn07777psl+mlcYhIAGpo4pP46pka2YMGCLPArFc9XrVqVFi1alLbYYou1XhNB3UUXXZRys3xJapE27J5aHGWtvFtr3W6p9bullvXqVRXN9K+88sr0p+kPpCFDhmTLtj3my+mRGQ+n66+7Lt3x05+k66dMTsccd3x6Y/789Jvf/CbNmfVk6tS+3X/fY/Xq7DUXfvP8dOB++2b//vHNN6Wt+34o3XPXL9NRRx5Z83asWZ0lLSZfc3XWsiK8/PLL6c4770yvvfrPbHylcNbYMen+3/0uS5pceuml6aqrrsoS8+eee2729x122CHNnDkz3X///Y1XZohJGlNLPZe0xPN2Sy1vZa28W2HdbnFxyE03pUu/c0m6atKkZhGHNHoyIkQT2FLF5rLly4vGjx+fxo0bV/E8Wkb06dOnkbcSgJbm+eefT//5z3/SgZ8aUWl5dJP4+K67Zv8+8nOfS/f86tdp4uWXpyk/vDa74JYbssceFf/ebLPN0o477pBeeOGvtdqGzp07p5133rni+VNz5mTXuR3677RWq78ePXtm/37hhReyrhmVtmHIEMmIHIhJAGizcUiPHtm/X/hrxCGHN3kc0ujJiM033zy7E1Fq4cKFqWPHjhWFUS763MYDAGpSHKTpt/fem7baasu1riXFbhSzn3oqdejQIb344ku1LtDqEublYpyK0nVjm+KzZv/5iez/pTbuvmmlpDz5EpMA0KbjkI03blZxSKMnIyLDEs1RSj3wwANp0KBBqVOnTo398QC0YsVBI+fOm5v23vuTVa5z5tlnp/bt26ff3XdfGnnIIengkSPTfv/XFLLo8SeeyAaTDP/+97/T3//+YvrIR3as1zbFnZAYzXrhwjfTsGF7Vf5jh44V2/34449X3oay5zQ8MQkAbToO+T879W8ecUidkxHvvPNOeumllypN3RmDb0VzkijA6GLx+uuvp9tuu61isLCYBjS6XZx88snZ4FE33XRT1o8FANbHJptsks4aNy6NPfOs7E7AXnvumXXtm/nYY1n2v2ePnunmW25Njz0yI+22227p3HPOScedeGJ6Zs5T6QMf+EDF+1x8yXdSj816pN69e6UJ37wg9ezZMx126KH12qZofvmlL34hHXvCCen7370iCwpijKQ//c+D6WO77JJNdx1TXA8dOjSbBeSwww7LkvTGi6g7MQkATR6HnHVWy4lDBgxII0eOSGecdmoaOuyTTR6H1Hk2jZgF4+Mf/3j2CJFkiH9fcMEF2fP58+enuXPnVqzfr1+/NG3atGxKs1133TV9+9vfzkY1P+KIIxpyPwBoo7598UXpgvMnpImXX5H6D/hYOmjkwek39/02fajvh9Kor3wlfeuCC7IAIFx4wTfTlltskUafcmql97jsO99JXx83Lg3cfXB2Hbv3nruzPpj1FQNEHfvlL6czzz4n7bjTR9NnPnt4euLPf64Y/2iPPfbIZt+45pprsmtjBAExGjd1IyYBoKnF79uWE4ds3azikHaF5tJhpAaRXerevXtasmRJ6tatW8N/gNF986Os86W8lXcLH7U6BoWKFniR2O7atetas2msrwcffCjte8AB6d+L3kybbvrf8Rwa1f9102jQssjjOkl+Ze28nS/lraxb+HWyWur2eqnpmlulesYlD+YdhzSzmKTOLSMAAAAA1odkBABUY8TBn85mwKjqcenEy5QbANBoRrTyOKTRZ9MAgOZqn332ToVV71f79xtvuD699957Vf4tBm4GABCH1I9kBABUY6uttlI2AECT2KqVxyG6aQAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAtA4r3s33UU+TJ09O/fr1S127dk0DBw5MM2bMaNBiAAAa0fIlaz+aY7wxZUrqt932qetGG6eBu++eZsx4pMb1H3rooSwuifhk2223Tdddd11qbJIRAJCTqVOnpjFjxqQJEyakOXPmpGHDhqURI0akuXPn+g4AgAYx9ec/T2PGnZkmjD83zZn1ZBq2115pxKc/XW288corr6SRI0dmcUnEJ+edd14644wz0l133ZUak2QEAOTkyiuvTKNGjUonnXRS6t+/f5o0aVLq06dPmjJliu8AAGgQV/5gUhp14gnppFGj/htvXHnlf+ON666vcv1oBbHNNttkcUmsH3HKiSeemL73ve+lxiQZAQA5WLlyZZo9e3YaPnx4peXxfObMmb4DAKBh4o2nnkrDDzyw0vLhBx6QZj72WJWveeyxx9aKTw466KA0a9as9P7776fGIhkBADlYtGhRWr16derdu3el5fF8wYIFvgMAoOHijV69Ki3v3at3WvDGG1W+JuKQquKTVatWZe/XWCQjACBH7dq1q/S8UCistQwAIM94o6r1q1rekCQjACAHPXv2TB06dFirFcTChQvXuhsBALBe8UZZK4iFby5cq7VE0eabb15lfNKxY8fUo0eP1FgkIwAgB507d86mzJo+fXql5fF86NChvgMAoGHijd12S9P/8IdKy6f/4Y9p6JAhVb5myJAha8UnDzzwQBo0aFDq1KlTaiwdG+2dAYBKxo0bl4455pjs4h4X/htuuCGbZmv06NFKCgBoEOPGjknHHHd8GjRwYBqyxx7phh/d+N9446tfyf4+/rwJ6fX589Ntt92WPY845Nprr83ilJNPPjkb0PKmm25Kd955Z2pMkhEAtA5dNkrN3dFHH50WL16cLr744jR//vw0YMCANG3atNS3b9+m3jQAoDY27L72stWrmlXZHX3UUf+NNy75zv/FGx9N037zm4p4Y/6C+Vlyoqhfv35ZPDJ27Nj0wx/+MG255Zbp6quvTkcccUSjbqdkBADk6JRTTskeAACN5ZSvfS17VOXWm29OqUPlVMDee++dnnrqqZQnY0YAAAAAuZKMAAAAAHIlGQEAAADkSjICAAAAyJVkBADNXqFQSG2dMgAA197WFJNIRgDQbHXq1Cn7//Lly1NbVyyDYpkAAI1D/FGzlStXZv/v0KFDWh+m9gSg2YqL3KabbpoWLlyYPd9www1Tu3btmt183rVWNo1Wbe8+RCIiyiDKYn0v/ABAPeOP6rTEuKRD/VIBa9asSW+++WZWJh07rl86QTICgGZt8803z/5fDAgya9akFql9/RskRlBULAsAoAnij+q0xLikff1jkvbt26dtttmm5gRNLUhGANCsxYVuiy22SL169Urvv//+fxe+tyy1SBtsUu/molpEAEATxx/VaYlxyQb1i0lC586ds4TE+pKMAKBFiB/jFT/I16xILVLXrk29BQBAfeOP6rTEuKRr08ckBrAEAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAADNPxkxefLk1K9fv9S1a9c0cODANGPGjBrXv/3229Muu+ySNtxww7TFFlukE044IS1evLi+2wwAICYBgLaUjJg6dWoaM2ZMmjBhQpozZ04aNmxYGjFiRJo7d26V6z/yyCPp2GOPTaNGjUrPPfdc+sUvfpGefPLJdNJJJzXE9gMAbZSYBADaUDLiyiuvzBILkUzo379/mjRpUurTp0+aMmVKles//vjj6UMf+lA644wzstYUe+21V/rqV7+aZs2aVe1nrFixIi1durTSAwBATAIAbTAZsXLlyjR79uw0fPjwSsvj+cyZM6t8zdChQ9Nrr72Wpk2blgqFQnrjjTfSL3/5y3TwwQdX+zkTJ05M3bt3r3hEsgMAQEwCAG0wGbFo0aK0evXq1Lt370rL4/mCBQuqTUbEmBFHH3106ty5c9p8883Tpptumq655ppqP2f8+PFpyZIlFY958+bVZTMBgFZOTAIAbXAAy3bt2lV6Hi0eypcVPf/881kXjQsuuCBrVXH//fenV155JY0ePbra9+/SpUvq1q1bpQcAgJgEAFqHjnVZuWfPnqlDhw5rtYJYuHDhWq0lSrtc7Lnnnunss8/Onu+8885po402yga+vOSSS7LZNQAAxCQA0HbUqWVEdLOIqTynT59eaXk8j+4YVVm+fHlq377yx0RCo9iiAgCgrsQkANDGummMGzcu3Xjjjenmm29OL7zwQho7dmw2rWex20WM9xBTeRYdcsgh6e67785m2/jHP/6RHn300azbxu6775623HLLht0bAKDNEJMAQBvpphFiIMrFixeniy++OM2fPz8NGDAgmymjb9++2d9jWSQnio4//vi0bNmydO2116YzzzwzG7xyv/32S5dffnnD7gkA0KaISQCg5WpXaAF9JZYuXZpN8RkzazTKYJbLl6QWacPuqcVR1sq7tdbtllq/lXWrKO9Gv06SX1m3xPNIcC5R3uuibudLeSvvFhCT1Gs2DQAAAID6kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAAKD5JyMmT56c+vXrl7p27ZoGDhyYZsyYUeP6K1asSBMmTEh9+/ZNXbp0SR/+8IfTzTffXN9tBgAQkwBAC9axri+YOnVqGjNmTJaQ2HPPPdP111+fRowYkZ5//vm0zTbbVPmao446Kr3xxhvppptuStttt11auHBhWrVqVUNsPwDQRolJAKDlalcoFAp1ecHgwYPTbrvtlqZMmVKxrH///umwww5LEydOXGv9+++/P33+859P//jHP9Jmm21Wr41cunRp6t69e1qyZEnq1q1banDLl6QWacPuqcVR1sq7tdbtllq/lXWrKO9Gv042U2KSZsS5RHm3xmtkULeVd2ut3xs2fUxSp24aK1euTLNnz07Dhw+vtDyez5w5s8rX3HvvvWnQoEHpiiuuSFtttVXaYYcd0llnnZXee++9Grt1xE6UPgAAxCQA0Aa7aSxatCitXr069e7du9LyeL5gwYIqXxMtIh555JFsfIl77rkne49TTjklvfXWW9WOGxEtLC666KK6bBoA0IaISQCgDQ5g2a5du0rPo6dH+bKiNWvWZH+7/fbb0+67755GjhyZrrzyynTrrbdW2zpi/PjxWbOO4mPevHn12UwAoJUTkwBAG2gZ0bNnz9ShQ4e1WkHEgJTlrSWKtthii6x7RvQbKR1jIhIYr732Wtp+++3Xek3MuBEPAAAxCQC08ZYRnTt3zqbynD59eqXl8Xzo0KFVviZm3PjXv/6V3nnnnYplf//731P79u3T1ltvXd/tBgDaMDEJALSxbhrjxo1LN954YzbewwsvvJDGjh2b5s6dm0aPHl3RxeLYY4+tWP+LX/xi6tGjRzrhhBOy6T8ffvjhdPbZZ6cTTzwxbbDBBg27NwBAmyEmAYA20k0jHH300Wnx4sXp4osvTvPnz08DBgxI06ZNS3379s3+HssiOVG08cYbZy0nTj/99GxWjUhMHHXUUemSSy5p2D0BANoUMQkAtFztCjF4QzPX6POnt8R5YVvqvMfKWnm31rrdUuu3sm4V5d3o10nyK+uWeB4JziXKe13U7Xwpb+XdAmKSes2mAQAAAFBfkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAADT/ZMTkyZNTv379UteuXdPAgQPTjBkzavW6Rx99NHXs2DHtuuuu9flYAAAxCQC0xWTE1KlT05gxY9KECRPSnDlz0rBhw9KIESPS3Llza3zdkiVL0rHHHpv233//9dleAAAxCQC0tWTElVdemUaNGpVOOumk1L9//zRp0qTUp0+fNGXKlBpf99WvfjV98YtfTEOGDFmf7QUAEJMAQFtKRqxcuTLNnj07DR8+vNLyeD5z5sxqX3fLLbekl19+OV144YW1+pwVK1akpUuXVnoAAIhJAKANJiMWLVqUVq9enXr37l1peTxfsGBBla958cUX07nnnptuv/32bLyI2pg4cWLq3r17xSNaXgAAiEkAoA0PYNmuXbtKzwuFwlrLQiQuomvGRRddlHbYYYdav//48eOzMSaKj3nz5tVnMwGAVk5MAgAtU+2aKvyfnj17pg4dOqzVCmLhwoVrtZYIy5YtS7NmzcoGujzttNOyZWvWrMmSF9FK4oEHHkj77bffWq/r0qVL9gAAEJMAQBtvGdG5c+dsKs/p06dXWh7Phw4dutb63bp1S88++2x6+umnKx6jR49OO+64Y/bvwYMHr/8eAABtjpgEANpQy4gwbty4dMwxx6RBgwZlM2PccMMN2bSekWQodrF4/fXX02233Zbat2+fBgwYUOn1vXr1Sl27dl1rOQCAmAQA2oY6JyOOPvrotHjx4nTxxRen+fPnZ0mFadOmpb59+2Z/j2WRnAAAaExiEgBoudoVYgCHZi6m9oxZNWIwy+j60eCWL0kt0obdU4ujrJV3a63bLbV+K+tWUd6Nfp0kv7JuieeR4FyivNdF3c6X8lbeLSAmqddsGgAAAAD1JRkBAAAA5EoyAgAAAMiVZAQAAACQK8kIAAAAIFeSEQAAAECuJCMAAACAXElGAAAAALmSjAAAAAByJRkBAAAA5EoyAgAAAMiVZAQAAACQK8kIAAAAIFeSEQAAAECuJCMAAACAXElGAAAAALmSjAAAAAByJRkBAAAA5EoyAgAAAMiVZAQAAACQK8kIAAAAIFeSEQAAAECuJCMAAACAXElGAAAAALmSjAAAAAByJRkBAAAA5EoyAgAAAMiVZAQAAACQK8kIAAAAIFeSEQAAAECuJCMAAACAXElGAAAAALmSjAAAAAByJRkBAAAA5EoyAgAAAMiVZAQAAACQK8kIAAAAIFeSEQAAAECuJCMAAACAXElGAAAAALmSjAAAAAByJRkBAAAA5EoyAgAAAMiVZAQAAACQK8kIAAAAIFeSEQAAAECuJCMAAACAXElGAAAAALmSjAAAAAByJRkBAAAA5EoyAgAAAMiVZAQAAACQK8kIAAAAIFeSEQAAAECuJCMAAACAXElGAAAAALmSjAAAAAByJRkBAAAA5EoyAgAAAMiVZAQAAACQK8kIAAAAIFeSEQAAAECuJCMAAACA5p+MmDx5curXr1/q2rVrGjhwYJoxY0a16959993pwAMPTB/84AdTt27d0pAhQ9Lvf//79dlmAAAxCQC0pWTE1KlT05gxY9KECRPSnDlz0rBhw9KIESPS3Llzq1z/4YcfzpIR06ZNS7Nnz0777rtvOuSQQ7LXAgDUl5gEAFqudoVCoVCXFwwePDjttttuacqUKRXL+vfvnw477LA0ceLEWr3HRz/60XT00UenCy64oMq/r1ixInsULV26NPXp0yctWbIka13R4JYvSS3Sht1Ti6OslXdrrdsttX4r61ZR3nGd7N69e+NdJ5spMUkz4lyivFvjNTKo28q7tdbvDZs+JqlTy4iVK1dmrRuGDx9eaXk8nzlzZq3eY82aNWnZsmVps802q3adSGrEDhQfkYgAABCTAEDrUKdkxKJFi9Lq1atT7969Ky2P5wsWLKjVe3z/+99P7777bjrqqKOqXWf8+PFZJqX4mDdvXl02EwBo5cQkANCydazPi9q1a1fpefT0KF9WlTvvvDN961vfSr/+9a9Tr169ql2vS5cu2QMAQEwCAG08GdGzZ8/UoUOHtVpBLFy4cK3WElUNMjVq1Kj0i1/8Ih1wwAH121oAADEJALStbhqdO3fOpvKcPn16peXxfOjQoTW2iDj++OPTHXfckQ4++OD6by0AgJgEANpeN41x48alY445Jg0aNCgNGTIk3XDDDdm0nqNHj64Y7+H1119Pt912W0Ui4thjj01XXXVV2mOPPSpaVWywwQbZ4JQAAPUhJgGANpSMiCk5Fy9enC6++OI0f/78NGDAgDRt2rTUt2/f7O+xLJITRddff31atWpVOvXUU7NH0XHHHZduvfXWhtoPAKCNEZMAQMvVrhCjTzZzjT5/ekucF7alznusrJV3a63bLbV+K+tWUd6Nfp0kv7JuieeR4FyivNdF3c6X8lbeLSAmqdOYEQAAAADrSzICAAAAyJVkBAAAAJAryQgAAAAgV5IRAAAAQK4kIwAAAIBcSUYAAAAAuZKMAAAAAHIlGQEAAADkSjICAAAAyJVkBAAAAJAryQgAAAAgV5IRAAAAQK4kIwAAAIBcSUYAAAAAuZKMAAAAAHIlGQEAAADkSjICAAAAyJVkBAAAAJAryQgAAAAgV5IRAAAAQK4kIwAAAIBcSUYAAAAAuZKMAAAAAHIlGQEAAADkSjICAAAAyJVkBAAAAJAryQgAAAAgV5IRAAAAQK4kIwAAAIBcSUYAAAAAuZKMAAAAAHIlGQEAAADkSjICAAAAyJVkBAAAAJAryQgAAAAgV5IRAAAAQK4kIwAAAIBcSUYAAAAAuZKMAAAAAHIlGQEAAADkSjICAAAAyJVkBAAAAJAryQgAAAAgV5IRAAAAQK4kIwAAAIBcSUYAAAAAuZKMAAAAAHIlGQEAAADkSjICAAAAyJVkBAAAAJAryQgAAAAgV5IRAAAAQK4kIwAAAIBcSUYAAAAAuZKMAAAAAHIlGQEAAADkSjICAAAAyJVkBAAAAJAryQgAAAAgV5IRAAAAQK4kIwAAAIDmn4yYPHly6tevX+ratWsaOHBgmjFjRo3rP/TQQ9l6sf62226brrvuuvpuLwCAmAQA2loyYurUqWnMmDFpwoQJac6cOWnYsGFpxIgRae7cuVWu/8orr6SRI0dm68X65513XjrjjDPSXXfd1RDbDwC0UWISAGi52hUKhUJdXjB48OC02267pSlTplQs69+/fzrssMPSxIkT11r/G9/4Rrr33nvTCy+8ULFs9OjR6S9/+Ut67LHHavWZS5cuTd27d09LlixJ3bp1Sw1u+ZLUIm3YPbU4ylp5t9a63VLrt7JuFeXd6NfJZkpM0ow4lyjv1niNDOq28m6t9XvDpo9JOtbljVeuXJlmz56dzj333ErLhw8fnmbOnFnlayLhEH8vddBBB6Wbbropvf/++6lTp05rvWbFihXZoyh2pLhjjWJ5I71vY1vVLrU4ylp5t9a63VLrt7JuFeVdvD7W8f5CiyYmaWacS5R3a7xGBnVbebfW+r2q6WOSOiUjFi1alFavXp169+5daXk8X7BgQZWvieVVrb9q1ars/bbYYou1XhMtLC666KK1lvfp06cumwsAbcqyZcuyuxFtgZgEAFp2TFKnZERRu3aVsyiR9Shftq71q1peNH78+DRu3LiK52vWrElvvfVW6tGjR42f09xEVigSKPPmzWtTzWabgrJW3q2Vuq28ayOuq3HR33LLLVNbIyapHeeSfClvZd1aqdvKuyFjkjolI3r27Jk6dOiwViuIhQsXrtX6oWjzzTevcv2OHTtmyYWqdOnSJXuU2nTTTVNLFYkIyQhl3Rqp28q6tWqJdbuttIgoEpO0nbrdkilvZd1aqdvKuyFikjrNptG5c+dsis7p06dXWh7Phw4dWuVrhgwZstb6DzzwQBo0aFCV40UAAIhJAKB1q/PUntF94sYbb0w333xzNkPG2LFjs2k9Y4aMYheLY489tmL9WP7qq69mr4v143UxeOVZZ53VsHsCALQpYhIAaLnqPGbE0UcfnRYvXpwuvvjiNH/+/DRgwIA0bdq01Ldv3+zvsSySE0X9+vXL/h5Jix/+8IdZ35Grr746HXHEEam1i64mF1544VpdTlDWLZ26raxbK3W7ZRGT1J66nS/lraxbK3VbeTekdoW2NA8YAAAA0PK6aQAAAACsD8kIAAAAIFeSEQAAAECuJCMAAACAXElGNJLJkydnM4l07do1DRw4MM2YMaOxPqpNe/jhh9MhhxySzdLSrl279Ktf/aqpN6nVmjhxYvrEJz6RNtlkk9SrV6902GGHpb/97W9NvVmt1pQpU9LOO++cunXrlj2GDBmSfve73zX1ZrWZuh7nkzFjxjT1pkCDEZfkQ1ySH3FJvsQlTWdiK45LJCMawdSpU7PKMmHChDRnzpw0bNiwNGLEiEpTntIw3n333bTLLruka6+9VpE2soceeiideuqp6fHHH0/Tp09Pq1atSsOHD8++Axre1ltvnS677LI0a9as7LHffvulQw89ND333HOKuxE9+eST6YYbbsgSQdBaiEvyIy7Jj7gkX+KSpvFkK49LTO3ZCAYPHpx22223LINY1L9//+xOcmS2aByRMbznnnuycqbxvfnmm1kLiQgGPvnJTyryHGy22Wbpu9/9bho1apTybgTvvPNOdu6OO8iXXHJJ2nXXXdOkSZOUNS2euKRpiEvyJS7Jn7ikcb3TBuISLSMa2MqVK9Ps2bOzO8al4vnMmTMb+uOgySxZsqTiQkTjWr16dfrZz36W3XGL7ho0jmj5c/DBB6cDDjhAEdNqiEtoK8Ql+RGX5OPUNhCXdGzqDWhtFi1alB2gvXv3rrQ8ni9YsKDJtgsaUqFQSOPGjUt77bVXGjBggMJtJM8++2yWfPjPf/6TNt5446zlz0477aS8G0Eke5566qmsOSS0JuIS2gJxST7EJfn5WRuJSyQjGrFpXvlJsnwZtFSnnXZaeuaZZ9IjjzzS1JvSqu24447p6aefTm+//Xa666670nHHHZd1i5GQaFjz5s1LX//619MDDzyQDToMrZG4hNZMXJIPcUk+5rWhuEQyooH17NkzdejQYa1WEAsXLlyrtQS0RKeffnq69957sxHDYzAjGk/nzp3Tdtttl/170KBBWXb8qquuStdff71ib0DRtS7O0THzUVG0cIs6HoPjrlixIjuvQ0skLqG1E5fkR1ySj9ltKC4xZkQjHKRRcWK2gVLxfOjQoQ39cZCbaN0Tdx7uvvvu9Kc//Smbupb8v4O4ANGw9t9//6zpabRCKT4i+fOlL30p+3drueDTNolLaK3EJU1PXNI49m9DcYmWEY0g+tIfc8wxWaWJ/t4xHUtM6zl69OjG+LjU1keZfemllyqev/LKK9lBGoMqbrPNNk26ba1xEJ077rgj/frXv06bbLJJReuf7t27pw022KCpN6/VOe+887Ipgfv06ZOWLVuW9R188MEH0/3339/Um9bqRH0uH/tko402Sj169DAmCq2CuCQ/4pL8iEvyJS7JzyZtKC6RjGgERx99dFq8eHG6+OKL0/z587NKM23atNS3b9/G+Lg2bdasWWnfffetFHCF6Ft/6623NuGWtT7FqWr32WefSstvueWWdPzxxzfRVrVeb7zxRpbUjHNIJHxifulIRBx44IFNvWlACyMuyY+4JD/iknyJS2gM7QrRvgYAAAAgJ8aMAAAAAHIlGQEAAADkSjICAAAAyJVkBAAAAJAryQgAAAAgV5IRAAAAQK4kIwAAAIBcSUYAAAAAuZKMAOrt+OOPT4cddth6leCDDz6Y2rVrl95++23fBAAgJoE2omNTbwDQcl111VWpUCg09WYAAG2cmARaHskIoM5Wr16dtWbo3r270gMAmoyYBFou3TSgDdhnn33Saaedlj023XTT1KNHj3T++edXtGpYuXJlOuecc9JWW22VNtpoozR48OCs+0TRrbfemr3uvvvuSzvttFPq0qVLevXVV9fqprFixYp0xhlnpF69eqWuXbumvfbaKz355JOVtmXatGlphx12SBtssEHad9990z//+c8cSwIAaEpiEqBIMgLaiB//+MepY8eO6YknnkhXX311+sEPfpBuvPHG7G8nnHBCevTRR9PPfvaz9Mwzz6QjjzwyfepTn0ovvvhixeuXL1+eJk6cmL3mueeeyxIO5SKhcdddd2Wf9dRTT6XtttsuHXTQQemtt97K/j5v3rx0+OGHp5EjR6ann346nXTSSencc8/NsRQAgKYmJgEyBaDV23vvvQv9+/cvrFmzpmLZN77xjWzZSy+9VGjXrl3h9ddfr/Sa/fffvzB+/Pjs37fccks0oSg8/fTTldY57rjjCoceemj273feeafQqVOnwu23317x95UrVxa23HLLwhVXXJE9j/erajvivf/973830t4DAM2FmAQoMmYEtBF77LFHNs5D0ZAhQ9L3v//9NGvWrKy7RnSdKBVdLqI7R1Hnzp3TzjvvXO37v/zyy+n9999Pe+65Z8WyTp06pd133z298MIL2fP4f1XbAQC0HWISIEhGAKlDhw5p9uzZ2f9LbbzxxhX/jjEeSpMI5YrjT5SvE8uLy8y8AQDUREwCbYcxI6CNePzxx9d6vv3226ePf/zj2UjUCxcuzMZ4KH1svvnmtX7/WD9aTzzyyCMVy6KlRLS86N+/f/Y8Br+sajsAgLZDTAIEyQhoI2LwyHHjxqW//e1v6c4770zXXHNN+vrXv551z/jSl76Ujj322HT33XenV155JZsB4/LLL89mvqitmIXja1/7Wjr77LPT/fffn55//vl08sknZwNfjho1Kltn9OjRWXeO4nbccccd2UwdAEDbISYBgm4a0EZEsuG9997LxnCIJpCnn356+spXvpL97ZZbbkmXXHJJOvPMM9Prr7+ejRURYznErBd1cdlll6U1a9akY445Ji1btiwNGjQo/f73v08f+MAHsr9vs8022WwbY8eOTZMnT8625dJLL00nnnhio+wzAND8iEmA0C5GsVQU0LrFnN677rprmjRpUlNvCgDQholJgCLdNAAAAIBcSUYAAAAAudJNAwAAAMiVlhEAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACDl6f8BZsk+HkN+pywAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(1, 2, figsize=(13, 5))\n", + "df.groupby(\"period\").exp_red.value_counts(normalize=True).unstack().plot(\n", + " stacked=True,\n", + " kind=\"bar\",\n", + " rot=0,\n", + " title=\"Experience Red - Discrete Experience Stocks\",\n", + " ax=ax[0],\n", + " cmap=\"Reds\",\n", + ")\n", + "\n", + "df_cont_exp.groupby(\"period\").exp_red.value_counts(normalize=True).unstack().plot(\n", + " stacked=True,\n", + " kind=\"bar\",\n", + " rot=0,\n", + " title=\"Experience Red - Continuous Experience Stocks\",\n", + " ax=ax[1],\n", + " cmap=\"Reds\",\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "744cfdf8", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "strenuousjobs", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/background/two_occupations.ipynb b/docs/source/background/two_occupations.ipynb new file mode 100644 index 00000000..f52c52d8 --- /dev/null +++ b/docs/source/background/two_occupations.ipynb @@ -0,0 +1,553 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 11, + "id": "71137156", + "metadata": {}, + "outputs": [], + "source": [ + "import jax\n", + "import jax.numpy as jnp\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import dcegm\n", + "\n", + "jax.config.update(\"jax_enable_x64\", True)" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "252300ef", + "metadata": {}, + "outputs": [], + "source": [ + "params = {}\n", + "params[\"interest_rate\"] = 0.02\n", + "params[\"max_wealth\"] = 50\n", + "params[\"wage_constant\"] = 3\n", + "params[\"wage_exp_green\"] = 0.5\n", + "params[\"wage_exp_red\"] = 0.8\n", + "params[\"income_shock_std\"] = 1\n", + "params[\"income_shock_mean\"] = 0\n", + "params[\"taste_shock_scale\"] = 1\n", + "params[\"discount_factor\"] = 0.95\n", + "params[\"rho\"] = 0.9\n", + "params[\"delta\"] = 1.5\n", + "params[\"beta_green\"] = 0.2\n", + "params[\"beta_red\"] = 0.1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b9b99382", + "metadata": {}, + "outputs": [], + "source": [ + "model_specs = {\n", + " \"choices\": [0, 1, 2],\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb4dfd65", + "metadata": {}, + "outputs": [], + "source": [ + "model_config = {\n", + " \"n_periods\": 5,\n", + " \"choices\": [0, 1, 2],\n", + " \"continuous_states\": {\n", + " \"assets_end_of_period\": jnp.linspace(0, 50, 100),\n", + " \"assets_begin_of_period\": jnp.linspace(0, 50, 100),\n", + " },\n", + " \"deterministic_states\": {\n", + " \"exp_green\": jnp.arange(0, 7, dtype=int),\n", + " \"exp_red\": jnp.arange(0, 7, dtype=int),\n", + " },\n", + " \"n_quad_points\": 5,\n", + " \"upper_envelope\": {\"method\": \"druedahl_jorgensen\"},\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "df1be5dc", + "metadata": {}, + "outputs": [], + "source": [ + "model_config_cont_exp = {\n", + " \"n_periods\": 5,\n", + " \"choices\": [0, 1, 2],\n", + " \"continuous_states\": {\n", + " \"assets_end_of_period\": jnp.linspace(0, 50, 100),\n", + " \"assets_begin_of_period\": jnp.linspace(0, 50, 100),\n", + " \"exp_green\": jnp.arange(0, 7, dtype=float),\n", + " \"exp_red\": jnp.arange(0, 7, dtype=float),\n", + " },\n", + " \"n_quad_points\": 5,\n", + " \"upper_envelope\": {\"method\": \"druedahl_jorgensen\"},\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "b8defe75", + "metadata": {}, + "outputs": [], + "source": [ + "# Utility functions\n", + "def flow_util(consumption, choice, params):\n", + " rho = params[\"rho\"]\n", + " beta_green = params[\"beta_green\"]\n", + " beta_red = params[\"beta_red\"]\n", + " disutility = beta_red * (choice == 0) + beta_green * (choice == 1)\n", + " u = consumption ** (1 - rho) / (1 - rho) - disutility\n", + " return u\n", + "\n", + "\n", + "def marginal_utility(consumption, params):\n", + " rho = params[\"rho\"]\n", + " u_prime = consumption ** (-rho)\n", + " return u_prime\n", + "\n", + "\n", + "def inverse_marginal_utility(marginal_utility, params):\n", + " rho = params[\"rho\"]\n", + " return marginal_utility ** (-1 / rho)\n", + "\n", + "\n", + "utility_functions = {\n", + " \"utility\": flow_util,\n", + " \"inverse_marginal_utility\": inverse_marginal_utility,\n", + " \"marginal_utility\": marginal_utility,\n", + "}\n", + "\n", + "\n", + "def final_period_utility(wealth: float, choice: int, params):\n", + " return flow_util(wealth, choice, params)\n", + "\n", + "\n", + "def marginal_final(wealth, choice, params):\n", + " return marginal_utility(wealth, params)\n", + "\n", + "\n", + "utility_functions_final_period = {\n", + " \"utility\": final_period_utility,\n", + " \"marginal_utility\": marginal_final,\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "760035c7", + "metadata": {}, + "outputs": [], + "source": [ + "def state_specific_choice_set(\n", + " period,\n", + " lagged_choice,\n", + " model_specs,\n", + "):\n", + " # Once the agent choses retirement, she can only choose retirement thereafter.\n", + " # Hence, retirement is an absorbing state.\n", + " if lagged_choice == 2:\n", + " choice_set = [2]\n", + " elif period == 4:\n", + " choice_set = [2]\n", + " else:\n", + " choice_set = model_specs[\"choices\"]\n", + "\n", + " return choice_set\n", + "\n", + "\n", + "def next_period_deterministic_state_cont(\n", + " period,\n", + " choice,\n", + " lagged_choice,\n", + "):\n", + " return {\n", + " \"period\": period + 1,\n", + " \"lagged_choice\": choice,\n", + " }\n", + "\n", + "\n", + "def next_period_continuous_state(\n", + " lagged_choice,\n", + " period,\n", + " exp_green,\n", + " exp_red,\n", + "):\n", + " return {\n", + " \"exp_red\": exp_red + (lagged_choice == 0),\n", + " \"exp_green\": exp_green + (lagged_choice == 1),\n", + " }\n", + "\n", + "\n", + "state_space_functions_cont_exp = {\n", + " \"state_specific_choice_set\": state_specific_choice_set,\n", + " \"next_period_deterministic_state\": next_period_deterministic_state_cont,\n", + " \"next_period_continuous_state\": next_period_continuous_state,\n", + "}\n", + "\n", + "\n", + "def next_period_deterministic_state(\n", + " period,\n", + " choice,\n", + " lagged_choice,\n", + " exp_green,\n", + " exp_red,\n", + "):\n", + " next_exp_green = exp_green + (lagged_choice == 1)\n", + " next_exp_red = exp_red + (lagged_choice == 0)\n", + " return {\n", + " \"period\": period + 1,\n", + " \"exp_green\": next_exp_green,\n", + " \"exp_red\": next_exp_red,\n", + " \"lagged_choice\": choice,\n", + " }\n", + "\n", + "\n", + "def sparsity_condition(\n", + " period,\n", + " lagged_choice,\n", + " exp_green,\n", + " exp_red,\n", + "):\n", + " if (exp_green + exp_red) > period:\n", + " return False\n", + " else:\n", + " return True\n", + "\n", + "\n", + "state_space_functions_discrete_exp = {\n", + " \"state_specific_choice_set\": state_specific_choice_set,\n", + " \"next_period_deterministic_state\": next_period_deterministic_state,\n", + " \"sparsity_condition\": sparsity_condition,\n", + "}\n", + "\n", + "\n", + "def budget_constraint_discrete_exp(\n", + " lagged_choice,\n", + " exp_green,\n", + " exp_red,\n", + " asset_end_of_previous_period,\n", + " income_shock_previous_period,\n", + " params,\n", + "):\n", + " interest_factor = 1 + params[\"interest_rate\"]\n", + " wage = (\n", + " params[\"wage_constant\"]\n", + " + params[\"wage_exp_green\"] * exp_green * (lagged_choice == 1)\n", + " + params[\"wage_exp_red\"] * exp_red * (lagged_choice == 0)\n", + " )\n", + " resource = (\n", + " interest_factor * asset_end_of_previous_period\n", + " + (wage + income_shock_previous_period) * (lagged_choice != 2)\n", + " + (wage + income_shock_previous_period) * 0.5 * (lagged_choice == 2)\n", + " )\n", + " return jnp.maximum(resource, 0.5)\n", + "\n", + "\n", + "def budget_constraint_cont_exp(\n", + " period,\n", + " lagged_choice,\n", + " exp_green,\n", + " exp_red,\n", + " asset_end_of_previous_period,\n", + " income_shock_previous_period,\n", + " params,\n", + "):\n", + " interest_factor = 1 + params[\"interest_rate\"]\n", + " wage = (\n", + " params[\"wage_constant\"]\n", + " + params[\"wage_exp_green\"] * exp_green * (lagged_choice == 1)\n", + " + params[\"wage_exp_red\"] * exp_red * (lagged_choice == 0)\n", + " )\n", + " resource = (\n", + " interest_factor * asset_end_of_previous_period\n", + " + (wage + income_shock_previous_period) * (lagged_choice != 2)\n", + " + (wage + income_shock_previous_period) * 0.5 * (lagged_choice == 2)\n", + " )\n", + " return jnp.maximum(resource, 0.5)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "c48af7cc", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Starting state space creation\n", + "State space created.\n", + "\n", + "Starting state-choice space creation and child state mapping.\n", + "State, state-choice and child state mapping created.\n", + "\n", + "Start creating batches for the model.\n", + "The batch size of the backwards induction is 42\n", + "The batch size of the backwards induction is 41\n", + "The batch size of the backwards induction is 40\n", + "The batch size of the backwards induction is 39\n", + "The batch size of the backwards induction is 38\n", + "The batch size of the backwards induction is 37\n", + "The batch size of the backwards induction is 36\n", + "The batch size of the backwards induction is 35\n", + "The batch size of the backwards induction is 34\n", + "The batch size of the backwards induction is 33\n", + "The batch size of the backwards induction is 32\n", + "The batch size of the backwards induction is 31\n", + "The batch size of the backwards induction is 30\n", + "The batch size of the backwards induction is 29\n", + "The batch size of the backwards induction is 28\n", + "The batch size of the backwards induction is 27\n", + "The batch size of the backwards induction is 26\n", + "The batch size of the backwards induction is 25\n", + "The batch size of the backwards induction is 24\n", + "The batch size of the backwards induction is 23\n", + "The batch size of the backwards induction is 22\n", + "The batch size of the backwards induction is 21\n", + "Model setup complete.\n", + "\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/opt/homebrew/Cellar/micromamba/2.5.0_2/envs/strenuousjobs/lib/python3.13/site-packages/dcegm/pre_processing/model_structure/state_choice_space.py:295: UserWarning: \n", + "\n", + "\n", + "\n", + " Some states are not child states of any state-choice combination or stochastic transition. Please revisit the sparsity condition. \n", + " \n", + "An example of a state that is not a child state is: \n", + " \n", + "{'period': np.uint8(1), 'lagged_choice': np.uint8(0), 'exp_green': np.uint8(0), 'exp_red': np.uint8(0), 'dummy_stochastic': np.uint8(0)} \n", + " \n", + "\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "model = dcegm.setup_model(\n", + " model_config=model_config,\n", + " model_specs=model_specs,\n", + " utility_functions=utility_functions,\n", + " utility_functions_final_period=utility_functions_final_period,\n", + " state_space_functions=state_space_functions_discrete_exp,\n", + " stochastic_states_transitions={},\n", + " budget_constraint=budget_constraint_discrete_exp,\n", + ")\n", + "\n", + "solved_model = model.solve(params)" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "87e7255d", + "metadata": {}, + "outputs": [], + "source": [ + "n_agents = 100\n", + "states_initial = {\n", + " \"n_agents\": n_agents,\n", + " \"assets_begin_of_period\": jnp.ones(n_agents),\n", + " \"exp_green\": jnp.zeros(n_agents),\n", + " \"exp_red\": jnp.zeros(n_agents),\n", + " \"lagged_choice\": jnp.zeros(n_agents),\n", + " \"period\": jnp.zeros(n_agents, dtype=int),\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a0f03fd2", + "metadata": {}, + "outputs": [], + "source": [ + "simulate = model.get_solve_and_simulate_func(states_initial=states_initial, seed=99)\n", + "df = simulate(params)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "fa58eb96", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Sparsity condition not provided. Assume all states are valid.\n", + "Starting state space creation\n", + "State space created.\n", + "\n", + "Starting state-choice space creation and child state mapping.\n", + "State, state-choice and child state mapping created.\n", + "\n", + "Start creating batches for the model.\n", + "The batch size of the backwards induction is 7\n", + "Model setup complete.\n", + "\n" + ] + } + ], + "source": [ + "model_cont_exp = dcegm.setup_model(\n", + " model_config=model_config_cont_exp,\n", + " model_specs=model_specs,\n", + " utility_functions=utility_functions,\n", + " utility_functions_final_period=utility_functions_final_period,\n", + " state_space_functions=state_space_functions_cont_exp,\n", + " stochastic_states_transitions={},\n", + " budget_constraint=budget_constraint_cont_exp,\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a6c40ad3", + "metadata": {}, + "outputs": [], + "source": [ + "# n_agents = 100\n", + "# states_initial = {\n", + "# \"n_agents\": n_agents,\n", + "# \"assets_begin_of_period\": jnp.ones(n_agents),\n", + "# \"exp_green\": jnp.zeros(n_agents),\n", + "# \"exp_red\": jnp.zeros(n_agents),\n", + "# \"lagged_choice\": jnp.zeros(n_agents),\n", + "# \"period\": jnp.zeros(n_agents, dtype=int),\n", + "# }\n", + "\n", + "simulate_cont_exp = model_cont_exp.get_solve_and_simulate_func(\n", + " states_initial=states_initial, seed=99\n", + ")\n", + "\n", + "df_cont_exp = simulate_cont_exp(params)" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "1c823807", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABCMAAAHUCAYAAAAA4OLOAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQAASetJREFUeJzt3Qm8VWW5MPD3ADI4YYriBEgOhZGa4ACKU4mh10+bpOw6wk0cQxxSaVDyC4fyoimk14EsB245VqTSoKBoV1HS1ExNBQ1EMAUcQGB/v2fdb5/2OWcfOAfOWWfY//+vHe511t57rXdNz3rWO1QVCoVCAgAAAMhJh7x+CAAAACBIRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRtTj6aefTieccELq27dv6tq1a9pwww3T7rvvni677LL09ttvV8+33XbbpX/7t39rsg3y6quvpqqqqjR58uSUpwMOOCD73eKrW7duadddd00TJkxIq1atarLfifWK73/iiSea7DsvvPDC7DsXLlzYoPWMV6n4bHxH0YMPPphNi3+Lpk6dWmOeUrEPHH/88am1KpZP8bX++uunbbfdNh1yyCHpxz/+cVqyZEmdz8T6xHq1Rs8991y2TnGsNKXisVffq77t35q0leVck/feey9deuml2Tlo4403ThtttFHafvvt01FHHZUeeuihZt8X1uUcAw0hxhBjtJcYo2jGjBnZOXqbbbZJnTt3Tt27d0+DBw9OkyZNys7pzWV114HWHMs0h2L8Wt8r73uLtnIP1BwWLVqUzj///LTzzjunDTbYIDsePvnJT6ZjjjkmO/8XzZw5M9t/33nnnWZdnjgW4l62NerU0gvQGv3Xf/1XOuWUU9InPvGJdM4552Q70kcffZTdQP/kJz9Jjz76aLrrrrua5be32mqr7Psj8M7bxz/+8XTLLbdk/71gwYJsXc8888w0b9687MagPZg4ceIa54mkU2yD2O6lgcI111xTNliIfSFumFq7++67LzsZLl++PP3jH/9Iv//979O5556bLr/88vSrX/0qu/Er+s53vpO++c1vptYoAo+LLrooSyo1R5Bx+umnp6OPPrrO9EjgtHax37aF5VydlStXpqFDh6ZnnnkmO//uueee2fQXX3wx208j4N1///1z2RegOYgxxBjtLcb43ve+l8aNG5clH77//e9nMez7779ffaP1t7/9Lf3nf/5ns/z26q4DrTmWaU4/+MEP0oEHHlhnekvcW7SVe6CmtHTp0rT33ntn/0YcE/H1Bx98kB0Hd955Z5o9e3baZZddsnlnzpyZ7b+RLNhkk01SJZKMqCUOgpNPPjkdfPDB6e67705dunSp/ltMO+uss7KbuuYSvxc7cEuI2hClvz1s2LAsi3f11Veniy++OK233np1PlMoFNKHH36YfbYtKL341ycu+o3ZBp/5zGdSWzBgwIDUo0eP6vdf/epX02mnnZbd2P2f//N/spNkcX/P80IQN58rVqyocay1pN69e7fYMbg2So/BtrTc9Zk+fXp2cb7xxhuz2mlFUZMn9temrKkFeRNjiDHaW4zxi1/8IktEjBgxIku0xZPt0jgyHnrEft8S2vpN7dracccd21Q8UBoHtqXlXt0x8dJLL6U//OEPdZJCY8aMEcfUoplGmWxinEivu+66sjdHUfUsbtxqiwRFPFGPG4K4gY9Aura//OUv6Ygjjkgf+9jHsqYfu+22W/rpT3/aoCpKf/3rX9PXvva11LNnz2y54obp2GOPTcuWLaueZ/78+emkk07KnozGckYTk8i2xQG+NiL5EDewkd1+6623smmxbHFDELUm+vXrly1LcR0efvjh9NnPfjarUh1NASJD/pvf/Kbsd//zn//MbjQ23XTTrPrS4Ycfnv7+97/XmGfatGlZecX6RHntsMMO2frVV1V67ty56Ytf/GJ2oY8aAP/+7/9evdyra6ZRW+1mGpGtjCcWxfUvvopVAstVoVy8eHE6++yzs20Q2yKqLY4ePbpOVcU4Ye21117Z8kaZRe2UE088MeUlsrVjx45Nc+bMSVOmTFlt1caGLGtUM4uEXfwt9o0tttgiHXroodn+W7p/R3OnSHBF+cR8f/zjH7O/R+2jOL5iv4htHkHYf//3f1d/fxwXX/nKV7L/jhN8uaqHv/vd77L9MPaDWM599tknqwXSVOIJfXx3cTmK4qLTsWPH7ElM7WZc8WQrsuCxTlE2V111VZ3vbeg+s7pjsFwzjYacF4rb5Yc//GG64oorsnmiOt+gQYPSY489VmdZ//SnP2XH7GabbZatUwR8say1yylqmMQ+EMsYy1o8jtZUtbH4hKScDh06NHhfiPNw7OOxjLFPfeELX0jPP//8Wq1PbbFPx7aMYyJqkoWnnnoq297Fdd56663TYYcdll5//fU1rjeVQYzxL2KM9hFjRCIi4tq4rpUmIooiJozabkWRPI/q66XLfuqpp9appl68fq4uvl7TdaBcLFO8hv7sZz/LrktRLnGd+PWvf92gJh7FZnulGrpO9TWlrL2NI+4ubt/i9WvgwIHptttuS00h4vU4/uI3yjWlvuGGG+qU17XXXpt22mmn7NoWD/Zuv/32Ot/bmHijXBxY3z1QQ+KJYuweZRRxbVx/I1b73Oc+l1544YU6yxr7VcSKxWMjvnP8+PE15llTTLqucUzsC+ecc07231EOxf23eP8RD1+inGK/L8bUce9XLqZoyPrU9sgjj2QPKeM4K547IpaN+6SIh+KYi/vNL33pS9k+2WwKVFuxYkVh/fXXL+y1114NLpU+ffoUtt1228LOO+9cuPnmmwv3339/4Stf+Uohivahhx6qnu+vf/1rYaONNipsv/322Xy/+c1vCl/72tey+S699NLq+V555ZVs2k033VQ9bfbs2YUNN9ywsN122xV+8pOfFH7/+98Xfv7znxeOOuqowuLFi7N55s2bV+jVq1e2PNdee23hd7/7XeH73/9+oUuXLoXjjz9+jeux//77Fz71qU/Vmb777rsXOnXqVHj//fez97Fs22yzTWGXXXYp3HrrrYU//OEPhb/85S+FBx98sLDeeusVBgwYUJgyZUrh7rvvLgwdOrRQVVVVuP3226u/L9YrviOW9cQTTyz89re/LVx33XWFLbbYIpv2z3/+s3reSZMmFcaPH1+49957s7L86U9/Wth1110Ln/jEJwrLly+vnu973/te9p2x7uecc062Da644orCBhtsUPjMZz5TY95Yz3iVis/GdxT98Y9/zKbFv+Gll14qfPnLX86mPfroo9WvDz/8sHofOO6446o//9577xV22223Qo8ePbLliG1x5ZVXFrp371446KCDCqtWrcrmmzlzZlY+X/3qVwtTp07NyjLK55hjjik0pWL5vPXWW2X/Hvtm/H3EiBHV02J9Yr2KGrKssS/GPhTlPm7cuGw73HHHHYVvfvOb2fyl+3fsQwceeGDhl7/8ZeGBBx7Ipsc8nTt3LgwZMiTbh+67775s3y09HhYsWFD4wQ9+kE275pprqrdFTA8/+9nPsuU88sgjC3feeWfh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qlXphqtuSMP2GJY+s81n0ud2+1y69oprq/8+97W56YQjT0gDew9MXzzgi2n247NrfH7ar6alI/Y9IvtsVH+cPHHyaqtELn53cbpwzIVpv533S7tvu3s6csiR6cEHHqz++8yZM9N+++2XDdnZq1evdMYZZ6T33nuvWcsAAGh5YpJ/kYwAoN2bcPGEdMOPb0ijzhqV7nn4nnTZTy5Lm22+WfXfr/rBVen4U45Pv/zjL9N2H98unXvSuWnFihXZ357987PprJFnpWFfGJbumn5XOuWcU9LVl1yd7r7t7nqDjJO/enKW0Lhk4iXZ743+zujUscP/jsP9zDPPpEMOOSR98YtfTE8//XSaMmVKevjhh9Npp52WU2kAAC1FTPIvmmkA0K69t/S99PPrfp4uGH9BOuKrR2TTevftnXbfe/ess6gQiYj9h+6f/fep3zo1qwUx55U56eM7fjzdPOnmtNd+e2WJjLDd9tull//2crrpmpvSkV87ss7vPfrQo+mZJ59J9868N5s39NquV/XfL7/88nT00Uen0aNHZ+933HHHdNVVV6X9998/TZo0KXXt2jWHUgEA8iYmqUnNCADatb//7e9p+bLlae/99q53np0+tVP1f/fo2SP79+233q7+/Gf2/EyN+eP9a39/La1cubLOd73wlxdSz617Viciaps1a1aaPHly2nDDDatfUVMialS88sora72eAEDrJiapSc0IANq1Ll27rHGe9TqtV/3fVVVV2b+rCquyfwuFQvW0opi2tr8XSYeTTjop6yeitt69e69xWQGAtklMUpNkBADtWp+P90ldu3VNj01/LH35mC83+vPbf2L79OSfnqwxLfqDiJoPHTv+bz8QpXbaeaf05j/eTK++/GrZ2hG77757evbZZ9MOO+zQ6GUBANouMUlNmmkA0O6fQpx4+onpinFXpHum3JP1BfHnJ/6c7vj5HQ36/HEnH5f+NP1P6Sc/+kmWYLjn9nvSbTfclvUzUc4e++yRBgwakM484cw088GZ6fXXXk8zfjcjPfz7h7O/f+tb30qPPvpoOvXUU9Ps2bPTiy++mO699950+umnN+l6AwCti5ikJjUjAGj3ovPJqMVwzaXXpAXzF6TNe26ejjruqAZ9duddd04/uv5H6epLr84SEvHZ6OSyXOeVRRNumpAu/97l2agcH7z/QdZh5uhv/2+Hlbvsskt66KGH0tixY9OQIUOyJh/bb799Gj58eJOtLwDQOolJ/kUyAoB2r0OHDumkMSdlr9r+8tZfarzfuPvGdaYdfPjB2as+Dzz5QI333T/WPV181cX1zr/HHnukBx6o+RkAoP0Tk/yLZhoAAABAriQjAAAAgFxJRgAAAAC5kowAAAAAciUZAQAAAORKMgIAAADIlWQEAAAAkCvJCAAAACBXkhEAAABArjrl+3MAkI/DfvhqbkX9m7O3y+23AIC2Jc+YpC3FJWpGAEALmThxYurbt2/q2rVrGjBgQJoxY4ZtAQBURFwiGQEALWDKlClp9OjRaezYsempp55KQ4YMScOGDUtz5syxPQCAdh+XSEYAQAu44oor0ogRI9LIkSNTv3790oQJE1KvXr3SpEmTbA8AoN3HJZIRAJCz5cuXp1mzZqWhQ4fWmB7vZ86caXsAAO0+LpGMAICcLVy4MK1cuTL17NmzxvR4P3/+fNsDAGj3cYlkBAC0kKqqqhrvC4VCnWkAAO0xLpGMAICc9ejRI3Xs2LHO04YFCxbUeSoBANAe4xLJCADIWefOnbMhs6ZNm1ZjerwfPHiw7QEAtPu4pFOzfTMAUK8xY8akY445Jg0cODANGjQoXXfdddnwWaNGjVJqAEC7j0skIwBol35z9napNRs+fHhatGhRGjduXJo3b17q379/mjp1aurTp09LLxoAUEExSUvFJZIRANBCTjnllOwFAFBpcYk+IwAAAIBcSUYAAAAAuZKMAAAAAHIlGQEAAADkSjICAAAAyJVkBAAAAJAryQgAAAAgV5IRAAAAQK4kIwAAAIBcdcr35wAgH5+6enBuRf3saTNz+y0AoG3JMyZpS3GJmhEA0AKmT5+eDj/88LT11lunqqqqdPfdd9sOAEDFxCWSEQDQAt5777206667pquvvlr5AwAVF5dopgEALWDYsGHZCwCgEuMSNSMAAACAXElGAAAAALmSjAAAAAByJRkBAAAA5EoyAgAAAMiV0TQAoAUsXbo0vfTSS9XvX3nllTR79uy06aabpt69e9smAEC7jkskIwBol549bWZqzZ544ol04IEHVr8fM2ZM9u9xxx2XJk+e3IJLBgBUUkzSUnGJZAQAtIADDjggFQoFZQ8AVGRcos8IAAAAIFeSEQAAAECuJCMAAACAXElGAAAAALmSjAAAAAByJRkBAAAAtP5kxMSJE1Pfvn1T165d04ABA9KMGTNWO/8tt9ySdt1117T++uunrbbaKp1wwglp0aJFa7vMAABiEgCopGTElClT0ujRo9PYsWPTU089lYYMGZKGDRuW5syZU3b+hx9+OB177LFpxIgR6dlnn02/+MUv0uOPP55GjhzZFMsPAFQoMQkAVFAy4oorrsgSC5FM6NevX5owYULq1atXmjRpUtn5H3vssbTddtulM844I6tNse+++6aTTjopPfHEE02x/ABAhRKTAEDb1akxMy9fvjzNmjUrnXfeeTWmDx06NM2cObPsZwYPHpzVopg6dWpWg2LBggXpl7/8ZTrssMPq/Z1ly5Zlr6LFixc3ZjEBIH31N1/NrRRuP+x2JZ4zMQkAbUWeMUlbiksaVTNi4cKFaeXKlalnz541psf7+fPn15uMiD4jhg8fnjp37py23HLLtMkmm6Qf//jH9f7O+PHjU/fu3atfUfMCANqLuM7tscceaaONNkpbbLFFOvLII9MLL7zQ0ovVpohJAKBtxyVr1YFlVVVVjfeFQqHOtKLnnnsua6Lx3e9+N6tVcd9996VXXnkljRo1qt7vP//889O7775b/Zo7d+7aLCYAtEoPPfRQOvXUU7OmjNOmTUsrVqzIahm+9957Lb1obY6YBADaZlzSqGYaPXr0SB07dqxTCyKaXtSuLVGaZdlnn33SOeeck73fZZdd0gYbbJB1fHnxxRdno2vU1qVLl+wFAO1RJOZL3XTTTdmTiEja77fffi22XG2JmAQA2nZc0qiaEdHMIobyjGxJqXgfzTHKef/991OHDjV/JhIaxRoVAFDpohZg2HTTTVt6UdoMMQkAtO24pNHNNMaMGZOuv/76dOONN6bnn38+nXnmmdmwnsVmF9HEIobyLDr88MPTnXfemY228fe//z098sgjWbONPffcM2299dZNuzYA0MZEYj6urTHaVP/+/Vt6cdoUMQkAtN24pFHNNEJ0RLlo0aI0bty4NG/evGwBY6SMPn36ZH+PaZGcKDr++OPTkiVL0tVXX53OOuusrPPKgw46KF166aVNuyYA0Aaddtpp6emnn04PP/xwSy9KmyMmAYC2G5c0OhkRTjnllOxVzuTJk+tMO/3007MXAFDz+njvvfem6dOnp2233VbRiEkAoGLikrVKRgAA61YFMi74d911V3rwwQdT3759FScAUFFxiWQEAOQshs+69dZb0z333JON6V0cpap79+6pW7dutgcA0O7jEskIANql2w+7PbVW0alzOOCAA+oMpRV9LQEA7UdrjklaMi6RjACAnBnaGgCo9Lik0UN7AgAAAKwLyQgAAAAgV5IRAAAAQK4kIwAAAIBcSUYA0GatSqtSIRVS/K+90+klALT+uCTTzuOSQhN1eCkZAUCb9e6Kd9OKVStSYXk7v+qnlN5///3s3/XWW6+lFwUAKOP9le+nFYUVqbCyfccly5cvz/7t2LHjOn2PoT0BaLM+XPVhmr5oejqk0yHpY+ljqapzVUpVqdX68MMP1+rpQyQiFixYkDbZZJN1vvADAM1j6cql6fnFz6fu3bqnDTpu0O5ikrBq1ar01ltvpfXXXz916rRu6QTJCADatF+/9evs3/1W7Jc6deiUqlrxlb/TO2t/2Y1ExJZbbtmkywMANJ1oOnrHm3ekXt16pe4fdm+3MUmHDh1S7969U1XVuq2fZAQAbf7C/6u3fpWmLZqWunfqnjq04haI937h3rX6XDTNUCMCAFq/d1a8k77/8vfTZuttljqmju0uJgmdO3fOEhLrSjICgHbTZOPD5WtX5TAvXbt2belFAACa2crCyrRg+YJWXc5dW0FM0nofHwEAAADtkmQEAAAAkCvJCAAAACBXkhEAAABAriQjAAAAgFxJRgAAAAC5MrQnAABAO/LMK3NaehFgjdSMAAAAAHIlGQEAAADkSjICAAAAyJU+IwCaiPaZAADQMGpGAAAAALmSjAAAAAByJRkBAAAA5EoyAgAAAMiVZAQAAACQK8kIAAAAIFeSEQAAAECuJCMAAACAXElGAAAAALmSjAAAAAByJRkBAAAA5EoyAgAAAMiVZAQAAACQK8kIAAAAIFeSEQAAAECuJCMAAACAXElGAAAAALmSjAAAAAByJRkBAAAA5EoyAgAAAMiVZAQAAACQK8kIAAAAIFeSEQAAAECuJCMAAACAXElGAAAAALmSjAAAAAByJRkBAAAA5EoyAgAAAMiVZAQAAACQq075/hyQp+0+vLVNFvirLb0AAABAs1IzAgAAAMiVZAQAAACQK8kIAAAAIFeSEQAAAECuJCMAAACAXElGAAAAALmSjAAAAABy1Snfn6PSbffhrakterWlFwAAAKAdUTMCAAAAyJVkBAAAAJAryQgAAACg9fcZMXHixHT55ZenefPmpU996lNpwoQJaciQIfXOv2zZsjRu3Lj085//PM2fPz9tu+22aezYsenEE09MrYF+DACgbWpvMQkAVIpGJyOmTJmSRo8enV3899lnn3TttdemYcOGpeeeey717t277GeOOuqo9Oabb6Ybbrgh7bDDDmnBggVpxYoVTbH8AECFEpMAQAUlI6644oo0YsSINHLkyOx9PIG4//7706RJk9L48ePrzH/fffelhx56KP39739Pm266aTZtu+22a4plBwAqmJgEACqkz4jly5enWbNmpaFDh9aYHu9nzpxZ9jP33ntvGjhwYLrsssvSNttsk3baaad09tlnpw8++GC1VSgXL15c4wUAICYBgAqsGbFw4cK0cuXK1LNnzxrT4320uywnakQ8/PDDqWvXrumuu+7KvuOUU05Jb7/9drrxxhvLfiZqWFx00UWNWTQAoIKISaBt0Ucb0CSjaVRVVdV4XygU6kwrWrVqVfa3W265Je25557p0EMPzapVTp48ud7aEeeff3569913q19z585dm8UEANo5MQkAVEDNiB49eqSOHTvWqQURHVLWri1RtNVWW2XNM7p37149rV+/flkC4/XXX0877rhjnc906dIlewEAiEkAoMJrRnTu3DkNGDAgTZs2rcb0eD948OCyn4kRN/7xj3+kpUuXVk/729/+ljp06JANpwUA0FhiEgCosGYaY8aMSddff33W38Pzzz+fzjzzzDRnzpw0atSo6iYWxx57bPX8Rx99dNpss83SCSeckA3/OX369HTOOedk43l369atadcGAKgYYhIAqKChPYcPH54WLVqUxo0bl+bNm5f69++fpk6dmvr06ZP9PaZFcqJoww03zGpOnH766dmoGpGYOOqoo9LFF1/ctGsCAFQUMQkAVFAyIsRoGPEqJzqmrO2Tn/xknaYdAADrSkwCABWUjAAAgLbOcJMAbWxoTwAAAIC1JRkBAAAA5EoyAgAAAMiVPiMAAFoJfRgAUCnUjAAAAAByJRkBAAAA5EoyAgAAAMiVZAQAAACQK8kIAAAAIFeSEQAAAECuJCMAAACAXElGAAAAALmSjAAAAAByJRkBAAAA5EoyAgAAAMiVZAQAAACQK8kIAAAAIFeSEQAAAECuJCMAAACAXElGAAAAALmSjAAAAAByJRkBAAAA5EoyAgAAAMiVZAQAAACQK8kIAAAA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69X5zLJly7JX0bvvvpv9u3jx4tQcVi17P7VFzVUezUlZK+/2um+31f27rZb1yg9Wpraoucq7+L2FQiFVCjFJ69JWzyVt8bzdVstbWSvv9rpvt9W4ZHEriEkalYxYuHBhWrlyZerZs2eN6fF+/vz5ZT8T08vNv2LFiuz7ttpqqzqfGT9+fLrooovqTI8aGPxL9wlKIy/KOl/KW1m3V91P7t6s379kyZLUvXvz/kZrISZpXZy3lXd7Zd9W3u1V91YQkzQqGVFUVVVV431kPWpPW9P85aYXnX/++WnMmDHV71etWpXefvvttNlmm632d1qbyApFAmXu3Llp4403bunFadeUtfJur+zbyrsh4roaF/2tt946VRoxScM4l+RLeSvr9sq+rbybMiZpVDKiR48eqWPHjnVqQSxYsKBO7YeiLbfcsuz8nTp1ypIL5XTp0iV7ldpkk01SWxWJCMkIZd0e2beVdXvVFvftSqkRUSQmqZx9uy1T3sq6vbJvK++miEka1YFl586dsyE6p02bVmN6vB88eHDZzwwaNKjO/A888EAaOHBg2f4iAADEJADQvjV6NI1oPnH99denG2+8MT3//PPpzDPPzIb1HDVqVHUTi2OPPbZ6/pj+2muvZZ+L+eNz0Xnl2Wef3bRrAgBUFDEJALRdje4zYvjw4WnRokVp3Lhxad68eal///5p6tSpqU+fPtnfY1okJ4r69u2b/T2SFtdcc03WduSqq65KX/rSl1J7F01Nvve979VpcoKybuvs28q6vbJvty1ikoazb+dLeSvr9sq+rbybUlWhksYBAwAAANpeMw0AAACAdSEZAQAAAORKMgIAAADIlWQEAAAAkCvJiGYyceLEbCSRrl27pgEDBqQZM2Y0109VtOnTp6fDDz88G6Wlqqoq3X333S29SO3W+PHj0x577JE22mijtMUWW6QjjzwyvfDCCy29WO3WpEmT0i677JI23njj7DVo0KD029/+tqUXq2L29TifjB49uqUXBZqMuCQf4pL8iEvyJS5pOePbcVwiGdEMpkyZku0sY8eOTU899VQaMmRIGjZsWI0hT2ka7733Xtp1113T1VdfrUib2UMPPZROPfXU9Nhjj6Vp06alFStWpKFDh2bbgKa37bbbpksuuSQ98cQT2euggw5KRxxxRHr22WcVdzN6/PHH03XXXZclgqC9EJfkR1ySH3FJvsQlLePxdh6XGNqzGey1115p9913zzKIRf369cueJEdmi+YRGcO77rorK2ea31tvvZXVkIhgYL/99lPkOdh0003T5ZdfnkaMGKG8m8HSpUuzc3c8Qb744ovTbrvtliZMmKCsafPEJS1DXJIvcUn+xCXNa2kFxCVqRjSx5cuXp1mzZmVPjEvF+5kzZzb1z0GLeffdd6svRDSvlStXpttvvz174hbNNWgeUfPnsMMOS5/73OcUMe2GuIRKIS7Jj7gkH6dWQFzSqaUXoL1ZuHBhdoD27NmzxvR4P3/+/BZbLmhKhUIhjRkzJu27776pf//+CreZPPPMM1ny4cMPP0wbbrhhVvNn5513Vt7NIJI9Tz75ZFYdEtoTcQmVQFySD3FJfm6vkLhEMqIZq+bVPknWngZt1WmnnZaefvrp9PDDD7f0orRrn/jEJ9Ls2bPTO++8k+6444503HHHZc1iJCSa1ty5c9M3v/nN9MADD2SdDkN7JC6hPROX5ENcko+5FRSXSEY0sR49eqSOHTvWqQWxYMGCOrUloC06/fTT07333pv1GB6dGdF8OnfunHbYYYfsvwcOHJhlx6+88sp07bXXKvYmFE3r4hwdIx8VRQ232Mejc9xly5Zl53Voi8QltHfikvyIS/Ixq4LiEn1GNMNBGjtOjDZQKt4PHjy4qX8OchO1e+LJw5133pn+8Ic/ZEPXkv82iAsQTeuzn/1sVvU0aqEUX5H8+frXv579d3u54FOZxCW0V+KSlicuaR6fraC4RM2IZhBt6Y855phsp4n23jEcSwzrOWrUqOb4uVTpvcy+9NJL1e9feeWV7CCNThV79+7dosvWHjvRufXWW9M999yTNtpoo+raP927d0/dunVr6cVrdy644IJsSOBevXqlJUuWZG0HH3zwwXTfffe19KK1O7E/1+77ZIMNNkibbbaZPlFoF8Ql+RGX5Edcki9xSX42qqC4RDKiGQwfPjwtWrQojRs3Ls2bNy/baaZOnZr69OnTHD9X0Z544ol04IEH1gi4QrStnzx5cgsuWftTHKr2gAMOqDH9pptuSscff3wLLVX79eabb2ZJzTiHRMInxpeORMTBBx/c0osGtDHikvyIS/IjLsmXuITmUFWI+jUAAAAAOdFnBAAAAJAryQgAAAAgV5IRAAAAQK4kIwAAAIBcSUYAAAAAuZKMAAAAAHIlGQEAAADkSjICAAAAyJVkBLDWjj/++HTkkUeuUwk++OCDqaqqKr3zzju2BAAgJoEK0amlFwBou6688spUKBRaejEAgAonJoG2RzICaLSVK1dmtRm6d++u9ACAFiMmgbZLMw2oAAcccEA67bTTstcmm2ySNttss/Ttb3+7ulbD8uXL07nnnpu22WabtMEGG6S99toraz5RNHny5Oxzv/71r9POO++cunTpkl577bU6zTSWLVuWzjjjjLTFFlukrl27pn333Tc9/vjjNZZl6tSpaaeddkrdunVLBx54YHr11VdzLAkAoCWJSYAiyQioED/96U9Tp06d0p/+9Kd01VVXpf/8z/9M119/ffa3E044IT3yyCPp9ttvT08//XT6yle+kj7/+c+nF198sfrz77//fho/fnz2mWeffTZLONQWCY077rgj+60nn3wy7bDDDumQQw5Jb7/9dvb3uXPnpi9+8Yvp0EMPTbNnz04jR45M5513Xo6lAAC0NDEJkCkA7d7+++9f6NevX2HVqlXV0771rW9l01566aVCVVVV4Y033qjxmc9+9rOF888/P/vvm266KapQFGbPnl1jnuOOO65wxBFHZP+9dOnSwnrrrVe45ZZbqv++fPnywtZbb1247LLLsvfxfeWWI777n//8ZzOtPQDQWohJgCJ9RkCF2HvvvbN+HooGDRqUfvSjH6Unnngia64RTSdKRZOLaM5R1Llz57TLLrvU+/0vv/xy+uijj9I+++xTPW299dZLe+65Z3r++eez9/FvueUAACqHmAQIkhFA6tixY5o1a1b2b6kNN9yw+r+jj4fSJEJtxf4nas8T04vTjLwBAKyOmAQqhz4joEI89thjdd7vuOOO6TOf+UzWE/WCBQuyPh5KX1tuuWWDvz/mj9oTDz/8cPW0qCkRNS/69euXvY/OL8stBwBQOcQkQJCMgAoRnUeOGTMmvfDCC+m2225LP/7xj9M3v/nNrHnG17/+9XTsscemO++8M73yyivZCBiXXnppNvJFQ8UoHCeffHI655xz0n333Zeee+659B//8R9Zx5cjRozI5hk1alTWnKO4HLfeems2UgcAUDnEJEDQTAMqRCQbPvjgg6wPh6gCefrpp6dvfOMb2d9uuummdPHFF6ezzjorvfHGG1lfEdGXQ4x60RiXXHJJWrVqVTrmmGPSkiVL0sCBA9P999+fPvaxj2V/7927dzbaxplnnpkmTpyYLcsPfvCDdOKJJzbLOgMArY+YBAhV0YulooD2Lcb03m233dKECRNaelEAgAomJgGKNNMAAAAAciUZAQAAAORKMw0AAAAgV2pGAAAAALmSjAAAAAByJRkBAAAA5EoyAgAAAMiVZAQAAACQK8kIAAAAIFeSEQAAAECuJCMAAACAlKf/B0qWjLePvUVPAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(1, 2, figsize=(13, 5))\n", + "df.groupby(\"period\").choice.value_counts(normalize=True).unstack().plot(\n", + " stacked=True,\n", + " kind=\"bar\",\n", + " rot=0,\n", + " title=\"Choice Probabilities - Discrete Experience Stocks\",\n", + " ax=ax[0],\n", + ")\n", + "\n", + "df_cont_exp.groupby(\"period\").choice.value_counts(normalize=True).unstack().plot(\n", + " stacked=True,\n", + " kind=\"bar\",\n", + " rot=0,\n", + " title=\"Choice Probabilities - Continuous Experience Stocks\",\n", + " ax=ax[1],\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "f698fa58", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "fig, ax = plt.subplots(1, 2, figsize=(13, 5))\n", + "df.groupby([\"period\", \"choice\"]).consumption.mean().unstack().plot(\n", + " rot=0, title=\"Choice Probabilities - Discrete Experience Stocks\", ax=ax[0]\n", + ")\n", + "\n", + "df.groupby([\"period\", \"choice\"]).consumption.mean().unstack().plot(\n", + " rot=0, title=\"Choice Probabilities - Continuous Experience Stocks\", ax=ax[1]\n", + ")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "strenuousjobs", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.13.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/source/development/roadmap.rst b/docs/source/development/roadmap.rst index b64da080..757c3f7c 100644 --- a/docs/source/development/roadmap.rst +++ b/docs/source/development/roadmap.rst @@ -3,6 +3,21 @@ Roadmap ======= -.. raw:: html +Below is a list of features that are currently planned for `dcegm` as well as associated issues and pull requests. -

via GIPHY

+- **Alternative batching** (`#200 `_) + The current solver relies on batching to handle large state spaces efficiently. We can add an alternative execution path that avoids batching, which may be more suitable for certain model specifications and/or and option to fill in dummy states for larger batches. + +- **Generalized income timing** (`#199 `_) + The classic `dcegm` implementation applies the income shock at the beginning of the period so that income is a function of last period's choice. Consequently, wealth determining the consumption choice is the same across all discrete choices. We plan to make income timing more flexible, allowing users to specify when income is received relative to the continuous (consumption) decisions. + +- **Improve documentation and interface for user specified functions** (`#94 `_) + The current interface already includes various error messages and warnings for misspecified user functions, but the interface could be improved to further help the user specify their model. In particular we want to add information that documents which inputs user functions can and cannot handle and some common sources of errors in user functions. + + +Recent Developments +=================== + +April 2026: **Multiple deterministic continuous state variables** + +Previously, `dcegm` only supported a single deterministic continuous state variable. We extended the solver to handle multiple deterministic continuous states, including support for the Druedahl–Jørgensen upper envelope algorithm. See PR `#197 `_ & `#198 `_ for more information. diff --git a/docs/source/development/team.rst b/docs/source/development/team.rst index 2d45bb4d..c4308c0e 100644 --- a/docs/source/development/team.rst +++ b/docs/source/development/team.rst @@ -3,7 +3,7 @@ Team ======= -`dc-egm` is a collaborative opensource project developed a team of junior researchers and students. We are located in Berlin, Munich, and Copenhagen. +`dcegm` is a collaborative open source project developed a team of junior researchers and students. We are located in Berlin, Munich, and Copenhagen. .. list-table:: :widths: 45 45 45 45 45 @@ -39,13 +39,17 @@ Team Projects & Theses ------------------ -`dc-egm` is collectively developed and used in multiple research projects as well as Undergraduate and Master theses. +`dcegm` is collectively developed and used in multiple research projects as well as Undergraduate and Master theses. Research Projects .................. -Blesch, M. and Veltri, B. (2025). Policy Uncertainty, Misinformation, and Statutory Retirement Age Reform. Working Paper. +Blesch, M. and Veltri, B. (2025). Life-Cycle Responses to Pension Reform: The Role of Subjective Policy Beliefs . `Working Paper `_. + +Gehlen, A. (2026). Occupations, Disability Insurance, and Career Choices. (Work in Progress) + +Gsell, S. (2026). The Career Costs of Elderly Parent Care. (Work in Progress) Theses ........ diff --git a/docs/source/guides/batching.rst b/docs/source/guides/batching.rst new file mode 100644 index 00000000..93c10304 --- /dev/null +++ b/docs/source/guides/batching.rst @@ -0,0 +1,152 @@ +.. _batching_guide: + +Batching Strategy and Segmentation +================================= + +The backward induction in ``dcegm`` is solved in batches. This is a computational detail to make array shapes compatible with fast JAX scans while preserving the model logic. + +Why batching exists +------------------- + +The number of feasible state-choice combinations usually changes over the life cycle. However, vectorized scan steps work best with equal leading dimensions. Batching groups state-choice rows into equal-sized chunks so each scan step can run with fixed shapes. + +Two batching modes +------------------ + +``dcegm`` supports two batching modes: + +- ``largest_block``: + + - Finds large dependency-safe batches. + - Typically yields fewer and larger batches. + - Good default for smooth state-choice profiles. + - **Default configuration**: ``dcegm`` uses this batching mode with no segmentation if batching is not configured otherwise. + +- ``period_max``: + + - Uses one batch per period within a segment. + - Pads smaller period batches to the segment-specific maximum number of state choices per period. + - Useful when state-choice counts vary strongly by period. + - **Padding rule**: If a period has fewer state choices than the segment maximum, the batch is padded with a valid dummy state-choice index from the same batch (deterministically the first one). This keeps shapes aligned and does not change the solution logic. + +Segmenting the horizon +---------------------- + +Use ``min_period_batch_segments`` to split the pre-terminal part of the horizon into segments. + +- Without segmentation: + + - ``batch_mode`` must be a single string. + +- With segmentation: + + - ``batch_mode`` can be a string (reused for all segments), or + - ``batch_mode`` can be a list with one entry per segment. + +The number of segments is ``len(min_period_batch_segments) + 1``. + +Valid strings are ``"largest_block"`` and ``"period_max"``. + +Examples +~~~~~~~~ + +No segmentation: + +.. code-block:: python + + model_config = { + "n_periods": 20, + "choices": np.arange(3, dtype=int), + "continuous_states": {"assets_end_of_period": np.linspace(0, 100, 200)}, + "n_quad_points": 5, + "batch_mode": "period_max", + } + +With segmentation: + +.. code-block:: python + + model_config = { + "n_periods": 20, + "choices": np.arange(3, dtype=int), + "continuous_states": {"assets_end_of_period": np.linspace(0, 100, 200)}, + "n_quad_points": 5, + "min_period_batch_segments": [8, 14], + "batch_mode": ["period_max", "largest_block", "period_max"], + } + +Tipp: Use ``get_n_state_choices_per_period`` to choose segments +---------------------------------------------------------------- + +To determine sensible segments for batching, inspect the number of state-choice combinations per period. + +.. code-block:: python + + model = dcegm.setup_model( + model_config=model_config, + model_specs=model_specs, + utility_functions=utility_functions, + utility_functions_final_period=utility_functions_final_period, + budget_constraint=budget_constraint, + state_space_functions=state_space_functions, + stochastic_states_transitions=stochastic_states_transitions, + ) + + n_state_choices = model.get_n_state_choices_per_period() + print(n_state_choices) + +This series can be used to detect structural breaks in complexity. Typical heuristics are: + +- Keep periods with similar counts in one segment. +- Split where there are abrupt jumps/drops. +- Use ``period_max`` in highly uneven segments. +- Keep ``largest_block`` in smoother segments. + +Example: experience growth and retirement regimes +------------------------------------------------- + +Consider a model with a discrete experience state where: + +- choice 0: no work, experience unchanged, +- choice 1: regular work, experience increases by 1, +- choice 2: intensive work, experience increases by 2, +- choice 3: retirement. + +Suppose retirement becomes available from period 8, and is mandatory from period 14. + +.. code-block:: python + + def choice_set(period, lagged_choice): + if period >= 14: + return np.array([3], dtype=int) # mandatory retirement + if period >= 8: + return np.array([0, 1, 2, 3], dtype=int) # retirement becomes available + return np.array([0, 1, 2], dtype=int) + + def next_period_deterministic_state(period, choice, experience): + if choice == 1: + experience_next = experience + 1 + elif choice == 2: + experience_next = experience + 2 + else: + experience_next = experience + return { + "period": period + 1, + "lagged_choice": choice, + "experience": experience_next, + } + +In this setup you often see: + +- gradual growth in state-choice counts early on, +- a jump when retirement becomes optional, +- a drop when retirement becomes mandatory. + +This pattern is a good reason to separate segments around the two regime changes: + +.. code-block:: python + + model_config["min_period_batch_segments"] = [8, 14] + model_config["batch_mode"] = ["period_max", "largest_block", "period_max"] + +We suggest testing different segmentation choices to determine the fastest solution for your model. diff --git a/docs/source/guides/practitioner_guide.rst b/docs/source/guides/practitioner_guide.rst index d5b0dc5d..629b4d34 100644 --- a/docs/source/guides/practitioner_guide.rst +++ b/docs/source/guides/practitioner_guide.rst @@ -168,3 +168,50 @@ This guide explains how to specify, solve, simulate and potentially estimate str - state_name (any key of `deterministic_states`, `stochastic_states`, `continuous_states`) The interfaces of `marginal_utility` and `inverse_marginal_utility` are accept the same inputs, except `inverse_marginal_utility` where naturally consumption is not accepted, but instead `marginal_utility`. + + + + +Tips and Suggestions +..................... + + + + +**Jax Compatibility** + +`dcegm` is fully `JAX `_ compatible. When specifying your model, pay attention that your user-supplied functions are jax compatible as well. + +In particular, make sure that your budget constraint, utility functions, and transition functions are fully JAX compatible and try to avoid if conditions and for loops wherever possible to speed up the code. Note that all user specified state space objects (the sparisty condition, next period determinist state functions, and state specific choice set) on the other hand should avoid using jax functions. + + + +**Model Sparsity** + +Thanks to it's efficient JAX implementation, `dcegm` is capable of solving and simulating large models. Nonetheless, it usually makes sense to exclude unreachable states from the state space for faster solution to the model. + +`dcegm` constructs the (discrete) state space by building the cartesian product of the model state variables. It is up to the user to exclude undesired states. To this end, `dcegm` requires users to specify sparsity conditions. These should be defined carefully and must be customized for your model. Refer to guidance on sparsity conditions in the documentation for help: :ref:`sparsity_conditions`. + + + +**Model Timing** + +When specifying the model, pay heed to the timing structure underlying the implementation which is important for the realization of choices, payoffs, and state transitions. For details, please refer to Iskhakov et al. (2017). + +This implementation currently supports the following timing of events where each period :math:`t` unfolds in the following sequence: + + +1. Agent observes the current state :math:`s_t` (including beginning-of-period + assets) and choice specific taste shocks. + +2. The agent makes a **discrete choice** :math:`d_t` + (e.g. whether to work or retire). + +3. The agent makes a **continuous choice** :math:`c_t` (e.g. how much to + consume). + +4. **Utility** :math:`u(c_t, d_t)` realizes. It depends on the continuous + and discrete choices but not on next-period states. + +5. State transitions that affect next-period states realize + (e.g. health shocks and income shocks), determining :math:`s_{t+1}`. diff --git a/docs/source/index.rst b/docs/source/index.rst index 3dc2cd37..6b7dceb8 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -30,6 +30,7 @@ Check out our :ref:`guides` to find information on getting sta :hidden: guides/practitioner_guide + guides/batching guides/templates guides/minimal_example.ipynb @@ -43,6 +44,7 @@ Check out our :ref:`guides` to find information on getting sta background/limitations background/literature background/interface_plots.ipynb + background/two_occupation_model.ipynb background/two_period_model_tutorial.ipynb background/specify_exogenous_processes.md background/sparsity_conditions diff --git a/src/dcegm/backward_induction.py b/src/dcegm/backward_induction.py index a6bb1b87..8f9fc03c 100644 --- a/src/dcegm/backward_induction.py +++ b/src/dcegm/backward_induction.py @@ -54,14 +54,19 @@ def backward_induction( cont_grids_next_period = calc_grids_jit(income_shock_draws_unscaled, params) + # Infer n_continuous_state_combinations from model structure + n_continuous_state_combinations = model_structure["continuous_state_space"][ + next(iter(model_structure["continuous_state_space"])) + ].shape[0] + ( value_solved, policy_solved, endog_grid_solved, ) = create_solution_container( - continuous_states_info=model_config["continuous_states_info"], + n_continuous_state_combinations=n_continuous_state_combinations, # Read out grid size - n_total_wealth_grid=model_config["tuning_params"]["n_total_wealth_grid"], + n_total_wealth_grid=model_config["n_total_wealth_grid"], n_state_choices=model_structure["state_choice_space"].shape[0], ) @@ -70,9 +75,11 @@ def backward_induction( lambda params_inner, cont_grids, weights, val_solved, pol_solved, endog_solved: solve_last_two_periods( params=params_inner, continuous_states_info=continuous_states_info, + model_structure=model_structure, cont_grids_next_period=cont_grids, income_shock_weights=weights, model_funcs=model_funcs, + upper_envelope_method=model_config["upper_envelope"]["method"], last_two_period_batch_info=batch_info["last_two_period_info"], value_solved=val_solved, policy_solved=pol_solved, @@ -104,9 +111,11 @@ def backward_induction( xs=xs, params=params, continuous_grids_info=continuous_states_info, + continuous_state_space=model_structure["continuous_state_space"], cont_grids_next_period=cont_grids_next_period, model_funcs=model_funcs, income_shock_weights=income_shock_weights, + upper_envelope_method=model_config["upper_envelope"]["method"], debug_info=None, ) diff --git a/src/dcegm/egm/interpolate_marginal_utility.py b/src/dcegm/egm/interpolate_marginal_utility.py index 96217cdf..dce4fb55 100644 --- a/src/dcegm/egm/interpolate_marginal_utility.py +++ b/src/dcegm/egm/interpolate_marginal_utility.py @@ -1,12 +1,16 @@ -from typing import Any, Callable, Dict, Tuple +from typing import Any, Callable, Dict, Tuple, cast from jax import numpy as jnp from jax import vmap from dcegm.interpolation.interp1d import interp1d_policy_and_value_on_wealth -from dcegm.interpolation.interp2d import ( +from dcegm.interpolation.interp1d_dj import interp1d_policy_and_value_on_wealth_dj +from dcegm.interpolation.interp2d_irregular import ( interp2d_policy_and_value_on_wealth_and_regular_grid, ) +from dcegm.interpolation.interpnd_regular import ( + interpnd_policy_and_value_for_child_states_on_regular_grids, +) def interpolate_value_and_marg_util( @@ -18,7 +22,9 @@ def interpolate_value_and_marg_util( policy_child_state_choice: jnp.ndarray, value_child_state_choice: jnp.ndarray, child_state_idxs: jnp.ndarray, + continuous_state_space, params: Dict[str, float], + upper_envelope_method: str, ) -> Tuple[jnp.ndarray, jnp.ndarray]: """Interpolate value and policy for all child states and compute marginal utility. @@ -60,47 +66,46 @@ def interpolate_value_and_marg_util( compute_utility = model_funcs["compute_utility"] discount_factor = model_funcs["read_funcs"]["discount_factor"](params) - if continuous_grids_info["second_continuous_exists"]: - continuous_state_child_states = cont_grids_next_period["second_continuous"][ - child_state_idxs - ] - regular_grid = continuous_grids_info["second_continuous_grid"] + # Check if interpolation needs to be multidimensional and irregular + multi_dim = continuous_grids_info["has_additional_continuous_state"] + irregular = upper_envelope_method == "fues" - interp_for_single_state_choice = vmap( - interp2d_value_and_marg_util_for_state_choice, - in_axes=( - None, - None, - 0, - None, - 0, - 0, - 0, - 0, - 0, - None, - None, - ), # discrete state-choice + if multi_dim & irregular: + return _interpolate_value_and_marg_util_2d_irregular( + compute_marginal_utility=compute_marginal_utility, + compute_utility=compute_utility, + state_choice_vec=state_choice_vec, + continuous_grids_info=continuous_grids_info, + cont_grids_next_period=cont_grids_next_period, + child_state_idxs=child_state_idxs, + wealth_child_states=wealth_child_states, + endog_grid_child_state_choice=endog_grid_child_state_choice, + policy_child_state_choice=policy_child_state_choice, + value_child_state_choice=value_child_state_choice, + params=params, + discount_factor=discount_factor, ) - return interp_for_single_state_choice( - compute_marginal_utility, - compute_utility, - state_choice_vec, - regular_grid, - wealth_child_states, - continuous_state_child_states, - endog_grid_child_state_choice, - policy_child_state_choice, - value_child_state_choice, - params, - discount_factor, + elif multi_dim & (not irregular): + return _interpolate_value_and_marg_util_nd_regular( + compute_marginal_utility=compute_marginal_utility, + compute_utility=compute_utility, + state_choice_vec=state_choice_vec, + continuous_grids_info=continuous_grids_info, + cont_grids_next_period=cont_grids_next_period, + child_state_idxs=child_state_idxs, + wealth_child_states=wealth_child_states, + endog_grid_child_state_choice=endog_grid_child_state_choice, + policy_child_state_choice=policy_child_state_choice, + value_child_state_choice=value_child_state_choice, + params=params, + discount_factor=discount_factor, ) - else: + # Selects inside if jorgensen_druedahl or fues (different treatment of budget constraint) interp_for_single_state_choice = vmap( interp1d_value_and_marg_util_for_state_choice, - in_axes=(None, None, 0, 0, 0, 0, 0, None, None), # discrete state-choice + in_axes=(None, None, 0, 0, 0, 0, 0, None, None, None), ) return interp_for_single_state_choice( @@ -113,6 +118,7 @@ def interpolate_value_and_marg_util( value_child_state_choice, params, discount_factor, + upper_envelope_method == "druedahl_jorgensen", ) @@ -126,6 +132,7 @@ def interp1d_value_and_marg_util_for_state_choice( value_child_state_choice: jnp.ndarray, params: Dict[str, float], discount_factor: float, + use_dj_interpolation: bool, ) -> Tuple[jnp.ndarray, jnp.ndarray]: """Interpolate value and policy for given child state and compute marginal utility. @@ -161,17 +168,34 @@ def interp1d_value_and_marg_util_for_state_choice( """ + endog_grid_child_state_choice = jnp.asarray(endog_grid_child_state_choice) + policy_child_state_choice = jnp.asarray(policy_child_state_choice) + value_child_state_choice = jnp.asarray(value_child_state_choice) + assets_beginning_of_next_period = jnp.asarray(assets_beginning_of_next_period) + def interp_on_single_wealth_point(wealth_point): - policy_interp, value_interp = interp1d_policy_and_value_on_wealth( - wealth=wealth_point, - wealth_grid=endog_grid_child_state_choice, - policy_grid=policy_child_state_choice, - value_grid=value_child_state_choice, - compute_utility=compute_utility, - state_choice_vec=state_choice_vec, - params=params, - discount_factor=discount_factor, - ) + if use_dj_interpolation: + policy_interp, value_interp = interp1d_policy_and_value_on_wealth_dj( + wealth=wealth_point, + wealth_grid=endog_grid_child_state_choice[0], + policy_grid=policy_child_state_choice[0], + value_grid=value_child_state_choice[0], + compute_utility=compute_utility, + state_choice_vec=state_choice_vec, + params=params, + discount_factor=discount_factor, + ) + else: + policy_interp, value_interp = interp1d_policy_and_value_on_wealth( + wealth=wealth_point, + wealth_grid=endog_grid_child_state_choice[0], + policy_grid=policy_child_state_choice[0], + value_grid=value_child_state_choice[0], + compute_utility=compute_utility, + state_choice_vec=state_choice_vec, + params=params, + discount_factor=discount_factor, + ) marg_util_interp = compute_marginal_utility( consumption=policy_interp, params=params, **state_choice_vec ) @@ -182,18 +206,185 @@ def interp_on_single_wealth_point(wealth_point): vmap(interp_on_single_wealth_point) # income shocks ) # wealth grid + # Select dummy dimension + assets_points = jnp.asarray(assets_beginning_of_next_period)[0] value_interp, marg_util_interp = interp_over_single_wealth_and_income_shock_draw( - assets_beginning_of_next_period + assets_points + ) + value_interp = jnp.asarray(value_interp) + marg_util_interp = jnp.asarray(marg_util_interp) + + # Add it back in the beginning + return value_interp[None, :, :], marg_util_interp[None, :, :] + + +def _interpolate_value_and_marg_util_2d_irregular( + compute_marginal_utility: Callable, + compute_utility: Callable, + state_choice_vec: Dict[str, int], + continuous_grids_info: Dict[str, Any], + cont_grids_next_period: Dict[str, jnp.ndarray], + child_state_idxs: jnp.ndarray, + wealth_child_states: jnp.ndarray, + endog_grid_child_state_choice: jnp.ndarray, + policy_child_state_choice: jnp.ndarray, + value_child_state_choice: jnp.ndarray, + params: Dict[str, float], + discount_factor: float, +) -> Tuple[jnp.ndarray, jnp.ndarray]: + continuous_state_name = continuous_grids_info["additional_continuous_state_names"][ + 0 + ] + continuous_states_next = _get_continuous_states_next(cont_grids_next_period) + continuous_state_child_states = continuous_states_next[continuous_state_name][ + child_state_idxs + ] + + interp_for_single_state_choice = vmap( + interp2d_value_and_marg_util_for_state_choice, + in_axes=( + None, + None, + 0, + None, + 0, + 0, + 0, + 0, + 0, + None, + None, + ), + ) + + return interp_for_single_state_choice( + compute_marginal_utility, + compute_utility, + state_choice_vec, + continuous_grids_info["additional_continuous_state_grids"], + wealth_child_states, + continuous_state_child_states, + endog_grid_child_state_choice, + policy_child_state_choice, + value_child_state_choice, + params, + discount_factor, + ) + + +def _interpolate_value_and_marg_util_nd_regular( + compute_marginal_utility: Callable, + compute_utility: Callable, + state_choice_vec: Dict[str, int], + continuous_grids_info: Dict[str, Any], + cont_grids_next_period: Dict[str, jnp.ndarray], + child_state_idxs: jnp.ndarray, + wealth_child_states: jnp.ndarray, + endog_grid_child_state_choice: jnp.ndarray, + policy_child_state_choice: jnp.ndarray, + value_child_state_choice: jnp.ndarray, + params: Dict[str, float], + discount_factor: float, +) -> Tuple[jnp.ndarray, jnp.ndarray]: + continuous_state_names = continuous_grids_info["additional_continuous_state_names"] + continuous_states_next = _get_continuous_states_next(cont_grids_next_period) + continuous_state_child_states = { + name: continuous_states_next[name][child_state_idxs] + for name in continuous_state_names + } + + policy_interp, value_interp = ( + interpnd_policy_and_value_for_child_states_on_regular_grids( + additional_continuous_state_grids=continuous_grids_info[ + "additional_continuous_state_grids" + ], + wealth_grid=endog_grid_child_state_choice[0, 0], + policy_grid_child_states=policy_child_state_choice, + value_grid_child_states=value_child_state_choice, + continuous_state_child_states=continuous_state_child_states, + wealth_child_states=wealth_child_states, + state_choice_child_states=state_choice_vec, + compute_utility=compute_utility, + params=params, + discount_factor=discount_factor, + ) ) + marg_util_interp = _compute_nd_marginal_utility( + compute_marginal_utility=compute_marginal_utility, + policy_interp=policy_interp, + state_choice_child_states=state_choice_vec, + continuous_state_child_states=continuous_state_child_states, + params=params, + ) return value_interp, marg_util_interp +def _get_continuous_states_next( + cont_grids_next_period: Dict[str, jnp.ndarray], +) -> Dict[str, jnp.ndarray]: + if "continuous_states" not in cont_grids_next_period: + raise KeyError( + "Expected key 'continuous_states' in cont_grids_next_period. " + "This object should come from law_of_motion.calc_cont_grids_next_period()." + ) + return cast(Dict[str, jnp.ndarray], cont_grids_next_period["continuous_states"]) + + +def _compute_nd_marginal_utility( + compute_marginal_utility: Callable, + policy_interp: jnp.ndarray, + state_choice_child_states: Dict[str, Any], + continuous_state_child_states: Dict[str, jnp.ndarray], + params: Dict[str, float], +) -> jnp.ndarray: + """Compute marginal utility pointwise on ND interpolation output via vmaps.""" + state_choice_child_states_marg = { + key: jnp.asarray(value) for key, value in state_choice_child_states.items() + } + continuous_state_child_states_marg = { + key: jnp.asarray(value) for key, value in continuous_state_child_states.items() + } + + def _marg_util_at_point( + consumption_point, + state_choice_point, + continuous_state_point, + ): + out = compute_marginal_utility( + consumption=consumption_point, + params=params, + **state_choice_point, + **continuous_state_point, + ) + if isinstance(out, tuple): + out = out[0] + return jnp.asarray(out) + + return vmap( + vmap( + vmap( + vmap( + _marg_util_at_point, + in_axes=(0, None, None), + ), + in_axes=(0, None, None), + ), + in_axes=(0, None, 0), + ), + in_axes=(0, 0, 0), + )( + policy_interp, + state_choice_child_states_marg, + continuous_state_child_states_marg, + ) + + def interp2d_value_and_marg_util_for_state_choice( compute_marginal_utility: Callable, compute_utility: Callable, state_choice_vec: Dict[str, int], - regular_grid: jnp.ndarray, + continuous_state_space: Dict[str, jnp.ndarray], assets_beginning_of_next_period: jnp.ndarray, continuous_state_beginning_of_next_period: jnp.ndarray, endog_grid_child_state_choice: jnp.ndarray, @@ -240,7 +431,7 @@ def interp_on_single_wealth_point(wealth_point, second_cont_grid_point): policy_interp, value_interp = ( interp2d_policy_and_value_on_wealth_and_regular_grid( - regular_grid=regular_grid, + continuous_state_space=continuous_state_space, wealth_grid=endog_grid_child_state_choice, policy_grid=policy_child_state_choice, value_grid=value_child_state_choice, diff --git a/src/dcegm/egm/solve_euler_equation.py b/src/dcegm/egm/solve_euler_equation.py index 3cc3f8da..a81a2341 100644 --- a/src/dcegm/egm/solve_euler_equation.py +++ b/src/dcegm/egm/solve_euler_equation.py @@ -1,19 +1,19 @@ """Auxiliary functions for the EGM algorithm.""" -from typing import Callable, Dict, Tuple +from typing import Any, Callable, Dict, Tuple from jax import numpy as jnp from jax import vmap def calculate_candidate_solutions_from_euler_equation( - continuous_grids_info: jnp.ndarray, + continuous_grids_info: Dict[str, Any], + continuous_state_space: Dict[str, jnp.ndarray], marg_util_next: jnp.ndarray, emax_next: jnp.ndarray, - state_choice_mat: jnp.ndarray, + state_choice_mat: Dict[str, jnp.ndarray], idx_post_decision_child_states: jnp.ndarray, - model_funcs: Dict[str, Callable], - has_second_continuous_state: bool, + model_funcs: Dict[str, Any], params: Dict[str, float], ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray]: """Calculate candidates for the optimal policy and value function.""" @@ -23,50 +23,29 @@ def calculate_candidate_solutions_from_euler_equation( ) feasible_emax_child = jnp.take(emax_next, idx_post_decision_child_states, axis=0) - if has_second_continuous_state: - ( - endog_grid, - policy, - value, - expected_value, - ) = vmap( - vmap( - vmap( - compute_optimal_policy_and_value_wrapper, - in_axes=(1, 1, None, 0, None, None, None), # assets - ), - in_axes=(1, 1, 0, None, None, None, None), # second continuous state - ), - in_axes=(0, 0, None, None, 0, None, None), # discrete states choices - )( - feasible_marg_utils_child, - feasible_emax_child, - continuous_grids_info["second_continuous_grid"], - continuous_grids_info["assets_grid_end_of_period"], - state_choice_mat, - model_funcs, - params, - ) - else: - ( - endog_grid, - policy, - value, - expected_value, - ) = vmap( + ( + endog_grid, + policy, + value, + expected_value, + ) = vmap( + vmap( vmap( compute_optimal_policy_and_value, - in_axes=(1, 1, 0, None, None, None), # assets grid + in_axes=(1, 1, None, 0, None, None, None), ), - in_axes=(0, 0, None, 0, None, None), # states and choices - )( - feasible_marg_utils_child, - feasible_emax_child, - continuous_grids_info["assets_grid_end_of_period"], - state_choice_mat, - model_funcs, - params, - ) + in_axes=(1, 1, 0, None, None, None, None), + ), + in_axes=(0, 0, None, None, 0, None, None), + )( + feasible_marg_utils_child, + feasible_emax_child, + continuous_state_space, + continuous_grids_info["assets_grid_end_of_period"], + state_choice_mat, + model_funcs, + params, + ) return ( endog_grid, @@ -76,75 +55,33 @@ def calculate_candidate_solutions_from_euler_equation( ) -def compute_optimal_policy_and_value_wrapper( - marg_util_next: jnp.ndarray, - emax_next: jnp.ndarray, - second_continuous_grid: jnp.ndarray, - assets_grid_end_of_period: jnp.ndarray, - state_choice_vec: Dict, - model_funcs: Dict[str, Callable], - params: Dict[str, float], -) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray]: - """Write second continuous grid point into state_choice_vec.""" - state_choice_vec["continuous_state"] = second_continuous_grid - - return compute_optimal_policy_and_value( - marg_util_next, - emax_next, - assets_grid_end_of_period, - state_choice_vec, - model_funcs, - params, - ) - - def compute_optimal_policy_and_value( marg_util_next: jnp.ndarray, emax_next: jnp.ndarray, + continuous_state_vec: Any, assets_grid_end_of_period: jnp.ndarray, - state_choice_vec: Dict, - model_funcs: Dict[str, Callable], + state_choice_vec: Any, + model_funcs: Dict[str, Any], params: Dict[str, float], ) -> Tuple[jnp.ndarray, jnp.ndarray, jnp.ndarray, jnp.ndarray]: - """Compute optimal child-state- and choice-specific policy and value function. - - Given the marginal utilities of possible child states and next period wealth, we - compute the optimal policy and value functions by solving the euler equation - and using the optimal consumption level in the bellman equation. + """Compute EGM candidates for one state-choice and one continuous-state point. Args: - marg_utils (np.ndarray): 1d array of shape (n_stochastic_states,) containing - the state-choice specific marginal utilities for a given point on - the savings grid. - emax (np.ndarray): 1d array of shape (n_stochastic_states,) containing - the state-choice specific expected maximum value for a given point on - the savings grid. - assets_grid_end_of_period (np.ndarray): 1d array of shape (n_grid_wealth,) - containing the exogenous savings grid. - trans_vec_state (np.ndarray): 1d array of shape (n_stochastic_states,) containing - for each exogenous process state the corresponding transition probability. - state_choice_vec (np.ndarray): A dictionary containing the states and a - corresponding admissible choice of a particular state choice vector. - compute_inverse_marginal_utility (Callable): Function for calculating the - inverse marginal utility, which takes the marginal utility as only input. - compute_value (callable): Function for calculating the value from consumption - level, discrete choice and expected value. The inputs ```discount_rate``` - and ```compute_utility``` are already partialled in. - params (dict): Dictionary of model parameters. + marg_util_next: Marginal utilities in child states for one assets grid point. + emax_next: Expected maximum values in child states for one assets grid point. + continuous_state_vec: Continuous-state values for one continuous-state point. + assets_grid_end_of_period: Exogenous end-of-period asset grid. + state_choice_vec: Dictionary of discrete states and choice. + model_funcs: Processed model functions used by the EGM step. + params: Model parameter dictionary. Returns: - tuple: - - - endog_grid (np.ndarray): 1d array of shape (n_grid_wealth + 1,) - containing the current state- and choice-specific endogenous grid. - - policy (np.ndarray): 1d array of shape (n_grid_wealth + 1,) - containing the current state- and choice-specific policy function. - - value (np.ndarray): 1d array of shape (n_grid_wealth + 1,) - containing the current state- and choice-specific value function. - - expected_value_zero_savings (float): The agent's expected value given that - she saves nothing. + A tuple ``(endog_grid, policy, value, expected_value)`` where each array is + state-choice specific on the assets grid. """ + state_choice_vec = {**state_choice_vec, **continuous_state_vec} + compute_inverse_marginal_utility = model_funcs["compute_inverse_marginal_utility"] compute_utility = model_funcs["compute_utility"] compute_stochastic_transition_vec = model_funcs["compute_stochastic_transition_vec"] diff --git a/src/dcegm/final_periods.py b/src/dcegm/final_periods.py index a3a2a2f4..96668e6b 100644 --- a/src/dcegm/final_periods.py +++ b/src/dcegm/final_periods.py @@ -5,18 +5,17 @@ import jax.numpy as jnp from jax import vmap -from dcegm.law_of_motion import ( - calc_assets_beginning_of_period_2cont_vec, -) from dcegm.solve_single_period import solve_for_interpolated_values def solve_last_two_periods( params: Dict[str, float], continuous_states_info: Dict[str, Any], - cont_grids_next_period: Dict[str, jnp.ndarray], + model_structure: Dict[str, Any], + cont_grids_next_period: Dict[str, Any], income_shock_weights: jnp.ndarray, - model_funcs: Dict[str, Callable], + model_funcs: Dict[str, Any], + upper_envelope_method: str, last_two_period_batch_info, value_solved, policy_solved, @@ -48,6 +47,7 @@ def solve_last_two_periods( for all states, end of period assets, and income shocks. """ + batch_info = last_two_period_batch_info ( value_solved, policy_solved, @@ -55,17 +55,13 @@ def solve_last_two_periods( value_interp_final_period, marginal_utility_final_last_period, ) = solve_final_period( - idx_state_choices_final_period=last_two_period_batch_info[ - "idx_state_choices_final_period" - ], - idx_parent_states_final_period=last_two_period_batch_info[ - "idxs_parent_states_final_period" - ], - state_choice_mat_final_period=last_two_period_batch_info[ - "state_choice_mat_final_period" - ], + idx_state_choices_final_period=batch_info["idx_state_choices_final_period"], + idx_parent_states_final_period=batch_info["idxs_parent_states_final_period"], + state_choice_mat_final_period=batch_info["state_choice_mat_final_period"], cont_grids_next_period=cont_grids_next_period, continuous_states_info=continuous_states_info, + upper_envelope_method=upper_envelope_method, + model_structure=model_structure, params=params, model_funcs=model_funcs, value_solved=value_solved, @@ -103,6 +99,7 @@ def solve_last_two_periods( params=params, income_shock_weights=income_shock_weights, continuous_grids_info=continuous_states_info, + continuous_state_space=model_structure["continuous_state_space"], model_funcs=model_funcs, debug_info=debug_info, ) @@ -151,10 +148,12 @@ def solve_final_period( idx_state_choices_final_period, idx_parent_states_final_period, state_choice_mat_final_period, + cont_grids_next_period: Dict[str, Any], continuous_states_info: Dict[str, Any], - cont_grids_next_period: Dict[str, jnp.ndarray], + upper_envelope_method: str, + model_structure: Dict[str, Any], params: Dict[str, float], - model_funcs: Dict[str, Callable], + model_funcs: Dict[str, Any], value_solved, policy_solved, endog_grid_solved, @@ -182,223 +181,82 @@ def solve_final_period( """ - if continuous_states_info["second_continuous_exists"]: - ( - value_solved, - policy_solved, - endog_grid_solved, - value, - marg_util, - ) = solve_final_period_second_continuous( - idx_state_choices_final_period=idx_state_choices_final_period, - idx_parent_states_final_period=idx_parent_states_final_period, - state_choice_mat_final_period=state_choice_mat_final_period, - cont_grids_next_period=cont_grids_next_period, - continuous_states_info=continuous_states_info, - params=params, - model_funcs=model_funcs, - value_solved=value_solved, - policy_solved=policy_solved, - endog_grid_solved=endog_grid_solved, - ) - else: - ( - value_solved, - policy_solved, - endog_grid_solved, - value, - marg_util, - ) = solve_final_period_discrete( - idx_state_choices_final_period=idx_state_choices_final_period, - idx_parent_states_final_period=idx_parent_states_final_period, - state_choice_mat_final_period=state_choice_mat_final_period, - cont_grids_next_period=cont_grids_next_period, - params=params, - compute_utility=model_funcs["compute_utility_final"], - compute_marginal_utility=model_funcs["compute_marginal_utility_final"], - value_solved=value_solved, - policy_solved=policy_solved, - endog_grid_solved=endog_grid_solved, - ) - - return ( - value_solved, - policy_solved, - endog_grid_solved, - value, - marg_util, - ) - - -# ===================================================================================== -# Solve final period discrete states only -# ===================================================================================== - - -def solve_final_period_discrete( - idx_state_choices_final_period, - idx_parent_states_final_period, - state_choice_mat_final_period, - cont_grids_next_period: Dict[str, jnp.ndarray], - params: Dict[str, float], - compute_utility: Callable, - compute_marginal_utility: Callable, - value_solved, - policy_solved, - endog_grid_solved, -): - """Solve final period for only discrete states. - - Here we make use a trick to solve the final period directly at the wealth gridpoints - of next period. When saving the solution, we take (randomly) the middle of income - shock draws. + compute_utility = model_funcs["compute_utility_final"] + compute_marginal_utility = model_funcs["compute_marginal_utility_final"] - """ wealth_child_states_final_period = cont_grids_next_period["assets_begin_of_period"][ idx_parent_states_final_period ] - # n_wealth = model_config["wealth"].shape[0] + continuous_state_final = { + key: value[idx_parent_states_final_period] + for key, value in cont_grids_next_period["continuous_states"].items() + } + + n_assets = wealth_child_states_final_period.shape[-2] value, marg_util = vmap( vmap( vmap( - calc_value_and_marg_util_for_each_gridpoint, - in_axes=(None, 0, None, None, None), # income shocks + vmap( + calc_value_and_marg_util_for_each_gridpoint, + in_axes=(None, None, 0, None, None, None), + ), + in_axes=(None, None, 0, None, None, None), ), - in_axes=(None, 0, None, None, None), # wealth + in_axes=(None, 0, 0, None, None, None), ), - in_axes=(0, 0, None, None, None), # discrete state choices + in_axes=(0, 0, 0, None, None, None), )( state_choice_mat_final_period, + continuous_state_final, wealth_child_states_final_period, params, compute_utility, compute_marginal_utility, ) - # Choose which draw we take for policy and value function as those are not - # saved with respect to the draws - middle_of_draws = int((value.shape[2] - 1) / 2) - # Select solutions to store - value_final = value[:, :, middle_of_draws] - - # The policy in the last period is eat it all. Either as bequest or by consuming. - # The user defines this by the bequest functions. So we save the wealth also - # in the policy container. We also need to sort the wealth and value - wealth_to_save = wealth_child_states_final_period[:, :, middle_of_draws] - sort_idx = jnp.argsort(wealth_to_save, axis=1) - wealth_sorted = jnp.take_along_axis(wealth_to_save, sort_idx, axis=1) - values_sorted = jnp.take_along_axis(value_final, sort_idx, axis=1) - - # Store results and add zero entry for the first column - zeros_to_append = jnp.zeros(value_final.shape[0]) - - # Add as first column to the sorted arrays - values_with_zeros = jnp.column_stack((zeros_to_append, values_sorted)) - wealth_with_zeros = jnp.column_stack((zeros_to_append, wealth_sorted)) - - value_solved = value_solved.at[ - idx_state_choices_final_period, : values_with_zeros.shape[1] - ].set(values_with_zeros) - policy_solved = policy_solved.at[ - idx_state_choices_final_period, : values_with_zeros.shape[1] - ].set(wealth_with_zeros) - endog_grid_solved = endog_grid_solved.at[ - idx_state_choices_final_period, : values_with_zeros.shape[1] - ].set(wealth_with_zeros) - return ( - value_solved, - policy_solved, - endog_grid_solved, - value, - marg_util, - ) - - -# ===================================================================================== -# Solver final period with second continuous state -# ===================================================================================== - - -def solve_final_period_second_continuous( - idx_state_choices_final_period, - idx_parent_states_final_period, - state_choice_mat_final_period, - cont_grids_next_period: Dict[str, jnp.ndarray], - continuous_states_info: Dict[str, Any], - params: Dict[str, float], - model_funcs: Dict[str, Callable], - value_solved, - policy_solved, - endog_grid_solved, -): - """Solve final period with second continuous state. - - Here we solve the final period two times: - Once for wealth and second continuous calculated by the law of motion and once for their exogenous - grid values. We do that, because the solution is always assumed to be calculated on the exogenous grid - of the second continuous state. - - """ - wealth_child_states_final_period = cont_grids_next_period["assets_begin_of_period"][ - idx_parent_states_final_period - ] - - n_assets = wealth_child_states_final_period.shape[-2] - - continuous_state_final = cont_grids_next_period["second_continuous"][ - idx_parent_states_final_period - ] + if continuous_states_info["has_additional_continuous_state"]: + # We also need to solve at the state space and the child states to store correctly + # For Druedahl Jorgensen wealth needs to be assets_begin_of_period + assets_begin = "assets_begin_of_period" in continuous_states_info.keys() + if upper_envelope_method == "druedahl_jorgensen": + asset_grid = continuous_states_info["assets_begin_of_period"] + else: + asset_grid = continuous_states_info["assets_grid_end_of_period"] - value, marg_util = vmap( - vmap( + values_regular, wealth_at_regular = vmap( vmap( vmap( - calc_value_and_marg_util_for_each_gridpoint_second_continuous, - in_axes=(None, 0, None, None, None, None), # income shocks + calc_value_and_budget_for_each_gridpoint, + in_axes=(None, None, 0, None, None, None, None), ), - in_axes=(None, 0, None, None, None, None), # wealth + in_axes=(None, 0, None, None, None, None, None), ), - in_axes=(None, 0, 0, None, None, None), # second continuous_state - ), - in_axes=(0, 0, 0, None, None, None), # discrete state choices - )( - state_choice_mat_final_period, - wealth_child_states_final_period, - continuous_state_final, - params, - model_funcs["compute_utility_final"], - model_funcs["compute_marginal_utility_final"], - ) + in_axes=(0, None, None, None, None, None, None), + )( + state_choice_mat_final_period, + model_structure["continuous_state_space"], + asset_grid, + params, + compute_utility, + model_funcs["compute_assets_begin_of_period"], + assets_begin, + ) - # For the value to save in the second continuous case, we calculate the value - # at the exogenous wealth and second continuous points - value_regular, wealth_at_regular = vmap( - vmap( - vmap( - calc_value_and_budget_for_each_gridpoint, - in_axes=(None, 0, None, None, None, None), # wealth - ), - in_axes=(None, None, 0, None, None, None), # second continuous_state - ), - in_axes=(0, None, None, None, None, None), # discrete state choices - )( - state_choice_mat_final_period, - continuous_states_info["assets_grid_end_of_period"], - continuous_states_info["second_continuous_grid"], - params, - model_funcs["compute_utility_final"], - model_funcs["compute_assets_begin_of_period"], - ) + sort_idx = jnp.argsort(wealth_at_regular, axis=2) + wealth_sorted = jnp.take_along_axis(wealth_at_regular, sort_idx, axis=2) + values_sorted = jnp.take_along_axis(values_regular, sort_idx, axis=2) + else: + middle_of_draws = int((value.shape[3] - 1) / 2) + value_final = value[:, :, :, middle_of_draws] - sort_idx = jnp.argsort(wealth_at_regular, axis=2) - wealth_sorted = jnp.take_along_axis(wealth_at_regular, sort_idx, axis=2) - values_sorted = jnp.take_along_axis(value_regular, sort_idx, axis=2) + wealth_to_save = wealth_child_states_final_period[:, :, :, middle_of_draws] + sort_idx = jnp.argsort(wealth_to_save, axis=2) + wealth_sorted = jnp.take_along_axis(wealth_to_save, sort_idx, axis=2) + values_sorted = jnp.take_along_axis(value_final, sort_idx, axis=2) - # Store results and add zero entry for the first column zeros_to_append = jnp.zeros(values_sorted.shape[:-1]) - # Stack along the second-to-last axis (axis 1) values_with_zeros = jnp.concatenate( (zeros_to_append[..., None], values_sorted), axis=2 ) @@ -426,45 +284,23 @@ def solve_final_period_second_continuous( def calc_value_and_marg_util_for_each_gridpoint( - state_choice_vec, wealth, params, compute_utility, compute_marginal_utility -): - """Continuous state is missing here!""" - value = compute_utility( - **state_choice_vec, - wealth=wealth, - params=params, - ) - - marg_util = compute_marginal_utility( - **state_choice_vec, - wealth=wealth, - params=params, - ) - - return value, marg_util - - -def calc_value_and_marg_util_for_each_gridpoint_second_continuous( state_choice_vec, - wealth_final_period, - second_continuous_state, + continuous_state_vec, + wealth, params, compute_utility, compute_marginal_utility, ): - """Continuous state is missing here!""" - value = calc_value_for_each_gridpoint_second_continuous( - state_choice_vec, - wealth_final_period, - second_continuous_state, - params, - compute_utility, + all_states = {**state_choice_vec, **continuous_state_vec} + value = compute_utility( + **all_states, + wealth=wealth, + params=params, ) marg_util = compute_marginal_utility( - **state_choice_vec, - wealth=wealth_final_period, - continuous_state=second_continuous_state, + **all_states, + wealth=wealth, params=params, ) @@ -473,46 +309,33 @@ def calc_value_and_marg_util_for_each_gridpoint_second_continuous( def calc_value_and_budget_for_each_gridpoint( state_choice_vec, + continuous_state_vec, asset_grid_point_end_of_previous_period, - second_continuous_state, params, compute_utility, compute_assets_begin_of_period, + assets_begin, ): state_vec = state_choice_vec.copy() state_vec.pop("choice") - wealth_final_period = calc_assets_beginning_of_period_2cont_vec( - state_vec=state_vec, - continuous_state_beginning_of_period=second_continuous_state, - asset_end_of_previous_period=asset_grid_point_end_of_previous_period, - income_shock_draw=jnp.array(0.0), - params=params, - compute_assets_begin_of_period=compute_assets_begin_of_period, - aux_outs=False, - ) - - value = calc_value_for_each_gridpoint_second_continuous( - state_choice_vec=state_choice_vec, - wealth_final_period=wealth_final_period, - second_continuous_state=second_continuous_state, - params=params, - compute_utility=compute_utility, - ) - - return value, wealth_final_period - + if assets_begin: + # If assets begin, the grid is directly the assets we start from + wealth_final_period = asset_grid_point_end_of_previous_period + else: + wealth_final_period = compute_assets_begin_of_period( + **state_vec, + **continuous_state_vec, + asset_end_of_previous_period=asset_grid_point_end_of_previous_period, + income_shock_previous_period=jnp.array(0.0), + params=params, + ) -def calc_value_for_each_gridpoint_second_continuous( - state_choice_vec, - wealth_final_period, - second_continuous_state, - params, - compute_utility, -): - return compute_utility( + value = compute_utility( **state_choice_vec, + **continuous_state_vec, wealth=wealth_final_period, - continuous_state=second_continuous_state, params=params, ) + + return value, wealth_final_period diff --git a/src/dcegm/interfaces/inspect_solution.py b/src/dcegm/interfaces/inspect_solution.py index 52c9ad5f..482c8442 100644 --- a/src/dcegm/interfaces/inspect_solution.py +++ b/src/dcegm/interfaces/inspect_solution.py @@ -55,15 +55,18 @@ def partially_solve( relevant_state_choices_mask ] + n_continuous_state_combinations = model_structure["continuous_state_space"][ + next(iter(model_structure["continuous_state_space"])) + ].shape[0] ( value_solved, policy_solved, endog_grid_solved, ) = create_solution_container( - continuous_states_info=model_config["continuous_states_info"], # Read out grid size - n_total_wealth_grid=model_config["tuning_params"]["n_total_wealth_grid"], + n_total_wealth_grid=model_config["n_total_wealth_grid"], n_state_choices=relevant_state_choice_space.shape[0], + n_continuous_state_combinations=n_continuous_state_combinations, ) if return_candidates: @@ -72,9 +75,9 @@ def partially_solve( ].shape[0] value_candidates, policy_candidates, endog_grid_candidates = ( create_solution_container( - continuous_states_info=model_config["continuous_states_info"], n_total_wealth_grid=n_assets_end_of_period, n_state_choices=relevant_state_choice_space.shape[0], + n_continuous_state_combinations=n_continuous_state_combinations, ) ) @@ -96,9 +99,11 @@ def partially_solve( last_two_period_sols = solve_last_two_periods( params=params, continuous_states_info=continuous_states_info, + model_structure=model_structure, cont_grids_next_period=cont_grids_next_period, income_shock_weights=income_shock_weights, model_funcs=model_funcs, + upper_envelope_method=model_config["upper_envelope"]["method"], last_two_period_batch_info=last_two_period_batch_info, value_solved=value_solved, policy_solved=policy_solved, @@ -211,9 +216,11 @@ def partially_solve( xs=xs, params=params, continuous_grids_info=continuous_states_info, + continuous_state_space=model_structure["continuous_state_space"], cont_grids_next_period=cont_grids_next_period, model_funcs=model_funcs, income_shock_weights=income_shock_weights, + upper_envelope_method=model_config["upper_envelope"]["method"], debug_info=debug_info, ) diff --git a/src/dcegm/interfaces/interface.py b/src/dcegm/interfaces/interface.py index 20a92518..fd4c6350 100644 --- a/src/dcegm/interfaces/interface.py +++ b/src/dcegm/interfaces/interface.py @@ -80,7 +80,7 @@ def policy_and_value_for_states_and_choices( policy, value = jax.vmap( interpolate_policy_and_value_for_state_and_choice, - in_axes=(0, 0, 0, 0, None, None, None), + in_axes=(0, 0, 0, 0, None, None, None, None), )( value_grid_state_choice, policy_grid_state_choice, @@ -89,6 +89,7 @@ def policy_and_value_for_states_and_choices( params, model_config, model_funcs, + model_structure, ) return ( jnp.squeeze(policy), @@ -135,7 +136,7 @@ def value_for_state_and_choice( value = jax.vmap( interpolate_value_for_state_and_choice, - in_axes=(0, 0, 0, None, None, None), + in_axes=(0, 0, 0, None, None, None, None), )( value_grid_state_choice, endog_grid_state_choice, @@ -143,6 +144,7 @@ def value_for_state_and_choice( params, model_config, model_funcs, + model_structure, ) return jnp.squeeze(value) @@ -150,8 +152,11 @@ def value_for_state_and_choice( def policy_for_state_choice_vec( states, choices, + params, endog_grid_solved, policy_solved, + value_solved, + model_funcs, model_structure, model_config, ): @@ -180,15 +185,20 @@ def policy_for_state_choice_vec( ) endog_grid_state_choice = jnp.take(endog_grid_solved, state_choice_idx, axis=0) policy_grid_state_choice = jnp.take(policy_solved, state_choice_idx, axis=0) + value_grid_state_choice = jnp.take(value_solved, state_choice_idx, axis=0) policy = jax.vmap( interpolate_policy_for_state_and_choice, - in_axes=(0, 0, 0, None), + in_axes=(0, 0, 0, 0, None, None, None, None), )( policy_grid_state_choice, + value_grid_state_choice, endog_grid_state_choice, state_choices, + params, model_config, + model_funcs, + model_structure, ) return jnp.squeeze(policy) @@ -300,6 +310,7 @@ def choice_values_for_states( states, model_config, model_funcs, + model_structure, ): value_grid_states = jnp.take( value_solved, @@ -331,6 +342,7 @@ def wrapper_interp_value_for_choice( params=params, model_config=model_config, model_funcs=model_funcs, + model_structure=model_structure, ) # Read out choice range to loop over @@ -353,10 +365,14 @@ def wrapper_interp_value_for_choice( def choice_policies_for_states( policy_solved, + value_solved, endog_grid_solved, state_choice_indexes, states, + params, model_config, + model_funcs, + model_structure, ): policy_grid_states = jnp.take( policy_solved, @@ -372,10 +388,18 @@ def choice_policies_for_states( mode="fill", fill_value=jnp.nan, ) + value_grid_states = jnp.take( + value_solved, + state_choice_indexes, + axis=0, + mode="fill", + fill_value=jnp.nan, + ) def wrapper_interp_value_for_choice( state, policy_grid_state_choice, + value_grid_state_choice, endog_grid_state_choice, choice, ): @@ -383,9 +407,13 @@ def wrapper_interp_value_for_choice( return interpolate_policy_for_state_and_choice( policy_grid_state_choice=policy_grid_state_choice, + value_grid_state_choice=value_grid_state_choice, endog_grid_state_choice=endog_grid_state_choice, state_choice_vec=state_choice_vec, + params=params, model_config=model_config, + model_funcs=model_funcs, + model_structure=model_structure, ) # Read out choice range to loop over @@ -394,12 +422,13 @@ def wrapper_interp_value_for_choice( choice_values_per_state = jax.vmap( jax.vmap( wrapper_interp_value_for_choice, - in_axes=(None, 0, 0, 0), + in_axes=(None, 0, 0, 0, 0), ), - in_axes=(0, 0, 0, None), + in_axes=(0, 0, 0, 0, None), )( states, policy_grid_states, + value_grid_states, endog_grid_states, choice_range, ) diff --git a/src/dcegm/interfaces/model_class.py b/src/dcegm/interfaces/model_class.py index ea72ce01..1fe9d9d0 100644 --- a/src/dcegm/interfaces/model_class.py +++ b/src/dcegm/interfaces/model_class.py @@ -304,6 +304,7 @@ def get_solve_and_simulate_func( self, states_initial, seed, + slow_version=False, ): """Create a fast function for solving and simulation that is jit compiled in the first call.""" @@ -364,7 +365,10 @@ def solve_and_simulate_function_to_jit( return sim_dict - solve_simulate_func = jax.jit(solve_and_simulate_function_to_jit) + if slow_version: + solve_simulate_func = solve_and_simulate_function_to_jit + else: + solve_simulate_func = jax.jit(solve_and_simulate_function_to_jit) # Generate the function. The user only needs to provide params, but we call with the objects for jit. def solve_and_simulate_function(params): diff --git a/src/dcegm/interfaces/sol_interface.py b/src/dcegm/interfaces/sol_interface.py index d0f3270f..61a66919 100644 --- a/src/dcegm/interfaces/sol_interface.py +++ b/src/dcegm/interfaces/sol_interface.py @@ -148,10 +148,13 @@ def policy_for_states_and_choices(self, states, choices): return policy_for_state_choice_vec( states=states, choices=choices, + params=self.params, model_config=self.model_config, model_structure=self.model_structure, + model_funcs=self.model_funcs, endog_grid_solved=self.endog_grid, policy_solved=self.policy, + value_solved=self.value, ) def get_solution_for_discrete_state_choice(self, states, choices): @@ -241,6 +244,7 @@ def choice_values_for_states(self, states): states=states, model_config=self.model_config, model_funcs=self.model_funcs, + model_structure=self.model_structure, ) def choice_policies_for_states(self, states): @@ -263,8 +267,12 @@ def choice_policies_for_states(self, states): ) return choice_policies_for_states( policy_solved=self.policy, + value_solved=self.value, endog_grid_solved=self.endog_grid, state_choice_indexes=state_choice_idxs, states=states, + params=self.params, model_config=self.model_config, + model_funcs=self.model_funcs, + model_structure=self.model_structure, ) diff --git a/src/dcegm/interpolation/interp1d.py b/src/dcegm/interpolation/interp1d.py index 6f7d73a9..bc1ee1a8 100644 --- a/src/dcegm/interpolation/interp1d.py +++ b/src/dcegm/interpolation/interp1d.py @@ -15,7 +15,8 @@ def linear_interpolation_formula( interpolate_dist = x_new - x_low interpolate_slope = (y_high - y_low) / (x_high - x_low) interpol_res = (interpolate_slope * interpolate_dist) + y_low - + nan_slope = jnp.isnan(interpolate_slope) + interpol_res = nan_slope * y_low + (1 - nan_slope) * interpol_res return interpol_res @@ -63,7 +64,6 @@ def interp1d_policy_and_value_on_wealth( - value_interp (float): Interpolated value for wealth. """ - # For all choices, the wealth is the same in the solution ind_high, ind_low = get_index_high_and_low(x=wealth_grid, x_new=wealth) diff --git a/src/dcegm/interpolation/interp1d_dj.py b/src/dcegm/interpolation/interp1d_dj.py new file mode 100644 index 00000000..7dc75249 --- /dev/null +++ b/src/dcegm/interpolation/interp1d_dj.py @@ -0,0 +1,87 @@ +from typing import Callable, Dict, Tuple + +import jax.numpy as jnp + +from dcegm.interpolation.interp1d import ( + get_index_high_and_low, + linear_interpolation_formula, +) + + +def interp1d_policy_and_value_on_wealth_dj( + wealth: float | jnp.ndarray, + wealth_grid: jnp.ndarray, + policy_grid: jnp.ndarray, + value_grid: jnp.ndarray, + compute_utility: Callable, + state_choice_vec: Dict[str, int], + params: Dict[str, float], + discount_factor: float, +) -> Tuple[jnp.ndarray | float, jnp.ndarray | float]: + """1D interpolation for DJ with consume-all overwrite for policy and value.""" + ind_high, ind_low = get_index_high_and_low(x=wealth_grid, x_new=wealth) + + policy_interp = linear_interpolation_formula( + y_high=policy_grid[ind_high], + y_low=policy_grid[ind_low], + x_high=wealth_grid[ind_high], + x_low=wealth_grid[ind_low], + x_new=wealth, + ) + value_interp_on_grid = linear_interpolation_formula( + y_high=value_grid[ind_high], + y_low=value_grid[ind_low], + x_high=wealth_grid[ind_high], + x_low=wealth_grid[ind_low], + x_new=wealth, + ) + + consume_all_value = _consume_all_value( + wealth=wealth, + value_at_zero_wealth=value_grid[0], + compute_utility=compute_utility, + state_choice_vec=state_choice_vec, + params=params, + discount_factor=discount_factor, + ) + overwrite_mask = consume_all_value > value_interp_on_grid + policy = jnp.where(overwrite_mask, wealth, policy_interp) + value = jnp.where(overwrite_mask, consume_all_value, value_interp_on_grid) + return policy, value + + +def interp1d_value_on_wealth_dj( + wealth: float | jnp.ndarray, + wealth_grid: jnp.ndarray, + value_grid: jnp.ndarray, + compute_utility: Callable, + state_choice_vec: Dict[str, int], + params: Dict[str, float], + discount_factor: float, +) -> jnp.ndarray | float: + """1D value interpolation for DJ with consume-all overwrite.""" + _, value = interp1d_policy_and_value_on_wealth_dj( + wealth=wealth, + wealth_grid=wealth_grid, + policy_grid=wealth_grid, + value_grid=value_grid, + compute_utility=compute_utility, + state_choice_vec=state_choice_vec, + params=params, + discount_factor=discount_factor, + ) + return value + + +def _consume_all_value( + wealth: float | jnp.ndarray, + value_at_zero_wealth: float | jnp.ndarray, + compute_utility: Callable, + state_choice_vec: Dict[str, int], + params: Dict[str, float], + discount_factor: float, +) -> jnp.ndarray: + util = compute_utility(consumption=wealth, params=params, **state_choice_vec) + if isinstance(util, tuple): + util = util[0] + return jnp.asarray(util) + discount_factor * value_at_zero_wealth diff --git a/src/dcegm/interpolation/interp2d.py b/src/dcegm/interpolation/interp2d_irregular.py similarity index 97% rename from src/dcegm/interpolation/interp2d.py rename to src/dcegm/interpolation/interp2d_irregular.py index 8dcfe71e..681cb654 100644 --- a/src/dcegm/interpolation/interp2d.py +++ b/src/dcegm/interpolation/interp2d_irregular.py @@ -18,7 +18,7 @@ def interp2d_policy_and_value_on_wealth_and_regular_grid( - regular_grid: jnp.ndarray, + continuous_state_space: Dict[str, jnp.ndarray], wealth_grid: jnp.ndarray, policy_grid: jnp.ndarray, value_grid: jnp.ndarray, @@ -62,9 +62,11 @@ def interp2d_policy_and_value_on_wealth_and_regular_grid( value function. """ + # We only call this function for one continuous state besides assets_begin_of_period + cont_state_name = list(continuous_state_space.keys())[0] regular_points, wealth_points, coords_idxs = find_grid_coords_for_interp( - regular_grid=regular_grid, + regular_grid=continuous_state_space[cont_state_name], wealth_grid=wealth_grid, regular_point_to_interp=regular_point_to_interp, wealth_point_to_interp=wealth_point_to_interp, @@ -90,6 +92,7 @@ def interp2d_policy_and_value_on_wealth_and_regular_grid( wealth_min_unconstrained=wealth_grid[:, 1], value_at_zero_wealth=value_grid[:, 0], state_choice_vec=state_choice_vec, + cont_state_name=cont_state_name, params=params, discount_factor=discount_factor, ) @@ -107,6 +110,7 @@ def interp2d_value_on_wealth_and_regular_grid( state_choice_vec: Dict[str, int], params: dict, discount_factor, + cont_state_name: str = "continuous_state", ): """Interpolate the value function on a 2D grid. @@ -157,6 +161,7 @@ def interp2d_value_on_wealth_and_regular_grid( wealth_min_unconstrained=wealth_grid[:, 1], value_at_zero_wealth=value_grid[:, 0], state_choice_vec=state_choice_vec, + cont_state_name=cont_state_name, params=params, discount_factor=discount_factor, ) @@ -274,6 +279,7 @@ def interp2d_value_and_check_creditconstraint( wealth_min_unconstrained, value_at_zero_wealth, state_choice_vec, + cont_state_name, params, discount_factor, ): @@ -315,12 +321,15 @@ def interp2d_value_and_check_creditconstraint( wealth_point_to_interp <= wealth_min_unconstrained[regular_idx_left] ) + state_choice_vec = { + **state_choice_vec, + cont_state_name: regular_point_to_interp, + } # Now recalculate the closed-form value of consuming all wealth value_calc_left = ( compute_utility( consumption=wealth_point_to_interp, params=params, - continuous_state=regular_point_to_interp, **state_choice_vec, ) + discount_factor * value_at_zero_wealth[regular_idx_left] @@ -333,7 +342,6 @@ def interp2d_value_and_check_creditconstraint( value_calc_right = ( compute_utility( consumption=wealth_point_to_interp, - continuous_state=regular_point_to_interp, params=params, **state_choice_vec, ) diff --git a/src/dcegm/interpolation/interp_interfaces.py b/src/dcegm/interpolation/interp_interfaces.py index f0a66b0b..a9249b79 100644 --- a/src/dcegm/interpolation/interp_interfaces.py +++ b/src/dcegm/interpolation/interp_interfaces.py @@ -1,12 +1,19 @@ +import jax.numpy as jnp + from dcegm.interpolation.interp1d import ( interp1d_policy_and_value_on_wealth, interp_policy_on_wealth, interp_value_on_wealth, ) -from dcegm.interpolation.interp2d import ( +from dcegm.interpolation.interp1d_dj import ( + interp1d_policy_and_value_on_wealth_dj, + interp1d_value_on_wealth_dj, +) +from dcegm.interpolation.interp2d_irregular import ( interp2d_policy_and_value_on_wealth_and_regular_grid, - interp2d_policy_on_wealth_and_regular_grid, - interp2d_value_on_wealth_and_regular_grid, +) +from dcegm.interpolation.interpnd_regular import ( + interpnd_policy_and_value_for_child_states_on_regular_grids, ) @@ -17,35 +24,70 @@ def interpolate_value_for_state_and_choice( params, model_config, model_funcs, + model_structure, ): """Interpolate the value for a state and choice given the respective grids.""" continuous_states_info = model_config["continuous_states_info"] + upper_envelope_method = model_config["upper_envelope"]["method"] discount_factor = model_funcs["read_funcs"]["discount_factor"](params) compute_utility = model_funcs["compute_utility"] - if continuous_states_info["second_continuous_exists"]: - second_continuous = state_choice_vec[ - continuous_states_info["second_continuous_state_name"] - ] + multidim = continuous_states_info["has_additional_continuous_state"] + + continuous_state_space = model_structure["continuous_state_space"] - value = interp2d_value_on_wealth_and_regular_grid( - regular_grid=continuous_states_info["second_continuous_grid"], + if (upper_envelope_method == "fues") & multidim: + continuous_state_name = continuous_states_info[ + "additional_continuous_state_names" + ][0] + continuous_state = state_choice_vec[continuous_state_name] + + _, value = interp2d_policy_and_value_on_wealth_and_regular_grid( + continuous_state_space=continuous_state_space, wealth_grid=endog_grid_state_choice, + policy_grid=endog_grid_state_choice, value_grid=value_grid_state_choice, - regular_point_to_interp=second_continuous, + regular_point_to_interp=continuous_state, wealth_point_to_interp=state_choice_vec["assets_begin_of_period"], compute_utility=compute_utility, state_choice_vec=state_choice_vec, params=params, discount_factor=discount_factor, ) - else: + elif (upper_envelope_method == "druedahl_jorgensen") & multidim: + _, value = _interp_policy_and_value_multidim_dj_for_state_choice( + policy_grid_state_choice=value_grid_state_choice, + value_grid_state_choice=value_grid_state_choice, + endog_grid_state_choice=endog_grid_state_choice, + state_choice_vec=state_choice_vec, + additional_continuous_state_grids=continuous_states_info[ + "additional_continuous_state_grids" + ], + continuous_state_space=continuous_state_space, + continuous_state_names=continuous_states_info[ + "additional_continuous_state_names" + ], + compute_utility=compute_utility, + params=params, + discount_factor=discount_factor, + ) + elif upper_envelope_method == "druedahl_jorgensen": + value = interp1d_value_on_wealth_dj( + wealth=state_choice_vec["assets_begin_of_period"], + wealth_grid=endog_grid_state_choice[0], + value_grid=value_grid_state_choice[0], + compute_utility=compute_utility, + state_choice_vec=state_choice_vec, + params=params, + discount_factor=discount_factor, + ) + else: value = interp_value_on_wealth( wealth=state_choice_vec["assets_begin_of_period"], - wealth_grid=endog_grid_state_choice, - value=value_grid_state_choice, + wealth_grid=endog_grid_state_choice[0], + value=value_grid_state_choice[0], compute_utility=compute_utility, state_choice_vec=state_choice_vec, params=params, @@ -56,31 +98,64 @@ def interpolate_value_for_state_and_choice( def interpolate_policy_for_state_and_choice( policy_grid_state_choice, + value_grid_state_choice, endog_grid_state_choice, state_choice_vec, + params, model_config, + model_funcs, + model_structure, ): """Interpolate the value for a state and choice given the respective grids.""" continuous_states_info = model_config["continuous_states_info"] + upper_envelope_method = model_config["upper_envelope"]["method"] + multidim = continuous_states_info["has_additional_continuous_state"] + continuous_state_space = model_structure["continuous_state_space"] - if continuous_states_info["second_continuous_exists"]: - second_continuous = state_choice_vec[ - continuous_states_info["second_continuous_state_name"] - ] - - policy = interp2d_policy_on_wealth_and_regular_grid( - regular_grid=continuous_states_info["second_continuous_grid"], + if (upper_envelope_method == "fues") & multidim: + continuous_state_name = continuous_states_info[ + "additional_continuous_state_names" + ][0] + continuous_state = state_choice_vec[continuous_state_name] + policy, _ = interp2d_policy_and_value_on_wealth_and_regular_grid( + continuous_state_space=continuous_state_space, wealth_grid=endog_grid_state_choice, policy_grid=policy_grid_state_choice, - regular_point_to_interp=second_continuous, + value_grid=policy_grid_state_choice, + regular_point_to_interp=continuous_state, wealth_point_to_interp=state_choice_vec["assets_begin_of_period"], + compute_utility=lambda consumption, params, **kwargs: consumption, + state_choice_vec=state_choice_vec, + params={}, + discount_factor=0.0, + ) + elif (upper_envelope_method == "druedahl_jorgensen") & multidim: + policy, _ = interpolate_policy_and_value_for_state_and_choice( + value_grid_state_choice=value_grid_state_choice, + policy_grid_state_choice=policy_grid_state_choice, + endog_grid_state_choice=endog_grid_state_choice, + state_choice_vec=state_choice_vec, + params=params, + model_config=model_config, + model_funcs=model_funcs, + model_structure=model_structure, + ) + elif upper_envelope_method == "druedahl_jorgensen": + policy, _ = interp1d_policy_and_value_on_wealth_dj( + wealth=state_choice_vec["assets_begin_of_period"], + wealth_grid=endog_grid_state_choice[0], + policy_grid=policy_grid_state_choice[0], + value_grid=value_grid_state_choice[0], + compute_utility=model_funcs["compute_utility"], + state_choice_vec=state_choice_vec, + params=params, + discount_factor=model_funcs["read_funcs"]["discount_factor"](params), ) - else: policy = interp_policy_on_wealth( wealth=state_choice_vec["assets_begin_of_period"], - endog_grid=endog_grid_state_choice, - policy=policy_grid_state_choice, + endog_grid=endog_grid_state_choice[0], + policy=policy_grid_state_choice[0], ) return policy @@ -94,36 +169,68 @@ def interpolate_policy_and_value_for_state_and_choice( params, model_config, model_funcs, + model_structure, ): continuous_states_info = model_config["continuous_states_info"] + upper_envelope_method = model_config["upper_envelope"]["method"] compute_utility = model_funcs["compute_utility"] discount_factor = model_funcs["read_funcs"]["discount_factor"](params) + continuous_state_space = model_structure["continuous_state_space"] - if continuous_states_info["second_continuous_exists"]: - - second_continuous = state_choice_vec[ - continuous_states_info["second_continuous_state_name"] - ] + multidim = continuous_states_info["has_additional_continuous_state"] + if (upper_envelope_method == "fues") & multidim: + continuous_state_name = continuous_states_info[ + "additional_continuous_state_names" + ][0] + continuous_state = state_choice_vec[continuous_state_name] policy, value = interp2d_policy_and_value_on_wealth_and_regular_grid( - regular_grid=continuous_states_info["second_continuous_grid"], + continuous_state_space=continuous_state_space, wealth_grid=endog_grid_state_choice, policy_grid=policy_grid_state_choice, value_grid=value_grid_state_choice, - regular_point_to_interp=second_continuous, + regular_point_to_interp=continuous_state, wealth_point_to_interp=state_choice_vec["assets_begin_of_period"], compute_utility=compute_utility, state_choice_vec=state_choice_vec, params=params, discount_factor=discount_factor, ) + elif (upper_envelope_method == "druedahl_jorgensen") & multidim: + policy, value = _interp_policy_and_value_multidim_dj_for_state_choice( + policy_grid_state_choice=policy_grid_state_choice, + value_grid_state_choice=value_grid_state_choice, + endog_grid_state_choice=endog_grid_state_choice, + state_choice_vec=state_choice_vec, + additional_continuous_state_grids=continuous_states_info[ + "additional_continuous_state_grids" + ], + continuous_state_space=continuous_state_space, + continuous_state_names=continuous_states_info[ + "additional_continuous_state_names" + ], + compute_utility=compute_utility, + params=params, + discount_factor=discount_factor, + ) + elif upper_envelope_method == "druedahl_jorgensen": + policy, value = interp1d_policy_and_value_on_wealth_dj( + wealth=state_choice_vec["assets_begin_of_period"], + wealth_grid=endog_grid_state_choice[0], + policy_grid=policy_grid_state_choice[0], + value_grid=value_grid_state_choice[0], + compute_utility=compute_utility, + state_choice_vec=state_choice_vec, + params=params, + discount_factor=discount_factor, + ) else: policy, value = interp1d_policy_and_value_on_wealth( wealth=state_choice_vec["assets_begin_of_period"], - wealth_grid=endog_grid_state_choice, - policy_grid=policy_grid_state_choice, - value_grid=value_grid_state_choice, + wealth_grid=endog_grid_state_choice[0], + policy_grid=policy_grid_state_choice[0], + value_grid=value_grid_state_choice[0], compute_utility=compute_utility, state_choice_vec=state_choice_vec, params=params, @@ -131,3 +238,66 @@ def interpolate_policy_and_value_for_state_and_choice( ) return policy, value + + +def _interp_policy_and_value_multidim_dj_for_state_choice( + policy_grid_state_choice, + value_grid_state_choice, + endog_grid_state_choice, + state_choice_vec, + additional_continuous_state_grids, + continuous_state_space, + continuous_state_names, + compute_utility, + params, + discount_factor, +): + continuous_state_child_states = { + name: jnp.asarray(state_choice_vec[name])[None, None] + for name in continuous_state_names + } + state_choice_child_states = { + key: jnp.asarray(value)[None] + for key, value in state_choice_vec.items() + if key not in {"assets_begin_of_period", *continuous_state_names} + } + policy_nd, value_nd = interpnd_policy_and_value_for_child_states_on_regular_grids( + additional_continuous_state_grids=additional_continuous_state_grids, + wealth_grid=endog_grid_state_choice[0], + policy_grid_child_states=policy_grid_state_choice[None, ...], + value_grid_child_states=value_grid_state_choice[None, ...], + continuous_state_child_states=continuous_state_child_states, + wealth_child_states=jnp.asarray(state_choice_vec["assets_begin_of_period"])[ + None, None, None, None + ], + state_choice_child_states=state_choice_child_states, + compute_utility=compute_utility, + params=params, + discount_factor=discount_factor, + ) + policy_nd = policy_nd[0, 0, 0, 0] + value_nd = value_nd[0, 0, 0, 0] + + exact_mask = jnp.ones_like(next(iter(continuous_state_space.values())), dtype=bool) + for name in continuous_state_names: + exact_mask = exact_mask & jnp.isclose( + continuous_state_space[name], + state_choice_vec[name], + ) + has_exact_combo = jnp.any(exact_mask) + combo_idx = jnp.argmax(exact_mask) + + policy_exact, value_exact = interp1d_policy_and_value_on_wealth_dj( + wealth=state_choice_vec["assets_begin_of_period"], + wealth_grid=endog_grid_state_choice[combo_idx], + policy_grid=policy_grid_state_choice[combo_idx], + value_grid=value_grid_state_choice[combo_idx], + compute_utility=compute_utility, + state_choice_vec=state_choice_vec, + params=params, + discount_factor=discount_factor, + ) + + policy = jnp.where(has_exact_combo, policy_exact, policy_nd) + value = jnp.where(has_exact_combo, value_exact, value_nd) + return policy, value diff --git a/src/dcegm/interpolation/interpnd_regular.py b/src/dcegm/interpolation/interpnd_regular.py new file mode 100644 index 00000000..deb8ce50 --- /dev/null +++ b/src/dcegm/interpolation/interpnd_regular.py @@ -0,0 +1,601 @@ +"""N-dimensional regular-grid interpolation for policy/value. + +Assumptions: +- Shared 1D wealth grid across all regular-grid combinations. +- Child-state policy/value grids are flattened in regular dimensions: + ``(n_child_state_choices, n_continuous_combinations, n_wealth)``. +- Child-state interpolation points are provided as + ``continuous_state_child_states[name]`` with shape + ``(n_child_state_choices, n_continuous_combinations)``. + +""" + +from typing import Any, Callable, Dict + +import jax.numpy as jnp +from jax import vmap + +from dcegm.interpolation.interp1d import ( + get_index_high_and_low, + linear_interpolation_formula, +) + + +def interpnd_policy_for_child_states_on_regular_grids( + additional_continuous_state_grids: Dict[str, jnp.ndarray], + wealth_grid: jnp.ndarray, + policy_grid_child_states: jnp.ndarray, + value_grid_child_states: jnp.ndarray, + continuous_state_child_states: Dict[str, jnp.ndarray], + wealth_child_states: jnp.ndarray, + state_choice_child_states: Dict[str, Any], + compute_utility: Callable, + params: Dict[str, Any], + discount_factor: float, +) -> jnp.ndarray: + """Interpolate policy, using value-based overwrite logic. + + Returns shape + ``(n_child_state_choices, n_continuous_combinations, n_wealth, n_quad_points)``. + + """ + policy, _ = interpnd_policy_and_value_for_child_states_on_regular_grids( + additional_continuous_state_grids=additional_continuous_state_grids, + wealth_grid=wealth_grid, + policy_grid_child_states=policy_grid_child_states, + value_grid_child_states=value_grid_child_states, + continuous_state_child_states=continuous_state_child_states, + wealth_child_states=wealth_child_states, + state_choice_child_states=state_choice_child_states, + compute_utility=compute_utility, + params=params, + discount_factor=discount_factor, + ) + return policy + + +def interpnd_policy_and_value_for_child_states_on_regular_grids( + additional_continuous_state_grids: Dict[str, jnp.ndarray], + wealth_grid: jnp.ndarray, + policy_grid_child_states: jnp.ndarray, + value_grid_child_states: jnp.ndarray, + continuous_state_child_states: Dict[str, jnp.ndarray], + wealth_child_states: jnp.ndarray, + state_choice_child_states: Dict[str, Any], + compute_utility: Callable, + params: Dict[str, Any], + discount_factor: float, +) -> tuple[jnp.ndarray, jnp.ndarray]: + """Interpolate policy/value and apply consume-all overwrite. + + If consume-all value dominates interpolated value at a point, overwrite policy with + consume-all policy (=wealth point). + + """ + objs = _precompute_interp_objects( + additional_continuous_state_grids=additional_continuous_state_grids, + continuous_state_child_states=continuous_state_child_states, + wealth_grid=wealth_grid, + wealth_child_states=wealth_child_states, + ) + + def _interp_one_child_state( + policy_grid_one_child, + value_grid_one_child, + regular_low_idxs_one_child, + regular_high_idxs_one_child, + regular_low_weights_one_child, + regular_high_weights_one_child, + wealth_points_one_child, + wealth_low_idxs_one_child, + wealth_high_idxs_one_child, + ): + return vmap( + _interp_policy_and_value_one_comb, + in_axes=( + None, + None, + 1, + 1, + 1, + 1, + 0, + 0, + 0, + None, + None, + None, + ), + )( + policy_grid_one_child, + value_grid_one_child, + regular_low_idxs_one_child, + regular_high_idxs_one_child, + regular_low_weights_one_child, + regular_high_weights_one_child, + wealth_points_one_child, + wealth_low_idxs_one_child, + wealth_high_idxs_one_child, + objs["strides"], + objs["corner_table"], + wealth_grid, + ) + + policy_interp, value_interp = vmap( + _interp_one_child_state, + in_axes=(0, 0, 1, 1, 1, 1, 0, 0, 0), + )( + policy_grid_child_states, + value_grid_child_states, + objs["regular_low_idxs"], + objs["regular_high_idxs"], + objs["regular_low_weights"], + objs["regular_high_weights"], + wealth_child_states, + objs["wealth_low_idxs"], + objs["wealth_high_idxs"], + ) + + # We need to interpolate the expected value at zero savings, because we only know it for the regular + # grid corners + expected_value_zero_savings = _interp_regular_only_all( + values_over_regular_grid_child_states=value_grid_child_states[..., 0], + regular_low_idxs=objs["regular_low_idxs"], + regular_high_idxs=objs["regular_high_idxs"], + regular_low_weights=objs["regular_low_weights"], + regular_high_weights=objs["regular_high_weights"], + strides=objs["strides"], + corner_table=objs["corner_table"], + ) + + consume_all_value = _compute_consume_all_value( + expected_value_zero_savings=expected_value_zero_savings, + wealth_child_states=wealth_child_states, + state_choice_child_states=state_choice_child_states, + continuous_state_child_states=continuous_state_child_states, + compute_utility=compute_utility, + params=params, + discount_factor=discount_factor, + ) + + overwrite_mask = consume_all_value > value_interp + policy_final = jnp.where(overwrite_mask, wealth_child_states, policy_interp) + value_final = jnp.where(overwrite_mask, consume_all_value, value_interp) + return policy_final, value_final + + +def interpnd_value_for_child_states_on_regular_grids( + additional_continuous_state_grids: Dict[str, jnp.ndarray], + wealth_grid: jnp.ndarray, + value_grid_child_states: jnp.ndarray, + continuous_state_child_states: Dict[str, jnp.ndarray], + wealth_child_states: jnp.ndarray, + state_choice_child_states: Dict[str, Any], + compute_utility: Callable, + params: Dict[str, Any], + discount_factor: float, +) -> jnp.ndarray: + """Interpolate value and apply consume-all overwrite. + + Returns shape + ``(n_child_state_choices, n_continuous_combinations, n_wealth, n_quad_points)``. + + """ + objs = _precompute_interp_objects( + additional_continuous_state_grids=additional_continuous_state_grids, + continuous_state_child_states=continuous_state_child_states, + wealth_grid=wealth_grid, + wealth_child_states=wealth_child_states, + ) + + def _interp_one_child_state( + value_grid_one_child, + regular_low_idxs_one_child, + regular_high_idxs_one_child, + regular_low_weights_one_child, + regular_high_weights_one_child, + wealth_points_one_child, + wealth_low_idxs_one_child, + wealth_high_idxs_one_child, + ): + def _interp_one_comb( + regular_low_idxs_one_comb, + regular_high_idxs_one_comb, + regular_low_weights_one_comb, + regular_high_weights_one_comb, + wealth_points_one_comb, + wealth_low_idxs_one_comb, + wealth_high_idxs_one_comb, + ): + corner_linear_idxs, corner_weights = _corner_linear_indices_and_weights( + regular_low_idxs_one_comb=regular_low_idxs_one_comb, + regular_high_idxs_one_comb=regular_high_idxs_one_comb, + regular_low_weights_one_comb=regular_low_weights_one_comb, + regular_high_weights_one_comb=regular_high_weights_one_comb, + strides=objs["strides"], + corner_table=objs["corner_table"], + ) + return _interp_single_grid_one_comb( + grid_one_child=value_grid_one_child, + corner_linear_idxs=corner_linear_idxs, + corner_weights=corner_weights, + wealth_points_one_comb=wealth_points_one_comb, + wealth_low_idxs_one_comb=wealth_low_idxs_one_comb, + wealth_high_idxs_one_comb=wealth_high_idxs_one_comb, + wealth_grid=wealth_grid, + ) + + return vmap( + _interp_one_comb, + in_axes=(1, 1, 1, 1, 0, 0, 0), + )( + regular_low_idxs_one_child, + regular_high_idxs_one_child, + regular_low_weights_one_child, + regular_high_weights_one_child, + wealth_points_one_child, + wealth_low_idxs_one_child, + wealth_high_idxs_one_child, + ) + + value_interp = vmap( + _interp_one_child_state, + in_axes=(0, 1, 1, 1, 1, 0, 0, 0), + )( + value_grid_child_states, + objs["regular_low_idxs"], + objs["regular_high_idxs"], + objs["regular_low_weights"], + objs["regular_high_weights"], + wealth_child_states, + objs["wealth_low_idxs"], + objs["wealth_high_idxs"], + ) + + expected_value_zero_savings = _interp_regular_only_all( + values_over_regular_grid_child_states=value_grid_child_states[..., 0], + regular_low_idxs=objs["regular_low_idxs"], + regular_high_idxs=objs["regular_high_idxs"], + regular_low_weights=objs["regular_low_weights"], + regular_high_weights=objs["regular_high_weights"], + strides=objs["strides"], + corner_table=objs["corner_table"], + ) + + consume_all_value = _compute_consume_all_value( + expected_value_zero_savings=expected_value_zero_savings, + wealth_child_states=wealth_child_states, + state_choice_child_states=state_choice_child_states, + continuous_state_child_states=continuous_state_child_states, + compute_utility=compute_utility, + params=params, + discount_factor=discount_factor, + ) + + return jnp.asarray( + jnp.where(consume_all_value > value_interp, consume_all_value, value_interp) + ) + + +def _compute_consume_all_value( + expected_value_zero_savings: jnp.ndarray, + wealth_child_states: jnp.ndarray, + state_choice_child_states: Dict[str, Any], + continuous_state_child_states: Dict[str, jnp.ndarray], + compute_utility: Callable, + params: Dict[str, Any], + discount_factor: float, +) -> jnp.ndarray: + + def _utility_at_point( + consumption_point: jnp.ndarray, + state_choice_point: Dict[str, jnp.ndarray], + continuous_state_point: Dict[str, jnp.ndarray], + ) -> jnp.ndarray: + out = compute_utility( + consumption=consumption_point, + params=params, + **state_choice_point, + **continuous_state_point, + ) + return out + + consume_all_utility = vmap( + vmap( + vmap( + vmap( + _utility_at_point, + in_axes=(0, None, None), + ), + in_axes=(0, None, None), + ), + in_axes=(0, None, 0), + ), + in_axes=(0, 0, 0), + )( + wealth_child_states, + state_choice_child_states, + continuous_state_child_states, + ) + + expected_value_zero_savings = expected_value_zero_savings[:, :, None, None] + return consume_all_utility + discount_factor * expected_value_zero_savings + + +def _interp_policy_and_value_one_comb( + policy_grid_one_child: jnp.ndarray, + value_grid_one_child: jnp.ndarray, + regular_low_idxs_one_comb: jnp.ndarray, + regular_high_idxs_one_comb: jnp.ndarray, + regular_low_weights_one_comb: jnp.ndarray, + regular_high_weights_one_comb: jnp.ndarray, + wealth_points_one_comb: jnp.ndarray, + wealth_low_idxs_one_comb: jnp.ndarray, + wealth_high_idxs_one_comb: jnp.ndarray, + strides: jnp.ndarray, + corner_table: jnp.ndarray, + wealth_grid: jnp.ndarray, +) -> tuple[jnp.ndarray, jnp.ndarray]: + corner_linear_idxs, corner_weights = _corner_linear_indices_and_weights( + regular_low_idxs_one_comb=regular_low_idxs_one_comb, + regular_high_idxs_one_comb=regular_high_idxs_one_comb, + regular_low_weights_one_comb=regular_low_weights_one_comb, + regular_high_weights_one_comb=regular_high_weights_one_comb, + strides=strides, + corner_table=corner_table, + ) + + policy_interp = _interp_single_grid_one_comb( + grid_one_child=policy_grid_one_child, + corner_linear_idxs=corner_linear_idxs, + corner_weights=corner_weights, + wealth_points_one_comb=wealth_points_one_comb, + wealth_low_idxs_one_comb=wealth_low_idxs_one_comb, + wealth_high_idxs_one_comb=wealth_high_idxs_one_comb, + wealth_grid=wealth_grid, + ) + value_interp = _interp_single_grid_one_comb( + grid_one_child=value_grid_one_child, + corner_linear_idxs=corner_linear_idxs, + corner_weights=corner_weights, + wealth_points_one_comb=wealth_points_one_comb, + wealth_low_idxs_one_comb=wealth_low_idxs_one_comb, + wealth_high_idxs_one_comb=wealth_high_idxs_one_comb, + wealth_grid=wealth_grid, + ) + return policy_interp, value_interp + + +def _interp_single_grid_one_comb( + grid_one_child: jnp.ndarray, + corner_linear_idxs: jnp.ndarray, + corner_weights: jnp.ndarray, + wealth_points_one_comb: jnp.ndarray, + wealth_low_idxs_one_comb: jnp.ndarray, + wealth_high_idxs_one_comb: jnp.ndarray, + wealth_grid: jnp.ndarray, +) -> jnp.ndarray: + corner_rows = grid_one_child[corner_linear_idxs] + corner_values = vmap( + _interp_wealth_for_corner, + in_axes=(0, None, None, None, None), + )( + corner_rows, + wealth_points_one_comb, + wealth_low_idxs_one_comb, + wealth_high_idxs_one_comb, + wealth_grid, + ) + return jnp.sum(corner_weights[:, None, None] * corner_values, axis=0) + + +def _interp_wealth_for_corner( + grid_row: jnp.ndarray, + wealth_points_one_comb: jnp.ndarray, + wealth_low_idxs_one_comb: jnp.ndarray, + wealth_high_idxs_one_comb: jnp.ndarray, + wealth_grid: jnp.ndarray, +) -> jnp.ndarray: + high = _take_last_axis(grid_row, wealth_high_idxs_one_comb) + low = _take_last_axis(grid_row, wealth_low_idxs_one_comb) + out = linear_interpolation_formula( + y_high=high, + y_low=low, + x_high=wealth_grid[wealth_high_idxs_one_comb], + x_low=wealth_grid[wealth_low_idxs_one_comb], + x_new=wealth_points_one_comb, + ) + return jnp.asarray(out) + + +def _interp_regular_only_all( + values_over_regular_grid_child_states: jnp.ndarray, + regular_low_idxs: jnp.ndarray, + regular_high_idxs: jnp.ndarray, + regular_low_weights: jnp.ndarray, + regular_high_weights: jnp.ndarray, + strides: jnp.ndarray, + corner_table: jnp.ndarray, +) -> jnp.ndarray: + """Interpolate values over regular dimensions only.""" + + def _interp_one_child_state( + values_over_regular_grid_one_child, + regular_low_idxs_one_child, + regular_high_idxs_one_child, + regular_low_weights_one_child, + regular_high_weights_one_child, + ): + return vmap( + _interp_regular_only, + in_axes=(None, 1, 1, 1, 1, None, None), + )( + values_over_regular_grid_one_child, + regular_low_idxs_one_child, + regular_high_idxs_one_child, + regular_low_weights_one_child, + regular_high_weights_one_child, + strides, + corner_table, + ) + + return vmap( + _interp_one_child_state, + in_axes=(0, 1, 1, 1, 1), + )( + values_over_regular_grid_child_states, + regular_low_idxs, + regular_high_idxs, + regular_low_weights, + regular_high_weights, + ) + + +def _corner_linear_indices_and_weights( + regular_low_idxs_one_comb: jnp.ndarray, + regular_high_idxs_one_comb: jnp.ndarray, + regular_low_weights_one_comb: jnp.ndarray, + regular_high_weights_one_comb: jnp.ndarray, + strides: jnp.ndarray, + corner_table: jnp.ndarray, +) -> tuple[jnp.ndarray, jnp.ndarray]: + choose_high = corner_table.astype(bool) + selected_idxs = jnp.where( + choose_high, + regular_high_idxs_one_comb[None, :], + regular_low_idxs_one_comb[None, :], + ) + selected_weights = jnp.where( + choose_high, + regular_high_weights_one_comb[None, :], + regular_low_weights_one_comb[None, :], + ) + corner_linear_idxs = jnp.sum(selected_idxs * strides[None, :], axis=1) + corner_weights = jnp.prod(selected_weights, axis=1) + return corner_linear_idxs, corner_weights + + +def _interp_regular_only( + values_over_regular_grid: jnp.ndarray, + regular_low_idxs_one_comb: jnp.ndarray, + regular_high_idxs_one_comb: jnp.ndarray, + regular_low_weights_one_comb: jnp.ndarray, + regular_high_weights_one_comb: jnp.ndarray, + strides: jnp.ndarray, + corner_table: jnp.ndarray, +) -> jnp.ndarray: + corner_linear_idxs, corner_weights = _corner_linear_indices_and_weights( + regular_low_idxs_one_comb=regular_low_idxs_one_comb, + regular_high_idxs_one_comb=regular_high_idxs_one_comb, + regular_low_weights_one_comb=regular_low_weights_one_comb, + regular_high_weights_one_comb=regular_high_weights_one_comb, + strides=strides, + corner_table=corner_table, + ) + corner_vals = values_over_regular_grid[corner_linear_idxs] + return jnp.sum(corner_weights * corner_vals) + + +def _precompute_interp_objects( + additional_continuous_state_grids: Dict[str, jnp.ndarray], + continuous_state_child_states: Dict[str, jnp.ndarray], + wealth_grid: jnp.ndarray, + wealth_child_states: jnp.ndarray, +) -> Dict[str, jnp.ndarray]: + """Precompute reusable interpolation objects for policy/value paths.""" + state_names = list(additional_continuous_state_grids.keys()) + regular_shape = [ + int(additional_continuous_state_grids[name].shape[0]) for name in state_names + ] + strides = jnp.asarray(_compute_row_major_strides(regular_shape), dtype=jnp.int32) + regular_low_idxs, regular_high_idxs, regular_low_weights, regular_high_weights = ( + _precompute_regular_indices_and_weights( + additional_continuous_state_grids=additional_continuous_state_grids, + continuous_state_child_states=continuous_state_child_states, + state_names=state_names, + ) + ) + wealth_high_idxs, wealth_low_idxs = get_index_high_and_low( + wealth_grid, wealth_child_states + ) + corner_table = _corner_table(len(state_names)) + return { + "strides": strides, + "regular_low_idxs": regular_low_idxs, + "regular_high_idxs": regular_high_idxs, + "regular_low_weights": regular_low_weights, + "regular_high_weights": regular_high_weights, + "wealth_low_idxs": wealth_low_idxs, + "wealth_high_idxs": wealth_high_idxs, + "corner_table": corner_table, + } + + +def _precompute_regular_indices_and_weights( + additional_continuous_state_grids: Dict[str, jnp.ndarray], + continuous_state_child_states: Dict[str, jnp.ndarray], + state_names, +): + """Precompute low/high idx and weights for all regular child points. + + Returns arrays of shape + ``(n_dims, n_child_state_choices, n_continuous_combinations)``. + + """ + low_idxs = [] + high_idxs = [] + low_weights = [] + high_weights = [] + + for name in state_names: + grid_1d = additional_continuous_state_grids[name] + points = continuous_state_child_states[name] + high_idx, low_idx = get_index_high_and_low(grid_1d, points) + x_low = grid_1d[low_idx] + x_high = grid_1d[high_idx] + high_w = (points - x_low) / (x_high - x_low) + low_w = 1.0 - high_w + low_idxs.append(low_idx) + high_idxs.append(high_idx) + low_weights.append(low_w) + high_weights.append(high_w) + + return ( + jnp.stack(low_idxs, axis=0), + jnp.stack(high_idxs, axis=0), + jnp.stack(low_weights, axis=0), + jnp.stack(high_weights, axis=0), + ) + + +def _take_last_axis(arr, idx): + """Take along last axis with batched indices.""" + if arr.ndim != idx.ndim + 1: + n_missing = idx.ndim - (arr.ndim - 1) + arr = arr.reshape(arr.shape[:-1] + (1,) * n_missing + (arr.shape[-1],)) + arr = jnp.broadcast_to(arr, idx.shape + (arr.shape[-1],)) + return jnp.take_along_axis(arr, idx[..., None], axis=-1)[..., 0] + + +def _compute_row_major_strides(shape): + strides = [1] * len(shape) + running = 1 + for i in range(len(shape) - 1, -1, -1): + strides[i] = running + running *= shape[i] + return strides + + +def _corner_table(n_dims: int) -> jnp.ndarray: + """Return binary corner table of shape ``(2**n_dims, n_dims)``. + + Pseudo-code equivalent: + + for corner in range(2**n_dims): + for dim in range(n_dims): + table[corner, dim] = (corner >> dim) & 1 + + """ + corners = jnp.arange(2**n_dims, dtype=jnp.int32) + shifts = jnp.arange(n_dims, dtype=jnp.int32) + return ((corners[:, None] >> shifts[None, :]) & 1).astype(jnp.int32) diff --git a/src/dcegm/interpolation/simulation_interp.py b/src/dcegm/interpolation/simulation_interp.py new file mode 100644 index 00000000..a33fb9b9 --- /dev/null +++ b/src/dcegm/interpolation/simulation_interp.py @@ -0,0 +1,386 @@ +from jax import numpy as jnp +from jax import vmap + +from dcegm.interfaces.index_functions import get_state_choice_index_per_discrete_states +from dcegm.interpolation.interp1d import interp1d_policy_and_value_on_wealth +from dcegm.interpolation.interp1d_dj import interp1d_policy_and_value_on_wealth_dj +from dcegm.interpolation.interp2d_irregular import ( + interp2d_policy_and_value_on_wealth_and_regular_grid, +) +from dcegm.interpolation.interpnd_regular import ( + interpnd_policy_and_value_for_child_states_on_regular_grids, +) + + +def interpolate_policy_and_value_for_all_agents( + discrete_states_beginning_of_period, + continuous_state_beginning_of_period, + assets_begin_of_period, + value_solved, + policy_solved, + endog_grid_solved, + map_state_choice_to_index, + choice_range, + params, + discrete_states_names, + compute_utility, + continuous_state_space, + additional_continuous_state_grids, + upper_envelope_method, + has_additional_continuous_state, + discount_factor, +): + + # 1D interpolation path is independent of upper-envelope method and only + # depends on whether an additional continuous state exists. + if not has_additional_continuous_state: + discrete_state_choice_indexes = get_state_choice_index_per_discrete_states( + states=discrete_states_beginning_of_period, + map_state_choice_to_index=map_state_choice_to_index, + discrete_states_names=discrete_states_names, + ) + + value_grid_agent = jnp.take( + value_solved, + discrete_state_choice_indexes, + axis=0, + mode="fill", + fill_value=jnp.nan, + )[:, :, 0, :] + policy_grid_agent = jnp.take( + policy_solved, discrete_state_choice_indexes, axis=0 + )[:, :, 0, :] + endog_grid_agent = jnp.take( + endog_grid_solved, discrete_state_choice_indexes, axis=0 + )[:, :, 0, :] + + vectorized_interp = vmap( + vmap( + interp1d_policy_and_value_function, + in_axes=( + None, + None, + 0, + 0, + 0, + 0, + None, + None, + None, + None, + ), + ), + in_axes=(0, 0, 0, 0, 0, None, None, None, None, None), + ) + + policy_agent, value_agent = vectorized_interp( + assets_begin_of_period, + discrete_states_beginning_of_period, + endog_grid_agent, + value_grid_agent, + policy_grid_agent, + choice_range, + params, + compute_utility, + discount_factor, + upper_envelope_method == "druedahl_jorgensen", + ) + + return policy_agent, value_agent + + if upper_envelope_method == "fues": + + discrete_state_choice_indexes = get_state_choice_index_per_discrete_states( + states=discrete_states_beginning_of_period, + map_state_choice_to_index=map_state_choice_to_index, + discrete_states_names=discrete_states_names, + ) + + value_grid_agent = jnp.take( + value_solved, + discrete_state_choice_indexes, + axis=0, + mode="fill", + fill_value=jnp.nan, + ) + policy_grid_agent = jnp.take( + policy_solved, discrete_state_choice_indexes, axis=0 + ) + endog_grid_agent = jnp.take( + endog_grid_solved, discrete_state_choice_indexes, axis=0 + ) + + continuous_state_name = list(continuous_state_beginning_of_period.keys())[0] + + vectorized_interp = vmap( + vmap( + interp2d_policy_and_value_function, + in_axes=( + None, + None, + None, + 0, + 0, + 0, + 0, + None, + None, + None, + None, + None, + ), # choices + ), + in_axes=( + 0, + 0, + 0, + 0, + 0, + 0, + None, + None, + None, + None, + None, + None, + ), + ) + + # ================================================================================= + + policy_agent, value_agent = vectorized_interp( + assets_begin_of_period, + continuous_state_beginning_of_period, + discrete_states_beginning_of_period, + endog_grid_agent, + value_grid_agent, + policy_grid_agent, + choice_range, + continuous_state_space, + continuous_state_name, + params, + compute_utility, + discount_factor, + ) + + return policy_agent, value_agent + + if upper_envelope_method == "druedahl_jorgensen": + discrete_state_choice_indexes = get_state_choice_index_per_discrete_states( + states=discrete_states_beginning_of_period, + map_state_choice_to_index=map_state_choice_to_index, + discrete_states_names=discrete_states_names, + ) + + value_grid_agent = jnp.take( + value_solved, + discrete_state_choice_indexes, + axis=0, + mode="fill", + fill_value=jnp.nan, + ) + policy_grid_agent = jnp.take( + policy_solved, discrete_state_choice_indexes, axis=0 + ) + endog_grid_agent = jnp.take( + endog_grid_solved, discrete_state_choice_indexes, axis=0 + ) + + additional_continuous_state_names = list(continuous_state_space.keys()) + + vectorized_interp = vmap( + vmap( + interpnd_policy_and_value_function, + in_axes=( + None, + None, + None, + 0, + 0, + 0, + 0, + None, + None, + None, + None, + None, + None, + ), + ), + in_axes=( + 0, + 0, + 0, + 0, + 0, + 0, + None, + None, + None, + None, + None, + None, + None, + ), + ) + + policy_agent, value_agent = vectorized_interp( + assets_begin_of_period, + continuous_state_beginning_of_period, + discrete_states_beginning_of_period, + endog_grid_agent, + value_grid_agent, + policy_grid_agent, + choice_range, + continuous_state_space, + additional_continuous_state_grids, + additional_continuous_state_names, + params, + compute_utility, + discount_factor, + ) + + return policy_agent, value_agent + + raise ValueError( + "Unknown upper envelope method. Use 'fues' or 'druedahl_jorgensen'." + ) + + +def interp1d_policy_and_value_function( + wealth_beginning_of_period, + state, + endog_grid_agent, + value_agent, + policy_agent, + choice, + params, + compute_utility, + discount_factor, + use_dj_interpolation, +): + state_choice_vec = {**state, "choice": choice} + + if use_dj_interpolation: + policy_interp, value_interp = interp1d_policy_and_value_on_wealth_dj( + wealth=wealth_beginning_of_period, + wealth_grid=endog_grid_agent, + policy_grid=policy_agent, + value_grid=value_agent, + compute_utility=compute_utility, + state_choice_vec=state_choice_vec, + params=params, + discount_factor=discount_factor, + ) + else: + policy_interp, value_interp = interp1d_policy_and_value_on_wealth( + wealth=wealth_beginning_of_period, + wealth_grid=endog_grid_agent, + policy_grid=policy_agent, + value_grid=value_agent, + compute_utility=compute_utility, + state_choice_vec=state_choice_vec, + params=params, + discount_factor=discount_factor, + ) + + return policy_interp, value_interp + + +def interp2d_policy_and_value_function( + wealth_beginning_of_period, + continuous_state_beginning_of_period, + state, + endog_grid_agent, + value_agent, + policy_agent, + choice, + continuous_state_space, + continuous_state_name, + params, + compute_utility, + discount_factor, +): + state_choice_vec = {**state, "choice": choice} + + policy_interp, value_interp = interp2d_policy_and_value_on_wealth_and_regular_grid( + continuous_state_space=continuous_state_space, + wealth_grid=endog_grid_agent, + policy_grid=policy_agent, + value_grid=value_agent, + wealth_point_to_interp=wealth_beginning_of_period, + regular_point_to_interp=continuous_state_beginning_of_period[ + continuous_state_name + ], + compute_utility=compute_utility, + state_choice_vec=state_choice_vec, + params=params, + discount_factor=discount_factor, + ) + + return policy_interp, value_interp + + +def interpnd_policy_and_value_function( + wealth_beginning_of_period, + continuous_state_beginning_of_period, + state, + endog_grid_agent, + value_agent, + policy_agent, + choice, + continuous_state_space, + additional_continuous_state_grids, + additional_continuous_state_names, + params, + compute_utility, + discount_factor, +): + state_choice_vec = {**state, "choice": choice} + + continuous_state_child_states = { + name: continuous_state_beginning_of_period[name][None, None] + for name in additional_continuous_state_names + } + state_choice_child_states = { + key: value[None] for key, value in state_choice_vec.items() + } + + policy_interp, value_interp = ( + interpnd_policy_and_value_for_child_states_on_regular_grids( + additional_continuous_state_grids=additional_continuous_state_grids, + wealth_grid=endog_grid_agent[0], + policy_grid_child_states=policy_agent[None, ...], + value_grid_child_states=value_agent[None, ...], + continuous_state_child_states=continuous_state_child_states, + wealth_child_states=wealth_beginning_of_period[None, None, None, None], + state_choice_child_states=state_choice_child_states, + compute_utility=compute_utility, + params=params, + discount_factor=discount_factor, + ) + ) + + exact_mask = jnp.ones_like(next(iter(continuous_state_space.values())), dtype=bool) + for name in additional_continuous_state_names: + exact_mask = exact_mask & jnp.isclose( + continuous_state_space[name], continuous_state_beginning_of_period[name] + ) + has_exact_combo = jnp.any(exact_mask) + combo_idx = jnp.argmax(exact_mask) + + policy_exact, value_exact = interp1d_policy_and_value_on_wealth_dj( + wealth=wealth_beginning_of_period, + wealth_grid=endog_grid_agent[combo_idx], + policy_grid=policy_agent[combo_idx], + value_grid=value_agent[combo_idx], + compute_utility=compute_utility, + state_choice_vec=state_choice_vec, + params=params, + discount_factor=discount_factor, + ) + + policy = jnp.where(has_exact_combo, policy_exact, policy_interp[0, 0, 0, 0]) + value = jnp.where(has_exact_combo, value_exact, value_interp[0, 0, 0, 0]) + + return policy, value diff --git a/src/dcegm/law_of_motion.py b/src/dcegm/law_of_motion.py index b3c28d2c..0f264cd6 100644 --- a/src/dcegm/law_of_motion.py +++ b/src/dcegm/law_of_motion.py @@ -1,3 +1,4 @@ +import jax.numpy as jnp from jax import vmap from dcegm.check_func_outputs import ( @@ -16,6 +17,10 @@ def calc_cont_grids_next_period( continuous_states_info = model_config["continuous_states_info"] state_space_dict = model_structure["state_space_dict"] + has_additional_continuous_states = continuous_states_info[ + "has_additional_continuous_state" + ] + # Scale income shock draws income_shock_mean = model_funcs["read_funcs"]["income_shock_mean"](params) income_shock_std = model_funcs["read_funcs"]["income_shock_std"](params) @@ -23,36 +28,24 @@ def calc_cont_grids_next_period( income_shock_draws_unscaled * income_shock_std + income_shock_mean ) - # Generate result dict - cont_grids_next_period = {} - - if continuous_states_info["second_continuous_exists"]: - continuous_state_next_period = calculate_continuous_state( - discrete_states_beginning_of_period=state_space_dict, - continuous_grid=continuous_states_info["second_continuous_grid"], - params=params, - compute_continuous_state=model_funcs["next_period_continuous_state"], - ) - # Fill in result dict - cont_grids_next_period["second_continuous"] = continuous_state_next_period - - # Prepare dict used to calculate beginning of period assets - state_specific_grids = { - "states": state_space_dict, - "continuous_state": continuous_state_next_period, - } - else: - state_specific_grids = { - "states": state_space_dict, - } + continuous_state_space = model_structure["continuous_state_space"] + continuous_state_next_period = _get_continuous_state_next_period( + has_additional_continuous_states=has_additional_continuous_states, + state_space_dict=state_space_dict, + continuous_state_space=continuous_state_space, + params=params, + model_funcs=model_funcs, + ) def fix_assets_and_shocks_for_broadcast( states, + continuous_state_vec, asset_end_of_previous_period, income_draw, ): + all_states = {**states, **continuous_state_vec} assets_begin_of_period = calc_beginning_of_period_assets_for_single_state( - state_vec=states, + state_vec=all_states, asset_end_of_previous_period=asset_end_of_previous_period, income_shock_draw=income_draw, params=params, @@ -63,50 +56,74 @@ def fix_assets_and_shocks_for_broadcast( ) return assets_begin_of_period - broadcast_function = lambda states: vmap( + assets_begin_of_next_period = vmap( vmap( - fix_assets_and_shocks_for_broadcast, - in_axes=(None, None, 0), # income shocks + vmap( + vmap( + fix_assets_and_shocks_for_broadcast, + in_axes=(None, None, None, 0), + ), + in_axes=(None, None, 0, None), + ), + in_axes=(None, 0, None, None), ), - in_axes=(None, 0, None), # assets + in_axes=(0, 0, None, None), )( - states, + state_space_dict, + continuous_state_next_period, continuous_states_info["assets_grid_end_of_period"], income_shocks_scaled, ) - final_args = () - # Default is no chaining of vmaps. Then I add consequently vmap over specific grids - vmap_chain = broadcast_function - - for grid_name in state_specific_grids.keys(): - if grid_name != "states": - # Use default argument to capture current values - vmap_chain = add_vmap_chain_for_grid(vmap_chain, grid_name) - final_args += (state_specific_grids[grid_name],) - - final_args = (state_specific_grids["states"],) + final_args - assets_begin_of_next_period = vmap(vmap_chain)(*final_args) - cont_grids_next_period["assets_begin_of_period"] = assets_begin_of_next_period - return cont_grids_next_period - + # Generate result dict + return { + "continuous_states": continuous_state_next_period, + "assets_begin_of_period": assets_begin_of_next_period, + } -def add_vmap_chain_for_grid(inner_func, gname): - """The function adds a vmap layer for a specific grid. - It vmaps over the remaining dimension of the grid. So if we have a grid that is - (n_discrete_states, n_grid_points), we can later vmap over the discrete states and - this function will add the n_grid_points dimension to be vmapped over. The function - only expects later the grid to arrive in n_grid_points. So we can also use the - function in the final period calculation. +def _get_continuous_state_next_period( + has_additional_continuous_states, + state_space_dict, + continuous_state_space, + params, + model_funcs, +): + if not has_additional_continuous_states: + # Use an explicit zero-valued dummy continuous state with stable shape + # (n_states, 1) to keep downstream shapes constant. + n_states = next(iter(state_space_dict.values())).shape[0] + dummy_name = "dummy_cont" + dummy_dtype = continuous_state_space[dummy_name].dtype + dummy_states = { + dummy_name: jnp.zeros((n_states, 1), dtype=dummy_dtype), + } + return dummy_states - """ + continuous_state_next_period = calculate_continuous_state( + discrete_states_beginning_of_period=state_space_dict, + continuous_states_end_of_last_period=continuous_state_space, + params=params, + compute_continuous_state=model_funcs["next_period_continuous_state"], + ) + _check_continuous_state_output_keys( + continuous_state_output=continuous_state_next_period, + continuous_state_space=continuous_state_space, + ) + return continuous_state_next_period - def grid_wrapper(states, new_state_grid): - all_states = {**states, gname: new_state_grid} - return inner_func(all_states) - return vmap(grid_wrapper, in_axes=(None, 0)) +def _check_continuous_state_output_keys( + continuous_state_output, + continuous_state_space, +): + expected_keys = set(continuous_state_space.keys()) + output_keys = set(continuous_state_output.keys()) + if output_keys != expected_keys: + raise ValueError( + "next_period_continuous_state output keys must match continuous_state_space keys. " + f"Expected {sorted(expected_keys)}, got {sorted(output_keys)}." + ) def calc_beginning_of_period_assets_for_single_state( @@ -160,7 +177,7 @@ def calc_assets_beginning_of_period_2cont_vec( def calculate_continuous_state( discrete_states_beginning_of_period, - continuous_grid, + continuous_states_end_of_last_period, params, compute_continuous_state, ): @@ -172,7 +189,7 @@ def calculate_continuous_state( in_axes=(0, None, None, None), # discrete states )( discrete_states_beginning_of_period, - continuous_grid, + continuous_states_end_of_last_period, params, compute_continuous_state, ) @@ -181,13 +198,13 @@ def calculate_continuous_state( def calc_continuous_state_for_each_grid_point( state_vec, - exog_continuous_grid_point, + continuous_state_vec, params, compute_continuous_state, ): out = compute_continuous_state( **state_vec, - continuous_state=exog_continuous_grid_point, + **continuous_state_vec, params=params, ) return out diff --git a/src/dcegm/pre_processing/alternative_sim_functions.py b/src/dcegm/pre_processing/alternative_sim_functions.py index 2e7c60ce..b8888f27 100644 --- a/src/dcegm/pre_processing/alternative_sim_functions.py +++ b/src/dcegm/pre_processing/alternative_sim_functions.py @@ -128,13 +128,6 @@ def process_alternative_sim_functions( """ continuous_states_info = model_config["continuous_states_info"] - # Assign name - if continuous_states_info["second_continuous_exists"]: - second_continuous_state_name = continuous_states_info[ - "second_continuous_state_name" - ] - else: - second_continuous_state_name = None # Now exogenous transition function if present compute_stochastic_transition_vec, processed_stochastic_funcs_dict = ( @@ -142,7 +135,6 @@ def process_alternative_sim_functions( stochastic_states_transition, model_config=model_config, model_specs=model_specs_jax, - continuous_state_name=second_continuous_state_name, ) ) @@ -152,19 +144,21 @@ def process_alternative_sim_functions( state_space_functions, model_config=model_config, model_specs=model_specs, - continuous_state_name=second_continuous_state_name, ) ) next_period_continuous_state = process_second_continuous_update_function( - second_continuous_state_name, state_space_functions, model_specs=model_specs_jax + state_space_functions, + model_specs=model_specs_jax, + has_additional_continuous_states=continuous_states_info[ + "has_additional_continuous_state" + ], ) # Budget equation compute_assets_begin_of_period = ( determine_function_arguments_and_partial_model_specs( func=budget_constraint, - continuous_state_name=second_continuous_state_name, model_specs=model_specs_jax, ) ) @@ -172,7 +166,6 @@ def process_alternative_sim_functions( # Upper envelope function compute_upper_envelope = create_upper_envelope_function( model_config=model_config, - continuous_state=second_continuous_state_name, ) taste_shock_function_processed, taste_shock_scale_in_params = ( @@ -180,7 +173,9 @@ def process_alternative_sim_functions( shock_functions=shock_functions, model_specs=model_specs, model_specs_jax=model_specs_jax, - continuous_state_name=second_continuous_state_name, + additional_continuous_state_names=continuous_states_info[ + "additional_continuous_state_names" + ], ) ) diff --git a/src/dcegm/pre_processing/batches/batch_creation.py b/src/dcegm/pre_processing/batches/batch_creation.py index 9c9d620a..e9b2e8ab 100644 --- a/src/dcegm/pre_processing/batches/batch_creation.py +++ b/src/dcegm/pre_processing/batches/batch_creation.py @@ -10,16 +10,16 @@ def create_batches_and_information( model_structure, n_periods, min_period_batch_segments=None, + batch_mode="largest_block", ): """Batches are used instead of periods to have chunks of equal sized state choices. - The batch inparams=paramsformation dictionary contains the following arrays - reflecting the. + The returned batch information dictionary contains the following arrays + reflecting steps in the backward induction: - steps in the backward induction: - batches_state_choice_idx: The state choice indexes in each batch to be solved. - To solve the state choices in the egm step, we have to look at the child states - and the corresponding state choice indexes in the child states. For that we save - the following: + To solve the state choices in the egm step, we have to look at the child states + and the corresponding state choice indexes in the child states. For that we save + the following: - child_state_choice_idxs_to_interp: The state choice indexes in we need to interpolate the wealth on. - child_states_idxs: The parent state indexes of the child states, i.e. the @@ -64,10 +64,22 @@ def create_batches_and_information( state_choice_space = model_structure["state_choice_space"] bool_state_choices_to_batch = state_choice_space[:, 0] < n_periods - 2 + valid_batch_modes = {"largest_block", "period_max"} + if min_period_batch_segments is None: + if isinstance(batch_mode, list): + raise ValueError( + "If min_period_batch_segments is not supplied, batch_mode must be a string." + ) + if batch_mode not in valid_batch_modes: + raise ValueError( + f"batch_mode must be one of {valid_batch_modes}. Got {batch_mode}." + ) single_batch_segment_info = create_single_segment_of_batches( - bool_state_choices_to_batch, model_structure + bool_state_choices_to_batch, + model_structure, + batch_mode=batch_mode, ) segment_infos = { "n_segments": 1, @@ -97,6 +109,24 @@ def create_batches_and_information( "The periods to split the batches have to be increasing and at least two periods apart." ) + if isinstance(batch_mode, str): + if batch_mode not in valid_batch_modes: + raise ValueError( + f"batch_mode must be one of {valid_batch_modes}. Got {batch_mode}." + ) + batch_mode = [batch_mode] * n_segments + elif isinstance(batch_mode, list): + if len(batch_mode) != n_segments: + raise ValueError( + "If min_period_batch_segments is supplied, batch_mode must be a list with one entry per segment." + ) + if not all(mode in valid_batch_modes for mode in batch_mode): + raise ValueError( + f"All entries in batch_mode must be one of {valid_batch_modes}." + ) + else: + raise ValueError("batch_mode must be a string or a list of strings.") + segment_infos = { "n_segments": n_segments, } @@ -111,7 +141,9 @@ def create_batches_and_information( bool_state_choices_segment = bool_state_choices_to_batch & (~split_cond) segment_batch_info = create_single_segment_of_batches( - bool_state_choices_segment, model_structure + bool_state_choices_segment, + model_structure, + batch_mode=batch_mode[id_segment], ) segment_infos[f"batches_info_segment_{id_segment}"] = segment_batch_info @@ -119,7 +151,9 @@ def create_batches_and_information( bool_state_choices_to_batch = bool_state_choices_to_batch & split_cond last_segment_batch_info = create_single_segment_of_batches( - bool_state_choices_to_batch, model_structure + bool_state_choices_to_batch, + model_structure, + batch_mode=batch_mode[n_segments - 1], ) # We loop until n_segments - 2 and then add the last segment diff --git a/src/dcegm/pre_processing/batches/single_segment.py b/src/dcegm/pre_processing/batches/single_segment.py index de7db506..de0e0bb7 100644 --- a/src/dcegm/pre_processing/batches/single_segment.py +++ b/src/dcegm/pre_processing/batches/single_segment.py @@ -3,10 +3,14 @@ from dcegm.pre_processing.batches.algo_batch_size import determine_optimal_batch_size -def create_single_segment_of_batches(bool_state_choices_to_batch, model_structure): +def create_single_segment_of_batches( + bool_state_choices_to_batch, + model_structure, + batch_mode="largest_block", +): """Create a single segment of evenly sized batches. - If the last batch is not evenly we correct it. + If the last batch is not evenly sized we correct it. """ @@ -24,34 +28,54 @@ def create_single_segment_of_batches(bool_state_choices_to_batch, model_structur ] map_state_choice_to_index = model_structure["map_state_choice_to_index_with_proxy"] - ( - batches_list, - child_state_choice_idxs_to_interp_list, - child_state_choices_to_aggr_choice_list, - child_states_to_integrate_stochastic_list, - ) = determine_optimal_batch_size( - bool_state_choices_to_batch=bool_state_choices_to_batch, - state_choice_space=state_choice_space, - map_state_choice_to_child_states=map_state_choice_to_child_states, - map_state_choice_to_index=map_state_choice_to_index, - state_space=state_space, - ) + if batch_mode == "largest_block": + ( + batches_list, + child_state_choice_idxs_to_interp_list, + child_state_choices_to_aggr_choice_list, + child_states_to_integrate_stochastic_list, + ) = determine_optimal_batch_size( + bool_state_choices_to_batch=bool_state_choices_to_batch, + state_choice_space=state_choice_space, + map_state_choice_to_child_states=map_state_choice_to_child_states, + map_state_choice_to_index=map_state_choice_to_index, + state_space=state_space, + ) - ( - batches_list, - child_states_to_integrate_stochastic_list, - child_state_choices_to_aggr_choice_list, - child_state_choice_idxs_to_interp_list, - batches_cover_all, - last_batch_info, - ) = correct_for_uneven_last_batch( - batches_list, - child_states_to_integrate_stochastic_list, - child_state_choices_to_aggr_choice_list, - child_state_choice_idxs_to_interp_list, - state_choice_space_dict, - map_state_choice_to_parent_state, - ) + ( + batches_list, + child_states_to_integrate_stochastic_list, + child_state_choices_to_aggr_choice_list, + child_state_choice_idxs_to_interp_list, + batches_cover_all, + last_batch_info, + ) = correct_for_uneven_last_batch( + batches_list, + child_states_to_integrate_stochastic_list, + child_state_choices_to_aggr_choice_list, + child_state_choice_idxs_to_interp_list, + state_choice_space_dict, + map_state_choice_to_parent_state, + ) + elif batch_mode == "period_max": + ( + batches_list, + child_state_choice_idxs_to_interp_list, + child_state_choices_to_aggr_choice_list, + child_states_to_integrate_stochastic_list, + ) = determine_period_max_batch_size( + bool_state_choices_to_batch=bool_state_choices_to_batch, + state_choice_space=state_choice_space, + map_state_choice_to_child_states=map_state_choice_to_child_states, + map_state_choice_to_index=map_state_choice_to_index, + state_space=state_space, + ) + batches_cover_all = True + last_batch_info = None + else: + raise ValueError( + f"Unknown batch_mode {batch_mode}. Use 'largest_block' or 'period_max'." + ) single_batch_segment_info = prepare_and_align_batch_arrays( batches_list, @@ -69,6 +93,94 @@ def create_single_segment_of_batches(bool_state_choices_to_batch, model_structur return single_batch_segment_info +def determine_period_max_batch_size( + bool_state_choices_to_batch, + state_choice_space, + map_state_choice_to_child_states, + map_state_choice_to_index, + state_space, +): + invalid_state_idx = np.iinfo(map_state_choice_to_index.dtype).max + out_of_bounds_state_choice_idx = state_choice_space.shape[0] + 1 + + idx_state_choice_raw = np.where(bool_state_choices_to_batch)[0] + if idx_state_choice_raw.size == 0: + raise ValueError("No state choices to batch in segment.") + + periods_to_batch = state_choice_space[idx_state_choice_raw, 0] + periods_unique_desc = np.sort(np.unique(periods_to_batch))[::-1] + + n_state_vars = state_space.shape[1] + + batches_to_check = [] + child_states_to_integrate_exog = [] + child_state_choices_to_aggr_choice = [] + child_state_choice_idxs_to_interpolate = [] + + for period in periods_unique_desc: + batch = idx_state_choice_raw[periods_to_batch == period] + batches_to_check += [batch] + + child_states_idxs = map_state_choice_to_child_states[batch] + unique_child_states, inverse_ids = np.unique( + child_states_idxs, return_index=False, return_inverse=True + ) + child_states_to_integrate_exog += [inverse_ids.reshape(child_states_idxs.shape)] + + child_states_batch = np.take(state_space, unique_child_states, axis=0) + child_states_tuple = tuple( + child_states_batch[:, i] for i in range(n_state_vars) + ) + unique_state_choice_idxs_childs = map_state_choice_to_index[child_states_tuple] + + ( + unique_child_state_choice_idxs, + inverse_child_state_choice_ids, + ) = np.unique( + unique_state_choice_idxs_childs, return_index=False, return_inverse=True + ) + + if ( + len(unique_child_state_choice_idxs) > 0 + and unique_child_state_choice_idxs[-1] == invalid_state_idx + ): + unique_child_state_choice_idxs = unique_child_state_choice_idxs[:-1] + inverse_child_state_choice_ids[ + inverse_child_state_choice_ids >= np.max(inverse_child_state_choice_ids) + ] = out_of_bounds_state_choice_idx + + child_state_choices_to_aggr_choice += [ + inverse_child_state_choice_ids.reshape( + unique_state_choice_idxs_childs.shape + ) + ] + child_state_choice_idxs_to_interpolate += [unique_child_state_choice_idxs] + + max_batch_size = max(len(batch) for batch in batches_to_check) + + for id_batch, batch in enumerate(batches_to_check): + n_to_add = max_batch_size - len(batch) + if n_to_add > 0: + pad_state_choice_idx = np.full(n_to_add, batch[0], dtype=batch.dtype) + batches_to_check[id_batch] = np.concatenate([batch, pad_state_choice_idx]) + + first_row = child_states_to_integrate_exog[id_batch][0:1, :] + child_states_to_integrate_exog[id_batch] = np.concatenate( + [ + child_states_to_integrate_exog[id_batch], + np.repeat(first_row, repeats=n_to_add, axis=0), + ], + axis=0, + ) + + return ( + batches_to_check, + child_state_choice_idxs_to_interpolate, + child_state_choices_to_aggr_choice, + child_states_to_integrate_exog, + ) + + def correct_for_uneven_last_batch( batches_list, child_states_to_integrate_stochastic_list, diff --git a/src/dcegm/pre_processing/check_model_config.py b/src/dcegm/pre_processing/check_model_config.py index 718a9133..f134d546 100644 --- a/src/dcegm/pre_processing/check_model_config.py +++ b/src/dcegm/pre_processing/check_model_config.py @@ -73,48 +73,55 @@ def check_model_config_and_process(model_config): continuous_states_grids["assets_end_of_period"], dtype=float ) - if len(continuous_states_grids) > 2: - raise ValueError("At most two continuous states are supported.") - - elif len(continuous_states_grids) == 2: - second_continuous_state = next( - ( - {key: value} - for key, value in model_config["continuous_states"].items() - if key != "assets_end_of_period" - ), - None, - ) - - continuous_states_info["second_continuous_exists"] = True - - second_continuous_state_name = list(second_continuous_state.keys())[0] - continuous_states_info["second_continuous_state_name"] = ( - second_continuous_state_name - ) + additional_continuous_states = { + key: value + for key, value in continuous_states_grids.items() + if key not in ("assets_end_of_period", "assets_begin_of_period") + } + + continuous_states_info["additional_continuous_state_names"] = list( + additional_continuous_states.keys() + ) + continuous_states_info["additional_continuous_state_grids"] = { + key: jnp.asarray(value) for key, value in additional_continuous_states.items() + } + continuous_states_info["n_additional_continuous_states"] = len( + additional_continuous_states + ) + continuous_states_info["has_additional_continuous_state"] = ( + continuous_states_info["n_additional_continuous_states"] > 0 + ) - second_continuous_state_grid = jnp.asarray( - continuous_states_grids[second_continuous_state_name] - ) - continuous_states_info["second_continuous_grid"] = second_continuous_state_grid - # ToDo: Check if grid is array or list and monotonic increasing + processed_model_config["continuous_states_info"] = continuous_states_info - continuous_states_info["n_second_continuous_grid"] = len( - second_continuous_state_grid + # Set default upper envelope method if not given. + if "upper_envelope" not in model_config: + upper_envelope = {} + upper_envelope["method"] = "fues" + elif "method" not in model_config["upper_envelope"]: + upper_envelope = model_config["upper_envelope"] + upper_envelope["method"] = "fues" + elif ( + "upper_envelope" in model_config + and model_config["upper_envelope"]["method"] == "druedahl_jorgensen" + and "tuning_params" in model_config["upper_envelope"] + ): + raise ValueError( + "'tuning_params' cannot be used with the 'druedahl_jorgensen'," + " specify 'begin_of_period_assets_grid' in 'continuous_states' instead and delete 'tuning_params' " + "from the model_config['upper_envelope']" ) - else: - continuous_states_info["second_continuous_exists"] = False - continuous_states_info["second_continuous_state_name"] = None - continuous_states_info["n_second_continuous_grid"] = None - continuous_states_info["second_continuous_grid"] = None + upper_envelope = dict(model_config["upper_envelope"]) - processed_model_config["continuous_states_info"] = continuous_states_info - - if "tuning_params" not in model_config: + if "tuning_params" not in upper_envelope: tuning_params = {} + elif "tuning_params" in model_config: + raise ValueError( + "tuning_params should be nested in model_config['upper_envelope']" + ) else: - tuning_params = model_config["tuning_params"] + tuning_params = model_config["upper_envelope"]["tuning_params"] tuning_params["extra_wealth_grid_factor"] = ( tuning_params["extra_wealth_grid_factor"] @@ -144,18 +151,52 @@ def check_model_config_and_process(model_config): # Set jump threshold to default 2 if it is not given tuning_params["fues_jump_thresh"] = int( tuning_params["fues_jump_threshold"] - if "fues_jump_threshold" in tuning_params + if ("fues_jump_threshold" in tuning_params) + & (upper_envelope["method"] == "fues") else 2 ) # Set fues_n_points_to_scan to 10 if not given tuning_params["fues_n_points_to_scan"] = int( tuning_params["fues_n_points_to_scan"] - if "fues_n_points_to_scan" in tuning_params + if ("fues_n_points_to_scan" in tuning_params) + & (upper_envelope["method"] == "fues") else 10 ) - processed_model_config["tuning_params"] = tuning_params + upper_envelope["tuning_params"] = tuning_params + processed_model_config["upper_envelope"] = upper_envelope + + if ( + continuous_states_info["n_additional_continuous_states"] > 1 + and upper_envelope["method"] != "druedahl_jorgensen" + ): + raise ValueError( + "If more than one additional continuous state is specified, " + "use upper_envelope['method'] = 'druedahl_jorgensen'." + ) + + if upper_envelope["method"] == "druedahl_jorgensen": + if "assets_begin_of_period" not in model_config["continuous_states"]: + raise ValueError( + "Specify 'assets_begin_of_period' in model_config['continuous_states'] when using " + "the 'druedahl_jorgensen' upper envelope method." + ) + processed_model_config["continuous_states_info"]["assets_begin_of_period"] = ( + jnp.asarray(model_config["continuous_states"]["assets_begin_of_period"]) + ) + + if upper_envelope["method"] == "fues": + processed_model_config["n_total_wealth_grid"] = tuning_params[ + "n_total_wealth_grid" + ] + elif upper_envelope["method"] == "druedahl_jorgensen": + # Expected value at 0, so add 1 + processed_model_config["n_total_wealth_grid"] = ( + len(model_config["continuous_states"]["assets_begin_of_period"]) + 1 + ) + else: + raise ValueError("Something wrong internally") if "min_period_batch_segments" in model_config.keys(): processed_model_config["min_period_batch_segments"] = model_config[ @@ -164,6 +205,51 @@ def check_model_config_and_process(model_config): else: processed_model_config["min_period_batch_segments"] = None + if "batch_mode" in model_config.keys(): + batch_mode = model_config["batch_mode"] + valid_batch_modes = {"largest_block", "period_max"} + if not isinstance(batch_mode, (str, list)): + raise ValueError("batch_mode must be a string or a list of strings.") + + if isinstance(batch_mode, str): + if batch_mode not in valid_batch_modes: + raise ValueError( + f"batch_mode must be one of {valid_batch_modes}. Got {batch_mode}." + ) + else: + if not all(isinstance(mode, str) for mode in batch_mode): + raise ValueError( + "If batch_mode is a list, all entries must be strings." + ) + if not all(mode in valid_batch_modes for mode in batch_mode): + raise ValueError( + f"All entries in batch_mode must be one of {valid_batch_modes}." + ) + + min_period_batch_segments = processed_model_config[ + "min_period_batch_segments" + ] + if min_period_batch_segments is None: + expected_n_segments = 1 + elif isinstance(min_period_batch_segments, int): + expected_n_segments = 2 + elif isinstance(min_period_batch_segments, list): + expected_n_segments = len(min_period_batch_segments) + 1 + else: + raise ValueError( + "min_period_batch_segments must be None, int, or list." + ) + + if len(batch_mode) != expected_n_segments: + raise ValueError( + "If batch_mode is a list, it must have one entry per segment. " + f"Expected {expected_n_segments}, got {len(batch_mode)}." + ) + + processed_model_config["batch_mode"] = batch_mode + else: + processed_model_config["batch_mode"] = "largest_block" + if "stochastic_states" in model_config.keys(): processed_model_config["stochastic_states"] = model_config["stochastic_states"] diff --git a/src/dcegm/pre_processing/model_functions/process_model_functions.py b/src/dcegm/pre_processing/model_functions/process_model_functions.py index 354eef4c..75e5d8d5 100644 --- a/src/dcegm/pre_processing/model_functions/process_model_functions.py +++ b/src/dcegm/pre_processing/model_functions/process_model_functions.py @@ -67,10 +67,11 @@ def process_model_functions_and_extract_info( transition probabilities for each state. """ - # Assign continuous state name - second_continuous_state_name = model_config["continuous_states_info"][ - "second_continuous_state_name" + # Assign continuous-state information. + additional_continuous_state_names = model_config["continuous_states_info"][ + "additional_continuous_state_names" ] + has_additional_continuous_states = len(additional_continuous_state_names) > 0 # We use this for functions which are called later in the jitted code model_specs_jax = jax.tree_util.tree_map(try_jax_array, model_specs) @@ -79,20 +80,17 @@ def process_model_functions_and_extract_info( compute_utility = determine_function_arguments_and_partial_model_specs( func=utility_functions["utility"], model_specs=model_specs_jax, - continuous_state_name=second_continuous_state_name, ) compute_marginal_utility = determine_function_arguments_and_partial_model_specs( func=utility_functions["marginal_utility"], model_specs=model_specs_jax, - continuous_state_name=second_continuous_state_name, ) compute_inverse_marginal_utility = ( determine_function_arguments_and_partial_model_specs( func=utility_functions["inverse_marginal_utility"], model_specs=model_specs_jax, - continuous_state_name=second_continuous_state_name, ) ) @@ -105,14 +103,12 @@ def process_model_functions_and_extract_info( compute_utility_final = determine_function_arguments_and_partial_model_specs( func=utility_functions_final_period["utility"], model_specs=model_specs_jax, - continuous_state_name=second_continuous_state_name, ) compute_marginal_utility_final = ( determine_function_arguments_and_partial_model_specs( func=utility_functions_final_period["marginal_utility"], model_specs=model_specs_jax, - continuous_state_name=second_continuous_state_name, ) ) @@ -127,7 +123,6 @@ def process_model_functions_and_extract_info( stochastic_states_transitions, model_config=model_config, model_specs=model_specs_jax, - continuous_state_name=second_continuous_state_name, ) ) @@ -137,19 +132,20 @@ def process_model_functions_and_extract_info( state_space_functions, model_config=model_config, model_specs=model_specs, - continuous_state_name=second_continuous_state_name, + additional_continuous_state_names=additional_continuous_state_names, ) ) next_period_continuous_state = process_second_continuous_update_function( - second_continuous_state_name, state_space_functions, model_specs=model_specs_jax + state_space_functions=state_space_functions, + model_specs=model_specs_jax, + has_additional_continuous_states=has_additional_continuous_states, ) # Budget equation compute_assets_begin_of_period = ( determine_function_arguments_and_partial_model_specs( func=budget_constraint, - continuous_state_name=second_continuous_state_name, model_specs=model_specs_jax, ) ) @@ -157,7 +153,6 @@ def process_model_functions_and_extract_info( # Upper envelope function compute_upper_envelope = create_upper_envelope_function( model_config=model_config, - continuous_state=second_continuous_state_name, ) taste_shock_function_processed, taste_shock_scale_in_params = ( @@ -165,7 +160,7 @@ def process_model_functions_and_extract_info( shock_functions=shock_functions, model_specs=model_specs, model_specs_jax=model_specs_jax, - continuous_state_name=second_continuous_state_name, + additional_continuous_state_names=additional_continuous_state_names, ) ) model_config_processed = model_config @@ -194,7 +189,7 @@ def process_state_space_functions( state_space_functions, model_config, model_specs, - continuous_state_name, + additional_continuous_state_names=None, ): state_space_functions = ( @@ -211,11 +206,15 @@ def state_specific_choice_set(**kwargs): return jnp.array(model_config["choices"]) else: + not_allowed_state_choices = ["assets_begin_of_period"] + if additional_continuous_state_names is not None: + not_allowed_state_choices += list(additional_continuous_state_names) + state_specific_choice_set = ( determine_function_arguments_and_partial_model_specs( func=state_space_functions["state_specific_choice_set"], model_specs=model_specs, - continuous_state_name=continuous_state_name, + not_allowed_state_choices=not_allowed_state_choices, ) ) @@ -233,7 +232,6 @@ def next_period_deterministic_state(**kwargs): determine_function_arguments_and_partial_model_specs( func=state_space_functions["next_period_deterministic_state"], model_specs=model_specs, - continuous_state_name=continuous_state_name, ) ) @@ -264,16 +262,23 @@ def sparsity_condition(**kwargs): def process_second_continuous_update_function( - continuous_state_name, state_space_functions, model_specs + state_space_functions=None, + model_specs=None, + has_additional_continuous_states=False, ): - if continuous_state_name is not None: - func_name = f"next_period_{continuous_state_name}" + if has_additional_continuous_states: + if state_space_functions is None: + state_space_functions = {} + if "next_period_continuous_state" not in state_space_functions: + raise ValueError( + "If additional continuous states are defined, provide " + "'next_period_continuous_state' in state_space_functions." + ) next_period_continuous_state = ( determine_function_arguments_and_partial_model_specs( - func=state_space_functions[func_name], + func=state_space_functions["next_period_continuous_state"], model_specs=model_specs, - continuous_state_name=continuous_state_name, ) ) else: diff --git a/src/dcegm/pre_processing/model_functions/taste_shock_function.py b/src/dcegm/pre_processing/model_functions/taste_shock_function.py index d674f8c9..dfb22250 100644 --- a/src/dcegm/pre_processing/model_functions/taste_shock_function.py +++ b/src/dcegm/pre_processing/model_functions/taste_shock_function.py @@ -6,7 +6,7 @@ def process_shock_functions( - shock_functions, model_specs, model_specs_jax, continuous_state_name + shock_functions, model_specs, model_specs_jax, additional_continuous_state_names ): taste_shock_function_processed = {} shock_functions = {} if shock_functions is None else shock_functions @@ -14,7 +14,7 @@ def process_shock_functions( taste_shock_scale_per_state = get_taste_shock_function_for_state( draw_function_taste_shocks=shock_functions["taste_shock_scale_per_state"], model_specs=model_specs_jax, - continuous_state_name=continuous_state_name, + additional_continuous_state_names=additional_continuous_state_names, ) taste_shock_function_processed["taste_shock_scale_per_state"] = ( taste_shock_scale_per_state @@ -49,17 +49,16 @@ def process_shock_functions( def get_taste_shock_function_for_state( - draw_function_taste_shocks, continuous_state_name, model_specs + draw_function_taste_shocks, additional_continuous_state_names, model_specs ): not_allowed_states = ["assets_begin_of_period", "choice"] - if continuous_state_name is not None: - not_allowed_states += [continuous_state_name] + if additional_continuous_state_names is not None: + not_allowed_states += additional_continuous_state_names taste_shock_scale_per_state_function = ( determine_function_arguments_and_partial_model_specs( func=draw_function_taste_shocks, model_specs=model_specs, not_allowed_state_choices=not_allowed_states, - continuous_state_name=continuous_state_name, ) ) diff --git a/src/dcegm/pre_processing/model_functions/upper_evelope_wrapper.py b/src/dcegm/pre_processing/model_functions/upper_evelope_wrapper.py index ebac54bd..1833d1d2 100644 --- a/src/dcegm/pre_processing/model_functions/upper_evelope_wrapper.py +++ b/src/dcegm/pre_processing/model_functions/upper_evelope_wrapper.py @@ -1,115 +1,83 @@ from jax import numpy as jnp -from upper_envelope.jax import fues_jax +from upper_envelope.jax import drued_jorg_jax, fues_jax -def create_upper_envelope_function(model_config, continuous_state=None): +def create_upper_envelope_function( + model_config, +): if len(model_config["choices"]) < 2: - compute_upper_envelope = no_upper_envelope_dummy_function - else: - - tuning_params = model_config["tuning_params"] - - if continuous_state: - - def compute_upper_envelope( - endog_grid, - policy, - value, - expected_value_zero_assets, - second_continuous_state, - state_choice_dict, - utility_function, - params, - discount_factor, - ): - value_kwargs = { - "second_continuous_state": second_continuous_state, - "expected_value_zero_assets": expected_value_zero_assets, - "params": params, - "discount_factor": discount_factor, - **state_choice_dict, - } - - def value_function( - consumption, - second_continuous_state, - expected_value_zero_assets, - params, - discount_factor, - **state_choice_dict, - ): - return ( - utility_function( - consumption=consumption, - continuous_state=second_continuous_state, - params=params, - **state_choice_dict, - ) - + discount_factor * expected_value_zero_assets - ) - - return fues_jax( - endog_grid=endog_grid, - policy=policy, - value=value, - expected_value_zero_savings=expected_value_zero_assets, - value_function=value_function, - value_function_kwargs=value_kwargs, - n_constrained_points_to_add=tuning_params[ - "n_constrained_points_to_add" - ], - n_final_wealth_grid=tuning_params["n_total_wealth_grid"], - jump_thresh=tuning_params["fues_jump_thresh"], - n_points_to_scan=tuning_params["fues_n_points_to_scan"], + return no_upper_envelope_dummy_function + + tuning_params = model_config["upper_envelope"]["tuning_params"] + method = model_config["upper_envelope"]["method"] + + def compute_upper_envelope( + endog_grid, + policy, + value, + expected_value_zero_assets, + continuous_state_dict, + state_choice_dict, + utility_function, + params, + discount_factor, + ): + state_choice_vars = {**state_choice_dict, **continuous_state_dict} + + value_kwargs = { + "expected_value_zero_assets": expected_value_zero_assets, + "params": params, + "discount_factor": discount_factor, + } + + def value_function( + consumption, + expected_value_zero_assets, + params, + discount_factor, + ): + return ( + utility_function( + consumption=consumption, + params=params, + **state_choice_vars, ) + + discount_factor * expected_value_zero_assets + ) + + # --- method dispatch --- + if method == "fues": + return fues_jax( + endog_grid=endog_grid, + policy=policy, + value=value, + expected_value_zero_savings=expected_value_zero_assets, + value_function=value_function, + value_function_kwargs=value_kwargs, + n_constrained_points_to_add=tuning_params[ + "n_constrained_points_to_add" + ], + n_final_wealth_grid=tuning_params["n_total_wealth_grid"], + jump_thresh=tuning_params["fues_jump_thresh"], + n_points_to_scan=tuning_params["fues_n_points_to_scan"], + ) + + elif method == "druedahl_jorgensen": + return drued_jorg_jax( + endog_grid=endog_grid, + policy=policy, + value=value, + expected_value_zero_savings=expected_value_zero_assets, + value_function=value_function, + value_function_kwargs=value_kwargs, + m_grid=model_config["continuous_states_info"]["assets_begin_of_period"], + ) else: - - def compute_upper_envelope( - endog_grid, - policy, - value, - expected_value_zero_assets, - state_choice_dict, - utility_function, - params, - discount_factor, - ): - value_kwargs = { - "expected_value_zero_assets": expected_value_zero_assets, - "params": params, - "discount_factor": discount_factor, - **state_choice_dict, - } - - def value_function( - consumption, - expected_value_zero_assets, - params, - discount_factor, - **state_choice_dict, - ): - return ( - utility_function( - consumption=consumption, params=params, **state_choice_dict - ) - + discount_factor * expected_value_zero_assets - ) - - return fues_jax( - endog_grid=endog_grid, - policy=policy, - value=value, - expected_value_zero_savings=expected_value_zero_assets, - value_function=value_function, - value_function_kwargs=value_kwargs, - n_constrained_points_to_add=tuning_params[ - "n_constrained_points_to_add" - ], - n_final_wealth_grid=tuning_params["n_total_wealth_grid"], - jump_thresh=tuning_params["fues_jump_thresh"], - n_points_to_scan=tuning_params["fues_n_points_to_scan"], - ) + raise ValueError( + f"Unknown upper envelope method: {method}. " + "Choose 'fues' or 'druedahl_jorgensen'." + ) return compute_upper_envelope diff --git a/src/dcegm/pre_processing/model_structure/model_structure.py b/src/dcegm/pre_processing/model_structure/model_structure.py index 864eaeeb..eb24180b 100644 --- a/src/dcegm/pre_processing/model_structure/model_structure.py +++ b/src/dcegm/pre_processing/model_structure/model_structure.py @@ -26,6 +26,28 @@ def create_model_structure( - "transform_between_state_and_state_choice_vec" (callable) """ + # Create continuous state space + continuous_states_info = model_config["continuous_states_info"] + additional_continuous_state_grids = continuous_states_info.get( + "additional_continuous_state_grids", {} + ) + + if continuous_states_info["has_additional_continuous_state"]: + continuous_state_names = continuous_states_info[ + "additional_continuous_state_names" + ] + continuous_grids = [ + additional_continuous_state_grids[name] for name in continuous_state_names + ] + + continuous_state_mesh = jnp.meshgrid(*continuous_grids, indexing="ij") + continuous_state_space = { + name: grid.ravel() + for name, grid in zip(continuous_state_names, continuous_state_mesh) + } + else: + continuous_state_space = {"dummy_cont": jnp.zeros(1)} + print("Starting state space creation") state_space_objects = create_state_space( model_config=model_config, @@ -52,5 +74,6 @@ def create_model_structure( **state_space_objects, **state_choice_and_child_state_objects, "choice_range": jnp.asarray(model_config["choices"]), + "continuous_state_space": continuous_state_space, } return jax.tree.map(create_array_with_smallest_int_dtype, model_structure) diff --git a/src/dcegm/pre_processing/model_structure/stochastic_states.py b/src/dcegm/pre_processing/model_structure/stochastic_states.py index 8c4ebdab..f9ec3eae 100644 --- a/src/dcegm/pre_processing/model_structure/stochastic_states.py +++ b/src/dcegm/pre_processing/model_structure/stochastic_states.py @@ -14,7 +14,7 @@ def create_stochastic_transition_function( - stochastic_states_transitions, model_config, model_specs, continuous_state_name + stochastic_states_transitions, model_config, model_specs ): """Create the stochastic process transition function. @@ -31,7 +31,6 @@ def create_stochastic_transition_function( stochastic_states_transitions, model_config=model_config, model_specs=model_specs, - continuous_state_name=continuous_state_name, ) trans_func_list = [func_dict[name] for name in func_dict.keys()] @@ -44,7 +43,7 @@ def create_stochastic_transition_function( def process_stochastic_transitions( - stochastic_states_transitions, model_config, model_specs, continuous_state_name + stochastic_states_transitions, model_config, model_specs ): """Process stochastic functions. @@ -63,7 +62,6 @@ def process_stochastic_transitions( processed_exog_func = determine_function_arguments_and_partial_model_specs( func=func, model_specs=model_specs, - continuous_state_name=continuous_state_name, ) func_list += [processed_exog_func] func_dict[name] = processed_exog_func diff --git a/src/dcegm/pre_processing/setup_model.py b/src/dcegm/pre_processing/setup_model.py index 6de10304..d285cffd 100644 --- a/src/dcegm/pre_processing/setup_model.py +++ b/src/dcegm/pre_processing/setup_model.py @@ -128,6 +128,7 @@ def create_model_dict( model_structure=model_structure, n_periods=model_config_processed["n_periods"], min_period_batch_segments=model_config_processed["min_period_batch_segments"], + batch_mode=model_config_processed["batch_mode"], ) if not debug_info == "all": # Delete large arrays which is not needed. Not if all is requested diff --git a/src/dcegm/pre_processing/shared.py b/src/dcegm/pre_processing/shared.py index 4ff716e9..9e8e4f4f 100644 --- a/src/dcegm/pre_processing/shared.py +++ b/src/dcegm/pre_processing/shared.py @@ -7,7 +7,9 @@ def determine_function_arguments_and_partial_model_specs( - func, model_specs, not_allowed_state_choices=None, continuous_state_name=None + func, + model_specs, + not_allowed_state_choices=None, ): signature = set(inspect.signature(func).parameters) not_allowed_state_choices = ( @@ -29,9 +31,6 @@ def determine_function_arguments_and_partial_model_specs( @functools.wraps(func) def processed_func(**kwargs): - if continuous_state_name: - kwargs[continuous_state_name] = kwargs.get("continuous_state") - func_kwargs = {key: kwargs[key] for key in signature} return partialed_func(**func_kwargs) diff --git a/src/dcegm/pre_processing/sol_container.py b/src/dcegm/pre_processing/sol_container.py index 68e4efe0..b8bf2f3e 100644 --- a/src/dcegm/pre_processing/sol_container.py +++ b/src/dcegm/pre_processing/sol_container.py @@ -4,44 +4,25 @@ def create_solution_container( - continuous_states_info: Dict[str, Any], n_total_wealth_grid: int, n_state_choices: int, + n_continuous_state_combinations: int, ): """Create solution containers for value, policy, and endog_grid.""" - if continuous_states_info["second_continuous_exists"]: - n_second_continuous_grid = continuous_states_info["n_second_continuous_grid"] - - value_solved = jnp.full( - (n_state_choices, n_second_continuous_grid, n_total_wealth_grid), - dtype=jnp.float64, - fill_value=jnp.nan, - ) - policy_solved = jnp.full( - (n_state_choices, n_second_continuous_grid, n_total_wealth_grid), - dtype=jnp.float64, - fill_value=jnp.nan, - ) - endog_grid_solved = jnp.full( - (n_state_choices, n_second_continuous_grid, n_total_wealth_grid), - dtype=jnp.float64, - fill_value=jnp.nan, - ) - else: - value_solved = jnp.full( - (n_state_choices, n_total_wealth_grid), - dtype=jnp.float64, - fill_value=jnp.nan, - ) - policy_solved = jnp.full( - (n_state_choices, n_total_wealth_grid), - dtype=jnp.float64, - fill_value=jnp.nan, - ) - endog_grid_solved = jnp.full( - (n_state_choices, n_total_wealth_grid), - dtype=jnp.float64, - fill_value=jnp.nan, - ) + value_solved = jnp.full( + (n_state_choices, n_continuous_state_combinations, n_total_wealth_grid), + dtype=jnp.float64, + fill_value=jnp.nan, + ) + policy_solved = jnp.full( + (n_state_choices, n_continuous_state_combinations, n_total_wealth_grid), + dtype=jnp.float64, + fill_value=jnp.nan, + ) + endog_grid_solved = jnp.full( + (n_state_choices, n_continuous_state_combinations, n_total_wealth_grid), + dtype=jnp.float64, + fill_value=jnp.nan, + ) return value_solved, policy_solved, endog_grid_solved diff --git a/src/dcegm/simulation/sim_utils.py b/src/dcegm/simulation/sim_utils.py index 38670b49..e0d10d22 100644 --- a/src/dcegm/simulation/sim_utils.py +++ b/src/dcegm/simulation/sim_utils.py @@ -1,140 +1,14 @@ import jax import numpy as np import pandas as pd -from jax import numpy as jnp from jax import vmap -from dcegm.interfaces.index_functions import get_state_choice_index_per_discrete_states -from dcegm.interpolation.interp1d import interp1d_policy_and_value_on_wealth -from dcegm.interpolation.interp2d import ( - interp2d_policy_and_value_on_wealth_and_regular_grid, -) from dcegm.law_of_motion import ( calculate_assets_begin_of_period_for_all_agents, calculate_second_continuous_state_for_all_agents, ) -def interpolate_policy_and_value_for_all_agents( - discrete_states_beginning_of_period, - continuous_state_beginning_of_period, - assets_begin_of_period, - value_solved, - policy_solved, - endog_grid_solved, - map_state_choice_to_index, - choice_range, - params, - discrete_states_names, - compute_utility, - continuous_grid, - discount_factor, -): - - if continuous_state_beginning_of_period is not None: - - discrete_state_choice_indexes = get_state_choice_index_per_discrete_states( - states=discrete_states_beginning_of_period, - map_state_choice_to_index=map_state_choice_to_index, - discrete_states_names=discrete_states_names, - ) - - value_grid_agent = jnp.take( - value_solved, - discrete_state_choice_indexes, - axis=0, - mode="fill", - fill_value=jnp.nan, - ) - policy_grid_agent = jnp.take( - policy_solved, discrete_state_choice_indexes, axis=0 - ) - endog_grid_agent = jnp.take( - endog_grid_solved, discrete_state_choice_indexes, axis=0 - ) - - vectorized_interp = vmap( - vmap( - interp2d_policy_and_value_function, - in_axes=( - None, - None, - None, - None, - 0, - 0, - 0, - 0, - None, - None, - None, - ), # choices - ), - in_axes=(0, 0, 0, None, 0, 0, 0, None, None, None, None), # agents - ) - - # ================================================================================= - - policy_agent, value_agent = vectorized_interp( - assets_begin_of_period, - continuous_state_beginning_of_period, - discrete_states_beginning_of_period, - continuous_grid, - endog_grid_agent, - value_grid_agent, - policy_grid_agent, - choice_range, - params, - compute_utility, - discount_factor, - ) - - return policy_agent, value_agent - - else: - discrete_state_choice_indexes = get_state_choice_index_per_discrete_states( - states=discrete_states_beginning_of_period, - map_state_choice_to_index=map_state_choice_to_index, - discrete_states_names=discrete_states_names, - ) - - value_grid_agent = jnp.take( - value_solved, - discrete_state_choice_indexes, - axis=0, - mode="fill", - fill_value=jnp.nan, - ) - policy_grid_agent = jnp.take( - policy_solved, discrete_state_choice_indexes, axis=0 - ) - endog_grid_agent = jnp.take( - endog_grid_solved, discrete_state_choice_indexes, axis=0 - ) - - vectorized_interp = vmap( - vmap( - interp1d_policy_and_value_function, - in_axes=(None, None, 0, 0, 0, 0, None, None, None), # choices - ), - in_axes=(0, 0, 0, 0, 0, None, None, None, None), # agents - ) - - policy_agent, value_agent = vectorized_interp( - assets_begin_of_period, - discrete_states_beginning_of_period, - endog_grid_agent, - value_grid_agent, - policy_grid_agent, - choice_range, - params, - compute_utility, - discount_factor, - ) - - return policy_agent, value_agent - - def transition_to_next_period( discrete_states_beginning_of_period, continuous_state_beginning_of_period, @@ -148,9 +22,10 @@ def transition_to_next_period( n_agents = assets_end_of_period.shape[0] stochastic_states_next_period = vmap( - realize_stochastic_states, in_axes=(0, 0, 0, None, None) + realize_stochastic_states, in_axes=(0, 0, 0, 0, None, None) )( discrete_states_beginning_of_period, + continuous_state_beginning_of_period, choice, sim_keys["stochastic_state_keys"], params, @@ -205,7 +80,7 @@ def transition_to_next_period( all_states_next_period = { **discrete_states_next_period, - "continuous_state": continuous_state_next_period, + **continuous_state_next_period, } else: all_states_next_period = discrete_states_next_period.copy() @@ -251,18 +126,6 @@ def update_discrete_states_for_one_agent(update_func, state, choice, params): return update_func(**state, choice=choice, params=params) -def next_period_continuous_state_for_one_agent( - update_func, discrete_states, continuous_state, choice, params -): - - return update_func( - **discrete_states, - continuous_state=continuous_state, - choice=choice, - params=params, - ) - - def vectorized_utility(consumption_period, state, choice, params, compute_utility): utility = compute_utility( consumption=consumption_period, params=params, choice=choice, **state @@ -270,12 +133,18 @@ def vectorized_utility(consumption_period, state, choice, params, compute_utilit return utility -def realize_stochastic_states(state, choice, key, params, processed_stochastic_funcs): +def realize_stochastic_states( + state, cont_state, choice, key, params, processed_stochastic_funcs +): + if cont_state is not None: + all_states = {**state, **cont_state} + else: + all_states = state stochastic_states_next_period = {} for state_name in processed_stochastic_funcs.keys(): key, subkey = jax.random.split(key) state_vec = processed_stochastic_funcs[state_name]( - params=params, **state, choice=choice + params=params, **all_states, choice=choice ) stochastic_states_next_period[state_name] = jax.random.choice( key=subkey, a=state_vec.shape[0], p=state_vec @@ -283,64 +152,6 @@ def realize_stochastic_states(state, choice, key, params, processed_stochastic_f return stochastic_states_next_period -def interp1d_policy_and_value_function( - wealth_beginning_of_period, - state, - endog_grid_agent, - value_agent, - policy_agent, - choice, - params, - compute_utility, - discount_factor, -): - state_choice_vec = {**state, "choice": choice} - - policy_interp, value_interp = interp1d_policy_and_value_on_wealth( - wealth=wealth_beginning_of_period, - wealth_grid=endog_grid_agent, - policy_grid=policy_agent, - value_grid=value_agent, - compute_utility=compute_utility, - state_choice_vec=state_choice_vec, - params=params, - discount_factor=discount_factor, - ) - - return policy_interp, value_interp - - -def interp2d_policy_and_value_function( - wealth_beginning_of_period, - continuous_state_beginning_of_period, - state, - regular_grid, - endog_grid_agent, - value_agent, - policy_agent, - choice, - params, - compute_utility, - discount_factor, -): - state_choice_vec = {**state, "choice": choice} - - policy_interp, value_interp = interp2d_policy_and_value_on_wealth_and_regular_grid( - regular_grid=regular_grid, - wealth_grid=endog_grid_agent, - policy_grid=policy_agent, - value_grid=value_agent, - wealth_point_to_interp=wealth_beginning_of_period, - regular_point_to_interp=continuous_state_beginning_of_period, - compute_utility=compute_utility, - state_choice_vec=state_choice_vec, - params=params, - discount_factor=discount_factor, - ) - - return policy_interp, value_interp - - def create_simulation_df(sim_dict): n_periods, n_agents, n_choices = sim_dict["taste_shocks"].shape diff --git a/src/dcegm/simulation/simulate.py b/src/dcegm/simulation/simulate.py index c0ab9a92..b51ca5c6 100644 --- a/src/dcegm/simulation/simulate.py +++ b/src/dcegm/simulation/simulate.py @@ -8,10 +8,12 @@ from jax import vmap from dcegm.interfaces.index_functions import get_state_choice_index_per_discrete_states +from dcegm.interpolation.simulation_interp import ( + interpolate_policy_and_value_for_all_agents, +) from dcegm.simulation.random_keys import draw_random_keys_for_seed from dcegm.simulation.sim_utils import ( compute_final_utility_for_each_choice, - interpolate_policy_and_value_for_all_agents, transition_to_next_period, vectorized_utility, ) @@ -53,11 +55,16 @@ def simulate_all_periods( ) continuous_states_info = model_config["continuous_states_info"] + has_additional_continuous_state = continuous_states_info[ + "has_additional_continuous_state" + ] + additional_continuous_state_names = continuous_states_info[ + "additional_continuous_state_names" + ] - if continuous_states_info["second_continuous_exists"]: - states_initial_dtype[continuous_states_info["second_continuous_state_name"]] = ( - states_initial[continuous_states_info["second_continuous_state_name"]] - ) + if has_additional_continuous_state: + for name in additional_continuous_state_names: + states_initial_dtype[name] = states_initial[name] n_agents = len(states_initial["period"]) @@ -137,14 +144,20 @@ def simulate_single_period( ): continuous_states_info = model_config["continuous_states_info"] + has_additional_continuous_state = continuous_states_info[ + "has_additional_continuous_state" + ] + additional_continuous_state_names = continuous_states_info[ + "additional_continuous_state_names" + ] - if continuous_states_info["second_continuous_exists"]: - continuous_state_name = continuous_states_info["second_continuous_state_name"] - continuous_grid = continuous_states_info["second_continuous_grid"] + if has_additional_continuous_state: + continuous_state_name = additional_continuous_state_names[0] - continuous_state_beginning_of_period = states_beginning_of_period[ - continuous_state_name - ] + continuous_state_beginning_of_period = { + name: states_beginning_of_period[name] + for name in additional_continuous_state_names + } discrete_states_beginning_of_period = { key: value for key, value in states_beginning_of_period.items() @@ -153,7 +166,6 @@ def simulate_single_period( else: discrete_states_beginning_of_period = states_beginning_of_period continuous_state_beginning_of_period = None - continuous_grid = None assets_begin_of_period = states_beginning_of_period["assets_begin_of_period"] @@ -175,7 +187,12 @@ def simulate_single_period( params=params, discrete_states_names=model_structure_sol["discrete_states_names"], compute_utility=compute_utility, - continuous_grid=continuous_grid, + continuous_state_space=model_structure_sol["continuous_state_space"], + additional_continuous_state_grids=continuous_states_info[ + "additional_continuous_state_grids" + ], + upper_envelope_method=model_config["upper_envelope"]["method"], + has_additional_continuous_state=has_additional_continuous_state, discount_factor=discount_factor, ) @@ -227,8 +244,9 @@ def simulate_single_period( states_next_period = discrete_states_next_period - if continuous_states_info["second_continuous_exists"]: - states_next_period[continuous_state_name] = continuous_state_next_period + if has_additional_continuous_state: + for name in additional_continuous_state_names: + states_next_period[name] = continuous_state_next_period[name] states_next_period["assets_begin_of_period"] = assets_beginning_of_next_period @@ -273,17 +291,18 @@ def simulate_final_period( "assets_begin_of_period" ] - if continuous_states_info["second_continuous_exists"]: - continuous_state_name = continuous_states_info["second_continuous_state_name"] - - continuous_state_beginning_of_period = states_begin_of_final_period[ - continuous_state_name + if continuous_states_info["has_additional_continuous_state"]: + additional_continuous_state_names = continuous_states_info[ + "additional_continuous_state_names" ] + continuous_states_beginning_of_period = { + name: states_begin_of_final_period[name] + for name in additional_continuous_state_names + } states_begin_of_final_period = { **discrete_states_begin_last_period, - continuous_state_name: continuous_state_beginning_of_period, + **continuous_states_beginning_of_period, } - else: states_begin_of_final_period = discrete_states_begin_last_period diff --git a/src/dcegm/solve_single_period.py b/src/dcegm/solve_single_period.py index 7cf40435..df546dff 100644 --- a/src/dcegm/solve_single_period.py +++ b/src/dcegm/solve_single_period.py @@ -1,3 +1,4 @@ +import jax.numpy as jnp from jax import vmap from dcegm.egm.aggregate_marginal_utility import aggregate_marg_utils_and_exp_values @@ -12,9 +13,11 @@ def solve_single_period( xs, params, continuous_grids_info, + continuous_state_space, cont_grids_next_period, model_funcs, income_shock_weights, + upper_envelope_method, debug_info, ): """Solve a single period of the model using DCEGM.""" @@ -39,10 +42,12 @@ def solve_single_period( endog_grid_child_state_choice=endog_grid_solved[ child_state_choice_idxs_to_interp ], + continuous_state_space=continuous_state_space, policy_child_state_choice=policy_solved[child_state_choice_idxs_to_interp], value_child_state_choice=value_solved[child_state_choice_idxs_to_interp], child_state_idxs=child_state_idxs, params=params, + upper_envelope_method=upper_envelope_method, ) # Check if we have a scalar taste shock scale or state specific. Extract in each of the cases. @@ -72,6 +77,7 @@ def solve_single_period( taste_shock_scale_is_scalar=taste_shock_scale_is_scalar, income_shock_weights=income_shock_weights, continuous_grids_info=continuous_grids_info, + continuous_state_space=continuous_state_space, model_funcs=model_funcs, debug_info=debug_info, ) @@ -118,9 +124,11 @@ def solve_for_interpolated_values( taste_shock_scale_is_scalar, income_shock_weights, continuous_grids_info, + continuous_state_space, model_funcs, debug_info, ): + # EGM step 2) # Aggregate the marginal utilities and expected values over all child state-choice # combinations and income shock draws @@ -141,12 +149,12 @@ def solve_for_interpolated_values( expected_values, ) = calculate_candidate_solutions_from_euler_equation( continuous_grids_info=continuous_grids_info, + continuous_state_space=continuous_state_space, marg_util_next=marg_util, emax_next=emax, state_choice_mat=state_choice_mat, idx_post_decision_child_states=child_state_idxs, model_funcs=model_funcs, - has_second_continuous_state=continuous_grids_info["second_continuous_exists"], params=params, ) @@ -163,12 +171,11 @@ def solve_for_interpolated_values( policy_candidate=policy_candidate, value_candidate=value_candidate, expected_values=expected_values, - continuous_grid_info=continuous_grids_info, + continuous_state_space=continuous_state_space, state_choice_mat=state_choice_mat, compute_utility=model_funcs["compute_utility"], params=params, discount_factor=discount_factor, - has_second_continuous_state=continuous_grids_info["second_continuous_exists"], compute_upper_envelope_for_state_choice=model_funcs["compute_upper_envelope"], ) out_dict = { @@ -192,12 +199,11 @@ def run_upper_envelope( policy_candidate, value_candidate, expected_values, - continuous_grid_info, + continuous_state_space, state_choice_mat, compute_utility, params, discount_factor, - has_second_continuous_state, compute_upper_envelope_for_state_choice, ): """Run upper envelope to remove suboptimal candidates. @@ -206,37 +212,8 @@ def run_upper_envelope( """ - if has_second_continuous_state: - return vmap( - vmap( - compute_upper_envelope_for_state_choice, - in_axes=(0, 0, 0, 0, 0, None, None, None, None), # continuous state - ), - in_axes=( - 0, - 0, - 0, - 0, - None, - 0, - None, - None, - None, - ), # discrete states and choices - )( - endog_grid_candidate, - policy_candidate, - value_candidate, - expected_values[:, :, 0], - continuous_grid_info["second_continuous_grid"], - state_choice_mat, - compute_utility, - params, - discount_factor, - ) - - else: - return vmap( + return vmap( + vmap( compute_upper_envelope_for_state_choice, in_axes=( 0, @@ -247,14 +224,28 @@ def run_upper_envelope( None, None, None, - ), # discrete states and choice combs - )( - endog_grid_candidate, - policy_candidate, - value_candidate, - expected_values[:, 0], - state_choice_mat, - compute_utility, - params, - discount_factor, - ) + None, + ), + ), + in_axes=( + 0, + 0, + 0, + 0, + None, + 0, + None, + None, + None, + ), + )( + endog_grid_candidate, + policy_candidate, + value_candidate, + expected_values[:, :, 0], + continuous_state_space, + state_choice_mat, + compute_utility, + params, + discount_factor, + ) diff --git a/src/dcegm/toy_models/cons_ret_model_with_cont_exp/state_space_objects.py b/src/dcegm/toy_models/cons_ret_model_with_cont_exp/state_space_objects.py index 31364430..7b2f9643 100644 --- a/src/dcegm/toy_models/cons_ret_model_with_cont_exp/state_space_objects.py +++ b/src/dcegm/toy_models/cons_ret_model_with_cont_exp/state_space_objects.py @@ -12,13 +12,14 @@ def create_state_space_function_dict(): """ return { "state_specific_choice_set": get_state_specific_feasible_choice_set, - "next_period_experience": next_period_experience, + "next_period_continuous_state": next_period_experience, } def next_period_experience(period, lagged_choice, experience, model_specs): max_experience_period = period + model_specs["max_init_experience"] - return (1 / max_experience_period) * ( - (max_experience_period - 1) * experience + (lagged_choice == 0) - ) + return { + "experience": (1 / max_experience_period) + * ((max_experience_period - 1) * experience + (lagged_choice == 0)) + } diff --git a/tests/sandbox/vmap_timing.py b/tests/sandbox/vmap_timing.py new file mode 100644 index 00000000..3ef174c8 --- /dev/null +++ b/tests/sandbox/vmap_timing.py @@ -0,0 +1,70 @@ +import math +import time + +import jax +import jax.numpy as jnp + +N_STATES = 5_000 +N_DECISIONS = 200 +N_RUNS = 100 + + +def simple_op(x): + return x * x + 2.0 * x + + +vmap_2d = jax.jit(jax.vmap(jax.vmap(simple_op, in_axes=0), in_axes=0)) +vmap_3d = jax.jit( + jax.vmap( + jax.vmap( + jax.vmap(simple_op, in_axes=0), + in_axes=0, + ), + in_axes=0, + ) +) + + +def time_function(func, x, n_runs): + func(x).block_until_ready() + run_times = [] + for _ in range(n_runs): + start = time.perf_counter() + func(x).block_until_ready() + end = time.perf_counter() + run_times.append(end - start) + + avg = sum(run_times) / n_runs + if n_runs > 1: + variance = sum((t - avg) ** 2 for t in run_times) / (n_runs - 1) + se = math.sqrt(variance) / math.sqrt(n_runs) + else: + se = 0.0 + return avg, se + + +def main(): + x_2d = jnp.arange(N_STATES * N_DECISIONS, dtype=jnp.float32).reshape( + N_STATES, + N_DECISIONS, + ) + x_3d = x_2d[:, None, :] + + avg_2d, se_2d = time_function(vmap_2d, x_2d, N_RUNS) + avg_3d, se_3d = time_function(vmap_3d, x_3d, N_RUNS) + + y_2d = vmap_2d(x_2d) + y_3d = vmap_3d(x_3d).squeeze(axis=1) + max_diff = jnp.max(jnp.abs(y_2d - y_3d)) + + print(f"Input 2D shape: {x_2d.shape}") + print(f"Input 3D shape: {x_3d.shape}") + print(f"Average time (2D vmap): {avg_2d * 1e3:.3f} ms") + print(f"SE time (2D vmap): {se_2d * 1e3:.3f} ms") + print(f"Average time (3D with singleton axis): {avg_3d * 1e3:.3f} ms") + print(f"SE time (3D with singleton axis): {se_3d * 1e3:.3f} ms") + print(f"Max absolute difference in outputs: {float(max_diff):.6f}") + + +if __name__ == "__main__": + main() diff --git a/tests/sparse_death/state_space.py b/tests/sparse_death/state_space.py index 3362ef1a..f41d57be 100644 --- a/tests/sparse_death/state_space.py +++ b/tests/sparse_death/state_space.py @@ -2,7 +2,7 @@ def create_state_space_functions(): """Return dict with state space functions.""" out = { "state_specific_choice_set": state_specific_choice_set, - "next_period_experience": next_period_experience, + "next_period_continuous_state": next_period_continuous_state, "sparsity_condition": sparsity_condition, "next_period_deterministic_state": next_period_deterministic_state, } @@ -103,3 +103,13 @@ def next_period_experience(lagged_choice, experience, model_specs): """If working add one year of experience years (1/exp_scale)""" working = lagged_choice == 2 return experience + working * (1 / model_specs["exp_scale"]) + + +def next_period_continuous_state(lagged_choice, experience, model_specs): + return { + "experience": next_period_experience( + lagged_choice=lagged_choice, + experience=experience, + model_specs=model_specs, + ) + } diff --git a/tests/test_batch_mode_period_max.py b/tests/test_batch_mode_period_max.py new file mode 100644 index 00000000..f4493556 --- /dev/null +++ b/tests/test_batch_mode_period_max.py @@ -0,0 +1,217 @@ +import numpy as np +from numpy.testing import assert_allclose + +import dcegm +from dcegm.toy_models.cons_ret_model_dcegm_paper import ( + inverse_marginal_utility_crra, + marginal_utility_crra, + marginal_utility_final_consume_all, +) +from tests.test_changing_choice_set import ( + budget, + choice_set, + flow_utility, + next_period_state, + prob_health, + prob_partner, + sparsity_condition, + utility_final, +) + + +def _get_model_objects(n_periods): + params = { + "rho": 0.5, + "delta": 1, + "phi": 0.5, + "constant": 1, + "exp": 0.1, + "exp_squared": -0.01, + "pension_per_experience": 0.3, + "unemployment_benefits": 0.4, + "health_costs": 0.5, + "consumption_floor": 0, + "p_bad_health_given_good_health": 0.2, + "p_bad_health_given_bad_health": 1, + "p_partner_given_single": 0.5, + "p_partner_given_partner": 0.9, + } + + model_specs = { + "min_age": 0, + "n_periods": n_periods, + "n_choices": 3, + "n_health_states": 2, + "n_partner_states": 2, + "max_experience": n_periods - 1, + "interest_rate": 0.05, + "discount_factor": 0.95, + "taste_shock_scale": 1, + "income_shock_std": 1, + "income_shock_mean": 0.0, + } + + model_config = { + "n_periods": n_periods, + "choices": np.arange(3), + "deterministic_states": { + "experience": np.arange(n_periods), + }, + "continuous_states": { + "assets_end_of_period": np.linspace(0, 500, 100), + }, + "stochastic_states": { + "health": [0, 1], + "partner": [0, 1], + }, + "n_quad_points": 5, + } + + state_space_functions = { + "state_specific_choice_set": choice_set, + "next_period_deterministic_state": next_period_state, + "sparsity_condition": sparsity_condition, + } + + utility_functions = { + "utility": flow_utility, + "marginal_utility": marginal_utility_crra, + "inverse_marginal_utility": inverse_marginal_utility_crra, + } + + utility_functions_final_period = { + "utility": utility_final, + "marginal_utility": marginal_utility_final_consume_all, + } + + exogenous_states_transition = { + "health": prob_health, + "partner": prob_partner, + } + + return ( + params, + model_specs, + model_config, + state_space_functions, + utility_functions, + utility_functions_final_period, + exogenous_states_transition, + ) + + +def _solve_with_config( + model_config, + model_specs, + params, + state_space_functions, + utility_functions, + utility_functions_final_period, + exogenous_states_transition, +): + model = dcegm.setup_model( + model_config=model_config, + model_specs=model_specs, + state_space_functions=state_space_functions, + utility_functions=utility_functions, + utility_functions_final_period=utility_functions_final_period, + budget_constraint=budget, + stochastic_states_transitions=exogenous_states_transition, + ) + return model.solve(params) + + +def test_period_max_equals_largest_block_without_segments(): + ( + params, + model_specs, + model_config, + state_space_functions, + utility_functions, + utility_functions_final_period, + exogenous_states_transition, + ) = _get_model_objects(n_periods=8) + + model_config_baseline = dict(model_config) + model_config_baseline["batch_mode"] = "largest_block" + + model_config_period_max = dict(model_config) + model_config_period_max["batch_mode"] = "period_max" + + solved_baseline = _solve_with_config( + model_config=model_config_baseline, + model_specs=model_specs, + params=params, + state_space_functions=state_space_functions, + utility_functions=utility_functions, + utility_functions_final_period=utility_functions_final_period, + exogenous_states_transition=exogenous_states_transition, + ) + solved_period_max = _solve_with_config( + model_config=model_config_period_max, + model_specs=model_specs, + params=params, + state_space_functions=state_space_functions, + utility_functions=utility_functions, + utility_functions_final_period=utility_functions_final_period, + exogenous_states_transition=exogenous_states_transition, + ) + + assert_allclose(solved_baseline.value, solved_period_max.value, equal_nan=True) + assert_allclose(solved_baseline.policy, solved_period_max.policy, equal_nan=True) + assert_allclose( + solved_baseline.endog_grid, solved_period_max.endog_grid, equal_nan=True + ) + + +def test_period_max_equals_largest_block_with_segments(): + ( + params, + model_specs, + model_config, + state_space_functions, + utility_functions, + utility_functions_final_period, + exogenous_states_transition, + ) = _get_model_objects(n_periods=8) + + model_config_baseline = dict(model_config) + model_config_baseline["min_period_batch_segments"] = [2, 3] + model_config_baseline["batch_mode"] = [ + "largest_block", + "largest_block", + "largest_block", + ] + + model_config_period_max = dict(model_config) + model_config_period_max["min_period_batch_segments"] = [2, 3] + model_config_period_max["batch_mode"] = [ + "period_max", + "largest_block", + "period_max", + ] + + solved_baseline = _solve_with_config( + model_config=model_config_baseline, + model_specs=model_specs, + params=params, + state_space_functions=state_space_functions, + utility_functions=utility_functions, + utility_functions_final_period=utility_functions_final_period, + exogenous_states_transition=exogenous_states_transition, + ) + solved_period_max = _solve_with_config( + model_config=model_config_period_max, + model_specs=model_specs, + params=params, + state_space_functions=state_space_functions, + utility_functions=utility_functions, + utility_functions_final_period=utility_functions_final_period, + exogenous_states_transition=exogenous_states_transition, + ) + + assert_allclose(solved_baseline.value, solved_period_max.value, equal_nan=True) + assert_allclose(solved_baseline.policy, solved_period_max.policy, equal_nan=True) + assert_allclose( + solved_baseline.endog_grid, solved_period_max.endog_grid, equal_nan=True + ) diff --git a/tests/test_changing_choice_set.py b/tests/test_changing_choice_set.py index 440ebfab..a3b616f6 100644 --- a/tests/test_changing_choice_set.py +++ b/tests/test_changing_choice_set.py @@ -264,7 +264,7 @@ def test_extended_choice_set_model( # We can't use the last period if state_choice_space[i, 0] < 4: # Read out relevant row of arrays and compare first two elements - value_i = value[i] + value_i = value[i, 0] value_expec_i = value_expec_reindexed[i] assert_allclose(value_i[:2], value_expec_i[:2]) # Now check all elements that are not nan in the arrays and do not equal the diff --git a/tests/test_discrete_versus_continuous_experience.py b/tests/test_discrete_versus_continuous_experience.py index 2c65545f..f052a927 100644 --- a/tests/test_discrete_versus_continuous_experience.py +++ b/tests/test_discrete_versus_continuous_experience.py @@ -13,7 +13,7 @@ import dcegm import dcegm.toy_models as toy_models from dcegm.interpolation.interp1d import interp1d_policy_and_value_on_wealth -from dcegm.interpolation.interp2d import ( +from dcegm.interpolation.interp2d_irregular import ( interp2d_policy_and_value_on_wealth_and_regular_grid, ) diff --git a/tests/test_interpnd_regular.py b/tests/test_interpnd_regular.py new file mode 100644 index 00000000..bf65312d --- /dev/null +++ b/tests/test_interpnd_regular.py @@ -0,0 +1,419 @@ +"""Tests for N-dimensional regular-grid interpolation of policy and value functions. + +Validates interpnd_policy, interpnd_value, and joint interpnd_policy_and_value +against scipy.interpolate.RegularGridInterpolator on a two-continuous-state setup +(exp_green x exp_red) with extrapolation. + +Also verifies the consume-all overwrite logic: when consuming all wealth dominates +the interpolated continuation value, the policy must be replaced with the wealth point +(credit-constrained case). + +""" + +import jax.numpy as jnp +import numpy as np +import pytest +from scipy.interpolate import RegularGridInterpolator + +from dcegm.interpolation.interpnd_regular import ( + interpnd_policy_and_value_for_child_states_on_regular_grids, + interpnd_policy_for_child_states_on_regular_grids, + interpnd_value_for_child_states_on_regular_grids, +) + +# ==================================================================================== +# Setup +# ==================================================================================== + + +@pytest.fixture +def interpnd_inputs(): + # Regular grids for two continuous states + shared wealth grid. + exp_green_grid = np.array([0.0, 0.3, 0.8], dtype=float) + exp_red_grid = np.array([0.0, 0.5], dtype=float) + wealth_grid = np.array([0.0, 2.0, 5.0, 9.0], dtype=float) + + n_child_state_choices = 8 + n_cont_combinations = exp_green_grid.size * exp_red_grid.size + n_wealth_eval = 5 + n_quad = 6 + + rng = np.random.default_rng(123) + green_span = exp_green_grid.max() - exp_green_grid.min() + red_span = exp_red_grid.max() - exp_red_grid.min() + wealth_span = wealth_grid.max() - wealth_grid.min() + + # Include both in-grid and out-of-grid points for extrapolation checks. + continuous_state_child_states = { + "exp_green": rng.uniform( + exp_green_grid.min() - 0.3 * green_span, + exp_green_grid.max() + 0.3 * green_span, + size=(n_child_state_choices, n_cont_combinations), + ), + "exp_red": rng.uniform( + exp_red_grid.min() - 0.3 * red_span, + exp_red_grid.max() + 0.3 * red_span, + size=(n_child_state_choices, n_cont_combinations), + ), + } + wealth_child_states = rng.uniform( + wealth_grid.min() - 0.3 * wealth_span, + wealth_grid.max() + 0.3 * wealth_span, + size=(n_child_state_choices, n_cont_combinations, n_wealth_eval, n_quad), + ) + + return { + "exp_green_grid": exp_green_grid, + "exp_red_grid": exp_red_grid, + "wealth_grid": wealth_grid, + "n_child_state_choices": n_child_state_choices, + "n_cont_combinations": n_cont_combinations, + "continuous_state_child_states": continuous_state_child_states, + "wealth_child_states": wealth_child_states, + } + + +def _run_interpnd(policy_grid_child_states, value_grid_child_states, inputs): + return interpnd_policy_for_child_states_on_regular_grids( + additional_continuous_state_grids={ + "exp_green": jnp.asarray(inputs["exp_green_grid"]), + "exp_red": jnp.asarray(inputs["exp_red_grid"]), + }, + wealth_grid=jnp.asarray(inputs["wealth_grid"]), + policy_grid_child_states=jnp.asarray(policy_grid_child_states), + value_grid_child_states=jnp.asarray(value_grid_child_states), + continuous_state_child_states={ + "exp_green": jnp.asarray( + inputs["continuous_state_child_states"]["exp_green"] + ), + "exp_red": jnp.asarray(inputs["continuous_state_child_states"]["exp_red"]), + }, + wealth_child_states=jnp.asarray(inputs["wealth_child_states"]), + state_choice_child_states={ + "choice": jnp.zeros(inputs["n_child_state_choices"], dtype=jnp.int32) + }, + compute_utility=_compute_utility, + params={"u_scale": 2.0}, + discount_factor=0.95, + ) + + +def _scipy_expected(policy_grid_child_states, inputs): + exp_green_grid = inputs["exp_green_grid"] + exp_red_grid = inputs["exp_red_grid"] + wealth_grid = inputs["wealth_grid"] + n_child_state_choices = inputs["n_child_state_choices"] + n_cont_combinations = inputs["n_cont_combinations"] + continuous_state_child_states = inputs["continuous_state_child_states"] + wealth_child_states = inputs["wealth_child_states"] + + expected = np.empty_like(wealth_child_states) + for i in range(n_child_state_choices): + policy_grid_nd = policy_grid_child_states[i].reshape( + exp_green_grid.size, exp_red_grid.size, wealth_grid.size + ) + interp_kwargs = { + "method": "linear", + "bounds_error": False, + "fill_value": None, + } + interp = RegularGridInterpolator( + (exp_green_grid, exp_red_grid, wealth_grid), + policy_grid_nd, + **interp_kwargs, + ) + + for j in range(n_cont_combinations): + for w in range(wealth_child_states.shape[2]): + for q in range(wealth_child_states.shape[3]): + point = np.array( + [ + continuous_state_child_states["exp_green"][i, j], + continuous_state_child_states["exp_red"][i, j], + wealth_child_states[i, j, w, q], + ] + ) + expected[i, j, w, q] = interp(point).item() + return expected + + +def _compute_utility(consumption, params, **kwargs): + return consumption ** params["u_scale"] + + +def _run_interpnd_policy_value( + policy_grid_child_states, value_grid_child_states, inputs +): + return interpnd_policy_and_value_for_child_states_on_regular_grids( + additional_continuous_state_grids={ + "exp_green": jnp.asarray(inputs["exp_green_grid"]), + "exp_red": jnp.asarray(inputs["exp_red_grid"]), + }, + wealth_grid=jnp.asarray(inputs["wealth_grid"]), + policy_grid_child_states=jnp.asarray(policy_grid_child_states), + value_grid_child_states=jnp.asarray(value_grid_child_states), + continuous_state_child_states={ + "exp_green": jnp.asarray( + inputs["continuous_state_child_states"]["exp_green"] + ), + "exp_red": jnp.asarray(inputs["continuous_state_child_states"]["exp_red"]), + }, + wealth_child_states=jnp.asarray(inputs["wealth_child_states"]), + state_choice_child_states={ + "choice": jnp.zeros(inputs["n_child_state_choices"], dtype=jnp.int32) + }, + compute_utility=_compute_utility, + params={"u_scale": 2.0}, + discount_factor=0.95, + ) + + +def _run_interpnd_value_only(value_grid_child_states, inputs): + return interpnd_value_for_child_states_on_regular_grids( + additional_continuous_state_grids={ + "exp_green": jnp.asarray(inputs["exp_green_grid"]), + "exp_red": jnp.asarray(inputs["exp_red_grid"]), + }, + wealth_grid=jnp.asarray(inputs["wealth_grid"]), + value_grid_child_states=jnp.asarray(value_grid_child_states), + continuous_state_child_states={ + "exp_green": jnp.asarray( + inputs["continuous_state_child_states"]["exp_green"] + ), + "exp_red": jnp.asarray(inputs["continuous_state_child_states"]["exp_red"]), + }, + wealth_child_states=jnp.asarray(inputs["wealth_child_states"]), + state_choice_child_states={ + "choice": jnp.zeros(inputs["n_child_state_choices"], dtype=jnp.int32) + }, + compute_utility=_compute_utility, + params={"u_scale": 2.0}, + discount_factor=0.95, + ) + + +def _scipy_expected_value_with_consume_all(value_grid_child_states, inputs): + exp_green_grid = inputs["exp_green_grid"] + exp_red_grid = inputs["exp_red_grid"] + wealth_grid = inputs["wealth_grid"] + n_child_state_choices = inputs["n_child_state_choices"] + n_cont_combinations = inputs["n_cont_combinations"] + continuous_state_child_states = inputs["continuous_state_child_states"] + wealth_child_states = inputs["wealth_child_states"] + + value_interp = np.empty_like(wealth_child_states) + value_at_zero_interp = np.empty( + (n_child_state_choices, n_cont_combinations), dtype=float + ) + + for i in range(n_child_state_choices): + value_grid_nd = value_grid_child_states[i].reshape( + exp_green_grid.size, exp_red_grid.size, wealth_grid.size + ) + interp = RegularGridInterpolator( + (exp_green_grid, exp_red_grid, wealth_grid), + value_grid_nd, + method="linear", + bounds_error=False, + fill_value=None, + ) + interp_v0 = RegularGridInterpolator( + (exp_green_grid, exp_red_grid), + value_grid_nd[..., 0], + method="linear", + bounds_error=False, + fill_value=None, + ) + + for j in range(n_cont_combinations): + point_reg = np.array( + [ + continuous_state_child_states["exp_green"][i, j], + continuous_state_child_states["exp_red"][i, j], + ] + ) + value_at_zero_interp[i, j] = interp_v0(point_reg).item() + + for w in range(wealth_child_states.shape[2]): + for q in range(wealth_child_states.shape[3]): + point = np.array( + [point_reg[0], point_reg[1], wealth_child_states[i, j, w, q]] + ) + value_interp[i, j, w, q] = interp(point).item() + + consume_all_value = ( + wealth_child_states**2 + 0.95 * value_at_zero_interp[:, :, None, None] + ) + return np.maximum(value_interp, consume_all_value) + + +# ==================================================================================== +# Test cases +# ==================================================================================== + + +def test_interpnd_regular_policy_random_against_scipy(interpnd_inputs): + """Policy interpolation matches scipy RegularGridInterpolator with extrapolation.""" + + inputs = interpnd_inputs + rng = np.random.default_rng(321) + + policy_grid_child_states = rng.normal( + loc=0.0, + scale=2.0, + size=( + inputs["n_child_state_choices"], + inputs["n_cont_combinations"], + inputs["wealth_grid"].size, + ), + ) + # Set value grid to very high values to ensure policy interpolation dominates and no overwrite happens. + # Interpolation always yields 1e8 and in consume all we have 0.95 * 1e8 + value_grid_child_states = np.full_like(policy_grid_child_states, 1e8) + out = _run_interpnd(policy_grid_child_states, value_grid_child_states, inputs) + expected = _scipy_expected(policy_grid_child_states, inputs) + np.testing.assert_allclose(np.asarray(out), expected) + + +def test_interpnd_regular_policy_wrapper_matches_policy_from_joint_path( + interpnd_inputs, +): + """Policy-only wrapper returns the same result as the joint policy-and-value + path.""" + + inputs = interpnd_inputs + rng = np.random.default_rng(111) + + policy_grid_child_states = rng.normal( + size=( + inputs["n_child_state_choices"], + inputs["n_cont_combinations"], + inputs["wealth_grid"].size, + ) + ) + value_grid_child_states = rng.normal( + loc=-20.0, + scale=1.0, + size=( + inputs["n_child_state_choices"], + inputs["n_cont_combinations"], + inputs["wealth_grid"].size, + ), + ) + + policy_from_wrapper = _run_interpnd( + policy_grid_child_states, + value_grid_child_states, + inputs, + ) + policy_from_joint, _ = _run_interpnd_policy_value( + policy_grid_child_states, + value_grid_child_states, + inputs, + ) + + np.testing.assert_allclose( + np.asarray(policy_from_wrapper), np.asarray(policy_from_joint) + ) + + +def test_interpnd_regular_policy_wrapper_overwrite_case(interpnd_inputs): + """Policy is overwritten to wealth when consume-all value dominates.""" + + inputs = interpnd_inputs + rng = np.random.default_rng(222) + + policy_grid_child_states = rng.normal( + size=( + inputs["n_child_state_choices"], + inputs["n_cont_combinations"], + inputs["wealth_grid"].size, + ) + ) + # Make value interpolation very low so consume-all dominates. + value_grid_child_states = np.full_like(policy_grid_child_states, -2_000_000.0) + + policy_out = _run_interpnd( + policy_grid_child_states, + value_grid_child_states, + inputs, + ) + + expected_value = _scipy_expected_value_with_consume_all( + value_grid_child_states, inputs + ) + expected_value_interp = _scipy_expected(value_grid_child_states, inputs) + overwrite_mask = expected_value > expected_value_interp + + assert np.any(overwrite_mask) + np.testing.assert_allclose( + np.asarray(policy_out)[overwrite_mask], + np.asarray(inputs["wealth_child_states"])[overwrite_mask], + ) + + +def test_interpnd_regular_policy_and_value_random(interpnd_inputs): + """Joint policy and value interpolation matches scipy with consume-all overwrite.""" + + inputs = interpnd_inputs + rng = np.random.default_rng(654) + + policy_grid_child_states = rng.normal( + size=( + inputs["n_child_state_choices"], + inputs["n_cont_combinations"], + inputs["wealth_grid"].size, + ) + ) + value_grid_child_states = rng.normal( + loc=-20.0, + scale=1.0, + size=( + inputs["n_child_state_choices"], + inputs["n_cont_combinations"], + inputs["wealth_grid"].size, + ), + ) + + policy_out, value_out = _run_interpnd_policy_value( + policy_grid_child_states, + value_grid_child_states, + inputs, + ) + + expected_policy_interp = _scipy_expected(policy_grid_child_states, inputs) + expected_value = _scipy_expected_value_with_consume_all( + value_grid_child_states, inputs + ) + + # If consume-all dominates, policy must be overwritten to wealth. + mask_overwrite = expected_value > _scipy_expected(value_grid_child_states, inputs) + expected_policy = np.where( + mask_overwrite, inputs["wealth_child_states"], expected_policy_interp + ) + + np.testing.assert_allclose(np.asarray(value_out), expected_value) + np.testing.assert_allclose(np.asarray(policy_out), expected_policy) + + +def test_interpnd_regular_value_only_random(interpnd_inputs): + """Value-only interpolation matches scipy with consume-all max applied.""" + + inputs = interpnd_inputs + rng = np.random.default_rng(987) + + value_grid_child_states = rng.normal( + loc=-15.0, + scale=2.0, + size=( + inputs["n_child_state_choices"], + inputs["n_cont_combinations"], + inputs["wealth_grid"].size, + ), + ) + + value_out = _run_interpnd_value_only(value_grid_child_states, inputs) + expected_value = _scipy_expected_value_with_consume_all( + value_grid_child_states, inputs + ) + np.testing.assert_allclose(np.asarray(value_out), expected_value) diff --git a/tests/test_interpolation.py b/tests/test_interpolation.py index d14e5653..4f6874f5 100644 --- a/tests/test_interpolation.py +++ b/tests/test_interpolation.py @@ -17,7 +17,7 @@ from numpy.testing import assert_array_almost_equal as aaae from scipy.interpolate import griddata, interp1d -from dcegm.interpolation.interp2d import ( +from dcegm.interpolation.interp2d_irregular import ( interp2d_policy_on_wealth_and_regular_grid, interp2d_value_on_wealth_and_regular_grid, ) @@ -119,7 +119,6 @@ def functional_form(x, y): compute_utility = determine_function_arguments_and_partial_model_specs( utility_crra, model_specs={}, - continuous_state_name="continuous_state", ) test_cases[test_id]["test_points"] = test_points diff --git a/tests/test_law_of_motion.py b/tests/test_law_of_motion.py index afb5e2a8..23fe7606 100644 --- a/tests/test_law_of_motion.py +++ b/tests/test_law_of_motion.py @@ -191,7 +191,10 @@ def test_wealth_and_second_continuous_state(model_name, max_wealth, n_grid_point ) exp_next = calculate_continuous_state( - child_state_dict, experience_grid, params, _next_period_continuous_state + child_state_dict, + {"continuous_state": experience_grid}, + params, + _next_period_continuous_state, ) aaae(exp_next, experience_next) diff --git a/tests/test_model_config.py b/tests/test_model_config.py index f0bf1a66..c496ff2c 100644 --- a/tests/test_model_config.py +++ b/tests/test_model_config.py @@ -25,7 +25,7 @@ def valid_model_config(): }, }, "n_quad_points": 5, - "tuning_params": {}, + "upper_envelope": {"tuning_params": {}}, } @@ -87,15 +87,21 @@ def test_missing_continuous_states(valid_model_config): def test_tuning_params_defaults(valid_model_config): - del valid_model_config["tuning_params"] + del valid_model_config["upper_envelope"]["tuning_params"] options = check_model_config_and_process(valid_model_config) - assert options["tuning_params"]["extra_wealth_grid_factor"] == 0.2 - assert options["tuning_params"]["n_constrained_points_to_add"] == 1 + assert options["upper_envelope"]["tuning_params"]["extra_wealth_grid_factor"] == 0.2 + assert ( + options["upper_envelope"]["tuning_params"]["n_constrained_points_to_add"] == 1 + ) def test_tuning_params_invalid_grid_factors(valid_model_config): - valid_model_config["tuning_params"]["extra_wealth_grid_factor"] = 0.01 - valid_model_config["tuning_params"]["n_constrained_points_to_add"] = 100 + valid_model_config["upper_envelope"]["tuning_params"][ + "extra_wealth_grid_factor" + ] = 0.01 + valid_model_config["upper_envelope"]["tuning_params"][ + "n_constrained_points_to_add" + ] = 100 with pytest.raises( ValueError, match="The extra wealth grid factor .* is too small" ): @@ -106,7 +112,14 @@ def test_second_continuous_state_handling(valid_model_config): processed_model_config = check_model_config_and_process(valid_model_config) continuous_states_info = processed_model_config["continuous_states_info"] - assert continuous_states_info["second_continuous_state_name"] == "experience" - assert continuous_states_info["n_second_continuous_grid"] == len( - valid_model_config["continuous_states"]["experience"] + assert continuous_states_info["additional_continuous_state_names"] == ["experience"] + assert len( + continuous_states_info["additional_continuous_state_grids"]["experience"] + ) == len(valid_model_config["continuous_states"]["experience"]) + + +def test_upper_envelope_method_default(valid_model_config): + assert ( + check_model_config_and_process(valid_model_config)["upper_envelope"]["method"] + == "fues" ) diff --git a/tests/test_partial_and_interfaces.py b/tests/test_partial_and_interfaces.py index 4ecd52fc..022a9a06 100644 --- a/tests/test_partial_and_interfaces.py +++ b/tests/test_partial_and_interfaces.py @@ -57,7 +57,7 @@ def test_partial_solve_func(): model_solved.model_structure["discrete_states_names"] ) } - states_dict["assets_begin_of_period"] = model_solved.endog_grid[:, 5] + states_dict["assets_begin_of_period"] = model_solved.endog_grid[:, 0, 5] value_states_all_choices = model_solved.choice_values_for_states(states=states_dict) # Take in each row the value corresponding to the choice made @@ -65,7 +65,7 @@ def test_partial_solve_func(): np.arange(value_states_all_choices.shape[0]), choices ] - aaae(model_solved.value[:, 5], value_choices) + aaae(model_solved.value[:, 0, 5], value_choices) # Same for policies policy_states_all_choices = model_solved.choice_policies_for_states( @@ -74,9 +74,9 @@ def test_partial_solve_func(): policy_choices = policy_states_all_choices[ np.arange(policy_states_all_choices.shape[0]), choices ] - aaae(model_solved.policy[:, 5], policy_choices) + aaae(model_solved.policy[:, 0, 5], policy_choices) - model_solved_fast = model.get_solve_func()(params) + model_solved_fast = model.solve(params) aaae(model_solved.value, model_solved_fast.value) aaae(model_solved.policy, model_solved_fast.policy) aaae(model_solved.endog_grid, model_solved_fast.endog_grid) diff --git a/tests/test_pre_processing.py b/tests/test_pre_processing.py index 8a962c74..b8468cc5 100644 --- a/tests/test_pre_processing.py +++ b/tests/test_pre_processing.py @@ -187,8 +187,21 @@ def test_second_continuous_state(period, lagged_choice, continuous_state): model_specs = {"savings_rate": 0.04} params = {} + def next_period_continuous_state( + period, lagged_choice, experience, model_specs, params + ): + return { + "experience": get_next_experience( + period=period, + lagged_choice=lagged_choice, + experience=experience, + model_specs=model_specs, + params=params, + ) + } + state_space_functions = create_state_space_function_dict() - state_space_functions["next_period_experience"] = get_next_experience + state_space_functions["next_period_continuous_state"] = next_period_continuous_state model_config = check_model_config_and_process(model_config) @@ -206,7 +219,7 @@ def test_second_continuous_state(period, lagged_choice, continuous_state): got = next_period_continuous_state( period=period, lagged_choice=lagged_choice, - continuous_state=continuous_state, + experience=continuous_state, model_config=model_config, params=params, ) @@ -218,4 +231,4 @@ def test_second_continuous_state(period, lagged_choice, continuous_state): params=params, ) - np.testing.assert_allclose(got, expected) + np.testing.assert_allclose(got["experience"], expected) diff --git a/tests/test_simulate.py b/tests/test_simulate.py index f6de0330..b8a304b3 100644 --- a/tests/test_simulate.py +++ b/tests/test_simulate.py @@ -116,9 +116,11 @@ def test_simulate_second_continuous_choice(model_setup): noise = jax.random.normal(key, shape=(24, 6, 120)) * 0 model_solved = model_setup["model_solved"] - value = jnp.repeat(model_solved.value[:, None, :], 6, axis=1) + noise - policy = jnp.repeat(model_solved.policy[:, None, :], 6, axis=1) + noise - endog_grid = jnp.repeat(model_solved.endog_grid[:, None, :], 6, axis=1) + noise + value = jnp.repeat(model_solved.value[:, 0, :][:, None, :], 6, axis=1) + noise + policy = jnp.repeat(model_solved.policy[:, 0, :][:, None, :], 6, axis=1) + noise + endog_grid = ( + jnp.repeat(model_solved.endog_grid[:, 0, :][:, None, :], 6, axis=1) + noise + ) n_agents = 100_000 diff --git a/tests/test_two_occupation_model.py b/tests/test_two_occupation_model.py new file mode 100644 index 00000000..62d48f6e --- /dev/null +++ b/tests/test_two_occupation_model.py @@ -0,0 +1,857 @@ +"""Tests for the two-occupation retirement model. + +The agent chooses between two occupations -- red (choice 0) and +green (choice 1) -- or retirement (choice 2, absorbing). Each +occupation accumulates its own experience stock (exp_red, +exp_green), which enters the wage equation. + +Three model variants are compared: (1) discrete experience as +deterministic state variables, (2) continuous experience on an +integer-aligned grid, and (3) continuous experience on an +off-grid (step 1.8) that requires interpolation at integer +query points. + +""" + +import jax +import jax.numpy as jnp +import pytest +from numpy.testing import assert_allclose + +import dcegm + +jax.config.update("jax_enable_x64", True) + +SHOW_DEBUG_PLOTS = False + + +# ==================================================================================== +# Model functions +# ==================================================================================== + + +def flow_util(consumption, choice, params): + rho = params["rho"] + beta_green = params["beta_green"] + beta_red = params["beta_red"] + disutility = beta_red * (choice == 0) + beta_green * (choice == 1) + u = consumption ** (1 - rho) / (1 - rho) - disutility + return u + + +def marginal_utility(consumption, params): + rho = params["rho"] + u_prime = consumption ** (-rho) + return u_prime + + +def inverse_marginal_utility(marginal_utility, params): + rho = params["rho"] + return marginal_utility ** (-1 / rho) + + +def state_specific_choice_set(period, lagged_choice, model_specs): + # Retirement is an absorbing state. + if lagged_choice == 2: + choice_set = [2] + elif period == 4: + choice_set = [2] + else: + choice_set = model_specs["choices"] + return choice_set + + +def final_period_utility(wealth, choice, params): + return flow_util(wealth, choice, params) + + +def marginal_final(wealth, choice, params): + return marginal_utility(wealth, params) + + +def next_period_deterministic_state_cont(period, choice, lagged_choice): + return { + "period": period + 1, + "lagged_choice": choice, + } + + +def next_period_continuous_state(lagged_choice, period, exp_green, exp_red): + return { + "exp_red": exp_red + (lagged_choice == 0), + "exp_green": exp_green + (lagged_choice == 1), + } + + +def budget_constraint_cont_exp( + period, + lagged_choice, + exp_green, + exp_red, + asset_end_of_previous_period, + income_shock_previous_period, + params, +): + interest_factor = 1 + params["interest_rate"] + wage = ( + params["wage_constant"] + + params["wage_exp_green"] * exp_green * (lagged_choice == 1) + + params["wage_exp_red"] * exp_red * (lagged_choice == 0) + ) + resource = ( + interest_factor * asset_end_of_previous_period + + (wage + income_shock_previous_period) * (lagged_choice != 2) + + (wage + income_shock_previous_period) * 0.5 * (lagged_choice == 2) + ) + return jnp.maximum(resource, 0.5) + + +def next_period_deterministic_state_discrete( + period, + choice, + lagged_choice, + exp_green, + exp_red, +): + next_exp_green = exp_green + (choice == 1) + next_exp_red = exp_red + (choice == 0) + return { + "period": period + 1, + "exp_green": next_exp_green, + "exp_red": next_exp_red, + "lagged_choice": choice, + } + + +def sparsity_condition( + period, + lagged_choice, + exp_green, + exp_red, +): + """Define sparsity condition to rule out state space points that are not feasible + given the model structure.""" + # Experience cannot exceed the period + if (exp_green + exp_red) > period: + return False + # If retired, experience sum can not be the same as period + elif (lagged_choice == 2) and ((exp_green + exp_red) == period) & (period > 0): + return False + # # As retirement is absorbing and there is no non-employment, experience sum must be at least one less than period in later periods. + elif (lagged_choice != 2) and ((exp_green + exp_red) < period): + return False + # In later periods, if last period chosen an occupation experience must be positive + elif (lagged_choice == 1) and (exp_green == 0) and (period > 0): + return False + # In later periods, if last period choosen an occupation experience must be positive + elif (lagged_choice == 0) and (exp_red == 0) and (period > 0): + return False + else: + return True + + +def budget_constraint_discrete_exp( + lagged_choice, + exp_green, + exp_red, + asset_end_of_previous_period, + income_shock_previous_period, + params, +): + interest_factor = 1 + params["interest_rate"] + wage = ( + params["wage_constant"] + + params["wage_exp_green"] * exp_green * (lagged_choice == 1) + + params["wage_exp_red"] * exp_red * (lagged_choice == 0) + ) + resource = ( + interest_factor * asset_end_of_previous_period + + (wage + income_shock_previous_period) * (lagged_choice != 2) + + (wage + income_shock_previous_period) * 0.5 * (lagged_choice == 2) + ) + return jnp.maximum(resource, 0.5) + + +# ==================================================================================== +# Function dictionaries for model setup +# ==================================================================================== + +utility_functions = { + "utility": flow_util, + "inverse_marginal_utility": inverse_marginal_utility, + "marginal_utility": marginal_utility, +} + +utility_functions_final_period = { + "utility": final_period_utility, + "marginal_utility": marginal_final, +} + +state_space_functions_cont_exp = { + "state_specific_choice_set": state_specific_choice_set, + "next_period_deterministic_state": next_period_deterministic_state_cont, + "next_period_continuous_state": next_period_continuous_state, +} + +state_space_functions_discrete_exp = { + "state_specific_choice_set": state_specific_choice_set, + "next_period_deterministic_state": next_period_deterministic_state_discrete, + "sparsity_condition": sparsity_condition, +} + + +# ==================================================================================== +# Assertion helpers +# ==================================================================================== + + +def assert_alignment( + solved_a, solved_b, states_a, states_b, choices, policy_atol, value_atol +): + """Assert policy and value alignment between two solved models at given states.""" + policy_a, value_a = solved_a.policy_and_value_for_states_and_choices( + states=states_a, + choices=choices, + ) + policy_b, value_b = solved_b.policy_and_value_for_states_and_choices( + states=states_b, + choices=choices, + ) + finite_policy = jnp.isfinite(policy_a) & jnp.isfinite(policy_b) + finite_value = jnp.isfinite(value_a) & jnp.isfinite(value_b) + + assert jnp.any(finite_policy) + assert jnp.any(finite_value) + + policy_gap = jnp.abs(policy_a - policy_b) + value_gap = jnp.abs(value_a - value_b) + assert_allclose( + float(jnp.mean(policy_gap[finite_policy])), 0.0, atol=policy_atol, rtol=0.0 + ) + assert_allclose( + float(jnp.mean(value_gap[finite_value])), 0.0, atol=value_atol, rtol=0.0 + ) + + +def assert_sim_shares_close(df_a, df_b, column, atol_mean, atol_max): + """Assert distribution of column by period is close between two simulation DFs.""" + shares_a = ( + df_a.groupby("period")[column] + .value_counts(normalize=True) + .unstack(fill_value=0.0) + ) + shares_b = ( + df_b.groupby("period")[column] + .value_counts(normalize=True) + .unstack(fill_value=0.0) + ) + all_cols = sorted(set(shares_a.columns).union(set(shares_b.columns))) + shares_a = shares_a.reindex(columns=all_cols, fill_value=0.0) + shares_b = shares_b.reindex(columns=all_cols, fill_value=0.0) + gap = (shares_a - shares_b).abs() + assert_allclose(float(gap.to_numpy().mean()), 0.0, atol=atol_mean, rtol=0.0) + assert_allclose(float(gap.to_numpy().max()), 0.0, atol=atol_max, rtol=0.0) + + +def assert_sim_means_close(df_a, df_b, column, group_by, atol_mean, atol_max): + """Assert grouped means of column are close between two simulation DFs.""" + if isinstance(group_by, list) and len(group_by) > 1: + means_a = df_a.groupby(group_by)[column].mean().unstack(fill_value=0) + means_b = df_b.groupby(group_by)[column].mean().unstack(fill_value=0) + all_cols = sorted(set(means_a.columns).union(set(means_b.columns))) + means_a = means_a.reindex(columns=all_cols, fill_value=0) + means_b = means_b.reindex(columns=all_cols, fill_value=0) + gap = (means_a - means_b).abs() + assert_allclose(float(gap.to_numpy().mean()), 0.0, atol=atol_mean, rtol=0.0) + assert_allclose(float(gap.to_numpy().max()), 0.0, atol=atol_max, rtol=0.0) + else: + means_a = df_a.groupby(group_by)[column].mean() + means_b = df_b.groupby(group_by)[column].mean() + gap = (means_a - means_b).abs() + assert_allclose(float(gap.mean()), 0.0, atol=atol_mean, rtol=0.0) + assert_allclose(float(gap.max()), 0.0, atol=atol_max, rtol=0.0) + + +def show_debug_plots( + solved_discrete, + solved_cont_exp, + solved_offgrid, + df_discrete, + df_cont_exp, +): + """Visual comparison of policy, value, and choice shares.""" + try: + import matplotlib.pyplot as plt + except ImportError: + pytest.skip("matplotlib needs to be installed to show debug plots") + + wealth_eval = jnp.linspace(0.5, 20.0, 300) + choices_plot = jnp.zeros(wealth_eval.shape[0], dtype=int) + n = wealth_eval.shape[0] + + states_plot_discrete = { + "period": jnp.full(n, 3, dtype=int), + "lagged_choice": jnp.zeros(n, dtype=int), + "exp_green": jnp.ones(n, dtype=int), + "exp_red": jnp.ones(n, dtype=int), + "assets_begin_of_period": wealth_eval, + } + states_plot_cont = { + "period": jnp.full(n, 3, dtype=int), + "lagged_choice": jnp.zeros(n, dtype=int), + "exp_green": jnp.ones(n, dtype=float), + "exp_red": jnp.ones(n, dtype=float), + "assets_begin_of_period": wealth_eval, + } + + policy_discrete_plot, value_discrete_plot = ( + solved_discrete.policy_and_value_for_states_and_choices( + states=states_plot_discrete, + choices=choices_plot, + ) + ) + policy_cont_exact_plot, value_cont_exact_plot = ( + solved_cont_exp.policy_and_value_for_states_and_choices( + states=states_plot_cont, + choices=choices_plot, + ) + ) + policy_cont_offgrid_plot, value_cont_offgrid_plot = ( + solved_offgrid.policy_and_value_for_states_and_choices( + states=states_plot_cont, + choices=choices_plot, + ) + ) + + fig_policy, ax_policy = plt.subplots(figsize=(8, 4.5)) + ax_policy.plot(wealth_eval, policy_discrete_plot, label="discrete") + ax_policy.plot(wealth_eval, policy_cont_exact_plot, label="continuous exact-grid") + ax_policy.plot(wealth_eval, policy_cont_offgrid_plot, label="continuous off-grid") + ax_policy.set_title("Policy By Wealth") + ax_policy.set_xlabel("Assets at beginning of period") + ax_policy.set_ylabel("Consumption") + ax_policy.legend() + + fig_value, ax_value = plt.subplots(figsize=(8, 4.5)) + ax_value.plot(wealth_eval, value_discrete_plot, label="discrete") + ax_value.plot(wealth_eval, value_cont_exact_plot, label="continuous exact-grid") + ax_value.plot(wealth_eval, value_cont_offgrid_plot, label="continuous off-grid") + ax_value.set_title("Value By Wealth") + ax_value.set_xlabel("Assets at beginning of period") + ax_value.set_ylabel("Value") + ax_value.legend() + plt.show() + + choice_shares_discrete = ( + df_discrete.groupby("period") + .choice.value_counts(normalize=True) + .unstack(fill_value=0.0) + ) + choice_shares_discrete.plot( + kind="bar", stacked=True, title="Choice Shares - Discrete Experience" + ) + plt.show() + + fig, ax = plt.subplots(1, 2, figsize=(13, 5)) + df_discrete.groupby(["period", "choice"]).consumption.mean().unstack().fillna( + 0 + ).plot(rot=0, title="Consumption - Discrete Experience Stocks", ax=ax[0]) + df_cont_exp.groupby(["period", "choice"]).consumption.mean().unstack().fillna( + 0 + ).plot(rot=0, title="Consumption - Continuous Experience Stocks", ax=ax[1]) + plt.show() + + fig, ax = plt.subplots(1, 2, figsize=(13, 5)) + df_discrete.groupby("period").exp_green.value_counts(normalize=True).unstack().plot( + stacked=True, + kind="bar", + rot=0, + title="Experience Green - Discrete Experience Stocks", + ax=ax[0], + cmap="Greens", + ) + df_cont_exp.groupby("period").exp_green.value_counts(normalize=True).unstack().plot( + stacked=True, + kind="bar", + rot=0, + title="Experience Green - Continuous Experience Stocks", + ax=ax[1], + cmap="Greens", + ) + plt.show() + + +# ==================================================================================== +# Fixtures +# ==================================================================================== + + +@pytest.fixture(scope="module") +def params(): + return { + "interest_rate": 0.02, + "max_wealth": 50, + "wage_constant": 3, + "wage_exp_green": 0.5, + "wage_exp_red": 0.8, + "income_shock_std": 1, + "income_shock_mean": 0, + "taste_shock_scale": 1, + "discount_factor": 0.95, + "rho": 0.9, + "delta": 1.5, + "beta_green": 0.2, + "beta_red": 0.1, + } + + +@pytest.fixture(scope="module") +def model_specs(): + return {"choices": [0, 1, 2]} + + +@pytest.fixture(scope="module") +def discrete_model(model_specs): + model_config = { + "n_periods": 5, + "choices": [0, 1, 2], + "continuous_states": { + "assets_end_of_period": jnp.linspace(0, 50, 100), + "assets_begin_of_period": jnp.linspace(0, 50, 100), + }, + "deterministic_states": { + "exp_green": jnp.arange(0, 7, dtype=int), + "exp_red": jnp.arange(0, 7, dtype=int), + }, + "n_quad_points": 5, + "upper_envelope": {"method": "druedahl_jorgensen"}, + } + return dcegm.setup_model( + model_config=model_config, + model_specs=model_specs, + utility_functions=utility_functions, + utility_functions_final_period=utility_functions_final_period, + state_space_functions=state_space_functions_discrete_exp, + stochastic_states_transitions={}, + budget_constraint=budget_constraint_discrete_exp, + ) + + +@pytest.fixture(scope="module") +def solved_discrete(discrete_model, params): + return discrete_model.solve(params) + + +@pytest.fixture(scope="module") +def cont_exp_model(model_specs): + model_config = { + "n_periods": 5, + "choices": [0, 1, 2], + "continuous_states": { + "assets_end_of_period": jnp.linspace(0, 50, 100), + "assets_begin_of_period": jnp.linspace(0, 50, 100), + "exp_green": jnp.arange(0, 7, dtype=float), + "exp_red": jnp.arange(0, 7, dtype=float), + }, + "n_quad_points": 5, + "upper_envelope": {"method": "druedahl_jorgensen"}, + } + return dcegm.setup_model( + model_config=model_config, + model_specs=model_specs, + utility_functions=utility_functions, + utility_functions_final_period=utility_functions_final_period, + state_space_functions=state_space_functions_cont_exp, + stochastic_states_transitions={}, + budget_constraint=budget_constraint_cont_exp, + ) + + +@pytest.fixture(scope="module") +def solved_cont_exp(cont_exp_model, params): + return cont_exp_model.solve(params) + + +@pytest.fixture(scope="module") +def offgrid_model(model_specs): + model_config = { + "n_periods": 5, + "choices": [0, 1, 2], + "continuous_states": { + "assets_end_of_period": jnp.linspace(0, 50, 100), + "assets_begin_of_period": jnp.linspace(0, 50, 100), + "exp_green": jnp.arange(0.0, 6.0 + 1e-8, 1.8, dtype=float), + "exp_red": jnp.arange(0.0, 6.0 + 1e-8, 1.8, dtype=float), + }, + "n_quad_points": 5, + "upper_envelope": {"method": "druedahl_jorgensen"}, + } + return dcegm.setup_model( + model_config=model_config, + model_specs=model_specs, + utility_functions=utility_functions, + utility_functions_final_period=utility_functions_final_period, + state_space_functions=state_space_functions_cont_exp, + stochastic_states_transitions={}, + budget_constraint=budget_constraint_cont_exp, + ) + + +@pytest.fixture(scope="module") +def solved_offgrid(offgrid_model, params): + return offgrid_model.solve(params) + + +@pytest.fixture(scope="module") +def initial_states(): + n_agents = 100 + return { + "n_agents": n_agents, + "assets_begin_of_period": jnp.ones(n_agents), + "exp_green": jnp.zeros(n_agents), + "exp_red": jnp.zeros(n_agents), + "lagged_choice": jnp.zeros(n_agents), + "period": jnp.zeros(n_agents, dtype=int), + } + + +@pytest.fixture(scope="module") +def df_discrete(discrete_model, initial_states, params): + simulate = discrete_model.get_solve_and_simulate_func( + states_initial=initial_states, + seed=99, + ) + return simulate(params) + + +@pytest.fixture(scope="module") +def df_cont_exp(cont_exp_model, initial_states, params): + simulate = cont_exp_model.get_solve_and_simulate_func( + states_initial=initial_states, + seed=99, + ) + return simulate(params) + + +@pytest.fixture(scope="module") +def df_offgrid(offgrid_model, initial_states, params): + simulate = offgrid_model.get_solve_and_simulate_func( + states_initial=initial_states, + seed=99, + ) + return simulate(params) + + +@pytest.fixture(scope="module") +def aligned_states(): + """Matched state points for comparing discrete vs continuous models.""" + discrete = { + "period": jnp.array([2, 3, 3], dtype=int), + "lagged_choice": jnp.array([0, 1, 0], dtype=int), + "exp_green": jnp.array([1, 2, 2], dtype=int), + "exp_red": jnp.array([1, 1, 1], dtype=int), + "assets_begin_of_period": jnp.array([3.0, 6.0, 8.0]), + } + continuous = { + "period": discrete["period"], + "lagged_choice": discrete["lagged_choice"], + "exp_green": discrete["exp_green"].astype(float), + "exp_red": discrete["exp_red"].astype(float), + "assets_begin_of_period": discrete["assets_begin_of_period"], + } + choices = jnp.array([0, 1, 0], dtype=int) + return discrete, continuous, choices + + +# ==================================================================================== +# Tests: discrete model interface +# ==================================================================================== + + +def test_discrete_interface_joint_vs_separate(solved_discrete): + """Joint policy+value query matches individual queries for the discrete model.""" + states_eval = { + "period": jnp.array([0, 1, 2], dtype=int), + "lagged_choice": jnp.array([2, 0, 1], dtype=int), + "exp_green": jnp.array([0, 1, 1], dtype=int), + "exp_red": jnp.array([0, 0, 1], dtype=int), + "assets_begin_of_period": jnp.array([0.5, 4.0, 9.0]), + } + choices_eval = jnp.array([2, 0, 1], dtype=int) + + policy_joint, value_joint = solved_discrete.policy_and_value_for_states_and_choices( + states=states_eval, + choices=choices_eval, + ) + policy_only = solved_discrete.policy_for_states_and_choices( + states=states_eval, + choices=choices_eval, + ) + value_only = solved_discrete.value_for_states_and_choices( + states=states_eval, + choices=choices_eval, + ) + + assert jnp.allclose(policy_joint, policy_only, equal_nan=True) + assert jnp.allclose(value_joint, value_only, equal_nan=True) + + +def test_discrete_interface_choice_values_match(solved_discrete): + """Choice-wise value/policy arrays match direct state+choice queries.""" + states_eval = { + "period": jnp.array([0, 1, 2], dtype=int), + "lagged_choice": jnp.array([2, 0, 1], dtype=int), + "exp_green": jnp.array([0, 1, 1], dtype=int), + "exp_red": jnp.array([0, 0, 1], dtype=int), + "assets_begin_of_period": jnp.array([0.5, 4.0, 9.0]), + } + choices_eval = jnp.array([2, 0, 1], dtype=int) + + value_only = solved_discrete.value_for_states_and_choices( + states=states_eval, + choices=choices_eval, + ) + policy_only = solved_discrete.policy_for_states_and_choices( + states=states_eval, + choices=choices_eval, + ) + + choice_values_all = jnp.asarray( + solved_discrete.choice_values_for_states(states=states_eval) + ) + choice_policies_all = jnp.asarray( + solved_discrete.choice_policies_for_states(states=states_eval) + ) + idx = jnp.arange(choices_eval.shape[0]) + assert jnp.allclose( + choice_values_all[idx, choices_eval], value_only, equal_nan=True + ) + assert jnp.allclose( + choice_policies_all[idx, choices_eval], policy_only, equal_nan=True + ) + + +def test_discrete_simulation_runs(df_discrete): + """Discrete model simulation completes and returns a non-empty DataFrame.""" + assert len(df_discrete) > 0 + + +# ==================================================================================== +# Tests: continuous experience model interface +# ==================================================================================== + + +def test_cont_exp_interface_joint_vs_separate(solved_cont_exp): + """Joint policy+value query matches individual queries for the continuous model.""" + states_eval = { + "period": jnp.array([0, 1, 2], dtype=int), + "lagged_choice": jnp.array([2, 0, 1], dtype=int), + "exp_green": jnp.array([0.0, 1.0, 1.0]), + "exp_red": jnp.array([0.0, 0.0, 1.0]), + "assets_begin_of_period": jnp.array([0.5, 4.0, 9.0]), + } + choices_eval = jnp.array([2, 0, 1], dtype=int) + + policy_joint, value_joint = solved_cont_exp.policy_and_value_for_states_and_choices( + states=states_eval, + choices=choices_eval, + ) + policy_only = solved_cont_exp.policy_for_states_and_choices( + states=states_eval, + choices=choices_eval, + ) + value_only = solved_cont_exp.value_for_states_and_choices( + states=states_eval, + choices=choices_eval, + ) + + assert jnp.allclose(policy_joint, policy_only, equal_nan=True) + assert jnp.allclose(value_joint, value_only, equal_nan=True) + + +# ==================================================================================== +# Tests: discrete vs continuous experience alignment +# ==================================================================================== + + +def test_discrete_vs_cont_exp_alignment( + solved_discrete, + solved_cont_exp, + aligned_states, +): + """Policy/value match between discrete and continuous at integer states.""" + states_disc, states_cont, choices = aligned_states + assert_alignment( + solved_discrete, + solved_cont_exp, + states_disc, + states_cont, + choices, + policy_atol=1e-10, + value_atol=2e-2, + ) + + +def test_discrete_vs_cont_exp_simulation(df_discrete, df_cont_exp): + """Simulated choice shares, consumption, and experience agree across models.""" + assert_sim_shares_close( + df_discrete, + df_cont_exp, + "choice", + atol_mean=0.2, + atol_max=0.3, + ) + assert_sim_means_close( + df_discrete, + df_cont_exp, + "consumption", + group_by="period", + atol_mean=0.7, + atol_max=1.0, + ) + assert_sim_means_close( + df_discrete, + df_cont_exp, + "consumption", + group_by=["period", "choice"], + atol_mean=0.5, + atol_max=1.0, + ) + assert_sim_shares_close( + df_discrete, + df_cont_exp, + "exp_green", + atol_mean=0.1, + atol_max=0.2, + ) + + +# ==================================================================================== +# Tests: off-grid continuous experience model +# ==================================================================================== + + +def test_offgrid_vs_discrete_alignment(solved_discrete, solved_offgrid, aligned_states): + """Off-grid model policy/value match discrete at integer states.""" + states_disc, states_cont, choices = aligned_states + # With an off-grid (step 1.8), queried integer-year points require interpolation. + assert_alignment( + solved_discrete, + solved_offgrid, + states_disc, + states_cont, + choices, + policy_atol=1e-2, + value_atol=1e-2, + ) + + +def test_exact_vs_offgrid_probe(solved_cont_exp, solved_offgrid): + """Exact-grid and off-grid models agree across state-choice probes.""" + state_choice_space = solved_cont_exp.model_structure["state_choice_space"] + discrete_state_names = solved_cont_exp.model_structure["discrete_states_names"] + state_choice_cols = [*discrete_state_names, "choice"] + idx_period = state_choice_cols.index("period") + idx_lagged_choice = state_choice_cols.index("lagged_choice") + idx_choice = state_choice_cols.index("choice") + + periods_probe = state_choice_space[:, idx_period].astype(int) + lagged_probe = state_choice_space[:, idx_lagged_choice].astype(int) + choices_probe = state_choice_space[:, idx_choice].astype(int) + + n_probe = periods_probe.shape[0] + probe_idx = jnp.arange(n_probe) + + exp_green_probe = jnp.clip( + 0.35 * periods_probe + + 0.11 * ((probe_idx % 5) - 2) + + 0.07 * (lagged_probe == 1), + 0.0, + 6.0, + ) + exp_red_probe = jnp.clip( + 0.45 * periods_probe + + 0.09 * (((probe_idx * 3) % 7) - 3) + + 0.05 * (lagged_probe == 0), + 0.0, + 6.0, + ) + wealth_probe = jnp.clip( + 1.0 + 2.0 * periods_probe + 0.6 * (probe_idx % 9) + 0.25 * lagged_probe, + 0.5, + 49.5, + ) + + states_probe = { + "period": periods_probe, + "lagged_choice": lagged_probe, + "exp_green": exp_green_probe, + "exp_red": exp_red_probe, + "assets_begin_of_period": wealth_probe, + } + + policy_exact, value_exact = solved_cont_exp.policy_and_value_for_states_and_choices( + states=states_probe, + choices=choices_probe, + ) + policy_offgrid, value_offgrid = ( + solved_offgrid.policy_and_value_for_states_and_choices( + states=states_probe, + choices=choices_probe, + ) + ) + + finite_probe = ( + jnp.isfinite(policy_exact) + & jnp.isfinite(value_exact) + & jnp.isfinite(policy_offgrid) + & jnp.isfinite(value_offgrid) + ) + assert jnp.all(finite_probe) + + policy_gap = jnp.abs(policy_exact - policy_offgrid) + value_gap = jnp.abs(value_exact - value_offgrid) + + assert_allclose(float(jnp.mean(policy_gap)), 0.0, atol=1e-2, rtol=0.0) + assert_allclose(float(jnp.max(policy_gap)), 0.0, atol=2e-1, rtol=0.0) + assert_allclose(float(jnp.mean(value_gap)), 0.0, atol=5e-3, rtol=0.0) + assert_allclose(float(jnp.max(value_gap)), 0.0, atol=2e-2, rtol=0.0) + + +def test_exact_vs_offgrid_simulation(df_cont_exp, df_offgrid): + """Simulated choice shares and consumption agree between exact and off-grid.""" + assert_sim_shares_close( + df_cont_exp, + df_offgrid, + "choice", + atol_mean=1e-2, + atol_max=1.01e-2, + ) + assert_sim_means_close( + df_cont_exp, + df_offgrid, + "consumption", + group_by="period", + atol_mean=2e-2, + atol_max=3e-2, + ) + + +def test_debug_plots( + solved_discrete, + solved_cont_exp, + solved_offgrid, + df_discrete, + df_cont_exp, +): + """Visual debug plots when SHOW_DEBUG_PLOTS is enabled.""" + if not SHOW_DEBUG_PLOTS: + pytest.skip("SHOW_DEBUG_PLOTS is False") + + show_debug_plots( + solved_discrete, + solved_cont_exp, + solved_offgrid, + df_discrete, + df_cont_exp, + ) diff --git a/tests/test_two_period_continuous_experience.py b/tests/test_two_period_continuous_experience.py index a081093c..d4f15423 100644 --- a/tests/test_two_period_continuous_experience.py +++ b/tests/test_two_period_continuous_experience.py @@ -187,6 +187,17 @@ def next_period_experience(period, lagged_choice, experience, params): return (1 / period) * ((period - 1) * experience + (lagged_choice == 0)) +def next_period_continuous_state(period, lagged_choice, experience, params): + return { + "experience": next_period_experience( + period=period, + lagged_choice=lagged_choice, + experience=experience, + params=params, + ) + } + + # ==================================================================================== # Test inputs # ==================================================================================== @@ -201,7 +212,6 @@ def create_test_inputs(): "interest_rate": 0.04, "taste_shock_scale": 1, # taste shock (scale) parameter "income_shock_std": 1, # shock on labor income, standard deviation - "income_shock_mean": 0.0, "income_shock_mean": 0, # shock on labor income, mean "constant": 0.75, "exp": 0.04, @@ -238,7 +248,7 @@ def create_test_inputs(): # ================================================================================= state_space_functions = { - "next_period_experience": next_period_experience, + "next_period_continuous_state": next_period_continuous_state, } model = dcegm.setup_model( @@ -283,7 +293,9 @@ def create_test_inputs(): ], cont_grids_next_period=cont_grids_next_period, continuous_states_info=model_config["continuous_states_info"], + model_structure=model.model_structure, params=params, + upper_envelope_method=model_config["upper_envelope"]["method"], model_funcs=model_funcs_cont, value_solved=value_solved, policy_solved=policy_solved, @@ -307,6 +319,7 @@ def create_test_inputs(): taste_shock_scale_is_scalar=True, income_shock_weights=income_shock_weights, continuous_grids_info=model_config["continuous_states_info"], + continuous_state_space=model.model_structure["continuous_state_space"], model_funcs=model_funcs_cont, debug_info=None, ) @@ -447,10 +460,17 @@ def _get_solve_last_two_periods_args(model, params, has_second_continuous_state) model_funcs=model_funcs, ) - # Create solution containers for value, policy, and endogenous grids - value_solved, policy_solved, endog_grid_solved = create_solution_container( - continuous_states_info=model_config["continuous_states_info"], - n_total_wealth_grid=model_config["tuning_params"]["n_total_wealth_grid"], + n_continuous_state_combinations = model_structure["continuous_state_space"][ + next(iter(model_structure["continuous_state_space"])) + ].shape[0] + ( + value_solved, + policy_solved, + endog_grid_solved, + ) = create_solution_container( + n_continuous_state_combinations=n_continuous_state_combinations, + # Read out grid size + n_total_wealth_grid=model_config["n_total_wealth_grid"], n_state_choices=model_structure["state_choice_space"].shape[0], ) diff --git a/tests/test_two_period_model.py b/tests/test_two_period_model.py index 0f721bfe..fb66e375 100644 --- a/tests/test_two_period_model.py +++ b/tests/test_two_period_model.py @@ -22,6 +22,11 @@ RANDOM_TEST_WEALTH = np.random.choice(list(range(100)), size=10, replace=False) +def _align_dummy_continuous_state_space_with_states(model): + """No-op helper kept for local test compatibility.""" + return + + @pytest.fixture(scope="session") def toy_model_exog_ltc_and_job_offer(): @@ -39,6 +44,7 @@ def toy_model_exog_ltc_and_job_offer(): model_specs=model_specs, **model_funcs, ) + _align_dummy_continuous_state_space_with_states(model) out["marginal_utility"] = model_funcs["utility_functions"]["marginal_utility"] @@ -66,6 +72,7 @@ def toy_model_exog_ltc(): model_specs=model_specs, **model_funcs, ) + _align_dummy_continuous_state_space_with_states(model) out["model_solved"] = model.solve(params) out["marginal_utility"] = model_funcs["utility_functions"]["marginal_utility"] @@ -127,8 +134,8 @@ def test_two_period( initial_conditions["bad_health"] = state_space_dict["ltc"][state_idx] for state_choice_idx in parent_states_of_state: - endog_grid = endog_grid_period[state_choice_idx, wealth_idx + 1] - cons_calc = policy_period[state_choice_idx, wealth_idx + 1] + endog_grid = endog_grid_period[state_choice_idx, 0, wealth_idx + 1] + cons_calc = policy_period[state_choice_idx, 0, wealth_idx + 1] choice = state_choice_space_0[state_choice_idx, -1] if ~np.isnan(endog_grid) and endog_grid > 0: diff --git a/tests/test_utility_second_continuous.py b/tests/test_utility_second_continuous.py index 705fab75..847a3483 100644 --- a/tests/test_utility_second_continuous.py +++ b/tests/test_utility_second_continuous.py @@ -10,7 +10,7 @@ import dcegm import dcegm.toy_models as toy_models from dcegm.interpolation.interp1d import interp1d_policy_and_value_on_wealth -from dcegm.interpolation.interp2d import ( +from dcegm.interpolation.interp2d_irregular import ( interp2d_policy_and_value_on_wealth_and_regular_grid, ) from dcegm.pre_processing.setup_model import create_model_dict @@ -373,7 +373,7 @@ def test_replication_discrete_versus_continuous_experience( policy_cont_interp, value_cont_interp = ( interp2d_policy_and_value_on_wealth_and_regular_grid( - regular_grid=experience_grid, + continuous_state_space={"experience": experience_grid}, wealth_grid=endog_grid_cont[idx_state_choice_cont], policy_grid=policy_cont[idx_state_choice_cont], value_grid=value_cont[idx_state_choice_cont], @@ -388,9 +388,9 @@ def test_replication_discrete_versus_continuous_experience( policy_disc_interp, value_disc_interp = interp1d_policy_and_value_on_wealth( wealth=jnp.array(wealth_to_test), - wealth_grid=endog_grid_disc[idx_state_choice_disc], - policy_grid=policy_disc[idx_state_choice_disc], - value_grid=value_disc[idx_state_choice_disc], + wealth_grid=endog_grid_disc[idx_state_choice_disc, 0], + policy_grid=policy_disc[idx_state_choice_disc, 0], + value_grid=value_disc[idx_state_choice_disc, 0], compute_utility=model_disc.model_funcs["compute_utility"], state_choice_vec=state_choice_disc_dict, params=PARAMS,