高温氦气物性补全;三通四通能量计算bug修正
This commit is contained in:
1 parent
6fc9afe41d
commit
2b07d996cf
133 files changed
+5230
-253310
No files matched your search
+32
-1
@@ -34,6 +34,29 @@ bash bat/setup-native-linux.sh
|
||||
|
||||
原生运行库已在本机 GCC 8.1 / SUNDIALS 7.4.0 验证。构建需要对应 C 头文件、导入库和 DLL,单纯安装 Python 包不能代替这些文件。构建器根据环境变量或当前 Python 基础环境查找 SUNDIALS,根据环境变量或 PATH 查找 GCC。
|
||||
|
||||
## 仿真结果分块存储
|
||||
|
||||
网页和 Python 原生运行入口将采样结果写入项目根目录的 `simresults/run-<id>/`,不依赖启动时的工作目录。每次运行保存 `manifest.json`、`states.bin` 与 `outputs.bin`;纯求解(`--solve-only`)不创建结果档案。此目录已加入 Git 忽略。
|
||||
|
||||
清单和返回诊断中的 `directory` 为相对项目根目录的 `simresults/run-<id>`,统一使用 `/`,`directoryBase` 为 `project`。读取时使用 `result_storage.result_archive_path(id)` 根据当前代码位置定位;旧清单中的绝对目录提示不参与定位。停止应用、移动或复制整个项目(保留 `simresults`)、重新启动后,结果随项目迁移。运行中传给原生进程的绝对路径由当前目录重新计算,不固定盘符或机器路径;不支持运行过程中移动项目目录。
|
||||
|
||||
- **容量**:默认 1 GiB(1,073,741,824 字节),通过后端环境变量 `SIMULATION_RESULT_STORAGE_MB` 配置,单位 MiB,必须是正整数。例如 PowerShell 使用 `$env:SIMULATION_RESULT_STORAGE_MB = '1024'`,Linux 使用 `export SIMULATION_RESULT_STORAGE_MB=1024`。按文件逻辑字节计量,不包含文件系统分配单元的额外开销。
|
||||
- **自动清理**:写每个采样块前,为状态块及其未来输出块一起预留空间。总配额涵盖所有结果档案和元数据;空间不足时依次删除最早结束的非活动档案(异常退出且未结束的档案按创建时间排序)。跨进程锁保护配额分配和正在使用的档案。符号链接、目录联接及用户另外放入的文件不会被自动删除;这些内容仍计入目录用量。活动档案、孤儿但仍存活的工作进程被保留。旧结果属于可淘汰历史,需要长期保留的结果应导出另存。
|
||||
- **空间仍不足**:停止采样并报告 `storage-quota`,保留此前完整提交的块,不删除当前运行的数据,不悄悄丢样后继续报告成功。结果诊断 `native.resultStorage` 分别记录求解状态、状态/输出样本数、已存储到的时间及有效字节数;已存储时间可能早于求解器最终到达时间。
|
||||
- **有限缓冲与背压**:采样缓冲最多 1024 行,按状态和输出列数进一步限制到约 1 MiB(单行更大时至少容纳一行)。缓冲写满后复用;采用同步批量写入,磁盘慢时求解线程等待,没有无限增长的异步写入队列。求解结束后逐块计算和写入输出,再分块生成兼容 JSON,原生工作进程不分配完整历史状态或输出数组。
|
||||
- **数值隔离**:只替换结果记录缓冲,保留 RK45/CVODE 当前状态、稠密输出和事件定位数据。输出计算维持原先的结束后顺序,避免在求解过程中改变模型缓存。相同时间戳沿用原有“后值替换前值”规则,包括块边界。
|
||||
- **取消与异常**:正常取消、求解失败会补写最后一个不满的块。每块具有 CRC32 校验和提交标记;强制终止后可通过 `result_storage.scan_blocks` 扫描完整前缀,截断尾块和损坏块不计为有效样本。强制终止时尚未计算的输出不会被伪造,已有状态块仍保留。元数据通过临时文件替换发布;块完成执行 `fflush`,不逐块强制物理落盘,因此这不是断电零丢失承诺。
|
||||
|
||||
块格式为 `SIMBLK01` + 四个小端 uint64(块序号、行数、列数、CRC32)+ 按列连续的小端 Float64 数据 + `COMMIT01`;每个块的第一列是时间。各文件的完整列顺序由清单的 `metadata.stateColumns`、`metadata.outputColumns` 指定;纯代数模型内部占位状态以 `null` 标记。公开状态和变量元数据仍保存在 `metadata.stateKeys`、`metadata.variables`。Windows/Linux 共用格式和 64 位文件偏移。
|
||||
|
||||
本阶段保留现有完整 JSON 传输和浏览器加载契约:后端返回结果和浏览器显示时仍可能加载完整数据。变量/时间范围查询和浏览器按需加载属于后续阶段;现有百万采样点保护仍保留。
|
||||
|
||||
回归入口(Windows 和 Linux 使用各自的 Python 环境):
|
||||
|
||||
```text
|
||||
python -W error::ResourceWarning -m unittest tests.test_result_storage tests.test_native_sample_storage tests.test_native_result_transport tests.test_native_worker_control -v
|
||||
```
|
||||
|
||||
## 独立生成与运行
|
||||
|
||||
```powershell
|
||||
@@ -50,7 +73,15 @@ test/native-v1/example-run/program/model.exe --method RK45 --start 0 --stop 10 -
|
||||
|
||||
EXE 不需要 Python、SciPy、XML 或原工程文件。DLL 需要与 EXE 一同保留。默认运行设置是 RK45、0–10 s、最大步长 0.001 s;按需要传入运行选项。
|
||||
|
||||
`--solve-only` 关闭轨迹采样,只输出最终状态与诊断。`solveSeconds` 是程序内部数值求解墙钟时间,包含求解必需的 RHS 和事件定位,排除模型初始化、结果投影和文件写入;`processWallSeconds` 另含进程启动与结果处理。预热一次后报告三次求解的中位数。
|
||||
`--solve-only` 关闭轨迹采样,只输出最终状态与诊断。`solveSeconds` 是程序内部数值求解墙钟时间,包含求解必需的 RHS 和事件定位,排除模型初始化、求解结束后的结果投影与 JSON 写入;启用采样时包含积分期间分块写盘及等待配额的时间。`processWallSeconds` 另含进程启动与结果处理。预热一次后报告三次求解的中位数。
|
||||
|
||||
## 求解器外层终止行为
|
||||
|
||||
BDF 外层仅在 CVODE 返回负错误码、返回非法时间/状态或触发取消、实际超时及资源保护时结束。成功返回但时间暂时未变时保留求解器历史继续调用,不以重复次数判失败,也不人为推进时间或重启来跳过该段。持续不推进沿用当前整次求解的墙钟时限;默认 300 秒,Python 进程监控另有 5 秒退出宽限。用户取消后,进程仍不响应且被强制结束时返回取消状态,并明确没有完整结果文件,只保留最后一次进度报告。
|
||||
|
||||
独立结果的 `solverControl`(网页位于 `diagnostics.native.solverControl`)记录终止原因、操作、CVODE 返回码、同时间返回次数和最长连续次数,以及最后的内部时间与步长。CVODE 数值诊断仅用于 BDF。正常返回的 `1` 表示到达指定停止边界;非有限诊断数值写为 JSON `null`。取消/超时的原因不会被随后产生的 CVODE 回调错误覆盖。强制取消没有可信轨迹时,`series`/`final` 为空,未知求解统计不填零,`statisticsComplete=false`。
|
||||
|
||||
原八路 10.8 秒退出问题的对照复现、50 秒完成验证及异常分支测试见 [封装修复验收报告](../docs/other/求解器外层提前终止修复与验收-2026-09-14.md)。专项回归:`python -m unittest tests.test_native_solver_control tests.test_native_worker_control -v`。
|
||||
|
||||
## 雅可比分组差分与诊断
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* 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.
|
||||
* Helium NASA ranges and continuation are checked against independent
|
||||
* Amesim 2404 property-library outputs, including high-temperature boundaries.
|
||||
*/
|
||||
#include "component_properties_internal.h"
|
||||
#include "component_constants_internal.h"
|
||||
@@ -24,12 +24,22 @@ NATIVE_COMPONENT_INTERNAL int same_medium(const NativeMedium *a,const NativeMedi
|
||||
}
|
||||
void native_properties_init(NativePropertyCache *cache,NativePropertyState *states,size_t capacity) {
|
||||
cache->states=states;cache->count=0;cache->capacity=states?capacity:0;
|
||||
cache->temperatures=NULL;
|
||||
}
|
||||
static void observe_temperature(NativePropertyTemperatures *report,const NativeMedium *m,double T,unsigned domains) {
|
||||
if(!report || !m->real_helium || !isfinite(T))return;
|
||||
for(int i=0;i<NATIVE_PROPERTY_DOMAINS;i++)if(domains&(1u<<i)) {
|
||||
if(!(report->seen&(1u<<i)))report->minimum[i]=report->maximum[i]=T;
|
||||
else {report->minimum[i]=fmin(report->minimum[i],T);report->maximum[i]=fmax(report->maximum[i],T);}
|
||||
report->seen|=1u<<i;
|
||||
}
|
||||
}
|
||||
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->count<cache->capacity ? &cache->states[cache->count++] : scratch;
|
||||
*s=(NativePropertyState){0};s->medium=*m;s->p=p;s->T=T;s->valid=valid?NATIVE_PROPERTY_PT:0;
|
||||
s->temperatures=cache?cache->temperatures:NULL;
|
||||
return s;
|
||||
}
|
||||
NATIVE_COMPONENT_INTERNAL NativePropertyState *property_pt(NativePropertyCache *cache,const NativeMedium *m,
|
||||
@@ -41,6 +51,7 @@ NATIVE_COMPONENT_INTERNAL NativePropertyState *property_pt(NativePropertyCache *
|
||||
return property_new(cache,m,p,T,scratch);
|
||||
}
|
||||
NATIVE_COMPONENT_INTERNAL double property_density(NativePropertyState *s) {
|
||||
observe_temperature(s->temperatures,&s->medium,s->T,1);
|
||||
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;
|
||||
@@ -48,6 +59,7 @@ NATIVE_COMPONENT_INTERNAL double property_density(NativePropertyState *s) {
|
||||
return s->rho;
|
||||
}
|
||||
NATIVE_COMPONENT_INTERNAL double property_viscosity(NativePropertyState *s) {
|
||||
observe_temperature(s->temperatures,&s->medium,s->T,4);
|
||||
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;
|
||||
@@ -55,6 +67,7 @@ NATIVE_COMPONENT_INTERNAL double property_viscosity(NativePropertyState *s) {
|
||||
return s->mu;
|
||||
}
|
||||
static void remember_gas(NativePropertyCache *cache,const NativeMedium *m,const NativeGas *gas) {
|
||||
observe_temperature(cache?cache->temperatures:NULL,m,gas->T,3);
|
||||
/* 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;
|
||||
@@ -68,9 +81,12 @@ static void remember_gas(NativePropertyCache *cache,const NativeMedium *m,const
|
||||
double native_temperature_ph_context(NativePropertyCache *cache,const NativeMedium *m,double p,double h) {
|
||||
if(cache)for(size_t i=0;i<cache->count;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;
|
||||
if((s->valid&NATIVE_PROPERTY_H) && s->p==p && s->h==h && same_medium(&s->medium,m)) {
|
||||
observe_temperature(cache->temperatures,m,s->T,3);return s->T;
|
||||
}
|
||||
}
|
||||
double T=native_temperature_ph(m,p,h);
|
||||
observe_temperature(cache?cache->temperatures:NULL,m,T,3);
|
||||
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);
|
||||
@@ -119,10 +135,75 @@ static double h_departure(double p,double T) {
|
||||
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; }
|
||||
/* Amesim 2404 helium_cp_h_s.data, NASA 2002 ideal contribution.
|
||||
* The two lower ranges have Cp/R=2.5. Amesim shifts the third range's
|
||||
* constant Cp coefficient and integration constant to preserve Cp and h
|
||||
* at 6000 K; copying its raw a8 would introduce a ~3.94 J/kg enthalpy jump.
|
||||
* The PR departure terms below are unchanged.
|
||||
*/
|
||||
static double helium_high_cp_r(double T) {
|
||||
return 3396845/(T*T)-2194.038/T+3.080232+
|
||||
T*(-8.068958e-5+T*(6.252785e-9+T*(-2.574990e-13+T*4.429960e-18)));
|
||||
}
|
||||
static double helium_high_cp_slope_r(double T) {
|
||||
return -2*3396845/(T*T*T)+2194.038/(T*T)-8.068958e-5+
|
||||
T*(2*6.252785e-9+T*(-3*2.574990e-13+T*4*4.429960e-18));
|
||||
}
|
||||
static double cp_ideal(double T) {
|
||||
if(T<=6000)return 2.5*rg;
|
||||
double offset=helium_high_cp_r(6000)-2.5;
|
||||
if(T<=20000)return rg*(helium_high_cp_r(T)-offset);
|
||||
/* Amesim's out-of-table continuation: quadratic Cp with matched value
|
||||
and slope. This is extrapolation, not a wider validity claim. */
|
||||
double c=helium_high_cp_r(20000)-offset;
|
||||
double x=(T-20000)*helium_high_cp_slope_r(20000)/c;
|
||||
return rg*c*(1+x+.5*x*x);
|
||||
}
|
||||
static double helium_high_enthalpy_increment(double T) {
|
||||
const double b=6000,b2=b*b,b3=b2*b,b4=b3*b;
|
||||
double x=T-b,t2=T*T,t3=t2*T,t4=t3*T;
|
||||
/* Integrate Cp from the boundary; log1p and factored powers retain
|
||||
precision when an integrator crosses 6000 K by a very small step. */
|
||||
return rg*(3396845*x/(T*b)-2194.038*log1p(x/b)+x*(
|
||||
3.080232-(helium_high_cp_r(b)-2.5)-8.068958e-5*(T+b)/2+
|
||||
6.252785e-9*(t2+T*b+b2)/3-2.574990e-13*(t3+t2*b+T*b2+b3)/4+
|
||||
4.429960e-18*(t4+t3*b+t2*b2+T*b3+b4)/5));
|
||||
}
|
||||
static double h_ideal(double T) {
|
||||
if(T<=6000)return rg*(2.5*T-745.375);
|
||||
double h=rg*(2.5*6000-745.375)+helium_high_enthalpy_increment(fmin(T,20000));
|
||||
if(T>20000) {
|
||||
double c=helium_high_cp_r(20000)-helium_high_cp_r(6000)+2.5;
|
||||
double dt=T-20000,x=dt*helium_high_cp_slope_r(20000)/c;
|
||||
h+=rg*c*dt*(1+.5*x+x*x/6);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
static double u_ideal(double T) {
|
||||
return T<=6000 ? rg*(1.5*T-745.375) : h_ideal(T)-rg*T;
|
||||
}
|
||||
static double temperature_ideal(double energy,int enthalpy) {
|
||||
double c=enthalpy?2.5:1.5,linear=(energy/rg+745.375)/c;
|
||||
if(linear<=6000 || !isfinite(linear))return linear;
|
||||
double lo=6000,hi=fmax(linear,6001);
|
||||
for(int i=0;i<64;i++) {
|
||||
double value=enthalpy?h_ideal(hi):u_ideal(hi);
|
||||
if(value>=energy)break;
|
||||
hi*=2;
|
||||
}
|
||||
double T=fmin(linear,hi);
|
||||
for(int i=0;i<64;i++) {
|
||||
double value=enthalpy?h_ideal(T):u_ideal(T),residual=value-energy;
|
||||
if(fabs(residual)<=1e-12*fmax(fabs(energy),1))return T;
|
||||
if(residual>0)hi=T;else lo=T;
|
||||
double next=T-residual/(cp_ideal(T)-(enthalpy?0:rg));
|
||||
if(!isfinite(next) || next<=lo || next>=hi)next=lo+.5*(hi-lo);
|
||||
T=next;
|
||||
}
|
||||
return T;
|
||||
}
|
||||
static double temperature_u(double u) { return temperature_ideal(u,0); }
|
||||
static double temperature_h(double h) { return temperature_ideal(h,1); }
|
||||
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;
|
||||
@@ -144,12 +225,13 @@ static double temperature_ph(double p,double h) {
|
||||
int done=fabs(next-T)<=1e-10*fmax(T,1);T=next;if(done) break; }return T;
|
||||
}
|
||||
static void local_isentropic(NativePropertyState *s) {
|
||||
observe_temperature(s->temperatures,&s->medium,s->T,3);
|
||||
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 cv=cp_ideal(T)-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;
|
||||
@@ -222,8 +304,18 @@ double native_temperature_ph(const NativeMedium *m, double p, double h) {
|
||||
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);
|
||||
if (m->real_helium) {
|
||||
/* helium_mu.data: NASA 96, microPoise -> Pa.s. Anchor ln(mu)
|
||||
at each boundary, as Amesim does, to remove coefficient rounding
|
||||
jumps. The last range is also its out-of-table extrapolation. */
|
||||
double l1000=.7501594*log(1000)+35.76324/1000-2212.129/1e6+.9212635;
|
||||
if(T<=1000)return 1e-7*exp(.7501594*log(T)+35.76324/T-2212.129/(T*T)+.9212635);
|
||||
if(T<=5000)return 1e-7*exp(l1000+.8339417*log(T/1000)+
|
||||
220.8266*(1/T-1./1000)-52852.59*(1/(T*T)-1e-6));
|
||||
double l5000=l1000+.8339417*log(5)+220.8266*(1./5000-1./1000)-52852.59*(1./25e6-1e-6);
|
||||
return 1e-7*exp(l5000+.8631635*log(T/5000)+
|
||||
962.0518*(1/T-1./5000)-1249870*(1/(T*T)-1./25e6));
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -17,4 +17,5 @@ int native_json_format_double(char output[NATIVE_JSON_DOUBLE_CAPACITY], double v
|
||||
* The caller owns FILE and must check its final flush/close before publishing. */
|
||||
int native_json_write_number(FILE *file, double value);
|
||||
int native_json_write_array(FILE *file, const double *values, size_t count, size_t stride);
|
||||
int native_json_write_values(FILE *file, const double *values, size_t count, size_t stride, int leading_comma);
|
||||
#endif
|
||||
@@ -24,14 +24,23 @@ enum {
|
||||
NATIVE_PROPERTY_PT=1, NATIVE_PROPERTY_H=2, NATIVE_PROPERTY_RHO=4,
|
||||
NATIVE_PROPERTY_MU=8, NATIVE_PROPERTY_ISENTROPIC=16
|
||||
};
|
||||
/* Optional observer for one evaluation, independent of cache capacity.
|
||||
* Slots: EOS, ideal caloric data, dynamic viscosity; each has min/max T. */
|
||||
#define NATIVE_PROPERTY_DOMAINS 3
|
||||
typedef struct {
|
||||
unsigned seen;
|
||||
double minimum[NATIVE_PROPERTY_DOMAINS], maximum[NATIVE_PROPERTY_DOMAINS];
|
||||
} NativePropertyTemperatures;
|
||||
typedef struct {
|
||||
NativeMedium medium;
|
||||
double p, T, h, rho, mu, isentropic_factor, isentropic_exponent;
|
||||
unsigned valid;
|
||||
NativePropertyTemperatures *temperatures;
|
||||
} NativePropertyState;
|
||||
typedef struct {
|
||||
NativePropertyState *states;
|
||||
size_t count, capacity;
|
||||
NativePropertyTemperatures *temperatures;
|
||||
} NativePropertyCache;
|
||||
void native_properties_init(NativePropertyCache *, NativePropertyState *, size_t capacity);
|
||||
int native_gas_context(NativePropertyCache *, double m, double U, double V, NativeGas *);
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
#define NATIVE_RUNTIME_H
|
||||
#include "model.h"
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#ifndef NFRICTIONS
|
||||
#define NFRICTIONS 0
|
||||
#endif
|
||||
@@ -10,10 +12,19 @@ typedef struct {
|
||||
int bdf, record_samples;
|
||||
const char *cancel_path;
|
||||
} NativeOptions;
|
||||
typedef struct {
|
||||
int seen;
|
||||
double first_time, first_temperature, extreme_time, extreme_temperature;
|
||||
} NativePropertyWarning;
|
||||
typedef struct {
|
||||
NativeOptions options;
|
||||
double *times, *states;
|
||||
size_t count, capacity, sample_index;
|
||||
/* Result history is a bounded buffer, independent of integrator memory. */
|
||||
size_t buffered, committed, output_count;
|
||||
FILE *sample_file, *output_file;
|
||||
const char *sample_path, *output_blocks_path;
|
||||
int storage_control, storage_failed;
|
||||
double final_time, final_state[NSTATES];
|
||||
double wall_start, cpu_start, solve_seconds, solve_cpu_seconds, last_progress;
|
||||
double max_accepted_step;
|
||||
@@ -22,13 +33,21 @@ typedef struct {
|
||||
int jacobian_verify, jacobian_colored;
|
||||
unsigned long jacobian_rhs, jacobian_colored_evals, jacobian_fallbacks;
|
||||
unsigned long jacobian_checks, jacobian_mismatches, cvode_rhs, linear_rhs;
|
||||
unsigned long same_time_returns, same_time_streak, max_same_time_streak;
|
||||
int stagnating, cvode_flag;
|
||||
double cvode_return_time, cvode_internal_time, cvode_last_step, cvode_next_step;
|
||||
const char *failure_reason, *failure_operation;
|
||||
char message_buffer[384];
|
||||
int status; /* 0 completed, 1 cancelled, 2 failed */
|
||||
const char *message;
|
||||
NativePropertyWarning property_warnings[6];
|
||||
} NativeRun;
|
||||
typedef int (*NativeDense)(void *context, double t, double *state);
|
||||
double native_wall_time(void);
|
||||
double native_cpu_time(void);
|
||||
int native_poll(NativeRun *run, double time);
|
||||
void native_property_warnings_json(NativeRun *run, FILE *file);
|
||||
int native_fail(NativeRun *run, const char *reason, const char *operation, const char *message);
|
||||
int native_rhs(NativeRun *run, double t, const double *y, double *dy);
|
||||
int native_jacobian_rhs(NativeRun *run, double t, const double *y, double *dy);
|
||||
int native_append(NativeRun *run, double t, const double *y);
|
||||
@@ -39,4 +58,9 @@ int native_rk45(NativeRun *run);
|
||||
int native_bdf(NativeRun *run);
|
||||
int native_solve(NativeRun *run);
|
||||
void native_run_free(NativeRun *run);
|
||||
int native_samples_append(NativeRun *run, double t, const double *state);
|
||||
int native_samples_flush(NativeRun *run);
|
||||
int native_samples_outputs(NativeRun *run);
|
||||
int native_samples_json(NativeRun *run, FILE *file, size_t column);
|
||||
int64_t native_file_tell(FILE *file);
|
||||
#endif
|
||||
+78
-21
@@ -30,21 +30,34 @@ double native_cpu_time(void) {
|
||||
#endif
|
||||
}
|
||||
|
||||
int native_fail(NativeRun *r,const char *reason,const char *operation,const char *message) {
|
||||
if (!r->status) {
|
||||
r->status=2; r->failure_reason=reason; r->failure_operation=operation;
|
||||
snprintf(r->message_buffer,sizeof(r->message_buffer),"%s",message);
|
||||
r->message=r->message_buffer;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int native_poll(NativeRun *r, double t) {
|
||||
double now=native_wall_time();
|
||||
if (r->status) return 0;
|
||||
if (r->options.timeout > 0 && now-r->wall_start > r->options.timeout) {
|
||||
r->status=2; r->message="Native solve exceeded its time limit."; return 0;
|
||||
}
|
||||
if (now-r->last_progress >= 0.1) {
|
||||
r->last_progress=now;
|
||||
if (r->options.cancel_path) {
|
||||
FILE *f=fopen(r->options.cancel_path,"rb");
|
||||
if (f) { fclose(f); r->status=1; r->message="Simulation cancelled."; return 0; }
|
||||
if (f) {
|
||||
fclose(f); r->status=1; r->message="Simulation cancelled.";
|
||||
r->failure_reason="cancelled"; r->failure_operation="cancel-check"; return 0;
|
||||
}
|
||||
}
|
||||
fprintf(stderr,"{\"phase\":\"integrating\",\"time\":%.17g,\"nfev\":%lu,\"acceptedSteps\":%lu}\n",t,r->nfev,r->accepted);
|
||||
fflush(stderr);
|
||||
}
|
||||
if (r->options.timeout > 0 && now-r->wall_start > r->options.timeout)
|
||||
return native_fail(r,r->stagnating?"time-stagnation":"timeout","integration",
|
||||
r->stagnating?"Native solver kept returning without advancing time until the run time limit expired.":
|
||||
"Native solve exceeded its time limit.");
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -70,20 +83,55 @@ int native_jacobian_rhs(NativeRun *r, double t, const double *y, double *dy) {
|
||||
int native_append(NativeRun *r, double t, const double *y) {
|
||||
r->final_time=t; memcpy(r->final_state,y,NSTATES*sizeof(double));
|
||||
if (!r->options.record_samples) return 1;
|
||||
if (r->count && r->times[r->count-1] == t) r->count--;
|
||||
if (r->count == r->capacity) {
|
||||
size_t cap=r->capacity ? r->capacity*2 : 1024;
|
||||
if (cap > 2000000 || cap > 268435456u / NSTATES / sizeof(double)) return 0;
|
||||
double *times=realloc(r->times,cap*sizeof(double));
|
||||
if (!times) return 0;
|
||||
r->times=times;
|
||||
double *states=realloc(r->states,cap*NSTATES*sizeof(double));
|
||||
if (!states) return 0;
|
||||
r->states=states; r->capacity=cap;
|
||||
return native_samples_append(r,t,y);
|
||||
}
|
||||
|
||||
/* Amesim helium data domains: EOS, ideal Cp/h, viscosity. Warnings describe
|
||||
* the property use, not a component. Trial/Newton/Jacobian evaluations never
|
||||
* publish warnings; replay of output samples is also side-effect free. */
|
||||
static const double property_lower[3]={2.2,200,200};
|
||||
static const double property_upper[3]={1500,20000,15000};
|
||||
static const char *property_domain[3]={"equation-of-state","ideal-caloric","dynamic-viscosity"};
|
||||
static void property_warning_json(FILE *f,int slot,const NativePropertyWarning *w) {
|
||||
int domain=slot/2,upper=slot%2;
|
||||
fprintf(f,"{\"code\":\"helium-%s-%s\",\"medium\":\"helium\",\"property\":\"%s\","
|
||||
"\"direction\":\"%s\",\"limit\":%.17g,\"time\":%.17g,\"temperature\":%.17g,"
|
||||
"\"extremeTime\":%.17g,\"extremeTemperature\":%.17g}",
|
||||
property_domain[domain],upper?"high":"low",property_domain[domain],upper?"high":"low",
|
||||
upper?property_upper[domain]:property_lower[domain],w->first_time,w->first_temperature,
|
||||
w->extreme_time,w->extreme_temperature);
|
||||
}
|
||||
void native_property_warnings_json(NativeRun *r,FILE *f) {
|
||||
int comma=0;fputc('[',f);
|
||||
for(int i=0;i<6;i++)if(r->property_warnings[i].seen) {
|
||||
if(comma++)fputc(',',f);
|
||||
property_warning_json(f,i,&r->property_warnings[i]);
|
||||
}
|
||||
r->times[r->count]=t;
|
||||
memcpy(r->states+r->count*NSTATES,y,NSTATES*sizeof(double));
|
||||
r->count++; return 1;
|
||||
fputc(']',f);
|
||||
}
|
||||
static void check_property_temperatures(NativeRun *r,double t,const double *y) {
|
||||
#if defined(MODEL_PROPERTY_TEMPERATURES) && MODEL_PROPERTY_TEMPERATURES
|
||||
NativePropertyTemperatures temperatures={0};
|
||||
/* Additional diagnostic evaluation does not alter states or solver counters. */
|
||||
if(!model_property_temperatures(t,y,&temperatures))return;
|
||||
for(int d=0;d<3;d++)if(temperatures.seen&(1u<<d))for(int upper=0;upper<2;upper++) {
|
||||
double T=upper?temperatures.maximum[d]:temperatures.minimum[d];
|
||||
double limit=upper?property_upper[d]:property_lower[d];
|
||||
/* Ignore inversion roundoff at an exact domain endpoint. */
|
||||
double tolerance=1e-9*fmax(limit,1);
|
||||
if(upper ? T<=limit+tolerance : T>=limit-tolerance)continue;
|
||||
int slot=2*d+upper;NativePropertyWarning *w=&r->property_warnings[slot];
|
||||
if(!w->seen) {
|
||||
w->seen=1;w->first_time=w->extreme_time=t;w->first_temperature=w->extreme_temperature=T;
|
||||
fputs("{\"phase\":\"property-warning\",\"warning\":",stderr);
|
||||
property_warning_json(stderr,slot,w);fputs("}\n",stderr);fflush(stderr);
|
||||
} else if(upper ? T>w->extreme_temperature : T<w->extreme_temperature) {
|
||||
w->extreme_temperature=T;w->extreme_time=t;
|
||||
}
|
||||
}
|
||||
#else
|
||||
(void)r;(void)t;(void)y;
|
||||
#endif
|
||||
}
|
||||
|
||||
static double locate(int idx, double bound, int lower, double left, double right,
|
||||
@@ -213,6 +261,7 @@ int native_accept(NativeRun *r, double t, double next, const double *old,
|
||||
} else memcpy(accepted_state,trial,NSTATES*sizeof(double));
|
||||
*accepted_time=stop;
|
||||
r->final_time=stop; memcpy(r->final_state,accepted_state,NSTATES*sizeof(double));
|
||||
check_property_temperatures(r,stop,accepted_state);
|
||||
return count ? 1 : 0;
|
||||
}
|
||||
|
||||
@@ -229,16 +278,24 @@ int native_solve(NativeRun *r) {
|
||||
}
|
||||
#endif
|
||||
r->sample_index=1;
|
||||
if (!native_append(r,r->options.start,y)) { r->status=2; r->message="Cannot allocate samples."; return 0; }
|
||||
check_property_temperatures(r,r->options.start,y);
|
||||
if (!native_append(r,r->options.start,y)) return 0;
|
||||
r->wall_start=native_wall_time(); r->cpu_start=native_cpu_time();
|
||||
r->last_progress=r->wall_start-1;
|
||||
int ok=r->options.bdf ? native_bdf(r) : native_rk45(r);
|
||||
r->solve_seconds=native_wall_time()-r->wall_start;
|
||||
r->solve_cpu_seconds=native_cpu_time()-r->cpu_start;
|
||||
if (!ok && !r->status) { r->status=2; r->message="Native integration failed to advance."; }
|
||||
if (!native_append(r,r->final_time,r->final_state)) { r->status=2; r->message="Cannot save final state."; }
|
||||
if (!ok && !r->status) native_fail(r,"integration-failure","integration","Native integration failed.");
|
||||
if (ok && !r->status && r->final_time!=r->options.stop)
|
||||
native_fail(r,"incomplete-result","integration","Native solver returned before the requested stop time.");
|
||||
if (!r->storage_failed) native_append(r,r->final_time,r->final_state);
|
||||
native_samples_flush(r);
|
||||
if (!r->message) r->message=r->status ? "Native integration failed." : "Simulation completed.";
|
||||
return !r->status;
|
||||
}
|
||||
|
||||
void native_run_free(NativeRun *r) { free(r->times); free(r->states); }
|
||||
void native_run_free(NativeRun *r) {
|
||||
free(r->times); free(r->states);
|
||||
if(r->sample_file) fclose(r->sample_file);
|
||||
if(r->output_file) fclose(r->output_file);
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <sunmatrix/sunmatrix_dense.h>
|
||||
#include <sunlinsol/sunlinsol_dense.h>
|
||||
#include <math.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <sundials/sundials_math.h>
|
||||
#ifndef MODEL_JACOBIAN_COLORED
|
||||
@@ -195,59 +196,126 @@ static void counters(NativeRun *r, void *solver) {
|
||||
CVodeGetNumLinRhsEvals(solver,&value); r->linear_rhs+=(unsigned long)value;
|
||||
}
|
||||
|
||||
static void cv_snapshot(NativeRun *r,void *solver) {
|
||||
CVodeGetCurrentTime(solver,&r->cvode_internal_time);
|
||||
CVodeGetLastStep(solver,&r->cvode_last_step);
|
||||
CVodeGetCurrentStep(solver,&r->cvode_next_step);
|
||||
}
|
||||
|
||||
static void cv_failure(NativeRun *r,int flag,const char *operation) {
|
||||
char message[256];
|
||||
r->cvode_flag=flag;
|
||||
snprintf(message,sizeof(message),"CVODE operation %s failed (return code %d) at simulation time %.17g s.",
|
||||
operation,flag,r->final_time);
|
||||
native_fail(r,"solver-error",operation,message);
|
||||
}
|
||||
|
||||
#define CV_CHECK(call) do { int code_=(call); if(code_<0) { \
|
||||
cv_failure(r,code_,#call); goto cleanup; } } while(0)
|
||||
|
||||
int native_bdf(NativeRun *r) {
|
||||
SUNContext ctx=NULL;
|
||||
if (SUNContext_Create(SUN_COMM_NULL,&ctx)) return 0;
|
||||
if (SUNContext_Create(SUN_COMM_NULL,&ctx))
|
||||
return native_fail(r,"initialization-failure","SUNContext_Create","Cannot create SUNDIALS context.");
|
||||
N_Vector y=N_VNew_Serial(NSTATES,ctx), atol=N_VNew_Serial(NSTATES,ctx), scratch=N_VNew_Serial(NSTATES,ctx);
|
||||
SUNMatrix matrix=NULL; SUNLinearSolver linear=NULL; void *solver=NULL;
|
||||
int success=0;
|
||||
int success=0, initialized=0;
|
||||
CvContext context={.run=r};
|
||||
if (!y || !atol || !scratch) goto cleanup;
|
||||
const char *allocation="N_VNew_Serial";
|
||||
if (!y || !atol || !scratch) goto allocation_failure;
|
||||
memcpy(N_VGetArrayPointer(y),r->final_state,NSTATES*sizeof(double));
|
||||
memcpy(N_VGetArrayPointer(atol),model_atol,NSTATES*sizeof(double));
|
||||
matrix=SUNDenseMatrix(NSTATES,NSTATES,ctx);
|
||||
if (!matrix) goto cleanup;
|
||||
allocation="SUNDenseMatrix";
|
||||
if (!matrix) goto allocation_failure;
|
||||
linear=SUNLinSol_Dense(y,matrix,ctx);
|
||||
if (!linear) goto cleanup;
|
||||
allocation="SUNLinSol_Dense";
|
||||
if (!linear) goto allocation_failure;
|
||||
solver=CVodeCreate(CV_BDF,ctx);
|
||||
if (!solver) goto cleanup;
|
||||
allocation="CVodeCreate";
|
||||
if (!solver) goto allocation_failure;
|
||||
context.solver=solver;
|
||||
double t=r->options.start;
|
||||
if (CVodeInit(solver,cv_rhs,t,y)<0 || CVodeSetUserData(solver,&context)<0 ||
|
||||
CVodeSVtolerances(solver,r->options.rtol,atol)<0 ||
|
||||
CVodeSetLinearSolver(solver,linear,matrix)<0 ||
|
||||
CVodeSetMaxStep(solver,r->options.max_step)<0) goto cleanup;
|
||||
r->cvode_return_time=t;
|
||||
CV_CHECK(CVodeInit(solver,cv_rhs,t,y));
|
||||
initialized=1;
|
||||
CV_CHECK(CVodeSetUserData(solver,&context));
|
||||
CV_CHECK(CVodeSVtolerances(solver,r->options.rtol,atol));
|
||||
CV_CHECK(CVodeSetLinearSolver(solver,linear,matrix));
|
||||
CV_CHECK(CVodeSetMaxStep(solver,r->options.max_step));
|
||||
#if MODEL_JACOBIAN_COLORED
|
||||
if (valid_coloring()) {
|
||||
context.weights=N_VClone(y);
|
||||
if (!context.weights) goto cleanup;
|
||||
allocation="N_VClone";
|
||||
if (!context.weights) goto allocation_failure;
|
||||
if (r->jacobian_verify) {
|
||||
context.reference=SUNDenseMatrix(NSTATES,NSTATES,ctx);
|
||||
if (!context.reference) goto cleanup;
|
||||
allocation="SUNDenseMatrix (verification)";
|
||||
if (!context.reference) goto allocation_failure;
|
||||
}
|
||||
context.colored=1; r->jacobian_colored=1;
|
||||
if (CVodeSetJacFn(solver,cv_jacobian)<0) goto cleanup;
|
||||
CV_CHECK(CVodeSetJacFn(solver,cv_jacobian));
|
||||
}
|
||||
#endif
|
||||
r->starts++;
|
||||
CvDense dense={solver,scratch};
|
||||
while (t<r->options.stop) {
|
||||
double boundary=model_next_break(t,r->options.stop);
|
||||
if (!isfinite(boundary) || boundary<=t || boundary>r->options.stop) {
|
||||
native_fail(r,"invalid-boundary","model_next_break","Model returned an invalid next time boundary.");
|
||||
goto cleanup;
|
||||
}
|
||||
double end=boundary<r->options.stop?nextafter(boundary,-INFINITY):boundary;
|
||||
if (CVodeSetStopTime(solver,end)<0) goto cleanup;
|
||||
CV_CHECK(CVodeSetStopTime(solver,end));
|
||||
while (t<end) {
|
||||
if (!native_poll(r,t) || r->accepted>10000000 || r->events>10000) goto cleanup;
|
||||
if (!native_poll(r,t)) goto cleanup;
|
||||
if (r->accepted>10000000 || r->events>10000) {
|
||||
native_fail(r,"resource-limit","integration",r->events>10000?
|
||||
"Native state-transition limit exceeded.":"Native accepted-step limit exceeded.");
|
||||
goto cleanup;
|
||||
}
|
||||
double old[NSTATES], accepted[NSTATES], next=t;
|
||||
memcpy(old,N_VGetArrayPointer(y),sizeof(old));
|
||||
int flag=CVode(solver,end,y,&next,CV_ONE_STEP);
|
||||
if (flag<0 || next<=t) goto cleanup;
|
||||
r->cvode_flag=flag; r->cvode_return_time=next;
|
||||
if (flag<0) { cv_failure(r,flag,"CVode"); goto cleanup; }
|
||||
if (!isfinite(next)) {
|
||||
native_fail(r,"nonfinite-time","CVode","CVODE returned a non-finite simulation time."); goto cleanup;
|
||||
}
|
||||
for (int i=0;i<NSTATES;i++) if (!isfinite(N_VGetArrayPointer(y)[i])) {
|
||||
native_fail(r,"nonfinite-state","CVode","CVODE returned a non-finite accepted state."); goto cleanup;
|
||||
}
|
||||
if (next<t || next>end) {
|
||||
native_fail(r,next<t?"time-regression":"time-overshoot","CVode",
|
||||
next<t?"CVODE returned a time earlier than the last accepted time.":
|
||||
"CVODE advanced beyond the requested time boundary."); goto cleanup;
|
||||
}
|
||||
if (next==t) {
|
||||
/* A successful internal step may be smaller than one time ULP.
|
||||
* Retain CVODE's history/state and continue under the existing
|
||||
* wall-clock timeout/cancellation checks. Never fabricate time,
|
||||
* reinitialize, or evaluate zero-length dense/event intervals. */
|
||||
r->same_time_returns++; r->same_time_streak++;
|
||||
if (r->same_time_streak>r->max_same_time_streak) r->max_same_time_streak=r->same_time_streak;
|
||||
if (!r->stagnating)
|
||||
fprintf(stderr,"{\"event\":\"solver-time-stagnation\",\"time\":%.17g,\"returnCode\":%d}\n",t,flag);
|
||||
r->stagnating=1;
|
||||
continue;
|
||||
}
|
||||
if (r->stagnating)
|
||||
fprintf(stderr,"{\"event\":\"solver-time-resumed\",\"time\":%.17g,\"sameTimeReturns\":%lu}\n",next,r->same_time_streak);
|
||||
r->stagnating=0; r->same_time_streak=0;
|
||||
if (!native_poll(r,t)) goto cleanup;
|
||||
r->accepted++; r->max_accepted_step=fmax(r->max_accepted_step,next-t);
|
||||
int impact=native_accept(r,t,next,old,N_VGetArrayPointer(y),cv_dense,&dense,&t,accepted);
|
||||
if (impact<0) goto cleanup;
|
||||
if (impact<0) {
|
||||
native_fail(r,"sampling-event-failure","native_accept","Cannot evaluate an accepted step's samples or events.");
|
||||
goto cleanup;
|
||||
}
|
||||
memcpy(N_VGetArrayPointer(y),accepted,sizeof(accepted));
|
||||
if (impact) {
|
||||
counters(r,solver);
|
||||
if (CVodeReInit(solver,t,y)<0) goto cleanup;
|
||||
CV_CHECK(CVodeReInit(solver,t,y));
|
||||
r->starts++;
|
||||
}
|
||||
}
|
||||
@@ -255,17 +323,23 @@ int native_bdf(NativeRun *r) {
|
||||
memcpy(r->final_state,N_VGetArrayPointer(y),NSTATES*sizeof(double));
|
||||
double sample=r->options.start+r->sample_index*r->options.sample_step;
|
||||
if (r->options.record_samples && sample<=t && sample<=r->options.stop) {
|
||||
if (!native_append(r,sample,r->final_state)) goto cleanup;
|
||||
if (!native_append(r,sample,r->final_state)) {
|
||||
native_fail(r,"sample-storage-failure","native_append","Cannot store a boundary sample."); goto cleanup;
|
||||
}
|
||||
r->sample_index++;
|
||||
}
|
||||
if (t<r->options.stop) {
|
||||
counters(r,solver);
|
||||
if (CVodeReInit(solver,t,y)<0) goto cleanup;
|
||||
CV_CHECK(CVodeReInit(solver,t,y));
|
||||
r->starts++;
|
||||
}
|
||||
}
|
||||
success=1;
|
||||
goto cleanup;
|
||||
allocation_failure:
|
||||
native_fail(r,"allocation-failure",allocation,"Cannot allocate native solver resources.");
|
||||
cleanup:
|
||||
if (initialized) cv_snapshot(r,solver);
|
||||
if (solver) { counters(r,solver); CVodeFree(&solver); }
|
||||
if (context.reference) SUNMatDestroy(context.reference);
|
||||
if (context.weights) N_VDestroy(context.weights);
|
||||
@@ -277,3 +351,4 @@ cleanup:
|
||||
SUNContext_Free(&ctx);
|
||||
return success;
|
||||
}
|
||||
#undef CV_CHECK
|
||||
@@ -62,22 +62,27 @@ int native_json_write_number(FILE *file, double value) {
|
||||
return length>0 && write_bytes(file,text,(size_t)length);
|
||||
}
|
||||
|
||||
int native_json_write_array(FILE *file, const double *values, size_t count, size_t stride) {
|
||||
int native_json_write_values(FILE *file, const double *values, size_t count, size_t stride, int leading_comma) {
|
||||
if (!file || ferror(file) || (count && !values)) return 0;
|
||||
if (count>1 && (!stride || stride>(size_t)PTRDIFF_MAX/sizeof(double)/(count-1))) return 0;
|
||||
char buffer[65536];
|
||||
size_t used=1;
|
||||
buffer[0]='[';
|
||||
size_t used=0;
|
||||
for (size_t i=0;i<count;i++) {
|
||||
if (used>sizeof(buffer)-NATIVE_JSON_DOUBLE_CAPACITY-2) {
|
||||
if (!write_bytes(file,buffer,used)) return 0;
|
||||
used=0;
|
||||
}
|
||||
if (i) buffer[used++]=',';
|
||||
if (i || leading_comma) buffer[used++]=',';
|
||||
int length=native_json_format_double(buffer+used,values[i*stride]);
|
||||
if (!length) return 0;
|
||||
used+=(size_t)length;
|
||||
}
|
||||
buffer[used++]=']';
|
||||
return write_bytes(file,buffer,used);
|
||||
}
|
||||
|
||||
int native_json_write_array(FILE *file, const double *values, size_t count, size_t stride) {
|
||||
if (!file || ferror(file) || (count && !values)) return 0;
|
||||
if (count>1 && (!stride || stride>(size_t)PTRDIFF_MAX/sizeof(double)/(count-1))) return 0;
|
||||
if (fputc('[',file)==EOF) return 0;
|
||||
return native_json_write_values(file,values,count,stride,0) && fputc(']',file)!=EOF;
|
||||
}
|
||||
+30
-24
@@ -31,13 +31,13 @@ static int probe(void) {
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
static int write_result_index(const char *path, long series_start, long series_end,
|
||||
long result_bytes, size_t sample_count) {
|
||||
static int write_result_index(const char *path, int64_t series_start, int64_t series_end,
|
||||
int64_t result_bytes, size_t sample_count) {
|
||||
if(series_start<0 || series_end<series_start || result_bytes<series_end) return 0;
|
||||
FILE *index=fopen(path,"wb"); if(!index) return 0;
|
||||
int ok=fprintf(index,"{\"version\":1,\"seriesStart\":%ld,\"seriesEnd\":%ld,"
|
||||
"\"resultBytes\":%ld,\"sampleCount\":%zu}\n",
|
||||
series_start,series_end,result_bytes,sample_count)>=0;
|
||||
int ok=fprintf(index,"{\"version\":1,\"seriesStart\":%lld,\"seriesEnd\":%lld,"
|
||||
"\"resultBytes\":%lld,\"sampleCount\":%zu}\n",
|
||||
(long long)series_start,(long long)series_end,(long long)result_bytes,sample_count)>=0;
|
||||
if(ferror(index)) ok=0;
|
||||
if(fclose(index)) ok=0;
|
||||
return ok;
|
||||
@@ -46,19 +46,23 @@ static int write_result(NativeRun *r, const char *path, const char *index_path)
|
||||
if(index_path && !strcmp(path,index_path)) return 0;
|
||||
double dy[NSTATES], final[NOUTPUTS];
|
||||
int final_ok=model_eval(r->final_time,r->final_state,dy,final);
|
||||
size_t length=r->count*NOUTPUTS;
|
||||
if (length>268435456u/sizeof(double)) return 0;
|
||||
double *values=length?malloc(length*sizeof(double)):NULL;
|
||||
if (length && !values) return 0;
|
||||
for (size_t i=0;i<r->count;i++) {
|
||||
if (!model_eval(r->times[i],r->states+i*NSTATES,dy,values+i*NOUTPUTS)) {
|
||||
free(values); return 0;
|
||||
}
|
||||
}
|
||||
FILE *f=fopen(path,"wb"); if (!f) { free(values); return 0; }
|
||||
if (!native_samples_outputs(r)) return 0;
|
||||
FILE *f=fopen(path,"wb"); if (!f) return 0;
|
||||
fprintf(f,"{\"success\":%s,\"status\":",!r->status?"true":"false");
|
||||
json_string(f,r->status==0?"completed":r->status==1?"cancelled":"failed");
|
||||
fprintf(f,",\"message\":"); json_string(f,r->message);
|
||||
fprintf(f,",\"propertyWarnings\":"); native_property_warnings_json(r,f);
|
||||
fprintf(f,",\"solverControl\":{\"reason\":"); json_string(f,r->failure_reason?r->failure_reason:"");
|
||||
fprintf(f,",\"operation\":"); json_string(f,r->failure_operation?r->failure_operation:"");
|
||||
fprintf(f,",\"returnCode\":%d,\"sameTimeReturns\":%lu,\"maxSameTimeStreak\":%lu",
|
||||
r->cvode_flag,r->same_time_returns,r->max_same_time_streak);
|
||||
const char *names[]={"returnTime","internalTime","lastStep","nextStep"};
|
||||
double diagnostic[]={r->cvode_return_time,r->cvode_internal_time,r->cvode_last_step,r->cvode_next_step};
|
||||
for(int i=0;i<4;i++) {
|
||||
fprintf(f,",\"%s\":",names[i]);
|
||||
if(isfinite(diagnostic[i])) fprintf(f,"%.17g",diagnostic[i]); else fputs("null",f);
|
||||
}
|
||||
fputc('}',f);
|
||||
fprintf(f,",\"jacobianMode\":\"%s\",\"jacobianRhsCalls\":%lu,"
|
||||
"\"jacobianColoredEvals\":%lu,\"jacobianFallbacks\":%lu,"
|
||||
"\"jacobianChecks\":%lu,\"jacobianMismatches\":%lu,"
|
||||
@@ -75,22 +79,22 @@ static int write_result(NativeRun *r, const char *path, const char *index_path)
|
||||
r->events,r->starts,r->njev,r->nlu,r->max_accepted_step);
|
||||
/* Binary-mode positions delimit the complete series object, including
|
||||
both braces. The optional index avoids scanning or parsing its values. */
|
||||
long series_start=-1,series_end=-1,result_bytes=-1;
|
||||
int64_t series_start=-1,series_end=-1,result_bytes=-1;
|
||||
if(index_path) {
|
||||
series_start=ftell(f);
|
||||
series_start=native_file_tell(f);
|
||||
if(series_start>0) series_start--; else series_start=-1;
|
||||
}
|
||||
int output_ok=1;
|
||||
if (r->count) {
|
||||
fprintf(f,"\"time\":");
|
||||
output_ok=native_json_write_array(f,r->times,r->count,1);
|
||||
output_ok=native_samples_json(r,f,0);
|
||||
for (int j=0;j<NOUTPUTS && output_ok;j++) {
|
||||
fputc(',',f); json_string(f,model_output_keys[j]); fputc(':',f);
|
||||
output_ok=native_json_write_array(f,values+j,r->count,NOUTPUTS);
|
||||
output_ok=native_samples_json(r,f,(size_t)j+1);
|
||||
}
|
||||
}
|
||||
fputc('}',f);
|
||||
if(index_path) series_end=ftell(f);
|
||||
if(index_path) series_end=native_file_tell(f);
|
||||
fprintf(f,",\"final\":{");
|
||||
if (final_ok) for (int j=0;j<NOUTPUTS && output_ok;j++) {
|
||||
if (j) fputc(',',f);
|
||||
@@ -101,14 +105,14 @@ static int write_result(NativeRun *r, const char *path, const char *index_path)
|
||||
if (output_ok) output_ok=native_json_write_array(f,r->final_state,NSTATES,1);
|
||||
fprintf(f,"}\n"); int ok=output_ok && !ferror(f);
|
||||
if(index_path) {
|
||||
result_bytes=ftell(f);
|
||||
result_bytes=native_file_tell(f);
|
||||
if(series_start<0 || series_end<0 || result_bytes<0) ok=0;
|
||||
}
|
||||
if (fclose(f)) ok=0;
|
||||
/* Publish the index only after the complete result was successfully
|
||||
flushed and closed. An index I/O failure is a failed result write. */
|
||||
if(ok && index_path) ok=write_result_index(index_path,series_start,series_end,result_bytes,r->count);
|
||||
free(values); return ok;
|
||||
return ok;
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
@@ -122,11 +126,14 @@ int main(int argc, char **argv) {
|
||||
double y[NSTATES]; if (!model_init(y)) return 2;
|
||||
vector(stdout,y,NSTATES); fputc('\n',stdout); return 0;
|
||||
}
|
||||
if (!strcmp(arg,"--storage-control")) { r.storage_control=1; continue; }
|
||||
if (!strcmp(arg,"--solve-only")) { r.options.record_samples=0; continue; }
|
||||
if (!strcmp(arg,"--verify-jacobian")) { r.jacobian_verify=1; continue; }
|
||||
if (i+1==argc) return 64;
|
||||
const char *value=argv[++i];
|
||||
if (!strcmp(arg,"--output")) output=value;
|
||||
else if (!strcmp(arg,"--sample-file")) r.sample_path=value;
|
||||
else if (!strcmp(arg,"--output-block-file")) r.output_blocks_path=value;
|
||||
else if (!strcmp(arg,"--result-index")) index_path=value;
|
||||
else if (!strcmp(arg,"--cancel-file")) r.options.cancel_path=value;
|
||||
else if (!strcmp(arg,"--method")) {
|
||||
@@ -150,8 +157,7 @@ int main(int argc, char **argv) {
|
||||
r.options.start+r.options.max_step==r.options.start ||
|
||||
!isfinite(r.options.stop-r.options.start)) return 64;
|
||||
if (r.options.record_samples &&
|
||||
((r.options.stop-r.options.start)/r.options.sample_step>1000000 ||
|
||||
((r.options.stop-r.options.start)/r.options.sample_step+1024)*(NSTATES+NOUTPUTS)*sizeof(double)>268435456)) return 64;
|
||||
((r.options.stop-r.options.start)/r.options.sample_step>1000000)) return 64;
|
||||
native_solve(&r);
|
||||
int saved=write_result(&r,output,index_path);
|
||||
int code=saved?(r.status==2?2:0):3;
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/* Append-only, little-endian binary64 blocks. No output evaluation during solve.
|
||||
* Each block: SIMBLK01, u64 sequence/rows/columns/crc32, column-major payload,
|
||||
* COMMIT01. A truncated/uncommitted tail is never a valid block.
|
||||
* Synchronous buffered writes provide backpressure without a producer queue. */
|
||||
#include "runtime.h"
|
||||
#include "json_numbers.h"
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <float.h>
|
||||
#include <limits.h>
|
||||
#include <sys/types.h>
|
||||
#ifndef NATIVE_SAMPLE_BUFFER_BYTES
|
||||
#define NATIVE_SAMPLE_BUFFER_BYTES 1048576u
|
||||
#endif
|
||||
|
||||
_Static_assert(sizeof(double)==8 && DBL_MANT_DIG==53 && DBL_MAX_EXP==1024,
|
||||
"Result blocks require IEEE-754 binary64");
|
||||
|
||||
int64_t native_file_tell(FILE *f) {
|
||||
/* MinGW/MSVCRT can overcount a buffered final newline in _ftelli64 even
|
||||
for a binary FILE. Query the committed byte position instead. */
|
||||
if(fflush(f)) return -1;
|
||||
#ifdef _WIN32
|
||||
return _ftelli64(f);
|
||||
#else
|
||||
return (int64_t)ftello(f);
|
||||
#endif
|
||||
}
|
||||
static int seek_file(FILE *f, uint64_t offset) {
|
||||
if(offset>INT64_MAX) return 0;
|
||||
#ifdef _WIN32
|
||||
return _fseeki64(f,(int64_t)offset,SEEK_SET)==0;
|
||||
#else
|
||||
return fseeko(f,(off_t)offset,SEEK_SET)==0;
|
||||
#endif
|
||||
}
|
||||
static void put64(unsigned char *p,uint64_t n) {
|
||||
for(int i=0;i<8;i++) {p[i]=(unsigned char)n;n>>=8;}
|
||||
}
|
||||
static uint64_t get64(const unsigned char *p) {
|
||||
uint64_t n=0;for(int i=7;i>=0;i--) n=(n<<8)|p[i];return n;
|
||||
}
|
||||
static void little_endian(double *values,size_t count) {
|
||||
const uint16_t one=1;
|
||||
if(*(const unsigned char *)&one) return;
|
||||
unsigned char *p=(unsigned char *)values;
|
||||
for(size_t i=0;i<count;i++,p+=8) for(int j=0;j<4;j++) {
|
||||
unsigned char v=p[j];p[j]=p[7-j];p[7-j]=v;
|
||||
}
|
||||
}
|
||||
static uint32_t checksum(const void *data,size_t length) {
|
||||
uint32_t table[256];
|
||||
for(unsigned i=0;i<256;i++) {
|
||||
uint32_t c=i;
|
||||
for(int j=0;j<8;j++) c=(c>>1)^((c&1)?UINT32_C(0xedb88320):0);
|
||||
table[i]=c;
|
||||
}
|
||||
uint32_t c=UINT32_MAX;const unsigned char *p=data;
|
||||
for(size_t i=0;i<length;i++) c=table[(c^p[i])&255]^(c>>8);
|
||||
return c^UINT32_MAX;
|
||||
}
|
||||
static int storage_error(NativeRun *r,const char *reason,const char *message) {
|
||||
r->storage_failed=1;
|
||||
/* Storage failure must also be visible when flushing a cancelled run. */
|
||||
r->status=2;r->failure_reason=reason;r->failure_operation="sample-storage";
|
||||
snprintf(r->message_buffer,sizeof(r->message_buffer),"%s",message);
|
||||
r->message=r->message_buffer;
|
||||
r->count=r->committed;r->buffered=0;
|
||||
return 0;
|
||||
}
|
||||
static int write_block(FILE *f,uint64_t sequence,size_t rows,size_t columns,double *data) {
|
||||
size_t length=rows*columns*sizeof(double);
|
||||
unsigned char header[40];memcpy(header,"SIMBLK01",8);
|
||||
put64(header+8,sequence);put64(header+16,rows);put64(header+24,columns);
|
||||
little_endian(data,rows*columns);
|
||||
put64(header+32,checksum(data,length));
|
||||
int ok=fwrite(header,1,sizeof(header),f)==sizeof(header) &&
|
||||
fwrite(data,1,length,f)==length && fwrite("COMMIT01",1,8,f)==8 &&
|
||||
fflush(f)==0 && !ferror(f);
|
||||
little_endian(data,rows*columns);
|
||||
return ok;
|
||||
}
|
||||
static int read_block(FILE *f,uint64_t sequence,size_t rows,size_t columns,double *data) {
|
||||
unsigned char header[40],tail[8];size_t length=rows*columns*sizeof(double);
|
||||
if(fread(header,1,40,f)!=40 || memcmp(header,"SIMBLK01",8) ||
|
||||
get64(header+8)!=sequence || get64(header+16)!=rows || get64(header+24)!=columns ||
|
||||
fread(data,1,length,f)!=length || fread(tail,1,8,f)!=8 || memcmp(tail,"COMMIT01",8) ||
|
||||
get64(header+32)!=checksum(data,length)) return 0;
|
||||
little_endian(data,rows*columns);return 1;
|
||||
}
|
||||
int native_samples_flush(NativeRun *r) {
|
||||
if(r->storage_failed) return 0;
|
||||
if(!r->buffered) return 1;
|
||||
size_t rows=r->buffered;
|
||||
/* Reserve both this state block and its future output block before writing.
|
||||
The parent serializes quota grants across concurrent simulations. */
|
||||
uint64_t bytes=96+(uint64_t)rows*(NSTATES+NOUTPUTS+2)*8;
|
||||
if(r->storage_control) {
|
||||
char reply[16];
|
||||
fprintf(stderr,"{\"phase\":\"storage-reserve\",\"bytes\":%llu}\n",(unsigned long long)bytes);
|
||||
fflush(stderr);
|
||||
if(!fgets(reply,sizeof(reply),stdin))
|
||||
return storage_error(r,"storage-control","Result storage controller disconnected; complete blocks were retained.");
|
||||
if(!strcmp(reply,"full\n"))
|
||||
return storage_error(r,"storage-quota","Result storage quota exhausted; complete blocks were retained.");
|
||||
if(strcmp(reply,"ok\n"))
|
||||
return storage_error(r,"storage-io","Result storage I/O failed; complete blocks were retained.");
|
||||
}
|
||||
double *data=malloc(rows*(NSTATES+1)*sizeof(double));
|
||||
if(!data) return storage_error(r,"storage-memory","Cannot allocate result block.");
|
||||
memcpy(data,r->times,rows*sizeof(double));
|
||||
for(int j=0;j<NSTATES;j++) for(size_t i=0;i<rows;i++)
|
||||
data[(j+1)*rows+i]=r->states[i*NSTATES+j];
|
||||
int ok=write_block(r->sample_file,r->committed/r->capacity,rows,NSTATES+1,data);
|
||||
free(data);
|
||||
if(!ok) return storage_error(r,"storage-io","Cannot write result block; complete blocks were retained.");
|
||||
r->committed+=rows;r->buffered=0;return 1;
|
||||
}
|
||||
int native_samples_append(NativeRun *r,double t,const double *state) {
|
||||
if(r->storage_failed) return 0;
|
||||
if(!r->capacity) {
|
||||
size_t columns=NSTATES>NOUTPUTS?NSTATES+1:NOUTPUTS+1;
|
||||
r->capacity=NATIVE_SAMPLE_BUFFER_BYTES/(columns*sizeof(double));
|
||||
if(!r->capacity) r->capacity=1;
|
||||
if(r->capacity>1024) r->capacity=1024;
|
||||
r->times=malloc(r->capacity*sizeof(double));
|
||||
r->states=malloc(r->capacity*NSTATES*sizeof(double));
|
||||
r->sample_file=r->sample_path?fopen(r->sample_path,"w+b"):tmpfile();
|
||||
if(!r->times || !r->states || !r->sample_file)
|
||||
return storage_error(r,"storage-io","Cannot initialize result storage.");
|
||||
setvbuf(r->sample_file,NULL,_IOFBF,65536);
|
||||
}
|
||||
/* Hold the newest sample until a distinct time arrives, preserving the
|
||||
pre-existing same-time replacement policy even at block boundaries. */
|
||||
if(r->buffered && r->times[r->buffered-1]==t) {r->buffered--;r->count--;}
|
||||
if(r->buffered==r->capacity && !native_samples_flush(r)) return 0;
|
||||
r->times[r->buffered]=t;
|
||||
memcpy(r->states+r->buffered*NSTATES,state,NSTATES*sizeof(double));
|
||||
r->buffered++;r->count++;return 1;
|
||||
}
|
||||
int native_samples_outputs(NativeRun *r) {
|
||||
if(!r->committed) return 1;
|
||||
r->output_file=r->output_blocks_path?fopen(r->output_blocks_path,"w+b"):tmpfile();
|
||||
if(!r->output_file || !seek_file(r->sample_file,0)) return 0;
|
||||
setvbuf(r->output_file,NULL,_IOFBF,65536);
|
||||
double *states=malloc(r->capacity*(NSTATES+1)*sizeof(double));
|
||||
double *outputs=malloc(r->capacity*(NOUTPUTS+1)*sizeof(double));
|
||||
int ok=states && outputs;
|
||||
for(size_t start=0;ok && start<r->committed;start+=r->capacity) {
|
||||
size_t rows=r->committed-start;if(rows>r->capacity) rows=r->capacity;
|
||||
ok=read_block(r->sample_file,start/r->capacity,rows,NSTATES+1,states);
|
||||
for(size_t i=0;ok && i<rows;i++) {
|
||||
double y[NSTATES],dy[NSTATES],w[NOUTPUTS];
|
||||
for(int j=0;j<NSTATES;j++) y[j]=states[(j+1)*rows+i];
|
||||
ok=model_eval(states[i],y,dy,w);
|
||||
outputs[i]=states[i];
|
||||
if(ok) for(int j=0;j<NOUTPUTS;j++) outputs[(j+1)*rows+i]=w[j];
|
||||
}
|
||||
if(ok) ok=write_block(r->output_file,start/r->capacity,rows,NOUTPUTS+1,outputs);
|
||||
if(ok) r->output_count+=rows;
|
||||
}
|
||||
free(states);free(outputs);return ok;
|
||||
}
|
||||
int native_samples_json(NativeRun *r,FILE *f,size_t column) {
|
||||
if(column>NOUTPUTS || r->output_count!=r->count || fputc('[',f)==EOF) return 0;
|
||||
double *values=malloc((r->capacity?r->capacity:1)*sizeof(double));
|
||||
if(!values) return 0;
|
||||
int ok=1;
|
||||
for(size_t start=0;ok && start<r->count;start+=r->capacity) {
|
||||
size_t rows=r->count-start;if(rows>r->capacity) rows=r->capacity;
|
||||
uint64_t offset=(uint64_t)(start/r->capacity)*(48+(uint64_t)r->capacity*(NOUTPUTS+1)*8);
|
||||
ok=seek_file(r->output_file,offset+40+(uint64_t)column*rows*8) &&
|
||||
fread(values,sizeof(double),rows,r->output_file)==rows;
|
||||
if(ok) {little_endian(values,rows);ok=native_json_write_values(f,values,rows,1,start!=0);}
|
||||
}
|
||||
free(values);return ok && fputc(']',f)!=EOF;
|
||||
}
|
||||
Reference in new issue
Block a user