# /// script
# requires-python = ">=3.12"
# dependencies = ["marimo==0.25.2"]
# ///
# Generated by scripts/learn-notebooks/build.py; standalone and editable.
import marimo

__generated_with = "0.25.2"
app = marimo.App(width="medium")


@app.cell
def _():
    import marimo as mo
    import math
    import struct
    from fractions import Fraction
    import json
    import inspect
    import hashlib
    from datetime import datetime, timezone
    from copy import deepcopy
    return Fraction, datetime, deepcopy, hashlib, inspect, json, math, mo, struct, timezone


@app.cell
def _():
    protocol = {'version': 2,
     'title': 'Learning rate on synthetic linear regression',
     'reference': {'rate': 0.25, 'start': -2, 'steps': 20},
     'controls': [{'key': 'rate',
                   'label': 'Learning rate',
                   'options': [{'value': 0.01, 'label': '0.01'},
                               {'value': 0.05, 'label': '0.05'},
                               {'value': 0.1, 'label': '0.1'},
                               {'value': 0.25, 'label': '0.25'},
                               {'value': 0.5, 'label': '0.5'},
                               {'value': 1, 'label': '1'},
                               {'value': 1.25, 'label': '1.25'},
                               {'value': 1.5, 'label': '1.5'},
                               {'value': 2, 'label': '2'}]},
                  {'key': 'start',
                   'label': 'Starting slope',
                   'options': [{'value': -4, 'label': '-4'},
                               {'value': -2, 'label': '-2'},
                               {'value': 0, 'label': '0'},
                               {'value': 3, 'label': '3'},
                               {'value': 4, 'label': '4'}]},
                  {'key': 'steps',
                   'label': 'Number of updates',
                   'options': [{'value': 5, 'label': '5'},
                               {'value': 10, 'label': '10'},
                               {'value': 20, 'label': '20'},
                               {'value': 40, 'label': '40'}]}],
     'fixed': '300 synthetic observations: y = 2x + 1 + uniform noise in [−0.6, 0.6), seed 42. Fixed '
              'split: 240 training / 60 validation. Model ŷ = wx + b; initial b = 0. Full-batch '
              'gradient descent uses training rows only. Loss = ½ mean squared error. Every run starts '
              'afresh; no preprocessing or resampling.',
     'sources': [{'title': 'How neural networks learn',
                  'url': 'https://indepth.dev/posts/1001/en/how-neural-networks-learn-backpropagation-gradient-descent'},
                 {'title': 'The effect of hyperparameters on training',
                  'url': 'https://indepth.dev/posts/1012/en/effect-of-hyperparameters-on-training'}],
     'id': 'learning-rate',
     'guided': 'Inspect the 300 records and fixed 240/60 train/validation split. Keep starting slope '
               '−2, intercept 0, and 20 updates fixed. Predict, then test rates 0.01, 1, 1.5, and 2 '
               'against the 0.25 reference. Compare both losses and intermediate slopes/intercepts.',
     'extension': 'Choose two starting slopes at a fixed rate and budget. Then compare 10 versus 40 '
                  'updates at rate 0.05 as a separate investigation. For a code extension, scale x in '
                  'every training and validation record inside calculate. Keep the transformed data '
                  'fixed within each rate comparison, record the scale, and predict whether the same '
                  'rate remains stable.',
     'reasoning': 'Training gradients average error*x and error across only the 240 training rows. At '
                  'rate 0.25 after 20 updates, training loss is about 0.06156 and validation loss '
                  'about 0.06321. Rate 0.01 is slow, 1 is faster here, 1.5 oscillates while '
                  'converging, and 2 diverges. Noisy targets leave a nonzero best training loss. '
                  'Validation does not enter updates; using it to select rates makes it unsuitable as '
                  'an unbiased final test. Stability depends on the training inputs and loss scale, '
                  'not just the generating slope 2.',
     'referenceDataset': {'id': 'learning-rate',
                          'version': 2,
                          'settings': {'rate': 0.25, 'start': -2, 'steps': 20},
                          'title': 'Synthetic linear regression',
                          'unit': 'observation',
                          'description': '300 observations generated from y = 2x + 1 + noise, with x '
                                         'sampled uniformly from [−2, 2) and noise uniformly from '
                                         '[−0.6, 0.6). A fixed shuffled split assigns 240 rows to '
                                         'training and 60 to validation.',
                          'columns': [{'key': 'id',
                                       'label': 'Row',
                                       'type': 'category',
                                       'role': 'identifier',
                                       'description': 'Stable identifier within this synthetic '
                                                      'fixture.'},
                                      {'key': 'x',
                                       'label': 'Input x',
                                       'type': 'number',
                                       'role': 'feature',
                                       'description': 'Raw input, rounded to six decimals. No fitted '
                                                      'normalization.'},
                                      {'key': 'target',
                                       'label': 'Target y',
                                       'type': 'number',
                                       'role': 'target',
                                       'description': '2x + 1 plus sampled noise, rounded to six '
                                                      'decimals.'},
                                      {'key': 'split',
                                       'label': 'Split',
                                       'type': 'category',
                                       'role': 'condition',
                                       'description': 'Training rows determine gradients; validation '
                                                      'rows are used only to evaluate the fitted '
                                                      'line.'}],
                          'rows': [{'id': '001', 'x': -0.990619, 'target': -1.475488, 'split': 'train'},
                                   {'id': '002', 'x': 0.309125, 'target': 1.285315, 'split': 'train'},
                                   {'id': '003', 'x': -0.497359, 'target': -0.563921, 'split': 'train'},
                                   {'id': '004', 'x': -0.210875, 'target': 0.120402, 'split': 'train'},
                                   {'id': '005', 'x': 1.495255, 'target': 4.584071, 'split': 'train'},
                                   {'id': '006', 'x': 1.412811, 'target': 3.825234, 'split': 'train'},
                                   {'id': '007', 'x': 0.568004, 'target': 2.569755, 'split': 'train'},
                                   {'id': '008', 'x': 0.385879, 'target': 1.280658, 'split': 'train'},
                                   {'id': '009',
                                    'x': -1.439161,
                                    'target': -1.338216,
                                    'split': 'validation'},
                                   {'id': '010', 'x': 1.698222, 'target': 4.863807, 'split': 'train'},
                                   {'id': '011', 'x': 0.202033, 'target': 1.020686, 'split': 'train'},
                                   {'id': '012', 'x': 0.200342, 'target': 1.111444, 'split': 'train'},
                                   {'id': '013', 'x': 1.772487, 'target': 4.930775, 'split': 'train'},
                                   {'id': '014', 'x': -1.378824, 'target': -1.362374, 'split': 'train'},
                                   {'id': '015', 'x': -0.132311, 'target': 0.208224, 'split': 'train'},
                                   {'id': '016', 'x': -1.910179, 'target': -2.775611, 'split': 'train'},
                                   {'id': '017',
                                    'x': 1.319841,
                                    'target': 4.054081,
                                    'split': 'validation'},
                                   {'id': '018', 'x': 0.723672, 'target': 2.304254, 'split': 'train'},
                                   {'id': '019', 'x': 0.342627, 'target': 1.920931, 'split': 'train'},
                                   {'id': '020', 'x': 0.845462, 'target': 2.953266, 'split': 'train'},
                                   {'id': '021', 'x': 1.96115, 'target': 4.862003, 'split': 'train'},
                                   {'id': '022', 'x': -1.608294, 'target': -2.586675, 'split': 'train'},
                                   {'id': '023', 'x': -0.210486, 'target': 0.131429, 'split': 'train'},
                                   {'id': '024', 'x': 1.07506, 'target': 3.262585, 'split': 'train'},
                                   {'id': '025', 'x': 1.218576, 'target': 3.304446, 'split': 'train'},
                                   {'id': '026', 'x': -1.136924, 'target': -1.547377, 'split': 'train'},
                                   {'id': '027', 'x': -1.923661, 'target': -3.113756, 'split': 'train'},
                                   {'id': '028', 'x': -0.664127, 'target': 0.131185, 'split': 'train'},
                                   {'id': '029', 'x': 1.823512, 'target': 4.278262, 'split': 'train'},
                                   {'id': '030', 'x': 1.485204, 'target': 4.069196, 'split': 'train'},
                                   {'id': '031', 'x': -0.653276, 'target': -0.529359, 'split': 'train'},
                                   {'id': '032', 'x': -1.482563, 'target': -1.389162, 'split': 'train'},
                                   {'id': '033', 'x': -1.588109, 'target': -1.670402, 'split': 'train'},
                                   {'id': '034', 'x': 1.22294, 'target': 3.365427, 'split': 'train'},
                                   {'id': '035', 'x': 0.840241, 'target': 2.44848, 'split': 'train'},
                                   {'id': '036', 'x': -1.525627, 'target': -2.040174, 'split': 'train'},
                                   {'id': '037', 'x': -1.530501, 'target': -2.368766, 'split': 'train'},
                                   {'id': '038', 'x': 0.416381, 'target': 2.070151, 'split': 'train'},
                                   {'id': '039', 'x': -0.03728, 'target': 0.577049, 'split': 'train'},
                                   {'id': '040', 'x': -1.090798, 'target': -0.948456, 'split': 'train'},
                                   {'id': '041', 'x': 1.527288, 'target': 4.434927, 'split': 'train'},
                                   {'id': '042', 'x': 0.788677, 'target': 2.984726, 'split': 'train'},
                                   {'id': '043',
                                    'x': -0.393437,
                                    'target': -0.203085,
                                    'split': 'validation'},
                                   {'id': '044', 'x': -1.494839, 'target': -1.941902, 'split': 'train'},
                                   {'id': '045', 'x': 0.606689, 'target': 2.798763, 'split': 'train'},
                                   {'id': '046', 'x': -0.296149, 'target': 0.704147, 'split': 'train'},
                                   {'id': '047', 'x': -0.278942, 'target': -0.103236, 'split': 'train'},
                                   {'id': '048', 'x': -1.69099, 'target': -1.99111, 'split': 'train'},
                                   {'id': '049',
                                    'x': 1.914925,
                                    'target': 5.189492,
                                    'split': 'validation'},
                                   {'id': '050', 'x': -1.325395, 'target': -1.786876, 'split': 'train'},
                                   {'id': '051', 'x': -0.867311, 'target': -0.746755, 'split': 'train'},
                                   {'id': '052', 'x': 1.363847, 'target': 4.224216, 'split': 'train'},
                                   {'id': '053',
                                    'x': -1.209679,
                                    'target': -1.640124,
                                    'split': 'validation'},
                                   {'id': '054',
                                    'x': 0.120136,
                                    'target': 0.957186,
                                    'split': 'validation'},
                                   {'id': '055', 'x': -0.384549, 'target': -0.354182, 'split': 'train'},
                                   {'id': '056', 'x': 0.591921, 'target': 2.02386, 'split': 'train'},
                                   {'id': '057', 'x': -0.102382, 'target': 0.501029, 'split': 'train'},
                                   {'id': '058', 'x': 0.373949, 'target': 1.988774, 'split': 'train'},
                                   {'id': '059', 'x': 0.211161, 'target': 1.546591, 'split': 'train'},
                                   {'id': '060', 'x': -1.072557, 'target': -1.289315, 'split': 'train'},
                                   {'id': '061', 'x': -0.326714, 'target': 0.869764, 'split': 'train'},
                                   {'id': '062', 'x': -1.058334, 'target': -0.611675, 'split': 'train'},
                                   {'id': '063', 'x': 1.799771, 'target': 4.685172, 'split': 'train'},
                                   {'id': '064',
                                    'x': 1.699402,
                                    'target': 3.829234,
                                    'split': 'validation'},
                                   {'id': '065', 'x': -0.979163, 'target': -0.909061, 'split': 'train'},
                                   {'id': '066', 'x': 0.008841, 'target': 1.560377, 'split': 'train'},
                                   {'id': '067', 'x': 1.919678, 'target': 5.088498, 'split': 'train'},
                                   {'id': '068', 'x': 1.264372, 'target': 3.299315, 'split': 'train'},
                                   {'id': '069',
                                    'x': 0.256135,
                                    'target': 1.734795,
                                    'split': 'validation'},
                                   {'id': '070', 'x': -1.801226, 'target': -2.795341, 'split': 'train'},
                                   {'id': '071', 'x': -0.161958, 'target': 0.9957, 'split': 'train'},
                                   {'id': '072',
                                    'x': 1.889328,
                                    'target': 4.224596,
                                    'split': 'validation'},
                                   {'id': '073', 'x': 0.302711, 'target': 1.369303, 'split': 'train'},
                                   {'id': '074', 'x': 0.580319, 'target': 2.362181, 'split': 'train'},
                                   {'id': '075', 'x': -0.128908, 'target': 0.447925, 'split': 'train'},
                                   {'id': '076', 'x': 0.884458, 'target': 2.776203, 'split': 'train'},
                                   {'id': '077', 'x': -1.862926, 'target': -2.312492, 'split': 'train'},
                                   {'id': '078', 'x': 0.925701, 'target': 3.336333, 'split': 'train'},
                                   {'id': '079', 'x': 0.954446, 'target': 2.953201, 'split': 'train'},
                                   {'id': '080', 'x': 1.985977, 'target': 5.551316, 'split': 'train'},
                                   {'id': '081', 'x': 0.484023, 'target': 2.309869, 'split': 'train'},
                                   {'id': '082',
                                    'x': -1.781674,
                                    'target': -2.511023,
                                    'split': 'validation'},
                                   {'id': '083', 'x': 0.463415, 'target': 1.626261, 'split': 'train'},
                                   {'id': '084', 'x': 0.988936, 'target': 2.406754, 'split': 'train'},
                                   {'id': '085',
                                    'x': 1.761963,
                                    'target': 4.46161,
                                    'split': 'validation'},
                                   {'id': '086', 'x': -0.589406, 'target': -0.08846, 'split': 'train'},
                                   {'id': '087',
                                    'x': 0.283322,
                                    'target': 1.003398,
                                    'split': 'validation'},
                                   {'id': '088', 'x': -0.369963, 'target': 0.367572, 'split': 'train'},
                                   {'id': '089',
                                    'x': -1.721788,
                                    'target': -2.874945,
                                    'split': 'validation'},
                                   {'id': '090', 'x': -1.568288, 'target': -2.428893, 'split': 'train'},
                                   {'id': '091', 'x': -0.870187, 'target': -0.667688, 'split': 'train'},
                                   {'id': '092',
                                    'x': -0.279555,
                                    'target': 1.010661,
                                    'split': 'validation'},
                                   {'id': '093', 'x': 0.674321, 'target': 2.466984, 'split': 'train'},
                                   {'id': '094', 'x': -0.554493, 'target': 0.13152, 'split': 'train'},
                                   {'id': '095', 'x': -0.880835, 'target': -0.263064, 'split': 'train'},
                                   {'id': '096', 'x': 1.528599, 'target': 4.123331, 'split': 'train'},
                                   {'id': '097',
                                    'x': 0.621376,
                                    'target': 2.540957,
                                    'split': 'validation'},
                                   {'id': '098', 'x': 0.358234, 'target': 1.44946, 'split': 'train'},
                                   {'id': '099', 'x': -1.411392, 'target': -2.142694, 'split': 'train'},
                                   {'id': '100',
                                    'x': -1.586971,
                                    'target': -2.363084,
                                    'split': 'validation'},
                                   {'id': '101',
                                    'x': -0.234678,
                                    'target': 0.864682,
                                    'split': 'validation'},
                                   {'id': '102', 'x': 0.576398, 'target': 2.545557, 'split': 'train'},
                                   {'id': '103', 'x': -0.833374, 'target': -0.435621, 'split': 'train'},
                                   {'id': '104', 'x': -0.294838, 'target': 0.235359, 'split': 'train'},
                                   {'id': '105', 'x': 0.922266, 'target': 2.872337, 'split': 'train'},
                                   {'id': '106', 'x': 1.014555, 'target': 3.250469, 'split': 'train'},
                                   {'id': '107', 'x': -0.906721, 'target': -0.450106, 'split': 'train'},
                                   {'id': '108', 'x': 1.84214, 'target': 5.038809, 'split': 'train'},
                                   {'id': '109', 'x': 1.523797, 'target': 3.641931, 'split': 'train'},
                                   {'id': '110', 'x': 0.353133, 'target': 1.725983, 'split': 'train'},
                                   {'id': '111', 'x': 0.790511, 'target': 1.991212, 'split': 'train'},
                                   {'id': '112', 'x': -0.25037, 'target': -0.037782, 'split': 'train'},
                                   {'id': '113', 'x': 1.312291, 'target': 3.297632, 'split': 'train'},
                                   {'id': '114', 'x': -0.453758, 'target': 0.661112, 'split': 'train'},
                                   {'id': '115',
                                    'x': 0.682462,
                                    'target': 2.491151,
                                    'split': 'validation'},
                                   {'id': '116',
                                    'x': -1.525363,
                                    'target': -2.051071,
                                    'split': 'validation'},
                                   {'id': '117', 'x': 0.286232, 'target': 2.087127, 'split': 'train'},
                                   {'id': '118',
                                    'x': -1.420515,
                                    'target': -2.256819,
                                    'split': 'validation'},
                                   {'id': '119', 'x': 1.265784, 'target': 4.092845, 'split': 'train'},
                                   {'id': '120', 'x': 1.068315, 'target': 2.679271, 'split': 'train'},
                                   {'id': '121', 'x': -1.104483, 'target': -0.866048, 'split': 'train'},
                                   {'id': '122',
                                    'x': 1.477772,
                                    'target': 4.403923,
                                    'split': 'validation'},
                                   {'id': '123', 'x': 1.429949, 'target': 3.369573, 'split': 'train'},
                                   {'id': '124',
                                    'x': 0.718783,
                                    'target': 2.954157,
                                    'split': 'validation'},
                                   {'id': '125', 'x': 0.486772, 'target': 2.064842, 'split': 'train'},
                                   {'id': '126', 'x': 1.563132, 'target': 4.440576, 'split': 'train'},
                                   {'id': '127', 'x': 1.027565, 'target': 2.989611, 'split': 'train'},
                                   {'id': '128', 'x': 1.133934, 'target': 2.953674, 'split': 'train'},
                                   {'id': '129', 'x': -0.001336, 'target': 1.112075, 'split': 'train'},
                                   {'id': '130', 'x': -1.037475, 'target': -1.488028, 'split': 'train'},
                                   {'id': '131', 'x': -0.305314, 'target': -0.043928, 'split': 'train'},
                                   {'id': '132',
                                    'x': 1.17425,
                                    'target': 3.570677,
                                    'split': 'validation'},
                                   {'id': '133',
                                    'x': -1.092786,
                                    'target': -1.509187,
                                    'split': 'validation'},
                                   {'id': '134', 'x': -1.634342, 'target': -1.89036, 'split': 'train'},
                                   {'id': '135',
                                    'x': 1.921129,
                                    'target': 4.721759,
                                    'split': 'validation'},
                                   {'id': '136', 'x': -0.264389, 'target': 0.019381, 'split': 'train'},
                                   {'id': '137', 'x': -1.578891, 'target': -1.748549, 'split': 'train'},
                                   {'id': '138',
                                    'x': -0.96721,
                                    'target': -0.996116,
                                    'split': 'validation'},
                                   {'id': '139',
                                    'x': 0.370779,
                                    'target': 1.578992,
                                    'split': 'validation'},
                                   {'id': '140',
                                    'x': 0.882112,
                                    'target': 2.51896,
                                    'split': 'validation'},
                                   {'id': '141', 'x': -1.072985, 'target': -0.593045, 'split': 'train'},
                                   {'id': '142', 'x': 1.052553, 'target': 2.564894, 'split': 'train'},
                                   {'id': '143', 'x': 1.451908, 'target': 3.524465, 'split': 'train'},
                                   {'id': '144', 'x': -0.771125, 'target': -0.747858, 'split': 'train'},
                                   {'id': '145',
                                    'x': 1.684887,
                                    'target': 4.735207,
                                    'split': 'validation'},
                                   {'id': '146', 'x': -0.15024, 'target': 0.158827, 'split': 'train'},
                                   {'id': '147', 'x': 1.137487, 'target': 3.19909, 'split': 'train'},
                                   {'id': '148', 'x': 1.574855, 'target': 3.598787, 'split': 'train'},
                                   {'id': '149', 'x': 0.773286, 'target': 2.056352, 'split': 'train'},
                                   {'id': '150', 'x': 0.943216, 'target': 2.618907, 'split': 'train'},
                                   {'id': '151', 'x': -0.964508, 'target': -0.497172, 'split': 'train'},
                                   {'id': '152', 'x': 0.785662, 'target': 2.044504, 'split': 'train'},
                                   {'id': '153',
                                    'x': 1.284687,
                                    'target': 3.185137,
                                    'split': 'validation'},
                                   {'id': '154', 'x': -1.046466, 'target': -0.808486, 'split': 'train'},
                                   {'id': '155',
                                    'x': -1.290774,
                                    'target': -1.715452,
                                    'split': 'validation'},
                                   {'id': '156', 'x': 1.926366, 'target': 5.164225, 'split': 'train'},
                                   {'id': '157', 'x': -0.229205, 'target': 0.55826, 'split': 'train'},
                                   {'id': '158', 'x': 0.860948, 'target': 2.357289, 'split': 'train'},
                                   {'id': '159',
                                    'x': -1.752376,
                                    'target': -2.493138,
                                    'split': 'validation'},
                                   {'id': '160', 'x': 0.644314, 'target': 2.880812, 'split': 'train'},
                                   {'id': '161', 'x': -1.062144, 'target': -1.26329, 'split': 'train'},
                                   {'id': '162', 'x': 1.513121, 'target': 4.127212, 'split': 'train'},
                                   {'id': '163',
                                    'x': -0.91443,
                                    'target': -0.314028,
                                    'split': 'validation'},
                                   {'id': '164', 'x': 1.801941, 'target': 4.798643, 'split': 'train'},
                                   {'id': '165', 'x': 0.591968, 'target': 2.783111, 'split': 'train'},
                                   {'id': '166', 'x': -0.61704, 'target': -0.586295, 'split': 'train'},
                                   {'id': '167', 'x': 1.074206, 'target': 3.206511, 'split': 'train'},
                                   {'id': '168', 'x': -1.640419, 'target': -2.787791, 'split': 'train'},
                                   {'id': '169',
                                    'x': 0.834396,
                                    'target': 3.114065,
                                    'split': 'validation'},
                                   {'id': '170', 'x': 0.576431, 'target': 2.476726, 'split': 'train'},
                                   {'id': '171',
                                    'x': -0.198752,
                                    'target': 0.911389,
                                    'split': 'validation'},
                                   {'id': '172', 'x': -1.491718, 'target': -2.410294, 'split': 'train'},
                                   {'id': '173', 'x': -0.493117, 'target': 0.027499, 'split': 'train'},
                                   {'id': '174', 'x': -0.909633, 'target': -1.15514, 'split': 'train'},
                                   {'id': '175',
                                    'x': -1.838705,
                                    'target': -3.233364,
                                    'split': 'validation'},
                                   {'id': '176', 'x': 1.052616, 'target': 2.93163, 'split': 'train'},
                                   {'id': '177',
                                    'x': -0.113035,
                                    'target': 0.705011,
                                    'split': 'validation'},
                                   {'id': '178', 'x': 1.320285, 'target': 3.511417, 'split': 'train'},
                                   {'id': '179', 'x': 0.176144, 'target': 1.801245, 'split': 'train'},
                                   {'id': '180', 'x': -0.956504, 'target': -0.795626, 'split': 'train'},
                                   {'id': '181', 'x': -1.366721, 'target': -1.710797, 'split': 'train'},
                                   {'id': '182',
                                    'x': -1.181808,
                                    'target': -1.394488,
                                    'split': 'validation'},
                                   {'id': '183', 'x': -0.934897, 'target': -0.277981, 'split': 'train'},
                                   {'id': '184', 'x': -0.339018, 'target': 0.648814, 'split': 'train'},
                                   {'id': '185', 'x': 1.980879, 'target': 5.036034, 'split': 'train'},
                                   {'id': '186', 'x': 1.300406, 'target': 3.338182, 'split': 'train'},
                                   {'id': '187', 'x': -0.434699, 'target': 0.603411, 'split': 'train'},
                                   {'id': '188',
                                    'x': 0.347098,
                                    'target': 1.363908,
                                    'split': 'validation'},
                                   {'id': '189', 'x': 0.186548, 'target': 1.142163, 'split': 'train'},
                                   {'id': '190', 'x': 1.905362, 'target': 4.623459, 'split': 'train'},
                                   {'id': '191',
                                    'x': -1.497999,
                                    'target': -1.747633,
                                    'split': 'validation'},
                                   {'id': '192', 'x': 1.188645, 'target': 3.965291, 'split': 'train'},
                                   {'id': '193',
                                    'x': 0.224109,
                                    'target': 1.647288,
                                    'split': 'validation'},
                                   {'id': '194', 'x': -1.298573, 'target': -1.225525, 'split': 'train'},
                                   {'id': '195', 'x': -0.391084, 'target': 0.048178, 'split': 'train'},
                                   {'id': '196', 'x': 1.550009, 'target': 4.518863, 'split': 'train'},
                                   {'id': '197', 'x': 0.72974, 'target': 2.211496, 'split': 'train'},
                                   {'id': '198', 'x': 0.444712, 'target': 1.474551, 'split': 'train'},
                                   {'id': '199', 'x': -1.401485, 'target': -2.342691, 'split': 'train'},
                                   {'id': '200', 'x': 0.955978, 'target': 2.549388, 'split': 'train'},
                                   {'id': '201', 'x': 1.197283, 'target': 3.367601, 'split': 'train'},
                                   {'id': '202', 'x': 0.586884, 'target': 2.423291, 'split': 'train'},
                                   {'id': '203', 'x': -1.517657, 'target': -2.57674, 'split': 'train'},
                                   {'id': '204', 'x': 0.224554, 'target': 1.72409, 'split': 'train'},
                                   {'id': '205', 'x': 1.42356, 'target': 3.648491, 'split': 'train'},
                                   {'id': '206', 'x': -0.467018, 'target': 0.657682, 'split': 'train'},
                                   {'id': '207', 'x': -0.858674, 'target': -0.368607, 'split': 'train'},
                                   {'id': '208', 'x': -1.150238, 'target': -0.788766, 'split': 'train'},
                                   {'id': '209', 'x': -0.796317, 'target': -0.810531, 'split': 'train'},
                                   {'id': '210',
                                    'x': -0.850777,
                                    'target': -0.251823,
                                    'split': 'validation'},
                                   {'id': '211', 'x': 1.146092, 'target': 3.048792, 'split': 'train'},
                                   {'id': '212', 'x': 0.634467, 'target': 2.113738, 'split': 'train'},
                                   {'id': '213', 'x': -1.723631, 'target': -2.994396, 'split': 'train'},
                                   {'id': '214', 'x': 1.52082, 'target': 4.069796, 'split': 'train'},
                                   {'id': '215', 'x': -0.172726, 'target': 1.139092, 'split': 'train'},
                                   {'id': '216', 'x': -0.426817, 'target': 0.577751, 'split': 'train'},
                                   {'id': '217',
                                    'x': -1.48075,
                                    'target': -2.055935,
                                    'split': 'validation'},
                                   {'id': '218', 'x': -0.040207, 'target': 0.54822, 'split': 'train'},
                                   {'id': '219',
                                    'x': 1.59583,
                                    'target': 3.952332,
                                    'split': 'validation'},
                                   {'id': '220', 'x': 1.984887, 'target': 4.776255, 'split': 'train'},
                                   {'id': '221',
                                    'x': 0.394187,
                                    'target': 1.311471,
                                    'split': 'validation'},
                                   {'id': '222', 'x': -1.737221, 'target': -2.119119, 'split': 'train'},
                                   {'id': '223', 'x': -1.486648, 'target': -1.510203, 'split': 'train'},
                                   {'id': '224', 'x': 0.895833, 'target': 2.967658, 'split': 'train'},
                                   {'id': '225',
                                    'x': -0.35742,
                                    'target': -0.02377,
                                    'split': 'validation'},
                                   {'id': '226',
                                    'x': -1.781151,
                                    'target': -2.766943,
                                    'split': 'validation'},
                                   {'id': '227', 'x': 1.112379, 'target': 3.143858, 'split': 'train'},
                                   {'id': '228', 'x': -1.32764, 'target': -2.170616, 'split': 'train'},
                                   {'id': '229', 'x': 0.230686, 'target': 1.545892, 'split': 'train'},
                                   {'id': '230', 'x': -0.632682, 'target': -0.791028, 'split': 'train'},
                                   {'id': '231', 'x': 1.466711, 'target': 3.412823, 'split': 'train'},
                                   {'id': '232', 'x': 1.903709, 'target': 5.066469, 'split': 'train'},
                                   {'id': '233', 'x': 1.544926, 'target': 4.419481, 'split': 'train'},
                                   {'id': '234', 'x': -0.994444, 'target': -0.902124, 'split': 'train'},
                                   {'id': '235',
                                    'x': -1.488281,
                                    'target': -1.909227,
                                    'split': 'validation'},
                                   {'id': '236', 'x': 0.534836, 'target': 2.295161, 'split': 'train'},
                                   {'id': '237', 'x': -0.162801, 'target': 0.314845, 'split': 'train'},
                                   {'id': '238', 'x': 0.446803, 'target': 1.995153, 'split': 'train'},
                                   {'id': '239', 'x': 1.614659, 'target': 4.73241, 'split': 'train'},
                                   {'id': '240',
                                    'x': -0.750782,
                                    'target': -0.9985,
                                    'split': 'validation'},
                                   {'id': '241', 'x': -0.079804, 'target': 1.271001, 'split': 'train'},
                                   {'id': '242', 'x': 1.527982, 'target': 4.198407, 'split': 'train'},
                                   {'id': '243', 'x': 0.394985, 'target': 1.5929, 'split': 'train'},
                                   {'id': '244', 'x': 1.101674, 'target': 3.574926, 'split': 'train'},
                                   {'id': '245',
                                    'x': 1.112711,
                                    'target': 3.525476,
                                    'split': 'validation'},
                                   {'id': '246', 'x': -0.632493, 'target': -0.321445, 'split': 'train'},
                                   {'id': '247', 'x': 0.794659, 'target': 2.442714, 'split': 'train'},
                                   {'id': '248', 'x': -0.598829, 'target': -0.583138, 'split': 'train'},
                                   {'id': '249', 'x': 1.434955, 'target': 3.86665, 'split': 'train'},
                                   {'id': '250', 'x': -0.626899, 'target': -0.374074, 'split': 'train'},
                                   {'id': '251', 'x': -0.601532, 'target': -0.142518, 'split': 'train'},
                                   {'id': '252', 'x': 1.843639, 'target': 4.524313, 'split': 'train'},
                                   {'id': '253',
                                    'x': 0.053867,
                                    'target': 0.861106,
                                    'split': 'validation'},
                                   {'id': '254', 'x': 0.499606, 'target': 1.871351, 'split': 'train'},
                                   {'id': '255',
                                    'x': 0.004331,
                                    'target': 0.582819,
                                    'split': 'validation'},
                                   {'id': '256', 'x': -1.105216, 'target': -1.05154, 'split': 'train'},
                                   {'id': '257', 'x': 1.978425, 'target': 4.555959, 'split': 'train'},
                                   {'id': '258',
                                    'x': 0.756795,
                                    'target': 2.982258,
                                    'split': 'validation'},
                                   {'id': '259', 'x': 0.443616, 'target': 1.774186, 'split': 'train'},
                                   {'id': '260',
                                    'x': 0.422899,
                                    'target': 1.728249,
                                    'split': 'validation'},
                                   {'id': '261', 'x': 1.004964, 'target': 3.056222, 'split': 'train'},
                                   {'id': '262', 'x': -1.032814, 'target': -0.966233, 'split': 'train'},
                                   {'id': '263', 'x': 1.651451, 'target': 3.711726, 'split': 'train'},
                                   {'id': '264', 'x': 0.58168, 'target': 1.698995, 'split': 'train'},
                                   {'id': '265', 'x': 0.912482, 'target': 2.377127, 'split': 'train'},
                                   {'id': '266', 'x': 0.773852, 'target': 3.054777, 'split': 'train'},
                                   {'id': '267', 'x': -1.012966, 'target': -0.609896, 'split': 'train'},
                                   {'id': '268', 'x': -1.056967, 'target': -0.624035, 'split': 'train'},
                                   {'id': '269',
                                    'x': -1.092042,
                                    'target': -1.628512,
                                    'split': 'validation'},
                                   {'id': '270', 'x': 0.898238, 'target': 2.749073, 'split': 'train'},
                                   {'id': '271', 'x': -0.19605, 'target': 0.183359, 'split': 'train'},
                                   {'id': '272', 'x': 1.879891, 'target': 4.312975, 'split': 'train'},
                                   {'id': '273', 'x': -0.678339, 'target': -0.067969, 'split': 'train'},
                                   {'id': '274',
                                    'x': 1.647966,
                                    'target': 4.891402,
                                    'split': 'validation'},
                                   {'id': '275', 'x': -0.72952, 'target': -0.015259, 'split': 'train'},
                                   {'id': '276', 'x': 0.199023, 'target': 1.535983, 'split': 'train'},
                                   {'id': '277', 'x': 1.552463, 'target': 4.668743, 'split': 'train'},
                                   {'id': '278', 'x': -0.325343, 'target': 0.398584, 'split': 'train'},
                                   {'id': '279', 'x': -1.560083, 'target': -2.178436, 'split': 'train'},
                                   {'id': '280', 'x': -0.976149, 'target': -0.548309, 'split': 'train'},
                                   {'id': '281', 'x': -1.736849, 'target': -2.138787, 'split': 'train'},
                                   {'id': '282',
                                    'x': 0.981737,
                                    'target': 2.475597,
                                    'split': 'validation'},
                                   {'id': '283',
                                    'x': 0.570873,
                                    'target': 2.545035,
                                    'split': 'validation'},
                                   {'id': '284', 'x': -0.072741, 'target': 1.231127, 'split': 'train'},
                                   {'id': '285', 'x': -0.174256, 'target': 0.685987, 'split': 'train'},
                                   {'id': '286', 'x': 0.182249, 'target': 1.036668, 'split': 'train'},
                                   {'id': '287', 'x': 1.697597, 'target': 4.795111, 'split': 'train'},
                                   {'id': '288', 'x': -0.685596, 'target': 0.068241, 'split': 'train'},
                                   {'id': '289', 'x': 1.016799, 'target': 3.318894, 'split': 'train'},
                                   {'id': '290', 'x': 0.00667, 'target': 1.001373, 'split': 'train'},
                                   {'id': '291', 'x': -0.549716, 'target': 0.240336, 'split': 'train'},
                                   {'id': '292', 'x': -0.944465, 'target': -1.286489, 'split': 'train'},
                                   {'id': '293', 'x': 1.962728, 'target': 4.512404, 'split': 'train'},
                                   {'id': '294',
                                    'x': -0.939278,
                                    'target': -0.734267,
                                    'split': 'validation'},
                                   {'id': '295', 'x': 0.12539, 'target': 1.292688, 'split': 'train'},
                                   {'id': '296', 'x': 1.368797, 'target': 3.282515, 'split': 'train'},
                                   {'id': '297', 'x': -0.664893, 'target': -0.752038, 'split': 'train'},
                                   {'id': '298', 'x': -0.16682, 'target': 0.748294, 'split': 'train'},
                                   {'id': '299', 'x': 0.005943, 'target': 1.297422, 'split': 'train'},
                                   {'id': '300',
                                    'x': -0.046947,
                                    'target': 0.939551,
                                    'split': 'validation'}],
                          'notes': ['Generation seed: 42. A 32-bit LCG (1664525 × state + 1013904223, '
                                    'modulo 2³²) produces inputs and noise, then a Fisher–Yates '
                                    'shuffle assigns the split.',
                                    'The same 300 rows and split are used for every learning-rate '
                                    'comparison. Neither initialization nor learning rate regenerates '
                                    'the dataset.',
                                    'Only x is a model feature. The target, split, and row identifier '
                                    'are never used as input features. There is no fitted '
                                    'preprocessing.',
                                    'Validation loss is useful for these comparisons, but repeated '
                                    'tuning on it does not produce an unbiased test score. There is no '
                                    'separate test set.',
                                    'Noise prevents a single straight line from fitting every target '
                                    'exactly. This is synthetic regression, not MNIST or a GPU '
                                    'benchmark.']},
     'lessonUrl': 'https://indepth.dev/learn/experiments/machine-learning/learning-rate'}
    return protocol,


