现有组件功能与Amesim组件比较对齐,补全功能;仿真服务启动环境检测,旧版json工程文件适配读取

This commit is contained in:
ljz committed 2026-09-13 20:01:56 +08:00
1 parent b6c22a54f8
commit 6fc9afe41d
119 files changed
+260114 -351

No files matched your search

+15 -3
View File
@@ -46,14 +46,19 @@ async def _app_lifespan(application: FastAPI) -> AsyncIterator[None]:
import logging
from app.simulation.backends import numeric_engine_name
from app.simulation.warmup import warm_up_simulation_runtime
from app.simulation.warmup import SimulationRuntimeCheck
application.state.simulation_numeric_engine = numeric_engine_name()
logging.getLogger("uvicorn.error").info(
"Simulation numeric engine: %s", application.state.simulation_numeric_engine,
)
application.state.simulation_warmup = warm_up_simulation_runtime().as_dict()
yield
runtime_check = SimulationRuntimeCheck()
application.state.simulation_runtime_check = runtime_check
runtime_check.start()
try:
yield
finally:
runtime_check.close()
app = FastAPI(
@@ -338,6 +343,13 @@ def get_component_catalog() -> dict[str, object]:
return build_component_catalog()
@app.get("/api/simulation/runtime-check")
def get_simulation_runtime_check(request: Request) -> Response:
report = request.app.state.simulation_runtime_check.snapshot()
return Response(content=json.dumps(report, ensure_ascii=False), media_type="application/json",
headers={"Cache-Control": "no-store"})
@app.post("/api/reactflow/system-xml")
def export_reactflow_system_xml(payload: ReactFlowProjectPayload) -> Response:
try:
@@ -56,7 +56,7 @@ class AmesimPnl0001(ThermodynamicVolumeComponent):
MODEL_TYPE = 'amesim_pnl0001'
MODEL_VERSION = '0.4.0'
PORTS = (PortDefinition.pneumatic('port_1', computation=FLOW_SUPPLY), PortDefinition.pneumatic('port_2', computation=THERMODYNAMIC_SUPPLY))
PARAMETERS = (AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition('diam', 0.01, label='管径', quantity='length', unit='m', minimum=0.0, minimum_exclusive=True, description='管路的有效内径,用于计算流通面积、储气容积和摩擦压降。'), ParameterDefinition('le', 1.0, label='管长', quantity='length', unit='m', minimum=0.0, minimum_exclusive=True, description='管路的有效长度,用于计算储气容积、换热面积和摩擦压降。'), ParameterDefinition('rr', 1e-05, label='相对粗糙度', quantity='dimensionless', unit='', minimum=0.0, maximum=0.1, description='管壁绝对粗糙度与管径之比,用于计算 Darcy 摩擦因子。'), ParameterDefinition('k', 1.35, label='多方指数', quantity='dimensionless', unit='', minimum=0.0, minimum_exclusive=True, maximum=2.0, description='mode=1 多方过程使用的指数;当前公开求解器保留该 AMESim 配置,尚未实现多方指数对状态方程的修正。', visible_when=(_DYNAMIC_PIPE_POLYTROPIC_MODE,)), ParameterDefinition('kth', 0.0, label='换热系数', quantity='heat_transfer_coefficient', unit='W/(m2*K)', minimum=0.0, description='mode=2 带换热过程使用的气体与外部环境对流换热系数。', visible_when=(_DYNAMIC_PIPE_HEAT_EXCHANGE_MODE,)), ParameterDefinition('extemp', 293.15, label='外部温度', quantity='temperature', unit='K', minimum=0.0, minimum_exclusive=True, description='mode=2 带换热过程使用的外部环境绝对温度。', visible_when=(_DYNAMIC_PIPE_HEAT_EXCHANGE_MODE,)), ParameterDefinition('mode', 2.0, label='热模型', quantity='dimensionless', unit='', minimum=1.0, maximum=2.0, editor='choice', options=(ParameterOption(1.0, '多方过程'), ParameterOption(2.0, '带换热')), description='AMESim 原始编码:1 为多方过程,2 为带换热。当前公开求解器在多方模式下关闭环境换热,在带换热模式下按换热系数和外部温度计算环境换热。'), ParameterDefinition('p0', 100000.0, label='初始压力', quantity='pressure', unit='Pa', minimum=0.0, minimum_exclusive=True, description='仿真开始时管内气体的绝对压力。'), ParameterDefinition('T0', 293.15, label='初始温度', quantity='temperature', unit='K', minimum=0.0, minimum_exclusive=True, description='仿真开始时管内气体的绝对温度。'))
PARAMETERS = (AMESIM_GAS_INDEX_PARAMETER, ParameterDefinition('diam', 0.01, label='管径', quantity='length', unit='m', minimum=0.0, minimum_exclusive=True, description='管路的有效内径,用于计算流通面积、储气容积和摩擦压降。'), ParameterDefinition('le', 1.0, label='管长', quantity='length', unit='m', minimum=0.0, minimum_exclusive=True, description='管路的有效长度,用于计算储气容积、换热面积和摩擦压降。'), ParameterDefinition('rr', 1e-05, label='相对粗糙度', quantity='dimensionless', unit='', minimum=0.0, maximum=0.1, description='管壁绝对粗糙度与管径之比,用于计算 Darcy 摩擦因子。'), ParameterDefinition('k', 1.35, label='多方指数', quantity='dimensionless', unit='', minimum=0.0, minimum_exclusive=True, description='mode=1 多方过程指数。采用理想气体储气关系,按质量变化计算温度与压力变化;k=1 为等温。流阻和端口焓仍使用选定介质。', visible_when=(_DYNAMIC_PIPE_POLYTROPIC_MODE,)), ParameterDefinition('kth', 0.0, label='换热系数', quantity='heat_transfer_coefficient', unit='W/(m2*K)', minimum=0.0, description='mode=2 带换热过程使用的气体与外部环境对流换热系数。', visible_when=(_DYNAMIC_PIPE_HEAT_EXCHANGE_MODE,)), ParameterDefinition('extemp', 293.15, label='外部温度', quantity='temperature', unit='K', minimum=0.0, minimum_exclusive=True, description='mode=2 带换热过程使用的外部环境绝对温度。', visible_when=(_DYNAMIC_PIPE_HEAT_EXCHANGE_MODE,)), ParameterDefinition('mode', 2.0, label='热模型', quantity='dimensionless', unit='', minimum=1.0, maximum=2.0, editor='choice', options=(ParameterOption(1.0, '多方过程'), ParameterOption(2.0, '带换热')), description='AMESim 原始编码:1 为多方过程,2 为带换热。多方模式按多方指数计算状态变化,不使用流入焓和环境换热决定温度;带换热模式采用质量与能量守恒。'), ParameterDefinition('p0', 100000.0, label='初始压力', quantity='pressure', unit='Pa', minimum=0.0, minimum_exclusive=True, description='仿真开始时管内气体的绝对压力。'), ParameterDefinition('T0', 293.15, label='初始温度', quantity='temperature', unit='K', minimum=0.0, minimum_exclusive=True, description='仿真开始时管内气体的绝对温度。'))
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES + (ResultVariableDefinition('re', label='Reynolds 数', quantity='dimensionless', unit='', category='derived', order=100), ResultVariableDefinition('cm', label='质量流量参数', quantity='dimensionless', unit='', category='derived', order=110), ResultVariableDefinition('v', label='平均气体速度', quantity='velocity', unit='m/s', category='derived', order=120), ResultVariableDefinition('ff', label='摩擦因子', quantity='dimensionless', unit='', category='derived', order=130))
DISPLAY = ComponentDisplaySpec(label='PNL0001 C-R 动态管路', library_id='amesim', category_id='flow', symbol='amesim_pnl0001', ports=(PortDisplaySpec('port_1', 'left', order=10), PortDisplaySpec('port_2', 'right', order=20)), order=30, parameter_groups=_DYNAMIC_PIPE_PARAMETER_GROUPS)
@@ -61,7 +61,7 @@ class AmesimMecmas21(DynamicComponent):
MODEL_TYPE = 'amesim_mecmas21'
MODEL_VERSION = '0.2.0'
PORTS = (PortDefinition.mechanical_translational('port_1'), PortDefinition.mechanical_translational('port_2'))
PARAMETERS = (ParameterDefinition('mass', 1.0, label='质量', quantity='mass', unit='kg', minimum=0.0, minimum_exclusive=True, description='平动质量,必须大于零。'), ParameterDefinition('fstick', 0.0, label='静摩擦力', quantity='force', unit='N', minimum=0.0, description='AMESim 静摩擦阈值。当前公开求解器尚未实现静摩擦保持与脱离逻辑,因此该参数暂不参与力计算。', visible_when=(_MECMAS21_NON_RESTITUTION, _MECMAS21_FRICTION_ENABLED)), ParameterDefinition('fcoul', 0.0, label='库仑摩擦力', quantity='force', unit='N', minimum=0.0, description='滑动时的库仑摩擦力绝对值,方向与速度相反。', visible_when=(_MECMAS21_NON_RESTITUTION, _MECMAS21_FRICTION_ENABLED)), ParameterDefinition('rvisc', 0.0, label='黏性摩擦系数', quantity='translational_damping', unit='N/(m/s)', minimum=0.0, description='与速度成正比的黏性摩擦系数。', visible_when=(_MECMAS21_FRICTION_ENABLED,)), ParameterDefinition('wind', 0.0, label='风阻系数', quantity='windage', unit='N/(m/s)^2', minimum=0.0, description='与速度平方成正比、方向与速度相反的风阻系数。', visible_when=(_MECMAS21_FRICTION_ENABLED,)), ParameterDefinition('dvel', 1e-06, label='粘滞速度阈值', quantity='velocity', unit='m/s', minimum=0.0, description='高级摩擦模型的低速切换阈值;当前公开求解器尚未实现对应的高级静摩擦公式。', visible_when=(_MECMAS21_NON_RESTITUTION, _MECMAS21_FRICTION_ENABLED, _MECMAS21_ADVANCED_FRICTION)), ParameterDefinition('restdvel', 1e-06, label='恢复速度阈值', quantity='velocity', unit='m/s', minimum=0.0, description='恢复碰撞低于该入射速度时按无回弹处理。', visible_when=(_MECMAS21_RESTITUTION_STOP,)), ParameterDefinition('restcoeff', 0.65, label='恢复系数', quantity='dimensionless', unit='', minimum=0.0, maximum=1.0, description='恢复碰撞后的速度与碰撞前速度绝对值之比。', visible_when=(_MECMAS21_RESTITUTION_STOP,)), ParameterDefinition('astrib', 0.001, label='Stribeck 常数', quantity='velocity', unit='m/s', minimum=0.0, description='高级 Stribeck 摩擦模型的速度常数;当前公开求解器尚未实现对应的 Stribeck 公式。', visible_when=(_MECMAS21_NON_RESTITUTION, _MECMAS21_FRICTION_ENABLED, _MECMAS21_ADVANCED_FRICTION, _MECMAS21_STRIBECK_ENABLED)), ParameterDefinition('xmin', -1.0, label='下位移限位', quantity='length', unit='m', description='理想、弹性或恢复碰撞限位的下边界位置。', visible_when=(_MECMAS21_LIMITS_ENABLED,)), ParameterDefinition('Kbmin', 1000000000.0, label='下限位刚度', quantity='translational_stiffness', unit='N/m', minimum=0.0, description='弹性下限位的接触刚度。', visible_when=(_MECMAS21_ELASTIC_STOP,)), ParameterDefinition('Dbmin', 10000.0, label='下限位阻尼', quantity='translational_damping', unit='N/(m/s)', minimum=0.0, description='弹性下限位的最大接触阻尼。', visible_when=(_MECMAS21_ELASTIC_STOP,)), ParameterDefinition('Pdmin', 0.0001, label='下限位满阻尼穿透', quantity='length', unit='m', minimum=0.0, description='弹性下限位阻尼由零增至全值所需的穿透量。', visible_when=(_MECMAS21_ELASTIC_STOP,)), ParameterDefinition('xmax', 0.8, label='上位移限位', quantity='length', unit='m', description='理想、弹性或恢复碰撞限位的上边界位置。', visible_when=(_MECMAS21_LIMITS_ENABLED,)), ParameterDefinition('Kbmax', 1000000000.0, label='上限位刚度', quantity='translational_stiffness', unit='N/m', minimum=0.0, description='弹性上限位的接触刚度。', visible_when=(_MECMAS21_ELASTIC_STOP,)), ParameterDefinition('Dbmax', 10000.0, label='上限位阻尼', quantity='translational_damping', unit='N/(m/s)', minimum=0.0, description='弹性上限位的最大接触阻尼。', visible_when=(_MECMAS21_ELASTIC_STOP,)), ParameterDefinition('Pdmax', 0.0001, label='上限位满阻尼穿透', quantity='length', unit='m', minimum=0.0, description='弹性上限位阻尼由零增至全值所需的穿透量。', visible_when=(_MECMAS21_ELASTIC_STOP,)), ParameterDefinition('theta', 0.0, label='倾角(度)', quantity='dimensionless', unit='', description='源 AMESim 参数以度为单位:+90° 表示端口 1 位于最低端,-90° 表示端口 1 位于最高端。当前一维公开求解器尚未加入由该倾角产生的重力分量。'), ParameterDefinition('useFriction', 2.0, label='启用摩擦', quantity='dimensionless', unit='', editor='choice', options=(ParameterOption(1.0, '否'), ParameterOption(2.0, '是')), description='AMESim 原始编码:1 表示不启用摩擦,2 表示启用摩擦。当前公开模型在启用时实现黏性摩擦、风阻和基础库仑摩擦;高级静摩擦与 Stribeck 公式尚未实现。'), ParameterDefinition('stoptype', 1.0, label='限位类型', quantity='dimensionless', unit='', editor='choice', options=(ParameterOption(1.0, '理想限位'), ParameterOption(2.0Line truncated
PARAMETERS = (ParameterDefinition('mass', 1.0, label='质量', quantity='mass', unit='kg', minimum=0.0, minimum_exclusive=True, description='平动质量,必须大于零。'), ParameterDefinition('fstick', 0.0, label='静摩擦力', quantity='force', unit='N', minimum=0.0, description='静止时可抵消的外力阈值;超过阈值后进入滑动,停止后重新保持。恢复碰撞模式不使用干摩擦。', visible_when=(_MECMAS21_NON_RESTITUTION, _MECMAS21_FRICTION_ENABLED)), ParameterDefinition('fcoul', 0.0, label='库仑摩擦力', quantity='force', unit='N', minimum=0.0, description='滑动时的库仑摩擦力绝对值,方向与速度相反;启用干摩擦时不得大于 fstick。', visible_when=(_MECMAS21_NON_RESTITUTION, _MECMAS21_FRICTION_ENABLED)), ParameterDefinition('rvisc', 0.0, label='黏性摩擦系数', quantity='translational_damping', unit='N/(m/s)', minimum=0.0, description='与速度成正比的黏性摩擦系数。', visible_when=(_MECMAS21_FRICTION_ENABLED,)), ParameterDefinition('wind', 0.0, label='风阻系数', quantity='windage', unit='N/(m/s)^2', minimum=0.0, description='与速度平方成正比、方向与速度相反的风阻系数。', visible_when=(_MECMAS21_FRICTION_ENABLED,)), ParameterDefinition('dvel', 1e-06, label='粘滞速度阈值', quantity='velocity', unit='m/s', minimum=0.0, description='高级摩擦模型的低速保持区间半宽;减速进入该区间时检查静摩擦保持条件。必须大于零。', visible_when=(_MECMAS21_NON_RESTITUTION, _MECMAS21_FRICTION_ENABLED, _MECMAS21_ADVANCED_FRICTION)), ParameterDefinition('restdvel', 1e-06, label='恢复速度阈值', quantity='velocity', unit='m/s', minimum=0.0, description='恢复碰撞低于该入射速度时按无回弹处理。', visible_when=(_MECMAS21_RESTITUTION_STOP,)), ParameterDefinition('restcoeff', 0.65, label='恢复系数', quantity='dimensionless', unit='', minimum=0.0, maximum=1.0, description='恢复碰撞后的速度与碰撞前速度绝对值之比。', visible_when=(_MECMAS21_RESTITUTION_STOP,)), ParameterDefinition('astrib', 0.001, label='Stribeck 常数', quantity='velocity', unit='m/s', minimum=0.0, description='高级 Stribeck 摩擦模型的速度常数,控制摩擦力从静摩擦阈值向库仑摩擦力的指数过渡;必须大于零。', visible_when=(_MECMAS21_NON_RESTITUTION, _MECMAS21_FRICTION_ENABLED, _MECMAS21_ADVANCED_FRICTION, _MECMAS21_STRIBECK_ENABLED)), ParameterDefinition('xmin', -1.0, label='下位移限位', quantity='length', unit='m', description='理想、弹性或恢复碰撞限位的下边界位置。', visible_when=(_MECMAS21_LIMITS_ENABLED,)), ParameterDefinition('Kbmin', 1000000000.0, label='下限位刚度', quantity='translational_stiffness', unit='N/m', minimum=0.0, description='弹性下限位的接触刚度,启用弹性限位时必须大于零。', visible_when=(_MECMAS21_ELASTIC_STOP,)), ParameterDefinition('Dbmin', 10000.0, label='下限位阻尼', quantity='translational_damping', unit='N/(m/s)', minimum=0.0, description='弹性下限位的最大接触阻尼,启用弹性限位时必须大于零。', visible_when=(_MECMAS21_ELASTIC_STOP,)), ParameterDefinition('Pdmin', 0.0001, label='下限位满阻尼穿透', quantity='length', unit='m', minimum=0.0, description='弹性下限位阻尼增至最大值约 63.2% 时的穿透量;阻尼随穿透量按指数规律趋近最大值。', visible_when=(_MECMAS21_ELASTIC_STOP,)), ParameterDefinition('xmax', 0.8, label='上位移限位', quantity='length', unit='m', description='理想、弹性或恢复碰撞限位的上边界位置。', visible_when=(_MECMAS21_LIMITS_ENABLED,)), ParameterDefinition('Kbmax', 1000000000.0, label='上限位刚度', quantity='translational_stiffness', unit='N/m', minimum=0.0, description='弹性上限位的接触刚度,启用弹性限位时必须大于零。', visible_when=(_MECMAS21_ELASTIC_STOP,)), ParameterDefinition('Dbmax', 10000.0, label='上限位阻尼', quantity='translational_damping', unit='N/(m/s)', minimum=0.0, description='弹性上限位的最大接触阻尼,启用弹性限位时必须大于零。', visible_when=(_MECMAS21_ELASTIC_STOP,)), ParameterDefinition('Pdmax', 0.0001, label='上限位满阻尼穿透', quantity='length', unit='m', minimum=0.0, description='弹性上限位阻尼增至最大值约 63.2% 时的穿透量;阻尼随穿透量按指数规律趋近最大值。', visible_when=(_MECMAS21_ELASTIC_STOP,)), ParameterDefinition('theta', 0.0, label='倾角(度)', quantity='dimensionless', unit='', description='源 AMESim 参数以度为单位:+90° 表示端口 1 位于最低端,-90° 表示端口 1 位于最高端。按标准重力加速度 9.80665 m/s² 计算沿运动方向的重力分量;正倾角产生正向加速度。'), ParameterDefinition('useFriction', 2.0, label='启用摩擦', quantity='dimensionless', unit='', editor='choice', options=(ParameterOption(1.0, '否'), ParameterOption(2.0, '是')), description='AMELine truncated
RESULT_VARIABLES = (ResultVariableDefinition('a', '加速度', 'acceleration', 'm/s2', 'state', 10), ResultVariableDefinition('v', '速度', 'velocity', 'm/s', 'state', 20), ResultVariableDefinition('x', '位移', 'length', 'm', 'state', 30), ResultVariableDefinition('Fvisc', '黏性摩擦力', 'force', 'N', 'derived', 40), ResultVariableDefinition('Ffric', '干摩擦力', 'force', 'N', 'derived', 50), ResultVariableDefinition('Fmin', '下限位力', 'force', 'N', 'derived', 60), ResultVariableDefinition('Fmax', '上限位力', 'force', 'N', 'derived', 70))
DISPLAY = ComponentDisplaySpec(label='MECMAS21 一维质量', library_id='amesim', category_id='mechanical', symbol='amesim_mecmas21', ports=(PortDisplaySpec('port_2', 'left', order=10), PortDisplaySpec('port_1', 'right', order=20)), order=30, parameter_groups=(ParameterGroupDisplaySpec(id='friction', label='摩擦', parameters=('frictionType', 'strib', 'astrib', 'fstick', 'fcoul', 'rvisc', 'wind', 'dvel'), order=10), ParameterGroupDisplaySpec(id='endstops', label='限位', parameters=('discContactOption', 'xmax', 'Kbmax', 'Dbmax', 'Pdmax', 'xmin', 'Kbmin', 'Dbmin', 'Pdmin', 'restcoeff', 'restdvel'), order=20)))
state_size = 2
@@ -94,7 +94,7 @@ class AmesimLstp00a(AlgebraicComponent):
MODEL_TYPE = 'amesim_lstp00a'
MODEL_VERSION = '0.2.0'
PORTS = (PortDefinition.mechanical_translational('port_1'), PortDefinition.mechanical_translational('port_2'))
PARAMETERS = (ParameterDefinition('na', 10.0, label='有效圈数', quantity='dimensionless', unit='', minimum=0.0, minimum_exclusive=True, description='几何刚度模式下使用的弹簧有效圈数。', visible_when=(_LSTP00A_GEOMETRICAL_STIFFNESS,)), ParameterDefinition('gap0', 0.0, label='初始间隙', quantity='length', unit='m', description='两个机械端口开始产生接触力前的初始间隙。'), ParameterDefinition('kcont', 1000000.0, label='接触刚度', quantity='translational_stiffness', unit='N/m', minimum=0.0, description='数值刚度模式下直接指定的接触刚度。', visible_when=(_LSTP00A_NUMERICAL_STIFFNESS,)), ParameterDefinition('G', 85700000000.0, label='剪切模量', quantity='pressure', unit='Pa', minimum=0.0, description='几何刚度模式下的材料剪切模量。', visible_when=(_LSTP00A_GEOMETRICAL_STIFFNESS,)), ParameterDefinition('sdiam', 0.02, label='弹簧直径', quantity='length', unit='m', minimum=0.0, description='几何刚度模式下的弹簧平均直径。', visible_when=(_LSTP00A_GEOMETRICAL_STIFFNESS,)), ParameterDefinition('wdiam', 0.002, label='线径', quantity='length', unit='m', minimum=0.0, description='几何刚度模式下的弹簧线径。', visible_when=(_LSTP00A_GEOMETRICAL_STIFFNESS,)), ParameterDefinition('rcont', 0.0, label='接触阻尼', quantity='translational_damping', unit='N/(m/s)', minimum=0.0, description='接触穿透过程中使用的最大阻尼系数。'), ParameterDefinition('Pdis', 1e-07, label='满阻尼穿透', quantity='length', unit='m', minimum=0.0, description='接触阻尼由零增至全值所需的穿透量。'), ParameterDefinition('stiffmode', 1.0, label='刚度模式', quantity='dimensionless', unit='', minimum=1.0, maximum=2.0, editor='choice', options=(ParameterOption(1.0, '数值刚度'), ParameterOption(2.0, '几何参数')), description='AMESim 原始编码:1 直接使用接触刚度,2 使用弹簧几何参数。当前公开求解器尚未实现由几何参数换算刚度,几何模式仍沿用已保存的接触刚度值。'), ParameterDefinition('discContactOption', 1.0, label='允许负接触力', quantity='dimensionless', unit='', minimum=1.0, maximum=2.0, editor='choice', options=(ParameterOption(1.0, '允许负接触力'), ParameterOption(2.0, '不允许负接触力')), description='AMESim 原始编码:1 保留阻尼项可能产生的负接触力,2 将接触力限制为非负。'))
PARAMETERS = (ParameterDefinition('na', 10.0, label='有效圈数', quantity='dimensionless', unit='', minimum=0.0, minimum_exclusive=True, description='几何刚度模式下使用的弹簧有效圈数。', visible_when=(_LSTP00A_GEOMETRICAL_STIFFNESS,)), ParameterDefinition('gap0', 0.0, label='初始间隙', quantity='length', unit='m', description='两个机械端口开始产生接触力前的初始间隙。'), ParameterDefinition('kcont', 1000000.0, label='接触刚度', quantity='translational_stiffness', unit='N/m', minimum=0.0, description='数值刚度模式下直接指定的接触刚度,执行仿真时必须大于零。', visible_when=(_LSTP00A_NUMERICAL_STIFFNESS,)), ParameterDefinition('G', 85700000000.0, label='剪切模量', quantity='pressure', unit='Pa', minimum=0.0, description='几何刚度模式下的材料剪切模量,允许为零;此时仅保留接触阻尼。', visible_when=(_LSTP00A_GEOMETRICAL_STIFFNESS,)), ParameterDefinition('sdiam', 0.02, label='弹簧直径', quantity='length', unit='m', minimum=0.0, description='几何刚度模式下的弹簧平均直径。', visible_when=(_LSTP00A_GEOMETRICAL_STIFFNESS,)), ParameterDefinition('wdiam', 0.002, label='线径', quantity='length', unit='m', minimum=0.0, description='几何刚度模式下的弹簧线径。', visible_when=(_LSTP00A_GEOMETRICAL_STIFFNESS,)), ParameterDefinition('rcont', 0.0, label='接触阻尼', quantity='translational_damping', unit='N/(m/s)', minimum=0.0, description='接触穿透过程中使用的最大阻尼系数。'), ParameterDefinition('Pdis', 1e-07, label='满阻尼穿透', quantity='length', unit='m', minimum=0.0, description='接触阻尼增至最大值约 63.2% 时的穿透量;阻尼随穿透量按指数规律趋近最大值。'), ParameterDefinition('stiffmode', 1.0, label='刚度模式', quantity='dimensionless', unit='', minimum=1.0, maximum=2.0, editor='choice', options=(ParameterOption(1.0, '数值刚度'), ParameterOption(2.0, '几何参数')), description='AMESim 原始编码:1 直接使用接触刚度,2 使用弹簧几何参数。几何模式按 G×线径⁴/(8×平均直径³×有效圈数) 计算刚度;此时不使用 kcont。'), ParameterDefinition('discContactOption', 1.0, label='允许负接触力', quantity='dimensionless', unit='', minimum=1.0, maximum=2.0, editor='choice', options=(ParameterOption(1.0, '允许负接触力'), ParameterOption(2.0, '不允许负接触力')), description='AMESim 原始编码:1 保留阻尼项可能产生的负接触力,2 将接触力限制为非负。'))
RESULT_VARIABLES = (ResultVariableDefinition('gap', '间隙', 'length', 'm', 'derived', 10), ResultVariableDefinition('penetration', '穿透', 'length', 'm', 'derived', 20), ResultVariableDefinition('force', '接触力', 'force', 'N', 'derived', 30))
DISPLAY = ComponentDisplaySpec(label='LSTP00A 弹性接触', library_id='amesim', category_id='mechanical', symbol='amesim_lstp00a', ports=(PortDisplaySpec('port_1', 'left', order=10), PortDisplaySpec('port_2', 'right', order=20)), order=40, parameter_groups=(ParameterGroupDisplaySpec(id='stiffness', label='刚度', parameters=('kcont', 'G', 'sdiam', 'wdiam', 'na'), order=10), ParameterGroupDisplaySpec(id='contact', label='接触', parameters=('gap0', 'rcont', 'Pdis'), order=20)))
@@ -0,0 +1,74 @@
"""External Amesim encodings, separate from the saved public model contract.
Apply these mappings only when reading Amesim parameters. A browser JSON/XML
already uses public values: applying the mapping again changes its meaning.
Reviewed against the installed Amesim 2404 submodel parameter declarations.
"""
from __future__ import annotations
AMESIM_CHOICE_VALUES = {
("amesim_ud00", "nstages"): {i: i for i in range(1, 9)},
("amesim_ud00", "iscyclic"): {1: 0, 2: 1},
("amesim_lstp00a", "stiffmode"): {1: 1, 2: 2},
("amesim_lstp00a", "discContactOption"): {1: 1, 2: 2},
("amesim_mecmas21", "useFriction"): {1: 1, 2: 2},
("amesim_mecmas21", "stoptype"): {1: 1, 2: 2, 3: 3, 4: 4},
("amesim_mecmas21", "discContactOption"): {1: 1, 2: 2},
("amesim_mecmas21", "strib"): {1: 1, 2: 2},
("amesim_mecmas21", "frictionType"): {1: 1, 2: 2},
("amesim_lmechn1", "sum"): {1: 1},
**{(model, "mode"): {1: 1, 2: 2} for model in
("amesim_pnl0001", "amesim_pnl0002", "amesim_pnl0003")},
**{(model, "flowset"): {1: 1, 2: 2, 3: 3} for model in
("amesim_pnor001", "amesim_pnvo001", "amesim_pnvo001_fixed")},
}
def amesim_choice_to_public(model_type: str, parameter: str, value: float) -> float:
mapping = AMESIM_CHOICE_VALUES.get((model_type, parameter))
if mapping is None:
return value
if value not in mapping:
raise ValueError(f"{model_type}.{parameter}: unknown Amesim encoding {value}; "
f"expected one of {tuple(mapping)}")
return float(mapping[value])
def contact_stiffness(component) -> float:
"""SI constant lowering; the contact force is evaluated in C."""
if int(component.stiffmode) == 1:
if component.kcont <= 0:
raise ValueError(f"{component.name}: LSTP00A 数值刚度模式要求 kcont > 0")
return component.kcont
if component.G < 0:
raise ValueError(f"{component.name}: LSTP00A 几何刚度模式要求 G >= 0")
for name in ("sdiam", "wdiam", "na"):
if getattr(component, name) <= 0:
raise ValueError(f"{component.name}: LSTP00A 几何刚度模式要求 {name} > 0")
return component.G * component.wdiam**4 / (8 * component.sdiam**3 * component.na)
def validate_numerical_semantics(component) -> None:
"""Reject requested behavior that cannot yet be faithfully executed.
Keep this at numerical compilation, so incomplete/future configurations
can still be edited and saved. Disabled friction settings have no effect.
"""
if component.model_type == "amesim_mecmas21" and component.use_friction and int(component.stoptype) != 3:
for name in ('dvel', 'astrib'):
if getattr(component, name) <= 0:
raise ValueError(f"{component.name}: MECMAS21 启用摩擦时 {name} 必须大于零(Amesim 参数约束)")
if component.fcoul > component.fstick:
raise ValueError(f"{component.name}: MECMAS21 库仑摩擦 fcoul 不能大于静摩擦 fstick(Amesim 参数约束)")
if component.model_type == 'amesim_mecmas21':
if int(component.stoptype) in (1, 2, 3) and component.xmin > component.xmax:
raise ValueError(f'{component.name}: MECMAS21 限位要求 xmin <= xmax')
if int(component.stoptype) == 2:
for name in ('Kbmin', 'Kbmax', 'Dbmin', 'Dbmax'):
if getattr(component, name) <= 0:
raise ValueError(f'{component.name}: MECMAS21 弹性限位要求 {name} > 0')
if component.model_type == 'amesim_mecmas21' and int(component.stoptype) == 3 and component.restdvel <= 0:
raise ValueError(f'{component.name}: MECMAS21 恢复碰撞模式要求 restdvel > 0(Amesim 参数约束)')
if component.model_type == "amesim_lstp00a":
contact_stiffness(component)
+71 -17
View File
@@ -5,6 +5,7 @@ from concurrent.futures import ThreadPoolExecutor
from dataclasses import dataclass, field
from hashlib import sha256
import json
import logging
import os
from pathlib import Path
import re
@@ -17,8 +18,20 @@ from typing import Any, Callable
from .compiler import NativeProgram
from .modules import component_modules
from .processes import command_environment, is_driver_launch_failure, is_transient_process_error, LAUNCH_RETRY_DELAYS
from .cache_storage import acquire_cache_lease, touch_cache_entry, prune_cache
LOGGER = logging.getLogger(__name__)
WINDOWS = os.name == 'nt'
class NativeCommandError(RuntimeError):
def __init__(self, message, command, *, exit_code=None, stdout=b'', stderr='', error_type='CalledProcessError', attempts=1):
super().__init__(message)
self.details = {'command': list(command), 'cwd': str(Path.cwd()), 'exitCode': exit_code,
'stdout': stdout.decode('utf-8',errors='replace')[-8000:], 'stderr': stderr[-8000:],
'errorType': error_type, 'attempts': attempts}
ROOT = Path(__file__).resolve().parents[3]
NATIVE = ROOT / "native"
CACHE = ROOT / "app/data/native-builds"
@@ -135,12 +148,38 @@ def _publish(stage: Path, target: Path, key_name: str, key: str, version: int, r
return manifest
def _command(command: list[str], *, log: list[str], timeout: float = 120) -> bytes:
result = subprocess.run(command, capture_output=True, timeout=timeout)
log.append(" ".join(command) + "\n" + result.stderr.decode("utf-8", errors="replace"))
if result.returncode:
raise RuntimeError("Native compilation failed: " + result.stderr.decode("utf-8", errors="replace")[-3000:])
return result.stdout
def _command(command: list[str], *, log: list[str], timeout: float = 120,
require_stdout: bool = False) -> bytes:
environment = command_environment(command[0])
for attempt in range(len(LAUNCH_RETRY_DELAYS) + 1):
try:
result = subprocess.run(command, capture_output=True, timeout=timeout,
stdin=subprocess.DEVNULL, close_fds=True, env=environment,
creationflags=subprocess.CREATE_NO_WINDOW if WINDOWS else 0)
except OSError as exc:
if not is_transient_process_error(exc) or attempt == len(LAUNCH_RETRY_DELAYS):
raise
reason = f'{type(exc).__name__}: {exc}'
else:
stderr = result.stderr.decode('utf-8', errors='replace')
log.append(' '.join(command) + '\n' + stderr)
empty = result.returncode == 0 and require_stdout and not result.stdout.strip()
transient = is_driver_launch_failure(stderr) or (empty and WINDOWS)
if not result.returncode and not empty:
return result.stdout
reason = 'Compiler returned no identification output.' if empty else stderr[-3000:]
if not transient or attempt == len(LAUNCH_RETRY_DELAYS):
prefix = ('GCC 已启动,但编译子进程启动失败(已完成短间隔重试);'
if is_driver_launch_failure(stderr) else 'Native compilation failed: ')
raise NativeCommandError(prefix + reason, command, exit_code=result.returncode,
stdout=result.stdout, stderr=stderr, error_type='EmptyCompilerOutput' if empty else 'CalledProcessError',
attempts=attempt+1)
delay = LAUNCH_RETRY_DELAYS[attempt]
log.append(f'Compiler launch retry {attempt+1}/{len(LAUNCH_RETRY_DELAYS)} after {delay:g}s: {reason}')
LOGGER.warning('Compiler process launch failed; retry %d/%d in %.2fs: %s',
attempt+1, len(LAUNCH_RETRY_DELAYS), delay, reason.strip())
time.sleep(delay)
raise AssertionError('Unreachable compiler retry state')
def toolchain() -> tuple[str, Path, str]:
@@ -156,16 +195,14 @@ def toolchain() -> tuple[str, Path, str]:
base = next((path for path in candidates if (path / "include/cvode/cvode.h").is_file()), candidates[0])
if not (base / "include/cvode/cvode.h").is_file():
raise RuntimeError("SUNDIALS C development files not found; set SUNDIALS_ROOT.")
version = subprocess.run([compiler, "--version"], capture_output=True, text=True, check=True, timeout=15).stdout.splitlines()[0]
if os.name == 'nt':
compiler = str(Path(shutil.which(compiler) or compiler).resolve())
version = _command([compiler, '--version'], log=[], timeout=15, require_stdout=True).decode('utf-8', errors='replace').strip().splitlines()[0]
return compiler, base, version
def build_native(program: NativeProgram, *, cache_dir: Path | None = None,
progress_callback: Callable[[str], None] | None = None) -> NativeBuild:
started = time.perf_counter()
if progress_callback:
progress_callback("native-cache-check")
compiler, sundials, compiler_version = toolchain()
def platform_build_inputs(sundials: Path) -> tuple[list[str], list[Path], list[Path], str]:
"""Share production flags and dependencies with the startup smoke check."""
flags = list(COMPILER_FLAGS)
executable_name = "model.exe" if os.name == "nt" else "model"
if os.name == "nt":
@@ -182,6 +219,23 @@ def build_native(program: NativeProgram, *, cache_dir: Path | None = None,
raise RuntimeError("SUNDIALS static libraries not found; set SUNDIALS_ROOT.")
libraries = [directory / f"libsundials_{name}.a" for name in LIBRARIES]
dlls = []
return flags, libraries, dlls, executable_name
def link_library_arguments(libraries: list[Path]) -> list[str]:
arguments = list(map(str, libraries))
if sys.platform.startswith("linux"):
arguments = ["-Wl,--start-group", *arguments, "-Wl,--end-group"]
return arguments
def build_native(program: NativeProgram, *, cache_dir: Path | None = None,
progress_callback: Callable[[str], None] | None = None) -> NativeBuild:
started = time.perf_counter()
if progress_callback:
progress_callback("native-cache-check")
compiler, sundials, compiler_version = toolchain()
flags, libraries, dlls, executable_name = platform_build_inputs(sundials)
cache = (cache_dir or CACHE).resolve()
cache.mkdir(parents=True, exist_ok=True)
for kind in ("models", "objects"):
@@ -205,7 +259,7 @@ def build_native(program: NativeProgram, *, cache_dir: Path | None = None,
(stage / "model.h").write_text(program.header, encoding="utf-8", newline="\n")
compiler_path = Path(shutil.which(compiler) or compiler).resolve()
compiler_identity = {"version": compiler_version, "binarySha256": _hash(compiler_path),
"target": _command([compiler, "-dumpmachine"], log=logs).decode().strip(),
"target": _command([compiler, "-dumpmachine"], log=logs, require_stdout=True).decode().strip(),
"flags": flags, "platform": sys.platform}
runtime = _runtime_sources(program)
sources = [stage / "model.c", *runtime]
@@ -252,6 +306,7 @@ def build_native(program: NativeProgram, *, cache_dir: Path | None = None,
"unitCount": len(units), "objectCacheHits": 0, "objectCompilations": 0,
"preprocessSeconds": preprocessing_seconds, "compileSeconds": 0.0,
"compileWallSeconds": 0.0, "linkSeconds": 0.0, "modelCacheHit": manifest is not None}
details['compilerLaunchRetries'] = sum(line.startswith('Compiler launch retry ') for line in logs)
if manifest is not None:
if progress_callback:
progress_callback("native-cache-hit")
@@ -302,14 +357,13 @@ def build_native(program: NativeProgram, *, cache_dir: Path | None = None,
details["objectCompilations"] = len(objects) - details["objectCacheHits"]
for item in objects:
logs.extend(item["log"])
link_libraries = list(map(str, libraries))
if sys.platform.startswith("linux"):
link_libraries = ["-Wl,--start-group", *link_libraries, "-Wl,--end-group"]
link_libraries = link_library_arguments(libraries)
if progress_callback:
progress_callback("native-linking")
before_link = time.perf_counter()
_command([compiler, *flags, *[str(item["path"]) for item in objects], *link_libraries,
"-lm", "-o", str(stage / executable_name)], log=logs)
details['compilerLaunchRetries'] = sum(line.startswith('Compiler launch retry ') for line in logs)
details["linkSeconds"] = time.perf_counter() - before_link
if {p.name: _hash(p) for p in libraries + dlls} != dependencies:
raise RuntimeError("Native dependencies changed during linking; retry with a stable toolchain.")
+10 -3
View File
@@ -74,11 +74,16 @@ def compile_native_program(network: SimulationNetwork) -> NativeProgram:
network.validate_port_supplies()
from .extended import catalog_contracts
contracts = catalog_contracts()
from app.simulation.components.amesim.semantics import validate_numerical_semantics
for component in network.components.values():
if type(component) is not contracts.get(component.model_type):
raise NativeCapabilityError(f'{component.name}: no native contract for {component.model_type}')
if component.MODEL_VERSION != SUPPORTED_VERSIONS.get(component.model_type):
raise NativeCapabilityError(f'{component.name}: native kernel version does not match the component contract')
try:
validate_numerical_semantics(component)
except ValueError as exc:
raise NativeCapabilityError(str(exc)) from exc
# Preserve the compact, verified schedule for its supported topology.
# Both paths emit C; this never falls back to Python numerical execution.
try:
@@ -258,9 +263,11 @@ def _compile_storage_anchored_program(network: SimulationNetwork) -> NativeProgr
put(c, "dvol", f"{get(c, 'vol')} <= {_number(c.cvol0 / 100)} ? 0.0 : ({rate})")
volume, rate = get(c, "vol"), get(c, "dvol")
volume_expressions[c.name] = (volume, rate)
# Match the Python constructor: m/U use configured storage volume;
# connected moving volumes subsequently change recovered p/T.
initial_volume = max(base, c.cvol0 / 100) if c.model_type == "amesim_pnch012" else base
# p0/T0 refer to the complete connected volume at the initial positions.
initial_volume = base
if c.model_type == "amesim_pnch012":
initial_volume += sum(p.effective_area * (p.x0 + mass_at(p, "port_5").x0 - mass_at(p, "port_4").x0) for p in connected)
initial_volume = max(initial_volume, c.cvol0 / 100)
init.append(f"if (!native_gas_init({_number(c.p0)}, {_number(c.T0)}, {_number(initial_volume)}, &y[{si(c, 'm')}])) return 0;")
lines.append(f"NativeGas gas_{gi};")
lines.append(f"if (!native_gas_context(properties, y[{si(c, 'm')}], y[{si(c, 'U')}], {volume}, &gas_{gi})) return 0;")
+112 -22
View File
@@ -6,6 +6,7 @@ All thermodynamics, flow/stream closure and derivatives execute in the EXE.
from __future__ import annotations
import json
import math
import re
from importlib import import_module
@@ -14,6 +15,7 @@ from .contracts import SUPPORTED_VERSIONS
from .schedule import Computation, EvaluationSchedule, references
from .tolerances import state_absolute_tolerance
from .jacobian import StateDependencies, expression_inputs
from app.simulation.components.amesim.semantics import contact_stiffness, validate_numerical_semantics
GAS_TYPES = {'amesim_pnch023', 'amesim_pnch012', 'amesim_pnl0001',
@@ -23,6 +25,10 @@ RESISTORS = {'amesim_pnor001', 'amesim_pnvo001_fixed', 'amesim_pnvo001',
'amesim_pnl00r', 'pipe', 'orifice'}
def is_polytropic(c):
return c.model_type in {'amesim_pnl0001', 'amesim_pnl0002', 'amesim_pnl0003'} and int(c.mode) == 1
def catalog_contracts():
from app.simulation.components.amesim.library import LIBRARY as a
from app.simulation.components.experimental.library import LIBRARY as e
@@ -94,6 +100,10 @@ def compile_extended_program(network):
raise NativeCapabilityError(f'{c.name}: no native contract for {c.model_type} / {type(c).__name__}')
if c.MODEL_VERSION != SUPPORTED_VERSIONS.get(c.model_type):
raise NativeCapabilityError(f'{c.name}: native kernel version does not match the component contract')
try:
validate_numerical_semantics(c)
except ValueError as exc:
raise NativeCapabilityError(str(exc)) from exc
variables = tuple(v for c in components for v in c.result_variable_metadata())
if not variables:
raise NativeCapabilityError('Simulation requires a runtime component; medium definitions alone have no outputs')
@@ -131,7 +141,8 @@ def compile_extended_program(network):
mass_groups = {}
for c in components:
if c.model_type in GAS_TYPES:
fields = ('m1', 'U1', 'm2', 'U2') if c.model_type == 'amesim_pnl0003' else ('m', 'U')
thermal = 'T' if is_polytropic(c) else 'U'
fields = ('m1', thermal+'1', 'm2', thermal+'2') if c.model_type == 'amesim_pnl0003' else ('m', thermal)
add_states(c, fields, [0.0]*len(fields))
elif c.model_type == 'amesim_mecmas21':
root = groups.find((c.name, 'port_1'))
@@ -144,6 +155,17 @@ def compile_extended_program(network):
raise NativeCapabilityError('Rigidly connected masses require consistent initial x/v')
for field in ('v', 'x'):
states[c.name, field] = states[ref.name, field]
friction_groups = []
for group in mass_groups.values():
dry = [c for c in group if c.use_friction and int(c.stoptype) != 3 and c.fstick > 0]
if dry:
ref = group[0]
threshold = min(c.dvel if int(c.frictionType) == 2 else 0 for c in dry)
mode = (1 if ref.v0 > 0 else -1) if abs(ref.v0) > threshold else 0
if mode == 0:
initial[states[ref.name, 'v']] = 0.0
add_states(ref, ('_friction_mode',), (mode,))
friction_groups.append((group, dry, threshold))
if len(state_keys)>1024 or len(variables)>16384:
raise NativeCapabilityError('Native model exceeds the 1024-state / 16384-output resource limit')
# Algebraic models use an internal constant state; the public state map stays empty.
@@ -270,13 +292,25 @@ def compile_extended_program(network):
initial_volume=(max(c.cvol0+sum(c.external_volumes.values()),c.cvol0/100)
if kind=='amesim_pnch012' else
c.cvol if kind=='amesim_pnch023' else c.V if kind in ('cylinder','tank') else c.volume)
gas_initializers.append(f'if(!native_medium_init({medium(c)},{num(p0)},{num(T0)},{num(initial_volume)},{int(kind in ("cylinder","tank"))},&y[{states[c.name,"m"+suffix]}])) return 0;')
lines.append(f'if(!native_medium_gas_context(gas_properties,{medium(c)}, {y(c,"m"+suffix)}, {y(c,"U"+suffix)}, {V}, &{gas})) return 0;')
gas_inputs = expression_inputs(f'{y(c,"m"+suffix)}+{y(c,"U"+suffix)}+({V})')
if kind == 'amesim_pnch012':
def initial_x(d, port):
return mass_groups[groups.find(ep(d, port))][0].x0
initial_volume = max(c.cvol0 + sum(c.external_volumes.values()) + sum(
d.effective_area * (d.x0 + initial_x(d, 'port_5') - initial_x(d, 'port_4'))
for d in attached), c.cvol0 / 100)
thermal = ('T' if is_polytropic(c) else 'U') + suffix
if is_polytropic(c):
gas_initializers += [f'{y(c,"m"+suffix)}={num(p0*initial_volume/(c.medium.R_gas*T0))};', f'{y(c,thermal)}={num(T0)};']
gas_function = 'native_polytropic_gas_context'
else:
gas_initializers.append(f'if(!native_medium_init({medium(c)},{num(p0)},{num(T0)},{num(initial_volume)},{int(kind in ("cylinder","tank"))},&y[{states[c.name,"m"+suffix]}])) return 0;')
gas_function = 'native_medium_gas_context'
lines.append(f'if(!{gas_function}(gas_properties,{medium(c)}, {y(c,"m"+suffix)}, {y(c,thermal)}, {V}, &{gas})) return 0;')
gas_inputs = expression_inputs(f'{y(c,"m"+suffix)}+{y(c,thermal)}+({V})')
for field in ('p','T','rho','u','h'):
dependencies.assign(gas+'.'+field, gas_inputs)
for field in ('m', 'U'):
put(c, field+suffix, y(c, field+suffix))
put(c, 'm'+suffix, y(c, 'm'+suffix))
put(c, 'U'+suffix, f'{y(c,"m"+suffix)}*{gas}.u' if is_polytropic(c) else y(c,'U'+suffix))
for field in ('p','T','rho','u','h'):
put(c, field+suffix, gas+'.'+field)
anchored = pnames(c)
@@ -296,6 +330,10 @@ def compile_extended_program(network):
if len(partitions)<2: continue
if any(volume is None for _,volume in partitions.values()) or len({id(c.medium) for c,_ in partitions.values()})!=1:
raise NativeCapabilityError('Direct gas-storage coupling requires compatible fixed pipe compliances; insert a resistance between independent chambers')
laws = {(is_polytropic(c), c.k if is_polytropic(c) else None) for c,_ in partitions.values()}
if len(laws) != 1:
raise NativeCapabilityError('Directly coupled pipe compliances require the same thermodynamic mode and polytropic exponent')
poly = next(iter(laws))[0]
offsets=[];volumes=[]
for (name,half),(c,volume) in partitions.items():
offsets.append(states[name,'m'+(str(half) if half else '')]);volumes.append(volume)
@@ -305,11 +343,12 @@ def compile_extended_program(network):
if max(values)-min(values)>1e-9*max(1,*map(abs,values)):
raise NativeCapabilityError('Ideally coupled pipe compliances require consistent initial pressure and temperature')
total=sum(volumes)
project.append('{ double mass='+ '+'.join(f'y[{i}]' for i in offsets)+',energy='+ '+'.join(f'y[{i+1}]' for i in offsets)+';')
thermal_sum = '+'.join(f'y[{i+1}]*{num(v/total)}' if poly else f'y[{i+1}]' for i,v in zip(offsets,volumes))
project.append('{ double mass='+ '+'.join(f'y[{i}]' for i in offsets)+',thermal='+thermal_sum+';')
for i,volume in zip(offsets,volumes):
project += [f'projected[{i}]=mass*{num(volume/total)};projected[{i+1}]=energy*{num(volume/total)};']
project += [f'projected[{i}]=mass*{num(volume/total)};projected[{i+1}]=thermal*{num(1 if poly else volume/total)};']
project.append('}')
coupled.append((root,offsets,volumes))
coupled.append((root,offsets,volumes,poly))
dependencies.project_states(offsets)
dependencies.project_states([i+1 for i in offsets])
for root, gas in anchor.items():
@@ -379,7 +418,7 @@ def compile_extended_program(network):
for terms,rhs in flow_eq:
known=''.join(f'-({num(v)})*{k}' for k,v in terms.items() if k in flow_known)
reduced.append(({k:v for k,v in terms.items() if k not in flow_known},rhs+known))
free_flows={f'q[{pi[e]}]' for root,_,_ in coupled for e in pneu if groups.find(e)==root}
free_flows={f'q[{pi[e]}]' for root,_,_,_ in coupled for e in pneu if groups.find(e)==root}
flow_bindings=linear_assignments(reduced,unknownq,free_flows)
flow_temporary_count=max((int(key[1:])+1 for key,_ in flow_bindings if key.startswith('b')),default=0)
for key,expr in flow_bindings:
@@ -474,7 +513,7 @@ def compile_extended_program(network):
elif kind=='amesim_lstp00a':
put(c,'gap',f'{num(c.gap0)}+{w(c,"port_2.x")}-{w(c,"port_1.x")}')
put(c,'penetration',f'fmax(-{w(c,"gap")},0)')
put(c,'force',f'native_contact({w(c,"penetration")},{w(c,"port_1.v")}-{w(c,"port_2.v")},{num(c.kcont)},{num(c.rcont)},{num(c.Pdis)},{int(c.discContactOption)})')
put(c,'force',f'native_contact({w(c,"penetration")},{w(c,"port_1.v")}-{w(c,"port_2.v")},{num(contact_stiffness(c))},{num(c.rcont)},{num(c.Pdis)},{int(c.discContactOption)})')
feq += [({w(c,'port_1.f'):1},w(c,'force')),({w(c,'port_2.f'):1},'-'+w(c,'force'))]
elif kind=='amesim_pnrp17':
put(c,'pressure_force',f'({w(c,"port_1.p")}-101300)*{num(c.effective_area)}')
@@ -484,19 +523,56 @@ def compile_extended_program(network):
elif kind=='amesim_mecmas21':
v,x=w(c,'v'),w(c,'x')
put(c,'Fvisc',f'-{num(c.rvisc)}*{v}' if c.use_friction else '0.0')
put(c,'Ffric',f'{v}>0?-{num(c.fcoul)}:({v}<0?{num(c.fcoul)}:0)' if c.use_friction else '0.0')
put(c,'Ffric','0.0')
for field,penetration,velocity,suffix in [('Fmin',num(c.xmin)+'-'+x,'-'+v,'min'),('Fmax',x+'-'+num(c.xmax),v,'max')]:
expr=f'native_limit_force({penetration},{velocity},{num(getattr(c,"Kb"+suffix))},{num(getattr(c,"Db"+suffix))},{num(getattr(c,"Pd"+suffix))},{int(c.discContactOption)})' if int(c.stoptype)==2 else '0.0'
put(c,field,expr)
ref=mass_groups[groups.find(ep(c,'port_1'))][0]
extra=f'{w(c,"Fvisc")}+{w(c,"Ffric")}+{w(c,"Fmin")}-{w(c,"Fmax")}'
if c.use_friction: extra+=f'-{num(c.wind)}*{v}*fabs({v})'
extra += f'+({num(c.mass * 9.80665 * math.sin(math.radians(c.theta)))})'
feq.append(({w(c,'port_1.f'):1,w(c,'port_2.f'):1,w(ref,'a'):-c.mass},f'-({extra})'))
unknownf=[w(c,name+'.f') for c in components for name in mnames(c)]+[w(group[0],'a') for group in mass_groups.values()]
# Keep the same elimination/order while registering its structured bindings.
for target, expression in linear_assignments(feq, unknownf):
mechanical_assignments = linear_assignments(feq, unknownf)
for target, expression in mechanical_assignments:
lines.append(f'{"double " if target.startswith("b") else ""}{target} = {expression};')
dependencies.expression(target, expression)
for index, (group, dry, threshold) in enumerate(friction_groups):
ref = group[0]
drive = f'friction_drive_{index}'
expression = f'{num(sum(c.mass for c in group))}*{w(ref,"a")}'
lines.append(f'double {drive}={expression};')
dependencies.expression(drive, expression)
# Amesim ideal stops take priority over dry friction. A blocked mass
# carries its load through the stop, not through an extra friction force.
blocked = f'friction_blocked_{index}'
ideal = [c for c in group if int(c.stoptype) == 1]
conditions = []
for c in ideal:
x, v = y(ref, 'x'), y(ref, 'v')
conditions += [f'({x}<={num(c.xmin+1e-12*max(abs(c.xmin),1))} && {v}<=1e-12 && {drive}<=0)',
f'({x}>={num(c.xmax-1e-12*max(abs(c.xmax),1))} && {v}>=-1e-12 && {drive}>=0)']
blocked_expression = ' || '.join(conditions) or '0'
lines.append(f'double {blocked}=({blocked_expression});')
dependencies.expression(blocked, blocked_expression)
lines.append(f'{drive}={blocked}?0:{drive};')
dependencies.expression(drive, f'{blocked}?0:{drive}')
lines.append(f'if(friction_drives) friction_drives[{index}]={drive};')
capacity = sum(c.fstick for c in dry)
for c in dry:
put(c, 'Ffric', f'{blocked}?0:native_dry_friction({y(ref,"v")},{drive}*{num(c.fstick/capacity)},'
f'{num(c.fcoul)},{num(c.fstick)},{num(c.astrib)},'
f'{int(int(c.frictionType)==2 and int(c.strib)==2)},{y(ref,"_friction_mode")})')
lines.append(f'dy[{states[ref.name,"_friction_mode"]}]=0;')
dependencies.expression(f'dy[{states[ref.name,"_friction_mode"]}]', '0')
if friction_groups:
# The first linear solve supplies the drive without dry friction. The
# second redistributes actual friction forces through the same network.
# No pressure/flow iteration or mutable RHS mode is introduced.
for target, expression in mechanical_assignments:
lines.append(f'{target}={expression};')
dependencies.expression(target, expression)
for c in components:
for name in mnames(c): assigned.add(c.name+'.'+name+'.f')
if c.model_type=='amesim_lmechn1':
@@ -545,11 +621,13 @@ def compile_extended_program(network):
if kind.startswith('amesim_pnch'):
heat=f'{num(c.kth*c.sth)}*({num(c.extemp)}-{gas}.T)-{gas}.p*({volume_rate[c.name]})'
elif kind.startswith('amesim_pnl') and int(c.mode)!=1:
temp=f'.5*({gases[c.name,1]}.T+{gases[c.name,2]}.T)' if half else gas+'.T'
temp=gas+'.T'
heat=f'{num(c.kth*c.exchange_area/(2 if half else 1))}*({num(c.extemp)}-({temp}))'
lines += [f'dy[{states[c.name,"m"+suffix]}]={mass};',f'dy[{states[c.name,"U"+suffix]}]={energy}+({heat});']
thermal = ('T' if is_polytropic(c) else 'U') + suffix
thermal_rhs = f'{num(c.k-1)}*{gas}.T/({y(c,"m"+suffix)})*({mass})' if is_polytropic(c) else f'{energy}+({heat})'
lines += [f'dy[{states[c.name,"m"+suffix]}]={mass};',f'dy[{states[c.name,thermal]}]={thermal_rhs};']
dependencies.expression(f'dy[{states[c.name,"m"+suffix]}]', mass)
dependencies.expression(f'dy[{states[c.name,"U"+suffix]}]', f'{energy}+({heat})')
dependencies.expression(f'dy[{states[c.name,thermal]}]', thermal_rhs)
if kind.startswith('amesim_pnl'):
diag=[]
if kind=='amesim_pnl0002':
@@ -578,10 +656,11 @@ def compile_extended_program(network):
expr=f'acc[{i}]/{len(diag)}'
put(c,field,f'fmin({expr},64000000)' if field=='ff' else expr)
lines.append('}')
for _,offsets,volumes in coupled:
lines.append('{ double mass='+ '+'.join(f'dy[{i}]' for i in offsets)+',energy='+ '+'.join(f'dy[{i+1}]' for i in offsets)+';')
for _,offsets,volumes,poly in coupled:
thermal_sum = '+'.join(f'dy[{i+1}]*{num(v/sum(volumes))}' if poly else f'dy[{i+1}]' for i,v in zip(offsets,volumes))
lines.append('{ double mass='+ '+'.join(f'dy[{i}]' for i in offsets)+',thermal='+thermal_sum+';')
for i,volume in zip(offsets,volumes):
lines.append(f'dy[{i}]=mass*{num(volume/sum(volumes))};dy[{i+1}]=energy*{num(volume/sum(volumes))};')
lines.append(f'dy[{i}]=mass*{num(volume/sum(volumes))};dy[{i+1}]=thermal*{num(1 if poly else volume/sum(volumes))};')
dependencies.assign(f'dy[{i}]', (f'dy[{j}]' for j in offsets))
dependencies.assign(f'dy[{i+1}]', (f'dy[{j+1}]' for j in offsets))
lines.append('}')
@@ -590,14 +669,21 @@ def compile_extended_program(network):
if not state_keys: lines.append('dy[0]=0;')
jacobian = dependencies.build()
np,ng,nq=max(1,len(pgroups)),max(1,gas_count),max(1,len(pneu))
# Advanced-friction reference runs at 5 N and 7 N confirm Amesim 2404's
# 0.1% breakaway hysteresis. Simple friction uses the exact static threshold.
source='\n'.join(['#include "model.h"','#include <math.h>',*declarations,
f'const NativeFriction model_frictions[{max(1,len(friction_groups))}] = {{'+(','.join(
'{'+str(states[group[0].name,'v'])+','+str(states[group[0].name,'_friction_mode'])+','+num(threshold)+','+
num(sum(c.fstick*(1.001 if int(c.frictionType)==2 else 1) for c in dry))+'}'
for group,dry,threshold in friction_groups) or '{0,0,0,0}')+'};',
f'const NativeStop model_stops[{max(1,len(stops))}] = {{'+(','.join('{'+str(s[0])+','+','.join(num(v) for v in s[1:])+'}' for s in stops) or '{0,0,0,0,0,0,0}')+'};',
'const double model_atol[NSTATES] = {'+','.join(map(state_absolute_tolerance, state_keys or ['dummy']))+'};',
'const char *const model_output_keys[NOUTPUTS] = {'+(','.join(json.dumps(v.key,ensure_ascii=True) for v in variables) or '""')+'};',
*jacobian.source_lines(),
*schedule_helpers,
'int model_init(double *y) {',*[f'y[{i}]={num(v)};' for i,v in enumerate(initial)],*gas_initializers,'return 1;}',
'static int model_eval_internal(double t,const double *y,double *dy,double *w,int canonical) {',
'static int model_eval_internal(double t,const double *y,double *dy,double *w,int canonical,double *friction_drives) {',
'(void)friction_drives;',
f'NativePropertyState property_states[{min(256,max(16,4*gas_count+2*len(components)))}];',
'NativePropertyCache property_cache, *properties=&property_cache;',
'native_properties_init(properties,property_states,sizeof(property_states)/sizeof(property_states[0]));',
@@ -609,8 +695,9 @@ def compile_extended_program(network):
'(void)t;(void)y;(void)w;(void)p;(void)h;(void)q;(void)g;',*lines,
'for(int i=0;i<NSTATES;i++) if(!isfinite(dy[i])) return 0;',
'for(int i=0;i<NOUTPUTS;i++) if(!isfinite(w[i])) return 0;','return 1;}',
'int model_eval(double t,const double *y,double *dy,double *w) {return model_eval_internal(t,y,dy,w,0);}',
'int model_eval_jacobian(double t,const double *y,double *dy,double *w) {return model_eval_internal(t,y,dy,w,1);}',
'int model_eval(double t,const double *y,double *dy,double *w) {return model_eval_internal(t,y,dy,w,0,NULL);}',
'int model_eval_jacobian(double t,const double *y,double *dy,double *w) {return model_eval_internal(t,y,dy,w,1,NULL);}',
'int model_friction_drives(double t,const double *y,double *drives) {double dy[NSTATES],w[NOUTPUTS];return model_eval_internal(t,y,dy,w,0,drives);}',
'double model_next_break(double t,double end) { double result=end;(void)t;',*breaks,'return result;}',''])
header=f'''#ifndef GENERATED_NATIVE_MODEL_H
#define GENERATED_NATIVE_MODEL_H
@@ -618,6 +705,9 @@ def compile_extended_program(network):
#define NSTATES {nstates}
#define NOUTPUTS {max(1,len(variables))}
#define NSTOPS {len(stops)}
#define NFRICTIONS {len(friction_groups)}
extern const NativeFriction model_frictions[{max(1,len(friction_groups))}];
int model_friction_drives(double t,const double *y,double *drives);
extern const NativeStop model_stops[{max(1,len(stops))}];
extern const double model_atol[NSTATES];
extern const char *const model_output_keys[NOUTPUTS];
+1 -1
View File
@@ -19,7 +19,7 @@ _IDENTIFIER = re.compile(r'\b[A-Za-z_]\w*\b')
_NUMBER = re.compile(r'(?<![\w.])(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?')
_FUNCTIONS = frozenset({
'fmax', 'fmin', 'fabs', 'sqrt', 'copysign', 'pow',
'native_signal', 'native_contact', 'native_limit_force',
'native_signal', 'native_contact', 'native_limit_force', 'native_dry_friction',
'native_pipe_flow_context', 'native_pipe_flow_cached_context',
'native_temperature_ph_context', 'native_density_context',
})
+2 -2
View File
@@ -8,13 +8,13 @@ EXPORTS = {
"native_properties_init", "native_temperature_ph_context", "native_density_context",
"native_gas_init", "native_gas", "native_gas_context", "native_medium_init",
"native_density", "native_temperature_ph", "native_viscosity",
"native_medium_gas", "native_medium_gas_context",
"native_medium_gas", "native_medium_gas_context", "native_polytropic_gas_context",
}),
"orifice": frozenset({"native_orifice", "native_orifice_context", "native_medium_orifice", "native_medium_orifice_context"}),
"pipe": frozenset({"native_pipe_resistance", "native_pipe_flow", "native_pipe_flow_context",
"native_pipe_flow_cached", "native_pipe_flow_cached_context",
"native_pipe_diagnostics", "native_pipe_diagnostics_context"}),
"mechanics": frozenset({"native_contact", "native_stop_motion", "native_limit_force"}),
"mechanics": frozenset({"native_contact", "native_stop_motion", "native_limit_force", "native_dry_friction"}),
"signal": frozenset({"native_signal", "native_signal_break"}),
}
DEPENDENCIES = {"orifice": ("properties",), "pipe": ("properties",)}
@@ -0,0 +1,35 @@
"""Portable process setup and classification for the native compiler driver.
MinGW maps different CreateProcess failures to ENOENT. That text does not prove
the compiler/subprogram is missing; never treat a C diagnostic as a launch retry.
"""
from __future__ import annotations
import os
from pathlib import Path
import re
import shutil
IS_WINDOWS = os.name == 'nt'
LAUNCH_RETRY_DELAYS = (0.05, 0.1, 0.2, 0.35, 0.5)
def command_environment(executable: str) -> dict[str, str] | None:
if not IS_WINDOWS:
return None # Preserve the Linux toolchain and environment unchanged.
env = os.environ.copy()
path_key = next((key for key in env if key.upper() == 'PATH'), 'PATH')
directory = str(Path(shutil.which(executable) or executable).resolve().parent)
env[path_key] = directory + os.pathsep + env.get(path_key, '')
return env
def is_driver_launch_failure(stderr: str) -> bool:
# GCC 8 emits "gcc.exe: error: CreateProcess: ..."; other releases add
# "cannot execute 'cc1':". Anchor the driver diagnostic, not source text.
return IS_WINDOWS and bool(re.search(
r'(?im)^(?:[^\r\n]*[/\\])?[\w.+-]*gcc(?:\.exe)?: (?:fatal )?error: (?:cannot execute [^\r\n]*: )?CreateProcess:', stderr))
def is_transient_process_error(exc: OSError) -> bool:
return IS_WINDOWS and getattr(exc, 'winerror', None) in (5, 32, 1450, 1455)
+208
View File
@@ -0,0 +1,208 @@
"""Small, uncached compile/link/run check using the production native toolchain."""
from __future__ import annotations
import os
from pathlib import Path
import shutil
import signal
import subprocess
import tempfile
from threading import Event
from time import monotonic
from typing import Callable
from .build import CACHE, NativeCommandError, link_library_arguments, platform_build_inputs, toolchain
from .processes import command_environment, is_driver_launch_failure
STAGE_MESSAGES = {
"toolchain": "正在查找编译器与 SUNDIALS",
"preparing": "正在准备最小自检程序",
"preprocessing": "正在预处理最小自检程序",
"compiling": "正在编译最小自检程序",
"linking": "正在链接最小自检程序",
"running": "正在运行最小自检程序",
"schema": "正在检查模型 XML 解析环境",
}
OUTPUT_MARKER = "native-runtime-check-ok"
# One smooth scalar equation exercises every library used by production BDF.
# Its known answer is independent of the user's model and cannot hit model cache.
SOURCE = r'''
#include <cvode/cvode.h>
#include <cvode/cvode_ls.h>
#include <nvector/nvector_serial.h>
#include <sunmatrix/sunmatrix_dense.h>
#include <sunlinsol/sunlinsol_dense.h>
#include <math.h>
#include <stdio.h>
static int rhs(sunrealtype t, N_Vector y, N_Vector dy, void *data) {
(void)t; (void)data;
NV_Ith_S(dy,0)=-NV_Ith_S(y,0);
return 0;
}
int main(void) {
SUNContext context=NULL;
N_Vector y=NULL;
SUNMatrix matrix=NULL;
SUNLinearSolver linear=NULL;
void *solver=NULL;
int result=1;
sunrealtype time=0;
if (SUNContext_Create(SUN_COMM_NULL,&context)) goto done;
y=N_VNew_Serial(1,context);
if (!y) goto done;
NV_Ith_S(y,0)=1;
matrix=SUNDenseMatrix(1,1,context);
if (!matrix) goto done;
linear=SUNLinSol_Dense(y,matrix,context);
solver=CVodeCreate(CV_BDF,context);
if (!linear || !solver) goto done;
if (CVodeInit(solver,rhs,0,y)<0 ||
CVodeSStolerances(solver,1e-8,1e-10)<0 ||
CVodeSetLinearSolver(solver,linear,matrix)<0 ||
CVode(solver,0.1,y,&time,CV_NORMAL)<0) goto done;
if (!isfinite(NV_Ith_S(y,0)) || fabs(NV_Ith_S(y,0)-exp(-0.1))>1e-6) goto done;
puts("native-runtime-check-ok");
result=0;
done:
if (result) fputs("Minimal BDF self-check failed.\n",stderr);
if (solver) CVodeFree(&solver);
if (linear) SUNLinSolFree(linear);
if (matrix) SUNMatDestroy(matrix);
if (y) N_VDestroy(y);
if (context) SUNContext_Free(&context);
return result;
}
'''
class CheckCancelled(Exception):
pass
class CheckFailure(RuntimeError):
def __init__(self, message: str, *, command=(), cwd=None, exit_code=None,
stdout="", stderr="", error_type="RuntimeError"):
super().__init__(message)
self.details = {"command": list(command), "cwd": str(cwd) if cwd else None,
"exitCode": exit_code, "stdout": stdout[-8000:],
"stderr": stderr[-8000:], "errorType": error_type}
def _stop_process_tree(process: subprocess.Popen) -> None:
# GCC starts cc1/as/ld. Killing only its driver can leave inherited pipes open.
if os.name == "nt":
try:
subprocess.run(["taskkill", "/PID", str(process.pid), "/T", "/F"],
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, timeout=5,
creationflags=subprocess.CREATE_NO_WINDOW)
except (OSError, subprocess.SubprocessError):
pass
if process.poll() is None:
process.kill()
else:
try:
os.killpg(process.pid, signal.SIGKILL)
except ProcessLookupError:
pass
def _command(command: list[str], directory: Path, stop: Event, timeout: float = 30) -> str:
if stop.is_set():
raise CheckCancelled()
try:
with subprocess.Popen(command, cwd=directory, stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
env=command_environment(command[0]), close_fds=True,
start_new_session=os.name != "nt",
creationflags=subprocess.CREATE_NO_WINDOW if os.name == "nt" else 0) as process:
deadline = monotonic() + timeout
while True:
try:
stdout, stderr = process.communicate(timeout=0.2)
break
except subprocess.TimeoutExpired:
if stop.is_set() or monotonic() >= deadline:
_stop_process_tree(process)
stdout, stderr = process.communicate()
if stop.is_set():
raise CheckCancelled()
raise CheckFailure(f"命令执行超过 {timeout:g} 秒", command=command,
cwd=directory, exit_code=process.returncode,
stdout=stdout.decode("utf-8", errors="replace"),
stderr=stderr.decode("utf-8", errors="replace"),
error_type="TimeoutExpired")
except OSError as exc:
raise CheckFailure(str(exc), command=command, cwd=directory,
error_type=type(exc).__name__) from exc
output = stdout.decode("utf-8", errors="replace")
if process.returncode:
stderr_text = stderr.decode('utf-8', errors='replace')
message = ('GCC 已启动,但无法启动编译子进程;该错误不能直接判定为编译器不存在'
if is_driver_launch_failure(stderr_text) else '命令执行失败')
# Whole-check retries remain owned by warmup (initial check + 5 rounds).
raise CheckFailure(message, command=command, cwd=directory,
exit_code=process.returncode, stdout=output,
stderr=stderr_text,
error_type="CalledProcessError")
return output
def check_native_runtime(progress: Callable[..., None], stop: Event) -> None:
def stage(name: str, **details):
if stop.is_set():
raise CheckCancelled()
progress(stage=name, message=STAGE_MESSAGES[name], **details)
candidate = os.environ.get("SIMULATION_NATIVE_CC") or shutil.which("gcc")
stage("toolchain", compiler=candidate)
try:
compiler, sundials, version = toolchain()
except NativeCommandError as exc:
details = exc.details
failure = CheckFailure(str(exc), command=details['command'], cwd=details['cwd'],
exit_code=details['exitCode'], stdout=details['stdout'], stderr=details['stderr'],
error_type=details['errorType'])
failure.details['attempts'] = details['attempts']
raise failure from exc
except subprocess.SubprocessError as exc:
def decoded(value):
return value.decode("utf-8", errors="replace") if isinstance(value, bytes) else (value or "")
raise CheckFailure(str(exc), command=getattr(exc, "cmd", ()), cwd=Path.cwd(),
exit_code=getattr(exc, "returncode", None),
stdout=decoded(getattr(exc, "stdout", "")),
stderr=decoded(getattr(exc, "stderr", "")),
error_type=type(exc).__name__) from exc
except OSError as exc:
raise CheckFailure(str(exc), command=[candidate, "--version"] if candidate else [],
cwd=Path.cwd(), error_type=type(exc).__name__) from exc
compiler = str(Path(shutil.which(compiler) or compiler).resolve())
stage("preparing", compiler=compiler, compilerVersion=version, sundialsRoot=str(sundials))
flags, libraries, dlls, executable_name = platform_build_inputs(sundials)
# Keep the probe on the build filesystem: Linux /tmp may be mounted noexec.
# Disposable probes never populate or evict the model/object caches.
directory_root = CACHE.parent / "native-runtime-checks"
directory_root.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix="check-", dir=directory_root) as temporary:
directory = Path(temporary).resolve()
source, preprocessed, obj = directory / "check.c", directory / "check.i", directory / "check.o"
executable = directory / executable_name
source.write_text(SOURCE, encoding="utf-8", newline="\n")
stage("preprocessing")
_command([compiler, *flags, "-E", "-P", f"-fmacro-prefix-map={directory}=/generated",
"-I", str(sundials / "include"), str(source), "-o", str(preprocessed)], directory, stop)
stage("compiling")
_command([compiler, *flags, "-x", "cpp-output", "-c", str(preprocessed), "-o", str(obj)], directory, stop)
stage("linking")
_command([compiler, *flags, str(obj), *link_library_arguments(libraries),
"-lm", "-o", str(executable)], directory, stop)
stage("running")
for dll in dlls:
shutil.copyfile(dll, directory / dll.name)
output = _command([str(executable)], directory, stop, timeout=10)
if output.strip() != OUTPUT_MARKER:
raise CheckFailure("最小程序未返回预期的自检成功标记", command=[str(executable)],
cwd=directory, exit_code=0, stdout=output, error_type="UnexpectedOutput")
stage("schema")
from app.system_xml import validate_system_xml_document
validate_system_xml_document(b"<System/>")
+115 -91
View File
@@ -1,115 +1,139 @@
"""Process-local warm-up for the numerical simulation runtime."""
"""Nonblocking startup diagnostics; editor availability is independent of GCC."""
from __future__ import annotations
from dataclasses import asdict, dataclass
from copy import deepcopy
from datetime import datetime, timezone
import logging
import os
from threading import Lock
from threading import Event, Lock, Thread
from time import perf_counter
from typing import Literal
from uuid import uuid4
from .native_codegen.self_test import CheckCancelled, CheckFailure, check_native_runtime
LOGGER = logging.getLogger(__name__)
WarmupStatus = Literal["completed", "failed", "disabled"]
@dataclass(frozen=True)
class SimulationWarmupReport:
status: WarmupStatus
duration_ms: float
error: str | None = None
def as_dict(self) -> dict[str, object]:
return asdict(self)
_WARMUP_LOCK = Lock()
_WARMUP_REPORT: SimulationWarmupReport | None = None
MAX_RETRIES = 5
RETRY_DELAY_SECONDS = 0.5
def simulation_warmup_enabled() -> bool:
raw_value = os.getenv("SIMULATIONAPP_WARMUP", "on").strip().lower()
if raw_value in {"", "1", "true", "yes", "on"}:
value = os.getenv("SIMULATIONAPP_WARMUP", "on").strip().lower()
if value in {"", "1", "true", "yes", "on"}:
return True
if raw_value in {"0", "false", "no", "off"}:
if value in {"0", "false", "no", "off"}:
return False
raise ValueError(
"SIMULATIONAPP_WARMUP must be one of: on, off, true, false, 1, 0."
)
raise ValueError("SIMULATIONAPP_WARMUP must be one of: on, off, true, false, 1, 0.")
class SimulationRuntimeCheck:
"""One bounded background check per application lifespan, with readable stages."""
def __init__(self):
self._lock = Lock()
self._stop = Event()
self._thread: Thread | None = None
self._history: list[dict] = []
self._started = perf_counter()
self._report = {"checkId": uuid4().hex, "status": "pending", "stage": "queued",
"message": "等待仿真环境启动自检", "durationMs": 0.0,
"attempt": 0, "maxRetries": MAX_RETRIES, "maxAttempts": MAX_RETRIES + 1,
"retryDelaySeconds": RETRY_DELAY_SECONDS, "startedAt": None, "finishedAt": None,
"compiler": None, "compilerVersion": None, "sundialsRoot": None,
"error": None, "failure": None}
def _run_native_warmup() -> None:
"""Check the native toolchain and XML schema without running SciPy solvers.
def snapshot(self) -> dict:
with self._lock:
report = deepcopy(self._report)
if report["status"] in {"running", "retrying"}:
report["durationMs"] = (perf_counter() - self._started) * 1000
elif report["status"] in {"completed", "failed", "disabled", "cancelled"}:
# Preserve first-failure evidence without surfacing details mid-retry.
report["attemptHistory"] = deepcopy(self._history)
return report
The model-specific executable is generated when a model is submitted.
"""
from app.simulation.native_codegen.build import toolchain
from app.system_xml import validate_system_xml_document
def _update(self, **values):
with self._lock:
if values.get("status") in {"completed", "failed", "disabled", "cancelled"}:
values["durationMs"] = (perf_counter() - self._started) * 1000
values["finishedAt"] = datetime.now(timezone.utc).isoformat()
self._report.update(values)
toolchain()
validate_system_xml_document(b"<System/>")
def start(self):
with self._lock:
if self._thread is not None:
return
self._started = perf_counter()
self._report.update(status="running", message="正在进行仿真环境首次启动自检",
startedAt=datetime.now(timezone.utc).isoformat())
self._thread = Thread(target=self._run, name="simulation-runtime-check", daemon=True)
self._thread.start()
def _run(self):
for attempt in range(1, MAX_RETRIES + 2):
if self._stop.is_set():
self._cancel()
return
attempt_started = perf_counter()
self._update(status="running", attempt=attempt, stage="queued", error=None, failure=None,
compiler=None, compilerVersion=None, sundialsRoot=None)
def warm_up_simulation_runtime() -> SimulationWarmupReport:
"""Warm one worker exactly once, returning a startup diagnostic report.
def progress(**values):
if attempt > 1 and "message" in values:
values["message"] = f"正在重试(第 {attempt - 1}/{MAX_RETRIES} 轮):{values['message']}"
self._update(**values)
Ordinary warm-up failures are reported but do not prevent the editor and
non-simulation APIs from starting. ``MemoryError`` remains fatal because
continuing a worker under memory exhaustion is unsafe.
"""
try:
if not simulation_warmup_enabled():
self._update(status="disabled", message="仿真环境启动自检已由服务器配置关闭")
return
check_native_runtime(progress, self._stop)
if self._stop.is_set():
raise CheckCancelled()
except CheckCancelled:
self._cancel()
return
except Exception as exc:
failure = deepcopy(exc.details) if isinstance(exc, CheckFailure) else {
"command": [], "cwd": None, "exitCode": None, "stdout": "", "stderr": "",
"errorType": type(exc).__name__,
}
with self._lock:
self._history.append({"attempt": attempt, "status": "failed", "stage": self._report["stage"],
"durationMs": (perf_counter() - attempt_started) * 1000,
"error": str(exc), "failure": failure})
if self._stop.is_set():
self._cancel()
return
if attempt <= MAX_RETRIES:
previous = "首次检测" if attempt == 1 else f"第 {attempt - 1} 轮重试"
message = (f"{previous}未通过,正在准备第 {attempt}/{MAX_RETRIES} 轮重试"
f"(间隔 {RETRY_DELAY_SECONDS:g} 秒);编辑器可正常使用")
self._update(status="retrying", stage="retry-wait", message=message)
LOGGER.warning("%s", message)
# Interruptible on both Windows and Linux; never sleep on the API thread.
if self._stop.wait(RETRY_DELAY_SECONDS):
self._cancel()
return
continue
self._update(status="failed", error=str(exc), failure=failure,
message=f"本次启动自检未通过:首次检测及 {MAX_RETRIES} 轮重试均失败;编辑器仍可使用")
LOGGER.error("Simulation startup self-check exhausted retries at %s; attempt history: %s",
self.snapshot()["stage"], self._history, exc_info=True)
return
else:
with self._lock:
self._history.append({"attempt": attempt, "status": "completed", "stage": "complete",
"durationMs": (perf_counter() - attempt_started) * 1000,
"error": None, "failure": None})
recovery = f"(第 {attempt - 1} 轮重试成功)" if attempt > 1 else ""
self._update(status="completed", stage="complete", error=None, failure=None,
message=f"本次启动自检通过,仿真环境可用{recovery}")
return
global _WARMUP_REPORT
def _cancel(self):
self._update(status="cancelled", message="后端正在关闭,启动自检及重试已取消")
from app.simulation.backends import numeric_engine_name
engine = numeric_engine_name()
with _WARMUP_LOCK:
if _WARMUP_REPORT is not None:
return _WARMUP_REPORT
if not simulation_warmup_enabled():
_WARMUP_REPORT = SimulationWarmupReport(
status="disabled",
duration_ms=0.0,
)
return _WARMUP_REPORT
started = perf_counter()
try:
_run_native_warmup()
except MemoryError:
raise
except Exception as exc:
_WARMUP_REPORT = SimulationWarmupReport(
status="failed",
duration_ms=(perf_counter() - started) * 1000.0,
error=f"{type(exc).__name__}: {exc}",
)
LOGGER.exception("Simulation runtime warm-up failed; startup will continue.")
else:
_WARMUP_REPORT = SimulationWarmupReport(
status="completed",
duration_ms=(perf_counter() - started) * 1000.0,
)
LOGGER.info(
"Simulation runtime warm-up (%s) completed in %.1f ms.",
engine,
_WARMUP_REPORT.duration_ms,
)
return _WARMUP_REPORT
def _reset_simulation_warmup_for_tests() -> None:
global _WARMUP_REPORT
with _WARMUP_LOCK:
_WARMUP_REPORT = None
__all__ = [
"SimulationWarmupReport",
"simulation_warmup_enabled",
"warm_up_simulation_runtime",
]
def close(self):
self._stop.set()
if self._thread is not None:
self._thread.join(timeout=1)