"""Six-job scheduling illustration. Synthetic, not a plant estimate or AI test.
Exhaustively evaluate all 720 sequences; one machine, all jobs available, no idle
insertion, nonpreemptive processing, soft due dates, deterministic times.
Setups: 1 hour and 3 cost units per family change; no initial setup.
Disruption: beta cost units * sum of absolute start-time changes for jobs A-E.
F's requested advancement receives no disruption charge.
A job's due date changes from 15h to 7h; all decisions occur before work starts.
Freezing means retaining the previously chosen sequence and its start times.
"""
import csv, itertools, json, sys
from pathlib import Path
OUT=Path(__file__).resolve().parent
JOBS={'A':(2,'X',5,4),'B':(3,'X',8,3),'C':(1,'Y',6,5),
      'D':(2,'Y',12,2),'E':(2,'X',10,4),'F':(1,'Y',15,6)}
SEQUENCES=list(itertools.permutations(JOBS))
DUE_OLD={j:v[2] for j,v in JOBS.items()}
DUE_NEW={**DUE_OLD,'F':7}

def score(sequence,due):
 t=0; last=None; starts={}; tardiness=0; setups=0
 for job in sequence:
  duration,family,_,weight=JOBS[job]
  if last is not None and family!=last:t+=1;setups+=1
  starts[job]=t;t+=duration
  tardiness+=weight*max(0,t-due[job]);last=family
 return dict(operating=tardiness+3*setups,tardiness=tardiness,setup_cost=3*setups,starts=starts,makespan=t)

OLD=min(SEQUENCES,key=lambda s:(score(s,DUE_OLD)['operating'],s))
STARTS=score(OLD,DUE_OLD)['starts']
def evaluate(sequence,beta):
 r=score(sequence,DUE_NEW)
 movement=sum(abs(r['starts'][j]-STARTS[j]) for j in JOBS if j!='F')
 return dict(sequence=''.join(sequence),**r,movement_hours=movement,
             f_completion=r['starts']['F']+JOBS['F'][0],
             f_lateness=max(0,r['starts']['F']+JOBS['F'][0]-DUE_NEW['F']),
             change_cost=beta*movement,total=r['operating']+beta*movement)

def best(beta):
 return min((evaluate(s,beta) for s in SEQUENCES),key=lambda r:(r['total'],r['movement_hours'],r['sequence']))

