# /// 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 = {'title': 'Can a nonzero update disappear?',
     'reference': {'format': 'float64', 'start': 16777216, 'delta': 1, 'steps': 8},
     'controls': [{'key': 'format',
                   'label': 'Stored number format',
                   'options': [{'value': 'float64', 'label': 'Float64 (Python float)'},
                               {'value': 'float32', 'label': 'Float32 (round after each update)'}]},
                  {'key': 'start',
                   'label': 'Starting value',
                   'options': [{'value': 1, 'label': '1'},
                               {'value': 1048576, 'label': '1048576'},
                               {'value': 16777216, 'label': '16777216'},
                               {'value': 9007199254740992, 'label': '9007199254740992'}]},
                  {'key': 'delta',
                   'label': 'Increment per update',
                   'options': [{'value': 0.25, 'label': '0.25'},
                               {'value': 1, 'label': '1'},
                               {'value': 2, 'label': '2'},
                               {'value': 4, 'label': '4'}]},
                  {'key': 'steps',
                   'label': 'Number of updates',
                   'options': [{'value': 4, 'label': '4'},
                               {'value': 8, 'label': '8'},
                               {'value': 16, 'label': '16'},
                               {'value': 32, 'label': '32'}]}],
     'fixed': 'Repeatedly add the same positive increment. Float32 rounds the stored value after every '
              'addition using Python struct pack/unpack; float64 uses Python float. The ideal total is '
              'computed exactly in quarter-units with Fraction. This is a storage-rounding experiment, '
              'not a model-quality or GPU-speed benchmark.',
     'sources': [{'title': "How JavaScript's Number and Python's float store numbers",
                  'url': 'https://indepth.dev/posts/2026/en/how-floats-work-javascript-and-python'},
                 {'title': 'Floats, bfloat, and how bits shape model memory',
                  'url': 'https://indepth.dev/posts/2025/en/floats-and-model-memory'}],
     'id': 'precision',
     'guided': 'Run float64 at start = 16777216, increment = 1, and eight updates. Change only the '
               'stored format to float32. Compare every stored value with the exact ideal value.',
     'extension': 'Choose an increment that survives rounding, then compare magnitudes separately. Try '
                  'float64 at 2**53. Predict before each run. Extend the calculation to compare '
                  'rounding once after the full sum with rounding after each update; keep the '
                  'requested updates matched.',
     'reasoning': 'Float32 spacing at 2**24 is 2. Adding 1 lands at a tie, rounded back to 2**24 by '
                  'ties-to-even. An increment of 2 survives. Float64 has the analogous boundary at '
                  '2**53. These observations concern stored arithmetic, not GPU speed or whole-model '
                  'quality.',
     'referenceDataset': {'id': 'precision',
                          'version': 1,
                          'settings': {'format': 'float64', 'start': 16777216, 'delta': 1, 'steps': 8},
                          'title': 'A repeated-update schedule',
                          'unit': 'scheduled update',
                          'description': 'The inputs are a starting number and a sequence of identical '
                                         'increments. Stored results are produced by the experiment '
                                         'and are deliberately absent from this input table.',
                          'columns': [{'key': 'id',
                                       'label': 'Row',
                                       'type': 'category',
                                       'role': 'identifier',
                                       'description': 'Stable identifier within this synthetic '
                                                      'fixture.'},
                                      {'key': 'step',
                                       'label': 'Update number',
                                       'type': 'number',
                                       'role': 'condition',
                                       'description': 'Order in which the increment is applied.'},
                                      {'key': 'start',
                                       'label': 'Starting value',
                                       'type': 'number',
                                       'role': 'condition',
                                       'description': 'Same starting value for the whole run; not a '
                                                      'reset at each step.'},
                                      {'key': 'delta',
                                       'label': 'Increment',
                                       'type': 'number',
                                       'role': 'feature',
                                       'description': 'Requested addition at each step.'}],
                          'rows': [{'id': '1', 'step': 1, 'start': 16777216, 'delta': 1},
                                   {'id': '2', 'step': 2, 'start': 16777216, 'delta': 1},
                                   {'id': '3', 'step': 3, 'start': 16777216, 'delta': 1},
                                   {'id': '4', 'step': 4, 'start': 16777216, 'delta': 1},
                                   {'id': '5', 'step': 5, 'start': 16777216, 'delta': 1},
                                   {'id': '6', 'step': 6, 'start': 16777216, 'delta': 1},
                                   {'id': '7', 'step': 7, 'start': 16777216, 'delta': 1},
                                   {'id': '8', 'step': 8, 'start': 16777216, 'delta': 1}],
                          'notes': ['Repeated increments are intentional. Feature duplicates are not a '
                                    'data-cleaning error here.',
                                    'This is an ordered arithmetic protocol, not independent samples '
                                    'or a training dataset.',
                                    'Changing the stored format leaves the schedule fixed. The '
                                    'notebook and experiment show resulting rounding separately.']},
     'lessonUrl': 'https://indepth.dev/learn/experiments/machine-learning/precision'}
    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 _(Fraction, math, struct):
    def calculate(s):
        def store(value):
            value = float(value)
            return struct.unpack('f', struct.pack('f', value))[0] if s['format'] == 'float32' else value
        value, rows = store(s['start']), []
        for step in range(s['steps'] + 1):
            ideal = Fraction(s['start']) + Fraction(s['delta']) * step
            whole, remainder = divmod(ideal.numerator, ideal.denominator)
            # The supplied increments are exact multiples of a quarter.
            exact = str(whole) + ({Fraction(0): '', Fraction(1, 4): '.25', Fraction(1, 2): '.5', Fraction(3, 4): '.75'}[Fraction(remainder, ideal.denominator)])
            rows.append({'Update': step, 'Stored value': str(int(value)) if value.is_integer() else str(value), 'Ideal value (exact)': exact, 'Observed total change': value - s['start']})
            value = store(value + s['delta'])
        return {'metrics': {'Requested total change': s['delta'] * s['steps'], 'Observed total change': rows[-1]['Observed total change']}, '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()