@app.cell(hide_code=True)
def _(mo, protocol):
    mo.md(f"""
    # {protocol['title']}

    [Read the lesson]({protocol['lessonUrl']}). This notebook is an editable Python companion.
    These small deterministic calculations run on CPU; a GPU is unnecessary.
    They do not reproduce full training runs from the source articles.

    **Protocol:** predict → run a reference → change one condition → compare → explain → design a variation.

    {protocol['fixed']}

    **First run:** use the reference settings shown below and write your prediction.
    **Then investigate:** {protocol['guided']}

    Runs remain in this running notebook session. Download your report before restarting or leaving.
    Editing the function does not recalculate old evidence; each new run saves the function source.
    """)
    return


@app.cell(hide_code=True)
def _(json, mo, protocol):
    _dataset = protocol['referenceDataset']
    mo.vstack([mo.md('## Inspect the reference inputs\n\n' + _dataset['description']),
               mo.ui.table(_dataset['rows'], selection=None, show_column_summaries=False),
               mo.md('\n'.join('- ' + note for note in _dataset['notes'])),
               mo.md('These are reference inputs. The input-dependent settings and full calculation source are saved with each run.'),
               mo.download(json.dumps(_dataset, indent=2).encode(), filename=protocol['id'] + '-reference-inputs.json', mimetype='application/json', label='Download reference inputs')])
    return


