修复循环信号与事件采样并接入 LSTP 接触定位,补充八路验证及复用实验

相较上一版 Jacobian 确定性复用更新,本次补齐事件边界一致性、结果两侧采样及接触事件定位;保留已有物性复用和组件力学公式。

- 统一 UD00 信号求值与下一事件查询的绝对时间边界,修复循环边界浮点舍入导致的阶段错位、重复或漏报,并覆盖零时长、多阶段及长周期场景。
- 引入原生输出语义 v2:保留规则网格真实时间,补充内部时间事件和状态事件的左邻及事件后采样,按保存时间、状态和离散模式重放结果。
- 两条代码生成路径均发出 LSTP 接触描述,默认定位间隙过零及非负力模式的力截断;仅在接受事件时更新防重复记录,增加 contactEvents 诊断计数。
- 补充 MASS/LSTP 独立事件实验、八路全曲线与驱动阶段配对评估,以及 Amesim 不连续点输出对照和力差定位报告;MASS 新增释放机制仍保留为独立实验。
- 保存局部 probe、context 访问与回退、shadow replay、R288 real skip/typed replay 及阀门数值尾部诊断工具和报告;未证明净收益的实验不启用为生产默认优化。
- 更新原生运行说明和元件建模规范,补充信号边界、输出语义、接触事件和实验依赖回归测试。

验证:五组专项回归共 34 项全部通过;37 个待提交 Python 文件语法检查通过;git diff --cached --check 通过。
This commit is contained in:
ljz committed 2026-09-17 23:50:13 +08:00
1 parent 1aac220084
commit 7611f13208
85 files changed
+11358 -33

No files matched your search

