"""Definitional tracked-constituent mass ledger; hypothetical process fractions."""
import csv,json,math
from pathlib import Path
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
ROOT=Path(__file__).resolve().parent
def ledger(feed,fraction,recovery,yield_fraction):
    if not all(math.isfinite(v) for v in [feed,fraction,recovery,yield_fraction]) or feed<0 or not all(0<=v<=1 for v in [fraction,recovery,yield_fraction]):raise ValueError('Nonnegative finite feed and fractions in [0,1] required')
    target=feed*fraction;recovered=target*recovery;product=recovered*yield_fraction
    return dict(feed_kg=feed,constituent_fraction=fraction,recovery_fraction=recovery,fabrication_yield_fraction=yield_fraction,target_in_feed_kg=target,other_feed_material_kg=feed-target,recovered_target_kg=recovered,target_in_separation_residue_kg=target-recovered,target_in_product_kg=product,target_in_fabrication_rejects_kg=recovered-product)
rows=[ledger(100,.2,i/20,j/20) for i in range(21) for j in range(21)]
with (ROOT/'ledger-grid.csv').open('w',newline='',encoding='utf-8') as f:
    w=csv.DictWriter(f,fieldnames=rows[0]);w.writeheader();w.writerows(rows)
for r in rows:
    pieces=[r[k] for k in ['target_in_separation_residue_kg','target_in_product_kg','target_in_fabrication_rejects_kg']]
    assert all(x>=-1e-12 for x in pieces)
    assert math.isclose(sum(pieces),r['target_in_feed_kg'],abs_tol=1e-12)
    assert math.isclose(sum(pieces)+r['other_feed_material_kg'],r['feed_kg'],abs_tol=1e-12)
for f in [0,1]:
 for rec in [0,1]:
  for y in [0,1]:
   r=ledger(100,f,rec,y);assert r['target_in_product_kg']==100*f*rec*y
assert ledger(0,.5,.5,.5)['target_in_product_kg']==0
bad=0
for args in [(-1,.2,.8,.9),(100,-.1,.8,.9),(100,1.1,.8,.9),(100,.2,-.1,.9),(100,.2,1.1,.9),(100,.2,.8,-.1),(100,.2,.8,1.1),(float('nan'),.2,.8,.9),(100,.2,float('inf'),.9)]:
    try:ledger(*args)
    except ValueError:bad+=1
assert bad==9
example=ledger(100,.2,.8,.9)
meta=dict(title='Follow one conserved constituent',kind='Definitional hypothetical mass ledger; no tested extraction or manufacturing process',inputs=dict(feed_kg=100,constituent_fraction=.2,recovery_range=[0,1],fabrication_yield_range=[0,1],fraction_step=.05),example=example,equations=dict(product='feed*fraction*recovery*yield',separation_residue_target='feed*fraction*(1-recovery)',fabrication_reject_target='feed*fraction*recovery*(1-yield)'),tracked_basis='Mass of a conserved target constituent, expressed on the same basis in every stream. If chemical species changes, use conserved elemental mass or an explicitly equivalent constituent basis, not the mass of unlike compounds.',limits=['Product quantity means target constituent in the accepted product, not total part mass.','Other feed mass is not traced through the processing plant.','Reagents, solvents, binder, water, gases, energy and inventories are outside this ledger.','Recovery and yield are prescribed fractions, not computed chemistry or measured process performance.','Reject and residue are destinations in the accounting, not necessarily unrecoverable waste.','No purity, strength, cost, energy or emissions result follows.'],rows=len(rows),method_source='Conservation bookkeeping derived explicitly in this study; no external process parameters used.')
(ROOT/'metadata.json').write_text(json.dumps(meta,indent=2)+'\n')
(ROOT/'checks.json').write_text(json.dumps(dict(all_pass=True,rows_checked=len(rows),fraction_corner_cases=8,zero_feed=True,invalid_cases=bad,constituent_conservation=True),indent=2)+'\n')
plt.rcParams.update({'font.family':'DejaVu Sans','font.size':10,'axes.spines.top':False,'axes.spines.right':False})
fig,ax=plt.subplots(1,2,figsize=(10.5,4.2),layout='constrained')
for y,c in [(0.25,'#a44598'),(.5,'#078b93'),(.9,'#355ed0'),(1.,'#666666')]:
    rr=[ledger(100,.2,i/100,y) for i in range(101)]
    ax[0].plot([r['recovery_fraction']*100 for r in rr],[r['target_in_product_kg'] for r in rr],color=c,label=f'{y*100:g}% fabrication yield')
ax[0].set(xlabel='Separation recovery (%)',ylabel='Target constituent in accepted product (kg)',title='A. Two stages multiply, not add');ax[0].legend(fontsize=8)
vals=[4,1.6,14.4];labels=['Separation residue','Fabrication rejects','Accepted product'];colors=['#9d599b','#0d9297','#365dcc'];left=0
for v,label,c in zip(vals,labels,colors):
    ax[1].barh([0],v,left=left,color=c,label=label,height=.3);ax[1].text(left+v/2,0,f'{v:g}',ha='center',va='center',color='white');left+=v
ax[1].set(xlabel='Tracked constituent mass (kg)',yticks=[],title='B. Example: 80% recovery, 90% yield',xlim=(0,20));ax[1].legend(loc='upper left',fontsize=8);ax[1].text(10,.24,'20 kg entering = 4 + 1.6 + 14.4 kg leaving',ha='center',fontsize=9);ax[1].set_ylim(-.4,.65)
ax[0].grid(alpha=.2)
fig.suptitle('100 kg feed contains 20 kg of a tracked constituent • hypothetical fractions',fontsize=12)
fig.supxlabel('Accepted product total mass and total plant mass balance are not calculated; reagents and other inputs are excluded.',fontsize=9)
for ext in ('png','svg','pdf'):fig.savefig(ROOT/f'constituent-ledger.{ext}',dpi=200,bbox_inches='tight')
print(json.dumps(dict(rows=len(rows),all_checks_pass=True,example=example)))
