现有组件功能与Amesim组件比较对齐,补全功能;仿真服务启动环境检测,旧版json工程文件适配读取
This commit is contained in:
1 parent
b6c22a54f8
commit
6fc9afe41d
119 files changed
+260114
-351
No files matched your search
@@ -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)
|
||||
@@ -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.")
|
||||
|
||||
@@ -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;")
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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',
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
@@ -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
@@ -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)
|
||||
Reference in new issue
Block a user