@app.cell
def _(math, protocol):
    def calculate(s):
        data = protocol['referenceDataset']['rows']
        training = [row for row in data if row['split'] == 'train']
        validation = [row for row in data if row['split'] == 'validation']
        weight, bias = s['start'], 0
        rows = []
        def loss(records):
            try:
                return sum((weight * row['x'] + bias - row['target']) ** 2 for row in records) / (2 * len(records))
            except OverflowError:
                return math.inf
        for step in range(s['steps'] + 1):
            train_loss, validation_loss = loss(training), loss(validation)
            if not math.isfinite(train_loss) or not math.isfinite(validation_loss):
                return {'metrics': {}, 'rows': rows, 'status': f'Non-finite loss at update {step}; no finite final score.'}
            rows.append({'Update': step, 'Slope': weight, 'Intercept': bias, 'Training loss': train_loss, 'Validation loss': validation_loss})
            if step == s['steps']:
                break
            dw, db = 0, 0
            for row in training:
                error = weight * row['x'] + bias - row['target']
                dw += error * row['x']
                db += error
            weight -= s['rate'] * dw / len(training)
            bias -= s['rate'] * db / len(training)
        return {'metrics': {'Final training loss': rows[-1]['Training loss'], 'Final validation loss': rows[-1]['Validation loss'], 'Final slope': weight, 'Final intercept': bias, 'Updates': s['steps']}, 'rows': rows, 'status': 'Completed'}
    return calculate,


