整合求解器活动监控与步长回归证据
同步远端 PNL0003 诊断和大采样网格能力,语义合并活动感知的 60 秒真停滞判定与旧后端 15 分钟兼容兜底。 纳管热路径优化、15 单元运行证据、浏览器与 API 报告,并补充北京时间更新日志和遗留问题。
This commit is contained in:
1 parent
c19cf77aee
commit
e18399c022
46 files changed
+181589
-170
No files matched your search
@@ -1,9 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from math import isfinite
|
||||
import os
|
||||
from typing import Callable, Literal, Mapping, Sequence
|
||||
|
||||
from app.simulation.components.amesim.mechanical.translational import (
|
||||
AmesimLstp00a,
|
||||
AmesimMecmas21,
|
||||
)
|
||||
from app.simulation.core.base import DynamicComponent
|
||||
@@ -12,15 +15,114 @@ from app.simulation.systems.network import SimulationNetwork
|
||||
|
||||
|
||||
ConstraintMode = Literal["uninitialized", "free", "lower", "upper"]
|
||||
MechanicalAbsoluteToleranceMode = Literal["legacy", "contact-aware-v1"]
|
||||
DenseState = Callable[[float], Sequence[float]]
|
||||
|
||||
|
||||
MECHANICAL_ATOL_MODE_ENVIRONMENT_VARIABLE = (
|
||||
"SIMULATION_MECHANICAL_ATOL_MODE"
|
||||
)
|
||||
|
||||
|
||||
def _requested_mechanical_absolute_tolerance_mode(
|
||||
) -> MechanicalAbsoluteToleranceMode:
|
||||
value = os.environ.get(
|
||||
MECHANICAL_ATOL_MODE_ENVIRONMENT_VARIABLE,
|
||||
"legacy",
|
||||
).strip().lower()
|
||||
if value == "legacy":
|
||||
return "legacy"
|
||||
if value in {"contact-aware-v1", "contact_aware_v1", "contact-aware"}:
|
||||
return "contact-aware-v1"
|
||||
raise ValueError(
|
||||
f"{MECHANICAL_ATOL_MODE_ENVIRONMENT_VARIABLE} must be "
|
||||
"'legacy' or 'contact-aware-v1'."
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MechanicalToleranceGroupPlan:
|
||||
"""One rigid-coordinate group's state tolerances and proof result."""
|
||||
|
||||
components: tuple[str, ...]
|
||||
contacts: tuple[str, ...]
|
||||
eligible: bool
|
||||
reason: str
|
||||
velocity_atol: float
|
||||
position_atol: float
|
||||
minimum_dvel: float | None
|
||||
minimum_contact_damping_length: float | None
|
||||
minimum_damping_strength_ratio: float | None
|
||||
minimum_force_limited_velocity_atol: float | None
|
||||
|
||||
def as_dict(self) -> dict[str, object]:
|
||||
return {
|
||||
"components": list(self.components),
|
||||
"contacts": list(self.contacts),
|
||||
"eligible": self.eligible,
|
||||
"reason": self.reason,
|
||||
"velocityAtol": self.velocity_atol,
|
||||
"positionAtol": self.position_atol,
|
||||
"minimumDvel": self.minimum_dvel,
|
||||
"minimumContactDampingLength": (
|
||||
self.minimum_contact_damping_length
|
||||
),
|
||||
"minimumDampingStrengthRatio": (
|
||||
self.minimum_damping_strength_ratio
|
||||
),
|
||||
"minimumForceLimitedVelocityAtol": (
|
||||
self.minimum_force_limited_velocity_atol
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class MechanicalAbsoluteTolerancePlan:
|
||||
"""State-aligned absolute tolerances with auditable group proofs."""
|
||||
|
||||
mode: MechanicalAbsoluteToleranceMode
|
||||
default_atol: float
|
||||
legacy_mechanical_atol: float
|
||||
values: tuple[float, ...]
|
||||
groups: tuple[MechanicalToleranceGroupPlan, ...]
|
||||
|
||||
def as_dict(self) -> dict[str, object]:
|
||||
legacy_value = min(
|
||||
self.default_atol,
|
||||
self.legacy_mechanical_atol,
|
||||
)
|
||||
relaxed_groups = tuple(
|
||||
group
|
||||
for group in self.groups
|
||||
if group.velocity_atol > legacy_value
|
||||
)
|
||||
return {
|
||||
"mode": self.mode,
|
||||
"defaultAtol": self.default_atol,
|
||||
"legacyMechanicalAtol": self.legacy_mechanical_atol,
|
||||
"stateCount": len(self.values),
|
||||
"groupCount": len(self.groups),
|
||||
"eligibleGroupCount": sum(group.eligible for group in self.groups),
|
||||
"relaxedVelocityGroupCount": len(relaxed_groups),
|
||||
"relaxedVelocityStateCount": len(relaxed_groups),
|
||||
"relaxedPositionStateCount": 0,
|
||||
"minimumEffectiveAtol": (
|
||||
min(self.values) if self.values else None
|
||||
),
|
||||
"maximumEffectiveAtol": (
|
||||
max(self.values) if self.values else None
|
||||
),
|
||||
"groups": [group.as_dict() for group in self.groups],
|
||||
}
|
||||
|
||||
|
||||
@dataclass
|
||||
class MechanicalConstraintGroup:
|
||||
"""MECMAS21 inertias that share one rigid translational coordinate."""
|
||||
|
||||
components: tuple[AmesimMecmas21, ...]
|
||||
mode: ConstraintMode = "uninitialized"
|
||||
contact_components: tuple[AmesimLstp00a, ...] = ()
|
||||
|
||||
@property
|
||||
def representative(self) -> AmesimMecmas21:
|
||||
@@ -361,9 +463,52 @@ class MechanicalStateReducer:
|
||||
roots = (find("x", first_port), find("v", first_port))
|
||||
masses_by_roots.setdefault(roots, []).append(component)
|
||||
|
||||
contacts_by_roots: dict[
|
||||
tuple[tuple[str, str], tuple[str, str]],
|
||||
dict[str, AmesimLstp00a],
|
||||
] = {}
|
||||
for component in self.network.components.values():
|
||||
if not isinstance(component, AmesimLstp00a):
|
||||
continue
|
||||
contact_roots = tuple(
|
||||
(
|
||||
find("x", (component.name, definition.name)),
|
||||
find("v", (component.name, definition.name)),
|
||||
)
|
||||
for definition in component.active_port_definitions
|
||||
if (
|
||||
definition.kind == "physical"
|
||||
and definition.domain == "mechanical"
|
||||
)
|
||||
)
|
||||
if (
|
||||
len({roots[0] for roots in contact_roots}) < 2
|
||||
or len({roots[1] for roots in contact_roots}) < 2
|
||||
):
|
||||
# A compliant contact whose two ports resolve to the same
|
||||
# rigid coordinate cannot damp that coordinate. Treating the
|
||||
# self-loop as proof would relax an unrelated velocity state.
|
||||
continue
|
||||
for definition in component.active_port_definitions:
|
||||
if (
|
||||
definition.kind != "physical"
|
||||
or definition.domain != "mechanical"
|
||||
):
|
||||
continue
|
||||
endpoint = (component.name, definition.name)
|
||||
roots = (find("x", endpoint), find("v", endpoint))
|
||||
contacts_by_roots.setdefault(roots, {})[
|
||||
component.name
|
||||
] = component
|
||||
|
||||
return tuple(
|
||||
MechanicalConstraintGroup(tuple(components))
|
||||
for components in masses_by_roots.values()
|
||||
MechanicalConstraintGroup(
|
||||
components=tuple(components),
|
||||
contact_components=tuple(
|
||||
contacts_by_roots.get(roots, {}).values()
|
||||
),
|
||||
)
|
||||
for roots, components in masses_by_roots.items()
|
||||
)
|
||||
|
||||
def _build_state_entries(self) -> tuple[StateEntry, ...]:
|
||||
@@ -391,26 +536,253 @@ class MechanicalStateReducer:
|
||||
def has_state_events(self) -> bool:
|
||||
return any(group.discrete_endstop_components for group in self.groups)
|
||||
|
||||
def absolute_tolerance_plan(
|
||||
self,
|
||||
default: float,
|
||||
*,
|
||||
mechanical: float = 1.0e-12,
|
||||
mode: MechanicalAbsoluteToleranceMode | None = None,
|
||||
) -> MechanicalAbsoluteTolerancePlan:
|
||||
"""Compile state tolerances without weakening non-smooth coordinates.
|
||||
|
||||
A scalar ``1e-8`` absolute tolerance makes SciPy perturb a zero-valued
|
||||
endstop position across the much smaller unilateral boundary band while
|
||||
constructing finite-difference Jacobians. Positions and ideal endstop
|
||||
states therefore retain the legacy machine-scale tolerance.
|
||||
|
||||
Strongly damped, compliant LSTP contact can instead drive a *free*
|
||||
velocity close to zero for hundreds of accepted steps. Only a
|
||||
compile-proven smooth-contact group may use the bounded velocity floor;
|
||||
the contact position coordinate remains unchanged.
|
||||
"""
|
||||
default_atol = float(default)
|
||||
mechanical_atol = float(mechanical)
|
||||
if not isfinite(default_atol) or default_atol <= 0.0:
|
||||
raise ValueError(
|
||||
"default absolute tolerance must be finite and positive."
|
||||
)
|
||||
if not isfinite(mechanical_atol) or mechanical_atol <= 0.0:
|
||||
raise ValueError(
|
||||
"mechanical absolute tolerance must be finite and positive."
|
||||
)
|
||||
selected_mode = mode or _requested_mechanical_absolute_tolerance_mode()
|
||||
if selected_mode not in {"legacy", "contact-aware-v1"}:
|
||||
raise ValueError(
|
||||
"mechanical absolute tolerance mode must be 'legacy' or "
|
||||
"'contact-aware-v1'."
|
||||
)
|
||||
|
||||
legacy_atol = min(default_atol, mechanical_atol)
|
||||
values: list[float] = []
|
||||
group_plans: list[MechanicalToleranceGroupPlan] = []
|
||||
for entry in self.state_entries:
|
||||
if not isinstance(entry, MechanicalConstraintGroup):
|
||||
values.extend([default_atol] * entry.state_size)
|
||||
continue
|
||||
|
||||
components = entry.components
|
||||
contacts = entry.contact_components
|
||||
positive_dvel = tuple(
|
||||
float(component.dvel)
|
||||
for component in components
|
||||
if isfinite(float(component.dvel))
|
||||
and float(component.dvel) > 0.0
|
||||
)
|
||||
positive_pdis = tuple(
|
||||
float(contact.Pdis)
|
||||
for contact in contacts
|
||||
if isfinite(float(contact.Pdis))
|
||||
and float(contact.Pdis) > 0.0
|
||||
)
|
||||
minimum_dvel = min(positive_dvel, default=None)
|
||||
minimum_pdis = min(positive_pdis, default=None)
|
||||
contact_scale_valid = True
|
||||
contact_force_velocity_limits_list: list[float] = []
|
||||
damping_strength_ratios_list: list[float] = []
|
||||
if selected_mode == "contact-aware-v1" and minimum_dvel is not None:
|
||||
for contact in contacts:
|
||||
stiffness = float(contact.kcont)
|
||||
damping_length = float(contact.Pdis)
|
||||
damping = float(contact.rcont)
|
||||
if not (
|
||||
isfinite(stiffness)
|
||||
and stiffness > 0.0
|
||||
and isfinite(damping_length)
|
||||
and damping_length > 0.0
|
||||
and isfinite(damping)
|
||||
and damping > 0.0
|
||||
):
|
||||
contact_scale_valid = False
|
||||
continue
|
||||
elastic_force_scale = stiffness * damping_length
|
||||
damping_force_scale = damping * minimum_dvel
|
||||
if not (
|
||||
isfinite(elastic_force_scale)
|
||||
and elastic_force_scale > 0.0
|
||||
and isfinite(damping_force_scale)
|
||||
and damping_force_scale > 0.0
|
||||
):
|
||||
contact_scale_valid = False
|
||||
continue
|
||||
force_velocity_limit = (
|
||||
1.0e-3 * elastic_force_scale / damping
|
||||
)
|
||||
damping_strength_ratio = (
|
||||
damping_force_scale / elastic_force_scale
|
||||
)
|
||||
if not (
|
||||
isfinite(force_velocity_limit)
|
||||
and force_velocity_limit > 0.0
|
||||
and isfinite(damping_strength_ratio)
|
||||
and damping_strength_ratio > 0.0
|
||||
):
|
||||
contact_scale_valid = False
|
||||
continue
|
||||
contact_force_velocity_limits_list.append(
|
||||
force_velocity_limit
|
||||
)
|
||||
damping_strength_ratios_list.append(
|
||||
damping_strength_ratio
|
||||
)
|
||||
contact_force_velocity_limits = tuple(
|
||||
contact_force_velocity_limits_list
|
||||
)
|
||||
minimum_force_velocity_atol = min(
|
||||
contact_force_velocity_limits,
|
||||
default=None,
|
||||
)
|
||||
damping_strength_ratios = tuple(
|
||||
damping_strength_ratios_list
|
||||
)
|
||||
minimum_damping_strength_ratio = min(
|
||||
damping_strength_ratios,
|
||||
default=None,
|
||||
)
|
||||
|
||||
if selected_mode == "legacy":
|
||||
eligible = False
|
||||
reason = "legacyMode"
|
||||
elif entry.discrete_endstop_components:
|
||||
eligible = False
|
||||
reason = "discreteEndstop"
|
||||
elif any(int(component.stoptype) != 4 for component in components):
|
||||
eligible = False
|
||||
reason = "unsupportedStopType"
|
||||
elif any(
|
||||
component.use_friction and float(component.fcoul) != 0.0
|
||||
for component in components
|
||||
):
|
||||
eligible = False
|
||||
reason = "dryFriction"
|
||||
elif not contacts:
|
||||
eligible = False
|
||||
reason = "noFlexibleContact"
|
||||
elif any(
|
||||
not isfinite(float(contact.Pdis))
|
||||
or float(contact.Pdis) <= 0.0
|
||||
for contact in contacts
|
||||
):
|
||||
eligible = False
|
||||
reason = "nonSmoothContactDampingLength"
|
||||
elif any(
|
||||
not isfinite(float(contact.rcont))
|
||||
or float(contact.rcont) <= 0.0
|
||||
for contact in contacts
|
||||
):
|
||||
eligible = False
|
||||
reason = "undampedContact"
|
||||
elif any(
|
||||
not isfinite(float(contact.kcont))
|
||||
or float(contact.kcont) <= 0.0
|
||||
for contact in contacts
|
||||
):
|
||||
eligible = False
|
||||
reason = "invalidContactStiffness"
|
||||
elif not contact_scale_valid:
|
||||
eligible = False
|
||||
reason = "invalidContactScale"
|
||||
elif any(
|
||||
int(contact.discContactOption) != 1
|
||||
for contact in contacts
|
||||
):
|
||||
eligible = False
|
||||
reason = "clampedContactForce"
|
||||
elif len(positive_dvel) != len(components):
|
||||
eligible = False
|
||||
reason = "invalidVelocityScale"
|
||||
elif (
|
||||
len(damping_strength_ratios) != len(contacts)
|
||||
or minimum_damping_strength_ratio is None
|
||||
or minimum_damping_strength_ratio < 1.0
|
||||
):
|
||||
eligible = False
|
||||
reason = "weakContactDamping"
|
||||
else:
|
||||
eligible = True
|
||||
reason = "eligibleFlexibleContact"
|
||||
|
||||
velocity_atol = legacy_atol
|
||||
if eligible:
|
||||
assert minimum_dvel is not None
|
||||
assert minimum_force_velocity_atol is not None
|
||||
velocity_atol = min(
|
||||
default_atol,
|
||||
max(
|
||||
mechanical_atol,
|
||||
min(
|
||||
1.0e-9,
|
||||
1.0e-3 * minimum_dvel,
|
||||
minimum_force_velocity_atol,
|
||||
),
|
||||
),
|
||||
)
|
||||
position_atol = legacy_atol
|
||||
values.extend((velocity_atol, position_atol))
|
||||
group_plans.append(
|
||||
MechanicalToleranceGroupPlan(
|
||||
components=tuple(
|
||||
component.name for component in components
|
||||
),
|
||||
contacts=tuple(contact.name for contact in contacts),
|
||||
eligible=eligible,
|
||||
reason=reason,
|
||||
velocity_atol=velocity_atol,
|
||||
position_atol=position_atol,
|
||||
minimum_dvel=minimum_dvel,
|
||||
minimum_contact_damping_length=minimum_pdis,
|
||||
minimum_damping_strength_ratio=(
|
||||
minimum_damping_strength_ratio
|
||||
),
|
||||
minimum_force_limited_velocity_atol=(
|
||||
minimum_force_velocity_atol
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
return MechanicalAbsoluteTolerancePlan(
|
||||
mode=selected_mode,
|
||||
default_atol=default_atol,
|
||||
legacy_mechanical_atol=mechanical_atol,
|
||||
values=tuple(values),
|
||||
groups=tuple(group_plans),
|
||||
)
|
||||
|
||||
def absolute_tolerances(
|
||||
self,
|
||||
default: float,
|
||||
*,
|
||||
mechanical: float = 1.0e-12,
|
||||
mode: MechanicalAbsoluteToleranceMode | None = None,
|
||||
) -> list[float]:
|
||||
"""Return state-aligned tolerances with machine-scale mechanics.
|
||||
"""Return state-aligned values from the auditable tolerance plan."""
|
||||
|
||||
A scalar ``1e-8`` absolute tolerance makes SciPy perturb a zero-valued
|
||||
endstop position across the much smaller unilateral boundary band while
|
||||
constructing finite-difference Jacobians. Mechanical coordinates need
|
||||
a tighter floor; thermodynamic states retain the caller's tolerance.
|
||||
"""
|
||||
values: list[float] = []
|
||||
for entry in self.state_entries:
|
||||
if isinstance(entry, MechanicalConstraintGroup):
|
||||
values.extend([min(default, mechanical)] * 2)
|
||||
else:
|
||||
values.extend([default] * entry.state_size)
|
||||
return values
|
||||
return list(
|
||||
self.absolute_tolerance_plan(
|
||||
default,
|
||||
mechanical=mechanical,
|
||||
mode=mode,
|
||||
).values
|
||||
)
|
||||
|
||||
def reset_constraint_modes(self) -> None:
|
||||
for group in self.groups:
|
||||
|
||||
Reference in new issue
Block a user