<jupyter_start><jupyter_text>Learning a Lyapunov Function for the Inverted PendulumConstruct and train a parameterized Lyapunov function for the inverted pendulum system.<jupyter_code>from __future__ import division, print_function

import numpy as np
import tensorflow as tf
import safe_learning
import matplotlib.pyplot as plt
import time
import os

from utilities import LyapunovNetwork, InvertedPendulum, balanced_class_weights, binary_cmap, compute_roa, monomials

# Nice progress bars
try:
    from tqdm import tqdm
except ImportError:
    tqdm = lambda x: x<jupyter_output><empty_output><jupyter_text>User Options<jupyter_code>class Options(object):
    def __init__(self, **kwargs):
        super(Options, self).__init__()
        self.__dict__.update(kwargs)

OPTIONS = Options(np_dtype              = safe_learning.config.np_dtype,
                  tf_dtype              = safe_learning.config.dtype,
                  eps                   = 1e-8,                            # numerical tolerance
                  saturate              = True,                            # apply saturation constraints to the control input
                  use_zero_threshold    = True,                            # assume the discretization is infinitely fine (i.e., tau = 0)
                  pre_train             = True,                            # pre-train the neural network to match a given candidate in a supervised approach
                  dpi                   = 150,
                  num_cores             = 4,
                  num_sockets           = 1,
                  tf_checkpoint_path    = "./tmp/lyapunov_function_learning.ckpt")<jupyter_output><empty_output><jupyter_text>TensorFlow SessionCustomize the TensorFlow session for the current device.<jupyter_code>os.environ["KMP_BLOCKTIME"]    = str(0)
os.environ["KMP_SETTINGS"]     = str(1)
os.environ["KMP_AFFINITY"]     = 'granularity=fine,noverbose,compact,1,0'
os.environ["OMP_NUM_THREADS"]  = str(OPTIONS.num_cores)

config = tf.ConfigProto(intra_op_parallelism_threads  = OPTIONS.num_cores,
                        inter_op_parallelism_threads  = OPTIONS.num_sockets,
                        allow_soft_placement          = False,
                        device_count                  = {'CPU': OPTIONS.num_cores})

try:
    session.close()
except NameError:
    pass
session = tf.InteractiveSession(config=config)

# Set random seed to reproduce results
seed = 1
tf.set_random_seed(seed)
np.random.seed(seed)<jupyter_output><empty_output><jupyter_text>DynamicsDefine the nonlinear and linearized forms of the inverted pendulum dynamics.<jupyter_code># Constants
dt = 0.01   # sampling time
g = 9.81    # gravity

# True system parameters
m = 0.15    # pendulum mass
L = 0.5     # pole length
b = 0.1     # rotational friction

# State and action normalizers
theta_max = np.deg2rad(180)                     # angular position [rad]
omega_max = np.deg2rad(360)                     # angular velocity [rad/s]
u_max     = g * m * L * np.sin(np.deg2rad(60))  # torque [N.m], control action

state_norm = (theta_max, omega_max)
action_norm = (u_max,)

# Dimensions and domains
state_dim     = 2
action_dim    = 1
state_limits  = np.array([[-1., 1.]] * state_dim)
action_limits = np.array([[-1., 1.]] * action_dim)

# Initialize system class and its linearization
pendulum = InvertedPendulum(m, L, b, dt, [state_norm, action_norm])
A, B = pendulum.linearize()
dynamics = pendulum.__call__<jupyter_output><empty_output><jupyter_text>State Discretization and Initial Safe SetDefine a uniform discretization, and an initial known safe set as a subset of this discretization.<jupyter_code># Number of states along each dimension
num_states = 251

# State grid
grid_limits = np.array([[-1., 1.], ] * state_dim)
state_discretization = safe_learning.GridWorld(grid_limits, num_states)

# Discretization constant
if OPTIONS.use_zero_threshold:
    tau = 0.0
else:
    tau = np.sum(state_discretization.unit_maxes) / 2

print('Grid size: {}'.format(state_discretization.nindex))
print('Discretization constant (tau): {}'.format(tau))

