import numpy as np

# Onderwijsplanning in werkdagen; geen werkelijk behaalde voortgang.
taken = {
 "A":("eisen en proefplan",3.,()),
 "B":("interfaces",4.,("A",)),
 "C":("tweelingmodel",5.,("A",)),
 "D":("lokaal prototype",6.,("B",)),
 "E":("modelsimulatie",4.,("B","C")),
 "F":("integratie",3.,("D","E")),
 "G":("systeemproeven",4.,("F",)),
 "H":("eindrapport",2.,("G",)),
 "I":("review en verdediging",1.,("H",)),
}

def cpm(net):
    # Topologisch sorteren: afhankelijkheden mogen niet vooruit worden gegokt.
    for k,(naam,d,pred) in net.items():
        if not np.isfinite(d) or d<0: raise ValueError("duur")
        if any(p not in net for p in pred): raise ValueError("ontbrekende voorganger")
    order=[]; over=set(net)
    while over:
        klaar=sorted(k for k in over if all(p in order for p in net[k][2]))
        if not klaar: raise ValueError("cyclus in afhankelijkheden")
        order.extend(klaar); over.difference_update(klaar)
    es={}; ef={}
    for k in order:
        es[k]=max((ef[p] for p in net[k][2]),default=0.)
        ef[k]=es[k]+net[k][1]
    eind=max(ef.values(),default=0.)
    succ={k:[j for j in net if k in net[j][2]] for k in net}
    ls={}; lf={}
    for k in reversed(order):
        lf[k]=min((ls[j] for j in succ[k]),default=eind)
        ls[k]=lf[k]-net[k][1]
    ruimte={k:ls[k]-es[k] for k in net}
    kritisch=[k for k in order if abs(ruimte[k])<1e-9]
    return eind,es,ef,ls,lf,ruimte,kritisch

R=cpm(taken)
print("ID ES EF LS LF speling (werkdagen)")
for k in taken:
    print(k,*[round(R[i][k],2) for i in range(1,6)])
print("eind",R[0],"kritische activiteiten",R[6])
# D en E delen nu exclusief dezelfde proefopstelling: D moet eerst klaar.
gedeeld=dict(taken)
naam,d,pred=gedeeld["E"]; gedeeld["E"]=(naam,d,pred+("D",))
RG=cpm(gedeeld)
print("met proefopstelling D voor E",RG[0],RG[6])
# Twee extra dagen voor C kunnen het kritische pad veranderen.
vertraging=dict(taken)
naam,d,pred=vertraging["C"]; vertraging["C"]=(naam,d+2,pred)
RV=cpm(vertraging)
print("C twee dagen langer",RV[0],RV[6])
# Illustratieve spreiding, niet uit werkelijke projecten geschat.
rng=np.random.default_rng(48); n=5000
duren=[]; hits={k:0 for k in taken}
for _ in range(n):
    scenario={k:(naam,rng.triangular(.6*d,d,1.8*d),pred)
              for k,(naam,d,pred) in taken.items()}
    s=cpm(scenario); duren.append(s[0])
    for k in s[6]: hits[k]+=1
Q=np.quantile(duren,[.1,.5,.9])
print("scenario P10/P50/P90",np.round(Q,3))
print("scenario kans duur > 23",round(np.mean(np.array(duren)>23),4))
print("kritiekfrequentie B/C/D/E",[round(hits[k]/n,4) for k in ["B","C","D","E"]])
assert R[0]==23 and RG[0]==27 and RV[0]==24
assert R[5]["C"]==1 and R[5]["E"]==1
cyclus=dict(taken); cyclus["A"]=("fout",3.,("I",))
try: cpm(cyclus)
except ValueError as e: print("foutnet geweigerd:",e)
else: raise AssertionError("cyclus niet gedetecteerd")
