C内核流量计算方法优化,前端文件名称读取优化
This commit is contained in:
1 parent
5d5a2e1843
commit
808c484f5b
94 files changed
+20163
-4418
No files matched your search
@@ -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;|#)', '&', 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
Reference in new issue
Block a user