接入 AMESim 机械接触和节点组件
This commit is contained in:
1 parent
2ddaaeb56a
commit
e87df789b2
8 files changed
+441
-9
No files matched your search
@@ -28,6 +28,8 @@ LIBRARY = ComponentLibrarySpec(
|
|||||||
"app.simulation.components.amesim.mechanical.translational:AmesimF000",
|
"app.simulation.components.amesim.mechanical.translational:AmesimF000",
|
||||||
"app.simulation.components.amesim.mechanical.translational:AmesimForc",
|
"app.simulation.components.amesim.mechanical.translational:AmesimForc",
|
||||||
"app.simulation.components.amesim.mechanical.translational:AmesimMecmas21",
|
"app.simulation.components.amesim.mechanical.translational:AmesimMecmas21",
|
||||||
|
"app.simulation.components.amesim.mechanical.translational:AmesimLstp00a",
|
||||||
|
"app.simulation.components.amesim.mechanical.translational:AmesimLmechn1",
|
||||||
"app.simulation.components.amesim.storage.chambers:AmesimPnch023",
|
"app.simulation.components.amesim.storage.chambers:AmesimPnch023",
|
||||||
"app.simulation.components.amesim.storage.chambers:AmesimPnch012",
|
"app.simulation.components.amesim.storage.chambers:AmesimPnch012",
|
||||||
"app.simulation.components.amesim.flow.orifices:AmesimPnor001",
|
"app.simulation.components.amesim.flow.orifices:AmesimPnor001",
|
||||||
|
|||||||
@@ -291,3 +291,216 @@ class AmesimMecmas21(DynamicComponent):
|
|||||||
"Fmin": self._lower_limit_force(),
|
"Fmin": self._lower_limit_force(),
|
||||||
"Fmax": self._upper_limit_force(),
|
"Fmax": self._upper_limit_force(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AmesimLstp00a(AlgebraicComponent):
|
||||||
|
"""AMESim LSTP00A first public elastic contact component."""
|
||||||
|
|
||||||
|
MODEL_TYPE = "amesim_lstp00a"
|
||||||
|
MODEL_VERSION = "0.1.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),
|
||||||
|
ParameterDefinition("gap0", 0.0, label="初始间隙", quantity="length", unit="m"),
|
||||||
|
ParameterDefinition("kcont", 1.0e6, label="接触刚度", quantity="translational_stiffness", unit="N/m", minimum=0.0),
|
||||||
|
ParameterDefinition("G", 8.57e10, label="剪切模量", quantity="pressure", unit="Pa", minimum=0.0),
|
||||||
|
ParameterDefinition("sdiam", 0.02, label="弹簧直径", quantity="length", unit="m", minimum=0.0),
|
||||||
|
ParameterDefinition("wdiam", 0.002, label="线径", quantity="length", unit="m", minimum=0.0),
|
||||||
|
ParameterDefinition("rcont", 0.0, label="接触阻尼", quantity="translational_damping", unit="N/(m/s)", minimum=0.0),
|
||||||
|
ParameterDefinition("Pdis", 1.0e-7, label="满阻尼穿透", quantity="length", unit="m", minimum=0.0),
|
||||||
|
ParameterDefinition("stiffmode", 1.0, label="刚度模式", quantity="dimensionless", unit="", minimum=0.0),
|
||||||
|
ParameterDefinition("discContactOption", 1.0, label="接触选项", quantity="dimensionless", unit="", minimum=0.0),
|
||||||
|
)
|
||||||
|
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="generic",
|
||||||
|
ports=(
|
||||||
|
PortDisplaySpec("port_1", "left", order=10),
|
||||||
|
PortDisplaySpec("port_2", "right", order=20),
|
||||||
|
),
|
||||||
|
order=40,
|
||||||
|
)
|
||||||
|
|
||||||
|
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":
|
||||||
|
for integer_name in ("stiffmode", "discContactOption"):
|
||||||
|
if not float(parameters[integer_name]).is_integer():
|
||||||
|
raise ValueError(f"LSTP00A {integer_name} must be an integer.")
|
||||||
|
return cls(name=name, medium=medium, **dict(parameters))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gap(self) -> float:
|
||||||
|
return self.gap0 - (self.port_2.x - self.port_1.x)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def penetration(self) -> float:
|
||||||
|
return max(-self.gap, 0.0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def penetration_velocity(self) -> float:
|
||||||
|
return self.port_2.v - self.port_1.v
|
||||||
|
|
||||||
|
@property
|
||||||
|
def contact_force(self) -> float:
|
||||||
|
if self.penetration <= 0.0:
|
||||||
|
return 0.0
|
||||||
|
return max(self.kcont * self.penetration + self.rcont * self.penetration_velocity, 0.0)
|
||||||
|
|
||||||
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
|
force = self.contact_force
|
||||||
|
return (
|
||||||
|
EquationResidual(
|
||||||
|
id=f"{self.name}:port_1_contact_force",
|
||||||
|
owner="component",
|
||||||
|
owner_id=self.name,
|
||||||
|
relation="constitutive",
|
||||||
|
variables=(f"{self.name}.port_1.f", f"{self.name}.port_1.x", f"{self.name}.port_2.x"),
|
||||||
|
role="flow",
|
||||||
|
value=self.port_1.f + force,
|
||||||
|
),
|
||||||
|
EquationResidual(
|
||||||
|
id=f"{self.name}:port_2_contact_force",
|
||||||
|
owner="component",
|
||||||
|
owner_id=self.name,
|
||||||
|
relation="constitutive",
|
||||||
|
variables=(f"{self.name}.port_2.f", f"{self.name}.port_1.x", f"{self.name}.port_2.x"),
|
||||||
|
role="flow",
|
||||||
|
value=self.port_2.f - force,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
def component_result_values(self) -> Mapping[str, float]:
|
||||||
|
return {
|
||||||
|
"gap": self.gap,
|
||||||
|
"penetration": self.penetration,
|
||||||
|
"force": self.contact_force,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class AmesimLmechn1(AlgebraicComponent):
|
||||||
|
"""AMESim LMECHN1 first public dynamic linear mechanical node."""
|
||||||
|
|
||||||
|
MODEL_TYPE = "amesim_lmechn1"
|
||||||
|
MODEL_VERSION = "0.1.0"
|
||||||
|
PORTS = tuple(
|
||||||
|
PortDefinition.mechanical_translational(f"port_{index}")
|
||||||
|
for index in range(1, 10)
|
||||||
|
)
|
||||||
|
PARAMETERS = (
|
||||||
|
ParameterDefinition("v1", 8.0, label="右侧端口数", quantity="dimensionless", unit="", minimum=1.0, maximum=8.0),
|
||||||
|
ParameterDefinition("sum", 1.0, label="节点求和模式", quantity="dimensionless", unit="", minimum=0.0),
|
||||||
|
)
|
||||||
|
RESULT_VARIABLES = (
|
||||||
|
ResultVariableDefinition("tforce", "节点合力", "force", "N", "derived", 10),
|
||||||
|
)
|
||||||
|
DISPLAY = ComponentDisplaySpec(
|
||||||
|
label="LMECHN1 线性机械节点",
|
||||||
|
library_id="amesim",
|
||||||
|
category_id="mechanical",
|
||||||
|
symbol="junction",
|
||||||
|
ports=tuple(
|
||||||
|
[PortDisplaySpec(f"port_{index}", "left", order=index * 10) for index in range(1, 9)]
|
||||||
|
+ [PortDisplaySpec("port_9", "right", order=90)]
|
||||||
|
),
|
||||||
|
order=50,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, name: str, medium: IdealGasMedium, *, v1: float = 8.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"])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_ports(self) -> tuple[str, ...]:
|
||||||
|
return tuple(f"port_{index}" for index in range(1, self.v1 + 1)) + ("port_9",)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def total_force(self) -> float:
|
||||||
|
return sum(self.get_port(port_name).f for port_name in self.active_ports)
|
||||||
|
|
||||||
|
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
|
reference = self.port_9
|
||||||
|
residuals: list[EquationResidual] = []
|
||||||
|
for port_name in self.active_ports[:-1]:
|
||||||
|
port = self.get_port(port_name)
|
||||||
|
residuals.append(
|
||||||
|
EquationResidual(
|
||||||
|
id=f"{self.name}:{port_name}_x_equal",
|
||||||
|
owner="component",
|
||||||
|
owner_id=self.name,
|
||||||
|
relation="equal",
|
||||||
|
variables=(f"{self.name}.{port_name}.x", f"{self.name}.port_9.x"),
|
||||||
|
role="effort",
|
||||||
|
value=port.x - reference.x,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
residuals.append(
|
||||||
|
EquationResidual(
|
||||||
|
id=f"{self.name}:{port_name}_v_equal",
|
||||||
|
owner="component",
|
||||||
|
owner_id=self.name,
|
||||||
|
relation="equal",
|
||||||
|
variables=(f"{self.name}.{port_name}.v", f"{self.name}.port_9.v"),
|
||||||
|
role="effort",
|
||||||
|
value=port.v - reference.v,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
residuals.append(
|
||||||
|
EquationResidual(
|
||||||
|
id=f"{self.name}:force_balance",
|
||||||
|
owner="component",
|
||||||
|
owner_id=self.name,
|
||||||
|
relation="sumToZero",
|
||||||
|
variables=tuple(f"{self.name}.{port_name}.f" for port_name in self.active_ports),
|
||||||
|
role="flow",
|
||||||
|
value=self.total_force,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return tuple(residuals)
|
||||||
|
|
||||||
|
def component_result_values(self) -> Mapping[str, float]:
|
||||||
|
return {"tforce": self.total_force}
|
||||||
@@ -44,8 +44,8 @@
|
|||||||
| `PNGD00` | 1 | 氦气气体定义 | 暂不公开 | medium 配置,不是画布物理组件 | 应映射为系统/介质设置;不能作为普通可连接组件注册。 |
|
| `PNGD00` | 1 | 氦气气体定义 | 暂不公开 | medium 配置,不是画布物理组件 | 应映射为系统/介质设置;不能作为普通可连接组件注册。 |
|
||||||
| `PNRP17` | 8 | 气动活塞与移动体耦合 | 暂不公开 | 内部固定算例;后续跨域组件 | 依赖气动端口和机械端口耦合;需要新增机械域、跨域状态和连接规则。 |
|
| `PNRP17` | 8 | 气动活塞与移动体耦合 | 暂不公开 | 内部固定算例;后续跨域组件 | 依赖气动端口和机械端口耦合;需要新增机械域、跨域状态和连接规则。 |
|
||||||
| `MECMAS21` | 10 | 一维平动质量 | 第一版公开 | `amesim_mecmas21`,`mechanical` | 已接入一维机械端口 `x/v/f`、双端质量状态和基本摩擦/限位项,并跑通零力源与信号力源最小 System XML;完整 AMESim 接触/事件语义仍留后续对齐。 |
|
| `MECMAS21` | 10 | 一维平动质量 | 第一版公开 | `amesim_mecmas21`,`mechanical` | 已接入一维机械端口 `x/v/f`、双端质量状态和基本摩擦/限位项,并跑通零力源与信号力源最小 System XML;完整 AMESim 接触/事件语义仍留后续对齐。 |
|
||||||
| `LMECHN1` | 2 | 动态线性机械节点 | 暂不公开 | 后续机械域组件 | 同上。 |
|
| `LMECHN1` | 2 | 动态线性机械节点 | 第一版公开 | `amesim_lmechn1`,`mechanical` | 已接入 9 端一维机械节点、端口位移/速度等值和节点力平衡,并跑通 `FORC -> LMECHN1 -> MECMAS21` 最小 System XML;端口朝向/符号细节留后续 AMESim baseline 对齐。 |
|
||||||
| `LSTP00A` | 8 | 弹性接触/端止动 | 暂不公开 | 后续机械域组件 | 依赖机械接触和事件/非光滑力模型。 |
|
| `LSTP00A` | 8 | 弹性接触/端止动 | 第一版公开 | `amesim_lstp00a`,`mechanical` | 已接入两端一维机械端口、相对位移/速度接触力和最小 System XML 仿真;当前是连续罚函数基础版,完整 AMESim 事件/非光滑接触语义留后续 baseline 对齐。 |
|
||||||
| `F000` | 16 | 零力源 | 第一版公开 | `amesim_f000`,`mechanical` | 已作为一端机械零力边界公开,约束端口力为 0。 |
|
| `F000` | 16 | 零力源 | 第一版公开 | `amesim_f000`,`mechanical` | 已作为一端机械零力边界公开,约束端口力为 0。 |
|
||||||
| `FORC` | 2 | 信号转力 | 第一版公开 | `amesim_forc`,`mechanical` | 已接入信号输入 `res` 到机械端口力源,可由 `STEP0/UD00` 驱动质量组件。 |
|
| `FORC` | 2 | 信号转力 | 第一版公开 | `amesim_forc`,`mechanical` | 已接入信号输入 `res` 到机械端口力源,可由 `STEP0/UD00` 驱动质量组件。 |
|
||||||
| `STEP0` | 8 | 阶跃信号源 | 第一版公开 | `amesim_step0`,`signals` | 已接入标量信号输出端口和求解时信号传播;当前是基础阶跃,不含更复杂事件调度语义。 |
|
| `STEP0` | 8 | 阶跃信号源 | 第一版公开 | `amesim_step0`,`signals` | 已接入标量信号输出端口和求解时信号传播;当前是基础阶跃,不含更复杂事件调度语义。 |
|
||||||
@@ -79,14 +79,14 @@
|
|||||||
- `STEP0`:已第一版公开。
|
- `STEP0`:已第一版公开。
|
||||||
- `PNVO001`:已第一版公开,保留固定开度变体。
|
- `PNVO001`:已第一版公开,保留固定开度变体。
|
||||||
- `UD00`:已第一版公开。
|
- `UD00`:已第一版公开。
|
||||||
- `MECMAS21`、`F000`、`FORC`:已第一版公开。
|
- `MECMAS21`、`F000`、`FORC`、`LSTP00A`、`LMECHN1`:已第一版公开。
|
||||||
- `LMECHN1`、`LSTP00A`、`PNRP17`
|
- `PNRP17`
|
||||||
|
|
||||||
当前已具备一对一标量信号传播和一维机械端口基础闭合;剩余机械/跨域组件仍需要继续设计多端机械节点、接触事件、气动-机械耦合、XML 协议、前端连线兼容和最小闭合系统测试。
|
当前已具备一对一标量信号传播、一维机械端口基础闭合、弹性接触基础件和 9 端机械节点;剩余跨域组件仍需要继续设计气动-机械耦合、XML 协议、前端连线兼容和最小闭合系统测试。
|
||||||
|
|
||||||
## 下一步执行建议
|
## 下一步执行建议
|
||||||
|
|
||||||
1. 先为 AMESim 公开组件建立独立库,例如 `app.simulation.components.amesim`,不要混入 `experimental`。
|
1. 先为 AMESim 公开组件建立独立库,例如 `app.simulation.components.amesim`,不要混入 `experimental`。
|
||||||
2. 先只登记一个完成度最高的气动代数组件,例如 `PNOR001` 或 `PNL00R`。
|
2. 先只登记一个完成度最高的气动代数组件,例如 `PNOR001` 或 `PNL00R`。
|
||||||
3. 每登记一个模型,都同步补充测试和目录校验,确认 `/api/components/catalog`、System XML 编译和最小仿真都通过。
|
3. 每登记一个模型,都同步补充测试和目录校验,确认 `/api/components/catalog`、System XML 编译和最小仿真都通过。
|
||||||
4. 动态管路 `PNL0001/2/3`、参数化 `PNCH012`、`STEP0`、`UD00`、信号版 `PNVO001` 和机械基础件 `F000/FORC/MECMAS21` 已完成第一版公开接入;下一步转向 `LMECHN1/LSTP00A/PNRP17` 或完整实时耦合。
|
4. 动态管路 `PNL0001/2/3`、参数化 `PNCH012`、`STEP0`、`UD00`、信号版 `PNVO001` 和机械基础件 `F000/FORC/MECMAS21/LSTP00A/LMECHN1` 已完成第一版公开接入;下一步转向 `PNRP17` 或完整实时耦合。
|
||||||
@@ -21,8 +21,6 @@ class AmesimComponentMigrationMatrixTests(unittest.TestCase):
|
|||||||
def test_unsupported_domains_are_not_marked_as_public_candidates(self) -> None:
|
def test_unsupported_domains_are_not_marked_as_public_candidates(self) -> None:
|
||||||
text = MATRIX_PATH.read_text(encoding="utf-8")
|
text = MATRIX_PATH.read_text(encoding="utf-8")
|
||||||
unsupported = {
|
unsupported = {
|
||||||
"LMECHN1",
|
|
||||||
"LSTP00A",
|
|
||||||
"PNRP17",
|
"PNRP17",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from app.simulation.components.amesim.mechanical.translational import (
|
|||||||
AmesimF000,
|
AmesimF000,
|
||||||
AmesimForc,
|
AmesimForc,
|
||||||
AmesimMecmas21,
|
AmesimMecmas21,
|
||||||
|
AmesimLstp00a,
|
||||||
|
AmesimLmechn1,
|
||||||
)
|
)
|
||||||
from app.simulation.core.medium import IdealGasMedium
|
from app.simulation.core.medium import IdealGasMedium
|
||||||
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
|
from app.simulation.registry import COMPONENT_MODEL_REGISTRY
|
||||||
@@ -56,6 +58,70 @@ class AmesimMechanicalPublicComponentTests(unittest.TestCase):
|
|||||||
self.assertAlmostEqual(mass.port_1.x, 0.1)
|
self.assertAlmostEqual(mass.port_1.x, 0.1)
|
||||||
self.assertAlmostEqual(mass.port_2.v, 0.2)
|
self.assertAlmostEqual(mass.port_2.v, 0.2)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def test_lmechn1_ties_active_port_kinematics_and_balances_force(self) -> None:
|
||||||
|
node = AmesimLmechn1("node_1", self.medium, v1=2.0, sum=1.0)
|
||||||
|
node.port_1.x = 0.2
|
||||||
|
node.port_1.v = 0.3
|
||||||
|
node.port_1.f = 4.0
|
||||||
|
node.port_2.x = 0.2
|
||||||
|
node.port_2.v = 0.3
|
||||||
|
node.port_2.f = 6.0
|
||||||
|
node.port_9.x = 0.2
|
||||||
|
node.port_9.v = 0.3
|
||||||
|
node.port_9.f = -10.0
|
||||||
|
|
||||||
|
residuals = node.pressure_flow_equation_residuals()
|
||||||
|
|
||||||
|
self.assertEqual(node.active_ports, ("port_1", "port_2", "port_9"))
|
||||||
|
self.assertAlmostEqual(node.total_force, 0.0)
|
||||||
|
self.assertEqual(len(residuals), 5)
|
||||||
|
self.assertTrue(all(abs(residual.value) <= 1.0e-12 for residual in residuals))
|
||||||
|
self.assertEqual(node.component_result_values(), {"tforce": 0.0})
|
||||||
|
|
||||||
|
def test_lmechn1_registry_rejects_fractional_integer_options(self) -> None:
|
||||||
|
with self.assertRaisesRegex(ValueError, "v1 must be an integer"):
|
||||||
|
COMPONENT_MODEL_REGISTRY["amesim_lmechn1"].create(
|
||||||
|
"node_1",
|
||||||
|
self.medium,
|
||||||
|
{"v1": 2.5},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_lstp00a_generates_opposing_contact_forces(self) -> None:
|
||||||
|
contact = AmesimLstp00a(
|
||||||
|
"contact_1",
|
||||||
|
self.medium,
|
||||||
|
gap0=0.0,
|
||||||
|
kcont=1000.0,
|
||||||
|
rcont=10.0,
|
||||||
|
)
|
||||||
|
contact.port_1.x = 0.0
|
||||||
|
contact.port_2.x = 0.002
|
||||||
|
contact.port_1.v = 0.0
|
||||||
|
contact.port_2.v = 0.1
|
||||||
|
contact.port_1.f = -3.0
|
||||||
|
contact.port_2.f = 3.0
|
||||||
|
|
||||||
|
self.assertAlmostEqual(contact.gap, -0.002)
|
||||||
|
self.assertAlmostEqual(contact.penetration, 0.002)
|
||||||
|
self.assertAlmostEqual(contact.contact_force, 3.0)
|
||||||
|
self.assertEqual(
|
||||||
|
contact.component_result_values(),
|
||||||
|
{"gap": -0.002, "penetration": 0.002, "force": 3.0},
|
||||||
|
)
|
||||||
|
residuals = contact.pressure_flow_equation_residuals()
|
||||||
|
self.assertAlmostEqual(residuals[0].value, 0.0)
|
||||||
|
self.assertAlmostEqual(residuals[1].value, 0.0)
|
||||||
|
|
||||||
|
def test_lstp00a_returns_zero_before_contact(self) -> None:
|
||||||
|
contact = AmesimLstp00a("contact_1", self.medium, gap0=0.001, kcont=1000.0)
|
||||||
|
contact.port_1.x = 0.0
|
||||||
|
contact.port_2.x = 0.0005
|
||||||
|
|
||||||
|
self.assertAlmostEqual(contact.gap, 0.0005)
|
||||||
|
self.assertAlmostEqual(contact.contact_force, 0.0)
|
||||||
|
|
||||||
def test_mecmas21_registry_rejects_fractional_integer_options(self) -> None:
|
def test_mecmas21_registry_rejects_fractional_integer_options(self) -> None:
|
||||||
with self.assertRaisesRegex(ValueError, "useFriction must be an integer"):
|
with self.assertRaisesRegex(ValueError, "useFriction must be an integer"):
|
||||||
COMPONENT_MODEL_REGISTRY["amesim_mecmas21"].create(
|
COMPONENT_MODEL_REGISTRY["amesim_mecmas21"].create(
|
||||||
|
|||||||
@@ -14,6 +14,20 @@ from tests.test_generic_system_xml_simulation import component_node, physical_ed
|
|||||||
from tests.test_system_xml_protocol import physical_port
|
from tests.test_system_xml_protocol import physical_port
|
||||||
|
|
||||||
|
|
||||||
|
LSTP00A_DEFAULTS = {
|
||||||
|
"na": 10.0,
|
||||||
|
"gap0": 0.0,
|
||||||
|
"kcont": 1000.0,
|
||||||
|
"G": 8.57e10,
|
||||||
|
"sdiam": 0.02,
|
||||||
|
"wdiam": 0.002,
|
||||||
|
"rcont": 0.0,
|
||||||
|
"Pdis": 1.0e-7,
|
||||||
|
"stiffmode": 1.0,
|
||||||
|
"discContactOption": 1.0,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
MECMAS21_DEFAULTS = {
|
MECMAS21_DEFAULTS = {
|
||||||
"mass": 2.0,
|
"mass": 2.0,
|
||||||
"fstick": 0.0,
|
"fstick": 0.0,
|
||||||
@@ -130,7 +144,127 @@ def signal_force_mass_project() -> ReactFlowProjectPayload:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def elastic_contact_project() -> ReactFlowProjectPayload:
|
||||||
|
left_parameters = dict(MECMAS21_DEFAULTS)
|
||||||
|
left_parameters["x0"] = 0.0
|
||||||
|
right_parameters = dict(MECMAS21_DEFAULTS)
|
||||||
|
right_parameters["x0"] = 0.001
|
||||||
|
return ReactFlowProjectPayload(
|
||||||
|
name="amesim-mechanical-elastic-contact-smoke",
|
||||||
|
nodes=[
|
||||||
|
component_node("zero_left", "amesim_f000", [mechanical_port("port_1", "right")]),
|
||||||
|
component_node(
|
||||||
|
"mass_left",
|
||||||
|
"amesim_mecmas21",
|
||||||
|
[mechanical_port("port_1", "left"), mechanical_port("port_2", "right")],
|
||||||
|
left_parameters,
|
||||||
|
),
|
||||||
|
component_node(
|
||||||
|
"contact_1",
|
||||||
|
"amesim_lstp00a",
|
||||||
|
[mechanical_port("port_1", "left"), mechanical_port("port_2", "right")],
|
||||||
|
LSTP00A_DEFAULTS,
|
||||||
|
),
|
||||||
|
component_node(
|
||||||
|
"mass_right",
|
||||||
|
"amesim_mecmas21",
|
||||||
|
[mechanical_port("port_1", "left"), mechanical_port("port_2", "right")],
|
||||||
|
right_parameters,
|
||||||
|
),
|
||||||
|
component_node("zero_right", "amesim_f000", [mechanical_port("port_1", "left")]),
|
||||||
|
],
|
||||||
|
edges=[
|
||||||
|
physical_edge("edge-1", "zero_left", "port_1", "mass_left", "port_1"),
|
||||||
|
physical_edge("edge-2", "mass_left", "port_2", "contact_1", "port_1"),
|
||||||
|
physical_edge("edge-3", "contact_1", "port_2", "mass_right", "port_1"),
|
||||||
|
physical_edge("edge-4", "mass_right", "port_2", "zero_right", "port_1"),
|
||||||
|
],
|
||||||
|
simulation={"t_start": 0.0, "t_stop": 0.02, "step": 0.01, "max_step": 0.005, "method": "BDF"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def force_node_mass_project() -> ReactFlowProjectPayload:
|
||||||
|
node_ports = [mechanical_port(f"port_{index}", "left") for index in range(1, 9)]
|
||||||
|
node_ports.append(mechanical_port("port_9", "right"))
|
||||||
|
zero_nodes = [
|
||||||
|
component_node(f"zero_{index}", "amesim_f000", [mechanical_port("port_1", "right")])
|
||||||
|
for index in range(2, 9)
|
||||||
|
]
|
||||||
|
zero_edges = [
|
||||||
|
physical_edge(f"edge-zero-{index}", f"zero_{index}", "port_1", "node_1", f"port_{index}")
|
||||||
|
for index in range(2, 9)
|
||||||
|
]
|
||||||
|
return ReactFlowProjectPayload(
|
||||||
|
name="amesim-mechanical-node-smoke",
|
||||||
|
nodes=[
|
||||||
|
component_node(
|
||||||
|
"step_1",
|
||||||
|
"amesim_step0",
|
||||||
|
[signal_port("out", "output", "right")],
|
||||||
|
{"initial": 10.0, "final": 10.0, "time": 0.0},
|
||||||
|
),
|
||||||
|
component_node(
|
||||||
|
"force_1",
|
||||||
|
"amesim_forc",
|
||||||
|
[signal_port("res", "input", "left"), mechanical_port("port_2", "right")],
|
||||||
|
),
|
||||||
|
component_node("node_1", "amesim_lmechn1", node_ports, {"v1": 8.0, "sum": 1.0}),
|
||||||
|
*zero_nodes,
|
||||||
|
component_node(
|
||||||
|
"mass_1",
|
||||||
|
"amesim_mecmas21",
|
||||||
|
[mechanical_port("port_1", "left"), mechanical_port("port_2", "right")],
|
||||||
|
MECMAS21_DEFAULTS,
|
||||||
|
),
|
||||||
|
component_node("zero_right", "amesim_f000", [mechanical_port("port_1", "left")]),
|
||||||
|
],
|
||||||
|
edges=[
|
||||||
|
signal_edge("signal-1", "step_1", "out", "force_1", "res"),
|
||||||
|
physical_edge("edge-force", "force_1", "port_2", "node_1", "port_1"),
|
||||||
|
*zero_edges,
|
||||||
|
physical_edge("edge-mass", "node_1", "port_9", "mass_1", "port_1"),
|
||||||
|
physical_edge("edge-right", "mass_1", "port_2", "zero_right", "port_1"),
|
||||||
|
],
|
||||||
|
simulation={"t_start": 0.0, "t_stop": 0.02, "step": 0.01, "max_step": 0.005, "method": "BDF"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class AmesimMechanicalXmlTests(unittest.TestCase):
|
class AmesimMechanicalXmlTests(unittest.TestCase):
|
||||||
|
|
||||||
|
|
||||||
|
def test_force_node_mass_project_compiles_and_simulates(self) -> None:
|
||||||
|
xml = build_reactflow_system_xml(force_node_mass_project())
|
||||||
|
report = validate_system_xml_document(xml)
|
||||||
|
|
||||||
|
self.assertTrue(report.valid, report.as_dict())
|
||||||
|
network = compile_reactflow_network(force_node_mass_project())
|
||||||
|
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
|
||||||
|
result = run_system_xml_simulation(xml)
|
||||||
|
|
||||||
|
self.assertTrue(result["success"], result["message"])
|
||||||
|
self.assertEqual(result["series"]["step_1.out.signal"], [10.0, 10.0, 10.0])
|
||||||
|
self.assertEqual(result["series"]["force_1.res.signal"], [10.0, 10.0, 10.0])
|
||||||
|
self.assertAlmostEqual(result["series"]["mass_1.a"][0], 5.0)
|
||||||
|
self.assertGreater(result["series"]["mass_1.v"][-1], 0.0)
|
||||||
|
self.assertGreater(result["series"]["mass_1.x"][-1], 0.0)
|
||||||
|
|
||||||
|
def test_elastic_contact_project_compiles_and_simulates(self) -> None:
|
||||||
|
xml = build_reactflow_system_xml(elastic_contact_project())
|
||||||
|
report = validate_system_xml_document(xml)
|
||||||
|
|
||||||
|
self.assertTrue(report.valid, report.as_dict())
|
||||||
|
network = compile_reactflow_network(elastic_contact_project())
|
||||||
|
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
|
||||||
|
result = run_system_xml_simulation(xml)
|
||||||
|
|
||||||
|
self.assertTrue(result["success"], result["message"])
|
||||||
|
self.assertEqual(result["series"]["time"], [0.0, 0.01, 0.02])
|
||||||
|
self.assertAlmostEqual(result["series"]["contact_1.force"][0], 1.0)
|
||||||
|
self.assertAlmostEqual(result["series"]["mass_left.a"][0], 0.5)
|
||||||
|
self.assertAlmostEqual(result["series"]["mass_right.a"][0], -0.5)
|
||||||
|
self.assertGreater(result["series"]["mass_left.x"][-1], 0.0)
|
||||||
|
self.assertLess(result["series"]["mass_right.x"][-1], 0.001)
|
||||||
|
|
||||||
def test_zero_force_mechanical_project_compiles_and_simulates(self) -> None:
|
def test_zero_force_mechanical_project_compiles_and_simulates(self) -> None:
|
||||||
xml = build_reactflow_system_xml(zero_force_mass_project())
|
xml = build_reactflow_system_xml(zero_force_mass_project())
|
||||||
report = validate_system_xml_document(xml)
|
report = validate_system_xml_document(xml)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class ComponentCatalogTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(library["label"], "AMESim 组件库")
|
self.assertEqual(library["label"], "AMESim 组件库")
|
||||||
self.assertEqual([category["id"] for category in library["categories"]], ["storage", "flow", "junctions", "boundary", "signals", "mechanical"])
|
self.assertEqual([category["id"] for category in library["categories"]], ["storage", "flow", "junctions", "boundary", "signals", "mechanical"])
|
||||||
self.assertEqual(set(components), {"amesim_pnpl01", "amesim_step0", "amesim_ud00", "amesim_f000", "amesim_forc", "amesim_mecmas21", "amesim_pnch023", "amesim_pnch012", "amesim_pnor001", "amesim_pnvo001_fixed", "amesim_pnvo001", "amesim_pnl00r", "amesim_pnl0001", "amesim_pnl0002", "amesim_pnl0003", "amesim_pn3node2", "amesim_p4node2"})
|
self.assertEqual(set(components), {"amesim_pnpl01", "amesim_step0", "amesim_ud00", "amesim_f000", "amesim_forc", "amesim_mecmas21", "amesim_lstp00a", "amesim_lmechn1", "amesim_pnch023", "amesim_pnch012", "amesim_pnor001", "amesim_pnvo001_fixed", "amesim_pnvo001", "amesim_pnl00r", "amesim_pnl0001", "amesim_pnl0002", "amesim_pnl0003", "amesim_pn3node2", "amesim_p4node2"})
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
[port["name"] for port in components["amesim_p4node2"]["ports"]],
|
[port["name"] for port in components["amesim_p4node2"]["ports"]],
|
||||||
["port_1", "port_2", "port_3", "port_4"],
|
["port_1", "port_2", "port_3", "port_4"],
|
||||||
@@ -112,6 +112,21 @@ class ComponentCatalogTests(unittest.TestCase):
|
|||||||
self.assertEqual([port["name"] for port in components["amesim_mecmas21"]["ports"]], ["port_1", "port_2"])
|
self.assertEqual([port["name"] for port in components["amesim_mecmas21"]["ports"]], ["port_1", "port_2"])
|
||||||
self.assertEqual(mecmas_parameters["mass"]["unit"], "kg")
|
self.assertEqual(mecmas_parameters["mass"]["unit"], "kg")
|
||||||
self.assertEqual(mecmas_parameters["Kbmin"]["unit"], "N/m")
|
self.assertEqual(mecmas_parameters["Kbmin"]["unit"], "N/m")
|
||||||
|
lstp_parameters = {
|
||||||
|
parameter["name"]: parameter
|
||||||
|
for parameter in components["amesim_lstp00a"]["parameters"]
|
||||||
|
}
|
||||||
|
self.assertEqual(components["amesim_lstp00a"]["category"]["id"], "mechanical")
|
||||||
|
self.assertEqual([port["name"] for port in components["amesim_lstp00a"]["ports"]], ["port_1", "port_2"])
|
||||||
|
self.assertEqual(lstp_parameters["gap0"]["unit"], "m")
|
||||||
|
self.assertEqual(lstp_parameters["kcont"]["unit"], "N/m")
|
||||||
|
lmechn_parameters = {
|
||||||
|
parameter["name"]: parameter
|
||||||
|
for parameter in components["amesim_lmechn1"]["parameters"]
|
||||||
|
}
|
||||||
|
self.assertEqual(components["amesim_lmechn1"]["category"]["id"], "mechanical")
|
||||||
|
self.assertEqual([port["name"] for port in components["amesim_lmechn1"]["ports"]], [f"port_{index}" for index in range(1, 10)])
|
||||||
|
self.assertEqual(lmechn_parameters["v1"]["maximum"], 8.0)
|
||||||
self.assertEqual(components["amesim_pnvo001"]["category"]["id"], "flow")
|
self.assertEqual(components["amesim_pnvo001"]["category"]["id"], "flow")
|
||||||
self.assertEqual([port["name"] for port in components["amesim_pnvo001"]["ports"]], ["res", "port_2", "port_3"])
|
self.assertEqual([port["name"] for port in components["amesim_pnvo001"]["ports"]], ["res", "port_2", "port_3"])
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ class ComponentRegistryTests(unittest.TestCase):
|
|||||||
"amesim_f000",
|
"amesim_f000",
|
||||||
"amesim_forc",
|
"amesim_forc",
|
||||||
"amesim_mecmas21",
|
"amesim_mecmas21",
|
||||||
|
"amesim_lstp00a",
|
||||||
|
"amesim_lmechn1",
|
||||||
"amesim_pnch023",
|
"amesim_pnch023",
|
||||||
"amesim_pnch012",
|
"amesim_pnch012",
|
||||||
"amesim_pnor001",
|
"amesim_pnor001",
|
||||||
@@ -66,6 +68,8 @@ class ComponentRegistryTests(unittest.TestCase):
|
|||||||
self.assertEqual(models["amesim_f000"].library.id, "amesim")
|
self.assertEqual(models["amesim_f000"].library.id, "amesim")
|
||||||
self.assertEqual(models["amesim_forc"].library.id, "amesim")
|
self.assertEqual(models["amesim_forc"].library.id, "amesim")
|
||||||
self.assertEqual(models["amesim_mecmas21"].library.id, "amesim")
|
self.assertEqual(models["amesim_mecmas21"].library.id, "amesim")
|
||||||
|
self.assertEqual(models["amesim_lstp00a"].library.id, "amesim")
|
||||||
|
self.assertEqual(models["amesim_lmechn1"].library.id, "amesim")
|
||||||
self.assertEqual(models["amesim_pnch023"].library.id, "amesim")
|
self.assertEqual(models["amesim_pnch023"].library.id, "amesim")
|
||||||
self.assertEqual(models["amesim_pnch012"].library.id, "amesim")
|
self.assertEqual(models["amesim_pnch012"].library.id, "amesim")
|
||||||
self.assertEqual(models["amesim_pnor001"].library.id, "amesim")
|
self.assertEqual(models["amesim_pnor001"].library.id, "amesim")
|
||||||
|
|||||||
Reference in new issue
Block a user