# Set initial safe set as a ball around the origin (in normalized coordinates)
cutoff_radius    = 0.1
initial_safe_set = np.linalg.norm(state_discretization.all_points, ord=2, axis=1) <= cutoff_radius<jupyter_output><empty_output><jupyter_text>Fixed PolicyFix the policy to the LQR solution for the linearized, discretized system, possibly with saturation constraints.<jupyter_code>Q = np.identity(state_dim).astype(OPTIONS.np_dtype)     # state cost matrix
R = np.identity(action_dim).astype(OPTIONS.np_dtype)    # action cost matrix
K, P_lqr = safe_learning.utilities.dlqr(A, B, Q, R)

policy = safe_learning.LinearSystem(- K, name='policy')
if OPTIONS.saturate:
    policy = safe_learning.Saturation(policy, -1, 1)<jupyter_output><empty_output><jupyter_text>Closed-Loop Dynamics Lipschitz Constant<jupyter_code># # Policy (linear)
L_pol = lambda x: np.linalg.norm(-K, 1)

# # Dynamics (linear approximation)
L_dyn = lambda x: np.linalg.norm(A, 1) + np.linalg.norm(B, 1) * L_pol(x)<jupyter_output><empty_output><jupyter_text>LQR Lyapunov CandidateDefine a Lyapunov candidate function corresponding to the LQR solution for the linearized system.<jupyter_code># Define the Lyapunov function corresponding to the LQR policy
lyapunov_function = safe_learning.QuadraticFunction(P_lqr)

# Approximate local Lipschitz constants with gradients
grad_lyapunov_function = safe_learning.LinearSystem((2 * P_lqr,))
L_v = lambda x: tf.norm(grad_lyapunov_function(x), ord=1, axis=1, keepdims=True)

# Initialize Lyapunov class
lyapunov_lqr = safe_learning.Lyapunov(state_discretization, lyapunov_function, dynamics, L_dyn, L_v, tau, policy, initial_safe_set)
lyapunov_lqr.update_values()
lyapunov_lqr.update_safe_set()<jupyter_output><empty_output><jupyter_text>SOS Lyapunov CandidateDefine a sixth-order polynomial Lyapunov candidate with the solution from SOSTOOLS in MatLab.<jupyter_code>def lyapunov_sos(x):
    Z = monomials(x, 3)
    Q = np.array([
                [       1.33,    0.00373,   5.933e-9,  -2.75e-10, -4.251e-11,     0.1402,   0.005985,  0.0001077,   0.001279],
                [    0.00373,    0.00882,  -2.75e-10, -4.251e-11,  -6.36e-11,   0.005985,  0.0001077,   0.001279,   0.000889],
                [   5.933e-9,  -2.75e-10,     0.1402,   0.005985,  0.0001077, -1.847e-10,  4.928e-12, -8.276e-13,  1.789e-12],
                [  -2.75e-10, -4.251e-11,   0.005985,  0.0001077,   0.001279,  4.928e-12, -8.276e-13,  1.789e-12, -1.937e-13],
                [ -4.251e-11,  -6.36e-11,  0.0001077,   0.001279,   0.000889, -8.276e-13,  1.789e-12, -1.937e-13, -3.181e-13],
                [     0.1402,   0.005985, -1.847e-10,  4.928e-12, -8.276e-13,   0.004209, -0.0003288,   5.837e-5,  -3.517e-5],
                [   0.005985,  0.0001077,  4.928e-12, -8.276e-13,  1.789e-12, -0.0003288,   5.837e-5,  -3.517e-5,  -3.109e-5],
                [  0.0001077,   0.001279, -8.276e-13,  1.789e-12, -1.937e-13,   5.837e-5,  -3.517e-5,  -3.109e-5,   4.497e-6],
                [   0.001279,   0.000889,  1.789e-12, -1.937e-13, -3.181e-13,  -3.517e-5,  -3.109e-5,   4.497e-6,   2.013e-5]])
    linear_form = np.matmul(Z, Q)
    quadratic = np.sum(linear_form * Z, axis=1, keepdims=True)
    return quadratic


values_sos = lyapunov_sos(state_discretization.all_points).ravel()
future = dynamics(state_discretization.all_points, policy(state_discretization.all_points)).eval()
future_values_sos = lyapunov_sos(future).ravel()
dv_sos = future_values_sos - values_sos