@app.cell
def _(mo):
    get_runs, set_runs = mo.state([])
    return get_runs, set_runs


@app.cell
def _(calculate, datetime, deepcopy, hashlib, inspect, mo, protocol, set_runs, timezone):
    def record_run(value):
        if value is None or not value['prediction'].strip():
            return
        settings = {key: value[key] for key in protocol['reference']}
        source = inspect.getsource(calculate)
        snapshot = {
            'recordedAt': datetime.now(timezone.utc).isoformat(),
            'settings': settings,
            'prediction': value['prediction'].strip(),
            'implementationNote': value['implementation'],
            'calculationSource': source,
            'calculationSha256': hashlib.sha256(source.encode()).hexdigest(),
            'result': calculate(settings),
        }
        set_runs(lambda previous: [*previous, deepcopy(snapshot)])

    _controls = {}
    _parts = []
    for _control in protocol['controls']:
        _options = {choice['label']: choice['value'] for choice in _control['options']}
        _default = next(label for label, value in _options.items() if value == protocol['reference'][_control['key']])
        _controls[_control['key']] = mo.ui.dropdown(_options, value=_default, label=_control['label'])
        _parts.append('{' + _control['key'] + '}')
    _controls['prediction'] = mo.ui.text_area(label='Prediction and reason for this run', full_width=True)
    _controls['implementation'] = mo.ui.text_area(value='Original lesson calculation', label='Describe any code changes', full_width=True)
    _parts.extend(['{prediction}', '{implementation}'])
    experiment_form = mo.md('\n\n'.join(_parts)).batch(**_controls).form(
        submit_button_label='Run experiment',
        validate=lambda value: None if value and value['prediction'].strip() else 'Write a prediction and reason before running.',
        on_change=record_run,
    )
    experiment_form
    return experiment_form,


