C内核流量计算方法优化,前端文件名称读取优化

This commit is contained in:
lujingze committed 2026-09-11 08:48:56 +00:00
1 parent 5d5a2e1843
commit 808c484f5b
94 files changed
+20163 -4418

No files matched your search

+403
View File
@@ -0,0 +1,403 @@
"""Audit the AME diagram against the corrected XML; emit an aligned browser project.
Run from the repository root with .venv/bin/python. Does not run Amesim or use
the stale eight-branch compiled/result cache inside the four-branch archive.
"""
from __future__ import annotations
import argparse
from collections import Counter, defaultdict
from copy import deepcopy
import hashlib
import json
from pathlib import Path
import re
import sys
if '--check' in sys.argv:
sys.dont_write_bytecode = True
import struct
import 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
from tests.test_test_mql_ame_contract import (
_blocks, _required_element_text, _element_text, _resolve_globals,
_public_model_type, _expected_parameters, _value_fields,
)
OUT = ROOT / 'test/solver-newton-20260911/mql4'
AME = ROOT / 'tests/data/test_mql_4.ame'
XML = ROOT / 'tests/fixtures/amesim/test-mql-4-corrected.xml'
REFERENCE = ROOT / 'tests/baselines/simulation/test_mql_4/test-mql-4-amesim-reference.json'
PROJECT = ROOT / 'tests/data/test-mql-4-corrected.json'
def sha(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
def public_port(model, index):
if index == 0 and model in {'amesim_pnvo001', 'amesim_forc'}:
return 'res'
if index == 0 and model in {'amesim_step0', 'amesim_ud00'}:
return 'out'
return f'port_{index + 1}'
def parameter_key(values):
return tuple(sorted((key, format(float(value), '.12g')) for key, value in values.items()))
def audit_source():
with tarfile.open(AME) as archive:
def read(suffix):
names = [name for name in archive.getnames() if name.endswith(suffix)]
if len(names) != 1:
raise ValueError((suffix, names))
return archive.extractfile(names[0]).read().decode('latin1')
cir = read('.cir')
sim = read('.sim')
modelinfo = read('.modelinfo')
variables = read('.var')
perf = read('.ameperf')
result_name = next(name for name in archive.getnames() if name.endswith('.results'))
result_data = archive.extractfile(result_name).read()
result_points, result_columns = struct.unpack('<2i', result_data[:8])
member_hashes = {}
for suffix in ['.cir', '.sim', '.modelinfo', '.var', '.results']:
name = next(name for name in archive.getnames() if name.endswith(suffix))
data = archive.extractfile(name).read()
member_hashes[name] = {'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()}
globals_ = _resolve_globals(cir)
components = _blocks(cir, 'COMP')
lines = _blocks(cir, 'LINE')
nodes = {}
for kind, entities in [('component', components), ('line', lines)]:
for index, body in enumerate(entities):
submodel = _required_element_text(body, 'SUB_NAME')
if submodel == 'DIRECT':
continue
model = _public_model_type(submodel)
nodes[f'{kind}:{index}'] = {
'modelType': model,
'alias': _required_element_text(body, 'ALIAS'),
'parameters': _expected_parameters(body, model, globals_),
'sourceValues': {name: {'expression': value.expression, 'unit': value.unit}
for name, value in _value_fields(body).items()},
'position': _element_text(body, 'COMP_POS') or _element_text(body, 'LINE_POINTS'),
'geometry': _element_text(body, 'COMP_GEOMETRY'),
'outputType': _element_text(body, 'OUTPUT_TYPE'),
}
edges = set()
contacts = set()
for index, body in enumerate(components):
for port_index, port_body in enumerate(_blocks(body, 'COMP_PORT')):
if _required_element_text(port_body, 'PORT_CONNECT') != '1':
continue
for connection in _blocks(port_body, 'CONNECT'):
other = int(_required_element_text(connection, 'CONNECT_ENTITY_NUM'))
other_port = int(_required_element_text(connection, 'CONNECT_ENTITY_PORT'))
contacts.add(((index, port_index), (other, other_port)))
assert all((b, a) in contacts for a, b in contacts)
def endpoint(index, port):
key = f'component:{index}'
return key, public_port(nodes[key]['modelType'], port)
for a, b in contacts:
edges.add(tuple(sorted((endpoint(*a), endpoint(*b)))))
for index, body in enumerate(lines):
assert _required_element_text(body, 'LINE_START_TYPE') == '0'
assert _required_element_text(body, 'LINE_END_TYPE') == '0'
start = endpoint(int(_required_element_text(body, 'LINE_START_ENTITY')),
int(_required_element_text(body, 'LINE_START_PORT')))
end = endpoint(int(_required_element_text(body, 'LINE_END_ENTITY')),
int(_required_element_text(body, 'LINE_END_PORT')))
if _required_element_text(body, 'SUB_NAME') == 'DIRECT':
edges.add(tuple(sorted((start, end))))
else:
key = f'line:{index}'
output_type = _required_element_text(body, 'OUTPUT_TYPE')
assert output_type in {'1', '2'}
first, second = ('port_1', 'port_2') if output_type == '1' else ('port_2', 'port_1')
edges.add(tuple(sorted((start, (key, first)))))
edges.add(tuple(sorted((end, (key, second)))))
count = Counter(node['modelType'] for node in nodes.values())
states = sum(count[model] * size for model, size in {
'amesim_pnch012': 2, 'amesim_pnch023': 2, 'amesim_pnl0001': 2,
'amesim_pnl0002': 2, 'amesim_pnl0003': 4, 'amesim_mecmas21': 2,
}.items())
return nodes, sorted(edges), {
'archiveSha256': sha(AME), 'archiveBytes': AME.stat().st_size,
'members': member_hashes, 'globals': globals_,
'componentCount': len(components), 'modeledLineCount': len(nodes) - len(components),
'directLineCount': len(lines) - (len(nodes) - len(components)),
'contactCount': len(contacts) // 2, 'publicNodeCount': len(nodes),
'publicParameterCount': sum(len(node['parameters']) for node in nodes.values()),
'connectionCount': len(edges), 'typeCounts': dict(count),
'diagramDynamicStateCount': states,
'simulationFileRaw': sim, 'savedSimulationNumericRows': [[float(x) for x in row.split()] for row in sim.splitlines()],
'cachedModelInfo': modelinfo,
'cachedResultsSuitableForFourBranchComparison': False,
'cacheRejectionReason': 'Diagram has 64 states / four pistons; cached modelinfo has 132 states, .ameperf contains eight LSTP00A instances, and .results declares 1116 saved variables while the current .var has only 640 rows.',
'cachedResultHeader': {'pointCount': result_points, 'savedVariableCount': abs(result_columns)},
'variableRowCount': len(variables.splitlines()),
'cachedPerformanceContactInstances': sorted(set(re.findall(r'LSTP00A-([0-9]+)', perf))),
'cachedPistonAliases': sorted(set(re.findall(r'Data_Path=\S+@(pn_brp2[^\s]*)', variables))),
}
def xml_model(root=None):
if root is None:
root = ET.parse(XML).getroot()
nodes = {item.attrib['id']: {
'modelType': item.attrib['type'],
'modelVersion': item.attrib['modelVersion'],
'parameters': {p.attrib['name']: float(p.attrib['value']) for p in item.findall('Parameter')},
} for item in root.findall('./Components/Component')}
edges = [tuple(sorted((ep.attrib['component'], ep.attrib['port']) for ep in item.findall('Endpoint')))
for item in root.findall('./Connections/Connection')]
return nodes, edges, root
def find_mapping(first, first_edges, second, second_edges, *, ports=False, parameters=True):
"""Find an actual graph bijection, preserving node types and all parameters."""
def edge_label(node, port):
if not ports:
return ''
return port
def graph(nodes, edges):
adjacency = {key: defaultdict(Counter) for key in nodes}
for (a, ap), (b, bp) in edges:
adjacency[a][b][(edge_label(a, ap), edge_label(b, bp))] += 1
adjacency[b][a][(edge_label(b, bp), edge_label(a, ap))] += 1
return adjacency
a, b = graph(first, first_edges), graph(second, second_edges)
def signature(nodes, adj, key):
return (nodes[key]['modelType'], parameter_key(nodes[key]['parameters']) if parameters else (),
sum(sum(labels.values()) for labels in adj[key].values()))
candidates = {key: [other for other in second if signature(first, a, key) == signature(second, b, other)]
for key in first}
if any(not options for options in candidates.values()):
return None
mapping = {}
used = set()
def search():
if len(mapping) == len(first):
return dict(mapping)
key = min((key for key in first if key not in mapping),
key=lambda key: (-sum(neighbor in mapping for neighbor in a[key]),
len([other for other in candidates[key] if other not in used]), -len(a[key])))
for other in candidates[key]:
if other in used:
continue
if any(a[key].get(existing, Counter()) != b[other].get(target, Counter())
for existing, target in mapping.items()):
continue
mapping[key] = other
used.add(other)
result = search()
if result is not None:
return result
used.remove(other)
del mapping[key]
return None
return search()
def check_project_contract(project, xml_root):
"""Check generated port metadata and exact JSON/XML execution data, without writes.
This covers the observed nullable optional-field failure and shared physical
data, not a replacement for the complete frontend project parser.
"""
xml_nodes, xml_edges, _ = xml_model(xml_root)
json_nodes = {node['id']: node['data'] for node in project['nodes']}
if len(json_nodes) != len(project['nodes']) or json_nodes.keys() != xml_nodes.keys():
raise ValueError('JSON/XML component IDs differ or JSON contains duplicate IDs.')
for node_id, data in json_nodes.items():
expected = xml_nodes[node_id]
if (data['componentType'] != expected['modelType'] or
data['modelType'] != expected['modelType'] or
data['modelVersion'] != expected['modelVersion']):
raise ValueError(f'{node_id}: JSON/XML model type or version differs.')
if data['parameters'] != expected['parameters']:
raise ValueError(f'{node_id}: JSON/XML parameters differ.')
for port in data['ports']:
# frontend/src/App.tsx:isProjectPortDefinitionValue accepts omission,
# or the literal intoComponent. An explicit null rejects the import.
if ('positiveFlowDirection' in port and
port['positiveFlowDirection'] != 'intoComponent'):
raise ValueError(f"{node_id}.{port['name']}: omit undefined positiveFlowDirection instead of null.")
json_edges = []
for edge in project['edges']:
endpoints = []
for side in ['source', 'target']:
component, port = edge[side], edge[side + 'Handle']
if component not in json_nodes:
raise ValueError(f'Unknown connection component {component}.')
data = json_nodes[component]
active_ports = get_component_model_spec(data['modelType']).active_ports(data['parameters'])
if port not in {item.name for item in active_ports}:
raise ValueError(f'{component}.{port}: connection port is inactive or unknown.')
endpoints.append((component, port))
json_edges.append(tuple(sorted(endpoints)))
if Counter(json_edges) != Counter(xml_edges):
raise ValueError('JSON/XML connection endpoints differ.')
simulation = xml_root.find('Simulation').attrib
for key, xml_key in [('t_start', 'tStart'), ('t_stop', 'tStop'),
('step', 'sampleStep'), ('max_step', 'maxStep')]:
if project['simulation'][key] != float(simulation[xml_key]):
raise ValueError(f'JSON/XML simulation {key} differs.')
if project['simulation']['method'] != simulation['method']:
raise ValueError('JSON/XML integration methods differ.')
return {'valid': True, 'componentCount': len(json_nodes), 'connectionCount': len(json_edges),
'parameterCount': sum(len(data['parameters']) for data in json_nodes.values()),
'optionalPortFieldsValid': True, 'jsonXmlExecutionDataEqual': True}
def generate_project(source, source_edges, target, target_edges, xml_root, mapping):
layout_source = ROOT / 'tests/fixtures/legacy/test-mql-8.json'
layout = json.loads(layout_source.read_text())
layout_nodes = {node['id']: node for node in layout['nodes']}
reverse_mapping = {value: key for key, value in mapping.items()}
nodes = []
for node_id, model in target.items():
source_node = source[reverse_mapping[node_id]]
template = layout_nodes.get(node_id)
if template is None:
template = next(node for node in layout['nodes'] if node['data']['modelType'] == model['modelType'])
node = deepcopy(template)
node['id'] = node_id
node['selected'] = False
node['data']['label'] = source_node['alias']
node['data']['parameters'] = source_node['parameters']
node['data']['modelVersion'] = model['modelVersion']
node['data']['ports'] = get_component_model_spec(model['modelType']).as_catalog_dict()['ports']
for port in node['data']['ports']:
if port.get('positiveFlowDirection') is None:
port.pop('positiveFlowDirection', None)
if model['modelType'] == 'amesim_p4node2':
node['data']['rotation'] = 180
node['data']['mirrored'] = False
if node_id not in layout_nodes:
# The extra closing pipe sits below the four retained branches.
node['position'] = {'x': 80.0, 'y': 900.0}
node['data']['rotation'] = 0
nodes.append(node)
original_by_pair = {}
for edge in layout['edges']:
pair = tuple(sorted((edge['source'], edge['target'])))
original_by_pair[pair] = edge
xml_by_pair = {}
for connection in xml_root.findall('./Connections/Connection'):
pair = tuple(sorted(ep.attrib['component'] for ep in connection.findall('Endpoint')))
xml_by_pair[pair] = connection
edges = []
changed_connections = []
for ((a, ap), (b, bp)) in source_edges:
a, b = mapping[a], mapping[b]
pair = tuple(sorted((a, b)))
existing = original_by_pair.get(pair)
edge = deepcopy(existing) if existing else {'id': 'ame-' + a + '-' + ap + '-' + b + '-' + bp}
if existing and existing['source'] == b:
a, ap, b, bp = b, bp, a, ap
old_connection = xml_by_pair[pair]
old_endpoints = {ep.attrib['component']: ep.attrib['port'] for ep in old_connection.findall('Endpoint')}
changed = old_endpoints != {a: ap, b: bp}
if changed:
changed_connections.append({'components': pair, 'before': old_endpoints, 'after': {a: ap, b: bp}})
edge.update({'source': a, 'target': b, 'sourceHandle': ap, 'targetHandle': bp})
edge.pop('selected', None)
if any(target[node_id]['modelType'] == 'amesim_p4node2' for node_id in [a, b]):
edge.setdefault('data', {}).pop('routePoints', None)
if changed or existing is None:
edge['data'] = {'isContactEdge': False}
edges.append(edge)
for endpoint in old_connection.findall('Endpoint'):
endpoint.attrib['port'] = {a: ap, b: bp}[endpoint.attrib['component']]
parameter_changes = []
for component in xml_root.findall('./Components/Component'):
node_id = component.attrib['id']
parameters = source[reverse_mapping[node_id]]['parameters']
for parameter in component.findall('Parameter'):
key = parameter.attrib['name']
before = float(parameter.attrib['value'])
after = parameters[key]
if abs(before - after) > max(1e-18, abs(after) * 1e-12):
parameter_changes.append({'component': node_id, 'parameter': key, 'before': before, 'after': after})
parameter.attrib['value'] = repr(after)
simulation = {'t_start': 0.0, 't_stop': 10.0, 'step': 0.01, 'max_step': 1e30, 'method': 'BDF'}
project = {'projectSchemaVersion': 1, 'name': 'test-mql-4-AME-aligned', 'nodes': nodes,
'edges': edges, 'simulation': simulation}
check_project_contract(project, xml_root)
PROJECT.write_text(json.dumps(project, indent=2, ensure_ascii=False) + '\n')
generated_xml = OUT / 'aligned-input.xml'
xml_root.attrib['name'] = project['name']
ET.indent(xml_root)
ET.ElementTree(xml_root).write(generated_xml, encoding='utf-8', xml_declaration=True)
XML.write_bytes(generated_xml.read_bytes())
# Check the delivered JSON itself, including every exact endpoint and number.
json_nodes = {node['id']: node['data'] for node in json.loads(PROJECT.read_text())['nodes']}
for source_id, project_id in mapping.items():
assert json_nodes[project_id]['parameters'] == source[source_id]['parameters']
expected_edges = {tuple(sorted(((mapping[a], ap), (mapping[b], bp)))) for ((a, ap), (b, bp)) in source_edges}
actual_edges = {tuple(sorted(((edge['source'], edge['sourceHandle']), (edge['target'], edge['targetHandle'])))) for edge in edges}
assert expected_edges == actual_edges
for edge in edges:
for side in ['source', 'target']:
data = json_nodes[edge[side]]
active_ports = get_component_model_spec(data['modelType']).active_ports(data['parameters'])
assert edge[side + 'Handle'] in {port.name for port in active_ports}
return {'path': str(PROJECT.relative_to(ROOT)), 'sha256': sha(PROJECT),
'xmlPath': str(generated_xml.relative_to(ROOT)), 'xmlSha256': sha(generated_xml),
'nodeCount': len(nodes), 'edgeCount': len(edges), 'parameterCount': sum(len(n['data']['parameters']) for n in nodes),
'allMappedParametersExactlyMatchAme': True, 'allMappedConnectionPortsExactlyMatchAme': True,
'parameterChangesFromLegacyXml': parameter_changes, 'connectionPortChangesFromLegacyXml': changed_connections,
'layoutMetadataSource': {'path': str(layout_source.relative_to(ROOT)), 'sha256': sha(layout_source)},
'simulation': simulation,
'simulationSettingsNote': 'AME archive stores historical fixed-step settings. This JSON uses native BDF; rtol is supplied by the backend and is not stored in the project. Initial validation used 1e-7; the backend default was subsequently changed to 1e-8. The earlier official Standard variable-step reference remains tolerance 1e-7. Inspect actual run diagnostics; solver algorithms are not identical.'}
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--check', action='store_true', help='Validate delivered JSON/XML only; do not generate or write files.')
arguments = parser.parse_args()
if arguments.check:
print(json.dumps(check_project_contract(json.loads(PROJECT.read_text()), ET.parse(XML).getroot()), indent=2))
return
OUT.mkdir(parents=True, exist_ok=True)
source, source_edges, audit = audit_source()
legacy_xml = OUT / 'legacy-corrected-input.xml'
if not legacy_xml.exists():
legacy_xml.write_bytes(XML.read_bytes())
target, target_edges, xml_root = xml_model()
mapping = find_mapping(source, source_edges, target, target_edges)
audit['correctedXml'] = {'path': str(XML.relative_to(ROOT)), 'sha256': sha(XML),
'parameterAndComponentTopologyIsomorphic': mapping is not None}
ref = json.loads(REFERENCE.read_text())
audit['reference'] = {'path': str(REFERENCE.relative_to(ROOT)), 'sha256': sha(REFERENCE),
'source': ref['source'], 'timeCount': len(ref['times']), 'seriesCount': len(ref['series']),
'ameHashMatches': ref['originalAmeSha256'] == sha(AME),
'xmlHashMatches': ref['xmlSha256'] == sha(XML),
'newAmesimRunPerformed': False}
(OUT / 'source-model.json').write_text(json.dumps({'nodes': source, 'edges': source_edges}, indent=2, ensure_ascii=False) + '\n')
if not mapping:
mapping = find_mapping(source, source_edges, target, target_edges, parameters=False)
if mapping:
audit['componentMapping'] = {key: {'alias': source[key]['alias'], 'projectId': value} for key, value in mapping.items()}
transformed = {tuple(sorted(((mapping[a], ap), (mapping[b], bp)))) for ((a, ap), (b, bp)) in source_edges}
target_set = set(target_edges)
audit['correctedXml']['exactPortDifferencesForTopologyMapping'] = {
'ameOnly': sorted(transformed - target_set), 'xmlOnly': sorted(target_set - transformed)}
if mapping:
audit['generatedProject'] = generate_project(source, source_edges, target, target_edges, xml_root, mapping)
audit['correctedXml']['scope'] = 'Input XML snapshot before this generation; generatedProject describes the delivered corrected files.'
audit_text = json.dumps(audit, indent=2, ensure_ascii=False) + '\n'
(OUT / 'model-audit.json').write_text(audit_text)
(ROOT / 'tests/model_audit/test-mql-4-audit.json').write_text(audit_text)
print(json.dumps({key: value for key, value in audit.items() if key not in {'componentMapping', 'members', 'globals'}}, indent=2))
if __name__ == '__main__':
main()
@@ -0,0 +1,101 @@
"""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;|#)', '&amp;', 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))
@@ -0,0 +1,98 @@
{
"source": "tests/data/test_mql_4.ame:test_mql_4_.cir COMP_PORT/PORT_POS (zero-based component indices)",
"archiveSha256": "99a071896671d634d651a7b21f487662dd7c122af8fd3d60ead404a5afa53ed4",
"components": [
{
"componentIndex": 47,
"alias": "pnnode4_16",
"geometry": 1,
"ports": {
"port_1": {
"x": 10.0,
"y": 19.0
},
"port_2": {
"x": 19.0,
"y": 10.0
},
"port_3": {
"x": 10.0,
"y": 1.0
},
"port_4": {
"x": 1.0,
"y": 10.0
}
}
},
{
"componentIndex": 48,
"alias": "pnnode4_17",
"geometry": 1,
"ports": {
"port_1": {
"x": 10.0,
"y": 19.0
},
"port_2": {
"x": 19.0,
"y": 10.0
},
"port_3": {
"x": 10.0,
"y": 1.0
},
"port_4": {
"x": 1.0,
"y": 10.0
}
}
},
{
"componentIndex": 49,
"alias": "pnnode4_18",
"geometry": 1,
"ports": {
"port_1": {
"x": 10.0,
"y": 19.0
},
"port_2": {
"x": 19.0,
"y": 10.0
},
"port_3": {
"x": 10.0,
"y": 1.0
},
"port_4": {
"x": 1.0,
"y": 10.0
}
}
},
{
"componentIndex": 50,
"alias": "pnnode4_19",
"geometry": 1,
"ports": {
"port_1": {
"x": 10.0,
"y": 19.0
},
"port_2": {
"x": 19.0,
"y": 10.0
},
"port_3": {
"x": 10.0,
"y": 1.0
},
"port_4": {
"x": 1.0,
"y": 10.0
}
}
}
]
}
File diff suppressed because it is too large. Load diff