"""Standalone synthetic teaching example. Run locally with Python, not in the HTML."""
import json
import numpy as np
import pandas as pd
from scipy.special import expit

def clean(value):
    if isinstance(value, dict): return {str(k):clean(v) for k,v in value.items()}
    if isinstance(value,(list,tuple,np.ndarray)): return [clean(v) for v in value]
    if isinstance(value,(np.integer,)): return int(value)
    if isinstance(value,(float,np.floating)): return float(value) if np.isfinite(value) else None
    if isinstance(value,(np.bool_,)): return bool(value)
    return value

def print_result(result):
    print(json.dumps(clean(result),ensure_ascii=False,allow_nan=False))


def logistic_loss_grad(Z, y, theta):
    """Z already includes an intercept column; mean negative log likelihood."""
    z = Z@theta
    loss = np.mean(np.logaddexp(0, z)-y*z)
    grad = Z.T@(expit(z)-y)/len(Z)
    return float(loss), grad

def logistic_gd(Z, y, rate=.1, steps=300):
    theta = np.zeros(Z.shape[1]); history = []
    for _ in range(steps):
        loss, grad = logistic_loss_grad(Z,y,theta)
        history.append(loss)
        theta -= rate*grad
    return theta, np.asarray(history)

if __name__ == "__main__":
    rng=np.random.default_rng(42)
    Z=np.column_stack([np.ones(12),rng.normal(size=(12,2))])
    y=rng.integers(0,2,12); theta=rng.normal(size=3)
    loss,gradient=logistic_loss_grad(Z,y,theta)
    eps=1e-6
    numeric=np.array([(logistic_loss_grad(Z,y,theta+eps*e)[0]-
        logistic_loss_grad(Z,y,theta-eps*e)[0])/(2*eps) for e in np.eye(3)])
    np.testing.assert_allclose(gradient,numeric,atol=1e-8)
    curves={}
    for rate in [.1,.25]:
        w=np.ones(2); values=[]
        for _ in range(20):
            values.append(.5*np.sum(np.array([1.,10.])*w*w))
            w-=rate*np.array([1.,10.])*w
        curves[str(rate)]={"first_loss":values[0],"last_loss":values[-1]}
    print_result({"gradient_max_error":abs(gradient-numeric).max(),"quadratic":curves,
        "stable_loss_at_logit_1000":logistic_loss_grad(np.ones((1,1)),np.zeros(1),np.array([1000.]))[0]})