@app.cell(hide_code=True)
def _(get_runs, mo):
    runs = get_runs()
    baseline = mo.ui.dropdown({f"Run {i + 1}": i for i in range(len(runs))}, value='Run 1' if runs else None, label='Comparison baseline')
    mo.vstack([mo.md(f'## Recorded evidence — {len(runs)} runs'), baseline])
    return baseline, runs


@app.cell(hide_code=True)
def _(baseline, math, mo, runs):
    mo.stop(not runs, mo.md('Write a prediction, then select **Run experiment** to record the reference.'))
    _current = runs[-1]
    _base = runs[baseline.value if baseline.value is not None else 0]
    _changes = [key for key, value in _current['settings'].items() if value != _base['settings'][key]]
    _code_changed = _current['calculationSha256'] != _base['calculationSha256']
    _metrics = [{'Measurement': key, 'Baseline': _base['result']['metrics'].get(key), 'Latest run': value,
                 'Difference': value - _base['result']['metrics'][key] if key in _base['result']['metrics'] else None}
                for key, value in _current['result']['metrics'].items()]
    _outputs = [mo.md(f"**Latest run:** {len(runs)} · {_current['result']['status']}"),
                mo.md('**Prediction:** ' + _current['prediction']),
                mo.md('**Changed settings:** ' + (', '.join(_changes) or 'none') + '. **Calculation changed:** ' + str(_code_changed) + '.'),
                mo.md('Compare runs that differ in one intended condition. Check the saved code when comparing implementations.')]
    if _metrics:
        _outputs.append(mo.ui.table(_metrics, selection=None, show_column_summaries=False))
    if _current['result']['rows'] and 'Training loss' in _current['result']['rows'][0]:
        _training = [math.log10(1 + row['Training loss']) for row in _current['result']['rows']]
        _validation = [math.log10(1 + row['Validation loss']) for row in _current['result']['rows']]
        _top = max(1, max(_training), max(_validation))
        _lines = []
        for _values, _color, _dash in [(_training, '#2337ff', ''), (_validation, '#ae5a0b', '7 4')]:
            _points = ' '.join(f'{40 + 500 * i / max(1, len(_values) - 1)},{200 - 160 * value / _top}' for i, value in enumerate(_values))
            _lines.append(f'<polyline points="{_points}" fill="none" stroke="{_color}" stroke-dasharray="{_dash}" stroke-width="2"/>')
        _outputs.append(mo.Html(f'<svg viewBox="0 0 600 250" role="img" aria-label="Training and validation loss trajectories"><text x="40" y="20">log10(1 + loss), top = {_top:.3g}</text><path d="M40 40 V200 H540" fill="none" stroke="gray"/>{"".join(_lines)}<text x="40" y="225">0</text><text x="460" y="225">Update {len(_training)-1}</text></svg>'))
        _outputs.append(mo.md('Blue solid = training; orange dashed = validation. Use the table for exact losses.'))
    _outputs.append(mo.accordion({'Latest run: all intermediate evidence': mo.ui.table(_current['result']['rows'], selection=None, show_column_summaries=False)}))
    _outputs.append(mo.accordion({f"Run {i + 1}: settings, prediction, code, and evidence": mo.json(run) for i, run in enumerate(runs)}))
    mo.vstack(_outputs)
    return


