"""Component parameters, ports and output definitions; numerical equations execute in C.""" from __future__ import annotations from collections.abc import Mapping from app.simulation.core.base import AlgebraicComponent, DynamicComponent from app.simulation.core.catalog import ComponentDisplaySpec, ParameterGroupDisplaySpec, PortDisplaySpec from app.simulation.core.metadata import ParameterCondition, ParameterDefinition, ParameterOption, ResultVariableDefinition from app.simulation.core.medium import IdealGasMedium from app.simulation.core.ports import PortDefinition _MECMAS21_FRICTION_ENABLED = ParameterCondition('useFriction', (2.0,)) _MECMAS21_NON_RESTITUTION = ParameterCondition('stoptype', (1.0, 2.0, 4.0)) _MECMAS21_LIMITS_ENABLED = ParameterCondition('stoptype', (1.0, 2.0, 3.0)) _MECMAS21_ELASTIC_STOP = ParameterCondition('stoptype', (2.0,)) _MECMAS21_RESTITUTION_STOP = ParameterCondition('stoptype', (3.0,)) _MECMAS21_ADVANCED_FRICTION = ParameterCondition('frictionType', (2.0,)) _MECMAS21_STRIBECK_ENABLED = ParameterCondition('strib', (2.0,)) _LSTP00A_NUMERICAL_STIFFNESS = ParameterCondition('stiffmode', (1.0,)) _LSTP00A_GEOMETRICAL_STIFFNESS = ParameterCondition('stiffmode', (2.0,)) class AmesimF000(AlgebraicComponent): """AMESim F000 zero force source.""" MODEL_TYPE = 'amesim_f000' MODEL_VERSION = '0.1.0' PORTS = (PortDefinition.mechanical_translational('port_1'),) PARAMETERS = () RESULT_VARIABLES = () DISPLAY = ComponentDisplaySpec(label='F000 零力源', library_id='amesim', category_id='mechanical', symbol='amesim_f000', ports=(PortDisplaySpec('port_1', 'right', order=10),), order=10) def __init__(self, name: str) -> None: super().__init__(name=name) self.set_parameter_values({}) self.port_1 = self.register_declared_port('port_1') @classmethod def create(cls, *, name: str, medium: IdealGasMedium, parameters: Mapping[str, float]) -> 'AmesimF000': return cls(name=name) EQUATIONS = ({'id': '__MODEL__:zero_force', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'constitutive', 'variables': ['__MODEL__.port_1.f'], 'role': 'flow'},) class AmesimForc(AlgebraicComponent): """AMESim FORC signal-to-force converter.""" MODEL_TYPE = 'amesim_forc' MODEL_VERSION = '0.2.0' PORTS = (PortDefinition.signal('res', nominal_role='input'), PortDefinition.mechanical_translational('port_2')) PARAMETERS = (ParameterDefinition('direction', 1.0, label='力方向', quantity='dimensionless', unit='', editor='choice', options=(ParameterOption(1.0, '正向'), ParameterOption(-1.0, '反向')), description='显式控制输入信号相对于机械端口正方向的力符号;图标旋转和镜像不会改变该参数。'),) RESULT_VARIABLES = (ResultVariableDefinition('force', '输出力', 'force', 'N', 'signal', 10),) DISPLAY = ComponentDisplaySpec(label='FORC 信号转力', library_id='amesim', category_id='mechanical', symbol='amesim_forc', ports=(PortDisplaySpec('res', 'left', order=10), PortDisplaySpec('port_2', 'right', order=20)), order=20) def __init__(self, name: str, *, direction: float=1.0) -> None: super().__init__(name=name) self.set_parameter_values({'direction': direction}) self.direction = float(direction) self.res = self.register_declared_port('res') self.port_2 = self.register_declared_port('port_2') @classmethod def create(cls, *, name: str, medium: IdealGasMedium, parameters: Mapping[str, float]) -> 'AmesimForc': return cls(name=name, direction=parameters['direction']) EQUATIONS = ({'id': '__MODEL__:signal_force', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'constitutive', 'variables': ['__MODEL__.port_2.f', '__MODEL__.res.signal'], 'role': 'flow'},) class AmesimMecmas21(DynamicComponent): """AMESim MECMAS21 first public one-dimensional translational mass.""" 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.0, '弹性限位'), ParameterOption(3.0, '恢复碰撞'), ParameterOption(4.0, '无限位')), description='AMESim 原始编码:1 为理想限位,2 为弹性限位,3 为带恢复系数的碰撞限位,4 为无限位。'), ParameterDefinition('discContactOption', 1.0, label='允许负接触力', quantity='dimensionless', unit='', editor='choice', options=(ParameterOption(1.0, '允许负接触力'), ParameterOption(2.0, '不允许负接触力')), description='仅在弹性限位中生效。AMESim 原始编码:1 保留阻尼项可能产生的负接触力,2 将接触力限制为非负。', visible_when=(_MECMAS21_ELASTIC_STOP,)), ParameterDefinition('strib', 1.0, label='Stribeck 效应', quantity='dimensionless', unit='', editor='choice', options=(ParameterOption(1.0, '否'), ParameterOption(2.0, '是')), description='AMESim 原始编码:1 表示不使用 Stribeck 效应,2 表示使用。该选项目前用于保留模型配置和控制参数显示;高级 Stribeck 摩擦公式尚未实现。', visible_when=(_MECMAS21_NON_RESTITUTION, _MECMAS21_FRICTION_ENABLED, _MECMAS21_ADVANCED_FRICTION)), ParameterDefinition('frictionType', 1.0, label='摩擦类型', quantity='dimensionless', unit='', editor='choice', options=(ParameterOption(1.0, '简单'), ParameterOption(2.0, '高级')), description='AMESim 原始编码:1 为简单摩擦,2 为高级摩擦。当前公开模型只实现黏性摩擦、风阻和基础库仑摩擦,高级静摩擦与 Stribeck 行为尚未实现。', visible_when=(_MECMAS21_NON_RESTITUTION, _MECMAS21_FRICTION_ENABLED)), ParameterDefinition('v0', 0.0, label='初始速度', quantity='velocity', unit='m/s', description='仿真开始时质量的平动速度。'), ParameterDefinition('x0', 0.0, label='初始位移', quantity='length', unit='m', description='仿真开始时质量的平动位置。')) 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 def __init__(self, name: str, medium: IdealGasMedium, **parameters: float) -> None: super().__init__(name=name) resolved = {definition.name: parameters.get(definition.name, definition.default) for definition in self.PARAMETERS} self.set_parameter_values(resolved) for name, value in resolved.items(): setattr(self, name, float(value)) self.use_friction = int(self.useFriction) == 2 self.port_1 = self.register_declared_port('port_1') self.port_2 = self.register_declared_port('port_2') @classmethod def create(cls, *, name: str, medium: IdealGasMedium, parameters: Mapping[str, float]) -> 'AmesimMecmas21': definitions = {definition.name: definition for definition in cls.PARAMETERS} for integer_name in ('useFriction', 'stoptype', 'discContactOption', 'strib', 'frictionType'): value = float(parameters[integer_name]) if not value.is_integer(): raise ValueError(f'MECMAS21 {integer_name} must be an integer.') message = definitions[integer_name].validation_message(value) if message is not None: raise ValueError(f'MECMAS21 {integer_name} {message}.') return cls(name=name, medium=medium, **dict(parameters)) EQUATIONS = ({'id': '__MODEL__:port_1_x_state', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'state', 'variables': ['__MODEL__.port_1.x'], 'role': 'effort'}, {'id': '__MODEL__:port_1_v_state', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'state', 'variables': ['__MODEL__.port_1.v'], 'role': 'effort'}, {'id': '__MODEL__:port_2_x_state', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'state', 'variables': ['__MODEL__.port_2.x'], 'role': 'effort'}, {'id': '__MODEL__:port_2_v_state', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'state', 'variables': ['__MODEL__.port_2.v'], 'role': 'effort'}) class AmesimLstp00a(AlgebraicComponent): """AMESim LSTP00A first public elastic contact component.""" 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 将接触力限制为非负。')) 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))) def __init__(self, name: str, medium: IdealGasMedium, **parameters: float) -> None: super().__init__(name=name) resolved = {definition.name: parameters.get(definition.name, definition.default) for definition in self.PARAMETERS} self.set_parameter_values(resolved) for name, value in resolved.items(): setattr(self, name, float(value)) self.port_1 = self.register_declared_port('port_1') self.port_2 = self.register_declared_port('port_2') @classmethod def create(cls, *, name: str, medium: IdealGasMedium, parameters: Mapping[str, float]) -> 'AmesimLstp00a': definitions = {definition.name: definition for definition in cls.PARAMETERS} for integer_name in ('stiffmode', 'discContactOption'): value = float(parameters[integer_name]) if not value.is_integer(): raise ValueError(f'LSTP00A {integer_name} must be an integer.') message = definitions[integer_name].validation_message(value) if message is not None: raise ValueError(f'LSTP00A {integer_name} {message}.') return cls(name=name, medium=medium, **dict(parameters)) EQUATIONS = ({'id': '__MODEL__:port_1_contact_force', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'constitutive', 'variables': ['__MODEL__.port_1.f', '__MODEL__.port_1.x', '__MODEL__.port_1.v', '__MODEL__.port_2.x', '__MODEL__.port_2.v'], 'role': 'flow'}, {'id': '__MODEL__:port_2_contact_force', 'owner': 'component', 'ownerId': '__MODEL__', 'relation': 'constitutive', 'variables': ['__MODEL__.port_2.f', '__MODEL__.port_1.x', '__MODEL__.port_1.v', '__MODEL__.port_2.x', '__MODEL__.port_2.v'], 'role': 'flow'}) class AmesimLmechn1(AlgebraicComponent): """AMESim LMECHN1 first public dynamic linear mechanical node.""" MODEL_TYPE = 'amesim_lmechn1' MODEL_VERSION = '0.2.0' PORTS = tuple((PortDefinition.mechanical_translational(f'port_{index}') for index in range(1, 22))) PARAMETERS = (ParameterDefinition('v1', 2.0, label='右侧端口数', quantity='dimensionless', unit='', minimum=1.0, maximum=20.0, description='设置工作区中显示的右侧机械端口数量,最多 20 个。'), ParameterDefinition('sum', 1.0, label='节点求和模式', quantity='dimensionless', unit='', editor='choice', options=(ParameterOption(1.0, '各端口力代数和为零(标准节点)'),))) RESULT_VARIABLES = (ResultVariableDefinition('tforce', '节点合力', 'force', 'N', 'derived', 10),) DISPLAY = ComponentDisplaySpec(label='LMECHN1 线性机械节点', library_id='amesim', category_id='mechanical', symbol='amesim_lmechn1', ports=tuple([PortDisplaySpec(f'port_{index}', 'right', order=index * 10) for index in range(1, 21)] + [PortDisplaySpec('port_21', 'left', order=210)]), order=50) def __init__(self, name: str, medium: IdealGasMedium, *, v1: float=2.0, sum: float=1.0) -> None: super().__init__(name=name) self.set_parameter_values({'v1': v1, 'sum': sum}) self.v1 = int(v1) self.sum = int(sum) for definition in self.PORTS: setattr(self, definition.name, self.register_declared_port(definition.name)) @classmethod def create(cls, *, name: str, medium: IdealGasMedium, parameters: Mapping[str, float]) -> 'AmesimLmechn1': for integer_name in ('v1', 'sum'): if not float(parameters[integer_name]).is_integer(): raise ValueError(f'LMECHN1 {integer_name} must be an integer.') return cls(name=name, medium=medium, v1=parameters['v1'], sum=parameters['sum']) @classmethod def active_port_definitions_for_parameters(cls, parameters: Mapping[str, float]) -> tuple[PortDefinition, ...]: right_port_count = float(parameters['v1']) if not right_port_count.is_integer(): raise ValueError('LMECHN1 v1 must be an integer.') count = int(right_port_count) if count < 1 or count > 20: raise ValueError('LMECHN1 v1 must be between 1 and 20.') return cls.PORTS[:count + 1] @property def active_port_definitions(self) -> tuple[PortDefinition, ...]: return self.PORTS[:self.v1 + 1] @property def active_ports(self) -> tuple[str, ...]: return tuple((definition.name for definition in self.active_port_definitions)) @property def reference_port_name(self) -> str: return f'port_{self.v1 + 1}' @property def required_connection_ports(self) -> tuple[str, ...]: return self.active_ports def equation_definitions(self): from app.simulation.core.equations import EquationDefinition result = [] reference = self.reference_port_name for name in self.active_ports[:-1]: for field in ('x', 'v'): result.append(EquationDefinition(id=f'{self.name}:{name}_{field}_equal', owner='component', owner_id=self.name, relation='equal', variables=(f'{self.name}.{name}.{field}', f'{self.name}.{reference}.{field}'), role='effort')) result.append(EquationDefinition(id=f'{self.name}:force_balance', owner='component', owner_id=self.name, relation='sumToZero', variables=tuple((f'{self.name}.{name}.f' for name in self.active_ports)), role='flow')) return tuple(result)