top.cpp serif/utils/serialize/top.hpp algorithm atomic cerrno chrono cmath cstdio cstring fstream iomanip limits locale numbers stdexcept string utility vector serif serif::utils serif::utils::serialize SERIF_TOP_METADATA member output << #member << " " << report.member << '\n' #include"serif/utils/serialize/top.hpp" #include<algorithm> #include<atomic> #include<cerrno> #include<chrono> #include<cmath> #include<cstdio> #include<cstring> #include<fstream> #include<iomanip> #include<limits> #include<locale> #include<numbers> #include<stdexcept> #include<string> #include<utility> #include<vector> #ifdefined(__linux__) #include<fcntl.h> #include<sys/syscall.h> #include<unistd.h> #endif namespaceserif::utils::serialize{ namespace{ constexprdoublepi=std::numbers::pi; usingMatrix=std::vector<std::vector<double>>; structFailure{Errorerror;}; [[noreturn]]voidfail(std::stringpath,std::stringmessage){ throwFailure{Error{std::move(path),std::move(message)}}; } doublechecked(Result<double>value,conststd::string&path){ if(!value){ fail(path+(value.error().path.empty()?"":"/"+value.error().path),value.error().message); } if(!std::isfinite(*value)){fail(path,"Thesourcereturnedanonfinitevalue.");} return*value; } boolpositive(doublevalue){returnstd::isfinite(value)&&value>0.;} std::vector<double>legendre(doublemu,intdegree){ std::vector<double>result(degree+1,1.); if(degree){result[1]=mu;} for(intl=2;l<=degree;++l){ result[l]=((2*l-1)*mu*result[l-1]-(l-1)*result[l-2])/l; } returnresult; } std::pair<std::vector<double>,std::vector<double>>gauss(intcount){ std::vector<double>nodes(count),weights(count); for(inti=0;i<(count+1)/2;++i){ doublex=std::cos(pi*(i+.75)/(count+.5)); doublederivative=0.; for(intstep=0;step<100;++step){ constautop=legendre(x,count); derivative=count*(x*p[count]-p[count-1])/(x*x-1.); constdoublenext=x-p[count]/derivative; if(std::abs(next-x)<=4*std::numeric_limits<double>::epsilon()){ x=next; constautofinal=legendre(x,count); derivative=count*(x*final[count]-final[count-1])/(x*x-1.); break; } x=next; if(step==99){fail("options/angular_points","Gauss-Legendrenodesdidnotconverge.");} } nodes[i]=-x;nodes[count-1-i]=x; weights[i]=weights[count-1-i]=2./((1-x*x)*derivative*derivative); } return{std::move(nodes),std::move(weights)}; } doublereconstruct(conststd::vector<double>&coefficients,doublemu){ constautop=legendre(mu,static_cast<int>(coefficients.size())-1); longdoublesum=0.; for(std::size_tl=0;l<coefficients.size();l+=2){ sum+=coefficients[l]*std::sqrt((2.*l+1)/(4*pi))*p[l]; } returnstatic_cast<double>(sum); } std::vector<double>project(conststd::vector<double>&values,conststd::vector<double>&mu, conststd::vector<double>&weights,intdegree){ std::vector<longdouble>sums(degree+1,0.); for(std::size_tj=0;j<mu.size();++j){ constautop=legendre(mu[j],degree); for(intl=0;l<=degree;l+=2){sums[l]+=values[j]*weights[j]*p[l];} } std::vector<double>result(degree+1,0.); for(intl=0;l<=degree;l+=2){ result[l]=static_cast<double>(sums[l])*std::sqrt(pi*(2*l+1)); } returnresult; } Matrixderivative_matrix(conststd::vector<double>&zeta){ constintcount=static_cast<int>(zeta.size()); Matrixresult(count,std::vector<double>(count)); for(inti=0;i<count;++i){ constdoublewi=(i%2?-1.:1.)*(i==0||i==count-1?.5:1.); for(intj=0;j<count;++j){ if(i==j){continue;} constdoublewj=(j%2?-1.:1.)*(j==0||j==count-1?.5:1.); result[i][j]=(wj/wi)/(zeta[i]-zeta[j]); } } returnresult; } Matrixdifferentiate(constMatrix&values,constMatrix&derivative){ Matrixresult(values.size(),std::vector<double>(values.front().size())); for(std::size_ti=0;i<values.size();++i){ for(std::size_tl=0;l<values[i].size();l+=2){ longdoublesum=0.; for(std::size_tj=0;j<values.size();++j){ sum+=derivative[i][j]*(values[j][l]-values[i][l]); } result[i][l]=static_cast<double>(sum); if(!std::isfinite(result[i][l])){fail("enthalpy_et_der","Aradialspectralderivativeisnonfinite.");} } } returnresult; } structTemporaryDirectory{ std::filesystem::pathpath; ~TemporaryDirectory(){if(!path.empty()){std::error_codeerror;std::filesystem::remove_all(path,error);}} }; std::ofstreamopen_output(conststd::filesystem::path&path){ std::ofstreamoutput(path); output.imbue(std::locale::classic()); output<<std::scientific<<std::setprecision(std::numeric_limits<double>::max_digits10); if(!output){fail(path.string(),"CouldnotcreateTOPoutputfile.");} returnoutput; } voidclose_output(std::ofstream&output,conststd::filesystem::path&path){ output.close(); if(!output){fail(path.string(),"CouldnotfinishwritingTOPoutputfile.");} } voidpublish(conststd::filesystem::path&temporary,conststd::filesystem::path&path){ #ifdefined(__APPLE__) if(::renamex_np(temporary.c_str(),path.c_str(),RENAME_EXCL)==0){return;} fail(path.string(),"CouldnotpublishTOPdirectorywithoutreplacement:"+std::string{std::strerror(errno)}); #elifdefined(__linux__)&&defined(SYS_renameat2) if(::syscall(SYS_renameat2,AT_FDCWD,temporary.c_str(),AT_FDCWD,path.c_str(),1U)==0){return;} fail(path.string(),"CouldnotpublishTOPdirectorywithoutreplacement:"+std::string{std::strerror(errno)}); #else fail(path.string(),"Atomicdirectorypublicationwithoutreplacementisunavailableonthisplatform."); #endif } } Result<TopExportReport>export_top(conststd::filesystem::path&requested_path, constTopPolytropeSource&source,constTopExportOptions&options){ try{ if(requested_path.empty()||requested_path.filename().empty()||requested_path.filename()=="."||requested_path.filename()==".."){ fail(requested_path.string(),"Specifyanewmodeldirectory,notanemptypathorparentdirectory."); } constautopath=std::filesystem::absolute(requested_path).lexically_normal(); if(std::filesystem::symlink_status(path).type()!=std::filesystem::file_type::not_found){ fail(path.string(),"TOPexportrefusestoreplaceanexistingdestination."); } if(!std::filesystem::is_directory(path.parent_path())){fail(path.string(),"Theoutputparentdirectorydoesnotexist.");} if(options.radial_points<4||options.radial_points>4096){fail("options/radial_points","Usebetween4and4096radialpoints.");} if(options.maximum_degree<0||options.maximum_degree>512||options.maximum_degree%2){ fail("options/maximum_degree","Themaximumharmonicdegreemustbeevenandbetween0and512."); } if(options.angular_points<std::max(2,options.maximum_degree+1)||options.angular_points>4096){ fail("options/angular_points","Angularquadraturerequiresatleastmaximum_degree+1pointsandbetween2and4096points."); } if(options.azimuthal_points<4||options.azimuthal_points>1024){fail("options/azimuthal_points","Usebetween4and1024azimuthalsamples.");} for(constauto&[name,value]:{std::pair{"relative_symmetry_tolerance",options.relative_symmetry_tolerance}, {"relative_surface_tolerance",options.relative_surface_tolerance}, {"relative_surface_projection_tolerance",options.relative_surface_projection_tolerance}}){ if(!std::isfinite(value)||value<0.){fail("options/"+std::string{name},"Tolerancemustbefiniteandnonnegative.");} } for(constauto&[name,value]:{std::pair{"polytropic_index",source.polytropic_index}, {"gravitational_constant",source.gravitational_constant},{"mass",source.mass}, {"central_density",source.central_density},{"central_enthalpy",source.central_enthalpy}}){ if(!positive(value)){fail("source/"+std::string{name},"Thisphysicalscalemustbefiniteandpositive.");} } if(source.polytropic_index<1.){fail("source/polytropic_index","TOP'svacuumpolytropeequationsrequiren>=1inthisexporter.");} if(!std::isfinite(source.angular_velocity)){fail("source/angular_velocity","Angularvelocitymustbefinite.");} if(!source.surface_radius||!source.enthalpy){fail("source","Bothsurface-radiusandenthalpysamplingcallbacksarerequired.");} TopExportReportreport{.directory=path,.radial_points=options.radial_points,.maximum_degree=options.maximum_degree, .angular_points=options.angular_points,.azimuthal_points=options.azimuthal_points}; constauto[mu,weights]=gauss(options.angular_points); autoradii=[&](doublecosine){ std::vector<double>values; values.reserve(2*options.azimuthal_points); for(doublesign:{1.,-1.}){ for(intk=0;k<options.azimuthal_points;++k){ constdoublephi=2*pi*k/options.azimuthal_points; constdoublevalue=checked(source.surface_radius(sign*cosine,phi),"source/surface_radius"); if(value<=0.){fail("source/surface_radius","Thesurfacemustbestar-shapedwithpositiveradiusineverysampleddirection.");} values.push_back(value); } } returnvalues; }; automean=[](conststd::vector<double>&values){ longdoublesum=0.;for(doublevalue:values){sum+=value;} returnstatic_cast<double>(sum/values.size()); }; report.source_equatorial_radius=mean(radii(0.)); report.source_polar_radius=mean(radii(1.)); autosurface_samples=[&](doublecosine){ autovalues=radii(cosine); constdoubleaverage=mean(values); for(doublevalue:values){ report.maximum_relative_surface_symmetry_error=std::max(report.maximum_relative_surface_symmetry_error, std::abs(value-average)/report.source_equatorial_radius); } returnvalues; }; Matrixradius_values; std::vector<double>surface_means; for(doublecosine:mu){ radius_values.push_back(surface_samples(cosine)); surface_means.push_back(mean(radius_values.back())); } autosurface_coefficients=project(surface_means,mu,weights,options.maximum_degree); report.equatorial_radius=reconstruct(surface_coefficients,0.); report.polar_radius=reconstruct(surface_coefficients,1.); if(!positive(report.equatorial_radius)||!positive(report.polar_radius)){fail("domains_boundaries","Projectedequatorialandpolarradiimustbepositive.");} report.minimum_mapping_derivative=std::numeric_limits<double>::infinity(); automapping_check=[&](doubleradius){ //A'(zeta)rangesfromzeroto15/8,sothisistheexactradialminimum. constdoubleminimum=std::min(report.polar_radius, report.polar_radius+1.875*(radius-report.polar_radius))/report.equatorial_radius; report.minimum_mapping_derivative=std::min(report.minimum_mapping_derivative,minimum); if(!positive(minimum)){fail("domains_boundaries","ThesourceorprojectedsurfacegivesanonpositiveTOPmappingderivative.");} }; for(intj=0;j<=2*options.angular_points;++j){ constdoublecosine=std::cos(pi*j/(2*options.angular_points)); constautovalues=surface_samples(cosine); constdoubleprojected=reconstruct(surface_coefficients,cosine); if(!positive(projected)){fail("domains_boundaries","Projectedsurfaceradiusmuststaypositive.");} mapping_check(projected); for(doublevalue:values){mapping_check(value);} report.maximum_relative_surface_projection_error=std::max(report.maximum_relative_surface_projection_error, std::abs(projected-mean(values))/report.source_equatorial_radius); } if(report.maximum_relative_surface_symmetry_error>options.relative_symmetry_tolerance){ fail("source/surface_radius","Surfaceaxisymmetryorequatorial-symmetrydefectexceedsrelative_symmetry_tolerance."); } if(report.maximum_relative_surface_projection_error>options.relative_surface_projection_tolerance){ fail("domains_boundaries","Surfaceharmonictruncationexceedsrelative_surface_projection_tolerance;increasemaximum_degreeorangular_points."); } report.flattening=1.-report.polar_radius/report.equatorial_radius; report.angular_frequency_unit=std::sqrt(4*pi*source.gravitational_constant*source.central_density); report.lambda=std::pow(report.angular_frequency_unit*report.equatorial_radius,2)/source.central_enthalpy; report.alpha=4*pi*source.central_density*std::pow(report.equatorial_radius,3)/(3*source.mass); report.rotation=source.angular_velocity*report.equatorial_radius/std::sqrt(source.central_enthalpy); report.rotation_over_kepler=source.angular_velocity*std::sqrt(std::pow(report.equatorial_radius,3)/(source.gravitational_constant*source.mass)); if(!positive(report.angular_frequency_unit)||!positive(report.lambda)||!positive(report.alpha) ||!std::isfinite(report.rotation)||!std::isfinite(report.rotation_over_kepler)){ fail("lambda_eps","Physicalnormalizationoverflowedorunderflowed."); } std::vector<double>zeta(options.radial_points); for(inti=0;i<options.radial_points;++i){zeta[i]=.5*(1-std::cos(pi*i/(options.radial_points-1)));} zeta.front()=0.;zeta.back()=1.; report.minimum_enthalpy=std::numeric_limits<double>::infinity(); autoenthalpy_samples=[&](inti,doublecosine,conststd::vector<double>&surfaces){ constdoublez=zeta[i],a=(5*z*z*z-3*std::pow(z,5))/2; std::vector<double>values; values.reserve(surfaces.size()); std::size_tindex=0; for(doublesign:{1.,-1.}){ for(intk=0;k<options.azimuthal_points;++k){ constdoubleradius=i==options.radial_points-1?surfaces[index]: report.polar_radius*z+a*(surfaces[index]-report.polar_radius); constdoubleraw=checked(source.enthalpy(radius,sign*cosine,2*pi*k/options.azimuthal_points), "source/enthalpy/radial_"+std::to_string(i))/source.central_enthalpy; if(!std::isfinite(raw)){fail("source/enthalpy","Normalizedenthalpyisnonfinite.");} if(i==options.radial_points-1){ report.maximum_relative_surface_enthalpy=std::max(report.maximum_relative_surface_enthalpy,std::abs(raw)); if(std::abs(raw)>options.relative_surface_tolerance){ fail("source/enthalpy/surface","TOP'svacuumsurfacerequireszeroenthalpywithinrelative_surface_tolerance."); } }elseif(raw<=0.){ fail("source/enthalpy/radial_"+std::to_string(i),"TOPrequiresstrictlypositiveinteriorenthalpyforitslogarithmicderivatives."); } if(i==0&&std::abs(raw-1.)>1e-8){fail("source/central_enthalpy","Thesuppliedcentralenthalpydoesnotmatchthesourcecenter.");} values.push_back(raw); ++index; } } constdoubleaverage=mean(values); for(doublevalue:values){ report.maximum_relative_enthalpy_symmetry_error=std::max(report.maximum_relative_enthalpy_symmetry_error,std::abs(value-average)); } if(report.maximum_relative_enthalpy_symmetry_error>options.relative_symmetry_tolerance){ fail("source/enthalpy","Enthalpyaxisymmetryorequatorial-symmetrydefectexceedsrelative_symmetry_tolerance."); } //Thevacuumboundaryisimposedonlyaftermeasuringandacceptingitssourcedefect. constdoubleresult=i==options.radial_points-1?0.:average; report.minimum_enthalpy=std::min(report.minimum_enthalpy,result); returnresult; }; Matrixcoefficients; coefficients.reserve(options.radial_points); for(inti=0;i<options.radial_points;++i){ std::vector<double>values; for(intj=0;j<options.angular_points;++j){values.push_back(enthalpy_samples(i,mu[j],radius_values[j]));} coefficients.push_back(project(values,mu,weights,options.maximum_degree)); for(intj=0;j<options.angular_points;++j){ report.maximum_relative_enthalpy_projection_error=std::max(report.maximum_relative_enthalpy_projection_error, std::abs(reconstruct(coefficients.back(),mu[j])-values[j])); } } //Includepolesandequatorinphysicalboundary/symmetrychecks,independentlyofGaussnodes. for(doublecosine:{0.,1.}){ constautosurfaces=surface_samples(cosine); for(inti=0;i<options.radial_points;++i){enthalpy_samples(i,cosine,surfaces);} } constautoderivative=derivative_matrix(zeta); constautofirst=differentiate(coefficients,derivative); constautosecond=differentiate(first,derivative); //TOPusesh_zeta_theta/h_zetaatthesurface,soaflatoroutward-increasing //exportedprofilecannotbehandedtothatreaderevenwhenallsamplesarefinite. for(intj=0;j<=2*options.angular_points;++j){ constdoubleslope=reconstruct(first.back(),std::cos(pi*j/(2*options.angular_points))); if(!std::isfinite(slope)||slope>=-128*std::numeric_limits<double>::epsilon()){ fail("enthalpy_et_der/surface_derivative","TOPrequiresstrictlynegative,nonvanishingsurfaceenthalpyderivative;refinetheexportorsourcemodel."); } } //TOPtakesabs(h)silentlyanddividesbyh.Rejectnonpositiveprojectionvaluesbeforeeitheroperation. for(inti=0;i<options.radial_points-1;++i){ for(intj=0;j<=2*options.angular_points;++j){ constdoublevalue=reconstruct(coefficients[i],std::cos(pi*j/(2*options.angular_points))); if(!positive(value)){ fail("enthalpy_et_der","Harmonicprojectioncreatesnonpositiveinteriorenthalpy;increaseangularresolution."); } } } if(source.roundoff_density_clamps){report.roundoff_density_clamps=source.roundoff_density_clamps();} for(double&coefficient:surface_coefficients){coefficient/=report.equatorial_radius;} TemporaryDirectorytemporary; staticstd::atomic<unsignedlonglong>sequence{0}; for(intattempt=0;attempt<100;++attempt){ constautosuffix=std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())+"-"+std::to_string(sequence++); autocandidate=path.parent_path()/("."+path.filename().string()+".top-tmp-"+suffix); if(std::filesystem::create_directory(candidate)){temporary.path=std::move(candidate);break;} } if(temporary.path.empty()){fail(path.string(),"CouldnotreserveatemporaryTOPdirectory.");} { constautofile=temporary.path/"lambda_eps"; autooutput=open_output(file); for(doublevalue:{report.lambda,report.flattening,report.alpha,0.,source.polytropic_index, source.polytropic_index,report.rotation,report.rotation_over_kepler}){output<<value<<'\n';} output<<options.radial_points<<'\n'; close_output(output,file); } { constautofile=temporary.path/"domains_boundaries"; autooutput=open_output(file); output<<options.maximum_degree<<'\n'; for(intl=0;l<=options.maximum_degree;++l){output<<0.<<'';}output<<'\n'; for(doublevalue:surface_coefficients){output<<value<<'';}output<<'\n'; close_output(output,file); } { constautofile=temporary.path/"enthalpy_et_der"; autooutput=open_output(file); output<<options.radial_points<<''<<options.maximum_degree+1<<'\n'; output<<"enthalpyinphysicalspace:(onehemisphere)\n"; constautophysical_mu=gauss(options.maximum_degree+1).first; for(intj=0;j<=options.maximum_degree/2;++j){ output<<j+1<<'\n'; for(constauto&row:coefficients){output<<reconstruct(row,physical_mu[j])<<'\n';} } autoblock=[&](constchar*label,constMatrix&values){ output<<label<<'\n'; for(intl=0;l<=options.maximum_degree;l+=2){ output<<l<<'\n';for(constauto&row:values){output<<row[l]<<'\n';} } }; block("enthalpyinlegendrespace:l=0,2,4...lmax",coefficients); block("firstradialderivativeinlegendrespace",first); block("secondradialderivativeinlegendrespace",second); close_output(output,file); } { constautofile=temporary.path/"metadata.txt"; autooutput=open_output(file); output<<"SERiFTOPpoly_esterexportversion1\n" <<"Coordinates:zeta=(1-cos(pi*i/(nr-1)))/2;mu=cos(theta)relativetothesourcerotationaxis.\n" <<"Basis:Y_l0=sqrt((2*l+1)/(4*pi))*P_l(mu);evenlonly.\n" <<"Projection:sourcesurfaceandenthalpyareaveragedoverazimuthandbothhemispheres,thenprojectedatcommonsurface-fittedzeta.\n" <<"Physicalsamplingmap:r=Rp*zeta+(5*zeta^3-3*zeta^5)/2*(source_surface_radius-Rp).\n" <<"TOPreplacessource_surface_radiuswithitsharmonicprojection;thisisarecordedgeometryapproximation,notanexactFEremap.\n" <<"Derivatives:Chebyshevinterpolationderivativesatfixedangle;exportrefinementisrequiredbeforeinterpretingmodeaccuracy.\n" <<"Surface:acceptedsourceenthalpydefectisrecorded,thenboundaryenthalpyissettozero.Interiorvaluesarenotclipped.\n" <<"Enthalpyprojectiondiagnostic:maximumGauss-nodeerroroftheaveragedH/Hcaftervacuum-boundarynormalization;thesourcesurfacedefectisreportedseparately.\n" <<"Diagnosticunits:enthalpyanditsdefectsarenormalizedbycentral_enthalpy;radialmappingderivativebyequatorial_radius.\n" <<"Frequency:TOPangularfrequencytimesangular_frequency_unitgivessource-unitangularfrequency;divideby2*piforcyclicfrequency.\n" <<"Gamma1isselectedbyTOP'sequationset,notbythesefiles.Diagnosticmaximaaresampled,notrigorousglobalbounds.\n"; #defineSERIF_TOP_METADATA(member)output<<#member<<""<<report.member<<'\n' SERIF_TOP_METADATA(radial_points);SERIF_TOP_METADATA(maximum_degree);SERIF_TOP_METADATA(angular_points);SERIF_TOP_METADATA(azimuthal_points); SERIF_TOP_METADATA(source_equatorial_radius);SERIF_TOP_METADATA(source_polar_radius); SERIF_TOP_METADATA(equatorial_radius);SERIF_TOP_METADATA(polar_radius);SERIF_TOP_METADATA(lambda);SERIF_TOP_METADATA(flattening); SERIF_TOP_METADATA(alpha);SERIF_TOP_METADATA(rotation);SERIF_TOP_METADATA(rotation_over_kepler);SERIF_TOP_METADATA(angular_frequency_unit); SERIF_TOP_METADATA(maximum_relative_surface_symmetry_error);SERIF_TOP_METADATA(maximum_relative_enthalpy_symmetry_error); SERIF_TOP_METADATA(maximum_relative_surface_projection_error);SERIF_TOP_METADATA(maximum_relative_enthalpy_projection_error); SERIF_TOP_METADATA(maximum_relative_surface_enthalpy); SERIF_TOP_METADATA(minimum_mapping_derivative);SERIF_TOP_METADATA(minimum_enthalpy);SERIF_TOP_METADATA(roundoff_density_clamps); #undefSERIF_TOP_METADATA output<<"polytropic_index"<<source.polytropic_index<<'\n'<<"gravitational_constant"<<source.gravitational_constant<<'\n' <<"mass"<<source.mass<<'\n'<<"angular_velocity"<<source.angular_velocity<<'\n' <<"central_density"<<source.central_density<<'\n'<<"central_enthalpy"<<source.central_enthalpy<<'\n' <<"relative_symmetry_tolerance"<<options.relative_symmetry_tolerance<<'\n' <<"relative_surface_tolerance"<<options.relative_surface_tolerance<<'\n' <<"relative_surface_projection_tolerance"<<options.relative_surface_projection_tolerance<<'\n'; close_output(output,file); } publish(temporary.path,path); temporary.path.clear(); returnreport; }catch(constFailure&failure){ returnstd::unexpected(failure.error); }catch(conststd::exception&error){ returnstd::unexpected(Error{requested_path.string(),error.what()}); } } }