#!/usr/bin/env python3
"""Andrade Safe: educational steady-state single-phase exchanger model.
Constant cp and U, no external heat losses, ideal counter/parallel flow.
Not equipment selection or final thermal/mechanical design. Python 3 only.
"""
import math
from dataclasses import dataclass
@dataclass(frozen=True)
class In:
    Thi: float=80.0
    Tci: float=20.0
    mh: float=1.2
    mc: float=1.0
    cph: float=4180.0
    cpc: float=4180.0
    U: float=850.0
    A: float=9.0
    mode: str='counter'
def lmtd(dt1,dt2):
    if not all(math.isfinite(v) and v>0 for v in (dt1,dt2)):raise ValueError('Positive finite terminal differences required')
    lo,hi=sorted((dt1,dt2));d=hi-lo
    return (dt1+dt2)/2 if d<=1e-10*hi else d/math.log1p(d/lo)
def validate(x):
    if not all(isinstance(v,(int,float)) and math.isfinite(v) for v in (x.Thi,x.Tci,x.mh,x.mc,x.cph,x.cpc,x.U,x.A)):raise ValueError('Finite numeric inputs required')
    if not (x.Thi>x.Tci and min(x.mh,x.mc,x.cph,x.cpc,x.U)>0 and x.A>=0 and x.mode in ('counter','parallel')):raise ValueError('Invalid model domain')
def evaluate(x:In):
    validate(x)
    Ch=x.mh*x.cph;Cc=x.mc*x.cpc;Cmin=min(Ch,Cc);Cr=Cmin/max(Ch,Cc);ntu=x.U*x.A/Cmin;delta=x.Thi-x.Tci
    if x.mode=='parallel':
        z=ntu*(1+Cr);eps=-math.expm1(-z)/(1+Cr);dt1=delta;dt2=delta*math.exp(-z)
        L=delta if z==0 else delta*(-math.expm1(-z))/z
    elif abs(1-Cr)<1e-10:
        eps=ntu/(1+ntu);dt1=dt2=L=delta/(1+ntu)
    else:
        z=ntu*(1-Cr);e=math.exp(-z);s=-math.expm1(-z);denom=(1-Cr)+Cr*s;eps=s/denom
        small=delta*(1-Cr)*e/denom;large=delta*(1-Cr)/denom
        dt1,dt2=(small,large) if Cc<=Ch else (large,small)
        # Analytical LMTD also handles extremely large NTU when a difference underflows.
        L=delta if ntu==0 else delta*eps/ntu
    Q=eps*Cmin*delta;Tho=x.Thi-Q/Ch;Tco=x.Tci+Q/Cc
    return dict(mode=x.mode,ntu=ntu,eps=eps,Cr=Cr,Ch=Ch,Cc=Cc,Q_kW=Q/1000,Tho=Tho,Tco=Tco,dT1=dt1,dT2=dt2,LMTD=L,UA_LMTD_kW=x.U*x.A*L/1000,residual_energy_W=(x.Thi-Tho)*Ch-(Tco-x.Tci)*Cc)
def size_counter_for_cold_out(x:In,target:float):
    validate(x)
    if not isinstance(target,(int,float)) or not math.isfinite(target) or not x.Tci<target<x.Thi:raise ValueError('Invalid cold outlet target')
    Q=x.mc*x.cpc*(target-x.Tci);Tho=x.Thi-Q/(x.mh*x.cph);dt1=x.Thi-target;dt2=Tho-x.Tci;L=lmtd(dt1,dt2)
    return dict(target_Tco=target,Q_kW=Q/1000,Tho=Tho,dT1=dt1,dT2=dt2,LMTD=L,A_m2=Q/(x.U*L))
if __name__=='__main__':
    for mode in ('counter','parallel'):print(evaluate(In(mode=mode)))
    print('Counterflow design target:',size_counter_for_cold_out(In(),55))
