"""Synthetic one-zone water-vapour balance; Python 3.10+ stdlib only.""" import json, math, sys from pathlib import Path R_V = 461.5 BOUNDS = { "duration_hours":(.1,168),"time_step_seconds":(1,3600),"room_volume_m3":(1,1e7), "air_temperature_c":(-10,60),"initial_rh_pct":(0,100),"outside_rh_pct":(0,100), "ventilation_m3_per_s":(0,1e5),"transpiration_lit_kg_per_h":(0,1e5), "transpiration_dark_kg_per_h":(0,1e5),"lights_on_hour":(0,24),"lights_off_hour":(0,24), "dehumidifier_capacity_kg_per_h":(0,1e5),"dehumidifier_on_rh_pct":(0,100), "dehumidifier_off_rh_pct":(0,100),"latent_heat_j_per_kg":(1e5,5e6)} def num(v,k,lo,hi): if isinstance(v,bool): raise ValueError(k+': boolean') try: v=float(v) except (TypeError,ValueError): raise ValueError(k+': nonnumeric') from None if not math.isfinite(v) or not lo<=v<=hi: raise ValueError(f'{k}: expected [{lo}, {hi}]') return v def validate(raw): if set(raw)!=set(BOUNDS): raise ValueError('keys must match contract; missing='+str(sorted(set(BOUNDS)-set(raw)))+' unknown='+str(sorted(set(raw)-set(BOUNDS)))) c={k:num(raw[k],k,*b) for k,b in BOUNDS.items()} if c['lights_off_hour']<=c['lights_on_hour']: raise ValueError('lights_off_hour must exceed lights_on_hour') if c['dehumidifier_off_rh_pct']>=c['dehumidifier_on_rh_pct']: raise ValueError('off threshold must be below on threshold') n=c['duration_hours']*3600/c['time_step_seconds'] if abs(n-round(n))>1e-9: raise ValueError('duration must contain integer steps') return c def saturation_pressure_pa(t): return 610.8*math.exp(17.27*t/(t+237.3)) def vapour_density(t,rh): return saturation_pressure_pa(t)*(rh/100)/(R_V*(t+273.15)) def rh_from_mass(t,mass,volume): return 100*(mass/volume)*R_V*(t+273.15)/saturation_pressure_pa(t) def simulate(raw): c=validate(raw); dt=c['time_step_seconds']; steps=round(c['duration_hours']*3600/dt) mass=vapour_density(c['air_temperature_c'],c['initial_rh_pct'])*c['room_volume_m3'] outside=vapour_density(c['air_temperature_c'],c['outside_rh_pct']) saturation=vapour_density(c['air_temperature_c'],100)*c['room_volume_m3'] dehum=False; totals={k:0.0 for k in ('transpired','vented_net','dehumidified','condensed')}; residual=0.0; lo=hi=c['initial_rh_pct']; hourly=[] for i in range(steps): hour=i*dt/3600; rh=rh_from_mass(c['air_temperature_c'],mass,c['room_volume_m3']) if dehum and rh<=c['dehumidifier_off_rh_pct']: dehum=False elif not dehum and rh>=c['dehumidifier_on_rh_pct']: dehum=True lit=c['lights_on_hour']<=hour%241 else Path(__file__).with_name('config.json') print(json.dumps(simulate(json.loads(p.read_text())),indent=2,allow_nan=False)) except (OSError,ValueError,json.JSONDecodeError) as e: raise SystemExit('Invalid input: '+str(e)) from None