@@ -4,7 +4,7 @@ jupytext:
44 extension : .md
55 format_name : myst
66 format_version : 0.13
7- jupytext_version : 1.17.2
7+ jupytext_version : 1.16.6
88kernelspec :
99 display_name : Python 3 (ipykernel)
1010 language : python
@@ -56,53 +56,14 @@ Let's start with some imports:
5656
5757``` {code-cell} ipython3
5858import matplotlib.pyplot as plt
59+ import numpy as np
5960import quantecon as qe
6061import yfinance as yf
61- import jax
62- import jax.numpy as jnp
63- from jax import random, vmap, jit
64- from functools import partial
65- from typing import NamedTuple
6662```
6763
6864Additional technical background related to this lecture can be found in the
6965monograph by {cite}` buraczewski2016stochastic ` .
7066
71- We will use the following general-purpose function for generating time series paths
72-
73- ``` {code-cell} ipython3
74- :tags: [hide-input]
75-
76- @partial(jax.jit, static_argnames=['f', 'num_steps'])
77- def generate_path(f, initial_state, num_steps, model, key):
78- """
79- Generate a time series by repeatedly applying an update rule.
80- Given a map f, initial state x_0, and model parameters θ, this
81- function computes and returns the sequence {x_t}_{t=0}^{T-1} when
82- x_{t+1} = f(x_t, t, θ)
83- Args:
84- f: Update function mapping (x_t, t, model, key) -> x_{t+1}
85- initial_state: Initial state x_0
86- num_steps: Number of time steps T to simulate
87- model: Model parameters
88- key: Random key for reproducible randomness
89- Returns:
90- Array of shape (dim(x), T) containing the time series path
91- [x_0, x_1, x_2, ..., x_{T-1}]
92- """
93- def update_wrapper(carry, t):
94- """Wrapper function that adapts f for use with JAX scan."""
95- state, subkey = carry
96- subkey, new_subkey = random.split(subkey)
97- next_state = f(state, t, model, new_subkey)
98- return (next_state, subkey), state
99-
100- # Initial carry: (initial_state, key)
101- init_carry = (initial_state, key)
102- _, path = jax.lax.scan(update_wrapper, init_carry, jnp.arange(num_steps))
103- return path.T
104- ```
105-
10667## Kesten processes
10768
10869``` {index} single: Kesten processes; heavy tails
@@ -366,49 +327,26 @@ This leads to spikes in the time series, which fill out the extreme right hand t
366327The spikes in the time series are visible in the following simulation, which generates of 10 paths when $a_t$ and $b_t$ are lognormal.
367328
368329``` {code-cell} ipython3
369- class KestenModel(NamedTuple):
370- """Parameters for Kesten process X_{t+1} = a_{t+1} X_t + η_{t+1}"""
371- μ: float = -0.5 # location parameter for log(a_t)
372- σ: float = 1.0 # scale parameter for log(a_t)
330+ μ = -0.5
331+ σ = 1.0
373332
374333
375- @jax.jit
376- def kesten_update(current_x, time_step, model, key):
377- """
378- Update function for Kesten process: X_{t+1} = a_{t+1} X_t + η_{t+1}
379- """
380- # Split key for random number generation
381- key_a, key_η = random.split(key, 2)
334+ def kesten_ts(ts_length=100):
335+ x = np.zeros(ts_length)
336+ for t in range(ts_length - 1):
337+ a = np.exp(μ + σ * np.random.randn())
338+ b = np.exp(np.random.randn())
339+ x[t+1] = a * x[t] + b
340+ return x
382341
383- # Generate random shocks
384- shock_a = random.normal(key_a)
385- shock_η = random.normal(key_η)
386-
387- # Compute a_t and η_t
388- a = jnp.exp(model.μ + model.σ * shock_a)
389- η = jnp.exp(shock_η)
390-
391- # Kesten process update
392- next_x = a * current_x + η
393-
394- return next_x
395342
396343fig, ax = plt.subplots()
397344
398345num_paths = 10
399- model = KestenModel( )
346+ np.random.seed(12 )
400347
401348for i in range(num_paths):
402- key = random.PRNGKey(i)
403-
404- path = generate_path(
405- kesten_update,
406- initial_state=0.0,
407- num_steps=100,
408- model=model,
409- key=key
410- )
411- ax.plot(path)
349+ ax.plot(kesten_ts())
412350
413351ax.set(xlabel="time", ylabel="$X_t$")
414352plt.show()
@@ -508,55 +446,31 @@ While the time path differs, you should see bursts of high volatility.
508446Here is one solution:
509447
510448``` {code-cell} ipython3
511- class GARCHModel(NamedTuple):
512- """Parameters for GARCH(1,1) volatility model"""
513- α_0: float = 1e-5 # constant term
514- α_1: float = 0.1 # coefficient on lagged squared shock
515- β: float = 0.9 # coefficient on lagged volatility
449+ α_0 = 1e-5
450+ α_1 = 0.1
451+ β = 0.9
516452
517453years = 15
518454days = years * 250
519455
520- @jax.jit
521- def garch_update(current_state, time_step, model, key):
522- """Update function for GARCH(1,1) volatility and returns"""
523- σ2_current, r_previous = current_state
524-
525- # Split key for random number generation
526- key_xi, key_zeta = random.split(key, 2)
527-
528- # Generate random shocks
529- ξ = random.normal(key_xi)
530- ζ = random.normal(key_zeta)
531-
532- # Update volatility
533- σ2_next = model.α_0 + σ2_current * (model.α_1 * ξ**2 + model.β)
534456
535- # Generate return
536- r_current = jnp.sqrt(σ2_current) * ζ
457+ def garch_ts(ts_length=days):
458+ σ2 = 0
459+ r = np.zeros(ts_length)
460+ for t in range(ts_length - 1):
461+ ξ = np.random.randn()
462+ σ2 = α_0 + σ2 * (α_1 * ξ**2 + β)
463+ r[t] = np.sqrt(σ2) * np.random.randn()
464+ return r
537465
538- return jnp.array([σ2_next, r_current])
539466
540467fig, ax = plt.subplots()
541468
542- key = random.PRNGKey(0)
543- model = GARCHModel()
469+ np.random.seed(12)
544470
545- # Initial state
546- initial_state = jnp.array([0.0, 0.0])
471+ ax.plot(garch_ts(), alpha=0.7)
547472
548- path = generate_path(
549- garch_update,
550- initial_state=initial_state,
551- num_steps=days,
552- model=model,
553- key=key
554- )
555-
556- # Extract and plot returns
557- ax.plot(path[1, :], alpha=0.7)
558-
559- ax.set(xlabel="time", ylabel="returns")
473+ ax.set(xlabel="time", ylabel="$\\sigma_t^2$")
560474plt.show()
561475```
562476
@@ -753,93 +667,108 @@ s_init = 1.0 # initial condition for each firm
753667:class: dropdown
754668```
755669
756- Here's one solution using the ` generate_path ` framework.
757-
758- First, we define the firm productivity update function:
670+ Here's one solution.
671+ First we generate the observations:
759672
760673``` {code-cell} ipython3
761- @jax.jit
762- def firm_product_update(current_product, time_step, model, key):
763- """
764- Update firm productivity according to entry/exit dynamics.
674+ import jax
675+ import jax.numpy as jnp
676+ from jax import random, vmap, jit
765677
766- If productivity is below threshold: firm exits and is replaced by new entrant
767- If productivity is above threshold: productivity evolves as Kesten process
768- """
769- # Split key for random number generation
770- key_a, key_η, key_e = random.split(key, 3)
771-
772- # Generate random shocks
773- shock_a = random.normal(key_a)
774- shock_η = random.normal(key_η)
775- shock_e = random.normal(key_e)
776-
777- # Calculate potential new productivity values
778- # If firm exits (s_t < s_bar): replaced by new entrant
779- product_entrant = jnp.exp(model.μ_e + model.σ_e * shock_e)
780-
781- # If firm continues (s_t >= s_bar): Kesten process dynamics
782- a = jnp.exp(model.μ_a + model.σ_a * shock_a)
783- η = jnp.exp(model.μ_b + model.σ_b * shock_η)
784- product_incumbent = a * current_product + η
785-
786- # Apply entry/exit rule
787- new_product = jnp.where(
788- current_product < model.s_bar,
789- product_entrant,
790- product_incumbent
791- )
792678
793- return new_product
794- ```
679+ def generate_single_draw(key, μ_a, σ_a, μ_b, σ_b, μ_e, σ_e, s_bar, T, s_init):
680+ """Generate a single draw using JAX's scan for the time loop."""
795681
796- Now we define a model container for parameters
682+ def step_fn(carry, t):
683+ s, subkey = carry
684+ subkey, new_subkey = random.split(subkey)
797685
798- ``` {code-cell} ipython3
799- class FirmDynamicsModel(NamedTuple):
800- """Parameters for firm dynamics with entry/exit"""
801- μ_a: float = -0.5 # location parameter for log(a_t)
802- σ_a: float = 0.1 # scale parameter for log(a_t)
803- μ_b: float = 0.0 # location parameter for log(η_t)
804- σ_b: float = 0.5 # scale parameter for log(η_t)
805- μ_e: float = 0.0 # location parameter for log(e_t)
806- σ_e: float = 0.5 # scale parameter for log(e_t)
807- s_bar: float = 1.0 # exit threshold
808- ```
686+ # Generate random normal samples
687+ rand_normal = random.normal(new_subkey)
688+
689+ # Conditional logic using jnp.where
690+ # If s < s_bar: new_s = exp(μ_e + σ_e * randn())
691+ # Else: new_s = a * s + b
692+ # where a = exp(μ_a + σ_a * randn()), b = exp(μ_b + σ_b * randn())
693+
694+ # For the else branch, we need two random numbers
695+ subkey, key1, key2 = random.split(subkey, 3)
696+ rand_a = random.normal(key1)
697+ rand_b = random.normal(key2)
698+
699+ # Calculate both possible new values
700+ new_s_under_bar = jnp.exp(μ_e + σ_e * rand_normal)
701+
702+ a = jnp.exp(μ_a + σ_a * rand_a)
703+ b = jnp.exp(μ_b + σ_b * rand_b)
704+ new_s_over_bar = a * s + b
705+
706+ # Choose based on condition
707+ new_s = jnp.where(s < s_bar, new_s_under_bar, new_s_over_bar)
708+
709+ return (new_s, subkey), new_s
710+
711+ # Initial state: (s_init, key)
712+ init_carry = (s_init, key)
713+
714+ # Run the scan
715+ final_carry, _ = jax.lax.scan(step_fn, init_carry, jnp.arange(T))
809716
810- Now we generate multiple firm trajectories in parallel
717+ # Return final s value
718+ return final_carry[0]
719+
720+
721+ generate_single_draw = jax.jit(generate_single_draw, static_argnums=(8,))
722+ ```
811723
812724``` {code-cell} ipython3
813- def generate_firm_distribution(model,
814- seed=0, M=1_000_000, T=500, s_init=1.0):
815- """Generate distribution of firm productivities after T periods."""
725+ # Use vmap to vectorize over the first argument (key)
726+ in_axes = [None] * 10
727+ in_axes[0] = 0
816728
817- # Create random keys for each firm
729+ vectorized_single_draw = vmap(
730+ generate_single_draw,
731+ in_axes=in_axes,
732+ )
733+ ```
734+
735+ ``` {code-cell} ipython3
736+ @jit
737+ def generate_draws(
738+ seed=0,
739+ μ_a=-0.5,
740+ σ_a=0.1,
741+ μ_b=0.0,
742+ σ_b=0.5,
743+ μ_e=0.0,
744+ σ_e=0.5,
745+ s_bar=1.0,
746+ T=500,
747+ M=1_000_000,
748+ s_init=1.0,
749+ ):
750+ """
751+ JAX-jit version of the generate_draws function.
752+ Returns:
753+ Array of M draws
754+ """
755+ # Create M different random keys for parallel execution
818756 key = random.PRNGKey(seed)
819757 keys = random.split(key, M)
820758
821- @jax.jit
822- def single_firm_path(firm_key):
823- # Generate path and return final productivity
824- path = generate_path(
825- firm_product_update,
826- initial_state=s_init,
827- num_steps=T,
828- model=model,
829- key=firm_key
830- )
831- return path[-1]
832-
833- # Apply to all firms in parallel
834- product_dist = vmap(single_firm_path)(keys)
759+ draws = vectorized_single_draw(
760+ keys, μ_a, σ_a, μ_b, σ_b, μ_e, σ_e, s_bar, T, s_init
761+ )
835762
836- return product_dist
763+ return draws
764+ ```
837765
838- # Generate the data
839- data = generate_firm_distribution(FirmDynamicsModel())
766+ ``` {code-cell} ipython3
767+ # Generate the observations
768+ data = generate_draws()
840769```
841770
842- Let's produce the rank-size plot
771+ Now we produce the rank-size plot:
843772
844773``` {code-cell} ipython3
845774fig, ax = plt.subplots()
0 commit comments