102 lines
6.8 KiB
Python
102 lines
6.8 KiB
Python
"""Independent read of AME XML fields; no shared expected-parameter helper."""
|
|
from pathlib import Path
|
|
import ast, collections, hashlib, json, math, operator, re, sys, tarfile
|
|
import xml.etree.ElementTree as ET
|
|
ROOT=Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0,str(ROOT))
|
|
from app.simulation.registry import get_component_model_spec
|
|
OUT=ROOT/'test/solver-newton-20260911/mql4/visual-recheck'
|
|
OUT.mkdir(parents=True,exist_ok=True)
|
|
with tarfile.open(ROOT/'tests/data/test_mql_4.ame') as archive:
|
|
cir=archive.extractfile('test_mql_4_.cir').read().decode('latin1')
|
|
# AME stores unescaped C-style && in visibility metadata, outside parameter values.
|
|
xml_text=re.sub(r'<VISIBILITY>.*?</VISIBILITY>', '<VISIBILITY/>', cir, flags=re.S)
|
|
xml_text=re.sub(r'&(?!amp;|lt;|gt;|quot;|apos;|#)', '&', xml_text)
|
|
root=ET.fromstring(xml_text)
|
|
BINARY={ast.Add:operator.add,ast.Sub:operator.sub,ast.Mult:operator.mul,ast.Div:operator.truediv,ast.Pow:operator.pow}
|
|
UNARY={ast.UAdd:operator.pos,ast.USub:operator.neg}
|
|
def evaluate(text,names):
|
|
def visit(node):
|
|
if isinstance(node,ast.Constant) and isinstance(node.value,(int,float)):return float(node.value)
|
|
if isinstance(node,ast.Name):return names[node.id]
|
|
if isinstance(node,ast.BinOp) and type(node.op) in BINARY:return BINARY[type(node.op)](visit(node.left),visit(node.right))
|
|
if isinstance(node,ast.UnaryOp) and type(node.op) in UNARY:return UNARY[type(node.op)](visit(node.operand))
|
|
raise ValueError(ast.dump(node))
|
|
return visit(ast.parse(text.strip().replace('^','**'),mode='eval').body)
|
|
variables={};pending={g.findtext('GLOB_PARAM_NAME').strip():g.findtext('VALUE').strip() for g in root.findall('.//GLOBALPARAM')}
|
|
while pending:
|
|
count=len(pending)
|
|
for key,value in list(pending.items()):
|
|
try:variables[key]=evaluate(value,variables)
|
|
except KeyError:continue
|
|
del pending[key]
|
|
assert len(pending)<count,pending
|
|
entities={}
|
|
for kind in ['COMP','LINE']:
|
|
for index,item in enumerate(root.findall('.//'+kind)):
|
|
model=item.findtext('./SUBMODEL/SUB_NAME')
|
|
if not model or model=='DIRECT':continue
|
|
fields={}
|
|
for tag in ['RPARAM','IPARAM','IVAR','EVAR']:
|
|
for field in item.findall('.//'+tag):
|
|
name=field.findtext('VARNAME');value=field.findtext('VALUE')
|
|
if name and value:
|
|
assert name not in fields,(item.findtext('ALIAS'),name)
|
|
fields[name.strip()]={'value':value.strip(),'unit':(field.findtext('UNITS') or '').strip()}
|
|
alias=item.findtext('ALIAS').strip()
|
|
assert alias not in entities
|
|
entities[alias]={'element':item,'fields':fields,'model':model,'source':kind+':'+str(index)}
|
|
# Explicit initial-state translations; all other public names are read verbatim.
|
|
state_names={
|
|
'MECMAS21':{'v0':'v1','x0':'x1'},'PNCH012':{'p0':'press','T0':'temp'},'PNCH023':{'p0':'press','T0':'temp'},
|
|
'PNL0001':{'p0':'p2','T0':'t2'},'PNL0002':{'p0':'pctr','T0':'tctr'},
|
|
'PNL0003':{'p1_0':'p1','T1_0':'t1','p2_0':'p2','T2_0':'t2'},
|
|
'STEP0':{'initial':'out0','final':'out1','time':'t0'},
|
|
}
|
|
unit_scale={('mm','m'):1e-3,('mm**2','m2'):1e-6,('L','m3'):1e-3,('N/mm','N/m'):1e3,('N/(mm/s)','N/(m/s)'):1e3}
|
|
unit_aliases={('null',''),('degree',''),('J/m**2/K/s','W/(m2*K)'),('N/m**2','Pa'),('m**2','m2'),('N/(m/s)**2','N/(m/s)^2')}
|
|
project=json.loads((ROOT/'tests/data/test-mql-4-corrected.json').read_text())
|
|
rows=[];issues=[];unit_counts=collections.Counter();extras=[]
|
|
for node in project['nodes']:
|
|
d=node['data'];entry=entities[d['label']];model=entry['model'];fields=entry['fields']
|
|
specs=get_component_model_spec(d['modelType']).parameter_by_name
|
|
for key,actual in d['parameters'].items():
|
|
source=state_names.get(model,{}).get(key,key)
|
|
rule='raw field'
|
|
if source in fields:
|
|
f=fields[source];raw=evaluate(f['value'],variables);source_unit=f['unit'];target_unit=specs[key].unit
|
|
if source in {'press','p1','p2','pctr'}:
|
|
assert (source_unit,target_unit)==('Pa','Pa')
|
|
expected=raw+101300.;rule='gauge Pa + 101300 Pa atmospheric pressure'
|
|
elif (source_unit,target_unit) in unit_scale:
|
|
expected=raw*unit_scale[source_unit,target_unit];rule='multiply by '+str(unit_scale[source_unit,target_unit])
|
|
elif source_unit==target_unit or (source_unit,target_unit) in unit_aliases:
|
|
expected=raw
|
|
else:raise AssertionError((source,source_unit,target_unit))
|
|
unit_counts[(source_unit,target_unit,rule)]+=1
|
|
row={'jsonId':node['id'],'ameAlias':d['label'],'sourceEntity':entry['source'],'parameter':key,'sourceField':source,
|
|
'rawExpression':f['value'],'rawValue':raw,'sourceUnit':source_unit,'targetUnit':target_unit,'conversion':rule,
|
|
'expected':expected,'actual':actual}
|
|
else:
|
|
if model=='PNCH012' and key in {'vol1','vol2','vol3','vol4','dvol1','dvol2','dvol3','dvol4'}:expected=0.;rule='public extra port-volume inputs default zero'
|
|
elif model=='PNVO001' and key=='opening0':expected=1.;rule='public fallback opening parameter; model signal port supplies actual command'
|
|
elif model=='FORC' and key=='direction':
|
|
geometry=entry['element'].findtext('COMP_GEOMETRY');assert geometry in {'2','8'}
|
|
expected=1. if geometry=='2' else -1.;rule='force direction from AME geometry '+geometry
|
|
elif model=='PNGD00' and key=='property_model':
|
|
codes={name:evaluate(fields[name]['value'],variables) for name in ['fluidType','eosType','gasSetting']}
|
|
assert codes=={'fluidType':12.,'eosType':6.,'gasSetting':1.},codes
|
|
expected=0.;rule='public helium model selection mapped from explicit AME fluid/eos/gas codes'
|
|
else:raise AssertionError((model,key))
|
|
row={'jsonId':node['id'],'ameAlias':d['label'],'sourceEntity':entry['source'],'parameter':key,'conversion':rule,'expected':expected,'actual':actual}
|
|
extras.append(row)
|
|
row['matches']=math.isclose(actual,expected,rel_tol=1e-12,abs_tol=1e-15)
|
|
if not row['matches']:issues.append(row)
|
|
rows.append(row)
|
|
result={'method':'Independent xml.etree XML parsing and arithmetic AST evaluation; does not import or call _expected_parameters or its source-field helpers.',
|
|
'nodeCount':len(project['nodes']),'parameterCount':len(rows),'directSourceFieldCount':len(rows)-len(extras),'publicExtraOrGeometryMappedCount':len(extras),
|
|
'unitConversionCounts':[{'source':a,'target':b,'rule':c,'count':n} for (a,b,c),n in unit_counts.items()],
|
|
'extraParameters':extras,'differences':issues,'parameters':rows}
|
|
(OUT/'independent-parameters.json').write_text(json.dumps(result,indent=2,ensure_ascii=False)+'\n')
|
|
print(json.dumps({k:v for k,v in result.items() if k not in ['parameters','extraParameters']},indent=2))
|