def main():
 old_score=score(OLD,DUE_OLD)
 assert len(SEQUENCES)==720 and ''.join(OLD)=='CABEDF' and old_score['operating']==6
 assert best(0)['sequence']=='CFABED' and best(0)['total']==8
 assert best(6)['sequence']=='CFABED' and best(6)['total']==32
 assert best(9)['sequence']=='CABEFD' and best(9)['total']==41
 assert best(12)['sequence']==''.join(OLD) and best(12)['total']==42
 # Cross-check each schedule: unique jobs, positive non-overlapping durations,
 # exact setup count, independent weighted tardiness and nonnegative disruption.
 for s in SEQUENCES:
  r=evaluate(s,9)
  assert len(set(s))==6 and r['movement_hours']>=0
  for i in range(1,6):
   prev=s[i-1];cur=s[i]
   assert r['starts'][cur]==r['starts'][prev]+JOBS[prev][0]+(JOBS[cur][1]!=JOBS[prev][1])
  independent=sum(JOBS[j][3]*max(0,r['starts'][j]+JOBS[j][0]-DUE_NEW[j]) for j in JOBS)
  assert independent==r['tardiness']
 selected=[('Keep the plan',OLD),('Replan for operating cost',tuple(best(0)['sequence'])),('Replan with change cost',tuple(best(9)['sequence']))]
 rows=[{'policy':name,**evaluate(s,9)} for name,s in selected]
 result={'synthetic':True,'jobs':JOBS,'old_due_dates':DUE_OLD,'new_due_dates':DUE_NEW,
         'old_schedule':''.join(OLD),'old_operating_cost':old_score['operating'],
         'beta_main':9,'schedules_enumerated':720,'scenarios':rows,
         'disruption_jobs':['A','B','C','D','E'],'requested_job_excluded':'F',
         'beta_sweep':[{'beta':i/100,**best(i/100)} for i in range(1501)],
         'ties':'Old plan: alphabetical sequence. New plan: least movement, then alphabetical.',
         'limitations':['Not stochastic; no empirical calibration','No lead-time constraints, hard due dates, material scarcity, yield uncertainty or machine failures','No endogenous arrivals, strategic customers, or repeated-replanning feedback','Only earliest-start schedules for each permutation; no intentional idle-time decisions','Economic coefficients and linear movement penalty are invented to illustrate a trade-off']}
 (OUT/'freeze-analysis.json').write_text(json.dumps(result,indent=2))
 with (OUT/'freeze-comparison.csv').open('w',newline='') as f:
  fields=['policy','sequence','operating','movement_hours','change_cost','total','f_completion','f_lateness']
  w=csv.DictWriter(f,fieldnames=fields,extrasaction='ignore');w.writeheader();w.writerows(rows)
 if '--plot' in sys.argv:
  import matplotlib
  matplotlib.use('Agg')
  import matplotlib.pyplot as plt
  plt.rcParams.update({'font.family':'DejaVu Sans','font.size':11,'axes.spines.top':False,'axes.spines.right':False})
  fig,(a,b)=plt.subplots(1,2,figsize=(12.8,5.3),gridspec_kw={'width_ratios':[1.12,1]})
  labels=['Keep\nthe plan','Replan for\noperating cost','Replan with\nchange cost']
  a.bar(labels,[r['operating'] for r in rows],color='#244e63',label='Operating cost',width=.6)
  a.bar(labels,[r['change_cost'] for r in rows],bottom=[r['operating'] for r in rows],color='#cd895a',label='Cost of changing the plan',width=.6)
  for i,r in enumerate(rows):a.text(i,r['total']+1.5,str(r['total']),ha='center',fontweight='bold')
  a.set(ylim=(0,60),ylabel='Illustrative cost units · lower is better')
  a.set_title('At a change penalty of 9',loc='left',fontsize=12,pad=15)
  a.legend(frameon=False,fontsize=9,loc='upper left');a.grid(axis='y',alpha=.16);a.set_axisbelow(True)
  x=[i/100 for i in range(1501)]
  curve_labels=['Original schedule (CABEDF)','Larger replan (CFABED)','Limited replan (CABEFD)']
  for (name,s),label,color,ls in zip(selected,curve_labels,['#71777b','#cd895a','#244e63'],['--',':','-']):
   b.plot(x,[evaluate(s,z)['total'] for z in x],label=label,color=color,ls=ls,lw=2)
  b.set(xlabel='Penalty per shifted job-hour (jobs A–E)',ylabel='Illustrative total cost',ylim=(0,85),xlim=(0,15))
  b.set_title('The preferred action depends on change cost',loc='left',fontsize=12,pad=15)
  b.axvline(8,color='#ccc',lw=.8);b.axvline(10,color='#ccc',lw=.8)
  b.legend(frameon=False,fontsize=8.5,loc='upper left');b.grid(axis='y',alpha=.16)
  fig.subplots_adjust(left=.075,right=.96,bottom=.26,top=.8,wspace=.37)
  fig.suptitle('The preferred revision depends on disruption cost',x=.075,ha='left',fontsize=18,fontweight='bold')
  fig.text(.075,.075,'Synthetic six-job example. F’s target moves from hour 15 to 7. Only movement of the other jobs is charged.\nAll 720 sequences evaluated. Limited revision finishes F at hour 11: it reduces lateness but does not meet the new target.',fontsize=9,color='#555',linespacing=1.7)
  fig.savefig(OUT/'freeze-comparison.png',dpi=180,facecolor='white')
  fig.savefig(OUT/'freeze-comparison.svg',facecolor='white')
  plt.close(fig)
 print(json.dumps({'old':''.join(OLD),'beta9':rows,'validated_sequences':720},indent=2))
if __name__=='__main__':main()
