"""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__":
    x=np.linspace(-2,2,20)
    Z=np.column_stack([np.ones(len(x)),x])
    y=np.array([0,0,0,1,0,0,0,1,0,1,0,1,1,1,1,0,1,1,1,1])
    theta,history=logistic_gd(Z,y,rate=.2,steps=500)
    assert history[-1]<history[0]
    loss,gradient=logistic_loss_grad(np.array([[-1.],[1.]]),np.array([0.,1.]),np.zeros(1))
    np.testing.assert_allclose([loss,gradient[0]],[np.log(2),-.5])
    print_result({"theta":theta,"initial_loss":history[0],"final_loss":history[-1],
        "probability_first_last":expit(Z@theta)[[0,-1]],"hand_gradient":gradient})