@app.cell(hide_code=True)
def _(mo, protocol):
    observations = mo.ui.text_area(label='Observations: cite runs and measured values', full_width=True, debounce=False)
    explanation = mo.ui.text_area(label='Explanation: why did this happen?', full_width=True, debounce=False)
    limitations = mo.ui.text_area(label='Limitations: what remains unresolved?', full_width=True, debounce=False)
    next_test = mo.ui.text_area(label='Your next test and a result that would challenge your explanation', full_width=True, debounce=False)
    mo.vstack([mo.md('## Explain, then extend'), observations, explanation, limitations,
               mo.md('### Design your own investigation\n\n' + protocol['extension']), next_test,
               mo.accordion({'Check the reasoning after writing your explanation': mo.md(protocol['reasoning'])})])
    return explanation, limitations, next_test, observations


@app.cell(hide_code=True)
def _(baseline, datetime, explanation, json, limitations, mo, next_test, observations, protocol, runs, timezone):
    report = {'schemaVersion': 1, 'protocol': protocol, 'exportedAt': datetime.now(timezone.utc).isoformat(),
              'comparisonRunNumber': baseline.value + 1 if baseline.value is not None else None, 'runs': runs, 'notes': {'observations': observations.value, 'explanation': explanation.value,
                                     'limitations': limitations.value, 'nextTest': next_test.value}}
    mo.download(json.dumps(report, indent=2, allow_nan=False).encode(), filename=protocol['id'] + '-notebook-report.json',
                mimetype='application/json', label='Download report', disabled=not runs)
    return report,


if __name__ == '__main__':
    app.run()
