"""Silhouette arithmetic on six authored points and fixed labels."""
import math,json
def silhouette(points,labels):
    n=len(points);groups=set(labels)
    if len(labels)!=n or not 2<=len(groups)<=n-1:raise ValueError("2 through n-1 groups required")
    rows=[]
    for i,x in enumerate(points):
        own=[j for j in range(n) if j!=i and labels[j]==labels[i]]
        if not own:rows.append({"a":None,"b":None,"s":0.});continue
        a=sum(math.dist(x,points[j]) for j in own)/len(own)
        other_means=[]
        for group in groups-{labels[i]}:
            members=[j for j in range(n) if labels[j]==group]
            other_means.append(sum(math.dist(x,points[j]) for j in members)/len(members))
        b=min(other_means);s=(b-a)/max(a,b) if max(a,b)>0 else 0.
        rows.append({"a":a,"b":b,"s":s})
    return rows,sum(r['s'] for r in rows)/n
if __name__=="__main__":
    X=[(0,0),(0,3),(4,0),(10,10),(10,13),(14,10)];labels=[0,0,0,1,1,1]
    rows,score=silhouette(X,labels);changed,other=silhouette(X,[0,0,0,1,1,0])
    assert math.isclose(score,.7211575101636455,abs_tol=1e-13)
    assert math.isclose(other,.3400527797004418,abs_tol=1e-13) and changed[-1]['s']<0
    assert rows[0]['a']==3.5 and all(-1<=r['s']<=1 for r in rows)
    singleton,_=silhouette([(0,0),(1,0),(9,0)],[0,0,1]);assert singleton[-1]['s']==0
    rejected=0
    for bad in [[0]*6,list(range(6))]:
        try:silhouette(X,bad)
        except ValueError:rejected+=1
    assert rejected==2
    print(json.dumps({"points":X,"labels":labels,"rows":rows,"mean":score,"changed_last_label_mean":other,"changed_last_s":changed[-1]['s'],"singleton_score_policy":0,"clustering_fitted":False},indent=2))
