Files
SystemSimulationApp/tests/manual/analyze_context_access.py
ljz 7611f13208 修复循环信号与事件采样并接入 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 通过。
2026-09-17 23:50:13 +08:00

257 lines
16 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Verify each actual read and ordered write, then summarize the two cases."""
from collections import Counter
from pathlib import Path
import ctypes as c
import hashlib
import json
import struct
ROOT = Path(__file__).resolve().parents[2]
OUT = ROOT/'test/context-access-20260917'
class Medium(c.Structure):
_fields_ = [('real_helium', c.c_int)] + [(k, c.c_double) for k in ['R','cp','Tref','slope','mu','muT','S']]
class State(c.Structure):
_fields_ = [('medium', Medium)] + [(k, c.c_double) for k in ['p','T','h','rho','mu','isentropic_factor','isentropic_exponent']] + [('valid', c.c_uint),('temperatures',c.c_void_p),('jacobian',c.c_void_p)]
class Pipe(c.Structure):
_fields_ = [('medium',Medium)] + [(k,c.c_double) for k in ['p1','p2','T','diameter','length','roughness','flow']] + [('kind',c.c_int),('valid',c.c_int)]
class Context(c.Structure):
_fields_ = [('states',c.c_void_p),('count',c.c_size_t),('capacity',c.c_size_t),('temperatures',c.c_void_p),('jacobian',c.c_void_p)]
def doubles(value):
data = bytes.fromhex(value)
return list(struct.unpack('<'+'d'*(len(data)//8),data))
def value(data, typ):
if typ == c.c_double:
return struct.unpack('<d', data)[0]
return int.from_bytes(data, 'little')
def named(domain, offset, size):
typ = {'states':State, 'pipes':Pipe, 'context':Context}[domain]
slot, off = divmod(offset,c.sizeof(typ))
if size == c.sizeof(typ):
return slot, '*', None
for name, ft in typ._fields_:
begin = getattr(typ,name).offset
if begin == off and c.sizeof(ft) == size:
return slot,name,ft
if ft == Medium and begin <= off < begin+c.sizeof(ft):
for mn,mt in Medium._fields_:
if getattr(Medium,mn).offset+begin == off:
return slot,'medium.'+mn,mt
raise AssertionError((domain,offset,size))
def first_match(memory, query):
ctx = Context.from_buffer_copy(memory['context'])
key = doubles(query['key']); p, second = key[:2]
matches = []
for i in range(ctx.count):
st = State.from_buffer_copy(memory['states'],i*c.sizeof(State))
flag, actual = (1, st.T) if query['kind']=='PT' else (2, st.h)
if st.valid & flag and st.p == p and actual == second and st.medium.real_helium == query['mediumKind'] and all(getattr(st.medium,k)==v for (k,_),v in zip(Medium._fields_[1:],key[2:])):
matches.append(i)
return matches
def analyze_operation(events):
begin=events[0]; entry=events[1]
assert begin['stateSize']==c.sizeof(State) and begin['pipeSize']==c.sizeof(Pipe)
memory={k:bytearray.fromhex(entry[k]) for k in ['context','states','pipes']}
known={k:bytearray(b'\1'*len(v)) for k,v in memory.items()}
ctx=Context.from_buffer_copy(memory['context'])
memory['states'].extend(bytes((ctx.capacity-ctx.count)*c.sizeof(State)))
known['states'].extend(bytes((ctx.capacity-ctx.count)*c.sizeof(State)))
summary={k:begin[k] for k in ['jac','group','position','t','inputs']}
summary.update(entryCount=ctx.count,capacity=ctx.capacity,queries=[],allocations=[],writes=[],consumed=[],validTests=[],sameValueWrites=0,accessReads=0,accessWrites=0,scalar=[])
assert not ctx.temperatures, 'Non-NULL observer is outside this verified contract.'
summary['bindings']={'jacobian':ctx.jacobian or 0,'temperatures':ctx.temperatures or 0}
st74=State.from_buffer_copy(memory['states'],74*c.sizeof(State)) if ctx.count>74 else None
if st74: summary['state74']={'p':st74.p,'T':st74.T,'valid':st74.valid}
pending=None
for event in events[2:]:
kind=event['event']
if kind=='query':
assert pending is None
pending=dict(event, allMatches=first_match(memory,event), tested=[])
elif kind=='match':
assert pending and pending['kind']==event['kind']
matches=pending['allMatches']; expected=matches[0] if matches else -1
assert event['slot']==expected and event['hit']==bool(matches)
expected_scans=list(range(expected+1 if expected>=0 else pending['count']))
assert pending['tested']==expected_scans,(begin,pending)
summary['queries'].append(dict(kind=event['kind'],key=pending['key'],keyValues=doubles(pending['key']),mediumKind=pending['mediumKind'],hit=event['hit'],slot=event['slot'],allMatches=matches,scanCount=len(expected_scans)))
pending=None
elif kind in ['read','write']:
domain=event['domain']; offset=event['offset']; data=bytes.fromhex(event['value'])
previous=memory[domain][offset:offset+len(data)]
slot,field,typ=named(domain,offset,len(data))
if kind=='read':
assert previous==data,(begin,event,'read mismatch')
summary['accessReads']+=1
# Lookup reads are recorded in raw events; this list isolates
# fields consumed after selection, including observers/keys.
if domain!='context' and pending is None and event['function']!='same_medium':
summary['consumed'].append(dict(domain=domain,slot=slot,field=field,value=event['value'],function=event['function']))
else:
summary['accessWrites']+=1
equal=all(known[domain][offset:offset+len(data)]) and previous==data
summary['sameValueWrites']+=equal
memory[domain][offset:offset+len(data)]=data
known[domain][offset:offset+len(data)]=b'\1'*len(data)
summary['writes'].append(dict(domain=domain,slot=slot,field=field,value=event['value'],sameValue=equal,function=event['function']))
elif kind=='valid_test':
st=State.from_buffer_copy(memory['states'],event['slot']*c.sizeof(State))
assert st.valid & event['mask']==event['value']
if pending is not None:
pending['tested'].append(event['slot'])
else:
summary['validTests'].append(event)
elif kind=='allocate':
assert event['branch']=='append', 'Scratch requires additional object registration; fail closed.'
assert event['slot']==event['countAfter']-1
assert event['countAfter']==Context.from_buffer_copy(memory['context']).count
assert event['countAfter']<=event['capacity']
summary['allocations'].append(event)
elif kind=='snapshot':
assert event['phase']=='exit'
for domain in memory:
expected=bytes.fromhex(event[domain])
assert memory[domain][:len(expected)]==expected,(begin,domain,'write replay mismatch')
summary['exitCount']=event['count']
# Everything not written remains the live probe entry, checked
# across every byte of every existing slot and every pipe slot.
summary['replayExact']=True
elif kind=='end':
summary['outputs']=event['outputs']
summary['outputValues']=doubles(event['outputs'])
elif kind.startswith('scalar_'):
summary['scalar'].append(event)
else:
raise AssertionError(event)
assert pending is None and summary['replayExact']
# Snapshot replay alone cannot detect an omitted equal-valued write.
# Check the reviewed source's mandatory store sequence independently.
pipe_writes=[w for w in summary['writes'] if w['domain']=='pipes']
assert [w['field'] for w in pipe_writes]==['valid','medium','p1','p2','T','diameter','length','roughness','kind','flow','valid']
assert int.from_bytes(bytes.fromhex(pipe_writes[0]['value']),'little')==0
assert int.from_bytes(bytes.fromhex(pipe_writes[-1]['value']),'little')==1
assert pipe_writes[-2]['value']==summary['outputs']
assert all(w['slot']==(28 if begin['position']==52 else 0) for w in pipe_writes)
for allocation in summary['allocations']:
writes=[w for w in summary['writes'] if w['domain']=='states' and w['slot']==allocation['slot']]
assert [w['field'] for w in writes[:7]]==['*','medium','p','T','valid','temperatures','jacobian']
assert not [w for w in summary['writes'] if w['domain']=='states' and w['slot']<summary['entryCount']]
return summary
def compare_logical(b,p):
"""Pair hits by query order and appends by creation order, never slot ID."""
mapping={}
assert b['bindings']==p['bindings'], 'Observer/memo ownership changed within this Jacobian.'
for bq,pq in zip(b['queries'],p['queries']):
assert (bq['kind'],bq['key'],bq['mediumKind'],bq['hit'])==(pq['kind'],pq['key'],pq['mediumKind'],pq['hit'])
if bq['hit']:
assert mapping.setdefault(bq['slot'],pq['slot'])==pq['slot']
assert len(b['allocations'])==len(p['allocations'])
for ba,pa in zip(b['allocations'],p['allocations']):
assert mapping.setdefault(ba['slot'],pa['slot'])==pa['slot']
def normalized(items, translate, writes=False):
result=[]
for event in items:
domain,slot,field,v=event['domain'],event['slot'],event['field'],event['value']
if domain=='states' and translate:slot=mapping[slot]
if domain=='context' and field=='count':v='increment'
if field in ['jacobian','temperatures']:
assert int.from_bytes(bytes.fromhex(v),'little')==b['bindings'][field]
v='current_context.'+field
result.append((domain,slot,field,v))
return result if writes else set(result)
assert normalized(b['consumed'],True)==normalized(p['consumed'],False),(b['jac'],b['position'],'consumed')
assert normalized(b['writes'],True,True)==normalized(p['writes'],False,True),(b['jac'],b['position'],'ordered writes')
assert [(mapping[v['slot']],v['mask'],v['value']) for v in b['validTests']]==[(v['slot'],v['mask'],v['value']) for v in p['validTests']]
return mapping
def write_checklists(examples, summaries):
lines=['# Context 访问级清单:实测数据', '', '由 `analyze_context_access.py` 从访问事件生成。slot、Jacobian 和 position 均从 0 开始;group=-1 为 baseline。', '',
'以下以 Jacobian 200 为主案例,补充 position 52 的 PH 命中/未命中路径。完整逐次记录位于 `test/context-access-20260917/operations.json`;原始访问事件在 `audit/access.jsonl`。', '']
chosen=[s for s in summaries if (s['jac']==200 and s['group'] in [6,18]) or (s['jac'] in [1,2] and s['group']==18)]
for s in chosen:
lines += [f"## Jacobian {s['jac']},group {s['group']},position {s['position']}", '',
f"t={s['t']:.17g};count {s['entryCount']} → {s['exitCount']};capacity={s['capacity']};输出 `{s['outputValues'][0]:.17g}`;model evaluator 返回 `{s['evalReturn']}`。", '',
'### 查询与首次匹配', '', '| 顺序 | 类型 | 完整 key:p, T 或 h, R, cp, Tref, slope, mu, muT, S | medium kind | 首个匹配 slot | 扫描条数 |', '|---|---|---|---|---|---|']
for i,q in enumerate(s['queries']):
lines.append(f"| {i+1} | {q['kind']} | `"+', '.join(format(v,'.17g') for v in q['keyValues'])+f"` | {q['mediumKind']} | {q['slot'] if q['hit'] else 'miss'} | {q['scanCount']} |")
lines += ['', '### 实际选中后的字段读取', '', '| 域 / slot | 字段 |', '|---|---|']
fields={}
for r in s['consumed']:fields.setdefault((r['domain'],r['slot']),set()).add(r['field'])
for (domain,slot),names in sorted(fields.items()):lines.append(f"| {domain}[{slot}] | "+', '.join(f'`{n}`' for n in sorted(names))+' |')
lines += ['', '查询扫描另外按短路次序读取 `valid & PT/H`、`p`、`T/h`、medium;未匹配项的字段不等于被用于物性计算。', '', '### 选中后的 valid 测试', '', '| 顺序 | slot | mask | 结果 |', '|---|---|---|---|']
for i,v in enumerate(s['validTests']):lines.append(f"| {i+1} | {v['slot']} | {v['mask']} | {v['value']} |")
lines += ['', '### 必须保留的 probe 数据', '', f"保留入口 `states[0:{s['entryCount']}]` 的全部字段;此路径没有写已有物性条目。仅追加以下新条目,修改本 operation 的 pipe 槽;其余 pipe 槽保持 probe 值。", '', '### 有序写入动作', '', '| 顺序 | 目标 | 写入值 | 已知同值写入 |', '|---|---|---|---|']
for i,w in enumerate(s['writes']):
field=w['field'];data=bytes.fromhex(w['value'])
if field=='*':text='完整 struct 清零(显式写入)'
elif field=='medium':text='复制上述完整 medium'
elif field in ['temperatures','jacobian']:text='NULL' if not int.from_bytes(data,'little') else '当前 context 的 Jacobian memo 指针'
elif field in ['count','valid','kind']:text=str(int.from_bytes(data,'little'))
else:text=format(struct.unpack('<d',data)[0],'.17g')
lines.append(f"| {i+1} | `{w['domain']}[{w['slot']}].{field}` | {text} | {'是' if w['sameValue'] else '—'} |")
lines += ['', '新条目分配分支均为 positive/finite key 且 `count < capacity`;每次在当前 count 追加,再 count++。未初始化槽的 struct 首次清零不计入“已知同值写入”。', '',
'### Fallback 原因', '', '查询 key/首匹配逻辑条目不同、已消费字段或已测试 valid 位不同、容量不足转 scratch、observer 非空、pipe 命中分支改变,或无法建立无冲突的 slot 映射时,执行原 operation。此清单不授权放宽现有 guard。', '']
(ROOT/'tests/manual/context_access_checklists.md').write_text('\n'.join(lines),encoding='utf-8')
def main():
summaries=[]; events=[]; return_pending=[]; returns=Counter(); examples=[]
with (OUT/'audit/access.jsonl').open(encoding='utf-8') as stream:
for line in stream:
event=json.loads(line)
if event['event']=='eval_return':
assert return_pending
for item in return_pending: item['evalReturn']=event['result']
return_pending=[]; returns[event['result']]+=1
elif event['event']=='begin':
assert not events
events=[event]
else:
assert events
events.append(event)
if event['event']=='end':
item=analyze_operation(events);summaries.append(item);return_pending.append(item)
if item['jac']==200: examples.append(dict(summary=item,events=events))
events=[]
assert not events and not return_pending
by_key={(x['jac'],x['group'],x['position']):x for x in summaries}
cases={}
for group,pos in [(18,52),(6,16)]:
pairs=[]
for jac in range(896):
b=by_key[jac,-1,pos];p=by_key[jac,group,pos]
assert b['inputs']==p['inputs'] and b['outputs']==p['outputs'] and b['evalReturn']==p['evalReturn']==1
equal_query=[(q['kind'],q['key'],q['mediumKind'],q['hit']) for q in b['queries']]==[(q['kind'],q['key'],q['mediumKind'],q['hit']) for q in p['queries']]
mapping=compare_logical(b,p)
pairs.append(dict(jac=jac,queryKeysAndHitsEqual=equal_query,logicalReadsAndOrderedWritesEqual=True,slotMapping=mapping,entryCounts=[b['entryCount'],p['entryCount']],exitCounts=[b['exitCount'],p['exitCount']],slots=[[q['slot'] for q in b['queries']],[q['slot'] for q in p['queries']]],allocations=[[a['slot'] for a in b['allocations']],[a['slot'] for a in p['allocations']]],state74=[b.get('state74'),p.get('state74')]))
cases[f'group{group}_position{pos}']=dict(pairs=len(pairs),equalQueryKeysAndHits=sum(p['queryKeysAndHitsEqual'] for p in pairs),countShiftPairs=sum(p['entryCounts'][0]!=p['entryCounts'][1] for p in pairs),rows=pairs)
result=dict(operations=len(summaries),replayExact=len(summaries),reads=sum(s['accessReads'] for s in summaries),writes=sum(s['accessWrites'] for s in summaries),sameValueWrites=sum(s['sameValueWrites'] for s in summaries),evalReturns=dict(returns),cases=cases)
for name,obj in [('summary.json',result),('operations.json',summaries),('jacobian-200.json',examples)]:
(OUT/name).write_text(json.dumps(obj,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
write_checklists(examples,summaries)
print(json.dumps({k:v for k,v in result.items() if k!='cases'},indent=2))
for name,case in cases.items(): print(name,{k:v for k,v in case.items() if k!='rows'})
if __name__=='__main__':main()