order = np.argsort(values_sos)
ordered_idx = np.argmax(dv_sos[order][1:] >= 0) + 1
max_idx = order[ordered_idx - 1]
cmax_sos = values_sos[max_idx]<jupyter_output><empty_output><jupyter_text>Neural Network Lyapunov CandidateDefine a parameterized Lyapunov candidate function with a neural network.<jupyter_code>layer_dims = [64, 64, 64]
activations = [tf.tanh, tf.tanh, tf.tanh]
lyapunov_function = LyapunovNetwork(state_dim, layer_dims, activations, OPTIONS.eps)

# Approximate local Lipschitz constants with gradients
grad_lyapunov_function = lambda x: tf.gradients(lyapunov_function(x), x)[0]
L_v = lambda x: tf.norm(grad_lyapunov_function(x), ord=1, axis=1, keepdims=True)

# Initialize parameters; need to use the template before parameter variables exist in the TensorFlow graph
temp = tf.placeholder(OPTIONS.tf_dtype, shape=[None, state_dim], name='states')
temp = lyapunov_function(temp)
session.run(tf.variables_initializer(lyapunov_function.parameters))

# Initialize Lyapunov class
lyapunov_nn = safe_learning.Lyapunov(state_discretization, lyapunov_function, dynamics, L_dyn, L_v, tau, policy, initial_safe_set)
lyapunov_nn.update_values()
lyapunov_nn.update_safe_set()<jupyter_output><empty_output><jupyter_text>TensorFlow Graph<jupyter_code># Dynamics
tf_states           = tf.placeholder(OPTIONS.tf_dtype, shape=[None, state_dim], name='states')
tf_actions          = policy(tf_states)
tf_future_states    = dynamics(tf_states, tf_actions)

# Neural network
tf_values_nn        = lyapunov_nn.lyapunov_function(tf_states)
tf_future_values_nn = lyapunov_nn.lyapunov_function(tf_future_states)
tf_dv_nn            = tf_future_values_nn - tf_values_nn
tf_threshold        = lyapunov_nn.threshold(tf_states, lyapunov_nn.tau)
tf_negative         = tf.squeeze(tf.less(tf_dv_nn, tf_threshold), axis=1)

# LQR
tf_values_lqr        = lyapunov_lqr.lyapunov_function(tf_states)
tf_future_values_lqr = lyapunov_lqr.lyapunov_function(tf_future_states)
tf_dv_lqr            = tf_future_values_lqr - tf_values_lqr<jupyter_output><empty_output><jupyter_text>True Region of AttractionCompute the true largest region of attraction (ROA), denoted as $\mathcal{S}_\pi$, by forward-simulating the closed-loop dynamics.<jupyter_code>closed_loop_dynamics = lambda x: tf_future_states.eval({tf_states: x})
horizon = 500
tol = 0.1
roa, trajectories = compute_roa(lyapunov_nn.discretization, closed_loop_dynamics, horizon, tol, no_traj=False)<jupyter_output><empty_output><jupyter_text>Neural Network Pre-TrainingPre-train on a spherical Lyapunov function to make sure an initial safe set exists.<jupyter_code>if OPTIONS.pre_train:
    obj = []
    level_states = state_discretization.all_points[initial_safe_set]
    
    # Spherical candidate
    P = 0.1 * np.eye(state_dim)
    lyapunov_function      = safe_learning.QuadraticFunction(P)
    grad_lyapunov_function = safe_learning.LinearSystem((2 * P,))
    L_v                    = lambda x: tf.norm(grad_lyapunov_function(x), ord=1, axis=1, keepdims=True)

    # Initialize class
    lyapunov_pre = safe_learning.Lyapunov(state_discretization, lyapunov_function, dynamics, L_dyn, L_v, tau, policy, initial_safe_set)
    lyapunov_pre.update_values()
    lyapunov_pre.update_safe_set()
    
    # TensorFlow graph elements
    tf_values_pre        = lyapunov_pre.lyapunov_function(tf_states)
    tf_future_values_pre = lyapunov_pre.lyapunov_function(tf_future_states)
    tf_dv_pre            = tf_future_values_pre - tf_values_pre
    
    with tf.name_scope('lyapunov_pre_training'):
        tf_losses        = tf.abs(tf_values_nn - tf_values_pre) # / tf.stop_gradient(tf_values_pre + OPTIONS.eps)
        tf_objective     = tf.reduce_mean(tf_losses, name='objective')
        tf_learning_rate = tf.placeholder(OPTIONS.tf_dtype, shape=[], name='learning_rate')
        optimizer        = tf.train.GradientDescentOptimizer(tf_learning_rate)
        lyapunov_update  = optimizer.minimize(tf_objective, var_list=lyapunov_nn.lyapunov_function.parameters)
        
        tf_batch_size = tf.placeholder(tf.int32, [], 'batch_size')
        tf_batch      = tf.random_uniform([tf_batch_size, ], 0, level_states.shape[0], dtype=tf.int32, name='batch_sample')

