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

+325
View File
@@ -0,0 +1,325 @@
"""Reproduce the eight-branch AME drawing -> editor model audit.
Run from the repository root with .venv/bin/python. The original inputs are
read only. Mapping uses typed graph isomorphism and endpoint edit cost, never
component display names; names are emitted only as evidence for review.
"""
from __future__ import annotations
from collections import Counter, defaultdict
from copy import deepcopy
import hashlib
import json
import math
import re
from pathlib import Path
import sys
import tarfile
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from tests.test_test_mql_ame_contract import (
_blocks, _element_text as element, _expected_parameters,
_public_model_type, _resolve_globals, _value_fields,
_SOURCE_FIELD_BY_PARAMETER, _numeric_project_value,
)
from app.simulation.registry import get_component_model_spec
HERE = ROOT / 'test/solver-newton-20260911/mql8'
AME = ROOT / 'tests/data/test_mql.ame'
INPUT = ROOT / 'tests/fixtures/legacy/test-mql-8.json'
OUTPUT = ROOT / 'tests/data/test-mql-8-corrected.json'
def read_drawing():
with tarfile.open(AME) as archive:
cir = archive.extractfile('test_mql_.cir').read().decode('latin1')
sim = archive.extractfile('test_mql_.sim').read().decode('ascii')
model_info = archive.extractfile('test_mql_.modelinfo').read().decode('ascii')
globals_ = _resolve_globals(cir)
components, lines = _blocks(cir, 'COMP'), _blocks(cir, 'LINE')
entities, edges = {}, []
def add(key, body):
kind = _public_model_type(element(body, 'SUB_NAME'))
entities[key] = {
'modelType': kind, 'alias': element(body, 'ALIAS'),
'parameters': _expected_parameters(body, kind, globals_),
'fields': {k: vars(v) for k, v in _value_fields(body).items()},
'geometry': element(body, 'COMP_GEOMETRY'),
'outputType': element(body, 'OUTPUT_TYPE'),
'scopeId': element(body, 'CIRCUIT_SCOPE_ID'),
'drawingPorts': [{'position': element(p, 'PORT_POS'), 'face': element(p, 'PORT_FACE')}
for p in _blocks(body, 'COMP_PORT')],
}
def port(component, index):
kind = entities[component]['modelType']
if index == 0 and kind in ('amesim_forc', 'amesim_pnvo001'):
return 'res'
if index == 0 and kind in ('amesim_step0', 'amesim_ud00'):
return 'out'
return f'port_{index + 1}'
for index, body in enumerate(components):
add(f'component:{index}', body)
contacts = set()
for index, body in enumerate(components):
for pindex, pbody in enumerate(_blocks(body, 'COMP_PORT')):
if element(pbody, 'PORT_CONNECT') != '1':
continue
for conn in _blocks(pbody, 'CONNECT'):
other = int(element(conn, 'CONNECT_ENTITY_NUM'))
op = int(element(conn, 'CONNECT_ENTITY_PORT'))
contacts.add(((index, pindex), (other, op)))
for left, right in sorted(contacts):
assert (right, left) in contacts, 'Non-reciprocal component contact'
if left > right:
continue
a, b = f'component:{left[0]}', f'component:{right[0]}'
edges.append(((a, port(a, left[1])), (b, port(b, right[1])), 'contact'))
for index, body in enumerate(lines):
assert element(body, 'LINE_START_TYPE') == element(body, 'LINE_END_TYPE') == '0'
a, b = ('component:' + element(body, 'LINE_START_ENTITY'),
'component:' + element(body, 'LINE_END_ENTITY'))
left = a, port(a, int(element(body, 'LINE_START_PORT')))
right = b, port(b, int(element(body, 'LINE_END_PORT')))
if element(body, 'SUB_NAME') == 'DIRECT':
edges.append((left, right, f'direct-line:{index}'))
continue
key = f'line:{index}'
add(key, body)
output_type = element(body, 'OUTPUT_TYPE')
assert output_type in ('1', '2'), (key, output_type)
# A reversed two-port line's first physical endpoint is submodel port 2.
lp, rp = ('port_1', 'port_2') if output_type == '1' else ('port_2', 'port_1')
edges.extend(((left, (key, lp), key), ((key, rp), right, key)))
return entities, edges, globals_, sim, model_info
def match_graph(entities, ame_edges, project):
nodes = {n['id']: n for n in project['nodes']}
types = {**{k: v['modelType'] for k, v in entities.items()},
**{k: v['data']['modelType'] for k, v in nodes.items()}}
adjacency = {k: {} for k in types}
for (a, ap), (b, bp), _ in ame_edges:
assert b not in adjacency[a], 'Parallel entity edges need a multigraph matcher'
adjacency[a][b], adjacency[b][a] = (ap, bp), (bp, ap)
for edge in project['edges']:
a, b = edge['source'], edge['target']
assert b not in adjacency[a], 'Parallel editor edges need a multigraph matcher'
ap, bp = edge['sourceHandle'], edge['targetHandle']
adjacency[a][b], adjacency[b][a] = (ap, bp), (bp, ap)
colors = types.copy()
for _ in range(len(entities)):
signatures = {k: (types[k], colors[k], tuple(sorted(colors[n] for n in adjacency[k]))) for k in types}
vocabulary = {v: i for i, v in enumerate(sorted(set(signatures.values())))}
updated = {k: vocabulary[v] for k, v in signatures.items()}
if len(set(updated.values())) == len(set(colors.values())):
colors = updated
break
colors = updated
groups = defaultdict(lambda: [[], []])
for key in entities:
groups[colors[key]][0].append(key)
for key in nodes:
groups[colors[key]][1].append(key)
for ames, editors in groups.values():
assert len(ames) == len(editors), 'Typed topology differs'
# All remaining structural symmetries in this exact input preserve
# parameters, so selection within them does not hide parameter errors.
assert len({json.dumps(entities[k]['parameters'], sort_keys=True) for k in ames}) == 1
candidates = {a: tuple(editors) for ames, editors in groups.values() for a in ames}
best, best_cost, examined = None, math.inf, 0
mapping, used = {}, set()
def choices(a):
result = []
for b in candidates[a]:
if b in used:
continue
cost = 0
for previous, assigned in mapping.items():
ae, be = adjacency[a].get(previous), adjacency[b].get(assigned)
if bool(ae) != bool(be):
break
if ae:
cost += sum(x != y for x, y in zip(ae, be))
else:
result.append((cost, b))
return sorted(result)
def visit(cost):
nonlocal best, best_cost, examined
if cost >= best_cost:
return
if len(mapping) == len(entities):
examined += 1
best, best_cost = mapping.copy(), cost
return
remaining = [(a, choices(a)) for a in entities if a not in mapping]
if any(not options for _, options in remaining):
return
# Each current candidate cost is charged only for edges to assigned
# nodes, so this lower bound does not double count any edge.
if cost + sum(options[0][0] for _, options in remaining) >= best_cost:
return
a, options = min(remaining, key=lambda item: (len(item[1]),
-sum(n in mapping for n in adjacency[item[0]]), -len(adjacency[item[0]]), item[0]))
for extra, b in options:
mapping[a] = b
used.add(b)
visit(cost + extra)
used.remove(b)
del mapping[a]
visit(0)
assert best is not None, 'No typed topology isomorphism'
return best, {'minimumEndpointEdits': best_cost, 'improvingSolutions': examined,
'typedRefinementGroups': len(groups), 'parameterPreservingSymmetries': True}
def main():
HERE.mkdir(parents=True, exist_ok=True)
original = json.loads(INPUT.read_text())
entities, edges, globals_, sim, model_info = read_drawing()
mapping, matching = match_graph(entities, edges, original)
corrected = deepcopy(original)
corrected['name'] = 'test-mql-8-corrected'
nodes = {n['id']: n for n in corrected['nodes']}
parameter_audit, parameter_changes = [], []
for source, target in mapping.items():
entity, node = entities[source], nodes[target]
spec = get_component_model_spec(entity['modelType'])
assert node['data']['modelVersion'] == spec.model_version
source_fields = _SOURCE_FIELD_BY_PARAMETER.get(entity['modelType'], {})
assert set(node['data']['parameters']) == set(entity['parameters'])
for parameter in spec.parameters:
name = parameter.name
expected = entity['parameters'][name]
actual = _numeric_project_value(node['data']['parameters'][name])
row = {'component': target, 'ameEntity': source, 'ameAlias': entity['alias'],
'parameter': name, 'unit': parameter.unit, 'old': actual, 'expected': expected,
'source': entity['fields'].get(source_fields.get(name, name)),
'matches': math.isclose(actual, expected, rel_tol=2e-14, abs_tol=1e-15)}
parameter_audit.append(row)
if not row['matches']:
parameter_changes.append(row)
node['data']['parameters'][name] = expected
desired = {}
for (a, ap), (b, bp), source in edges:
ja, jb = mapping[a], mapping[b]
key = frozenset((ja, jb))
assert key not in desired
desired[key] = ({ja: ap, jb: bp}, source)
connection_audit, connection_changes = [], []
cleared_contact_flags = []
for edge in corrected['edges']:
ends, source = desired.pop(frozenset((edge['source'], edge['target'])))
before = [(edge['source'], edge['sourceHandle']), (edge['target'], edge['targetHandle'])]
after = [(edge['source'], ends[edge['source']]), (edge['target'], ends[edge['target']])]
row = {'edgeId': edge['id'], 'ameSource': source, 'before': before, 'after': after, 'matches': before == after}
connection_audit.append(row)
if not row['matches']:
connection_changes.append(row)
edge['sourceHandle'], edge['targetHandle'] = after[0][1], after[1][1]
# A moved endpoint no longer certifies the old geometric contact.
# Keep its explicit connection visible; physical adjacency is intact.
if edge.get('data', {}).get('isContactEdge'):
edge['data']['isContactEdge'] = False
cleared_contact_flags.append(edge['id'])
assert not desired
# The corrected shared P4 symbol uses p1 up, p2 left, p3 down, p4 right.
# Read every drawing port to ensure the saved geometry needs exactly 180°.
display_audit, p4_nodes = [], set()
for source, target in mapping.items():
entity = entities[source]
if entity['modelType'] != 'amesim_p4node2':
continue
expected = [('10 19', '1'), ('19 10', '3'), ('10 1', '0'), ('1 10', '2')]
assert entity['geometry'] == '1'
assert [(p['position'], p['face']) for p in entity['drawingPorts']] == expected
data = nodes[target]['data']
display_audit.append({'component': target, 'ameEntity': source, 'ameAlias': entity['alias'],
'ameGeometry': entity['geometry'], 'drawingPorts': entity['drawingPorts'],
'previousRotation': data.get('rotation'), 'previousMirrored': data.get('mirrored'),
'rotation': 180, 'mirrored': False,
'portSides': {'port_1': 'bottom', 'port_2': 'right', 'port_3': 'top', 'port_4': 'left'}})
data['rotation'], data['mirrored'] = 180, False
p4_nodes.add(target)
cleared_route_points = []
for edge in corrected['edges']:
if edge['source'] in p4_nodes or edge['target'] in p4_nodes:
if 'routePoints' in edge.get('data', {}):
del edge['data']['routePoints']
cleared_route_points.append(edge['id'])
# The first five .sim values are recorded directly; no interpretation of
# undocumented solver-enumeration slots is needed for this correction.
sim_values = [float(x) for x in sim.splitlines()[0].split()]
corrected['simulation'].update(t_start=sim_values[0], t_stop=sim_values[1], step=sim_values[2], max_step=sim_values[3])
from app.main import ReactFlowProjectPayload, build_reactflow_system_xml, compile_reactflow_network
from app.simulation.native_codegen.compiler import compile_native_program
# Browser export resolves arithmetic editor expressions on a separate copy.
executable = deepcopy(corrected)
for node in executable['nodes']:
node['data']['parameters'] = {k: _numeric_project_value(v)
for k, v in node['data']['parameters'].items()}
payload = ReactFlowProjectPayload.model_validate(executable)
network = compile_reactflow_network(payload)
program = compile_native_program(network)
OUTPUT.write_text(json.dumps(corrected, ensure_ascii=False, indent=2) + '\n')
xml = build_reactflow_system_xml(payload)
(HERE / 'corrected.xml').write_bytes(xml if isinstance(xml, bytes) else xml.encode())
# The archive's parameter table and run input have matching row order.
with tarfile.open(AME) as archive:
param_lines = archive.extractfile('test_mql_.param').read().decode('latin1').splitlines()
data_lines = archive.extractfile('test_mql_.data').read().decode('latin1').splitlines()
assert len(param_lines) == len(data_lines)
run_parameters = {re.search(r'Data_Path=(\S+)', text).group(1): (i+1, data_lines[i])
for i,text in enumerate(param_lines) if 'Data_Path=' in text}
cache_checks, structural_fields = [], []
reverse_mapping = {v:k for k,v in mapping.items()}
from tests.test_test_mql_ame_contract import _evaluate_expression
for row in parameter_audit:
if row['source'] is None:
continue
entity = entities[reverse_mapping[row['component']]]
field = _SOURCE_FIELD_BY_PARAMETER.get(entity['modelType'], {}).get(row['parameter'], row['parameter'])
path = field + '@' + row['ameAlias']
if path not in run_parameters:
assert entity['modelType'] == 'amesim_lmechn1' and field == 'v1'
structural_fields.append(path)
continue
line, expression = run_parameters[path]
drawing_value = _evaluate_expression(row['source']['expression'], globals_)
run_value = _evaluate_expression(expression, globals_)
assert math.isclose(drawing_value, run_value, rel_tol=2e-14, abs_tol=1e-15), path
cache_checks.append({'dataPath': path, 'parameterDataLine': line,
'drawingValue': drawing_value, 'runValue': run_value})
audit = {
'sources': {str(p.relative_to(ROOT)): {'sha256': hashlib.sha256(p.read_bytes()).hexdigest(), 'bytes': p.stat().st_size} for p in (AME, INPUT)},
'output': str(OUTPUT.relative_to(ROOT)), 'globals': globals_, 'matching': matching,
'counts': {'components': len(entities), 'connections': len(edges), 'parameters': len(parameter_audit),
'changedParameters': len(parameter_changes), 'changedConnections': len(connection_changes),
'states': len(program.state_keys), 'outputs': len(program.variables)},
'simulation': {'old': original['simulation'], 'corrected': corrected['simulation'], 'ameRaw': sim,
'ameModelInfo': model_info, 'ameRtol': sim_values[4],
'note': 'BDF retained; AME solver enumeration is not equated with CVODE BDF. API rtol comes from the current backend default; this editor JSON does not carry rtol.'},
'mapping': [{'ameEntity': a, 'ameAlias': entities[a]['alias'], 'component': b,
'type': entities[a]['modelType'], 'lineOutputType': entities[a]['outputType']} for a,b in sorted(mapping.items())],
'parameterChanges': parameter_changes, 'connectionChanges': connection_changes,
'clearedGeometricContactFlags': cleared_contact_flags,
'p4DisplayAlignment': display_audit,
'clearedP4RoutePoints': cleared_route_points,
'archiveParameterCrossCheck': {'tableRows': len(param_lines), 'checked': len(cache_checks),
'drawingOnlyStructuralFields': structural_fields, 'checks': cache_checks},
'parameters': parameter_audit, 'connections': connection_audit,
'evaluationSchedule': program.evaluation_schedule,
}
(HERE / 'audit.json').write_text(json.dumps(audit, ensure_ascii=False, indent=2) + '\n')
print(json.dumps({'counts': audit['counts'], 'matching': matching,
'parameters': parameter_changes, 'connections': connection_changes,
'simulation': audit['simulation']}, ensure_ascii=False, indent=2))
if __name__ == '__main__':
main()