+32 -5
View File
@@ -86,6 +86,16 @@ int native_append(NativeRun *r, double t, const double *y) {
return native_samples_append(r,t,y);
}
/* The integrators already stop at the left-adjacent double of an internal
* time event. Preserve that actual endpoint and the continuous state at the
* event, so output replay can evaluate BOTH forcing phases without snapping
* a regular-grid timestamp or changing integration history. */
int native_time_boundary_samples(NativeRun *r,double boundary,const double *state) {
if (!r->options.record_samples || boundary>=r->options.stop) return 1;
return native_samples_append(r,nextafter(boundary,-INFINITY),state) &&
native_samples_append(r,boundary,state);
}
/* 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. */
@@ -166,12 +176,14 @@ static double locate_breakaway(NativeRun *r,int index,double left,double right,
}
#endif
#include "contact_events.h"
int native_accept(NativeRun *r, double t, double next, const double *old,
const double *trial, NativeDense dense, void *context,
double *accepted_time, double *accepted_state) {
double when[2*(NSTOPS+NFRICTIONS+1)], bounds[2*(NSTOPS+NFRICTIONS+1)];
double restitution[2*(NSTOPS+NFRICTIONS+1)], thresholds[2*(NSTOPS+NFRICTIONS+1)];
int indices[2*(NSTOPS+NFRICTIONS+1)], friction[2*(NSTOPS+NFRICTIONS+1)], count=0;
double when[2*(NSTOPS+NFRICTIONS+NCONTACTS+1)], bounds[2*(NSTOPS+NFRICTIONS+NCONTACTS+1)]={0};
double restitution[2*(NSTOPS+NFRICTIONS+NCONTACTS+1)]={0}, thresholds[2*(NSTOPS+NFRICTIONS+NCONTACTS+1)]={0};
int indices[2*(NSTOPS+NFRICTIONS+NCONTACTS+1)], friction[2*(NSTOPS+NFRICTIONS+NCONTACTS+1)], count=0;
for (int j=0;j<NSTOPS;j++) {
NativeStop s=model_stops[j]; int v=s.velocity_index, x=v+1;
double vt0=1e-12*fmax(fabs(old[v]),1), vt1=1e-12*fmax(fabs(trial[v]),1);
@@ -219,6 +231,9 @@ int native_accept(NativeRun *r, double t, double next, const double *old,
bounds[count]=restitution[count]=thresholds[count]=0;count++;
}
}
#endif
#if NCONTACTS
if(!contact_candidates(r,t,next,old,trial,dense,context,when,indices,friction,&count)) return -1;
#endif
double stop=next;
for (int i=0;i<count;i++) stop=fmin(stop,when[i]);
@@ -232,8 +247,17 @@ int native_accept(NativeRun *r, double t, double next, const double *old,
}
if (count) {
if (!dense(context,stop,accepted_state)) return -1;
/* Keep the last representable pre-event sample before applying a
* reset. Discrete modes are part of the saved state, never replayed
* from a mutable global mode. An event at the current step start has
* no new left interval; do not invent or back-date a sample. */
double before=nextafter(stop,-INFINITY);
if (r->options.record_samples && before>=t && before<stop) {
double sy[NSTATES];
if (!dense(context,before,sy) || !native_samples_append(r,before,sy)) return -1;
}
for (int i=0;i<count;i++) if (fabs(when[i]-stop)<=1e-12*fmax(fabs(stop),1)) {
if(friction[i]>=0) continue;
if(friction[i]!=-1) continue;
double incoming=accepted_state[indices[i]];
accepted_state[indices[i]]=fabs(incoming)<=thresholds[i]?0:-restitution[i]*incoming;
accepted_state[indices[i]+1]=bounds[i];
@@ -242,7 +266,7 @@ int native_accept(NativeRun *r, double t, double next, const double *old,
/* Stop/reverse only at an accepted event. The discrete mode travels
* with saved states so replayed results and Jacobian trials are pure. */
double drives[NFRICTIONS];
for(int i=0;i<count;i++) if(friction[i]<0 && fabs(when[i]-stop)<=1e-12*fmax(fabs(stop),1)) {
for(int i=0;i<count;i++) if(friction[i]==-1 && fabs(when[i]-stop)<=1e-12*fmax(fabs(stop),1)) {
for(int j=0;j<NFRICTIONS;j++) if(model_frictions[j].velocity_index==indices[i]) {
double velocity=accepted_state[indices[i]];
accepted_state[model_frictions[j].mode_index]=velocity>0?1:(velocity<0?-1:0);
@@ -254,6 +278,9 @@ int native_accept(NativeRun *r, double t, double next, const double *old,
accepted_state[f.velocity_index]=0;
accepted_state[f.mode_index]=fabs(drives[j])>f.breakaway_force ? (drives[j]>0?1:-1) : 0;
}
#endif
#if NCONTACTS
contact_commit(r,stop,accepted_state,when,indices,friction,count);
#endif
if (!native_append(r,stop,accepted_state)) return -1;
while (r->options.start+r->sample_index*r->options.sample_step<=stop) r->sample_index++;
+105
View File
@@ -0,0 +1,105 @@
/* Private implementation included by common.c. All probes use state-only
* descriptors: no model RHS, property evaluation, mode mutation or reset. */
#if NCONTACTS
#include <float.h>
static double contact_penetration(int j,const double *y) {
NativeContact c=model_contacts[j];
double x1=y[c.velocity1+1],x2=y[c.velocity2+1];
/* Match each lowering's existing force formula, including association. */
return c.subtract_first ? -(c.gap0+(x2-x1)) : -(c.gap0+x2-x1);
}
static double contact_velocity(int j,const double *y) {
NativeContact c=model_contacts[j];
return y[c.velocity1]-y[c.velocity2];
}
static double contact_value(int j,int kind,const double *y) {
if(kind==2) return contact_velocity(j,y);
double p=contact_penetration(j,y);
if(!kind) return p;
NativeContact c=model_contacts[j];
double fraction=c.depth>0 ? -expm1(-fmax(p,0)/c.depth) : 1;
return c.stiffness*p+fraction*c.damping*contact_velocity(j,y);
}
static double contact_locate(NativeRun *r,int j,int kind,double left,double right,
double sign,NativeDense dense,void *context) {
double y[NSTATES];
for(int k=0;k<60;k++) {
double mid=left+.5*(right-left);
if(mid<=left || mid>=right) break;
r->contact_dense++;r->contact_roots++;
if(!dense(context,mid,y)) return NAN;
double g=contact_value(j,kind,y);
if(!isfinite(g)) return NAN;
if(sign<0 ? g>=0 : g<=0) right=mid; else left=mid;
}
return right;
}
static double contact_bracket(NativeRun *r,int j,int force,double left,double right,
double a,double b,NativeDense dense,void *context,int *direction) {
int slot=2*j+force;
if(r->contact_direction[slot] && r->contact_last[slot]==left)
a=r->contact_direction[slot]*DBL_MIN;
if(a==0 || (a>0 ? b>0 : b<0)) return INFINITY;
*direction=a<0 ? 1 : -1;
return contact_locate(r,j,force,left,right,a,dense,context);
}
static double contact_candidate(NativeRun *r,int j,int force,double t,double next,
const double *old,const double *trial,
NativeDense dense,void *context,int *direction) {
double a=contact_value(j,force,old),b=contact_value(j,force,trial);
if(!isfinite(a) || !isfinite(b)) return NAN;
/* Resolve a same-sign endpoint excursion by splitting at velocity reversal.
* This assumes resolved steps, not arbitrarily many oscillations per step. */
if(!force) {
double va=contact_velocity(j,old),vb=contact_velocity(j,trial);
if((va<0 && vb>0) || (va>0 && vb<0)) {
double turn=contact_locate(r,j,2,t,next,va,dense,context),y[NSTATES];
if(!isfinite(turn)) return NAN;
r->contact_dense++;
if(!dense(context,turn,y)) return NAN;
double g=contact_value(j,force,y);
if(g==0 && ((a<0 && b<0) || (a>0 && b>0))) return INFINITY;
double found=contact_bracket(r,j,force,t,turn,a,g,dense,context,direction);
if(isfinite(found) || isnan(found)) return found;
return contact_bracket(r,j,force,turn,next,g,b,dense,context,direction);
}
}
return contact_bracket(r,j,force,t,next,a,b,dense,context,direction);
}
static int contact_candidates(NativeRun *r,double t,double next,const double *old,const double *trial,
NativeDense dense,void *context,double *when,int *indices,int *kinds,int *count) {
for(int j=0;j<NCONTACTS;j++) {
r->contact_checks++;
for(int force=0;force<2;force++) {
if(force && (model_contacts[j].signed_force==1 || contact_penetration(j,old)<=0)) continue;
int direction=0;
double at=contact_candidate(r,j,force,t,next,old,trial,dense,context,&direction);
if(isnan(at)) return 0;
if(isfinite(at)) {
if(force) {
double y[NSTATES];r->contact_dense++;
if(!dense(context,at,y)) return 0;
if(contact_penetration(j,y)<=0) continue;
}
int n=(*count)++;
when[n]=at;indices[n]=j;kinds[n]=force?-3:-2;
r->contact_pending[2*j+force]=direction;
}
}
}
return 1;
}
static void contact_commit(NativeRun *r,double t,const double *y,
const double *when,const int *indices,const int *kinds,int count) {
for(int i=0;i<count;i++) {
if(kinds[i]>-2 || when[i]!=t) continue;
int j=indices[i],force=kinds[i]==-3,slot=2*j+force;
r->contact_last[slot]=t;r->contact_direction[slot]=r->contact_pending[slot];
if(force) r->contact_clipping++; else r->contact_transitions++;
fprintf(stderr,"{\"phase\":\"mechanical-event\",\"kind\":\"%s\",\"index\":%d,\"time\":%.17g,\"direction\":%d,\"penetration\":%.17g,\"relativeVelocity\":%.17g}\n",
force?"force-clip":"lstp-contact",j,t,r->contact_direction[slot],
contact_penetration(j,y),contact_velocity(j,y));
}
}
#endif
+4
View File
@@ -366,6 +366,10 @@ int native_bdf(NativeRun *r) {
r->starts++;
}
}
if (!native_time_boundary_samples(r,boundary,N_VGetArrayPointer(y))) {
native_fail(r,"sample-storage-failure","native_time_boundary_samples","Cannot store time-event samples.");
goto cleanup;
}
t=boundary; r->final_time=t;
memcpy(r->final_state,N_VGetArrayPointer(y),NSTATES*sizeof(double));
double sample=r->options.start+r->sample_index*r->options.sample_step;
+8
View File
@@ -51,7 +51,15 @@ static int write_result(NativeRun *r, const char *path, const char *index_path)
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,",\"contactEvents\":{\"checks\":%lu,\"denseCalls\":%lu,\"rootIterations\":%lu,\"transitions\":%lu,\"forceClipTransitions\":%lu}",
r->contact_checks,r->contact_dense,r->contact_roots,r->contact_transitions,r->contact_clipping);
fprintf(f,",\"propertyWarnings\":"); native_property_warnings_json(r,f);
fputs(",\"outputSemantics\":{\"version\":2,"
"\"regularSamples\":\"actual-time-no-snap\","
"\"internalTimeEvents\":\"left-adjacent-and-at-event\","
"\"stateEvents\":\"left-adjacent-when-available-and-post-reset\","
"\"sameTime\":\"last-accepted-state\","
"\"replay\":\"saved-time-and-state-including-discrete-modes\"}",f);
fprintf(f,",\"jacobianReuse\":{\"gasEvaluations\":%lu,\"gasReuses\":%lu",
r->jacobian_gas_evaluations,r->jacobian_gas_reuses);
const char *reuse_names[]={"ph","density","pipe"};
+1
View File
@@ -108,6 +108,7 @@ int native_rk45(NativeRun *r) {
if (impact) restart=1;
else memcpy(f,K[6],sizeof(f));
}
if (!native_time_boundary_samples(r,boundary,y)) return 0;
t=boundary; r->final_time=t; memcpy(r->final_state,y,sizeof(y));
double sample=r->options.start+r->sample_index*r->options.sample_step;
if (r->options.record_samples && sample<=t && sample<=r->options.stop) {