if OPTIONS.pre_train:
    # Test set
    test_size = int(1e3)
    idx       = tf_batch.eval({tf_batch_size: int(1e3)})
    test_set  = level_states[idx, :]

    feed_dict = {
        tf_states:         level_states,
        tf_learning_rate:  1e-1,
        tf_batch_size:     int(1e3),
    }
    max_iters = 300

    for i in tqdm(range(max_iters)):
        idx = tf_batch.eval(feed_dict)
        feed_dict[tf_states] = level_states[idx, :]
        session.run(lyapunov_update, feed_dict)

        feed_dict[tf_states] = test_set
        obj.append(tf_objective.eval(feed_dict))
        
    lyapunov_nn.update_values()
    lyapunov_nn.update_safe_set()

if OPTIONS.pre_train:
    fig, ax = plt.subplots(1, 1, figsize=(3, 2), dpi=OPTIONS.dpi)
    ax.set_xlabel(r'iteration')
    ax.set_ylabel(r'objective')
    ax.plot(obj, '.-r')
    plt.show()<jupyter_output><empty_output><jupyter_text>Neural Network TrainingTrain the parameteric Lyapunov candidate in order to expand the verifiable safe set towards $\mathcal{S}_\pi$.<jupyter_code># Save TensorFlow checkpoint for the neural network parameters
saver = tf.train.Saver(var_list=lyapunov_nn.lyapunov_function.parameters)
ckpt_path = saver.save(session, OPTIONS.tf_checkpoint_path)

with tf.name_scope('roa_classification'):
    # Target the safe level set to extend out towards
    safe_level = tf.placeholder(OPTIONS.tf_dtype, shape=[], name='c_max')
    level_multiplier = tf.placeholder(OPTIONS.tf_dtype, shape=[], name='level_multiplier')
    
    # True class labels, converted from Boolean ROA labels {0, 1} to {-1, 1}
    roa_labels = tf.placeholder(OPTIONS.tf_dtype, shape=[None, 1], name='labels')
    class_labels = 2 * roa_labels - 1

    # Signed, possibly normalized distance from the decision boundary
    decision_distance = safe_level - tf_values_nn
    
    # Perceptron loss with class weights
    class_weights = tf.placeholder(OPTIONS.tf_dtype, shape=[None, 1], name='class_weights')
    classifier_loss = class_weights * tf.maximum(- class_labels * decision_distance, 0, name='classifier_loss')
    
    # Enforce decrease constraint with Lagrangian relaxation
    lagrange_multiplier = tf.placeholder(OPTIONS.tf_dtype, shape=[], name='lagrange_multiplier')
    decrease_loss = roa_labels * tf.maximum(tf_dv_nn, 0) / tf.stop_gradient(tf_values_nn + OPTIONS.eps)
        
    # Construct objective and optimizer
    objective = tf.reduce_mean(classifier_loss + lagrange_multiplier * decrease_loss, name='objective')
    learning_rate = tf.placeholder(OPTIONS.tf_dtype, shape=[], name='learning_rate')
    optimizer = tf.train.GradientDescentOptimizer(learning_rate)
    training_update = optimizer.minimize(objective, var_list=lyapunov_nn.lyapunov_function.parameters)

with tf.name_scope('sampling'):
    batch_size = tf.placeholder(tf.int32, [], 'batch_size')
    idx_range = tf.placeholder(tf.int32, shape=[], name='indices_to_sample')
    idx_batch = tf.random_uniform([batch_size, ], 0, idx_range, dtype=tf.int32, name='batch_sample')<jupyter_output><empty_output><jupyter_text>InitializationRestore parameter checkpoint, and try training again.<jupyter_code>saver.restore(session, ckpt_path)
lyapunov_nn.update_values()
lyapunov_nn.update_safe_set()

test_classifier_loss = []
test_decrease_loss   = []
roa_estimate         = np.copy(lyapunov_nn.safe_set)

