"""Air VPD and interval-mean DLI; Python 3.10+, standard library only.""" import csv,json,math,sys from datetime import datetime,timedelta from pathlib import Path def number(v,name,lo,hi): try:v=float(v) except (ValueError,TypeError):raise ValueError(name+': missing or nonnumeric') from None if not math.isfinite(v) or not lo<=v<=hi:raise ValueError(f'{name}: expected [{lo}, {hi}]') return v def air_vpd(t,rh): t=number(t,'temp_c',0,50);rh=number(rh,'rh_pct',0,100) return .6108*math.exp(17.27*t/(t+237.3))*(1-rh/100) def stamp(v): t=datetime.fromisoformat(v) if t.utcoffset() is None:raise ValueError('UTC offset required') return t def analyze(rows): if not rows:raise ValueError('empty data') first=stamp(rows[0]['start']);previous=first;dose=0;out=[] if (first.hour,first.minute,first.second,first.microsecond)!=(0,0,0,0):raise ValueError('start at midnight') finish=first+timedelta(days=1) for r in rows: a,b=stamp(r['start']),stamp(r['end']) if a.utcoffset()!=first.utcoffset() or b.utcoffset()!=first.utcoffset():raise ValueError('one fixed offset required') if a!=previous or b<=a or b>finish:raise ValueError('gap, overlap, order or day boundary error') p=number(r['ppfd_umol_m2_s'],'PPFD',0,3000) v=air_vpd(r['temp_c'],r['rh_pct']);seconds=(b-a).total_seconds();dose+=p*seconds/1e6 out.append(dict(start=r['start'],end=r['end'],seconds=seconds,air_vpd_kpa=v,cumulative_mol_m2=dose));previous=b if previous!=finish:raise ValueError('incomplete day') return dict(integration='interval-mean PPFD; fixed-offset 24-hour day',dli_mol_m2_day=dose,intervals=out) if __name__=='__main__': try: with open(sys.argv[1] if len(sys.argv)>1 else Path(__file__).with_name('synthetic.csv'),newline='',encoding='utf-8') as f:result=analyze(list(csv.DictReader(f))) print(json.dumps(result,indent=2,allow_nan=False)) except (ValueError,KeyError,OSError) as e:sys.exit('Invalid input: '+str(e))