/* Helium Peng-Robinson and compressible-orifice kernels. * Ported from the project's Python physical equations; validated independently * against Python at ordinary, reverse-flow and contact trial states. */ #include "kernels.h" #include #include #define RU 8.31446261815324 #define MOLAR_MASS 0.004002602 #define TC 5.1953 #define PC 227460.0 #define OMEGA (-0.382) static const double PI=3.1415926535897932384626433832795; static const double pr_a=.457235583*RU*RU*TC*TC/PC; static const double pr_b=.07779607*RU*TC/PC; static const double kappa=.37464+1.54226*OMEGA-.26992*OMEGA*OMEGA; static const double rg=RU/MOLAR_MASS; static const NativeMedium helium_medium={1,RU/MOLAR_MASS,2.5*RU/MOLAR_MASS,293.15,0,1.96e-5,293.15,79.4}; static int same_medium(const NativeMedium *a,const NativeMedium *b) { return a->real_helium==b->real_helium && a->R==b->R && a->cp==b->cp && a->Tref==b->Tref && a->slope==b->slope && a->mu==b->mu && a->muT==b->muT && a->S==b->S; } void native_properties_init(NativePropertyCache *cache,NativePropertyState *states,size_t capacity) { cache->states=states;cache->count=0;cache->capacity=states?capacity:0; } static NativePropertyState *property_new(NativePropertyCache *cache,const NativeMedium *m, double p,double T,NativePropertyState *scratch) { int valid=p>0 && T>0 && isfinite(p) && isfinite(T); NativePropertyState *s=valid && cache && cache->countcapacity ? &cache->states[cache->count++] : scratch; *s=(NativePropertyState){0};s->medium=*m;s->p=p;s->T=T;s->valid=valid?NATIVE_PROPERTY_PT:0; return s; } static NativePropertyState *property_pt(NativePropertyCache *cache,const NativeMedium *m, double p,double T,NativePropertyState *scratch) { if(cache)for(size_t i=0;icount;i++) { NativePropertyState *s=&cache->states[i]; if((s->valid&NATIVE_PROPERTY_PT) && s->p==p && s->T==T && same_medium(&s->medium,m))return s; } return property_new(cache,m,p,T,scratch); } static double property_density(NativePropertyState *s) { if(!(s->valid&NATIVE_PROPERTY_RHO)) { s->rho=native_density(&s->medium,s->p,s->T); if(s->rho>0 && isfinite(s->rho))s->valid|=NATIVE_PROPERTY_RHO; } return s->rho; } static double property_viscosity(NativePropertyState *s) { if(!(s->valid&NATIVE_PROPERTY_MU)) { s->mu=native_viscosity(&s->medium,s->T,0); if(s->mu>0 && isfinite(s->mu))s->valid|=NATIVE_PROPERTY_MU; } return s->mu; } static void remember_gas(NativePropertyCache *cache,const NativeMedium *m,const NativeGas *gas) { /* Below helium's critical temperature retain the existing vapor-root/PH selection. A future phase-aware medium contract can carry that state. */ if(!cache || (m->real_helium && gas->T<=TC))return; NativePropertyState scratch,*s=property_pt(cache,m,gas->p,gas->T,&scratch); if(!(s->valid&NATIVE_PROPERTY_PT) || !isfinite(gas->h))return; if((s->valid&NATIVE_PROPERTY_H) && s->h!=gas->h) s=property_new(cache,m,gas->p,gas->T,&scratch); s->h=gas->h;s->valid|=NATIVE_PROPERTY_H; if(gas->rho>0 && isfinite(gas->rho)){s->rho=gas->rho;s->valid|=NATIVE_PROPERTY_RHO;} } double native_temperature_ph_context(NativePropertyCache *cache,const NativeMedium *m,double p,double h) { if(cache)for(size_t i=0;icount;i++) { NativePropertyState *s=&cache->states[i]; if((s->valid&NATIVE_PROPERTY_H) && s->p==p && s->h==h && same_medium(&s->medium,m))return s->T; } double T=native_temperature_ph(m,p,h); if(cache && isfinite(h) && T>0 && isfinite(T) && p>0 && isfinite(p)) { NativePropertyState scratch,*s=property_pt(cache,m,p,T,&scratch); if((s->valid&NATIVE_PROPERTY_H) && s->h!=h)s=property_new(cache,m,p,T,&scratch); s->h=h;s->valid|=NATIVE_PROPERTY_H; } return T; } double native_density_context(NativePropertyCache *cache,const NativeMedium *m,double p,double T) { NativePropertyState scratch;return property_density(property_pt(cache,m,p,T,&scratch)); } static double cube_root(double x) { return x==0 ? 0 : copysign(pow(fabs(x),1.0/3.0),x); } static void attraction(double T, double *a, double *da, double *dda) { double tr=T/TC, sr=sqrt(tr), base=1+kappa*(1-sr); *a=pr_a*base*base; *da=pr_a*(-base*kappa/(TC*sr)); *dda=pr_a*kappa/(2*TC*TC)*(kappa/tr+base/(tr*sr)); } static double z_factor(double p,double T) { double a,da,dda; attraction(T,&a,&da,&dda); double A=a*p/(RU*RU*T*T), B=pr_b*p/(RU*T); double ca=-(1-B),cb=A-3*B*B-2*B,cc=-(A*B-B*B-B*B*B); double pp=cb-ca*ca/3,qq=2*ca*ca*ca/27-ca*cb/3+cc; double disc=pow(qq/2,2)+pow(pp/3,3),off=-ca/3,roots[3]; int n; if(disc>1e-14) { roots[0]=cube_root(-qq/2+sqrt(disc))+cube_root(-qq/2-sqrt(disc))+off;n=1; } else if(fabs(disc)<=1e-14) { double u=cube_root(-qq/2);roots[0]=2*u+off;roots[1]=-u+off;n=2; } else { if(pp>=0) return NAN; double radius=2*sqrt(-pp/3); double arg=(3*qq/(2*pp))*sqrt(-3/pp),theta=acos(fmax(-1,fmin(1,arg)))/3; for(int i=0;i<3;i++) { roots[i]=radius*cos(theta-2*PI*i/3)+off; } n=3; } double z=-INFINITY;for(int i=0;iB && isfinite(roots[i])) z=fmax(z,roots[i]); return isfinite(z)?z:NAN; } static double density(double p,double T) { return MOLAR_MASS/(z_factor(p,T)*RU*T/p); } static double log_volume(double rho) { double v=MOLAR_MASS/rho,sq=sqrt(2.0); return log((v+(1+sq)*pr_b)/(v+(1-sq)*pr_b))/(2*sq*pr_b*MOLAR_MASS); } static double u_departure(double T,double rho) { double a,da,dda;attraction(T,&a,&da,&dda);return (T*da-a)*log_volume(rho); } static double h_departure(double p,double T) { double a,da,dda;attraction(T,&a,&da,&dda); double z=z_factor(p,T),B=pr_b*p/(RU*T),sq=sqrt(2.0); double dep=RU*T*(z-1)+(T*da-a)*log((z+(1+sq)*B)/(z+(1-sq)*B))/(2*sq*pr_b); return dep/MOLAR_MASS; } static double u_ideal(double T) { return rg*(1.5*T-745.375); } static double h_ideal(double T) { return rg*(2.5*T-745.375); } static double temperature_u(double u) { return (u/rg+745.375)/1.5; } static double temperature_h(double h) { return (h/rg+745.375)/2.5; } static double pressure_rho(double T,double rho) { double v=MOLAR_MASS/rho,a,da,dda;attraction(T,&a,&da,&dda); if(v<=pr_b || T<=0) return NAN; return RU*T/(v-pr_b)-a/(v*(v+pr_b)+pr_b*(v-pr_b)); } static NativeGas gas_properties(double m,double U,double V) { NativeGas g;g.rho=m/V;g.u=U/m; double T=fmax(temperature_u(g.u),2.2); for(int i=0;i<16;i++) { double next=fmax(temperature_u(g.u-u_departure(T,g.rho)),2.2); int done=fabs(next-T)<=1e-10*fmax(T,1);T=next;if(done) break; } g.T=T;g.p=pressure_rho(T,g.rho); /* Same energy reference as u_ideal/h_ideal. Avoid a redundant cubic solve in the single-root region; preserve vapor-root semantics below TC. */ g.h=T>TC ? g.u+g.p/g.rho : h_ideal(T)+h_departure(g.p,T);return g; } static double temperature_ph(double p,double h) { double T=fmax(temperature_h(h),2.2); for(int i=0;i<16;i++) { double next=fmax(temperature_h(h-h_departure(p,T)),2.2); int done=fabs(next-T)<=1e-10*fmax(T,1);T=next;if(done) break; }return T; } static void local_isentropic(NativePropertyState *s) { if(s->valid&NATIVE_PROPERTY_ISENTROPIC)return; double p=s->p,T=s->T,rho=property_density(s),v=MOLAR_MASS/rho,a,da,dda;attraction(T,&a,&da,&dda); double d=v*(v+pr_b)+pr_b*(v-pr_b); double dpT=RU/(v-pr_b)-da/d; double dpR=(-RU*T/pow(v-pr_b,2)+a*2*(v+pr_b)/(d*d))*(-MOLAR_MASS/(rho*rho)); double cv=1.5*rg+T*dda*log_volume(rho); double cp=cv+T*dpT*dpT/(rho*rho*dpR),gamma=cp/cv; s->isentropic_factor=p/(rho*dpR*gamma);s->isentropic_exponent=p*(gamma-1)/(gamma*T*dpT); if(isfinite(s->isentropic_factor) && isfinite(s->isentropic_exponent))s->valid|=NATIVE_PROPERTY_ISENTROPIC; } static double isentropic(NativePropertyCache *cache,NativePropertyState *up,double pd) { local_isentropic(up);if(pd>=up->p)return up->isentropic_factor; double Td=fmax(up->T*pow(fmax(pd/up->p,1e-12),up->isentropic_exponent),2.2); NativePropertyState scratch,*down=property_pt(cache,&up->medium,fmax(pd,1),Td,&scratch); local_isentropic(down);return .5*(up->isentropic_factor+down->isentropic_factor); } static double subsonic_cm(double r,double gamma,double rho,double T,double p) { return sqrt(fmax(2/(1-gamma)*rho*T/p*(pow(r,2*gamma)-pow(r,1+gamma)),0)); } static void state_valve(NativePropertyCache *cache,NativePropertyState *up,double pd,double *cm,double *vel) { double p=up->p,T=up->T;pd=fmax(fmin(pd,p),0); const NativeMedium *m=&up->medium; double cp=m->cp+m->slope*(T-m->Tref); double factor=m->real_helium?isentropic(cache,up,pd):(cp-m->R)/cp; double g=fmax(1e-9,fmin(1-1e-9,factor)),rho=fmax(property_density(up),1e-12); double r=fmax(pd/p,0),critical=pow(2*g/(g+1),1/(1-g)),eff; if(r<=critical) { eff=critical;*cm=sqrt(2/(1+g)*rho*T/p)*pow(2*g/(g+1),g/(1-g));*vel=sqrt(2/(1+g)*p/rho); } else { eff=r;*cm=subsonic_cm(r,g,rho,T,p);*vel=sqrt(fmax(2/(1-g)*p/rho*(1-pow(r,1-g)),0)); } double ref=subsonic_cm(.9999,g,rho,T,p); if(*cm>0 && ref>0) { double smooth=tanh(fmax(12*fabs(*cm/ref)*log(eff)/log(.9999),0));*cm*=smooth;*vel*=smooth; } } static void medium_valve(NativePropertyCache *cache,const NativeMedium *m,double p,double pd,double T,double *cm,double *vel) { NativePropertyState scratch,*up=property_pt(cache,m,fmax(p,1),fmax(T,1),&scratch); state_valve(cache,up,pd,cm,vel); } int native_gas_init(double p, double T, double volume, double *mU) { if (!(p > 0 && T >= 2.2 && volume > 0)) return 0; double rho = density(p, T); mU[0] = rho * volume; mU[1] = mU[0] * (u_ideal(T) + u_departure(T, rho)); return isfinite(mU[0]) && isfinite(mU[1]) && mU[0] > 0; } int native_gas(double m, double U, double volume, NativeGas *gas) { if (!(m > 0 && volume > 0) || !isfinite(U)) return 0; *gas = gas_properties(m, U, volume); return gas->p > 0 && isfinite(gas->p) && isfinite(gas->h); } int native_gas_context(NativePropertyCache *cache,double m,double U,double V,NativeGas *gas) { int ok=native_gas(m,U,V,gas);if(ok)remember_gas(cache,&helium_medium,gas);return ok; } int native_orifice(double p1, double p2, double h1, double h2, double cq_area, double opening, double *flow, double *cm, double *velocity) { return native_orifice_context(NULL,p1,p2,h1,h2,cq_area,opening,flow,cm,velocity); } int native_orifice_context(NativePropertyCache *cache,double p1,double p2,double h1,double h2, double cq_area,double opening,double *flow,double *cm,double *velocity) { int forward = p1 >= p2; double p = forward ? p1 : p2, pd = forward ? p2 : p1; double T = native_temperature_ph_context(cache,&helium_medium,fmax(p,1),forward?h1:h2); medium_valve(cache,&helium_medium,p,pd,T,cm,velocity); double sign = forward ? 1 : -1; *velocity *= sign; *flow = opening == 0 || fabs(p1-p2) <= 1e-8 ? 0 : sign * cq_area * opening * fmax(p, 1) * *cm / sqrt(fmax(T, 1)); if (opening == 0) *velocity = 0; return isfinite(*flow) && isfinite(*cm) && isfinite(*velocity); } double native_contact(double penetration, double velocity, double stiffness, double damping, double pdis, int signed_force) { if (penetration <= 0) return 0; double fraction = pdis > 0 ? -expm1(-penetration / pdis) : 1; double force = stiffness * penetration + fraction * damping * velocity; return signed_force == 1 ? force : fmax(force, 0); } void native_stop_motion(double x, double v, double lower, double upper, double *acceleration, double *velocity) { double vt = 1e-12 * fmax(fabs(v), 1); if ((x <= lower + 1e-12*fmax(fabs(lower),1) && v <= vt && *acceleration <= 0) || (x >= upper - 1e-12*fmax(fabs(upper),1) && v >= -vt && *acceleration >= 0)) { *acceleration = 0; *velocity = 0; } } double native_signal(double t, double start, int stages, int cyclic, const double *data) { double elapsed = fmax(t-start,0), duration = 0, offset = 0; for (int i=0;i 0) elapsed = fmod(elapsed,duration); for (int i=0;i 0 && event <= t) { double cycle=fmax(0,floor((t-event)/duration)+1); event += cycle*duration; if (event <= t) event += duration; } if (event > t) result=fmin(result,event); offset += data[16+i]; } return result; } static double ideal_temperature(const NativeMedium *m, double energy, double c) { double delta = energy - c*m->Tref; if (fabs(m->slope) <= 1e-15) return m->Tref + delta/c; double root = sqrt(fmax(c*c + 2*m->slope*delta, 0)); double a = (-c+root)/m->slope, b = (-c-root)/m->slope; return m->Tref + (fabs(a)<=fabs(b) ? a : b); } int native_medium_init(const NativeMedium *medium, double p, double T, double V, int legacy_ideal_initial, double *mU) { if (!(p>0 && T>0 && V>0)) return 0; if (medium->real_helium && !legacy_ideal_initial) return native_gas_init(p,T,V,mU); double mass=p*V/(medium->R*T),dt=T-medium->Tref; double u=medium->real_helium?u_ideal(T):(medium->cp-medium->R)*T+.5*medium->slope*dt*dt; mU[0]=mass;mU[1]=mass*u; return isfinite(mU[0]) && isfinite(mU[1]); } double native_density(const NativeMedium *m, double p, double T) { return m->real_helium ? density(p,T) : p/(m->R*T); } double native_temperature_ph(const NativeMedium *m, double p, double h) { return m->real_helium ? temperature_ph(p,h) : ideal_temperature(m,h,m->cp); } double native_viscosity(const NativeMedium *m, double T, int diagnostic) { /* Retain the ABI argument; flow and diagnostics use the same property. */ (void)diagnostic; if (m->real_helium) return 1e-7*exp(.7501594*log(T)+35.76324/T-2212.129/(T*T)+.9212635); return m->mu*pow(T/m->muT,1.5)*(m->muT+m->S)/(T+m->S); } int native_medium_gas(const NativeMedium *medium, double m, double U, double V, NativeGas *g) { if (medium->real_helium) return native_gas(m,U,V,g); if (!(m>0 && V>0)) return 0; g->u=U/m; g->rho=m/V; g->T=ideal_temperature(medium,g->u,medium->cp-medium->R); g->p=g->rho*medium->R*g->T; double dt=g->T-medium->Tref; g->h=medium->cp*g->T+.5*medium->slope*dt*dt; return g->T>0 && isfinite(g->p) && isfinite(g->h); } int native_medium_gas_context(NativePropertyCache *cache,const NativeMedium *m,double mass,double U,double V,NativeGas *g) { int ok=native_medium_gas(m,mass,U,V,g);if(ok)remember_gas(cache,m,g);return ok; } int native_medium_orifice(const NativeMedium *m, double p1, double p2, double h1, double h2, double area, double opening, double *q, double *cm, double *v) { return native_medium_orifice_context(NULL,m,p1,p2,h1,h2,area,opening,q,cm,v); } int native_medium_orifice_context(NativePropertyCache *cache,const NativeMedium *m,double p1,double p2,double h1,double h2, double area,double opening,double *q,double *cm,double *v) { double p=fmax(p1,p2),pd=fmin(p1,p2),sign=p1>=p2?1:-1; double T=fmax(native_temperature_ph_context(cache,m,fmax(p,1),p1>=p2?h1:h2),1); medium_valve(cache,m,p,pd,T,cm,v); *q=fabs(p1-p2)<=1e-8?0:sign*area*opening*fmax(p,1)*(*cm)/sqrt(T); *v*=fabs(opening)<=1e-12?0:sign; return isfinite(*q) && isfinite(*cm) && isfinite(*v); } static double pipe_rough_limit(double rr) { return rr>0 ? 1/pow(-2*log10(rr/3.7),2) : 0; } static double pipe_friction_prepared(double re, double rr, double rough_limit) { if(re<=0) return 64000000; double lam=64/re; if(re<=89.96829989) return lam; double smooth=1/pow(-1.8*log10(6.9/re),2),turb=smooth; if(rr>0) { double r=re*rr,weight=r*r/(r*r+180*180);turb+=weight*(rough_limit-smooth); } double trans=pow((re-89.96829989)/2741.96700831,8.37293695); return lam+trans/(1+trans)*(turb-lam); } static double pipe_friction(double re, double rr) { return pipe_friction_prepared(re,rr,pipe_rough_limit(rr)); } /* Derivative of the existing friction blend, local to this scalar solve. */ static double pipe_friction_derivative(double re,double rr,double rough,double *df) { double lam=64/re,dl=-lam/re; if(re<=89.96829989){*df=dl;return lam;} double a=-1.8*log10(6.9/re),smooth=1/(a*a); double ds=-2*smooth/a*1.8/(log(10.0)*re),turb=smooth,dt=ds; if(rr>0){double r=re*rr,w=r*r/(r*r+180*180),dw=2*w*(1-w)/re; turb+=w*(rough-smooth);dt=ds*(1-w)+dw*(rough-smooth);} double z=pow((re-89.96829989)/2741.96700831,8.37293695),b=z/(1+z); double db=8.37293695*b*(1-b)/(re-89.96829989); *df=dl*(1-b)+b*dt+db*(turb-lam);return lam+b*(turb-lam); } double native_pipe_resistance(double K,double rr,double flow_per_re,NativePipeSolve *status) { NativePipeSolve local={0,0,0,INFINITY};if(!status)status=&local; *status=local; if(!(K>=0 && rr>=0 && flow_per_re>0) || !isfinite(K) || !isfinite(rr) || !isfinite(flow_per_re))return NAN; if(K/64<=89.96829989){status->converged=1;status->relative_residual=0;return K/64;} double rough=pipe_rough_limit(rr),lo=0,hi=fmax(sqrt(K/.02),1),df; for(int i=0;i<128 && hi*hi*pipe_friction_prepared(hi,rr,rough)iterations=i+1; double f=pipe_friction_derivative(re,rr,rough,&df),F=re*re*f-K; double target=sqrt(K/f); if(fabs(target-re)*flow_per_re<=fmax(1e-13,re*flow_per_re*1e-10)) { /* Check the returned point, not just the change of successive guesses. */ status->relative_residual=fabs(target*target*pipe_friction_prepared(target,rr,rough)/K-1); if(isfinite(target) && status->relative_residual<=1e-9){status->converged=1;return target;} } if(F>0)hi=re;else lo=re; double next=re-F/(2*re*f+re*re*df); if(!isfinite(next) || next<=lo || next>=hi){next=.5*(lo+hi);status->bisections++;} re=next; } return NAN; } double native_pipe_flow(const NativeMedium *m, double p1, double p2, double T, double d, double length, double rr, int kind) { return native_pipe_flow_context(NULL,m,p1,p2,T,d,length,rr,kind); } double native_pipe_flow_context(NativePropertyCache *cache,const NativeMedium *m,double p1,double p2,double T, double d,double length,double rr,int kind) { if(fabs(p1-p2)<=1e-8) return 0; double p=fmax(fmax(p1,p2),1),pd=fmin(p1,p2),sign=p1>p2?1:-1; T=fmax(T,1); NativePropertyState scratch,*up=property_pt(cache,m,p,T,&scratch); double area=PI*d*d/4,mu=property_viscosity(up),den=PI*d*mu; /* PNL0001/2/3 share compressible flow and its near-equilibrium smoothing. PNL0003 differs in storage placement, not in the resistance law. */ double cm,vel;state_valve(cache,up,pd,&cm,&vel); if(kind==0) { double lam=pow(area*p*cm,2)/(16*PI*mu*length*T); if(4*lam/den<=1000) return sign*lam; } double base=area*p*cm/sqrt(T),K=pow(4*base/den,2)*d/length; return sign*native_pipe_resistance(K,rr,den/4,NULL)*den/4; } double native_pipe_flow_cached(NativePipeCache *cache, const NativeMedium *m, double p1, double p2, double T, double d, double length, double rr, int kind) { return native_pipe_flow_cached_context(NULL,cache,m,p1,p2,T,d,length,rr,kind); } double native_pipe_flow_cached_context(NativePropertyCache *properties,NativePipeCache *cache,const NativeMedium *m, double p1,double p2,double T,double d,double length,double rr,int kind) { if(cache->valid && cache->p1==p1 && cache->p2==p2 && cache->T==T && cache->diameter==d && cache->length==length && cache->roughness==rr && cache->kind==kind && same_medium(&cache->medium,m)) return cache->flow; double result=properties?native_pipe_flow_context(properties,m,p1,p2,T,d,length,rr,kind): native_pipe_flow(m,p1,p2,T,d,length,rr,kind); cache->valid=0; if(isfinite(result)) { cache->medium=*m;cache->p1=p1;cache->p2=p2;cache->T=T; cache->diameter=d;cache->length=length;cache->roughness=rr;cache->kind=kind; cache->flow=result;cache->valid=1; } return result; } void native_pipe_diagnostics(const NativeMedium *m, double q, double p, double T, double d, double length, double rr, int diagnostic, double *r) { native_pipe_diagnostics_context(NULL,m,q,p,T,d,length,rr,diagnostic,r); } void native_pipe_diagnostics_context(NativePropertyCache *cache,const NativeMedium *m,double q,double p,double T, double d,double length,double rr,int diagnostic,double *r) { (void)diagnostic; NativePropertyState scratch,*state=property_pt(cache,m,p,T,&scratch); double area=PI*d*d/4,re=4*fabs(q)/(PI*d*property_viscosity(state)),ff=pipe_friction(re,rr); r[0]=re; r[1]=fabs(q)*sqrt(T)/fmax(sqrt(d/(length*ff))*area*p,1e-18); r[2]=q/(fmax(property_density(state),1e-12)*area);r[3]=ff; } double native_limit_force(double penetration, double velocity, double stiffness, double damping, double depth, int signed_force) { if(penetration<=0) return 0; double force=stiffness*penetration+(depth>0?fmin(penetration/depth,1):1)*damping*velocity; return signed_force==1?force:fmax(force,0); }