grid              = lyapunov_nn.discretization
c_max             = [lyapunov_nn.feed_dict[lyapunov_nn.c_max], ]
safe_set_fraction = [lyapunov_nn.safe_set.sum() / grid.nindex, ]<jupyter_output><empty_output><jupyter_text>Training<jupyter_code># Training hyperparameters
outer_iters = 20
inner_iters = 10
horizon     = 100
test_size   = int(1e4)

feed_dict = {
    tf_states:           np.zeros((1, grid.ndim)), # placeholder
    safe_level:          1.,
    lagrange_multiplier: 1000,
    #
    level_multiplier:    1.2,
    learning_rate:       6e-3,
    batch_size:          int(1e3),
}

print('Current metrics ...')
c = lyapunov_nn.feed_dict[lyapunov_nn.c_max]
num_safe = lyapunov_nn.safe_set.sum()
print('Safe level (c_k): {}'.format(c))
print('Safe set size: {} ({:.2f}% of grid, {:.2f}% of ROA)\n'.format(int(num_safe), 100 * num_safe / grid.nindex, 100 * num_safe / roa.sum()))
print('')
time.sleep(0.5)

for _ in range(outer_iters):
    print('Iteration (k): {}'.format(len(c_max)))
    time.sleep(0.5)
    
    # Identify the "gap" states, i.e., those between V(c_k) and V(a * c_k) for a > 1
    c = lyapunov_nn.feed_dict[lyapunov_nn.c_max]
    idx_small = lyapunov_nn.values.ravel() <= c
    idx_big = lyapunov_nn.values.ravel() <= feed_dict[level_multiplier] * c
    idx_gap = np.logical_and(idx_big, ~idx_small)
    
    # Forward-simulate "gap" states to determine which ones we can add to our ROA estimate
    gap_states = grid.all_points[idx_gap]
    for _ in range(horizon):
        gap_states = tf_future_states.eval({tf_states: gap_states})
    gap_future_values = tf_values_nn.eval({tf_states: gap_states})    
    roa_estimate[idx_gap] |= (gap_future_values <= c).ravel()
    
    # Identify the class labels for our current ROA estimate and the expanded level set
    target_idx = np.logical_or(idx_big, roa_estimate)
    target_set = grid.all_points[target_idx]
    target_labels = roa_estimate[target_idx].astype(OPTIONS.np_dtype).reshape([-1, 1])
    feed_dict[idx_range] = target_set.shape[0]
    
    # Test set
    idx_test = idx_batch.eval({batch_size: test_size, idx_range: target_set.shape[0]})
    test_set = target_set[idx_test]
    test_labels = target_labels[idx_test]

    # SGD for classification
    for _ in tqdm(range(inner_iters)):
        # Training step
        idx_batch_eval = idx_batch.eval(feed_dict)
        feed_dict[tf_states] = target_set[idx_batch_eval]
        feed_dict[roa_labels] = target_labels[idx_batch_eval]
        feed_dict[class_weights], class_counts = balanced_class_weights(feed_dict[roa_labels].astype(bool))
        session.run(training_update, feed_dict=feed_dict)

        # Record losses on test set
        feed_dict[tf_states] = test_set
        feed_dict[roa_labels] = test_labels
        feed_dict[class_weights], class_counts = balanced_class_weights(feed_dict[roa_labels].astype(bool))
        results = session.run([classifier_loss, decrease_loss], feed_dict)
        test_classifier_loss.append(results[0].mean())
        test_decrease_loss.append(results[1].mean())

    # Update Lyapunov values and ROA estimate, based on new parameter values
    lyapunov_nn.update_values()
    lyapunov_nn.update_safe_set()
    roa_estimate |= lyapunov_nn.safe_set

    c_max.append(lyapunov_nn.feed_dict[lyapunov_nn.c_max])
    safe_set_fraction.append(lyapunov_nn.safe_set.sum() / grid.nindex)
    print('Current safe level (c_k): {}'.format(c_max[-1]))
    print('Safe set size: {} ({:.2f}% of grid, {:.2f}% of ROA)\n'.format(int(lyapunov_nn.safe_set.sum()), 
                                                                         100 * safe_set_fraction[-1], 
                                                                         100 * safe_set_fraction[-1] * roa.size / roa.sum()))<jupyter_output><empty_output><jupyter_text>Results<jupyter_code>fig = plt.figure(figsize=(8, 3), dpi=OPTIONS.dpi, frameon=False)
fig.subplots_adjust(wspace=0.2, hspace=0.3)
plot_limits = np.column_stack((- np.rad2deg([theta_max, omega_max]), np.rad2deg([theta_max, omega_max])))

ax = plt.subplot(121)
colors = [(0,114/255,178/255), (213/255,94/255,0), (0,158/255,115/255), (240/255,228/255,66/255)]

# True ROA
z = roa.reshape(grid.num_points)
ax.imshow(z.T, origin='lower', extent=plot_limits.ravel(), cmap=binary_cmap(colors[0]), alpha=1, vmin=0)

# Neural network
z = lyapunov_nn.safe_set.reshape(grid.num_points)
ax.imshow(z.T, origin='lower', extent=plot_limits.ravel(), cmap=binary_cmap(colors[1]), alpha=1, vmin=0)

# LQR
z = lyapunov_lqr.safe_set.reshape(grid.num_points)
ax.imshow(z.T, origin='lower', extent=plot_limits.ravel(), cmap=binary_cmap(colors[2]), alpha=1, vmin=0)

# SOS
z = (values_sos <= cmax_sos).reshape(grid.num_points)
ax.contour(z.T, origin='lower', extent=plot_limits.ravel(), colors=(colors[3],), linewidths=2)
ax.imshow(z.T, origin='lower', extent=plot_limits.ravel(), cmap=binary_cmap(colors[3]), alpha=1, vmin=0)

# Plot some trajectories
N_traj = 11
skip = int(grid.num_points[0] / N_traj)
sub_idx = np.arange(grid.nindex).reshape(grid.num_points)
sub_idx = sub_idx[::skip, ::skip].ravel()
sub_trajectories = trajectories[sub_idx, :, :]
sub_states = grid.all_points[sub_idx]
for n in range(sub_trajectories.shape[0]):
    x = sub_trajectories[n, 0, :] * np.rad2deg(theta_max)
    y = sub_trajectories[n, 1, :] * np.rad2deg(omega_max)
    ax.plot(x, y, 'k--', linewidth=0.25)
sub_states = grid.all_points[sub_idx]
dx_dt = (tf_future_states.eval({tf_states: sub_states}) - sub_states) / dt
dx_dt = dx_dt / np.linalg.norm(dx_dt, ord=2, axis=1, keepdims=True)
ax.quiver(sub_states[:, 0] * np.rad2deg(theta_max), sub_states[:, 1] * np.rad2deg(omega_max), dx_dt[:, 0], dx_dt[:, 1], 
          scale=None, pivot='mid', headwidth=3, headlength=6, color='k')

ax.set_aspect(theta_max / omega_max)
ax.set_xlim(plot_limits[0])
ax.set_ylim(plot_limits[1])
ax.set_xlabel(r'angle [deg]')
ax.set_ylabel(r'angular velocity [deg/s]')

proxy = [plt.Rectangle((0,0), 1, 1, fc=c) for c in colors]    
legend = ax.legend(proxy, [r'$\mathcal{S}_\pi$', r'NN', r'LQR', r'SOS'], loc='upper right')
legend.get_frame().set_alpha(1.)

# Plot safe growth over the iterations
ax = plt.subplot(222)
ax.plot(c_max, '.-', color=colors[1])
ax.set_ylabel(r'safe level $c_k$')
ax.set_ylim([0, None])
plt.setp(ax.get_xticklabels(), visible=False)

roa_fraction = roa.sum() / roa.size
ax = plt.subplot(224)
ax.plot(np.array(safe_set_fraction) / roa_fraction, '.-', color=colors[1])
ax.set_ylabel(r'fraction of $S_\pi$')
ax.set_ylim([0, 1])
ax.set_xlabel(r'safe level set update iteration $k$')

plt.show()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.119988928357245, 'fraction_numerical': 0.0364441574018545, 'mean_word_length': 3.8670857656039517, 'pattern_counts': {'":': 0, '<': 74, '<?xml version=': 0, '>': 71, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '17', 'dir': 'jupyter-structured-clean-dedup', 'id': '363112', 'license': 'MIT', 'n_tokens_mistral': 8423, 'n_tokens_neox': 7773, 'n_words': 1447, 'path': 'examples/lyapunov_function_learning.ipynb', 'repo_name': 'weiqiao/safe_learning', 'size': '21677'}