C内核流量计算方法优化,前端文件名称读取优化
This commit is contained in:
1 parent
5d5a2e1843
commit
808c484f5b
94 files changed
+20065
-4320
No files matched your search
+8
-3
@@ -4,9 +4,14 @@
|
|||||||
|
|
||||||
# Regression manifests hash these files as raw bytes. Keep their checkout
|
# Regression manifests hash these files as raw bytes. Keep their checkout
|
||||||
# representation identical on Windows and Linux so hashes remain portable.
|
# representation identical on Windows and Linux so hashes remain portable.
|
||||||
tests/data/test-mql-8.xml text eol=lf
|
tests/fixtures/legacy/test-mql-8.xml text eol=lf
|
||||||
tests/data/test-mql-8.json text eol=lf
|
tests/fixtures/legacy/test-mql-8.json text eol=lf
|
||||||
tests/data/test_mql-full-branches-01-04.xml text eol=lf
|
tests/baselines/simulation/test_mql_full_branches/sources/test_mql-full-branches-01-04.xml text eol=lf
|
||||||
tests/baselines/simulation/**/*.json text eol=lf
|
tests/baselines/simulation/**/*.json text eol=lf
|
||||||
|
|
||||||
tests/baselines/simulation/**/sources/* text eol=lf
|
tests/baselines/simulation/**/sources/* text eol=lf
|
||||||
|
|
||||||
|
# Browser projects and relocated native references.
|
||||||
|
tests/data/*.json text eol=lf
|
||||||
|
tests/baselines/native/*.json text eol=lf
|
||||||
|
tests/fixtures/amesim/*.xml text eol=lf
|
||||||
@@ -93,7 +93,7 @@ jobs:
|
|||||||
python -W error::ResourceWarning -m unittest discover -s tests
|
python -W error::ResourceWarning -m unittest discover -s tests
|
||||||
- name: Complete the 10 second skill fixture in native RK45
|
- name: Complete the 10 second skill fixture in native RK45
|
||||||
run: |
|
run: |
|
||||||
python -m app.simulation.native_codegen tests/data/native-skill-test.xml --output-dir test/ci-skill --method RK45 --max-step 0.001 --rtol 1e-7 --runs 1 --solve-only
|
python -m app.simulation.native_codegen tests/fixtures/native-skill-test.xml --output-dir test/ci-skill --method RK45 --max-step 0.001 --rtol 1e-7 --runs 1 --solve-only
|
||||||
- if: always()
|
- if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ def simulation_config(simulation) -> SolveIVPConfig:
|
|||||||
# floors are emitted by native_codegen.tolerances; XML tolerance fields
|
# floors are emitted by native_codegen.tolerances; XML tolerance fields
|
||||||
# remain a separate protocol change.
|
# remain a separate protocol change.
|
||||||
return SolveIVPConfig(t_start=simulation.t_start, t_stop=simulation.t_stop,
|
return SolveIVPConfig(t_start=simulation.t_start, t_stop=simulation.t_stop,
|
||||||
method=simulation.method, rtol=1e-7, max_step=simulation.max_step)
|
method=simulation.method, rtol=1e-8, max_step=simulation.max_step)
|
||||||
|
|
||||||
|
|
||||||
def simulate_network(network, simulation, *, progress_callback=None,
|
def simulate_network(network, simulation, *, progress_callback=None,
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ def main():
|
|||||||
measured = rows[1:]
|
measured = rows[1:]
|
||||||
summary = {
|
summary = {
|
||||||
"source": str(args.input.resolve()), "sourceSha256": sha256(args.input.read_bytes()).hexdigest(),
|
"source": str(args.input.resolve()), "sourceSha256": sha256(args.input.read_bytes()).hexdigest(),
|
||||||
"xmlSha256": sha256(xml).hexdigest(), "executable": str(package / "model.exe"),
|
"xmlSha256": sha256(xml).hexdigest(), "executable": str(package / build.executable.name),
|
||||||
"cachedExecutable": str(build.executable),
|
"cachedExecutable": str(build.executable),
|
||||||
"buildKey": build.manifest["buildKey"], "buildSeconds": build.seconds,
|
"buildKey": build.manifest["buildKey"], "buildSeconds": build.seconds,
|
||||||
"preparationSeconds": preparation, "cacheHit": build.cache_hit,
|
"preparationSeconds": preparation, "cacheHit": build.cache_hit,
|
||||||
|
|||||||
@@ -33,10 +33,16 @@ def _hash(path: Path) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def toolchain() -> tuple[str, Path, str]:
|
def toolchain() -> tuple[str, Path, str]:
|
||||||
|
if os.name != "nt" and not sys.platform.startswith("linux"):
|
||||||
|
raise RuntimeError("Native builds support Windows and Linux; this platform is not supported.")
|
||||||
compiler = os.environ.get("SIMULATION_NATIVE_CC") or shutil.which("gcc")
|
compiler = os.environ.get("SIMULATION_NATIVE_CC") or shutil.which("gcc")
|
||||||
if not compiler:
|
if not compiler:
|
||||||
raise RuntimeError("C compiler not found; set SIMULATION_NATIVE_CC to gcc.")
|
raise RuntimeError("C compiler not found; set SIMULATION_NATIVE_CC to gcc.")
|
||||||
base = Path(os.environ.get("SUNDIALS_ROOT", str(Path(sys.base_prefix) / "Library")))
|
configured = os.environ.get("SUNDIALS_ROOT")
|
||||||
|
candidates = ([Path(configured)] if configured else
|
||||||
|
[Path(sys.base_prefix) / "Library"] if os.name == "nt" else
|
||||||
|
[Path(sys.prefix) / "native/sundials-7.4.0", Path("/usr/local"), Path("/usr")])
|
||||||
|
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():
|
if not (base / "include/cvode/cvode.h").is_file():
|
||||||
raise RuntimeError("SUNDIALS C development files not found; set SUNDIALS_ROOT.")
|
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]
|
version = subprocess.run([compiler, "--version"], capture_output=True, text=True, check=True, timeout=15).stdout.splitlines()[0]
|
||||||
@@ -48,6 +54,7 @@ def build_native(program: NativeProgram, *, cache_dir: Path | None = None) -> Na
|
|||||||
compiler, sundials, compiler_version = toolchain()
|
compiler, sundials, compiler_version = toolchain()
|
||||||
runtime = sorted(NATIVE.rglob("*.c")) + sorted((NATIVE / "include").glob("*.h"))
|
runtime = sorted(NATIVE.rglob("*.c")) + sorted((NATIVE / "include").glob("*.h"))
|
||||||
flags = ["-std=c11", "-O3", "-Wall", "-Wextra", "-Werror", "-ffp-contract=off", "-fno-fast-math"]
|
flags = ["-std=c11", "-O3", "-Wall", "-Wextra", "-Werror", "-ffp-contract=off", "-fno-fast-math"]
|
||||||
|
executable_name = "model.exe" if os.name == "nt" else "model"
|
||||||
if os.name == "nt":
|
if os.name == "nt":
|
||||||
flags += ["-D__USE_MINGW_ANSI_STDIO=1", "-static-libgcc"]
|
flags += ["-D__USE_MINGW_ANSI_STDIO=1", "-static-libgcc"]
|
||||||
libraries = [sundials / "lib" / f"sundials_{name}.lib" for name in LIBRARIES]
|
libraries = [sundials / "lib" / f"sundials_{name}.lib" for name in LIBRARIES]
|
||||||
@@ -55,8 +62,20 @@ def build_native(program: NativeProgram, *, cache_dir: Path | None = None) -> Na
|
|||||||
vc_runtime = sundials / "bin/vcruntime140.dll"
|
vc_runtime = sundials / "bin/vcruntime140.dll"
|
||||||
if vc_runtime.is_file():
|
if vc_runtime.is_file():
|
||||||
dlls.append(vc_runtime)
|
dlls.append(vc_runtime)
|
||||||
|
elif sys.platform.startswith("linux"):
|
||||||
|
flags += ["-D_POSIX_C_SOURCE=200809L"]
|
||||||
|
library_dirs = [sundials / "lib", sundials / "lib64", sundials / "lib/x86_64-linux-gnu"]
|
||||||
|
library_dir = next((path for path in library_dirs if all(
|
||||||
|
(path / f"libsundials_{name}.a").is_file() for name in LIBRARIES)), None)
|
||||||
|
if library_dir is None:
|
||||||
|
raise RuntimeError("SUNDIALS static libraries not found; build with BUILD_STATIC_LIBS=ON and set SUNDIALS_ROOT.")
|
||||||
|
libraries = [library_dir / f"libsundials_{name}.a" for name in LIBRARIES]
|
||||||
|
dlls = []
|
||||||
else:
|
else:
|
||||||
raise RuntimeError("Native v1 build packaging currently supports Windows x64; Linux packaging is pending.")
|
raise RuntimeError("Native builds support Windows and Linux; this platform is not supported.")
|
||||||
|
link_libraries = list(map(str, libraries))
|
||||||
|
if sys.platform.startswith("linux"):
|
||||||
|
link_libraries = ["-Wl,--start-group", *link_libraries, "-Wl,--end-group"]
|
||||||
sources = {str(p.relative_to(ROOT)): _hash(p) for p in runtime}
|
sources = {str(p.relative_to(ROOT)): _hash(p) for p in runtime}
|
||||||
sources["native/THIRD_PARTY_NOTICES.txt"] = _hash(NATIVE / "THIRD_PARTY_NOTICES.txt")
|
sources["native/THIRD_PARTY_NOTICES.txt"] = _hash(NATIVE / "THIRD_PARTY_NOTICES.txt")
|
||||||
dependencies = {str(p.name): _hash(p) for p in libraries + dlls}
|
dependencies = {str(p.name): _hash(p) for p in libraries + dlls}
|
||||||
@@ -75,7 +94,7 @@ def build_native(program: NativeProgram, *, cache_dir: Path | None = None) -> Na
|
|||||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||||
if all((target / name).is_file() and _hash(target / name) == digest
|
if all((target / name).is_file() and _hash(target / name) == digest
|
||||||
for name, digest in manifest["artifacts"].items()):
|
for name, digest in manifest["artifacts"].items()):
|
||||||
return NativeBuild(target / "model.exe", manifest, True, time.perf_counter()-start)
|
return NativeBuild(target / executable_name, manifest, True, time.perf_counter()-start)
|
||||||
raise RuntimeError(f"Native cache integrity check failed: {target}")
|
raise RuntimeError(f"Native cache integrity check failed: {target}")
|
||||||
cache.mkdir(parents=True, exist_ok=True)
|
cache.mkdir(parents=True, exist_ok=True)
|
||||||
stage = Path(tempfile.mkdtemp(prefix="building-", dir=cache))
|
stage = Path(tempfile.mkdtemp(prefix="building-", dir=cache))
|
||||||
@@ -84,7 +103,7 @@ def build_native(program: NativeProgram, *, cache_dir: Path | None = None) -> Na
|
|||||||
command = [compiler, *flags, "-I", str(stage), "-I", str(NATIVE / "include"),
|
command = [compiler, *flags, "-I", str(stage), "-I", str(NATIVE / "include"),
|
||||||
"-I", str(sundials / "include"), str(stage / "model.c"),
|
"-I", str(sundials / "include"), str(stage / "model.c"),
|
||||||
*[str(p) for p in runtime if p.suffix == ".c"],
|
*[str(p) for p in runtime if p.suffix == ".c"],
|
||||||
*map(str, libraries), "-lm", "-o", str(stage / "model.exe")]
|
*link_libraries, "-lm", "-o", str(stage / executable_name)]
|
||||||
result = subprocess.run(command, capture_output=True, text=True, timeout=120)
|
result = subprocess.run(command, capture_output=True, text=True, timeout=120)
|
||||||
(stage / "build.log").write_text(result.stdout + result.stderr, encoding="utf-8")
|
(stage / "build.log").write_text(result.stdout + result.stderr, encoding="utf-8")
|
||||||
if result.returncode:
|
if result.returncode:
|
||||||
@@ -116,4 +135,4 @@ def build_native(program: NativeProgram, *, cache_dir: Path | None = None) -> Na
|
|||||||
if stage.resolve().parent != cache:
|
if stage.resolve().parent != cache:
|
||||||
raise RuntimeError("Unexpected native build staging directory.")
|
raise RuntimeError("Unexpected native build staging directory.")
|
||||||
shutil.rmtree(stage)
|
shutil.rmtree(stage)
|
||||||
return NativeBuild(target / "model.exe", manifest, False, time.perf_counter()-start)
|
return NativeBuild(target / executable_name, manifest, False, time.perf_counter()-start)
|
||||||
@@ -15,7 +15,7 @@ DATA_DIR = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
SIMULATION_RUNS_DIR = DATA_DIR / "simulation-runs"
|
SIMULATION_RUNS_DIR = DATA_DIR / "simulation-runs"
|
||||||
SIMULATION_BASELINES_DIR = PROJECT_ROOT / "tests" / "data"
|
SIMULATION_BASELINES_DIR = PROJECT_ROOT / "tests" / "baselines"
|
||||||
AMESIM_TEST_MQL_ARCHIVE_PATH = PROJECT_ROOT / "AmesimModels" / "test_mql.ame"
|
AMESIM_TEST_MQL_ARCHIVE_PATH = PROJECT_ROOT / "AmesimModels" / "test_mql.ame"
|
||||||
MODELICA_TESTMODEL_RESULT_PATH = (
|
MODELICA_TESTMODEL_RESULT_PATH = (
|
||||||
PROJECT_ROOT / "ModelicaModels" / "Simulation" / "Testmodel_res.csv"
|
PROJECT_ROOT / "ModelicaModels" / "Simulation" / "Testmodel_res.csv"
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ def _run_native_warmup() -> None:
|
|||||||
from app.simulation.native_codegen.build import toolchain
|
from app.simulation.native_codegen.build import toolchain
|
||||||
from app.system_xml import validate_system_xml_document
|
from app.system_xml import validate_system_xml_document
|
||||||
|
|
||||||
if os.name != "nt":
|
|
||||||
raise RuntimeError("Native v1 currently supports Windows x64 build packaging only.")
|
|
||||||
toolchain()
|
toolchain()
|
||||||
validate_system_xml_document(b"<System/>")
|
validate_system_xml_document(b"<System/>")
|
||||||
|
|
||||||
|
|||||||
Executable
+31
@@ -0,0 +1,31 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build the pinned native dependency beside the Python virtual environment.
|
||||||
|
set -euo pipefail
|
||||||
|
REPO_ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||||
|
NATIVE_ENV="${SYSTEM_SIMULATION_NATIVE_ENV:-$REPO_ROOT/.venv/native}"
|
||||||
|
VERSION=7.4.0
|
||||||
|
ARCHIVE_SHA256=679ddacdd77610110e613164e8297d6d0cd35bae8e9c3afc8e8ff6f99a1c2a7b
|
||||||
|
SOURCE_ROOT="$NATIVE_ENV/build/sundials-$VERSION"
|
||||||
|
PREFIX="$NATIVE_ENV/sundials-$VERSION"
|
||||||
|
for tool in curl cmake gcc tar sha256sum; do
|
||||||
|
command -v "$tool" >/dev/null || { echo "Missing build tool: $tool" >&2; exit 1; }
|
||||||
|
done
|
||||||
|
mkdir -p "$SOURCE_ROOT"
|
||||||
|
ARCHIVE="$SOURCE_ROOT/sundials-$VERSION.tar.gz"
|
||||||
|
if [[ ! -f "$ARCHIVE" ]]; then
|
||||||
|
curl --fail --location "https://github.com/LLNL/sundials/releases/download/v$VERSION/sundials-$VERSION.tar.gz" --output "$ARCHIVE"
|
||||||
|
fi
|
||||||
|
printf '%s %s\n' "$ARCHIVE_SHA256" "$ARCHIVE" | sha256sum --check -
|
||||||
|
if [[ ! -d "$SOURCE_ROOT/sundials-$VERSION" ]]; then
|
||||||
|
tar -xzf "$ARCHIVE" -C "$SOURCE_ROOT"
|
||||||
|
fi
|
||||||
|
cmake -S "$SOURCE_ROOT/sundials-$VERSION" -B "$SOURCE_ROOT/build" \
|
||||||
|
-DCMAKE_INSTALL_PREFIX="$PREFIX" -DCMAKE_BUILD_TYPE=Release \
|
||||||
|
-DBUILD_STATIC_LIBS=ON -DBUILD_SHARED_LIBS=OFF \
|
||||||
|
-DBUILD_ARKODE=OFF -DBUILD_CVODES=OFF -DBUILD_IDA=OFF \
|
||||||
|
-DBUILD_IDAS=OFF -DBUILD_KINSOL=OFF \
|
||||||
|
-DEXAMPLES_ENABLE_C=OFF -DEXAMPLES_INSTALL=OFF
|
||||||
|
cmake --build "$SOURCE_ROOT/build" --parallel "${CMAKE_BUILD_PARALLEL_LEVEL:-4}"
|
||||||
|
cmake --install "$SOURCE_ROOT/build"
|
||||||
|
echo "Native dependency installed in $PREFIX"
|
||||||
|
echo "The repository .venv Python discovers this default location automatically."
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# test-mql-4 模型复核与输入修正(2026-09-11)
|
||||||
|
|
||||||
|
目录整理补充:`tests/data` 的 JSON/XML 仅保留浏览器可导入的四路、八路 corrected JSON;参考曲线、旧工程和后端 XML 已迁到 `tests/baselines` / `tests/fixtures`,本文当前路径同步更新,文件内容及来源哈希不变。详见 [数据目录说明](../../tests/data/README.md)。
|
||||||
|
|
||||||
|
本次从 `tests/data/test_mql_4.ame` 的原始 `.cir` 图纸重新核对元件、参数和端口连接,生成了可导入网页的 `tests/data/test-mql-4-corrected.json`,并同步 `tests/fixtures/amesim/test-mql-4-corrected.xml`。两份输入均有 81 个元件、90 条连接、582 个公开参数,对应 64 个动态状态;生成结果的每个公开参数和连接端口均已按同一元件映射与 AME 图纸逐项比较。生成的 XML 通过后端语法和语义校验,无错误、无警告。
|
||||||
|
|
||||||
|
`native-python-reference.json` 是 50 个网络的冻结数值参考,`test-mql-4-amesim-reference.json` 是 72 条曲线、57 个时刻的 Amesim 结果参考,两者都不是可导入的工程 JSON。本次未覆盖或改写这两份参考数据。
|
||||||
|
|
||||||
|
## 新发现及修正
|
||||||
|
|
||||||
|
此前名为 corrected 的 XML 仍有一处参数不一致:`amesim_pnl00r_4` 对应 AME 的 `pneumatic`(图纸 `LINE` 索引 38、PNL00R)。AME 的相对粗糙度 `rr` 为 `1e-5`,旧 XML 为 `0.0032142857142857142`;本次改为 `1e-5`。该管径在前轮已改为 `0.01 m`,本次确认管长 `1 m` 也一致。此前报告列出的五处参数修正未覆盖这项遗留差异。
|
||||||
|
|
||||||
|
旧 XML 和 AME 的元件连接图同构,但有 25 条连线涉及等价端口编号或方向差异。本次 JSON/XML 直接采用 AME 的确切端口编号:
|
||||||
|
|
||||||
|
- 11 条连接涉及 P4NODE2 的非参考分支口重排,固定参考口 `port_2` 原本已匹配。
|
||||||
|
- 8 条连接涉及 PNCH012 的同一容腔端口重排。
|
||||||
|
- 4 条连接涉及 LMECHN1 的支路编号重排,公共机械端保持原对应关系。
|
||||||
|
- 2 条连接涉及 PNL00R 两端方向反转。它不增加容腔状态,但其带符号端口输出应按新端口对应关系解释。
|
||||||
|
|
||||||
|
PNL0001 的状态端未颠倒:AME 建模连线的 `OUTPUT_TYPE=1` 使用起点→模型端口 1、终点→端口 2;`OUTPUT_TYPE=2` 使用相反对应。组件端口索引从 0 起,PNVO001/FORC 的索引 0 映射为公开 `res`,STEP0/UD00 的索引 0 映射为 `out`,其他组件端口映射为 `port_(索引+1)`。
|
||||||
|
|
||||||
|
工程保留先前结果变量使用的组件 ID;显示名改为 AME 图纸别名。已有画布布局来自八路 JSON 中同 ID 元件的展示元数据,缺少的闭合管另行放置。变化端口的连接改用可见连线,避免把不再重合的端口继续标记为接触连接。布局、名称和绘图连线路径不作为物理一致性的依据。
|
||||||
|
|
||||||
|
后续真实浏览器导入发现,组件目录中的信号端口带有 `positiveFlowDirection: null`,而前端工程合同只接受省略此可选字段或值为 `intoComponent`。已修正交付 JSON 及生成器:没有方向值时省略该键。该修复仅涉及展示元数据,物理参数、连接端点及数值 XML 不变;审计 JSON 中的工程文件 SHA256 已同步更新。仅通过 XML 校验不足以证明工程 JSON 可被前端导入。
|
||||||
|
|
||||||
|
## 用户复核新增:P4NODE2 图形编号与朝向
|
||||||
|
|
||||||
|
用户直接打开 AME 后指出参考口看起来不同。这是实际的显示缺陷,前面的物理连接审计没有覆盖图形编号与朝向:导出工程的 80/81 个节点位置沿用了八路 JSON 中同 ID 的位置,闭合管 `amesim_pnl0002_10` 另放在 `(80, 900)`;工程布局并没有恢复为 AME 原图的坐标和连线路径。
|
||||||
|
|
||||||
|
重新直接读取 `.cir` 的 4 个 P4NODE2,均为 `COMP_GEOMETRY=1`,原始四个 `COMP_PORT/PORT_POS` 依次为 `(10,19)`、`(19,10)`、`(10,1)`、`(1,10)`。因此原图端口 1 朝下、参考口 2 朝右、端口 3 朝上、端口 4 朝左。前端原图标的 3/4 号锚点编号与该对应关系不符,而且四路 JSON 的第 2~4 节点未旋转,令参考粗线朝左。仅旋转原图标不能修正全部四口,因为对顶端口的编号也不同。
|
||||||
|
|
||||||
|
已交换通用 P4NODE2 图标的端口 3/4 锚点,并把四路工程的 4 个 P4NODE2 统一为旋转 180°、不镜像,使四口完整方位符合 AME;清除了 1 条相关连线的旧路由点。参考粗线与实际 `port_2` Handle 使用同一镜像/旋转变换,二者本身没有脱离。通用锚点修正也影响其他工程(包括八路 corrected)的 P4NODE2 显示方位,不改变其连接所引用的端口名称。
|
||||||
|
|
||||||
|
这次修复没有改变任何元件 ID、模型类型/版本、参数、连线 ID/端点或仿真设置,数值 XML 完全未改。审计 JSON 保留此前参数和连接修正记录,另记本次展示变化及最新工程哈希。
|
||||||
|
|
||||||
|
AME 自带预览图以及重新直接读取的原始图纸均显示四支路:4 个 PNRP17 活塞、4 个 PNCH012 气室、4 个 PNVO001 阀、4 个 STEP0 输入、4 个 LSTP00A 接触元件;4 个支路质量块另与 2 个公共质量块连接,因此 MECMAS21 总数为 6。图中气动管线有 18 条作为子模型的 LINE,在网页中显示为独立管路元件,这也是两种画法的计数观感不同之处。
|
||||||
|
|
||||||
|
独立图形证据保存在 `tests/model_audit/p4node2-ame-geometry.json`,直接记录原始 AME 端口坐标和归档哈希。新增前端回归以这组独立坐标检查实际 DOM Handle 的相对方位,并检查镜像后粗线仍指向 `port_2`;它不直接复制前端锚点常量作为判断依据。两项定向浏览器测试(既有 PN3/P4 图形检查及新增 AME 四口/镜像检查)均通过,共 9.4 s;测试没有请求仿真。
|
||||||
|
|
||||||
|
## 独立原始参数复核
|
||||||
|
|
||||||
|
为避免共享参数转换代码造成共同漏检,另用 `tests/model_audit/independent_mql4_parameter_check.py` 直接解析 AME 原始字段和表达式,独立求值及换算,没有调用原审计的 `_expected_parameters` 或其字段转换辅助函数。582 项分为 543 个 AME 原生字段、32 个公开容积/容积变化输入的零默认值,以及 7 个适配字段(4 个阀开度回退值、2 个由图形方向映射的力方向、1 个氦气物性选择);公开额外字段不冒充 AME 原生参数。
|
||||||
|
|
||||||
|
当前 JSON 的 582 项均与各自来源或明确的适配规则相符。独立核对包括 26 个表压转绝压、58 个 mm→m、8 个 mm²→m²、6 个 L→m³,以及各 12 个刚度和阻尼从每毫米转为每米的换算。脚本已从可提交路径实际运行通过;逐字段表达式、原值、原单位、目标单位与比较结果输出到 `test/solver-newton-20260911/mql4/visual-recheck/independent-parameters.json`。
|
||||||
|
|
||||||
|
## 仿真设置及参考边界
|
||||||
|
|
||||||
|
生成输入为 BDF、0~10 s、输出间隔 0.01 s、最大步长 `1e30 s`;本轮初始核查阶段网页后端默认 `rtol=1e-7`,后续精度核查后已调整为 `1e-8`;JSON 不保存该后端默认值,实际运行值应以结果诊断为准。原 AME `.sim` 的原始两行数值保存在审计 JSON 中。前轮官方 API 核查记录该归档保存的是固定步长设置,而用于既有参考数据的是官方重新编译后改用 Standard 变步长、误差限 `1e-7`、最大步长 `1e30` 的四路运行。本次没有重新运行 Amesim,也不把 native BDF 与 Amesim Standard 自动算法称为同一积分器。
|
||||||
|
|
||||||
|
原 AME 归档内的图纸和编译/结果缓存不一致,不能直接把包内 `.results` 当作本次四路参考:
|
||||||
|
|
||||||
|
- `.cir` 有 63 个组件、18 条建模管线、26 条直接连线和 28 对接触连接,得到 81 个公开元件、90 条连接、64 个动态状态。
|
||||||
|
- `.modelinfo` 仍记载 132 个状态,`.ameperf` 包含 LSTP00A 第 1~8 实例,属于旧八路运行。
|
||||||
|
- `.results` 声明 1116 个保存变量,而当前 `.var` 只有 640 行;这两份缓存不能作为相互一致的结果集合使用。
|
||||||
|
|
||||||
|
既有 `test-mql-4-amesim-reference.json` 的来源声明为 2026-09-11 官方重编译四路模型,包含 72 条状态物理量及阀流量曲线、57 个选定时刻。它的原始 AME SHA256 与当前归档一致;其 `xmlSha256` 绑定的是修改前 XML,故本次修正后不再与该字段相等。这个历史字段和曲线数据保持不变,不能为通过校验而改写来源哈希。
|
||||||
|
|
||||||
|
该参考可用于本次曲线差异评估,但不表示本次重新运行了 Amesim,也不覆盖接触事件力峰或全部 1001 个常规采样点。新修改的粗糙度应作为本次输入差异明确记录,不能继续沿用“此前 corrected XML 已逐参数完全对齐”的结论。
|
||||||
|
|
||||||
|
## 复现与文件
|
||||||
|
|
||||||
|
在仓库根目录执行只读核查,不生成或改写工程与审计文件:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
.venv/bin/python tests/model_audit/audit_mql4_model.py --check
|
||||||
|
```
|
||||||
|
|
||||||
|
该模式检查生成端口可选字段的规则,并逐组件比较 JSON/XML 的模型类型、版本、参数、全部连接端点与仿真设置;不启动求解器。当前交付通过,内存中人为加入 `positiveFlowDirection: null`、修改参数或连接端点均被拒绝。它是针对交付输入的轻量检查,不替代完整前端解析与真实浏览器导入。
|
||||||
|
|
||||||
|
需要重新生成 corrected JSON/XML 及审计文件时执行:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
.venv/bin/python tests/model_audit/audit_mql4_model.py
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本只静态读取 AME 图纸和注册组件合同,生成/核对 corrected JSON/XML,并输出 `tests/model_audit/test-mql-4-audit.json`。参数和单位转换复用仓库 `tests/test_test_mql_ame_contract.py` 的明确映射,脚本不依赖 `test/` 中的程序。首次运行时将其输入 XML 备份到 `test/solver-newton-20260911/mql4/legacy-corrected-input.xml`,另在该目录保存展开后的源模型和过程审计;修改前 XML 的 SHA256 为 `7b67e61f791f33bf2a4a0045d151d7fd6a418e63a5ea7f3472d474fd4eff9c86`。
|
||||||
|
|
||||||
|
原 AME SHA256:`99a071896671d634d651a7b21f487662dd7c122af8fd3d60ead404a5afa53ed4`。完整成员哈希、元件映射、25 条连接变化和生成文件哈希保存在审计 JSON 中。本报告记录输入及图形复核,不记录求解性能或本轮数值曲线结论。
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# test-mql-8 模型对照与修正(2026-09-11)
|
||||||
|
|
||||||
|
目录整理补充:`tests/data` 的 JSON/XML 仅保留浏览器可导入的四路、八路 corrected JSON;参考曲线、旧工程和后端 XML 已迁到 `tests/baselines` / `tests/fixtures`,本文当前路径同步更新,文件内容及来源哈希不变。详见 [数据目录说明](../../tests/data/README.md)。
|
||||||
|
|
||||||
|
已依据 `tests/data/test_mql.ame` 的真实图纸生成 `tests/data/test-mql-8-corrected.json`,保留原 `test-mql-8.json` 供审计。修正版包含参数、逐端口连接和仿真最大步长的对齐。静态校验通过。rtol=1e-7 的首次求解在 120 s 限额内推进到 5.144858167 s 后超时;后续按用户确认的全局 rtol=1e-8,以同一执行模型和内核完成了完整 10 s 单次运行。本轮未运行 Amesim 官方程序,也未进行八路 Amesim 曲线对比。
|
||||||
|
|
||||||
|
## 来源与核查方法
|
||||||
|
|
||||||
|
AME 是可读取的 tar 归档。以 `test_mql_.cir` 的组件、接触连接、DIRECT 连接和带子模型的管线为模型来源,解析全局表达式、SI 单位和初始状态。压力字段按图纸保存的表压加 `101300 Pa` 转为公开模型的绝对压力。原始输入指纹:
|
||||||
|
|
||||||
|
- `tests/data/test_mql.ame`:21698560 字节,SHA256 `1ff0ea4284b9248260eeceb8b27cd0bc14dbccb43554ab8c0b49900d0b944c3d`。
|
||||||
|
- `tests/fixtures/legacy/test-mql-8.json`:272523 字节,SHA256 `4f1bf698519ba21429a11a83653adc7e52191f2d2b51fca943ee0b84f62feebd`。
|
||||||
|
|
||||||
|
图纸含 117 个组件、40 条带模型管线,展开后共 157 个元件(含介质)、178 条连接,与 JSON 的类型拓扑完全同构。映射以类型和完整邻接约束建立,再选端口修改数最少的对应关系,不使用组件显示名称猜测。类型拓扑的剩余对称候选具有相同 AME 参数;完整逐元件映射保存在审计文件中。
|
||||||
|
|
||||||
|
组件图纸端口索引从 0 开始;信号端按公开合同映射为 `res/out`。管线 `OUTPUT_TYPE=1` 时起点对应计算 port_1、终点对应 port_2;`OUTPUT_TYPE=2` 时相反。这个字段决定 PNL0001 的储气端,不能仅根据线段绘制方向推断。
|
||||||
|
|
||||||
|
1092 个公开参数全部核对,其中 1015 个可直接对应参数又与归档 `.param/.data` 逐值交叉核对,全部一致;这两份表均有 1170 行。两个 LMECHN1 的结构参数 `v1=8` 只在图纸中声明,其余公开补充参数按已有合同映射。归档结果未用于声称本轮已经重新运行 Amesim 或完成曲线一致性验证。
|
||||||
|
|
||||||
|
## 参数修正
|
||||||
|
|
||||||
|
以下四项均属于拓扑唯一对应的 `amesim_mecmas21_9`,AME 实体为 `component:35 / mass_friction_endstops_19`。
|
||||||
|
|
||||||
|
| 参数 | 原 JSON | AME 图纸与修正版 | `.param/.data` 行号 |
|
||||||
|
| --- | ---: | ---: | ---: |
|
||||||
|
| mass | 90000 kg | 100 kg | 364 |
|
||||||
|
| xmin | -0.72 m | -1 m | 373 |
|
||||||
|
| xmax | 0 m | 1 m | 377 |
|
||||||
|
| stoptype | 1 | 4 | 383 |
|
||||||
|
|
||||||
|
这不是只交换参考口的同数值优化;质量与限位配置的修正确实会改变系统行为。其余 1088 个参数已一致,保留原值及编辑器单位、表达式和显示设置。比较允许正常的浮点表达式舍入差异,不把 `15299999.999999998` 与 `15300000` 当作参数错误。
|
||||||
|
|
||||||
|
## 连接和仿真设置
|
||||||
|
|
||||||
|
所有 178 条边逐端口与 AME 对齐,共 62 条边各修改一个端点,元件之间的邻接关系保持一致:
|
||||||
|
|
||||||
|
| 类型 | 修改端点数 | 原因 |
|
||||||
|
| --- | ---: | --- |
|
||||||
|
| 8 个 P4NODE2 | 30 | 修正 7 个参考口的实际来源,并对齐其余支路的精确编号 |
|
||||||
|
| 8 个 PNCH012 | 16 | 对齐共享容腔的 port_2/port_3 编号 |
|
||||||
|
| 2 个 LMECHN1 | 16 | 对齐八条机械汇总支路的编号顺序 |
|
||||||
|
|
||||||
|
后两项属于现有共享状态/汇总端口的编号对齐,不应解释成 32 处独立物理机制错误。15 条受影响边原带有几何接触标记,端点变化后清除该标记,使连接以普通边显示;不删除任何物理连接,也不移动组件。
|
||||||
|
|
||||||
|
AME `.sim` 首行保存 `0 10 0.01 1e30 1e-7 ...`。修正版保持 0~10 s、输出间隔 0.01 s,最大积分步长由原 JSON 的 0.02 s 对齐为 1e30 s。公开模型仍采用 BDF;没有将 AME 的内部求解器编号等同于 CVODE BDF。AME 归档相对误差限为 1e-7,首次原生核验采用同值。后续经用户确认,全局网页/API 默认值调整为 1e-8;JSON 协议本身不新增误差限字段,后续执行按实际后端设置记录。
|
||||||
|
|
||||||
|
## 验证与复现
|
||||||
|
|
||||||
|
修正版通过工程载荷解析、XML 导出、端口供需与网络校验,以及正式 C 代码生成:132 个动态状态、1784 个输出、484 条气动计算关系、0 个系统代数循环块。编辑器保存的参数表达式在单独的执行副本中解析为数值,与浏览器导出行为一致。此项静态验证之后进行了下述一次真实数值执行;管流求根修改的其他模型性能对照由独立验证记录说明。
|
||||||
|
|
||||||
|
复现:在仓库根目录执行 `.venv/bin/python tools/audit_test_mql8_model.py`。脚本复用现有 `tests/test_test_mql_ame_contract.py` 中的 AME 表达式、单位和公开参数映射辅助函数,不执行该历史测试类。
|
||||||
|
|
||||||
|
脚本写入修正版 JSON,以及 Git 忽略目录 `test/solver-newton-20260911/mql8/` 中的 `audit.json`(157 个实体映射、1092 项参数、178 条连接、1015 项归档参数交叉证据和来源指纹)与 `corrected.xml`。它不修改原 JSON、AME 或冻结基准。
|
||||||
|
|
||||||
|
本轮实际求解与手工网页验收均显式使用修正版 `tests/data/test-mql-8-corrected.json`,作为后续用户运行的交付文件。原 JSON 仅保留为审计来源;旧 `simulation-live-mql8.spec.ts` 记录 2026-08-18 的 Python 后端活动心跳流程并写入该日基准文件,不作为本轮原生验收入口,未覆盖其历史证据。`fit-view.spec.ts` 的旧模型视图检查和 `tests/baselines/` 也保持原始输入。
|
||||||
|
|
||||||
|
## 单次真实 C 执行
|
||||||
|
|
||||||
|
使用修正版 JSON,经正式输入加载、C 生成和本机静态 SUNDIALS 7.4.0 构建后,直接调用一次 `execute_native`;未预热、未重复计时,也未更改物理参数、精度、积分器或库。请求 BDF/CVODE、0~10 s、rtol=1e-7、最大步长 1e30 s、采样间隔 0.01 s,执行限额 120 s。质量等状态的实际绝对误差限仍由当前生成器按物理量生成。
|
||||||
|
|
||||||
|
| 指标 | 单次运行结果 |
|
||||||
|
| --- | ---: |
|
||||||
|
| 完成状态 | failed,执行超时 |
|
||||||
|
| 实际推进终点 | 5.144858167417297 s |
|
||||||
|
| 纯求解墙钟 | 120.000105 s |
|
||||||
|
| 模型求值次数 | 1530472 |
|
||||||
|
| 接受步数 / 拒绝步数 | 77649 / 2590 |
|
||||||
|
| 输出采样数 | 517(含事件和退出时刻) |
|
||||||
|
| 气体质量状态数 | 56 |
|
||||||
|
| 初始气体总质量 | 5.5668930151375235 kg |
|
||||||
|
| 已返回采样的总质量最大漂移 | 1.62537e-13 kg |
|
||||||
|
|
||||||
|
所有已返回采样数值均有限。结果明确报告 `Native solve exceeded its time limit.`;超时后 RHS 拒绝求值产生的 CVODE Jacobian/setup 错误日志不能单独作为另一个雅可比故障的证据。这次运行证明修正模型能生成、构建并实际推进,不能表述为完整 10 s 回归通过,也不支持性能加速结论。未为取得完成标记放宽设置或追加运行。
|
||||||
|
|
||||||
|
材料保存在 `test/solver-newton-20260911/mql8/native-run/`:`input.xml`、`model-manifest.json`、`summary.json`、`execution/result.json` 和 `execution/worker.log`。构建键为 `403eb3539384555d3d2eb48951d2416f4754a7b1466f5c001a12879b4928eb7a`,该次执行时修正版 JSON 的 SHA256 为 `d034938422cc35e4e49183c76e900ad09ebbca250624fe3706fed76064e25038`,对应下述 P4 显示修复之前的工程。单次执行脚本是同目录上级的 `run_native_once.py`;复现必须改用新的运行目录,不能覆盖现有结果。
|
||||||
|
|
||||||
|
## P4 端口显示对齐
|
||||||
|
|
||||||
|
后续逐口复核发现,原通用 P4 符号的 port_3/port_4 方位与 AME 编号不一致。通用符号修正为默认 port_1 上、port_2 左、port_3 下、port_4 右后,对八路图纸逐个读取 `COMP_GEOMETRY` 和 `COMP_PORTS`:8 个 P4 均为 geometry 1,口位依次为下、右、上、左。因此修正版统一设为 `rotation=180`、`mirrored=false`,同时清理关联边上 6 组过期 `routePoints`;生成脚本保存了这项处理与每个原图端口的证据。
|
||||||
|
|
||||||
|
修复前后通过正式 `load_input` 导出的 XML **逐字节完全一致**,SHA256 为 `2804b33f04cabb3c10fcc3cc0843c35de17efbe1b6ee5e5e821edd9a5515a23d`;这次仅改变显示方位和旧路径缓存,没有改变物理端点、参数或执行设置。当前修正版 JSON 的 SHA256 为 `670977bef67e62d9c66e8af497bada208bd72a7301be45128d185d47282cf288`。验证记录在 `test/solver-newton-20260911/mql8/p4-display-verification.json`,此前的运行输入和结果保持原样。
|
||||||
|
|
||||||
|
## 全局 rtol=1e-8 的单次完整运行
|
||||||
|
|
||||||
|
用户确认全局网页/API 默认相对误差限改为 1e-8 后,对当前显示修正后的 JSON 再执行一次。脚本从正式 `simulation_config` 读取设置,并在运行前断言其确为 1e-8,未通过命令行或局部配置覆盖默认值。使用 BDF/CVODE 7.4.0、0~10 s、最大步长 1e30 s、采样间隔 0.01 s;执行限额仍为 120 s,无预热或重复运行。
|
||||||
|
|
||||||
|
| 指标 | 单次运行结果 |
|
||||||
|
| --- | ---: |
|
||||||
|
| 完成状态 | success=true,completed |
|
||||||
|
| 实际推进终点 | 10 s |
|
||||||
|
| 纯求解墙钟 / CPU | 5.907054 s / 5.906099 s |
|
||||||
|
| 模型求值次数 | 74265 |
|
||||||
|
| 接受步数 / 拒绝步数 | 6974 / 454 |
|
||||||
|
| 雅可比求值 / LU 次数 | 475 / 1656 |
|
||||||
|
| 状态转换 / 求解器启动次数 | 1 / 4 |
|
||||||
|
| 返回采样数 / 范围 | 1002 / 0~10 s |
|
||||||
|
| 气体质量状态数 | 56 |
|
||||||
|
| 气体总质量最大漂移 | 1.15463e-14 kg |
|
||||||
|
|
||||||
|
全部返回采样均为有限数值,132 个动态状态、1784 个输出和 0 个系统代数循环块保持一致。此次命中与 rtol=1e-7 运行相同的构建键 `403eb3539384555d3d2eb48951d2416f4754a7b1466f5c001a12879b4928eb7a`;P4 显示修复不改变执行 XML,相对误差限是运行时设置。两次运行精度条件不同,而且本轮与轻量前端检查并行,不能根据这两次时间记录宣称管流优化的加速倍数。
|
||||||
|
|
||||||
|
本轮证明当前修正版在全局 rtol=1e-8 下可以完整运行 10 s;不以此替代八路模型与 Amesim 曲线的独立一致性核验。此前 rtol=1e-7 超时记录保留,不改写为通过。
|
||||||
|
|
||||||
|
材料位于 `test/solver-newton-20260911/mql8/native-run-rtol1e8/`,包含 `input.xml`、`model-manifest.json`、`summary.json`、`execution/result.json` 和 `execution/worker.log`。执行脚本为其上级 `run_native_rtol1e8.py`,只有传入 `--execute` 才启动求解,且要求新的运行目录。输入 JSON SHA256 为 `670977bef67e62d9c66e8af497bada208bd72a7301be45128d185d47282cf288`。
|
||||||
|
|
||||||
|
## 八路真实网页补验
|
||||||
|
|
||||||
|
通过本次 `127.0.0.1:8011` 正式网页,直接导入最终 `test-mql-8-corrected.json`,以全局 `rtol=1e-8` 完整运行到10 s。曲线显示、CSV和结果文件下载、刷新后恢复均成功,无浏览器脚本异常。网页原始曲线和最终输出与独立原生八路运行逐值相同;CSV的1785列、1002行逐单元与原始数据相同。最后一轮为功能补验,不计入四路速度基准。
|
||||||
|
|
||||||
|
完整证据在 `test/solver-newton-20260911/browser-mql8-final/`:`summary.json`、`verification.json`、输入快照、原始/恢复后的`.simresult`、CSV和网页截图。初轮自动化曾因写死四路独有组件ID而在选择曲线时失败,应用求解已完成且无pageerror;脚本改为从当前工程选择真实存在的PNL0002后,上述流程全部通过。
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
# 牛顿管流求根、AME 输入复核与网页验证(2026-09-11)
|
||||||
|
|
||||||
|
目录整理补充:`tests/data` 的 JSON/XML 仅保留浏览器可导入的四路、八路 corrected JSON;参考曲线、旧工程和后端 XML 已迁到 `tests/baselines` / `tests/fixtures`,本文当前路径同步更新,文件内容及来源哈希不变。详见 [数据目录说明](../../tests/data/README.md)。
|
||||||
|
|
||||||
|
本轮在 `5d5a2e1843bc10cf7764c77cc2c1182c21a68df5` 的物性复用实现上,补全局部管流牛顿求根的区间保护和二分回退;按用户确认,将网页/API 与 Python CLI 默认相对误差限改为 `1e-8`。四路输入先按 AME 图纸核对,再做实际原生计算和浏览器验证。环境依赖、生成程序和运行数据均留在 Git 忽略目录,没有提交或推送 Git。
|
||||||
|
|
||||||
|
## 实现与适用范围
|
||||||
|
|
||||||
|
每条阻力支路在本次 RHS 所给定的两端压力、上游温度和物性下,局部求解一个标量方程:
|
||||||
|
|
||||||
|
\[
|
||||||
|
F(Re)=Re^2 f(Re,\varepsilon/D)-K=0,\qquad
|
||||||
|
Re=\frac{4|\dot m|}{\pi D\mu}.
|
||||||
|
\]
|
||||||
|
|
||||||
|
`K` 由当前可压缩流动系数、上游状态和管径、管长确定。流向由压差决定;摩擦系数仍使用现有的层流、过渡和湍流连续混合公式。PNL0001/2/3 的容腔质量、能量状态由系统积分器推进,机械运动及接触事件也在系统层处理。局部求根没有把全系统变成一个单独的牛顿方程组。
|
||||||
|
|
||||||
|
本次保护包括:
|
||||||
|
|
||||||
|
- 检查输入有限性及有效夹根区间;扩展区间和迭代均有 128 次上限。
|
||||||
|
- 牛顿导数无效、候选点越界,或上一步牛顿未使残差至少减半时,回到保留区间的中点。
|
||||||
|
- 对实际返回值重新检查归一化方程残差 `≤1e-9`,并检查流量修正量满足绝对 `1e-13 kg/s` 或相对 `1e-10` 的要求。
|
||||||
|
- 处理小根下溢、大流量溢出、相邻浮点数造成的区间停滞;未满足标准就报告失败,不把最后一次迭代当作成功。
|
||||||
|
|
||||||
|
PNL00R 原有直接层流分支继续保留。物性状态复用、物理方程、整体雅各比构造和积分器类型没有因局部保护而更换。夹根连续性、有效输入和浮点可表示范围仍是条件;这套方法不保证任意系统必然完成,也不能消除接触刚性或所有积分误差。
|
||||||
|
|
||||||
|
## 先核对模型,再比较数值
|
||||||
|
|
||||||
|
交付输入为 [四路 JSON](../../tests/data/test-mql-4-corrected.json)、[同步 XML](../../tests/fixtures/amesim/test-mql-4-corrected.xml) 和 [八路修正版 JSON](../../tests/data/test-mql-8-corrected.json)。完整差异分别见 [四路复核](test-mql-4模型复核与输入修正-2026-09-11.md) 与 [八路修正](test-mql-8模型对照与修正-2026-09-11.md)。
|
||||||
|
|
||||||
|
四路 AME 为 `tests/data/test_mql_4.ame`。其 `.cir` 包含 63 个组件和 18 条建模管线,展开为 81 个公开元件、90 条连接、582 项公开参数、64 个动态状态。修正遗留管阻 `amesim_pnl00r_4.rr` 为 `1e-5`,并将 25 条涉及等价端口编号/阻力方向的连线与 AME 编号逐项对齐。
|
||||||
|
|
||||||
|
用户指出 `pnnode4` 看起来不同后,又直接解析了 `.cir` 的端口变量和连接记录,使用原始唯一别名逐个匹配,独立于原有同构映射。四个 P4NODE2 的真实压力、温度参考口均为 `port_2`,下列连接原本就是一致的:
|
||||||
|
|
||||||
|
| AME 节点别名 | JSON 节点 | 参考口在两侧模型的连接对象 |
|
||||||
|
|---|---|---|
|
||||||
|
| pnnode4_16 | amesim_p4node2_1 | pneumatic_69 / amesim_pnl0001_13 的 port_2 |
|
||||||
|
| pnnode4_17 | amesim_p4node2_2 | pneumatic_68 / amesim_pnl0001_14 的 port_2 |
|
||||||
|
| pnnode4_18 | amesim_p4node2_3 | pneumatic_66 / amesim_pnl0001_15 的 port_2 |
|
||||||
|
| pnnode4_19 | amesim_p4node2_4 | pneumatic_65 / amesim_pnl0001_16 的 port_2 |
|
||||||
|
|
||||||
|
发现的实际显示问题是:三个节点沿用旧工程旋转方向,且前端 P4NODE2 的 3、4 号端口锚点与 AME 几何定义互换。修复图标锚点及修正版工程的旋转/连线路由,物理连接编号和参数保持审计结果。另一次独立参数解析核对了表压加 `101300 Pa`、毫米/面积/容积/刚度/阻尼单位转换和状态初值,未发现新数值差异。公开适配项与 AME 直接字段在审计中单列。
|
||||||
|
|
||||||
|
`native-python-reference.json` 是 50 网络冻结数值基准;`test-mql-4-amesim-reference.json` 是 72 曲线、57 时刻的结果基准,两者都不是工程文件,未改写。AME 四路归档仍包含旧八路编译/结果缓存(132 状态);本轮没有可调用的 Amesim 安装,不能把该缓存当作四路运行。本次比较使用仓库中已有的官方重编译四路参考,原 AME SHA256 与当前文件相符。它不能证明全部时间点、全部输出或接触力都与 Amesim 一致。
|
||||||
|
|
||||||
|
## 相同精度下的求解速度
|
||||||
|
|
||||||
|
使用同一四路输入、同一物性复用实现、同一 SUNDIALS 7.4.0/GCC 13.3.0 环境。BDF,0~10 s,输出步长 0.01 s,最大步长 `1e30`,各算法均使用 `rtol=1e-8`。每个版本预热一次、正式运行三次,交错运行次序;下表为中位数。
|
||||||
|
|
||||||
|
| 局部管流算法 | 纯求解时间 | 进程全程 |
|
||||||
|
|---|---:|---:|
|
||||||
|
| 旧固定点迭代(16/64 次上限、半步松弛) | 1.3309 s | 2.3479 s |
|
||||||
|
| 前次提交的牛顿实现 | 0.9171 s | 1.9009 s |
|
||||||
|
| 本次补全保护的牛顿+二分回退 | 0.9097 s | 1.8962 s |
|
||||||
|
|
||||||
|
本次相对固定点版本纯求解耗时减少 **31.6%**,约为其 **1.46 倍速度**。相对已经采用牛顿的前次提交,约 0.8% 差异不足以认定另有显著加速;本轮新增保护的主要收益是失败处理和收敛条件完整性。前次牛顿与本次版本的全部采样序列、最终输出和最终状态逐值完全相同。
|
||||||
|
|
||||||
|
固定点版本是在明确基线提交的 C 源码中,仅替换回原来的局部半步松弛迭代,不是运行旧版整个应用。因此这张表隔离了管流算法差异,不混入物性复用收益。基准脚本检查替换位置,并保存实际源码、输入快照、哈希、构建键和原始结果。
|
||||||
|
|
||||||
|
此前 `rtol=1e-7` 的同机基准:固定点 1.8731 s、前次牛顿 1.3149 s、本次 1.3074 s。收紧精度后,本模型本次纯求解反而降为 0.9097 s,RHS 次数从 32029 降至 21499;自适应积分路径改变,不能据此推断所有系统收紧误差限都会加速。
|
||||||
|
|
||||||
|
## Amesim 曲线验收
|
||||||
|
|
||||||
|
网页使用的默认误差限由 `1e-7` 改为 `1e-8`,绝对误差限继续按状态类型生成。原误差门槛不放宽。默认 `1e-7` 在确切端口编号对齐后最大温差为 `0.0168374 K`,超过 `0.015 K` 门槛;收紧后为 `0.00882331 K`。
|
||||||
|
|
||||||
|
| 物理量 | 最大绝对误差 | 既有门槛 |
|
||||||
|
|---|---:|---:|
|
||||||
|
| 压力 | 199.671 Pa | 250 Pa |
|
||||||
|
| 温度 | 0.00882331 K | 0.015 K |
|
||||||
|
| 位移 | 0.807942 μm | 2 μm |
|
||||||
|
| 速度 | 2.73964 μm/s | 10 μm/s |
|
||||||
|
| 质量流量 | 0.00707919 g/s | 0.03 g/s |
|
||||||
|
|
||||||
|
72 条曲线在 57 个保存的 Amesim 时刻全部通过。四路运行到 10 s,输出 1003 点(含额外事件点),全量数值有限;闭合气路总质量最大漂移 `2.4425e-14 kg`。误差用原始 native/网页导出数据线性插值到参考时刻计算,没有平滑数据或删去不利比较点。
|
||||||
|
|
||||||
|
等价端口重排改变了浮点加法次序,从而影响自适应积分轨迹。旧/新端口布置在 10 个相同状态上的 RHS 探测,最大缩放差约 `3.95e-15`;新保护与前次牛顿的整条结果也相同。因此收紧系统精度处理的是全系统积分敏感性,不能归因于局部二分保护本身。
|
||||||
|
|
||||||
|
既有 LSTP00A 碰撞事件力峰仍约 `1.50e11 N`,不在这 72 曲线参考范围内。局部求根和本次 `rtol` 调整没有解决该已知问题;原始结果仍保留这些峰值。详见 [此前接口力报告](test-mql-4网页仿真与LSTP00A接口力对比-2026-09-11.md)。
|
||||||
|
|
||||||
|
## 最终真实浏览器验证
|
||||||
|
|
||||||
|
在本次独立服务 `http://127.0.0.1:8011` 运行正式 Vite 构建,Playwright 驱动 Chromium 151.0.7922.34,未模拟 API 或注入预制仿真结果。使用图标修正后的交付 JSON(SHA256 `542494eed8e235cb36eae5f80a0776b812e547d9215fa370949b0882834ebc8b`)。预热一次、正式运行三次:
|
||||||
|
|
||||||
|
| 指标 | 三次正式运行中位数 |
|
||||||
|
|---|---:|
|
||||||
|
| C 内部纯求解 | 0.9249 s |
|
||||||
|
| 原生进程全程 | 1.8465 s |
|
||||||
|
| 点击运行至 HTTP 流结束 | 2.7666 s |
|
||||||
|
| 点击运行至结果可查看 | 3.5504 s |
|
||||||
|
|
||||||
|
“结果可查看”计时包含实际响应、浏览器结果处理、IndexedDB 保存完成,以及切换到结果页并出现导出控件;没有把它称为纯计算时间。各次实际诊断均为 `rtol=1e-8`、10 s 完成、21499 次 RHS、4970 个接受步、243 个拒绝步、1003 个采样点。
|
||||||
|
|
||||||
|
验证了 JSON 导入、真实仿真、图形选择和曲线绘制、结果文件下载、CSV 下载、刷新恢复,以及将下载的结果文件重新导入并显示温度曲线。无 `pageerror`。刷新前后完整结果逐值相等;网页原始结果与独立原生基准的全部曲线和最终输出逐值相同;CSV 的 883 列、1003 行逐单元与网页原始结果一致。结果参考比较也直接针对最终网页导出的 `.simresult` 重做,全部通过。
|
||||||
|
|
||||||
|
证据为 `test/solver-newton-20260911/browser-mql4-final-rtol1e8/` 中的输入快照、4 次 `.simresult`、`result.csv`、`restored.simresult`、`summary.json`、`native-equality.json`、`csv-equality.json`、`temperature-view-proof.json` 和页面截图。`result.png` 为首次选择的质量曲线;`result-temperature.png` 为精确选择温度后的截图。网页显示检查中发现脚本的宽泛 K 文本匹配也会命中 kg,已将可复现脚本改为精确匹配单位 K;该选择问题不影响任何原始数值或计时。
|
||||||
|
|
||||||
|
## 八路修正版的完成性检查
|
||||||
|
|
||||||
|
`test-mql-8-corrected.json` 对齐 157 个元件、178 条连接、1092 个公开参数。修正公共机械质量的 4 项参数(质量 90000→100 kg、限位 -0.72/0→-1/1 m、stoptype 1→4);修改 62 条边的端口,其中 7 个 P4NODE2 的参考口来源原先确实接错,其余包含共享容腔/汇总支路编号对齐。最大积分步长改为 AME 保存的 `1e30`,8 个 P4 显示方向也与 AME 对齐。原 JSON 保留为审计来源,新的实际运行使用 corrected 文件。
|
||||||
|
|
||||||
|
同一修正后执行 XML 和同一内核,在旧 `rtol=1e-7` 下 120 s 超时,推进至 5.144858167 s。按用户确认的新全局 `rtol=1e-8`,单次原生运行完整到 10 s:纯求解 5.9071 s,74265 次 RHS,6974 个接受步、454 个拒绝步,1002 个返回采样,全部有限;56 个气体质量状态的总质量最大漂移 `1.15463e-14 kg`。两次精度不同且只做单次完成性检查,不将其时间比值当作管流优化加速比。
|
||||||
|
|
||||||
|
这证明修正版在当前默认设置下可以完整运行,不等同于八路全部曲线已经与 Amesim 一致。本轮八路没有重跑 Amesim 或制作曲线参考。原始数据位于 `test/solver-newton-20260911/mql8/native-run-rtol1e8/`,旧超时记录仍在同级 `native-run/`。输入 JSON SHA256 为 `670977bef67e62d9c66e8af497bada208bd72a7301be45128d185d47282cf288`。
|
||||||
|
|
||||||
|
## 回归检查
|
||||||
|
|
||||||
|
- 9 个相关后端模块共 **38 项测试全部通过**,无跳过:局部求根、管路物理及四路 Amesim 门槛、物性复用、管流缓存、原生代码生成/打包/API、依赖顺序、50 网络目录基准、原生唯一后端及预热。
|
||||||
|
- 正式前端 TypeScript/Vite 构建通过。**2 项真实 DOM 几何测试通过**:用 AME `COMP_PORT/PORT_POS` 原始坐标验证旋转后的四个实际 Handle,并验证镜像后粗线仍指向 `port_2`;既有 PN3/P4 图形测试同时通过。
|
||||||
|
- 将原生程序复制到新目录,清空 `PATH/PYTHONPATH` 并去除 `LD_LIBRARY_PATH`,初始化得到 64 个状态,独立 BDF 0~10 s 运行成功。二进制哈希与打包清单一致。
|
||||||
|
|
||||||
|
日志分别为 `final-backend-tests.log`、`frontend-final-build.log`、P4 几何测试证据及 `standalone-package-verification.json`。求根单元测试含独立摩擦公式与二分参考、过渡区邻接浮点数、坏导数注入、无夹根/无可接受根及极端流量尺度,不以运行速度作为正确性断言。
|
||||||
|
|
||||||
|
## 环境与复现
|
||||||
|
|
||||||
|
Python 包使用现有 `.venv`。新增原生依赖放在 `.venv/native/sundials-7.4.0/`,浏览器缺失共享库放在 `.venv/native/browser-libs/`;没有安装到系统目录。`.venv/`、`app/data/`、`test/` 均已由现有 Git 规则忽略。只交付可读的环境安装脚本 `bat/setup-native-linux.sh`,不交付环境二进制。Linux 原生构建和预热已补齐,保留 Windows 路径和 DLL 打包分支。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 无需 Amesim 应用的输入核查
|
||||||
|
.venv/bin/python tests/model_audit/audit_mql4_model.py --check
|
||||||
|
|
||||||
|
# 三版本真实原生基准,输出必须选新目录
|
||||||
|
.venv/bin/python tests/manual/benchmark_native_pipe_solver.py \
|
||||||
|
tests/data/test-mql-4-corrected.json --output-dir test/new-pipe-benchmark --runs 3
|
||||||
|
|
||||||
|
# 结果比较;绘图使用已有系统 Python 的 NumPy/Matplotlib
|
||||||
|
python3.12 tests/manual/compare_native_amesim.py \
|
||||||
|
test/solver-newton-20260911/benchmark-mql4-rtol1e8/guarded-newton/run-1/result.json \
|
||||||
|
--output-dir test/new-amesim-comparison --plot
|
||||||
|
```
|
||||||
|
|
||||||
|
运行证据根目录为 `test/solver-newton-20260911/`。`benchmark-mql4-rtol1e8/summary.json` 包含公平计时、实际精度和哈希;`comparison-mql4-rtol1e8/` 包含逐曲线 CSV/JSON 及 SVG/PNG 对照图;`temperature-diagnostics/` 保留选择新默认精度的诊断。全部原始结果与可执行程序保留在本机忽略目录,未加入 Git。
|
||||||
|
|
||||||
|
## 八路真实网页补验
|
||||||
|
|
||||||
|
通过本次 `127.0.0.1:8011` 正式网页,直接导入最终 `test-mql-8-corrected.json`,以全局 `rtol=1e-8` 完整运行到10 s。曲线显示、CSV和结果文件下载、刷新后恢复均成功,无浏览器脚本异常。网页原始曲线和最终输出与独立原生八路运行逐值相同;CSV的1785列、1002行逐单元与原始数据相同。最后一轮为功能补验,不计入四路速度基准。
|
||||||
|
|
||||||
|
完整证据在 `test/solver-newton-20260911/browser-mql8-final/`:`summary.json`、`verification.json`、输入快照、原始/恢复后的`.simresult`、CSV和网页截图。初轮自动化曾因写死四路独有组件ID而在选择曲线时失败,应用求解已完成且无pageerror;脚本改为从当前工程选择真实存在的PNL0002后,上述流程全部通过。
|
||||||
|
|
||||||
|
## 验证快照与并行工作
|
||||||
|
|
||||||
|
本轮数值基准保留实际 C 源码、构建清单、程序与输入快照;网页证据记录了具体输入 SHA256 和实际结果。收尾检查时,工作区出现了并行修改的 `App.tsx`、`SimulationResultsView.tsx`、边线路由、压力单位和相关测试文件。本轮没有覆盖这些改动,也不将此前38项后端测试和网页构建结论扩大为后续并行改动的验证。当前任务核心文件哈希另存于 `test/solver-newton-20260911/final-task-source-hashes.json`。本轮实际执行环境为Linux,Windows构建分支保留,但本轮未在Windows重新运行。
|
||||||
@@ -34,7 +34,14 @@
|
|||||||
- 压力与焓同时互相依赖时,在这个局部块内保留嵌套求解。迭代到上限仍未满足条件,或出现非有限数值,返回求值失败;不把未完成的局部闭合当作成功。
|
- 压力与焓同时互相依赖时,在这个局部块内保留嵌套求解。迭代到上限仍未满足条件,或出现非有限数值,返回求值失败;不把未完成的局部闭合当作成功。
|
||||||
- 条件表达式记录所有可能分支的依赖,覆盖反向与零流量。此做法较为保守:某个特定工况下能进一步简化的关系仍可能留在小循环内。本版没有实现运行时分支特化。
|
- 条件表达式记录所有可能分支的依赖,覆盖反向与零流量。此做法较为保守:某个特定工况下能进一步简化的关系仍可能留在小循环内。本版没有实现运行时分支特化。
|
||||||
|
|
||||||
管路自身的摩擦/流量求根属于元件内部计算。2026-09-11起,PNL管流使用带区间保护的局部Newton求根,保留既有物理分支,并在实际返回点要求方程相对残差不超过 `1e-9`;不再把达到16/64次上限的迭代值直接当成解。上面的系统压力、焓闭合策略,以及积分器和系统雅可比策略仍然保留。系统循环的来源检查和局部划分不代表任意新非线性方程都已得到数值求解支持。
|
管路自身的摩擦/流量求根属于元件内部计算。PNL管流将既有阻力关系写成 `Re² × f(Re) = K`,使用带区间保护的局部Newton求根,保留可压缩流动、摩擦系数混合、流向和近平衡平滑公式。求根器遵守:
|
||||||
|
|
||||||
|
- 先检查有限输入,并通过最多128次上界试算显式确认夹根;无法得到有限且满足上界符号的值时失败,不进入未受保护的迭代。
|
||||||
|
- Newton试点越界、斜率非正或非有限时二分回退。一次Newton试点若未将方程绝对残差至少减半,下一步强制二分,使区间内的无效小步也能触发保护。
|
||||||
|
- 在实际返回候选点检查方程相对残差不超过 `1e-9`,同时保留流量变化的绝对 `1e-13 kg/s` 或相对 `1e-10` 判据;比较过程避免流量尺度溢出造成误判。解析低雷诺数分支也核对残差及返回流量的有限性。
|
||||||
|
- 求根最多128轮。区间已缩至相邻浮点值时核验两端候选,仍不满足上述条件则失败;非有限方程、无法夹根、迭代耗尽均返回失败值,不将最后一次试算当成解。
|
||||||
|
|
||||||
|
PNL00R原有的直接层流分支(对应 `Re ≤ 1000`)继续按其解析式计算;该保留分支不经过上述混合摩擦关系的求根检查。区间法的收敛仍以区间内方程连续等条件为前提,不代表任意经验公式、任意有限输入或整套耦合系统都能成功求解。上面的系统压力、焓闭合策略,以及积分器和系统雅可比策略均保留。系统循环的来源检查和局部划分不代表任意新非线性方程都已得到数值求解支持。
|
||||||
|
|
||||||
每次 `model_eval` 还建立调用者持有的 `NativePropertyCache`,并显式传给局部辅助函数。气体状态准备后登记已有温度、焓和密度;阀口、管路及诊断计算共享相同状态中的有效物性。压力、焓或介质参数改变时按完整输入重新查询,缓存不跨系统试算复用。记录容量满时使用正常计算回退。
|
每次 `model_eval` 还建立调用者持有的 `NativePropertyCache`,并显式传给局部辅助函数。气体状态准备后登记已有温度、焓和密度;阀口、管路及诊断计算共享相同状态中的有效物性。压力、焓或介质参数改变时按完整输入重新查询,缓存不跨系统试算复用。记录容量满时使用正常计算回退。
|
||||||
|
|
||||||
@@ -57,4 +64,4 @@ operations.append(Computation.assignment(
|
|||||||
|
|
||||||
对应测试:`tests/test_native_schedule.py`、`tests/test_native_catalog.py`。本轮模型对照记录见 [计算排序验证记录](../other/C计算依赖排序验证-2026-09-10.md)。
|
对应测试:`tests/test_native_schedule.py`、`tests/test_native_catalog.py`。本轮模型对照记录见 [计算排序验证记录](../other/C计算依赖排序验证-2026-09-10.md)。
|
||||||
|
|
||||||
物性复用和管流求根还由 `tests/test_native_properties.py`、`tests/test_native_pipe_physics.py` 覆盖。实现边界与实测结果见 [物性复用与管流求根实现及验证](../other/物性复用与管流求根实现及验证-2026-09-11.md)。
|
物性复用和管流求根还由 `tests/test_native_properties.py`、`tests/test_native_pipe_physics.py` 覆盖。`tests/test_native_pipe_solver.py` 仅依赖Python标准库和C编译器,通过独立二分、切换邻点及临时测试副本中的斜率/方程故障注入,验证区间保护、残差、回退与明确失败。实现边界与实测结果见 [物性复用与管流求根实现及验证](../other/物性复用与管流求根实现及验证-2026-09-11.md)。
|
||||||
@@ -138,6 +138,8 @@ v3 不保存 `name/componentType/x/y/rotation/mirrored`。其中:
|
|||||||
|
|
||||||
### 5.2 `Parameter`
|
### 5.2 `Parameter`
|
||||||
|
|
||||||
|
平台压力统一采用绝压,默认显示单位为 `Pa`,可切换为 `kPa`、`MPa` 或 `bar`。这些显示单位只作比例换算,不增加或减去大气压偏移,`1 bar = 100000 Pa`。工程 JSON 中的数值参数保存 SI 值,`parameterUnits` 保存显示单位;表达式按保存的显示单位求值后换算到 SI。例如输入 `2.5 bar` 时,XML 的压力参数值为 `250000`。后端仿真输入与压力结果均使用绝压 Pa。
|
||||||
|
|
||||||
```xml
|
```xml
|
||||||
<Parameter name="direction" value="-1"/>
|
<Parameter name="direction" value="-1"/>
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
# 2026-09-11:C 管路验证版合入网页后端
|
# 2026-09-11:C 管路验证版合入网页后端
|
||||||
|
|
||||||
|
数据目录整理:按用户要求,`tests/data` 的 JSON/XML 仅保留可在浏览器导入、且已与对应 AME 参数和端口对齐的 `test-mql-4-corrected.json` 与 `test-mql-8-corrected.json`。其余8份资料逐字节迁到 `tests/fixtures` / `tests/baselines`;活动测试、审计脚本、CI、基准清单路径和当前报告已同步。两个工程真实浏览器导入及XML下载通过,导出的物理参数、连接和仿真设置与工程输入一致;8项路径/清单相关回归通过。具体去向见 [目录说明](../../tests/data/README.md)。
|
||||||
|
|
||||||
|
最新补充:本轮完成管流牛顿迭代的有限夹根、残差进展检查与二分回退;按用户确认将网页/API 默认 `rtol` 改为 `1e-8`。重新核对四路/八路 AME 图纸并交付 `tests/data/test-mql-4-corrected.json` 与 `tests/data/test-mql-8-corrected.json`;用户复核发现的 P4NODE2 显示方向和 3/4 号端口锚点错误一并修正。相同新精度下,相比局部半步松弛迭代,四路纯求解中位耗时 1.3309→0.9097 s(减少31.6%);72条已有Amesim参考曲线均通过原门槛,最大温差0.00882331 K。38项后端测试与2项P4真实端口几何测试通过;四路最终网页从点击到结果可查看中位3.55 s。八路修正版在新精度下单次完整完成10 s,纯求解5.91 s。最新验收范围、网页证据和八路运行限制以 [本轮完整报告](../other/牛顿管流求根与四路网页验证-2026-09-11.md) 为准;下文旧默认精度和旧输入路径为此前历史记录。
|
||||||
|
|
||||||
当天后续更新:已将物性状态复用、等熵计算中的密度复用、焓直接计算,以及检查残差的管流求根接入正式C内核和两条生成路径。最终25项回归通过;MQL4纯求解中位耗时3.138→1.391 s,skill-test 0.255→0.139 s。已启动更新后的本机后台并以真实HTTP请求验证。详见 [实现与验证报告](../other/物性复用与管流求根实现及验证-2026-09-11.md)。下文为当天较早一次合入的历史记录。
|
当天后续更新:已将物性状态复用、等熵计算中的密度复用、焓直接计算,以及检查残差的管流求根接入正式C内核和两条生成路径。最终25项回归通过;MQL4纯求解中位耗时3.138→1.391 s,skill-test 0.255→0.139 s。已启动更新后的本机后台并以真实HTTP请求验证。详见 [实现与验证报告](../other/物性复用与管流求根实现及验证-2026-09-11.md)。下文为当天较早一次合入的历史记录。
|
||||||
|
|
||||||
后续网页检查补充:LSTP00A 接口力在新增碰撞事件点存在极短高峰,原有共同采样点回归没有覆盖该现象。详情及对原曲线一致性结论的限定见 [接口力对比报告](../other/test-mql-4网页仿真与LSTP00A接口力对比-2026-09-11.md)。
|
后续网页检查补充:LSTP00A 接口力在新增碰撞事件点存在极短高峰,原有共同采样点回归没有覆盖该现象。详情及对原曲线一致性结论的限定见 [接口力对比报告](../other/test-mql-4网页仿真与LSTP00A接口力对比-2026-09-11.md)。
|
||||||
|
|||||||
+64
-4
@@ -29,6 +29,7 @@ import {
|
|||||||
FileDown,
|
FileDown,
|
||||||
FileUp,
|
FileUp,
|
||||||
FlipHorizontal2,
|
FlipHorizontal2,
|
||||||
|
FlipVertical2,
|
||||||
FolderOpen,
|
FolderOpen,
|
||||||
Grip,
|
Grip,
|
||||||
ListChecks,
|
ListChecks,
|
||||||
@@ -5950,6 +5951,50 @@ function FlowWorkbench() {
|
|||||||
);
|
);
|
||||||
}, [appendConsoleEntry, recordHistory, selectedNodeId, setEdges, setNodes]);
|
}, [appendConsoleEntry, recordHistory, selectedNodeId, setEdges, setNodes]);
|
||||||
|
|
||||||
|
const mirrorSelectionVertically = useCallback(() => {
|
||||||
|
const selectedIds = new Set(
|
||||||
|
nodesRef.current
|
||||||
|
.filter((node) => node.selected || node.id === selectedNodeId)
|
||||||
|
.map((node) => node.id),
|
||||||
|
);
|
||||||
|
if (selectedIds.size === 0) {
|
||||||
|
appendConsoleEntry("warning", "请先选择需要镜像的组件");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
recordHistory();
|
||||||
|
const previousNodes = nodesRef.current;
|
||||||
|
const mirroredNodes = previousNodes.map((node) =>
|
||||||
|
selectedIds.has(node.id)
|
||||||
|
? {
|
||||||
|
...node,
|
||||||
|
data: {
|
||||||
|
...node.data,
|
||||||
|
// Reflect in canvas coordinates: Sy R(theta) Sx(m) = R(180-theta) Sx(!m).
|
||||||
|
rotation: normalizeNodeRotation((540 - normalizeNodeRotation(node.data.rotation)) % 360),
|
||||||
|
mirrored: !node.data.mirrored,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: node,
|
||||||
|
);
|
||||||
|
const { remainingEdges, removedEdges } = disconnectInvalidatedContactEdges(
|
||||||
|
previousNodes,
|
||||||
|
mirroredNodes,
|
||||||
|
edgesRef.current,
|
||||||
|
selectedIds,
|
||||||
|
);
|
||||||
|
nodesRef.current = mirroredNodes;
|
||||||
|
edgesRef.current = remainingEdges;
|
||||||
|
setNodes(mirroredNodes);
|
||||||
|
if (removedEdges.length > 0) {
|
||||||
|
setEdges(remainingEdges);
|
||||||
|
appendConsoleEntry(
|
||||||
|
"info",
|
||||||
|
`镜像使接口分离,已断开 ${removedEdges.length} 个接触连接`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
appendConsoleEntry("info", `已竖直镜像 ${selectedIds.size} 个组件`);
|
||||||
|
}, [appendConsoleEntry, recordHistory, selectedNodeId, setEdges, setNodes]);
|
||||||
|
|
||||||
const handleNodesChange = useCallback(
|
const handleNodesChange = useCallback(
|
||||||
(changes: NodeChange<SimulationNode>[]) => {
|
(changes: NodeChange<SimulationNode>[]) => {
|
||||||
const removesNodes = changes.some((change) => change.type === "remove");
|
const removesNodes = changes.some((change) => change.type === "remove");
|
||||||
@@ -7391,7 +7436,7 @@ function FlowWorkbench() {
|
|||||||
const loadedEdges = edgeNormalization.edges;
|
const loadedEdges = edgeNormalization.edges;
|
||||||
setNodes(loadedNodes);
|
setNodes(loadedNodes);
|
||||||
setEdges(loadedEdges);
|
setEdges(loadedEdges);
|
||||||
setProjectName(project.name || "untitled");
|
setProjectName(project.name.trim() || "untitled");
|
||||||
setSimulationConfig({ ...defaultSimulationConfig, ...project.simulation });
|
setSimulationConfig({ ...defaultSimulationConfig, ...project.simulation });
|
||||||
setSelectedNodeId(null);
|
setSelectedNodeId(null);
|
||||||
setSelectedEdgeId(null);
|
setSelectedEdgeId(null);
|
||||||
@@ -7472,7 +7517,8 @@ function FlowWorkbench() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
applyProject(
|
applyProject(
|
||||||
project,
|
// Older entries may still contain a name that differs from their storage key.
|
||||||
|
{ ...project, name },
|
||||||
`工程已加载:${project.nodes.length} 个组件,${project.edges.length} 条连接`,
|
`工程已加载:${project.nodes.length} 个组件,${project.edges.length} 条连接`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -7505,7 +7551,11 @@ function FlowWorkbench() {
|
|||||||
if (!project) {
|
if (!project) {
|
||||||
throw new Error("文件不是有效的 ReactFlow 工程 JSON");
|
throw new Error("文件不是有效的 ReactFlow 工程 JSON");
|
||||||
}
|
}
|
||||||
applyProject(project, `已导入工程:${file.name}`);
|
const importedName = file.name.trim().replace(/\.json$/i, "").trim();
|
||||||
|
applyProject(
|
||||||
|
{ ...project, name: importedName || project.name.trim() || "untitled" },
|
||||||
|
`已导入工程:${file.name}`,
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
appendConsoleEntry("error", `导入失败:${formatError(error)}`);
|
appendConsoleEntry("error", `导入失败:${formatError(error)}`);
|
||||||
}
|
}
|
||||||
@@ -8058,7 +8108,7 @@ function FlowWorkbench() {
|
|||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
pendingPasteRef.current &&
|
pendingPasteRef.current &&
|
||||||
((event.shiftKey && !commandKey && !event.altKey && key === "h") ||
|
((event.shiftKey && !commandKey && !event.altKey && ["h", "v"].includes(key)) ||
|
||||||
(commandKey && ["a", "c", "v", "y", "z"].includes(key)))
|
(commandKey && ["a", "c", "v", "y", "z"].includes(key)))
|
||||||
) {
|
) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -8067,6 +8117,9 @@ function FlowWorkbench() {
|
|||||||
if (event.shiftKey && !commandKey && !event.altKey && key === "h") {
|
if (event.shiftKey && !commandKey && !event.altKey && key === "h") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
mirrorSelection();
|
mirrorSelection();
|
||||||
|
} else if (event.shiftKey && !commandKey && !event.altKey && key === "v") {
|
||||||
|
event.preventDefault();
|
||||||
|
mirrorSelectionVertically();
|
||||||
} else if (commandKey && key === "z") {
|
} else if (commandKey && key === "z") {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.shiftKey ? redo() : undo();
|
event.shiftKey ? redo() : undo();
|
||||||
@@ -8093,6 +8146,7 @@ function FlowWorkbench() {
|
|||||||
cancelPortConnection,
|
cancelPortConnection,
|
||||||
copySelection,
|
copySelection,
|
||||||
mirrorSelection,
|
mirrorSelection,
|
||||||
|
mirrorSelectionVertically,
|
||||||
pasteSelection,
|
pasteSelection,
|
||||||
redo,
|
redo,
|
||||||
rotatePendingPaste,
|
rotatePendingPaste,
|
||||||
@@ -8753,6 +8807,12 @@ function FlowWorkbench() {
|
|||||||
onClick={mirrorSelection}
|
onClick={mirrorSelection}
|
||||||
pressed={allSelectedNodesMirrored}
|
pressed={allSelectedNodesMirrored}
|
||||||
/>
|
/>
|
||||||
|
<ToolbarButton
|
||||||
|
disabled={Boolean(pendingPaste) || selectedNodesForTools.length === 0}
|
||||||
|
icon={FlipVertical2}
|
||||||
|
label="竖直镜像 (Shift+V)"
|
||||||
|
onClick={mirrorSelectionVertically}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<span aria-hidden="true" className="toolbar-separator" />
|
<span aria-hidden="true" className="toolbar-separator" />
|
||||||
<div aria-label="画布显示" className="toolbar-group" role="group">
|
<div aria-label="画布显示" className="toolbar-group" role="group">
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
createOrthogonalSegmentDetour,
|
createOrthogonalSegmentDetour,
|
||||||
edgeSegmentAxis,
|
edgeSegmentAxis,
|
||||||
moveOrthogonalCorner,
|
|
||||||
moveOrthogonalSegment,
|
moveOrthogonalSegment,
|
||||||
orthogonalEdgePoints,
|
orthogonalEdgePoints,
|
||||||
orthogonalPolylinePath,
|
orthogonalPolylinePath,
|
||||||
@@ -25,7 +24,7 @@ export const CONTACT_AWARE_EDGE_TYPE = "contact-aware";
|
|||||||
type RouteDrag = {
|
type RouteDrag = {
|
||||||
basePoints: EdgeRoutePoint[];
|
basePoints: EdgeRoutePoint[];
|
||||||
cleanup: () => void;
|
cleanup: () => void;
|
||||||
kind: "corner" | "detour" | "segment";
|
kind: "detour" | "segment";
|
||||||
pointerId: number;
|
pointerId: number;
|
||||||
routeIndex: number;
|
routeIndex: number;
|
||||||
startPointer: EdgeRoutePoint;
|
startPointer: EdgeRoutePoint;
|
||||||
@@ -122,9 +121,7 @@ function RoutedEdge({
|
|||||||
y: pointer.y - drag.startPointer.y,
|
y: pointer.y - drag.startPointer.y,
|
||||||
};
|
};
|
||||||
const nextPoints =
|
const nextPoints =
|
||||||
drag.kind === "corner"
|
drag.kind === "detour"
|
||||||
? moveOrthogonalCorner(drag.basePoints, drag.routeIndex, delta)
|
|
||||||
: drag.kind === "detour"
|
|
||||||
? createOrthogonalSegmentDetour(
|
? createOrthogonalSegmentDetour(
|
||||||
drag.basePoints,
|
drag.basePoints,
|
||||||
drag.routeIndex,
|
drag.routeIndex,
|
||||||
@@ -166,12 +163,15 @@ function RoutedEdge({
|
|||||||
.slice(0, -1)
|
.slice(0, -1)
|
||||||
.map((start, index) => ({ end: points[index + 1], index, start }))
|
.map((start, index) => ({ end: points[index + 1], index, start }))
|
||||||
.filter(({ index }) => index > 0 && index < points.length - 2);
|
.filter(({ index }) => index > 0 && index < points.length - 2);
|
||||||
const internalCorners = points
|
|
||||||
.map((point, index) => ({ index, point }))
|
|
||||||
.filter(({ index }) => index > 0 && index < points.length - 1);
|
|
||||||
const fallbackIndex = longestSegmentIndex(points);
|
const fallbackIndex = longestSegmentIndex(points);
|
||||||
const fallbackStart = points[fallbackIndex];
|
|
||||||
const fallbackEnd = points[fallbackIndex + 1];
|
const fallbackEnd = points[fallbackIndex + 1];
|
||||||
|
// Straight and L-shaped routes need a detour before an internal segment exists.
|
||||||
|
const draggableSegments =
|
||||||
|
internalSegments.length > 0
|
||||||
|
? internalSegments
|
||||||
|
: fallbackEnd
|
||||||
|
? [{ end: fallbackEnd, index: fallbackIndex, start: points[fallbackIndex] }]
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -184,7 +184,7 @@ function RoutedEdge({
|
|||||||
style={style}
|
style={style}
|
||||||
/>
|
/>
|
||||||
{editable
|
{editable
|
||||||
? internalSegments.map(({ end, index, start }) => (
|
? draggableSegments.map(({ end, index, start }) => (
|
||||||
<g
|
<g
|
||||||
className="manual-edge-segment-control"
|
className="manual-edge-segment-control"
|
||||||
key={`${id}-segment-${index}`}
|
key={`${id}-segment-${index}`}
|
||||||
@@ -195,7 +195,12 @@ function RoutedEdge({
|
|||||||
data-edge-id={id}
|
data-edge-id={id}
|
||||||
data-segment-index={index}
|
data-segment-index={index}
|
||||||
onPointerDown={(event) =>
|
onPointerDown={(event) =>
|
||||||
beginRouteDrag(event, points, "segment", index)
|
beginRouteDrag(
|
||||||
|
event,
|
||||||
|
points,
|
||||||
|
internalSegments.length > 0 ? "segment" : "detour",
|
||||||
|
index,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
x1={start.x}
|
x1={start.x}
|
||||||
x2={end.x}
|
x2={end.x}
|
||||||
@@ -213,36 +218,6 @@ function RoutedEdge({
|
|||||||
</g>
|
</g>
|
||||||
))
|
))
|
||||||
: null}
|
: null}
|
||||||
{editable
|
|
||||||
? internalCorners.map(({ index, point }) => (
|
|
||||||
<circle
|
|
||||||
aria-label={`双向调整连接线拐点 ${index}`}
|
|
||||||
className="manual-edge-corner-handle"
|
|
||||||
cx={point.x}
|
|
||||||
cy={point.y}
|
|
||||||
data-corner-index={index}
|
|
||||||
data-edge-id={id}
|
|
||||||
key={`${id}-corner-${index}`}
|
|
||||||
onPointerDown={(event) =>
|
|
||||||
beginRouteDrag(event, points, "corner", index)
|
|
||||||
}
|
|
||||||
r={4.5}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
: null}
|
|
||||||
{editable && internalCorners.length === 0 && fallbackEnd ? (
|
|
||||||
<circle
|
|
||||||
aria-label="双向调整连接线路由"
|
|
||||||
className="manual-edge-route-handle"
|
|
||||||
cx={(fallbackStart.x + fallbackEnd.x) / 2}
|
|
||||||
cy={(fallbackStart.y + fallbackEnd.y) / 2}
|
|
||||||
data-edge-id={id}
|
|
||||||
onPointerDown={(event) =>
|
|
||||||
beginRouteDrag(event, points, "detour", fallbackIndex)
|
|
||||||
}
|
|
||||||
r={5}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1781,7 +1781,7 @@ export function SimulationResultsView({
|
|||||||
</strong>
|
</strong>
|
||||||
<span>
|
<span>
|
||||||
{isCombined
|
{isCombined
|
||||||
? `${visibleCombinedVariables.length}/${combinedVariables.length} 条显示 · ${isMixed ? "上下同步图" : variable.unit || "无量纲"}`
|
? `${visibleCombinedVariables.length}/${combinedVariables.length} 条显示 · ${isMixed ? "上下同步图" : selectedUnit.label || "无量纲"}`
|
||||||
: componentName}
|
: componentName}
|
||||||
{separatedEventSampleCount > 0
|
{separatedEventSampleCount > 0
|
||||||
? ` · 已隐藏 ${separatedEventSampleCount} 个孤立事件力尖峰`
|
? ` · 已隐藏 ${separatedEventSampleCount} 个孤立事件力尖峰`
|
||||||
@@ -1910,7 +1910,7 @@ export function SimulationResultsView({
|
|||||||
<strong>
|
<strong>
|
||||||
{isMixed
|
{isMixed
|
||||||
? "多单位"
|
? "多单位"
|
||||||
: variable.unit || "无量纲"}
|
: selectedUnit.label || "无量纲"}
|
||||||
</strong>
|
</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="result-chart-multi-menu-list">
|
<div className="result-chart-multi-menu-list">
|
||||||
@@ -1930,11 +1930,12 @@ export function SimulationResultsView({
|
|||||||
);
|
);
|
||||||
const itemUnitOptions =
|
const itemUnitOptions =
|
||||||
resultUnitOptions(item);
|
resultUnitOptions(item);
|
||||||
const itemUnit = resolveResultUnitOption(
|
const itemUnit = isMixed
|
||||||
|
? resolveResultUnitOption(
|
||||||
item,
|
item,
|
||||||
chartWindow.variableUnits[item.key] ??
|
chartWindow.variableUnits[item.key] ?? item.unit,
|
||||||
item.unit,
|
)
|
||||||
);
|
: selectedUnit;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="result-chart-multi-menu-row"
|
className="result-chart-multi-menu-row"
|
||||||
|
|||||||
@@ -246,8 +246,9 @@ export const PN3NODE2_PORT_ANCHORS = {
|
|||||||
export const P4NODE2_PORT_ANCHORS = {
|
export const P4NODE2_PORT_ANCHORS = {
|
||||||
port1: { x: 32, y: 11.2 },
|
port1: { x: 32, y: 11.2 },
|
||||||
port2: { x: 19.2, y: 24 },
|
port2: { x: 19.2, y: 24 },
|
||||||
port3: { x: 44.8, y: 24 },
|
// AME P4NODE2 geometry 1 has port 1 opposite port 3 and port 2 opposite port 4.
|
||||||
port4: { x: 32, y: 36.8 },
|
port3: { x: 32, y: 36.8 },
|
||||||
|
port4: { x: 44.8, y: 24 },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/** Three-port pneumatic junction with one inlet-side and two outlet-side ports. */
|
/** Three-port pneumatic junction with one inlet-side and two outlet-side ports. */
|
||||||
|
|||||||
@@ -459,97 +459,6 @@ export function moveOrthogonalSegment(
|
|||||||
return simplifyOrthogonalPoints(nextPoints);
|
return simplifyOrthogonalPoints(nextPoints);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Moves an internal bend in both axes while preserving an orthogonal route.
|
|
||||||
* Moving the bend also slides its two neighbouring bends along their existing
|
|
||||||
* perpendicular legs, which keeps the two live port endpoints fixed.
|
|
||||||
*/
|
|
||||||
export function moveOrthogonalCorner(
|
|
||||||
points: EdgeRoutePoint[],
|
|
||||||
cornerIndex: number,
|
|
||||||
delta: EdgeRoutePoint,
|
|
||||||
) {
|
|
||||||
const nextPoints = points.map((point) => ({ ...point }));
|
|
||||||
if (cornerIndex <= 0 || cornerIndex >= nextPoints.length - 1) {
|
|
||||||
return nextPoints;
|
|
||||||
}
|
|
||||||
const previous = nextPoints[cornerIndex - 1];
|
|
||||||
const corner = nextPoints[cornerIndex];
|
|
||||||
const incomingAxis = edgeSegmentAxis(previous, corner);
|
|
||||||
const outgoingAxis = edgeSegmentAxis(
|
|
||||||
corner,
|
|
||||||
nextPoints[cornerIndex + 1],
|
|
||||||
);
|
|
||||||
|
|
||||||
corner.x += delta.x;
|
|
||||||
corner.y += delta.y;
|
|
||||||
|
|
||||||
if (cornerIndex > 1) {
|
|
||||||
if (incomingAxis === "horizontal") {
|
|
||||||
previous.y = corner.y;
|
|
||||||
} else {
|
|
||||||
previous.x = corner.x;
|
|
||||||
}
|
|
||||||
} else if (incomingAxis === "horizontal") {
|
|
||||||
const middleX =
|
|
||||||
Math.abs(previous.x - corner.x) > ROUTE_EPSILON
|
|
||||||
? (previous.x + corner.x) / 2
|
|
||||||
: previous.x + 20;
|
|
||||||
nextPoints.splice(
|
|
||||||
cornerIndex,
|
|
||||||
0,
|
|
||||||
{ x: middleX, y: previous.y },
|
|
||||||
{ x: middleX, y: corner.y },
|
|
||||||
);
|
|
||||||
cornerIndex += 2;
|
|
||||||
} else {
|
|
||||||
const middleY =
|
|
||||||
Math.abs(previous.y - corner.y) > ROUTE_EPSILON
|
|
||||||
? (previous.y + corner.y) / 2
|
|
||||||
: previous.y + 20;
|
|
||||||
nextPoints.splice(
|
|
||||||
cornerIndex,
|
|
||||||
0,
|
|
||||||
{ x: previous.x, y: middleY },
|
|
||||||
{ x: corner.x, y: middleY },
|
|
||||||
);
|
|
||||||
cornerIndex += 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
const movedCorner = nextPoints[cornerIndex];
|
|
||||||
const following = nextPoints[cornerIndex + 1];
|
|
||||||
if (cornerIndex < nextPoints.length - 2) {
|
|
||||||
if (outgoingAxis === "horizontal") {
|
|
||||||
following.y = movedCorner.y;
|
|
||||||
} else {
|
|
||||||
following.x = movedCorner.x;
|
|
||||||
}
|
|
||||||
} else if (outgoingAxis === "horizontal") {
|
|
||||||
const middleX =
|
|
||||||
Math.abs(movedCorner.x - following.x) > ROUTE_EPSILON
|
|
||||||
? (movedCorner.x + following.x) / 2
|
|
||||||
: following.x - 20;
|
|
||||||
nextPoints.splice(
|
|
||||||
cornerIndex + 1,
|
|
||||||
0,
|
|
||||||
{ x: middleX, y: movedCorner.y },
|
|
||||||
{ x: middleX, y: following.y },
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
const middleY =
|
|
||||||
Math.abs(movedCorner.y - following.y) > ROUTE_EPSILON
|
|
||||||
? (movedCorner.y + following.y) / 2
|
|
||||||
: following.y - 20;
|
|
||||||
nextPoints.splice(
|
|
||||||
cornerIndex + 1,
|
|
||||||
0,
|
|
||||||
{ x: movedCorner.x, y: middleY },
|
|
||||||
{ x: following.x, y: middleY },
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return simplifyOrthogonalPoints(nextPoints);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Creates a movable dogleg when a selected route is still a straight line. */
|
/** Creates a movable dogleg when a selected route is still a straight line. */
|
||||||
export function createOrthogonalSegmentDetour(
|
export function createOrthogonalSegmentDetour(
|
||||||
points: EdgeRoutePoint[],
|
points: EdgeRoutePoint[],
|
||||||
|
|||||||
@@ -3767,23 +3767,6 @@ textarea {
|
|||||||
cursor: ew-resize;
|
cursor: ew-resize;
|
||||||
}
|
}
|
||||||
|
|
||||||
.manual-edge-route-handle,
|
|
||||||
.manual-edge-corner-handle {
|
|
||||||
fill: #ffffff;
|
|
||||||
stroke: #1675c1;
|
|
||||||
stroke-width: 2;
|
|
||||||
cursor: move;
|
|
||||||
pointer-events: all;
|
|
||||||
touch-action: none;
|
|
||||||
vector-effect: non-scaling-stroke;
|
|
||||||
}
|
|
||||||
|
|
||||||
.manual-edge-corner-handle:hover,
|
|
||||||
.manual-edge-route-handle:hover {
|
|
||||||
fill: #d9edff;
|
|
||||||
stroke: #0f5f9f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flow-canvas.connection-planning .react-flow__pane {
|
.flow-canvas.connection-planning .react-flow__pane {
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { expect, test, type Locator, type Page } from "@playwright/test";
|
import { expect, test, type Locator, type Page } from "@playwright/test";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
|
||||||
import { expandSimulationConsole } from "./fixtures";
|
import { expandSimulationConsole } from "./fixtures";
|
||||||
|
|
||||||
@@ -2439,8 +2440,8 @@ test("PN3NODE2 与 P4NODE2 使用参考图直线汇流结构且粗线对应 port
|
|||||||
anchors: [
|
anchors: [
|
||||||
{ portName: "port_1", x: 32, y: 11.2 },
|
{ portName: "port_1", x: 32, y: 11.2 },
|
||||||
{ portName: "port_2", x: 19.2, y: 24 },
|
{ portName: "port_2", x: 19.2, y: 24 },
|
||||||
{ portName: "port_3", x: 44.8, y: 24 },
|
{ portName: "port_3", x: 32, y: 36.8 },
|
||||||
{ portName: "port_4", x: 32, y: 36.8 },
|
{ portName: "port_4", x: 44.8, y: 24 },
|
||||||
],
|
],
|
||||||
center: { x: 32, y: 24 },
|
center: { x: 32, y: 24 },
|
||||||
finePath: "M32 11.2V36.8M32 24H44.8M19.2 24H22.1",
|
finePath: "M32 11.2V36.8M32 24H44.8M19.2 24H22.1",
|
||||||
@@ -2448,8 +2449,8 @@ test("PN3NODE2 与 P4NODE2 使用参考图直线汇流结构且粗线对应 port
|
|||||||
positions: {
|
positions: {
|
||||||
port_1: "top",
|
port_1: "top",
|
||||||
port_2: "left",
|
port_2: "left",
|
||||||
port_3: "right",
|
port_3: "bottom",
|
||||||
port_4: "bottom",
|
port_4: "right",
|
||||||
},
|
},
|
||||||
thickPath: "M22.1 24H32",
|
thickPath: "M22.1 24H32",
|
||||||
},
|
},
|
||||||
@@ -2505,6 +2506,57 @@ test("PN3NODE2 与 P4NODE2 使用参考图直线汇流结构且粗线对应 port
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("P4NODE2 的真实 Handle 在 180° 时匹配 AME 四口方位,镜像后参考粗线仍指向 port_2", async ({ page }) => {
|
||||||
|
const source = JSON.parse(readFileSync(new URL(
|
||||||
|
"../../../tests/model_audit/p4node2-ame-geometry.json", import.meta.url,
|
||||||
|
), "utf8")) as {
|
||||||
|
components: Array<{ geometry: number; ports: Record<string, { x: number; y: number }> }>;
|
||||||
|
};
|
||||||
|
// Independent AME COMP_PORT/PORT_POS coordinates, rather than frontend anchors.
|
||||||
|
const amePorts = source.components[0].ports;
|
||||||
|
for (const component of source.components) {
|
||||||
|
expect(component.geometry).toBe(1);
|
||||||
|
expect(component.ports).toEqual(amePorts);
|
||||||
|
}
|
||||||
|
const ameCenter = Object.values(amePorts).reduce(
|
||||||
|
(sum, port) => ({ x: sum.x + port.x / 4, y: sum.y + port.y / 4 }),
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
);
|
||||||
|
await page.goto("/");
|
||||||
|
const node = await dragSymbolToCanvas(page, "amesim_p4node2", { x: 460, y: 320 });
|
||||||
|
const rotate = page.getByRole("button", { name: "顺时针旋转 90° (Ctrl+R)", exact: true });
|
||||||
|
await rotate.click();
|
||||||
|
await rotate.click();
|
||||||
|
const svg = node.locator('[data-component-symbol="amesim_p4node2"] svg');
|
||||||
|
const getDirection = async (portName: string) => {
|
||||||
|
const center = await locatorCenter(svg.locator('[data-junction-part="center"]'));
|
||||||
|
const handle = await locatorCenter(node.locator(`.port-handle[data-port-name="${portName}"]`));
|
||||||
|
const length = Math.hypot(handle.x - center.x, handle.y - center.y);
|
||||||
|
expect(length).toBeGreaterThan(1);
|
||||||
|
return { x: (handle.x - center.x) / length, y: (handle.y - center.y) / length };
|
||||||
|
};
|
||||||
|
for (const [portName, coordinate] of Object.entries(amePorts)) {
|
||||||
|
const length = Math.hypot(coordinate.x - ameCenter.x, coordinate.y - ameCenter.y);
|
||||||
|
await expect.poll(async () => {
|
||||||
|
const actual = await getDirection(portName);
|
||||||
|
return Math.hypot(actual.x - (coordinate.x - ameCenter.x) / length,
|
||||||
|
actual.y - (coordinate.y - ameCenter.y) / length);
|
||||||
|
}).toBeLessThan(0.02);
|
||||||
|
}
|
||||||
|
const expectReferenceStroke = async () => {
|
||||||
|
const center = await locatorCenter(svg.locator('[data-junction-part="center"]'));
|
||||||
|
const stroke = await locatorCenter(svg.locator('[data-junction-part="collector-branch"]'));
|
||||||
|
const reference = await getDirection("port_2");
|
||||||
|
const length = Math.hypot(stroke.x - center.x, stroke.y - center.y);
|
||||||
|
expect((stroke.x - center.x) * reference.x + (stroke.y - center.y) * reference.y).toBeGreaterThan(0);
|
||||||
|
expect(Math.abs((stroke.x - center.x) * reference.y - (stroke.y - center.y) * reference.x) / length).toBeLessThan(0.02);
|
||||||
|
};
|
||||||
|
await expectReferenceStroke();
|
||||||
|
await page.getByRole("button", { name: "水平镜像 (Shift+H)", exact: true }).click();
|
||||||
|
await expect.poll(async () => (await getDirection("port_2")).x).toBeLessThan(-0.99);
|
||||||
|
await expectReferenceStroke();
|
||||||
|
});
|
||||||
|
|
||||||
test("PNL00R 与 PNL0001-0003 使用 standard 画布和 60% 工作区占比", async ({
|
test("PNL00R 与 PNL0001-0003 使用 standard 画布和 60% 工作区占比", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ test("复制块保留内部连接,同时未连接端口仍参与待放置吸
|
|||||||
await expect(targetPort).toHaveAttribute("data-connected", "true");
|
await expect(targetPort).toHaveAttribute("data-connected", "true");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("正交连接线可拖动内部线段,且无连接交叉点显示跨线桥", async ({
|
test("选中正交连接线不显示拐点,仍可拖动线段、撤销并保存跨线桥路由", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const project = {
|
const project = {
|
||||||
@@ -342,6 +342,7 @@ test("正交连接线可拖动内部线段,且无连接交叉点显示跨线
|
|||||||
.locator(".react-flow__edge-interaction")
|
.locator(".react-flow__edge-interaction")
|
||||||
.dispatchEvent("click");
|
.dispatchEvent("click");
|
||||||
await expect(horizontalEdge).toHaveClass(/selected|editor-edge-selected/);
|
await expect(horizontalEdge).toHaveClass(/selected|editor-edge-selected/);
|
||||||
|
await expect(horizontalEdge.locator("circle")).toHaveCount(0);
|
||||||
const horizontalSegment = horizontalEdge
|
const horizontalSegment = horizontalEdge
|
||||||
.locator(".manual-edge-segment-handle.horizontal")
|
.locator(".manual-edge-segment-handle.horizontal")
|
||||||
.first();
|
.first();
|
||||||
@@ -404,125 +405,21 @@ test("正交连接线可拖动内部线段,且无连接交叉点显示跨线
|
|||||||
pathBeforeVerticalDrag ?? "",
|
pathBeforeVerticalDrag ?? "",
|
||||||
);
|
);
|
||||||
|
|
||||||
const cornerHandle = horizontalEdge.locator(
|
const pathAfterSegmentDrags = await horizontalPath.getAttribute("d");
|
||||||
'.manual-edge-corner-handle[data-corner-index="2"]',
|
await expect(horizontalEdge.locator("circle")).toHaveCount(0);
|
||||||
);
|
|
||||||
await expect(cornerHandle).toHaveCount(1);
|
|
||||||
const cornerBefore = await cornerHandle.evaluate(
|
|
||||||
(circle: SVGCircleElement) => ({
|
|
||||||
flowX: circle.cx.baseVal.value,
|
|
||||||
flowY: circle.cy.baseVal.value,
|
|
||||||
screen: (() => {
|
|
||||||
const matrix = circle.getScreenCTM();
|
|
||||||
if (!matrix) {
|
|
||||||
throw new Error("无法获取连接线拐点坐标");
|
|
||||||
}
|
|
||||||
const point = new DOMPoint(
|
|
||||||
circle.cx.baseVal.value,
|
|
||||||
circle.cy.baseVal.value,
|
|
||||||
).matrixTransform(matrix);
|
|
||||||
return { x: point.x, y: point.y };
|
|
||||||
})(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const pathBeforeCornerDrag = await horizontalPath.getAttribute("d");
|
|
||||||
await page.mouse.move(cornerBefore.screen.x, cornerBefore.screen.y);
|
|
||||||
await page.mouse.down();
|
|
||||||
await page.mouse.move(
|
|
||||||
cornerBefore.screen.x + 32,
|
|
||||||
cornerBefore.screen.y + 24,
|
|
||||||
{ steps: 6 },
|
|
||||||
);
|
|
||||||
await page.mouse.up();
|
|
||||||
await expect(horizontalPath).not.toHaveAttribute(
|
|
||||||
"d",
|
|
||||||
pathBeforeCornerDrag ?? "",
|
|
||||||
);
|
|
||||||
const cornerAfter = await cornerHandle.evaluate(
|
|
||||||
(circle: SVGCircleElement) => ({
|
|
||||||
flowX: circle.cx.baseVal.value,
|
|
||||||
flowY: circle.cy.baseVal.value,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
expect(Math.abs(cornerAfter.flowX - cornerBefore.flowX)).toBeGreaterThan(1);
|
|
||||||
expect(Math.abs(cornerAfter.flowY - cornerBefore.flowY)).toBeGreaterThan(1);
|
|
||||||
|
|
||||||
await page.keyboard.press("Control+z");
|
await page.keyboard.press("Control+z");
|
||||||
await expect(horizontalPath).toHaveAttribute(
|
await expect(horizontalPath).toHaveAttribute(
|
||||||
"d",
|
"d",
|
||||||
pathBeforeCornerDrag ?? "",
|
pathBeforeVerticalDrag ?? "",
|
||||||
);
|
);
|
||||||
await horizontalEdge
|
await page.keyboard.press("Control+y");
|
||||||
.locator(".react-flow__edge-interaction")
|
await expect(horizontalPath).toHaveAttribute(
|
||||||
.dispatchEvent("click");
|
|
||||||
await expect(horizontalEdge).toHaveClass(/selected|editor-edge-selected/);
|
|
||||||
|
|
||||||
const boundaryCorner = horizontalEdge.locator(
|
|
||||||
'.manual-edge-corner-handle[data-corner-index="1"]',
|
|
||||||
);
|
|
||||||
await expect(boundaryCorner).toHaveCount(1);
|
|
||||||
const boundaryStart = await boundaryCorner.evaluate(
|
|
||||||
(circle: SVGCircleElement) => {
|
|
||||||
const matrix = circle.getScreenCTM();
|
|
||||||
if (!matrix) {
|
|
||||||
throw new Error("无法获取端点相邻拐点坐标");
|
|
||||||
}
|
|
||||||
const point = new DOMPoint(
|
|
||||||
circle.cx.baseVal.value,
|
|
||||||
circle.cy.baseVal.value,
|
|
||||||
).matrixTransform(matrix);
|
|
||||||
return { x: point.x, y: point.y };
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const boundaryPathLocator = horizontalEdge.locator(".react-flow__edge-path");
|
|
||||||
const boundaryPathBefore = await boundaryPathLocator.getAttribute("d");
|
|
||||||
const boundaryTarget = {
|
|
||||||
x: boundaryStart.x + 38,
|
|
||||||
y: boundaryStart.y + 30,
|
|
||||||
};
|
|
||||||
await page.mouse.move(boundaryStart.x, boundaryStart.y);
|
|
||||||
await page.mouse.down();
|
|
||||||
await page.mouse.move(
|
|
||||||
(boundaryStart.x + boundaryTarget.x) / 2,
|
|
||||||
(boundaryStart.y + boundaryTarget.y) / 2,
|
|
||||||
{ steps: 4 },
|
|
||||||
);
|
|
||||||
await page.mouse.move(boundaryTarget.x, boundaryTarget.y, { steps: 4 });
|
|
||||||
await page.mouse.up();
|
|
||||||
await expect(boundaryPathLocator).not.toHaveAttribute(
|
|
||||||
"d",
|
"d",
|
||||||
boundaryPathBefore ?? "",
|
pathAfterSegmentDrags ?? "",
|
||||||
);
|
);
|
||||||
await expect(boundaryPathLocator).toHaveAttribute("d", / Q /);
|
|
||||||
|
|
||||||
const cornerScreenPoints = await horizontalEdge
|
|
||||||
.locator(".manual-edge-corner-handle")
|
|
||||||
.evaluateAll((circles: SVGCircleElement[]) =>
|
|
||||||
circles.map((circle) => {
|
|
||||||
const matrix = circle.getScreenCTM();
|
|
||||||
if (!matrix) {
|
|
||||||
throw new Error("无法获取调整后的拐点坐标");
|
|
||||||
}
|
|
||||||
const point = new DOMPoint(
|
|
||||||
circle.cx.baseVal.value,
|
|
||||||
circle.cy.baseVal.value,
|
|
||||||
).matrixTransform(matrix);
|
|
||||||
return { x: point.x, y: point.y };
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const closestCornerDistance = Math.min(
|
|
||||||
...cornerScreenPoints.map((point) =>
|
|
||||||
Math.hypot(point.x - boundaryTarget.x, point.y - boundaryTarget.y),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
closestCornerDistance,
|
|
||||||
JSON.stringify({ boundaryStart, boundaryTarget, cornerScreenPoints }),
|
|
||||||
).toBeLessThan(3);
|
|
||||||
|
|
||||||
const boundaryPathAfter = await boundaryPathLocator.getAttribute("d");
|
|
||||||
const linePoints = [
|
const linePoints = [
|
||||||
...(boundaryPathAfter ?? "").matchAll(
|
...(pathAfterSegmentDrags ?? "").matchAll(
|
||||||
/[ML]\s+(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)/g,
|
/[ML]\s+(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)/g,
|
||||||
),
|
),
|
||||||
].map((match) => ({ x: Number(match[1]), y: Number(match[2]) }));
|
].map((match) => ({ x: Number(match[1]), y: Number(match[2]) }));
|
||||||
@@ -623,14 +520,24 @@ test("缩小画布后连接线端点仍与端口图标边缘对齐", async ({ pa
|
|||||||
expect(alignment!.targetGapY).toBeLessThan(0.75);
|
expect(alignment!.targetGapY).toBeLessThan(0.75);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("直线首次折弯后即使原手柄卸载,拖动仍连续且保持正交", async ({
|
for (const route of [
|
||||||
|
{ label: "直线", targetY: 260, targetRotation: 0, pointCount: 2 },
|
||||||
|
{ label: "L 型连接线", targetY: 480, targetRotation: 90, pointCount: 3 },
|
||||||
|
]) {
|
||||||
|
test(`${route.label}可直接拖动线条,首次折弯后仍连续且保持正交`, async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
const project = {
|
const project = {
|
||||||
...structuredClone(wideProject),
|
...structuredClone(wideProject),
|
||||||
nodes: [
|
nodes: [
|
||||||
projectNode("straight-source", 0, 260),
|
projectNode("straight-source", 0, 260),
|
||||||
projectNode("straight-target", 700, 260),
|
{
|
||||||
|
...projectNode("straight-target", 700, route.targetY),
|
||||||
|
data: {
|
||||||
|
...projectNode("straight-target", 700, route.targetY).data,
|
||||||
|
rotation: route.targetRotation,
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
edges: [
|
edges: [
|
||||||
{
|
{
|
||||||
@@ -657,20 +564,25 @@ test("直线首次折弯后即使原手柄卸载,拖动仍连续且保持正
|
|||||||
);
|
);
|
||||||
const edgePath = edge.locator(".react-flow__edge-path");
|
const edgePath = edge.locator(".react-flow__edge-path");
|
||||||
await edge.locator(".react-flow__edge-interaction").dispatchEvent("click");
|
await edge.locator(".react-flow__edge-interaction").dispatchEvent("click");
|
||||||
const routeHandle = edge.locator(".manual-edge-route-handle");
|
await expect(edge.locator("circle")).toHaveCount(0);
|
||||||
|
const routeHandle = edge.locator(
|
||||||
|
'.manual-edge-segment-handle[data-segment-index="0"]',
|
||||||
|
);
|
||||||
await expect(routeHandle).toHaveCount(1);
|
await expect(routeHandle).toHaveCount(1);
|
||||||
const dragStart = await routeHandle.evaluate((circle: SVGCircleElement) => {
|
await expect(routeHandle).toHaveCSS("pointer-events", "stroke");
|
||||||
const matrix = circle.getScreenCTM();
|
const dragStart = await routeHandle.evaluate((line: SVGLineElement) => {
|
||||||
|
const matrix = line.getScreenCTM();
|
||||||
if (!matrix) {
|
if (!matrix) {
|
||||||
throw new Error("无法获取直线路由手柄坐标");
|
throw new Error("无法获取待拖动连接线段坐标");
|
||||||
}
|
}
|
||||||
const point = new DOMPoint(
|
const point = new DOMPoint(
|
||||||
circle.cx.baseVal.value,
|
line.x1.baseVal.value * 0.75 + line.x2.baseVal.value * 0.25,
|
||||||
circle.cy.baseVal.value,
|
line.y1.baseVal.value * 0.75 + line.y2.baseVal.value * 0.25,
|
||||||
).matrixTransform(matrix);
|
).matrixTransform(matrix);
|
||||||
return { x: point.x, y: point.y };
|
return { x: point.x, y: point.y };
|
||||||
});
|
});
|
||||||
const pathBefore = await edgePath.getAttribute("d");
|
const pathBefore = await edgePath.getAttribute("d");
|
||||||
|
expect([...(pathBefore ?? "").matchAll(/[ML]\s/g)]).toHaveLength(route.pointCount);
|
||||||
await page.mouse.move(dragStart.x, dragStart.y);
|
await page.mouse.move(dragStart.x, dragStart.y);
|
||||||
await page.mouse.down();
|
await page.mouse.down();
|
||||||
await page.mouse.move(dragStart.x + 18, dragStart.y + 21, { steps: 4 });
|
await page.mouse.move(dragStart.x + 18, dragStart.y + 21, { steps: 4 });
|
||||||
@@ -709,6 +621,7 @@ test("直线首次折弯后即使原手柄卸载,拖动仍连续且保持正
|
|||||||
await page.mouse.up();
|
await page.mouse.up();
|
||||||
expect(Math.abs(segmentXAtFullMove - segmentXAtHalfMove)).toBeGreaterThan(10);
|
expect(Math.abs(segmentXAtFullMove - segmentXAtHalfMove)).toBeGreaterThan(10);
|
||||||
await expect(edgePath).not.toHaveAttribute("d", pathBefore ?? "");
|
await expect(edgePath).not.toHaveAttribute("d", pathBefore ?? "");
|
||||||
|
await expect(edge.locator("circle")).toHaveCount(0);
|
||||||
await expect(
|
await expect(
|
||||||
edge.locator(".manual-edge-segment-handle.horizontal").first(),
|
edge.locator(".manual-edge-segment-handle.horizontal").first(),
|
||||||
).toHaveCount(1);
|
).toHaveCount(1);
|
||||||
@@ -726,7 +639,8 @@ test("直线首次折弯后即使原手柄卸载,拖动仍连续且保持正
|
|||||||
Math.abs(point.y - previous.y) < 0.01,
|
Math.abs(point.y - previous.y) < 0.01,
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
test("同一组件不同端口发出的线路在远端交叉时仍显示跨线桥", async ({
|
test("同一组件不同端口发出的线路在远端交叉时仍显示跨线桥", async ({
|
||||||
page,
|
page,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { expect, test, type Page } from "@playwright/test";
|
import { expect, test, type Page } from "@playwright/test";
|
||||||
import { existsSync, readFileSync } from "node:fs";
|
import { readFileSync } from "node:fs";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import {
|
import {
|
||||||
expectAllNodesInsideCanvas,
|
expectAllNodesInsideCanvas,
|
||||||
@@ -22,12 +22,9 @@ type ImportedProject = {
|
|||||||
edges: ImportedProjectEdge[];
|
edges: ImportedProjectEdge[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const MQL_8_PROJECT_PATHS = [
|
const MQL_8_PROJECT_PATH = fileURLToPath(
|
||||||
new URL("../../../tests/data/test-mql-8%20.json", import.meta.url),
|
new URL("../../../tests/fixtures/legacy/test-mql-8.json", import.meta.url),
|
||||||
new URL("../../../tests/data/test-mql-8.json", import.meta.url),
|
);
|
||||||
].map(fileURLToPath);
|
|
||||||
const MQL_8_PROJECT_PATH =
|
|
||||||
MQL_8_PROJECT_PATHS.find(existsSync) ?? MQL_8_PROJECT_PATHS[0];
|
|
||||||
const mql8Project = JSON.parse(
|
const mql8Project = JSON.parse(
|
||||||
readFileSync(MQL_8_PROJECT_PATH, "utf8"),
|
readFileSync(MQL_8_PROJECT_PATH, "utf8"),
|
||||||
) as ImportedProject;
|
) as ImportedProject;
|
||||||
|
|||||||
@@ -210,8 +210,8 @@ test("组件参数和仿真设置使用共享的三列表格", async ({ page })
|
|||||||
const solver = page.getByLabel("求解器", { exact: true });
|
const solver = page.getByLabel("求解器", { exact: true });
|
||||||
await expect(solver).toHaveCSS("text-align", "right");
|
await expect(solver).toHaveCSS("text-align", "right");
|
||||||
await expect(solver).toHaveCSS("text-align-last", "right");
|
await expect(solver).toHaveCSS("text-align-last", "right");
|
||||||
await solver.selectOption("Radau");
|
await solver.selectOption("RK45");
|
||||||
await expect(solver).toHaveValue("Radau");
|
await expect(solver).toHaveValue("RK45");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("通用离散选项按多条件控制参数行且保留隐藏值和未知旧值", async ({
|
test("通用离散选项按多条件控制参数行且保留隐藏值和未知旧值", async ({
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import { expect, test, type Page } from "@playwright/test";
|
||||||
|
import { expandSimulationConsole, prepareApp, wideProject } from "./fixtures";
|
||||||
|
|
||||||
|
const componentId = "amesim_pneumatic_test_1";
|
||||||
|
const pressureInput = (page: Page) =>
|
||||||
|
page.getByLabel("参考压力", { exact: true });
|
||||||
|
const pressureUnit = (page: Page) =>
|
||||||
|
page.getByLabel("参考压力单位", { exact: true });
|
||||||
|
const selectPressureComponent = (page: Page) =>
|
||||||
|
page.locator(`.flow-canvas .react-flow__node[data-id="${componentId}"]`).click();
|
||||||
|
|
||||||
|
type ExportedProject = {
|
||||||
|
nodes: Array<{
|
||||||
|
data: {
|
||||||
|
parameters: Record<string, number | string>;
|
||||||
|
parameterUnits: Record<string, string>;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function exportProject(page: Page) {
|
||||||
|
const downloadPromise = page.waitForEvent("download");
|
||||||
|
await page.getByRole("button", { name: "导出工程 JSON", exact: true }).click();
|
||||||
|
const download = await downloadPromise;
|
||||||
|
const stream = await download.createReadStream();
|
||||||
|
if (!stream) throw new Error("无法读取导出的工程 JSON");
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
for await (const chunk of stream) chunks.push(Buffer.from(chunk));
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
return {
|
||||||
|
buffer,
|
||||||
|
project: JSON.parse(buffer.toString("utf8")) as ExportedProject,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectPressureXml(page: Page, expectedValue: number) {
|
||||||
|
await page.getByRole("button", { name: "生成系统 XML", exact: true }).click();
|
||||||
|
await expandSimulationConsole(page);
|
||||||
|
await expect(page.getByLabel("生成的系统 XML").last()).toContainText(
|
||||||
|
`<Parameter name="reference_pressure" value="${expectedValue}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addPressureComponent(page: Page) {
|
||||||
|
await page
|
||||||
|
.getByRole("button", { name: "AMESim 气动测试元件", exact: true })
|
||||||
|
.dragTo(page.locator(".flow-canvas .react-flow__pane"), {
|
||||||
|
targetPosition: { x: 620, y: 330 },
|
||||||
|
});
|
||||||
|
await selectPressureComponent(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
function legacyPressureProject(value: number | string, unit?: string) {
|
||||||
|
return {
|
||||||
|
...wideProject,
|
||||||
|
nodes: [{
|
||||||
|
...wideProject.nodes[0],
|
||||||
|
id: componentId,
|
||||||
|
data: {
|
||||||
|
...wideProject.nodes[0].data,
|
||||||
|
label: componentId,
|
||||||
|
componentType: "amesim_pneumatic_test",
|
||||||
|
modelType: "amesim_pneumatic_test",
|
||||||
|
parameters: {
|
||||||
|
gi: 0,
|
||||||
|
reference_pressure: value,
|
||||||
|
reference_temperature: 300,
|
||||||
|
},
|
||||||
|
parameterUnits: unit ? { reference_pressure: unit } : undefined,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
edges: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await prepareApp(page);
|
||||||
|
await page.goto("/");
|
||||||
|
await expect(
|
||||||
|
page.getByRole("button", { name: "AMESim 气动测试元件", exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("压力默认显示 Pa,bar 与 Pa 换算保持绝压且无大气压偏移", async ({ page }) => {
|
||||||
|
await addPressureComponent(page);
|
||||||
|
await expect(pressureUnit(page)).toHaveValue("Pa");
|
||||||
|
await expect(pressureInput(page)).toHaveValue("100000");
|
||||||
|
await expect(pressureUnit(page).locator('option[value="barA"]')).toHaveCount(0);
|
||||||
|
await pressureUnit(page).selectOption("bar");
|
||||||
|
await expect(pressureInput(page)).toHaveValue("1");
|
||||||
|
await pressureInput(page).fill("2.5");
|
||||||
|
await pressureInput(page).press("Enter");
|
||||||
|
await expectPressureXml(page, 250000);
|
||||||
|
|
||||||
|
for (const { unit, value } of [
|
||||||
|
{ unit: "Pa", value: "250000" },
|
||||||
|
{ unit: "kPa", value: "250" },
|
||||||
|
{ unit: "MPa", value: "0.25" },
|
||||||
|
{ unit: "bar", value: "2.5" },
|
||||||
|
]) {
|
||||||
|
await pressureUnit(page).selectOption(unit);
|
||||||
|
await expect(pressureInput(page)).toHaveValue(value);
|
||||||
|
await expectPressureXml(page, 250000);
|
||||||
|
}
|
||||||
|
const exported = await exportProject(page);
|
||||||
|
expect(exported.project.nodes[0].data.parameters.reference_pressure).toBe(250000);
|
||||||
|
expect(exported.project.nodes[0].data.parameterUnits.reference_pressure).toBe("bar");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bar 压力公式在保存、加载和 JSON 导入导出后保留原式与单位", async ({ page }) => {
|
||||||
|
await addPressureComponent(page);
|
||||||
|
await pressureUnit(page).selectOption("bar");
|
||||||
|
const expression = "1+1.5";
|
||||||
|
await pressureInput(page).fill(expression);
|
||||||
|
await pressureInput(page).press("Enter");
|
||||||
|
await expect(pressureUnit(page)).toHaveValue("bar");
|
||||||
|
await expectPressureXml(page, 250000);
|
||||||
|
await page.getByRole("button", { name: "保存工程", exact: true }).click();
|
||||||
|
|
||||||
|
await pressureInput(page).fill("7");
|
||||||
|
await pressureInput(page).press("Enter");
|
||||||
|
await pressureUnit(page).selectOption("kPa");
|
||||||
|
await page.getByRole("button", { name: "加载工程", exact: true }).click();
|
||||||
|
await selectPressureComponent(page);
|
||||||
|
await expect(pressureInput(page)).toHaveValue(expression);
|
||||||
|
await expect(pressureUnit(page)).toHaveValue("bar");
|
||||||
|
|
||||||
|
const exported = await exportProject(page);
|
||||||
|
expect(exported.project.nodes[0].data.parameters.reference_pressure).toBe(expression);
|
||||||
|
expect(exported.project.nodes[0].data.parameterUnits.reference_pressure).toBe("bar");
|
||||||
|
await pressureInput(page).fill("9");
|
||||||
|
await pressureInput(page).press("Enter");
|
||||||
|
await page.locator('input[type="file"]').setInputFiles({
|
||||||
|
name: "pressure-expression.json",
|
||||||
|
mimeType: "application/json",
|
||||||
|
buffer: exported.buffer,
|
||||||
|
});
|
||||||
|
// Wait for the imported project before selecting its component.
|
||||||
|
await expect(page.getByRole("textbox", { name: "工程", exact: true })).toHaveValue(
|
||||||
|
"pressure-expression",
|
||||||
|
);
|
||||||
|
await selectPressureComponent(page);
|
||||||
|
await expect(pressureInput(page)).toHaveValue(expression);
|
||||||
|
await expect(pressureUnit(page)).toHaveValue("bar");
|
||||||
|
await expectPressureXml(page, 250000);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const { storedUnit, displayedUnit, displayedValue } of [
|
||||||
|
{ storedUnit: "Pa", displayedUnit: "Pa", displayedValue: "250000" },
|
||||||
|
{ storedUnit: undefined, displayedUnit: "Pa", displayedValue: "250000" },
|
||||||
|
]) {
|
||||||
|
test(`旧 JSON 数值参数单位 ${storedUnit ?? "未记录"} 加载为 ${displayedUnit},SI 数值不变`, async ({ page }) => {
|
||||||
|
await page.locator('input[type="file"]').setInputFiles({
|
||||||
|
name: "legacy-pressure.json",
|
||||||
|
mimeType: "application/json",
|
||||||
|
buffer: Buffer.from(JSON.stringify(legacyPressureProject(250000, storedUnit))),
|
||||||
|
});
|
||||||
|
await selectPressureComponent(page);
|
||||||
|
await expect(pressureUnit(page)).toHaveValue(displayedUnit);
|
||||||
|
await expect(pressureInput(page)).toHaveValue(displayedValue);
|
||||||
|
await expectPressureXml(page, 250000);
|
||||||
|
const exported = await exportProject(page);
|
||||||
|
expect(exported.project.nodes[0].data.parameters.reference_pressure).toBe(250000);
|
||||||
|
expect(exported.project.nodes[0].data.parameterUnits.reference_pressure).toBe(displayedUnit);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
test("旧 JSON 公式没有记录单位时沿用原 Pa 含义", async ({ page }) => {
|
||||||
|
const expression = "100000+150000";
|
||||||
|
await page.locator('input[type="file"]').setInputFiles({
|
||||||
|
name: "legacy-pressure-expression.json",
|
||||||
|
mimeType: "application/json",
|
||||||
|
buffer: Buffer.from(JSON.stringify(legacyPressureProject(expression))),
|
||||||
|
});
|
||||||
|
await selectPressureComponent(page);
|
||||||
|
await expect(pressureUnit(page)).toHaveValue("Pa");
|
||||||
|
await expect(pressureInput(page)).toHaveValue(expression);
|
||||||
|
await expectPressureXml(page, 250000);
|
||||||
|
const exported = await exportProject(page);
|
||||||
|
expect(exported.project.nodes[0].data.parameters.reference_pressure).toBe(expression);
|
||||||
|
expect(exported.project.nodes[0].data.parameterUnits.reference_pressure).toBe("Pa");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("弹性接触的剪切模量 G 保持 Pa 默认单位", async ({ page }) => {
|
||||||
|
await page
|
||||||
|
.getByRole("button", { name: "LSTP00A 弹性接触", exact: true })
|
||||||
|
.dragTo(page.locator(".flow-canvas .react-flow__pane"), {
|
||||||
|
targetPosition: { x: 620, y: 330 },
|
||||||
|
});
|
||||||
|
await page.locator('.flow-canvas .react-flow__node[data-id="amesim_lstp00a_1"]').click();
|
||||||
|
await page.getByLabel("刚度模式", { exact: true }).selectOption("2");
|
||||||
|
await page.getByRole("table", { name: "组件参数" }).getByRole("button", { name: /刚度/ }).click();
|
||||||
|
await expect(page.getByLabel("剪切模量单位", { exact: true })).toHaveValue("Pa");
|
||||||
|
await expect(page.getByLabel("剪切模量", { exact: true })).toHaveValue("85700000000");
|
||||||
|
const exported = await exportProject(page);
|
||||||
|
expect(exported.project.nodes[0].data.parameters.G).toBe(8.57e10);
|
||||||
|
expect(exported.project.nodes[0].data.parameterUnits.G).toBe("Pa");
|
||||||
|
});
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
import { expect, test, type Page } from "@playwright/test";
|
||||||
|
import { prepareApp, wideProject } from "./fixtures";
|
||||||
|
|
||||||
|
const projectNameField = (page: Page) =>
|
||||||
|
page.getByRole("textbox", { name: "工程", exact: true });
|
||||||
|
|
||||||
|
async function exportProject(page: Page) {
|
||||||
|
const downloadPromise = page.waitForEvent("download");
|
||||||
|
await page.getByRole("button", { name: "导出工程 JSON", exact: true }).click();
|
||||||
|
const download = await downloadPromise;
|
||||||
|
const stream = await download.createReadStream();
|
||||||
|
if (!stream) throw new Error("无法读取导出的工程 JSON");
|
||||||
|
const chunks: Buffer[] = [];
|
||||||
|
for await (const chunk of stream) chunks.push(Buffer.from(chunk));
|
||||||
|
const buffer = Buffer.concat(chunks);
|
||||||
|
return {
|
||||||
|
filename: download.suggestedFilename(),
|
||||||
|
buffer,
|
||||||
|
project: JSON.parse(buffer.toString("utf8")) as typeof wideProject,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteFirstNode(page: Page) {
|
||||||
|
await page
|
||||||
|
.locator('.flow-canvas .react-flow__node[data-id="generic_sensor_1"]')
|
||||||
|
.click();
|
||||||
|
await page.getByRole("button", { name: "删除 (Delete)", exact: true }).click();
|
||||||
|
await expect(page.locator(".flow-canvas .react-flow__node")).toHaveCount(
|
||||||
|
wideProject.nodes.length - 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await prepareApp(page);
|
||||||
|
await page.goto("/");
|
||||||
|
await expect(page.getByRole("button", { name: /通用测试元件/ })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("外部重命名的工程 JSON 导入后名称一致,保存和刷新后均可加载", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.locator('input[type="file"]').setInputFiles({
|
||||||
|
name: `${wideProject.name}.json`,
|
||||||
|
mimeType: "application/json",
|
||||||
|
buffer: Buffer.from(JSON.stringify(wideProject)),
|
||||||
|
});
|
||||||
|
await expect(page.locator(".flow-canvas .react-flow__node")).toHaveCount(
|
||||||
|
wideProject.nodes.length,
|
||||||
|
);
|
||||||
|
const original = await exportProject(page);
|
||||||
|
expect(original.filename).toBe(`${wideProject.name}.json`);
|
||||||
|
expect(original.project.name).toBe(wideProject.name);
|
||||||
|
|
||||||
|
// Only the external filename changes; the exported JSON still has its old name.
|
||||||
|
const renamedProject = "气动 回路.v2";
|
||||||
|
await page.locator('input[type="file"]').setInputFiles({
|
||||||
|
name: `${renamedProject}.JSON`,
|
||||||
|
mimeType: "application/json",
|
||||||
|
buffer: original.buffer,
|
||||||
|
});
|
||||||
|
await expect(projectNameField(page)).toHaveValue(renamedProject);
|
||||||
|
const renamed = await exportProject(page);
|
||||||
|
expect(renamed.filename).toBe(`${renamedProject}.json`);
|
||||||
|
expect(renamed.project.name).toBe(renamedProject);
|
||||||
|
expect(renamed.project.nodes).toEqual(original.project.nodes);
|
||||||
|
expect(renamed.project.simulation).toEqual(original.project.simulation);
|
||||||
|
|
||||||
|
await page.getByRole("button", { name: "保存工程", exact: true }).click();
|
||||||
|
const saved = await page.evaluate((name) => {
|
||||||
|
const raw = localStorage.getItem(
|
||||||
|
`system-simulation-flow:project:${encodeURIComponent(name)}`,
|
||||||
|
);
|
||||||
|
return raw ? JSON.parse(raw) : null;
|
||||||
|
}, renamedProject);
|
||||||
|
expect(saved).toMatchObject({ name: renamedProject });
|
||||||
|
expect(saved.nodes).toHaveLength(wideProject.nodes.length);
|
||||||
|
|
||||||
|
await deleteFirstNode(page);
|
||||||
|
await page.getByRole("button", { name: "加载工程", exact: true }).click();
|
||||||
|
await expect(page.locator(".flow-canvas .react-flow__node")).toHaveCount(
|
||||||
|
wideProject.nodes.length,
|
||||||
|
);
|
||||||
|
await expect(projectNameField(page)).toHaveValue(renamedProject);
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
await page.getByRole("button", { name: "忽略", exact: true }).click();
|
||||||
|
await expect(page.locator(".flow-canvas .react-flow__node")).toHaveCount(0);
|
||||||
|
await projectNameField(page).fill(renamedProject);
|
||||||
|
await page.getByRole("button", { name: "加载工程", exact: true }).click();
|
||||||
|
await expect(page.locator(".flow-canvas .react-flow__node")).toHaveCount(
|
||||||
|
wideProject.nodes.length,
|
||||||
|
);
|
||||||
|
await expect(projectNameField(page)).toHaveValue(renamedProject);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("浏览器工程内容保留旧名称时,按存储名称连续加载仍能恢复工程", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const storedName = "已重命名 工程.v3";
|
||||||
|
await page.evaluate(
|
||||||
|
({ name, project }) => {
|
||||||
|
localStorage.setItem(
|
||||||
|
`system-simulation-flow:project:${encodeURIComponent(name)}`,
|
||||||
|
JSON.stringify(project),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
{ name: storedName, project: wideProject },
|
||||||
|
);
|
||||||
|
await projectNameField(page).fill(storedName);
|
||||||
|
await page.getByRole("button", { name: "加载工程", exact: true }).click();
|
||||||
|
await expect(page.locator(".flow-canvas .react-flow__node")).toHaveCount(
|
||||||
|
wideProject.nodes.length,
|
||||||
|
);
|
||||||
|
await expect(projectNameField(page)).toHaveValue(storedName);
|
||||||
|
|
||||||
|
await deleteFirstNode(page);
|
||||||
|
await page.getByRole("button", { name: "加载工程", exact: true }).click();
|
||||||
|
await expect(page.locator(".flow-canvas .react-flow__node")).toHaveCount(
|
||||||
|
wideProject.nodes.length,
|
||||||
|
);
|
||||||
|
await expect(projectNameField(page)).toHaveValue(storedName);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const { internalName, expectedName } of [
|
||||||
|
{ internalName: " 内部 工程.v1 ", expectedName: "内部 工程.v1" },
|
||||||
|
{ internalName: " ", expectedName: "untitled" },
|
||||||
|
]) {
|
||||||
|
test(`外部文件名没有基名时,工程名回退为 ${expectedName}`, async ({ page }) => {
|
||||||
|
await page.locator('input[type="file"]').setInputFiles({
|
||||||
|
name: " .JSON",
|
||||||
|
mimeType: "application/json",
|
||||||
|
buffer: Buffer.from(JSON.stringify({ ...wideProject, name: internalName })),
|
||||||
|
});
|
||||||
|
await expect(projectNameField(page)).toHaveValue(expectedName);
|
||||||
|
const exported = await exportProject(page);
|
||||||
|
expect(exported.filename).toBe(`${expectedName}.json`);
|
||||||
|
expect(exported.project.name).toBe(expectedName);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -211,6 +211,114 @@ test.beforeEach(async ({ page }) => {
|
|||||||
}, chartWindowSnapshot);
|
}, chartWindowSnapshot);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const quantity of ["pressure", "absolute_pressure"]) {
|
||||||
|
for (const kind of ["single", "multi", "mixed"] as const) {
|
||||||
|
test(`${quantity} 的 ${kind} 图默认 Pa,绝压换算及单位持久化不改变原始数据`, async ({ page }) => {
|
||||||
|
await page.addInitScript(({ snapshot, pressureQuantity }) => {
|
||||||
|
const pressure = snapshot.result.variables.find(
|
||||||
|
(variable) => variable.key === "generic_sensor_1.pressure",
|
||||||
|
);
|
||||||
|
if (pressure) pressure.quantity = pressureQuantity;
|
||||||
|
window.sessionStorage.setItem(
|
||||||
|
"system-simulation-flow:latest-result",
|
||||||
|
JSON.stringify(snapshot),
|
||||||
|
);
|
||||||
|
}, { snapshot: chartWindowSnapshot, pressureQuantity: quantity });
|
||||||
|
await page.goto("/");
|
||||||
|
await openResults(page);
|
||||||
|
|
||||||
|
const pressureVariable = page
|
||||||
|
.locator(".results-variable-list button")
|
||||||
|
.filter({ hasText: "压力" })
|
||||||
|
.first();
|
||||||
|
await expect(pressureVariable.locator("small")).toHaveText("Pa");
|
||||||
|
const chartWindow = page.locator(
|
||||||
|
`.result-chart-window[data-chart-kind="${kind}"]`,
|
||||||
|
);
|
||||||
|
if (kind === "single") {
|
||||||
|
await pressureVariable.click();
|
||||||
|
} else {
|
||||||
|
await page.getByRole("button", {
|
||||||
|
name: kind === "multi" ? "新建同单位多曲线对比窗口" : "新建异单位上下对比窗口",
|
||||||
|
exact: true,
|
||||||
|
}).click();
|
||||||
|
if (kind === "mixed") {
|
||||||
|
await page.locator(".results-variable-list button")
|
||||||
|
.filter({ hasText: "数值" }).first().dragTo(chartWindow);
|
||||||
|
}
|
||||||
|
await pressureVariable.dragTo(chartWindow);
|
||||||
|
}
|
||||||
|
const chart = chartWindow.locator('svg[data-result-chart="true"]');
|
||||||
|
const unitSelect = chartWindow.getByRole("combobox", {
|
||||||
|
name: kind === "mixed"
|
||||||
|
? "generic_sensor_1.pressure纵轴单位"
|
||||||
|
: kind === "multi" ? "多曲线纵轴单位" : "压力纵轴单位",
|
||||||
|
exact: true,
|
||||||
|
});
|
||||||
|
const showUnits = async () => {
|
||||||
|
if (kind === "mixed") {
|
||||||
|
await chartWindow.getByRole("button", {
|
||||||
|
name: "管理多曲线窗口中的曲线", exact: true,
|
||||||
|
}).click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const readPressureRange = () => chart.evaluate((element) => {
|
||||||
|
const ranges = element.getAttribute("data-view-y-ranges");
|
||||||
|
return ranges
|
||||||
|
? JSON.parse(ranges)["generic_sensor_1.pressure"] as number[]
|
||||||
|
: [
|
||||||
|
Number(element.getAttribute("data-view-y-min")),
|
||||||
|
Number(element.getAttribute("data-view-y-max")),
|
||||||
|
];
|
||||||
|
});
|
||||||
|
await showUnits();
|
||||||
|
await expect(unitSelect).toHaveValue("Pa");
|
||||||
|
await expect(unitSelect.locator('option[value="barA"]')).toHaveCount(0);
|
||||||
|
await expect(chart.locator(".result-chart-unit")).toContainText(["Pa"]);
|
||||||
|
const pascalRange = await readPressureRange();
|
||||||
|
expect(pascalRange[0]).toBeCloseTo(98400, 6);
|
||||||
|
expect(pascalRange[1]).toBeCloseTo(121600, 6);
|
||||||
|
|
||||||
|
await unitSelect.selectOption("bar");
|
||||||
|
await expect(unitSelect).toHaveValue("bar");
|
||||||
|
const barRange = await readPressureRange();
|
||||||
|
expect(barRange[0]).toBeCloseTo(0.984, 6);
|
||||||
|
expect(barRange[1]).toBeCloseTo(1.216, 6);
|
||||||
|
if (kind === "multi") {
|
||||||
|
await expect(chartWindow.locator(".result-chart-window-title")).toContainText("bar");
|
||||||
|
await chartWindow.getByRole("button", {
|
||||||
|
name: "管理多曲线窗口中的曲线", exact: true,
|
||||||
|
}).click();
|
||||||
|
await expect(chartWindow.locator(".result-chart-multi-menu-label small")).toContainText("bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
const storedPressure = await page.evaluate(() => {
|
||||||
|
const snapshot = JSON.parse(sessionStorage.getItem(
|
||||||
|
"system-simulation-flow:latest-result",
|
||||||
|
) ?? "null");
|
||||||
|
return {
|
||||||
|
values: snapshot.result.series["generic_sensor_1.pressure"],
|
||||||
|
final: snapshot.result.final["generic_sensor_1.pressure"],
|
||||||
|
unit: snapshot.result.variables.find(
|
||||||
|
(variable: { key: string }) => variable.key === "generic_sensor_1.pressure",
|
||||||
|
).unit,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(storedPressure).toEqual({
|
||||||
|
values: [100000, 110000, 120000], final: 120000, unit: "Pa",
|
||||||
|
});
|
||||||
|
|
||||||
|
await page.reload();
|
||||||
|
await openResults(page);
|
||||||
|
await showUnits();
|
||||||
|
await expect(unitSelect).toHaveValue("bar");
|
||||||
|
expect(await readPressureRange()).toEqual(barRange);
|
||||||
|
await unitSelect.selectOption("Pa");
|
||||||
|
expect(await readPressureRange()).toEqual(pascalRange);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test("机械状态事件力点不参与连续曲线纵轴,但原始数据与非力状态保持不变", async ({
|
test("机械状态事件力点不参与连续曲线纵轴,但原始数据与非力状态保持不变", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ type BrowserStreamEvidence = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PROJECT_PATH = fileURLToPath(
|
const PROJECT_PATH = fileURLToPath(
|
||||||
new URL("../../../tests/data/test-mql-8.json", import.meta.url),
|
new URL("../../../tests/fixtures/legacy/test-mql-8.json", import.meta.url),
|
||||||
);
|
);
|
||||||
const EVIDENCE_PATH = fileURLToPath(
|
const EVIDENCE_PATH = fileURLToPath(
|
||||||
new URL(
|
new URL(
|
||||||
@@ -514,7 +514,7 @@ test("test-mql-8 在真实浏览器流式链路完成 0.2 s BDF 仿真", async (
|
|||||||
servingMode: "isolated-vite-dev-proxy",
|
servingMode: "isolated-vite-dev-proxy",
|
||||||
},
|
},
|
||||||
source: {
|
source: {
|
||||||
path: "tests/data/test-mql-8.json",
|
path: "tests/fixtures/legacy/test-mql-8.json",
|
||||||
bytes: statSync(PROJECT_PATH).size,
|
bytes: statSync(PROJECT_PATH).size,
|
||||||
sha256: createHash("sha256").update(projectBytes).digest("hex"),
|
sha256: createHash("sha256").update(projectBytes).digest("hex"),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
import { expect, test, type Locator, type Page } from "@playwright/test";
|
||||||
|
import { componentCatalog, prepareApp, wideProject } from "./fixtures";
|
||||||
|
|
||||||
|
const VERTICAL_MIRROR = "竖直镜像 (Shift+V)";
|
||||||
|
const horizontalMirror = (page: Page) => page.getByRole("button", { name: "水平镜像 (Shift+H)", exact: true });
|
||||||
|
const verticalMirror = (page: Page) => page.getByRole("button", { name: VERTICAL_MIRROR, exact: true });
|
||||||
|
const nodeById = (page: Page, id: string) => page.locator(`.flow-canvas .react-flow__node[data-id="${id}"]`);
|
||||||
|
const extraPort = { ...wideProject.nodes[0].data.ports[0], name: "port_c" };
|
||||||
|
|
||||||
|
function mirrorProject(rotation = 0, mirrored = false) {
|
||||||
|
return {
|
||||||
|
...structuredClone(wideProject),
|
||||||
|
name: "vertical-mirror",
|
||||||
|
nodes: wideProject.nodes.slice(0, 2).map((node, index) => ({
|
||||||
|
...structuredClone(node),
|
||||||
|
position: { x: index * 420, y: index * 240 },
|
||||||
|
data: {
|
||||||
|
...structuredClone(node.data),
|
||||||
|
ports: [...node.data.ports, extraPort],
|
||||||
|
rotation: index === 0 ? rotation : 90,
|
||||||
|
mirrored: index === 0 ? mirrored : true,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
edges: [{
|
||||||
|
id: "mirror-edge",
|
||||||
|
source: "generic_sensor_1",
|
||||||
|
target: "generic_sensor_2",
|
||||||
|
sourceHandle: "port_a",
|
||||||
|
targetHandle: "port_b",
|
||||||
|
data: { isContactEdge: false },
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await prepareApp(page);
|
||||||
|
// Two left ports make the component asymmetric about its horizontal centerline.
|
||||||
|
const catalog = {
|
||||||
|
...componentCatalog,
|
||||||
|
libraries: componentCatalog.libraries.map((library) => ({
|
||||||
|
...library,
|
||||||
|
components: library.components.map((component) => component.type === "generic_sensor"
|
||||||
|
? { ...component, ports: [...component.ports, extraPort] }
|
||||||
|
: component),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
await page.route("**/api/components/catalog", (route) => route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: "application/json",
|
||||||
|
body: JSON.stringify(catalog),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
async function importMirrorProject(page: Page, project: ReturnType<typeof mirrorProject>) {
|
||||||
|
await page.goto("/");
|
||||||
|
await expect(page.getByRole("button", { name: /通用测试元件/ })).toBeVisible();
|
||||||
|
await page.locator('input[type="file"]').setInputFiles({
|
||||||
|
name: "vertical-mirror.json",
|
||||||
|
mimeType: "application/json",
|
||||||
|
buffer: Buffer.from(JSON.stringify(project)),
|
||||||
|
});
|
||||||
|
await expect(nodeById(page, "generic_sensor_1")).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveProject(page: Page) {
|
||||||
|
await page.getByRole("button", { name: "保存工程", exact: true }).click();
|
||||||
|
return page.evaluate(() => JSON.parse(window.localStorage.getItem(
|
||||||
|
"system-simulation-flow:project:vertical-mirror",
|
||||||
|
)!)) as Promise<ReturnType<typeof mirrorProject>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function nodeGeometry(node: Locator) {
|
||||||
|
return node.evaluate((element) => {
|
||||||
|
const body = element.querySelector<HTMLElement>(".sim-node")!;
|
||||||
|
const frame = element.querySelector<HTMLElement>(".sim-node-frame")!;
|
||||||
|
const matrix = new DOMMatrixReadOnly(getComputedStyle(frame).transform);
|
||||||
|
const width = Number(body.dataset.nodeLayoutWidth);
|
||||||
|
const height = Number(body.dataset.nodeLayoutHeight);
|
||||||
|
return {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
matrix: { a: matrix.a, b: matrix.b, c: matrix.c, d: matrix.d },
|
||||||
|
ports: Array.from(element.querySelectorAll<HTMLElement>(".port-handle")).map((port) => ({
|
||||||
|
name: port.dataset.handleid,
|
||||||
|
x: parseFloat(port.style.left) - width / 2,
|
||||||
|
y: parseFloat(port.style.top) - height / 2,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function expectEdgeAttached(page: Page) {
|
||||||
|
await expect.poll(() => page.evaluate(() => {
|
||||||
|
const path = document.querySelector<SVGPathElement>(
|
||||||
|
'.flow-canvas .react-flow__edge[data-id="mirror-edge"] .react-flow__edge-path',
|
||||||
|
);
|
||||||
|
const handle = document.querySelector<HTMLElement>(
|
||||||
|
'.flow-canvas .react-flow__node[data-id="generic_sensor_1"] .port-handle[data-handleid="port_a"]',
|
||||||
|
);
|
||||||
|
const matrix = path?.getScreenCTM();
|
||||||
|
if (!path || !handle || !matrix) return Infinity;
|
||||||
|
const point = path.getPointAtLength(0).matrixTransform(matrix);
|
||||||
|
const bounds = handle.getBoundingClientRect();
|
||||||
|
// React Flow attaches the path to the outer edge of a Handle, not its center.
|
||||||
|
const x = handle.classList.contains("react-flow__handle-left")
|
||||||
|
? bounds.left
|
||||||
|
: handle.classList.contains("react-flow__handle-right")
|
||||||
|
? bounds.right
|
||||||
|
: bounds.left + bounds.width / 2;
|
||||||
|
const y = handle.classList.contains("react-flow__handle-top")
|
||||||
|
? bounds.top
|
||||||
|
: handle.classList.contains("react-flow__handle-bottom")
|
||||||
|
? bounds.bottom
|
||||||
|
: bounds.top + bounds.height / 2;
|
||||||
|
return Math.hypot(point.x - x, point.y - y);
|
||||||
|
})).toBeLessThan(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const rotation of [0, 90, 180, 270]) {
|
||||||
|
for (const mirrored of [false, true]) {
|
||||||
|
test(`竖直镜像始终上下翻转画布组件与端口:旋转 ${rotation}°,镜像 ${mirrored}`, async ({ page }) => {
|
||||||
|
const project = mirrorProject(rotation, mirrored);
|
||||||
|
await importMirrorProject(page, project);
|
||||||
|
await expect(verticalMirror(page)).toBeDisabled();
|
||||||
|
const first = nodeById(page, "generic_sensor_1");
|
||||||
|
const second = nodeById(page, "generic_sensor_2");
|
||||||
|
await first.click();
|
||||||
|
await expect(verticalMirror(page)).toBeEnabled();
|
||||||
|
const [horizontalBox, verticalBox] = await Promise.all([
|
||||||
|
horizontalMirror(page).boundingBox(), verticalMirror(page).boundingBox(),
|
||||||
|
]);
|
||||||
|
expect(verticalBox!.x).toBeGreaterThan(horizontalBox!.x);
|
||||||
|
expect(verticalBox!.x - horizontalBox!.x - horizontalBox!.width).toBeLessThan(16);
|
||||||
|
const before = await nodeGeometry(first);
|
||||||
|
const otherBefore = await nodeGeometry(second);
|
||||||
|
const path = page.locator('.react-flow__edge[data-id="mirror-edge"] .react-flow__edge-path');
|
||||||
|
const pathBefore = await path.getAttribute("d");
|
||||||
|
await verticalMirror(page).click();
|
||||||
|
const after = await nodeGeometry(first);
|
||||||
|
expect(after.width).toBe(before.width);
|
||||||
|
expect(after.height).toBe(before.height);
|
||||||
|
expect(after.matrix.a).toBeCloseTo(before.matrix.a, 8);
|
||||||
|
expect(after.matrix.b).toBeCloseTo(-before.matrix.b, 8);
|
||||||
|
expect(after.matrix.c).toBeCloseTo(before.matrix.c, 8);
|
||||||
|
expect(after.matrix.d).toBeCloseTo(-before.matrix.d, 8);
|
||||||
|
after.ports.forEach((port, index) => {
|
||||||
|
expect(port.x).toBeCloseTo(before.ports[index].x, 6);
|
||||||
|
expect(port.y).toBeCloseTo(-before.ports[index].y, 6);
|
||||||
|
});
|
||||||
|
expect(await nodeGeometry(second)).toEqual(otherBefore);
|
||||||
|
await expect(path).not.toHaveAttribute("d", pathBefore!);
|
||||||
|
await expectEdgeAttached(page);
|
||||||
|
const saved = await saveProject(page);
|
||||||
|
expect(saved.nodes[0].data.rotation).toBe((540 - rotation) % 360);
|
||||||
|
expect(saved.nodes[0].data.mirrored).toBe(!mirrored);
|
||||||
|
expect(saved.nodes[0].position).toEqual(project.nodes[0].position);
|
||||||
|
expect(saved.nodes[0].data.parameters).toEqual(project.nodes[0].data.parameters);
|
||||||
|
expect(saved.edges[0]).toMatchObject(project.edges[0]);
|
||||||
|
await verticalMirror(page).click();
|
||||||
|
expect(await nodeGeometry(first)).toEqual(before);
|
||||||
|
await expectEdgeAttached(page);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("竖直镜像支持混合方向多选、快捷键、撤销重做和工程加载", async ({ page }) => {
|
||||||
|
const project = mirrorProject();
|
||||||
|
await importMirrorProject(page, project);
|
||||||
|
await nodeById(page, "generic_sensor_1").click();
|
||||||
|
await page.keyboard.press("Control+a");
|
||||||
|
await expect(page.locator(".flow-canvas .react-flow__node.selected")).toHaveCount(2);
|
||||||
|
await page.keyboard.press("Shift+v");
|
||||||
|
const mirrored = await saveProject(page);
|
||||||
|
expect(mirrored.nodes.map((node) => [node.data.rotation, node.data.mirrored])).toEqual([
|
||||||
|
[180, true], [90, false],
|
||||||
|
]);
|
||||||
|
await page.getByRole("button", { name: "撤销 (Ctrl+Z)", exact: true }).click();
|
||||||
|
let saved = await saveProject(page);
|
||||||
|
expect(saved.nodes.map((node) => [node.data.rotation, node.data.mirrored])).toEqual([
|
||||||
|
[0, false], [90, true],
|
||||||
|
]);
|
||||||
|
await page.getByRole("button", { name: "重做 (Ctrl+Y)", exact: true }).click();
|
||||||
|
saved = await saveProject(page);
|
||||||
|
expect(saved.nodes).toEqual(mirrored.nodes);
|
||||||
|
await page.getByRole("button", { name: "加载工程", exact: true }).click();
|
||||||
|
expect((await saveProject(page)).nodes).toEqual(mirrored.nodes);
|
||||||
|
await nodeById(page, "generic_sensor_1").click();
|
||||||
|
await page.keyboard.press("Control+a");
|
||||||
|
await verticalMirror(page).click();
|
||||||
|
saved = await saveProject(page);
|
||||||
|
expect(saved.nodes.map((node) => [node.data.rotation, node.data.mirrored])).toEqual([
|
||||||
|
[0, false], [90, true],
|
||||||
|
]);
|
||||||
|
expect(saved.nodes.map((node) => node.position)).toEqual(project.nodes.map((node) => node.position));
|
||||||
|
await expectEdgeAttached(page);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("竖直镜像使接触端口分离时断开连接,撤销恢复端口接触", async ({ page }) => {
|
||||||
|
const project = mirrorProject();
|
||||||
|
project.nodes[0].position = { x: 300, y: 200 };
|
||||||
|
project.nodes[1].data.rotation = 0;
|
||||||
|
project.nodes[1].data.mirrored = false;
|
||||||
|
await importMirrorProject(page, project);
|
||||||
|
const sourceGeometry = await nodeGeometry(nodeById(page, "generic_sensor_1"));
|
||||||
|
const targetGeometry = await nodeGeometry(nodeById(page, "generic_sensor_2"));
|
||||||
|
const sourcePort = sourceGeometry.ports.find((port) => port.name === "port_a")!;
|
||||||
|
const targetPort = targetGeometry.ports.find((port) => port.name === "port_b")!;
|
||||||
|
project.nodes[1].position = {
|
||||||
|
x: project.nodes[0].position.x + sourceGeometry.width / 2 + sourcePort.x
|
||||||
|
- targetGeometry.width / 2 - targetPort.x,
|
||||||
|
y: project.nodes[0].position.y + sourceGeometry.height / 2 + sourcePort.y
|
||||||
|
- targetGeometry.height / 2 - targetPort.y,
|
||||||
|
};
|
||||||
|
project.edges[0].data.isContactEdge = true;
|
||||||
|
await page.locator('input[type="file"]').setInputFiles({
|
||||||
|
name: "vertical-mirror.json",
|
||||||
|
mimeType: "application/json",
|
||||||
|
buffer: Buffer.from(JSON.stringify(project)),
|
||||||
|
});
|
||||||
|
const source = nodeById(page, "generic_sensor_1");
|
||||||
|
const handle = source.locator('.port-handle[data-handleid="port_a"]');
|
||||||
|
await expect(handle).toHaveAttribute("data-connected", "true");
|
||||||
|
const before = await saveProject(page);
|
||||||
|
expect(before.edges).toHaveLength(1);
|
||||||
|
expect(before.edges[0].data.isContactEdge).toBe(true);
|
||||||
|
await source.click();
|
||||||
|
await verticalMirror(page).click();
|
||||||
|
await expect(handle).toHaveAttribute("data-connected", "false");
|
||||||
|
expect((await saveProject(page)).edges).toHaveLength(0);
|
||||||
|
await page.getByRole("button", { name: "撤销 (Ctrl+Z)", exact: true }).click();
|
||||||
|
await expect(handle).toHaveAttribute("data-connected", "true");
|
||||||
|
const restored = await saveProject(page);
|
||||||
|
expect(restored.nodes).toEqual(before.nodes);
|
||||||
|
expect(restored.edges).toEqual(before.edges);
|
||||||
|
});
|
||||||
+18
-5
@@ -1,6 +1,6 @@
|
|||||||
# C 数值后端
|
# C 数值后端
|
||||||
|
|
||||||
后端 Python 校验 XML、检查连接、生成系统专用 C 并编译;独立 EXE 执行完整数值循环。原生运行不调用 Python。当前发布构建支持 Windows x64。
|
后端 Python 校验 XML、检查连接、生成系统专用 C 并编译;独立 EXE 执行完整数值循环。原生运行不调用 Python。当前构建支持 Windows x64 和 Linux x86_64;Linux 使用静态链接的 SUNDIALS。
|
||||||
|
|
||||||
## 启用网页后端
|
## 启用网页后端
|
||||||
|
|
||||||
@@ -13,6 +13,15 @@ $env:SUNDIALS_ROOT = 'F:/Anaconda/Library'
|
|||||||
.venv-win/Scripts/python.exe -m uvicorn app.main:app --host 127.0.0.1 --port 8000
|
.venv-win/Scripts/python.exe -m uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Linux 使用现有 Python 虚拟环境,并将 C 依赖独立放在 `.venv/native/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash bat/setup-native-linux.sh
|
||||||
|
.venv/bin/python -m uvicorn app.main:app --host 127.0.0.1 --port 8000
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本下载并校验固定版本 SUNDIALS 7.4.0,在本地构建静态库;不会安装系统包。构建器先读取 `SUNDIALS_ROOT`,未指定时查找当前 Python 环境的 `native/sundials-7.4.0`、`/usr/local` 和 `/usr`。Linux 安装需包含静态 `.a` 库;生成的 ELF 程序名为 `model`,不依赖 SUNDIALS 共享库或 Python。`.venv/`、`app/data/` 和 `test/` 均在 Git 忽略范围。
|
||||||
|
|
||||||
沿用 `/api/system-xml/simulate` 与 `/api/system-xml/simulate-stream`。`native` 遇到不支持的组件或方法时报告错误,不静默切换。旧 Python 后端已退役,显式选择 `python` 会报错。
|
沿用 `/api/system-xml/simulate` 与 `/api/system-xml/simulate-stream`。`native` 遇到不支持的组件或方法时报告错误,不静默切换。旧 Python 后端已退役,显式选择 `python` 会报错。
|
||||||
|
|
||||||
`bat/start-backend.bat`、`bat/start-backend.sh` 及其 `start-all` 上层入口默认设置 `native`,保留明确指定的环境变量覆盖。直接通过 uvicorn 启动也使用相同的默认选择。启动日志显示所选内核;C 模式只检查工具链与 XML Schema,不再预热 SciPy 积分/代数求解器。工具链检查失败会记录启动诊断,编辑器仍可使用,提交仿真时会明确报错,不会自动调用 Python 数值后端。
|
`bat/start-backend.bat`、`bat/start-backend.sh` 及其 `start-all` 上层入口默认设置 `native`,保留明确指定的环境变量覆盖。直接通过 uvicorn 启动也使用相同的默认选择。启动日志显示所选内核;C 模式只检查工具链与 XML Schema,不再预热 SciPy 积分/代数求解器。工具链检查失败会记录启动诊断,编辑器仍可使用,提交仿真时会明确报错,不会自动调用 Python 数值后端。
|
||||||
@@ -24,7 +33,7 @@ $env:SUNDIALS_ROOT = 'F:/Anaconda/Library'
|
|||||||
## 独立生成与运行
|
## 独立生成与运行
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
.venv-win/Scripts/python.exe -m app.simulation.native_codegen tests/data/native-skill-test.xml --output-dir test/native-v1/example-run --runs 3 --solve-only
|
.venv-win/Scripts/python.exe -m app.simulation.native_codegen tests/fixtures/native-skill-test.xml --output-dir test/native-v1/example-run --runs 3 --solve-only
|
||||||
```
|
```
|
||||||
|
|
||||||
输入支持 XML,或带普通算术表达式的工程 JSON。JSON 普通数字已经是 SI 值;算术表达式按编辑器所选单位换算,例如 `3.14*10**2/4 mm2` 转为 `0.0000785 m2`。较复杂的表达式应先在网页导出 XML。CLI 不执行任意 Python/JavaScript 表达式。
|
输入支持 XML,或带普通算术表达式的工程 JSON。JSON 普通数字已经是 SI 值;算术表达式按编辑器所选单位换算,例如 `3.14*10**2/4 mm2` 转为 `0.0000785 m2`。较复杂的表达式应先在网页导出 XML。CLI 不执行任意 Python/JavaScript 表达式。
|
||||||
@@ -46,7 +55,7 @@ EXE 不需要 Python、SciPy、XML 或原工程文件。DLL 需要与 EXE 一同
|
|||||||
- MECMAS21 支持现有 Python 方程中的摩擦、风阻、柔性限位和 `stoptype=1/2/3/4`,含反弹系数与速度阈值;气腔和管路支持换热。LSTP00A 接受两种刚度模式及接触力符号模式,严格沿用当前组件方程。已有参数中尚未参与 Python 方程的物理效应不会在 C 端凭空补造,详见覆盖记录。
|
- MECMAS21 支持现有 Python 方程中的摩擦、风阻、柔性限位和 `stoptype=1/2/3/4`,含反弹系数与速度阈值;气腔和管路支持换热。LSTP00A 接受两种刚度模式及接触力符号模式,严格沿用当前组件方程。已有参数中尚未参与 Python 方程的物理效应不会在 C 端凭空补造,详见覆盖记录。
|
||||||
- 扩展编译器上限 1024 状态、16384 输出;无连续状态的信号系统使用隐藏常量状态驱动输出。气动网络必须有压力状态锚点;独立气腔之间不能无阻力直接相连。兼容固定管路容腔是已实现的合并例外。闭合未收敛或方程欠定时明确失败,不静默回退。
|
- 扩展编译器上限 1024 状态、16384 输出;无连续状态的信号系统使用隐藏常量状态驱动输出。气动网络必须有压力状态锚点;独立气腔之间不能无阻力直接相连。兼容固定管路容腔是已实现的合并例外。闭合未收敛或方程欠定时明确失败,不静默回退。
|
||||||
- 支持原生 RK45 与 CVODE BDF。CVODE 继续使用默认数值雅可比和稠密线性求解;本次删除不改变 C 求解器的雅可比策略。
|
- 支持原生 RK45 与 CVODE BDF。CVODE 继续使用默认数值雅可比和稠密线性求解;本次删除不改变 C 求解器的雅可比策略。
|
||||||
- 默认 `rtol=1e-6`、气体状态 `atol=1e-8`、机械状态 `atol=1e-12`。CLI 可覆盖 rtol;本版不支持自定义 atol 或 first_step。不同积分器相同局部容差不保证全局曲线误差完全相同。
|
- 网页和 Python CLI 默认 `rtol=1e-8`;生成的状态绝对误差限为质量 `1e-14 kg`、内能 `1e-8 J`、速度/位移 `1e-12`(各自 SI 单位)。独立 C 程序默认 `rtol=1e-6`,对照时应显式传入。CLI 可覆盖 rtol;本版不支持自定义 atol 或 first_step。不同积分器相同局部容差不保证全局曲线误差完全相同。
|
||||||
- 时间信号显式分段,塑性/反弹端挡用稠密插值定位并重启。试探 RHS 不修改已接受状态。柔性接触沿用现有分段力公式,不改变刚度或阻尼来提速。
|
- 时间信号显式分段,塑性/反弹端挡用稠密插值定位并重启。试探 RHS 不修改已接受状态。柔性接触沿用现有分段力公式,不改变刚度或阻尼来提速。
|
||||||
- 每任务独立进程,支持进度、取消及超时。进程崩溃不会作为成功返回,受控失败保留最后接受状态。
|
- 每任务独立进程,支持进度、取消及超时。进程崩溃不会作为成功返回,受控失败保留最后接受状态。
|
||||||
- 编译缓存位于 `app/data/native-builds/`,校验模型、组件合同、源码、编译选项及依赖哈希。本版参数特化入 C,修改模型数值参数会重新编译;修改时间、步长、rtol、采样选项可复用 EXE。
|
- 编译缓存位于 `app/data/native-builds/`,校验模型、组件合同、源码、编译选项及依赖哈希。本版参数特化入 C,修改模型数值参数会重新编译;修改时间、步长、rtol、采样选项可复用 EXE。
|
||||||
@@ -71,6 +80,10 @@ EXE 不需要 Python、SciPy、XML 或原工程文件。DLL 需要与 EXE 一同
|
|||||||
- `config.py`、`sampling.py`、`results.py`:配置、进度、采样合法性和结果合同。
|
- `config.py`、`sampling.py`、`results.py`:配置、进度、采样合法性和结果合同。
|
||||||
- `performance.py`:Python 编排阶段计时,求解耗时读取 C 报告。
|
- `performance.py`:Python 编排阶段计时,求解耗时读取 C 报告。
|
||||||
|
|
||||||
`solvers/`、`systems/generic.py`、旧示例、Python 物性/流量/机械公式和物性缓存已删除。数值回归读取 `tests/data/native-python-reference.json` 的 50 个网络、112 组冻结参考状态,不需要 Python 求解器。旧算法及历史对照资料可从 Git 历史找回。
|
`solvers/`、`systems/generic.py`、旧示例、Python 物性/流量/机械公式和物性缓存已删除。数值回归读取 `tests/baselines/native/native-python-reference.json` 的 50 个网络、112 组冻结参考状态,不需要 Python 求解器。旧算法及历史对照资料可从 Git 历史找回。
|
||||||
|
|
||||||
测试安装:`python -m pip install -r requirements-test.txt`。当前完整数值测试要求 Windows x64、GCC 与 SUNDIALS;Linux 尚不提供 EXE 构建。
|
测试安装:`python -m pip install -r requirements-test.txt`。完整数值测试需要 GCC 与 SUNDIALS;Windows x64 使用导入库/DLL,Linux x86_64 使用静态库。
|
||||||
|
|
||||||
|
## AME 对齐后的工程输入
|
||||||
|
|
||||||
|
当前用户运行请使用 `tests/data/test-mql-4-corrected.json` 或 `tests/data/test-mql-8-corrected.json`。四路执行 XML 位于 `tests/fixtures/amesim/`,与其 JSON 同步;原八路 JSON 已移入 `tests/fixtures/legacy/` 供差异审计。`test-mql-4-amesim-reference.json` 是曲线基准,不是工程输入。模型复核、P4 端口图形修正、默认 `rtol=1e-8` 的验收和速度记录见 [本轮验证报告](../docs/other/牛顿管流求根与四路网页验证-2026-09-11.md)。
|
||||||
+67
-15
@@ -346,27 +346,79 @@ static double pipe_friction_derivative(double re,double rr,double rough,double *
|
|||||||
double db=8.37293695*b*(1-b)/(re-89.96829989);
|
double db=8.37293695*b*(1-b)/(re-89.96829989);
|
||||||
*df=dl*(1-b)+b*dt+db*(turb-lam);return lam+b*(turb-lam);
|
*df=dl*(1-b)+b*dt+db*(turb-lam);return lam+b*(turb-lam);
|
||||||
}
|
}
|
||||||
|
static double pipe_checked_solution(double re,double f,double K,double rr,double rough,
|
||||||
|
double flow_per_re,NativePipeSolve *status) {
|
||||||
|
double target=sqrt(K/f),change=fabs(target-re);
|
||||||
|
/* Separate the absolute and relative tests so a large flow scale cannot
|
||||||
|
make both sides overflow and accidentally satisfy the stopping test. */
|
||||||
|
if(!isfinite(target) || !isfinite(target*flow_per_re) ||
|
||||||
|
!(change<=re*1e-10 || change*flow_per_re<=1e-13))return NAN;
|
||||||
|
double value=target*target*pipe_friction_prepared(target,rr,rough);
|
||||||
|
status->relative_residual=fabs(value/K-1);
|
||||||
|
if(isfinite(value) && status->relative_residual<=1e-9) {
|
||||||
|
status->converged=1;return target;
|
||||||
|
}
|
||||||
|
return NAN;
|
||||||
|
}
|
||||||
double native_pipe_resistance(double K,double rr,double flow_per_re,NativePipeSolve *status) {
|
double native_pipe_resistance(double K,double rr,double flow_per_re,NativePipeSolve *status) {
|
||||||
NativePipeSolve local={0,0,0,INFINITY};if(!status)status=&local;
|
NativePipeSolve local={0,0,0,INFINITY};if(!status)status=&local;
|
||||||
*status=local;
|
*status=local;
|
||||||
if(!(K>=0 && rr>=0 && flow_per_re>0) || !isfinite(K) || !isfinite(rr) || !isfinite(flow_per_re))return NAN;
|
if(!(K>=0 && rr>=0 && flow_per_re>0) || !isfinite(K) || !isfinite(rr) || !isfinite(flow_per_re))return NAN;
|
||||||
if(K/64<=89.96829989){status->converged=1;status->relative_residual=0;return K/64;}
|
if(K==0){status->converged=1;status->relative_residual=0;return 0;}
|
||||||
double rough=pipe_rough_limit(rr),lo=0,hi=fmax(sqrt(K/.02),1),df;
|
if(K/64<=89.96829989) {
|
||||||
for(int i=0;i<128 && hi*hi*pipe_friction_prepared(hi,rr,rough)<K;i++)hi*=2;
|
double re=K/64;
|
||||||
if(!isfinite(hi))return NAN;
|
status->relative_residual=fabs(64*re/K-1);
|
||||||
double re=fmin(sqrt(K/.02),hi);
|
if(re>0 && isfinite(re*flow_per_re) && status->relative_residual<=1e-9) {
|
||||||
for(int i=0;i<80;i++) {
|
status->converged=1;return re;
|
||||||
status->iterations=i+1;
|
|
||||||
double f=pipe_friction_derivative(re,rr,rough,&df),F=re*re*f-K;
|
|
||||||
double target=sqrt(K/f);
|
|
||||||
if(fabs(target-re)*flow_per_re<=fmax(1e-13,re*flow_per_re*1e-10)) {
|
|
||||||
/* Check the returned point, not just the change of successive guesses. */
|
|
||||||
status->relative_residual=fabs(target*target*pipe_friction_prepared(target,rr,rough)/K-1);
|
|
||||||
if(isfinite(target) && status->relative_residual<=1e-9){status->converged=1;return target;}
|
|
||||||
}
|
}
|
||||||
|
return NAN;
|
||||||
|
}
|
||||||
|
double rough=pipe_rough_limit(rr),lo=0,hi=fmax(sqrt(K/.02),1),df;
|
||||||
|
if(!isfinite(rough) || !isfinite(hi))return NAN;
|
||||||
|
int bracketed=0;
|
||||||
|
for(int i=0;i<128;i++) {
|
||||||
|
double value=hi*hi*pipe_friction_prepared(hi,rr,rough);
|
||||||
|
if(!isfinite(value))return NAN;
|
||||||
|
if(value>=K){bracketed=1;break;}
|
||||||
|
lo=hi;hi*=2;
|
||||||
|
if(!isfinite(hi))return NAN;
|
||||||
|
}
|
||||||
|
/* F(0)=-K; each positive lower endpoint was checked while expanding.
|
||||||
|
Never start iteration with an unchecked or non-finite upper residual. */
|
||||||
|
if(!bracketed)return NAN;
|
||||||
|
double re=fmax(lo,fmin(sqrt(K/.02),hi)),previous_residual=INFINITY;
|
||||||
|
int previous_newton=0;
|
||||||
|
for(int i=0;i<128;i++) {
|
||||||
|
status->iterations=i+1;
|
||||||
|
double f=pipe_friction_derivative(re,rr,rough,&df),value=re*re*f,F=value-K;
|
||||||
|
if(!(f>0) || !isfinite(f) || !isfinite(value))return NAN;
|
||||||
|
status->relative_residual=fabs(value/K-1);
|
||||||
|
double target=pipe_checked_solution(re,f,K,rr,rough,flow_per_re,status);
|
||||||
|
if(status->converged)return target;
|
||||||
if(F>0)hi=re;else lo=re;
|
if(F>0)hi=re;else lo=re;
|
||||||
double next=re-F/(2*re*f+re*re*df);
|
double slope=2*re*f+re*re*df;
|
||||||
if(!isfinite(next) || next<=lo || next>=hi){next=.5*(lo+hi);status->bisections++;}
|
double next=slope>0 && isfinite(slope) ? re-F/slope : NAN;
|
||||||
|
/* An in-bracket Newton step is useful only if it reduces |F|.
|
||||||
|
After a step that fails to halve it, bisect the retained bracket;
|
||||||
|
successive Newton steps therefore cannot stagnate near an endpoint. */
|
||||||
|
int bisect=(previous_newton && fabs(F)>.5*previous_residual) ||
|
||||||
|
!(slope>0) || !isfinite(slope) || !isfinite(next) || next<=lo || next>=hi;
|
||||||
|
if(bisect) {
|
||||||
|
next=lo+.5*(hi-lo);status->bisections++;
|
||||||
|
if(next<=lo || next>=hi) {
|
||||||
|
/* Adjacent floating-point endpoints: inspect both actual
|
||||||
|
candidates, then fail if neither meets the original tests. */
|
||||||
|
double endpoints[]={lo,hi};
|
||||||
|
for(int j=0;j<2;j++) {
|
||||||
|
double endpoint=endpoints[j];
|
||||||
|
target=pipe_checked_solution(endpoint,pipe_friction_prepared(endpoint,rr,rough),
|
||||||
|
K,rr,rough,flow_per_re,status);
|
||||||
|
if(status->converged)return target;
|
||||||
|
}
|
||||||
|
return NAN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
previous_newton=!bisect;previous_residual=fabs(F);
|
||||||
re=next;
|
re=next;
|
||||||
}
|
}
|
||||||
return NAN;
|
return NAN;
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
@@ -3,7 +3,7 @@
|
|||||||
"id": "test_mql_full_branches-historical-v1",
|
"id": "test_mql_full_branches-historical-v1",
|
||||||
"description": "Historical slow-region and 2.05 s production regression target with three locked-environment repeats and approved state goldens.",
|
"description": "Historical slow-region and 2.05 s production regression target with three locked-environment repeats and approved state goldens.",
|
||||||
"source": {
|
"source": {
|
||||||
"path": "tests/data/test_mql-full-branches-01-04.xml",
|
"path": "tests/baselines/simulation/test_mql_full_branches/sources/test_mql-full-branches-01-04.xml",
|
||||||
"sha256": "2fb95e65f5de0c85a6a17802aef74ea004087323fd00fd8d01acf0184ff71d48",
|
"sha256": "2fb95e65f5de0c85a6a17802aef74ea004087323fd00fd8d01acf0184ff71d48",
|
||||||
"bytes": 58860,
|
"bytes": 58860,
|
||||||
"xmlIsAuthoritative": true,
|
"xmlIsAuthoritative": true,
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,24 @@
|
|||||||
|
# 浏览器四路、八路测试工程
|
||||||
|
|
||||||
|
本目录中的 JSON/XML 仅保留以下两个可由网页“导入工程 JSON”识别的工程文件。已逐项核对对应 AME 图纸的元件、公开物理参数及端口连接;AME 单位与工程 SI 单位之间的换算已纳入检查。
|
||||||
|
|
||||||
|
| 浏览器工程 | Amesim 原文件 | 元件 / 参数 / 连接 |
|
||||||
|
|---|---|---|
|
||||||
|
| [test-mql-4-corrected.json](test-mql-4-corrected.json) | [test_mql_4.ame](test_mql_4.ame) | 81 / 582 / 90 |
|
||||||
|
| [test-mql-8-corrected.json](test-mql-8-corrected.json) | [test_mql.ame](test_mql.ame) | 157 / 1092 / 178 |
|
||||||
|
|
||||||
|
网页当前只支持导入工程 JSON;XML 可从网页下载,用作后端输入。网页使用原生 BDF 和当前默认 `rtol=1e-8`,与 AME 内部积分器及历史数值设置不能视为相同。这里的参数对齐指模型物理参数与初值,不声称不同仿真器所有数值输出完全一致。
|
||||||
|
|
||||||
|
2026-09-11 整理时,两份工程内容保持不变。其他 JSON/XML 已按用途移出本目录,移动前后字节数及 SHA256 相同:
|
||||||
|
|
||||||
|
| 原文件 | 当前路径 | 用途 |
|
||||||
|
|---|---|---|
|
||||||
|
| native-python-reference.json | [原生数值基准](../baselines/native/native-python-reference.json) | 50 网络冻结参考 |
|
||||||
|
| test-mql-4-amesim-reference.json | [四路 Amesim 曲线基准](../baselines/simulation/test_mql_4/test-mql-4-amesim-reference.json) | 72 曲线、57 时刻 |
|
||||||
|
| test-mql-4-corrected.xml | [四路执行 XML](../fixtures/amesim/test-mql-4-corrected.xml) | 与四路 JSON 同步的后端输入 |
|
||||||
|
| test-mql-8.json / test-mql-8.xml | [旧 JSON](../fixtures/legacy/test-mql-8.json) / [旧 XML](../fixtures/legacy/test-mql-8.xml) | 修正前输入及布局来源 |
|
||||||
|
| test_mql-full-branches-01-04.xml | [历史分支基准输入](../baselines/simulation/test_mql_full_branches/sources/test_mql-full-branches-01-04.xml) | 历史回归清单绑定的输入 |
|
||||||
|
| native-skill-test.xml | [原生通用测试输入](../fixtures/native-skill-test.xml) | 原生编译、执行与 CI 检查 |
|
||||||
|
| fixtures/high_stiffness_explicit_rk45.xml | [高刚度 RK45 输入](../fixtures/high_stiffness_explicit_rk45.xml) | 独立刚度回归用例 |
|
||||||
|
|
||||||
|
AME 原文件及本目录中其他类型的资料保持原位。历史运行记录保留当时路径;活动测试、审计脚本、CI 和当前交付说明已更新到新路径。
|
||||||
@@ -1,325 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<System name="demo-system" schemaVersion="3" unitSystem="SI">
|
|
||||||
<Simulation tStart="0" tStop="10" sampleStep="0.02" maxStep="0.02" method="RK45"/>
|
|
||||||
<Components>
|
|
||||||
<Component id="amesim_pnch023_1" type="amesim_pnch023" modelVersion="0.1.0">
|
|
||||||
<Parameter name="gi" value="1"/>
|
|
||||||
<Parameter name="cvol" value="0.057"/>
|
|
||||||
<Parameter name="kth" value="0"/>
|
|
||||||
<Parameter name="sth" value="0.1"/>
|
|
||||||
<Parameter name="extemp" value="293.15"/>
|
|
||||||
<Parameter name="p0" value="15300000"/>
|
|
||||||
<Parameter name="T0" value="293.15"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_pnvo001_1" type="amesim_pnvo001" modelVersion="0.2.0">
|
|
||||||
<Parameter name="gi" value="1"/>
|
|
||||||
<Parameter name="cq" value="0.45"/>
|
|
||||||
<Parameter name="area0" value="0.0000785"/>
|
|
||||||
<Parameter name="Cv" value="0.5"/>
|
|
||||||
<Parameter name="Kv" value="0.4"/>
|
|
||||||
<Parameter name="flowset" value="1"/>
|
|
||||||
<Parameter name="opening0" value="0"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_step0_1" type="amesim_step0" modelVersion="0.1.0">
|
|
||||||
<Parameter name="initial" value="0"/>
|
|
||||||
<Parameter name="final" value="1"/>
|
|
||||||
<Parameter name="time" value="0.04"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_pnch012_1" type="amesim_pnch012" modelVersion="0.1.0">
|
|
||||||
<Parameter name="gi" value="1"/>
|
|
||||||
<Parameter name="cvol0" value="0.015"/>
|
|
||||||
<Parameter name="kth" value="1500"/>
|
|
||||||
<Parameter name="sth" value="0.7"/>
|
|
||||||
<Parameter name="extemp" value="293.15"/>
|
|
||||||
<Parameter name="p0" value="100000"/>
|
|
||||||
<Parameter name="T0" value="293.15"/>
|
|
||||||
<Parameter name="vol1" value="0"/>
|
|
||||||
<Parameter name="vol2" value="0"/>
|
|
||||||
<Parameter name="vol3" value="0"/>
|
|
||||||
<Parameter name="vol4" value="0"/>
|
|
||||||
<Parameter name="dvol1" value="0"/>
|
|
||||||
<Parameter name="dvol2" value="0"/>
|
|
||||||
<Parameter name="dvol3" value="0"/>
|
|
||||||
<Parameter name="dvol4" value="0"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_pnpl01_2" type="amesim_pnpl01" modelVersion="0.1.0"/>
|
|
||||||
<Component id="amesim_pnpl01_3" type="amesim_pnpl01" modelVersion="0.1.0"/>
|
|
||||||
<Component id="amesim_pnpl01_4" type="amesim_pnpl01" modelVersion="0.1.0"/>
|
|
||||||
<Component id="amesim_ud00_1" type="amesim_ud00" modelVersion="0.2.0">
|
|
||||||
<Parameter name="tstart" value="0"/>
|
|
||||||
<Parameter name="start1" value="100000000000000000"/>
|
|
||||||
<Parameter name="end1" value="100000000000000000"/>
|
|
||||||
<Parameter name="t1" value="0.8"/>
|
|
||||||
<Parameter name="start2" value="49000"/>
|
|
||||||
<Parameter name="end2" value="49000"/>
|
|
||||||
<Parameter name="t2" value="10"/>
|
|
||||||
<Parameter name="start3" value="1"/>
|
|
||||||
<Parameter name="end3" value="1"/>
|
|
||||||
<Parameter name="t3" value="0"/>
|
|
||||||
<Parameter name="start4" value="1"/>
|
|
||||||
<Parameter name="end4" value="1"/>
|
|
||||||
<Parameter name="t4" value="0"/>
|
|
||||||
<Parameter name="start5" value="1"/>
|
|
||||||
<Parameter name="end5" value="1"/>
|
|
||||||
<Parameter name="t5" value="0"/>
|
|
||||||
<Parameter name="start6" value="1"/>
|
|
||||||
<Parameter name="end6" value="1"/>
|
|
||||||
<Parameter name="t6" value="0"/>
|
|
||||||
<Parameter name="start7" value="1"/>
|
|
||||||
<Parameter name="end7" value="1"/>
|
|
||||||
<Parameter name="t7" value="0"/>
|
|
||||||
<Parameter name="start8" value="1"/>
|
|
||||||
<Parameter name="end8" value="1"/>
|
|
||||||
<Parameter name="t8" value="0"/>
|
|
||||||
<Parameter name="nstages" value="2"/>
|
|
||||||
<Parameter name="iscyclic" value="0"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_forc_1" type="amesim_forc" modelVersion="0.2.0">
|
|
||||||
<Parameter name="direction" value="1"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_pnrp17_1" type="amesim_pnrp17" modelVersion="0.1.0">
|
|
||||||
<Parameter name="gi" value="1"/>
|
|
||||||
<Parameter name="dp" value="0.2"/>
|
|
||||||
<Parameter name="dr" value="0.001"/>
|
|
||||||
<Parameter name="x0" value="0"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_helium_medium_1" type="amesim_helium_medium" modelVersion="0.1.0">
|
|
||||||
<Parameter name="gi" value="1"/>
|
|
||||||
<Parameter name="property_model" value="0"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_pnch012_2" type="amesim_pnch012" modelVersion="0.1.0">
|
|
||||||
<Parameter name="gi" value="1"/>
|
|
||||||
<Parameter name="cvol0" value="0.015"/>
|
|
||||||
<Parameter name="kth" value="1500"/>
|
|
||||||
<Parameter name="sth" value="0.7"/>
|
|
||||||
<Parameter name="extemp" value="293.15"/>
|
|
||||||
<Parameter name="p0" value="100000"/>
|
|
||||||
<Parameter name="T0" value="293.15"/>
|
|
||||||
<Parameter name="vol1" value="0"/>
|
|
||||||
<Parameter name="vol2" value="0"/>
|
|
||||||
<Parameter name="vol3" value="0"/>
|
|
||||||
<Parameter name="vol4" value="0"/>
|
|
||||||
<Parameter name="dvol1" value="0"/>
|
|
||||||
<Parameter name="dvol2" value="0"/>
|
|
||||||
<Parameter name="dvol3" value="0"/>
|
|
||||||
<Parameter name="dvol4" value="0"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_pnpl01_6" type="amesim_pnpl01" modelVersion="0.1.0"/>
|
|
||||||
<Component id="amesim_pnpl01_7" type="amesim_pnpl01" modelVersion="0.1.0"/>
|
|
||||||
<Component id="amesim_pnpl01_8" type="amesim_pnpl01" modelVersion="0.1.0"/>
|
|
||||||
<Component id="amesim_pnpl01_9" type="amesim_pnpl01" modelVersion="0.1.0"/>
|
|
||||||
<Component id="amesim_mecmas21_2" type="amesim_mecmas21" modelVersion="0.2.0">
|
|
||||||
<Parameter name="mass" value="50"/>
|
|
||||||
<Parameter name="fstick" value="0"/>
|
|
||||||
<Parameter name="fcoul" value="0"/>
|
|
||||||
<Parameter name="rvisc" value="0"/>
|
|
||||||
<Parameter name="wind" value="0"/>
|
|
||||||
<Parameter name="dvel" value="0.000001"/>
|
|
||||||
<Parameter name="restdvel" value="0.000001"/>
|
|
||||||
<Parameter name="restcoeff" value="0.65"/>
|
|
||||||
<Parameter name="astrib" value="0.001"/>
|
|
||||||
<Parameter name="xmin" value="-1"/>
|
|
||||||
<Parameter name="Kbmin" value="1000000000"/>
|
|
||||||
<Parameter name="Dbmin" value="10000"/>
|
|
||||||
<Parameter name="Pdmin" value="0.0001"/>
|
|
||||||
<Parameter name="xmax" value="0.8"/>
|
|
||||||
<Parameter name="Kbmax" value="1000000000"/>
|
|
||||||
<Parameter name="Dbmax" value="10000"/>
|
|
||||||
<Parameter name="Pdmax" value="0.0001"/>
|
|
||||||
<Parameter name="theta" value="0"/>
|
|
||||||
<Parameter name="useFriction" value="1"/>
|
|
||||||
<Parameter name="stoptype" value="4"/>
|
|
||||||
<Parameter name="discContactOption" value="1"/>
|
|
||||||
<Parameter name="strib" value="1"/>
|
|
||||||
<Parameter name="frictionType" value="1"/>
|
|
||||||
<Parameter name="v0" value="0"/>
|
|
||||||
<Parameter name="x0" value="0"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_f000_1" type="amesim_f000" modelVersion="0.1.0"/>
|
|
||||||
<Component id="amesim_f000_2" type="amesim_f000" modelVersion="0.1.0"/>
|
|
||||||
<Component id="amesim_lstp00a_1" type="amesim_lstp00a" modelVersion="0.2.0">
|
|
||||||
<Parameter name="na" value="10"/>
|
|
||||||
<Parameter name="gap0" value="0"/>
|
|
||||||
<Parameter name="kcont" value="100000000000"/>
|
|
||||||
<Parameter name="G" value="85700000000"/>
|
|
||||||
<Parameter name="sdiam" value="0.02"/>
|
|
||||||
<Parameter name="wdiam" value="0.002"/>
|
|
||||||
<Parameter name="rcont" value="100000000000"/>
|
|
||||||
<Parameter name="Pdis" value="1e-7"/>
|
|
||||||
<Parameter name="stiffmode" value="1"/>
|
|
||||||
<Parameter name="discContactOption" value="1"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_forc_2" type="amesim_forc" modelVersion="0.2.0">
|
|
||||||
<Parameter name="direction" value="1"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_ud00_2" type="amesim_ud00" modelVersion="0.2.0">
|
|
||||||
<Parameter name="tstart" value="0"/>
|
|
||||||
<Parameter name="start1" value="1000000000000"/>
|
|
||||||
<Parameter name="end1" value="1000000000000"/>
|
|
||||||
<Parameter name="t1" value="0.8"/>
|
|
||||||
<Parameter name="start2" value="0"/>
|
|
||||||
<Parameter name="end2" value="0"/>
|
|
||||||
<Parameter name="t2" value="10"/>
|
|
||||||
<Parameter name="start3" value="1"/>
|
|
||||||
<Parameter name="end3" value="1"/>
|
|
||||||
<Parameter name="t3" value="0"/>
|
|
||||||
<Parameter name="start4" value="1"/>
|
|
||||||
<Parameter name="end4" value="1"/>
|
|
||||||
<Parameter name="t4" value="0"/>
|
|
||||||
<Parameter name="start5" value="1"/>
|
|
||||||
<Parameter name="end5" value="1"/>
|
|
||||||
<Parameter name="t5" value="0"/>
|
|
||||||
<Parameter name="start6" value="1"/>
|
|
||||||
<Parameter name="end6" value="1"/>
|
|
||||||
<Parameter name="t6" value="0"/>
|
|
||||||
<Parameter name="start7" value="1"/>
|
|
||||||
<Parameter name="end7" value="1"/>
|
|
||||||
<Parameter name="t7" value="0"/>
|
|
||||||
<Parameter name="start8" value="1"/>
|
|
||||||
<Parameter name="end8" value="1"/>
|
|
||||||
<Parameter name="t8" value="0"/>
|
|
||||||
<Parameter name="nstages" value="2"/>
|
|
||||||
<Parameter name="iscyclic" value="0"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_mecmas21_5" type="amesim_mecmas21" modelVersion="0.2.0">
|
|
||||||
<Parameter name="mass" value="170000"/>
|
|
||||||
<Parameter name="fstick" value="0"/>
|
|
||||||
<Parameter name="fcoul" value="0"/>
|
|
||||||
<Parameter name="rvisc" value="0"/>
|
|
||||||
<Parameter name="wind" value="0"/>
|
|
||||||
<Parameter name="dvel" value="0.000001"/>
|
|
||||||
<Parameter name="restdvel" value="0.000001"/>
|
|
||||||
<Parameter name="restcoeff" value="0.65"/>
|
|
||||||
<Parameter name="astrib" value="0.001"/>
|
|
||||||
<Parameter name="xmin" value="0"/>
|
|
||||||
<Parameter name="Kbmin" value="1000000000"/>
|
|
||||||
<Parameter name="Dbmin" value="10000"/>
|
|
||||||
<Parameter name="Pdmin" value="0.0001"/>
|
|
||||||
<Parameter name="xmax" value="0.37"/>
|
|
||||||
<Parameter name="Kbmax" value="1000000000"/>
|
|
||||||
<Parameter name="Dbmax" value="10000"/>
|
|
||||||
<Parameter name="Pdmax" value="0.0001"/>
|
|
||||||
<Parameter name="theta" value="0"/>
|
|
||||||
<Parameter name="useFriction" value="1"/>
|
|
||||||
<Parameter name="stoptype" value="1"/>
|
|
||||||
<Parameter name="discContactOption" value="1"/>
|
|
||||||
<Parameter name="strib" value="1"/>
|
|
||||||
<Parameter name="frictionType" value="1"/>
|
|
||||||
<Parameter name="v0" value="0"/>
|
|
||||||
<Parameter name="x0" value="0"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_mecmas21_7" type="amesim_mecmas21" modelVersion="0.2.0">
|
|
||||||
<Parameter name="mass" value="90000"/>
|
|
||||||
<Parameter name="fstick" value="0"/>
|
|
||||||
<Parameter name="fcoul" value="0"/>
|
|
||||||
<Parameter name="rvisc" value="0"/>
|
|
||||||
<Parameter name="wind" value="0"/>
|
|
||||||
<Parameter name="dvel" value="0.000001"/>
|
|
||||||
<Parameter name="restdvel" value="0.000001"/>
|
|
||||||
<Parameter name="restcoeff" value="0.65"/>
|
|
||||||
<Parameter name="astrib" value="0.001"/>
|
|
||||||
<Parameter name="xmin" value="-0.72"/>
|
|
||||||
<Parameter name="Kbmin" value="1000000000"/>
|
|
||||||
<Parameter name="Dbmin" value="10000"/>
|
|
||||||
<Parameter name="Pdmin" value="0.0001"/>
|
|
||||||
<Parameter name="xmax" value="0"/>
|
|
||||||
<Parameter name="Kbmax" value="1000000000"/>
|
|
||||||
<Parameter name="Dbmax" value="10000"/>
|
|
||||||
<Parameter name="Pdmax" value="0.0001"/>
|
|
||||||
<Parameter name="theta" value="0"/>
|
|
||||||
<Parameter name="useFriction" value="1"/>
|
|
||||||
<Parameter name="stoptype" value="1"/>
|
|
||||||
<Parameter name="discContactOption" value="1"/>
|
|
||||||
<Parameter name="strib" value="1"/>
|
|
||||||
<Parameter name="frictionType" value="1"/>
|
|
||||||
<Parameter name="v0" value="0"/>
|
|
||||||
<Parameter name="x0" value="0"/>
|
|
||||||
</Component>
|
|
||||||
</Components>
|
|
||||||
<Connections>
|
|
||||||
<Connection id="edge-amesim_pnch023_1-port_2-amesim_pnvo001_1-port_2-1786524999267">
|
|
||||||
<Endpoint component="amesim_pnch023_1" port="port_2"/>
|
|
||||||
<Endpoint component="amesim_pnvo001_1" port="port_2"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_pnvo001_1-port_3-amesim_pnch012_1-port_1-1786525009973">
|
|
||||||
<Endpoint component="amesim_pnvo001_1" port="port_3"/>
|
|
||||||
<Endpoint component="amesim_pnch012_1" port="port_1"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_pnpl01_2-port_1-amesim_pnch012_1-port_2-1786525035706-0">
|
|
||||||
<Endpoint component="amesim_pnpl01_2" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnch012_1" port="port_2"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_pnpl01_3-port_1-amesim_pnch012_1-port_3-1786525054054-0">
|
|
||||||
<Endpoint component="amesim_pnpl01_3" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnch012_1" port="port_3"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_pnpl01_4-port_1-amesim_pnch012_1-port_4-1786525061349-0">
|
|
||||||
<Endpoint component="amesim_pnpl01_4" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnch012_1" port="port_4"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_step0_1-out-amesim_pnvo001_1-res-1786525074494">
|
|
||||||
<Endpoint component="amesim_step0_1" port="out"/>
|
|
||||||
<Endpoint component="amesim_pnvo001_1" port="res"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_ud00_1-out-amesim_forc_1-res-1786525088412">
|
|
||||||
<Endpoint component="amesim_ud00_1" port="out"/>
|
|
||||||
<Endpoint component="amesim_forc_1" port="res"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_pnpl01_6-port_1-amesim_pnch023_1-port_1-1786525164428-0">
|
|
||||||
<Endpoint component="amesim_pnpl01_6" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnch023_1" port="port_1"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_pnpl01_7-port_1-amesim_pnch012_2-port_1-1786525181969-0">
|
|
||||||
<Endpoint component="amesim_pnpl01_7" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnch012_2" port="port_1"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_pnpl01_8-port_1-amesim_pnch012_2-port_4-1786525185493-0">
|
|
||||||
<Endpoint component="amesim_pnpl01_8" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnch012_2" port="port_4"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_pnpl01_9-port_1-amesim_pnch012_2-port_2-1786525188482-0">
|
|
||||||
<Endpoint component="amesim_pnpl01_9" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnch012_2" port="port_2"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_f000_1-port_1-amesim_mecmas21_2-port_2-1786525202665-0">
|
|
||||||
<Endpoint component="amesim_f000_1" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_mecmas21_2" port="port_2"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_mecmas21_2-port_1-amesim_pnrp17_1-port_2-1786525203778">
|
|
||||||
<Endpoint component="amesim_mecmas21_2" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnrp17_1" port="port_2"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_pnrp17_1-port_5-amesim_lstp00a_1-port_1-1786525213548">
|
|
||||||
<Endpoint component="amesim_pnrp17_1" port="port_5"/>
|
|
||||||
<Endpoint component="amesim_lstp00a_1" port="port_1"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_ud00_2-out-amesim_forc_2-res-1786525226761">
|
|
||||||
<Endpoint component="amesim_ud00_2" port="out"/>
|
|
||||||
<Endpoint component="amesim_forc_2" port="res"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_pnrp17_1-port_4-amesim_f000_2-port_1-1786525729832-0">
|
|
||||||
<Endpoint component="amesim_pnrp17_1" port="port_4"/>
|
|
||||||
<Endpoint component="amesim_f000_2" port="port_1"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_pnrp17_1-port_1-amesim_pnch012_2-port_3-1786525729832-1">
|
|
||||||
<Endpoint component="amesim_pnrp17_1" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnch012_2" port="port_3"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_forc_1-port_2-amesim_mecmas21_7-port_2-1786525915163">
|
|
||||||
<Endpoint component="amesim_forc_1" port="port_2"/>
|
|
||||||
<Endpoint component="amesim_mecmas21_7" port="port_2"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_mecmas21_7-port_1-amesim_pnrp17_1-port_3-1786525916652">
|
|
||||||
<Endpoint component="amesim_mecmas21_7" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnrp17_1" port="port_3"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_mecmas21_5-port_1-amesim_forc_2-port_2-1786525922010">
|
|
||||||
<Endpoint component="amesim_mecmas21_5" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_forc_2" port="port_2"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_lstp00a_1-port_2-amesim_mecmas21_5-port_2-1786525924224">
|
|
||||||
<Endpoint component="amesim_lstp00a_1" port="port_2"/>
|
|
||||||
<Endpoint component="amesim_mecmas21_5" port="port_2"/>
|
|
||||||
</Connection>
|
|
||||||
</Connections>
|
|
||||||
</System>
|
|
||||||
@@ -1,325 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<System name="skill-test" schemaVersion="3" unitSystem="SI">
|
|
||||||
<Simulation tStart="0" tStop="10" sampleStep="0.02" maxStep="0.001" method="RK45"/>
|
|
||||||
<Components>
|
|
||||||
<Component id="amesim_pnch023_1" type="amesim_pnch023" modelVersion="0.1.0">
|
|
||||||
<Parameter name="gi" value="1"/>
|
|
||||||
<Parameter name="cvol" value="0.057"/>
|
|
||||||
<Parameter name="kth" value="0"/>
|
|
||||||
<Parameter name="sth" value="0.1"/>
|
|
||||||
<Parameter name="extemp" value="293.15"/>
|
|
||||||
<Parameter name="p0" value="15300000"/>
|
|
||||||
<Parameter name="T0" value="293.15"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_pnvo001_1" type="amesim_pnvo001" modelVersion="0.2.0">
|
|
||||||
<Parameter name="gi" value="1"/>
|
|
||||||
<Parameter name="cq" value="0.45"/>
|
|
||||||
<Parameter name="area0" value="0.0000785"/>
|
|
||||||
<Parameter name="Cv" value="0.5"/>
|
|
||||||
<Parameter name="Kv" value="0.4"/>
|
|
||||||
<Parameter name="flowset" value="1"/>
|
|
||||||
<Parameter name="opening0" value="0"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_step0_1" type="amesim_step0" modelVersion="0.1.0">
|
|
||||||
<Parameter name="initial" value="0"/>
|
|
||||||
<Parameter name="final" value="1"/>
|
|
||||||
<Parameter name="time" value="0.04"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_pnch012_1" type="amesim_pnch012" modelVersion="0.1.0">
|
|
||||||
<Parameter name="gi" value="1"/>
|
|
||||||
<Parameter name="cvol0" value="0.015"/>
|
|
||||||
<Parameter name="kth" value="1500"/>
|
|
||||||
<Parameter name="sth" value="0.7"/>
|
|
||||||
<Parameter name="extemp" value="293.15"/>
|
|
||||||
<Parameter name="p0" value="100000"/>
|
|
||||||
<Parameter name="T0" value="293.15"/>
|
|
||||||
<Parameter name="vol1" value="0"/>
|
|
||||||
<Parameter name="vol2" value="0"/>
|
|
||||||
<Parameter name="vol3" value="0"/>
|
|
||||||
<Parameter name="vol4" value="0"/>
|
|
||||||
<Parameter name="dvol1" value="0"/>
|
|
||||||
<Parameter name="dvol2" value="0"/>
|
|
||||||
<Parameter name="dvol3" value="0"/>
|
|
||||||
<Parameter name="dvol4" value="0"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_pnpl01_2" type="amesim_pnpl01" modelVersion="0.1.0"/>
|
|
||||||
<Component id="amesim_pnpl01_3" type="amesim_pnpl01" modelVersion="0.1.0"/>
|
|
||||||
<Component id="amesim_pnpl01_4" type="amesim_pnpl01" modelVersion="0.1.0"/>
|
|
||||||
<Component id="amesim_ud00_1" type="amesim_ud00" modelVersion="0.2.0">
|
|
||||||
<Parameter name="tstart" value="0"/>
|
|
||||||
<Parameter name="start1" value="100000000000000000"/>
|
|
||||||
<Parameter name="end1" value="100000000000000000"/>
|
|
||||||
<Parameter name="t1" value="0.8"/>
|
|
||||||
<Parameter name="start2" value="49000"/>
|
|
||||||
<Parameter name="end2" value="49000"/>
|
|
||||||
<Parameter name="t2" value="10"/>
|
|
||||||
<Parameter name="start3" value="1"/>
|
|
||||||
<Parameter name="end3" value="1"/>
|
|
||||||
<Parameter name="t3" value="0"/>
|
|
||||||
<Parameter name="start4" value="1"/>
|
|
||||||
<Parameter name="end4" value="1"/>
|
|
||||||
<Parameter name="t4" value="0"/>
|
|
||||||
<Parameter name="start5" value="1"/>
|
|
||||||
<Parameter name="end5" value="1"/>
|
|
||||||
<Parameter name="t5" value="0"/>
|
|
||||||
<Parameter name="start6" value="1"/>
|
|
||||||
<Parameter name="end6" value="1"/>
|
|
||||||
<Parameter name="t6" value="0"/>
|
|
||||||
<Parameter name="start7" value="1"/>
|
|
||||||
<Parameter name="end7" value="1"/>
|
|
||||||
<Parameter name="t7" value="0"/>
|
|
||||||
<Parameter name="start8" value="1"/>
|
|
||||||
<Parameter name="end8" value="1"/>
|
|
||||||
<Parameter name="t8" value="0"/>
|
|
||||||
<Parameter name="nstages" value="2"/>
|
|
||||||
<Parameter name="iscyclic" value="0"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_forc_1" type="amesim_forc" modelVersion="0.2.0">
|
|
||||||
<Parameter name="direction" value="1"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_pnrp17_1" type="amesim_pnrp17" modelVersion="0.1.0">
|
|
||||||
<Parameter name="gi" value="1"/>
|
|
||||||
<Parameter name="dp" value="0.2"/>
|
|
||||||
<Parameter name="dr" value="0.001"/>
|
|
||||||
<Parameter name="x0" value="0"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_helium_medium_1" type="amesim_helium_medium" modelVersion="0.1.0">
|
|
||||||
<Parameter name="gi" value="1"/>
|
|
||||||
<Parameter name="property_model" value="0"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_pnch012_2" type="amesim_pnch012" modelVersion="0.1.0">
|
|
||||||
<Parameter name="gi" value="1"/>
|
|
||||||
<Parameter name="cvol0" value="0.015"/>
|
|
||||||
<Parameter name="kth" value="1500"/>
|
|
||||||
<Parameter name="sth" value="0.7"/>
|
|
||||||
<Parameter name="extemp" value="293.15"/>
|
|
||||||
<Parameter name="p0" value="100000"/>
|
|
||||||
<Parameter name="T0" value="293.15"/>
|
|
||||||
<Parameter name="vol1" value="0"/>
|
|
||||||
<Parameter name="vol2" value="0"/>
|
|
||||||
<Parameter name="vol3" value="0"/>
|
|
||||||
<Parameter name="vol4" value="0"/>
|
|
||||||
<Parameter name="dvol1" value="0"/>
|
|
||||||
<Parameter name="dvol2" value="0"/>
|
|
||||||
<Parameter name="dvol3" value="0"/>
|
|
||||||
<Parameter name="dvol4" value="0"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_pnpl01_6" type="amesim_pnpl01" modelVersion="0.1.0"/>
|
|
||||||
<Component id="amesim_pnpl01_7" type="amesim_pnpl01" modelVersion="0.1.0"/>
|
|
||||||
<Component id="amesim_pnpl01_8" type="amesim_pnpl01" modelVersion="0.1.0"/>
|
|
||||||
<Component id="amesim_pnpl01_9" type="amesim_pnpl01" modelVersion="0.1.0"/>
|
|
||||||
<Component id="amesim_mecmas21_2" type="amesim_mecmas21" modelVersion="0.2.0">
|
|
||||||
<Parameter name="mass" value="50"/>
|
|
||||||
<Parameter name="fstick" value="0"/>
|
|
||||||
<Parameter name="fcoul" value="0"/>
|
|
||||||
<Parameter name="rvisc" value="0"/>
|
|
||||||
<Parameter name="wind" value="0"/>
|
|
||||||
<Parameter name="dvel" value="0.000001"/>
|
|
||||||
<Parameter name="restdvel" value="0.000001"/>
|
|
||||||
<Parameter name="restcoeff" value="0.65"/>
|
|
||||||
<Parameter name="astrib" value="0.001"/>
|
|
||||||
<Parameter name="xmin" value="-1"/>
|
|
||||||
<Parameter name="Kbmin" value="1000000000"/>
|
|
||||||
<Parameter name="Dbmin" value="10000"/>
|
|
||||||
<Parameter name="Pdmin" value="0.0001"/>
|
|
||||||
<Parameter name="xmax" value="0.8"/>
|
|
||||||
<Parameter name="Kbmax" value="1000000000"/>
|
|
||||||
<Parameter name="Dbmax" value="10000"/>
|
|
||||||
<Parameter name="Pdmax" value="0.0001"/>
|
|
||||||
<Parameter name="theta" value="0"/>
|
|
||||||
<Parameter name="useFriction" value="1"/>
|
|
||||||
<Parameter name="stoptype" value="4"/>
|
|
||||||
<Parameter name="discContactOption" value="1"/>
|
|
||||||
<Parameter name="strib" value="1"/>
|
|
||||||
<Parameter name="frictionType" value="1"/>
|
|
||||||
<Parameter name="v0" value="0"/>
|
|
||||||
<Parameter name="x0" value="0"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_f000_1" type="amesim_f000" modelVersion="0.1.0"/>
|
|
||||||
<Component id="amesim_f000_2" type="amesim_f000" modelVersion="0.1.0"/>
|
|
||||||
<Component id="amesim_lstp00a_1" type="amesim_lstp00a" modelVersion="0.2.0">
|
|
||||||
<Parameter name="na" value="10"/>
|
|
||||||
<Parameter name="gap0" value="0"/>
|
|
||||||
<Parameter name="kcont" value="100000000000"/>
|
|
||||||
<Parameter name="G" value="85700000000"/>
|
|
||||||
<Parameter name="sdiam" value="0.02"/>
|
|
||||||
<Parameter name="wdiam" value="0.002"/>
|
|
||||||
<Parameter name="rcont" value="100000000000"/>
|
|
||||||
<Parameter name="Pdis" value="1e-7"/>
|
|
||||||
<Parameter name="stiffmode" value="1"/>
|
|
||||||
<Parameter name="discContactOption" value="1"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_forc_2" type="amesim_forc" modelVersion="0.2.0">
|
|
||||||
<Parameter name="direction" value="1"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_ud00_2" type="amesim_ud00" modelVersion="0.2.0">
|
|
||||||
<Parameter name="tstart" value="0"/>
|
|
||||||
<Parameter name="start1" value="1000000000000"/>
|
|
||||||
<Parameter name="end1" value="1000000000000"/>
|
|
||||||
<Parameter name="t1" value="0.8"/>
|
|
||||||
<Parameter name="start2" value="0"/>
|
|
||||||
<Parameter name="end2" value="0"/>
|
|
||||||
<Parameter name="t2" value="10"/>
|
|
||||||
<Parameter name="start3" value="1"/>
|
|
||||||
<Parameter name="end3" value="1"/>
|
|
||||||
<Parameter name="t3" value="0"/>
|
|
||||||
<Parameter name="start4" value="1"/>
|
|
||||||
<Parameter name="end4" value="1"/>
|
|
||||||
<Parameter name="t4" value="0"/>
|
|
||||||
<Parameter name="start5" value="1"/>
|
|
||||||
<Parameter name="end5" value="1"/>
|
|
||||||
<Parameter name="t5" value="0"/>
|
|
||||||
<Parameter name="start6" value="1"/>
|
|
||||||
<Parameter name="end6" value="1"/>
|
|
||||||
<Parameter name="t6" value="0"/>
|
|
||||||
<Parameter name="start7" value="1"/>
|
|
||||||
<Parameter name="end7" value="1"/>
|
|
||||||
<Parameter name="t7" value="0"/>
|
|
||||||
<Parameter name="start8" value="1"/>
|
|
||||||
<Parameter name="end8" value="1"/>
|
|
||||||
<Parameter name="t8" value="0"/>
|
|
||||||
<Parameter name="nstages" value="2"/>
|
|
||||||
<Parameter name="iscyclic" value="0"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_mecmas21_5" type="amesim_mecmas21" modelVersion="0.2.0">
|
|
||||||
<Parameter name="mass" value="170000"/>
|
|
||||||
<Parameter name="fstick" value="0"/>
|
|
||||||
<Parameter name="fcoul" value="0"/>
|
|
||||||
<Parameter name="rvisc" value="0"/>
|
|
||||||
<Parameter name="wind" value="0"/>
|
|
||||||
<Parameter name="dvel" value="0.000001"/>
|
|
||||||
<Parameter name="restdvel" value="0.000001"/>
|
|
||||||
<Parameter name="restcoeff" value="0.65"/>
|
|
||||||
<Parameter name="astrib" value="0.001"/>
|
|
||||||
<Parameter name="xmin" value="0"/>
|
|
||||||
<Parameter name="Kbmin" value="1000000000"/>
|
|
||||||
<Parameter name="Dbmin" value="10000"/>
|
|
||||||
<Parameter name="Pdmin" value="0.0001"/>
|
|
||||||
<Parameter name="xmax" value="0.37"/>
|
|
||||||
<Parameter name="Kbmax" value="1000000000"/>
|
|
||||||
<Parameter name="Dbmax" value="10000"/>
|
|
||||||
<Parameter name="Pdmax" value="0.0001"/>
|
|
||||||
<Parameter name="theta" value="0"/>
|
|
||||||
<Parameter name="useFriction" value="1"/>
|
|
||||||
<Parameter name="stoptype" value="1"/>
|
|
||||||
<Parameter name="discContactOption" value="1"/>
|
|
||||||
<Parameter name="strib" value="1"/>
|
|
||||||
<Parameter name="frictionType" value="1"/>
|
|
||||||
<Parameter name="v0" value="0"/>
|
|
||||||
<Parameter name="x0" value="0"/>
|
|
||||||
</Component>
|
|
||||||
<Component id="amesim_mecmas21_7" type="amesim_mecmas21" modelVersion="0.2.0">
|
|
||||||
<Parameter name="mass" value="90000"/>
|
|
||||||
<Parameter name="fstick" value="0"/>
|
|
||||||
<Parameter name="fcoul" value="0"/>
|
|
||||||
<Parameter name="rvisc" value="0"/>
|
|
||||||
<Parameter name="wind" value="0"/>
|
|
||||||
<Parameter name="dvel" value="0.000001"/>
|
|
||||||
<Parameter name="restdvel" value="0.000001"/>
|
|
||||||
<Parameter name="restcoeff" value="0.65"/>
|
|
||||||
<Parameter name="astrib" value="0.001"/>
|
|
||||||
<Parameter name="xmin" value="-0.72"/>
|
|
||||||
<Parameter name="Kbmin" value="1000000000"/>
|
|
||||||
<Parameter name="Dbmin" value="10000"/>
|
|
||||||
<Parameter name="Pdmin" value="0.0001"/>
|
|
||||||
<Parameter name="xmax" value="0"/>
|
|
||||||
<Parameter name="Kbmax" value="1000000000"/>
|
|
||||||
<Parameter name="Dbmax" value="10000"/>
|
|
||||||
<Parameter name="Pdmax" value="0.0001"/>
|
|
||||||
<Parameter name="theta" value="0"/>
|
|
||||||
<Parameter name="useFriction" value="1"/>
|
|
||||||
<Parameter name="stoptype" value="1"/>
|
|
||||||
<Parameter name="discContactOption" value="1"/>
|
|
||||||
<Parameter name="strib" value="1"/>
|
|
||||||
<Parameter name="frictionType" value="1"/>
|
|
||||||
<Parameter name="v0" value="0"/>
|
|
||||||
<Parameter name="x0" value="0"/>
|
|
||||||
</Component>
|
|
||||||
</Components>
|
|
||||||
<Connections>
|
|
||||||
<Connection id="edge-amesim_pnch023_1-port_2-amesim_pnvo001_1-port_2-1786524999267">
|
|
||||||
<Endpoint component="amesim_pnch023_1" port="port_2"/>
|
|
||||||
<Endpoint component="amesim_pnvo001_1" port="port_2"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_pnvo001_1-port_3-amesim_pnch012_1-port_1-1786525009973">
|
|
||||||
<Endpoint component="amesim_pnvo001_1" port="port_3"/>
|
|
||||||
<Endpoint component="amesim_pnch012_1" port="port_1"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_pnpl01_2-port_1-amesim_pnch012_1-port_2-1786525035706-0">
|
|
||||||
<Endpoint component="amesim_pnpl01_2" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnch012_1" port="port_2"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_pnpl01_3-port_1-amesim_pnch012_1-port_3-1786525054054-0">
|
|
||||||
<Endpoint component="amesim_pnpl01_3" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnch012_1" port="port_3"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_pnpl01_4-port_1-amesim_pnch012_1-port_4-1786525061349-0">
|
|
||||||
<Endpoint component="amesim_pnpl01_4" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnch012_1" port="port_4"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_step0_1-out-amesim_pnvo001_1-res-1786525074494">
|
|
||||||
<Endpoint component="amesim_step0_1" port="out"/>
|
|
||||||
<Endpoint component="amesim_pnvo001_1" port="res"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_ud00_1-out-amesim_forc_1-res-1786525088412">
|
|
||||||
<Endpoint component="amesim_ud00_1" port="out"/>
|
|
||||||
<Endpoint component="amesim_forc_1" port="res"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_pnpl01_6-port_1-amesim_pnch023_1-port_1-1786525164428-0">
|
|
||||||
<Endpoint component="amesim_pnpl01_6" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnch023_1" port="port_1"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_pnpl01_7-port_1-amesim_pnch012_2-port_1-1786525181969-0">
|
|
||||||
<Endpoint component="amesim_pnpl01_7" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnch012_2" port="port_1"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_pnpl01_8-port_1-amesim_pnch012_2-port_4-1786525185493-0">
|
|
||||||
<Endpoint component="amesim_pnpl01_8" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnch012_2" port="port_4"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_pnpl01_9-port_1-amesim_pnch012_2-port_2-1786525188482-0">
|
|
||||||
<Endpoint component="amesim_pnpl01_9" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnch012_2" port="port_2"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_mecmas21_2-port_1-amesim_pnrp17_1-port_2-1786525203778">
|
|
||||||
<Endpoint component="amesim_mecmas21_2" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnrp17_1" port="port_2"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_pnrp17_1-port_5-amesim_lstp00a_1-port_1-1786525213548">
|
|
||||||
<Endpoint component="amesim_pnrp17_1" port="port_5"/>
|
|
||||||
<Endpoint component="amesim_lstp00a_1" port="port_1"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_ud00_2-out-amesim_forc_2-res-1786525226761">
|
|
||||||
<Endpoint component="amesim_ud00_2" port="out"/>
|
|
||||||
<Endpoint component="amesim_forc_2" port="res"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_pnrp17_1-port_1-amesim_pnch012_2-port_3-1786525729832-1">
|
|
||||||
<Endpoint component="amesim_pnrp17_1" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnch012_2" port="port_3"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_forc_1-port_2-amesim_mecmas21_7-port_2-1786525915163">
|
|
||||||
<Endpoint component="amesim_forc_1" port="port_2"/>
|
|
||||||
<Endpoint component="amesim_mecmas21_7" port="port_2"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_mecmas21_7-port_1-amesim_pnrp17_1-port_3-1786525916652">
|
|
||||||
<Endpoint component="amesim_mecmas21_7" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnrp17_1" port="port_3"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_mecmas21_5-port_1-amesim_forc_2-port_2-1786525922010">
|
|
||||||
<Endpoint component="amesim_mecmas21_5" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_forc_2" port="port_2"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-amesim_lstp00a_1-port_2-amesim_mecmas21_5-port_2-1786525924224">
|
|
||||||
<Endpoint component="amesim_lstp00a_1" port="port_2"/>
|
|
||||||
<Endpoint component="amesim_mecmas21_5" port="port_2"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_f000_2-port_1-amesim_pnrp17_1-port_4-1788429018340-0">
|
|
||||||
<Endpoint component="amesim_f000_2" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_pnrp17_1" port="port_4"/>
|
|
||||||
</Connection>
|
|
||||||
<Connection id="edge-contact-amesim_f000_1-port_1-amesim_mecmas21_2-port_2-1788429022851-0">
|
|
||||||
<Endpoint component="amesim_f000_1" port="port_1"/>
|
|
||||||
<Endpoint component="amesim_mecmas21_2" port="port_2"/>
|
|
||||||
</Connection>
|
|
||||||
</Connections>
|
|
||||||
</System>
|
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"projectSchemaVersion": 1,
|
"projectSchemaVersion": 1,
|
||||||
"name": "test-mql-8",
|
"name": "test-mql-8-corrected",
|
||||||
"nodes": [
|
"nodes": [
|
||||||
{
|
{
|
||||||
"id": "pn_gas_data",
|
"id": "pn_gas_data",
|
||||||
@@ -2679,7 +2679,7 @@
|
|||||||
"parameters": {},
|
"parameters": {},
|
||||||
"parameterUnits": {},
|
"parameterUnits": {},
|
||||||
"parameterScientificNotation": {},
|
"parameterScientificNotation": {},
|
||||||
"rotation": 0,
|
"rotation": 180,
|
||||||
"mirrored": false
|
"mirrored": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2732,7 +2732,7 @@
|
|||||||
"parameters": {},
|
"parameters": {},
|
||||||
"parameterUnits": {},
|
"parameterUnits": {},
|
||||||
"parameterScientificNotation": {},
|
"parameterScientificNotation": {},
|
||||||
"rotation": 0,
|
"rotation": 180,
|
||||||
"mirrored": false
|
"mirrored": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2785,7 +2785,7 @@
|
|||||||
"parameters": {},
|
"parameters": {},
|
||||||
"parameterUnits": {},
|
"parameterUnits": {},
|
||||||
"parameterScientificNotation": {},
|
"parameterScientificNotation": {},
|
||||||
"rotation": 0,
|
"rotation": 180,
|
||||||
"mirrored": false
|
"mirrored": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2838,7 +2838,7 @@
|
|||||||
"parameters": {},
|
"parameters": {},
|
||||||
"parameterUnits": {},
|
"parameterUnits": {},
|
||||||
"parameterScientificNotation": {},
|
"parameterScientificNotation": {},
|
||||||
"rotation": 0,
|
"rotation": 180,
|
||||||
"mirrored": false
|
"mirrored": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2891,7 +2891,7 @@
|
|||||||
"parameters": {},
|
"parameters": {},
|
||||||
"parameterUnits": {},
|
"parameterUnits": {},
|
||||||
"parameterScientificNotation": {},
|
"parameterScientificNotation": {},
|
||||||
"rotation": 0,
|
"rotation": 180,
|
||||||
"mirrored": false
|
"mirrored": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2944,7 +2944,7 @@
|
|||||||
"parameters": {},
|
"parameters": {},
|
||||||
"parameterUnits": {},
|
"parameterUnits": {},
|
||||||
"parameterScientificNotation": {},
|
"parameterScientificNotation": {},
|
||||||
"rotation": 0,
|
"rotation": 180,
|
||||||
"mirrored": false
|
"mirrored": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2997,7 +2997,7 @@
|
|||||||
"parameters": {},
|
"parameters": {},
|
||||||
"parameterUnits": {},
|
"parameterUnits": {},
|
||||||
"parameterScientificNotation": {},
|
"parameterScientificNotation": {},
|
||||||
"rotation": 0,
|
"rotation": 180,
|
||||||
"mirrored": false
|
"mirrored": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -7368,7 +7368,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"parameters": {
|
"parameters": {
|
||||||
"mass": 90000,
|
"mass": 100.0,
|
||||||
"fstick": 0,
|
"fstick": 0,
|
||||||
"fcoul": 0,
|
"fcoul": 0,
|
||||||
"rvisc": 0,
|
"rvisc": 0,
|
||||||
@@ -7377,17 +7377,17 @@
|
|||||||
"restdvel": 1e-06,
|
"restdvel": 1e-06,
|
||||||
"restcoeff": 0.65,
|
"restcoeff": 0.65,
|
||||||
"astrib": 0.001,
|
"astrib": 0.001,
|
||||||
"xmin": -0.72,
|
"xmin": -1.0,
|
||||||
"Kbmin": 1000000000,
|
"Kbmin": 1000000000,
|
||||||
"Dbmin": 10000,
|
"Dbmin": 10000,
|
||||||
"Pdmin": 0.0001,
|
"Pdmin": 0.0001,
|
||||||
"xmax": 0,
|
"xmax": 1.0,
|
||||||
"Kbmax": 1000000000,
|
"Kbmax": 1000000000,
|
||||||
"Dbmax": 10000,
|
"Dbmax": 10000,
|
||||||
"Pdmax": 0.0001,
|
"Pdmax": 0.0001,
|
||||||
"theta": 0,
|
"theta": 0,
|
||||||
"useFriction": 1,
|
"useFriction": 1,
|
||||||
"stoptype": 1,
|
"stoptype": 4.0,
|
||||||
"discContactOption": 1,
|
"discContactOption": 1,
|
||||||
"strib": 1,
|
"strib": 1,
|
||||||
"frictionType": 1,
|
"frictionType": 1,
|
||||||
@@ -8768,7 +8768,7 @@
|
|||||||
"source": "amesim_pnvo001_1",
|
"source": "amesim_pnvo001_1",
|
||||||
"target": "amesim_p4node2_1",
|
"target": "amesim_p4node2_1",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_3",
|
"targetHandle": "port_4",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
@@ -8778,7 +8778,7 @@
|
|||||||
"source": "amesim_pnvo001_2",
|
"source": "amesim_pnvo001_2",
|
||||||
"target": "amesim_p4node2_2",
|
"target": "amesim_p4node2_2",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_4",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
@@ -8788,7 +8788,7 @@
|
|||||||
"source": "amesim_pnvo001_3",
|
"source": "amesim_pnvo001_3",
|
||||||
"target": "amesim_p4node2_3",
|
"target": "amesim_p4node2_3",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_4",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
@@ -8798,7 +8798,7 @@
|
|||||||
"source": "amesim_pnvo001_4",
|
"source": "amesim_pnvo001_4",
|
||||||
"target": "amesim_p4node2_4",
|
"target": "amesim_p4node2_4",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_4",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
@@ -8807,7 +8807,7 @@
|
|||||||
"id": "edge-amesim_p4node2_2-port_4-amesim_pnl0002_2-port_1-1786949195556",
|
"id": "edge-amesim_p4node2_2-port_4-amesim_pnl0002_2-port_1-1786949195556",
|
||||||
"source": "amesim_p4node2_2",
|
"source": "amesim_p4node2_2",
|
||||||
"target": "amesim_pnl0002_2",
|
"target": "amesim_pnl0002_2",
|
||||||
"sourceHandle": "port_4",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_1",
|
"targetHandle": "port_1",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -8818,7 +8818,7 @@
|
|||||||
"source": "amesim_pnl0002_2",
|
"source": "amesim_pnl0002_2",
|
||||||
"target": "amesim_p4node2_3",
|
"target": "amesim_p4node2_3",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_1",
|
"targetHandle": "port_3",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
@@ -8828,26 +8828,26 @@
|
|||||||
"source": "amesim_pnl0002_3",
|
"source": "amesim_pnl0002_3",
|
||||||
"target": "amesim_p4node2_3",
|
"target": "amesim_p4node2_3",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_4",
|
"targetHandle": "port_1",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": true
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "edge-contact-amesim_p4node2_4-port_1-amesim_pnl0002_3-port_2-1786949205596-0",
|
"id": "edge-contact-amesim_p4node2_4-port_1-amesim_pnl0002_3-port_2-1786949205596-0",
|
||||||
"source": "amesim_p4node2_4",
|
"source": "amesim_p4node2_4",
|
||||||
"target": "amesim_pnl0002_3",
|
"target": "amesim_pnl0002_3",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_3",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": true
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "edge-amesim_p4node2_4-port_4-amesim_pnl0002_4-port_1-1786949231675",
|
"id": "edge-amesim_p4node2_4-port_4-amesim_pnl0002_4-port_1-1786949231675",
|
||||||
"source": "amesim_p4node2_4",
|
"source": "amesim_p4node2_4",
|
||||||
"target": "amesim_pnl0002_4",
|
"target": "amesim_pnl0002_4",
|
||||||
"sourceHandle": "port_4",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_1",
|
"targetHandle": "port_1",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -8858,7 +8858,7 @@
|
|||||||
"source": "amesim_pnl0002_4",
|
"source": "amesim_pnl0002_4",
|
||||||
"target": "amesim_p4node2_5",
|
"target": "amesim_p4node2_5",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_1",
|
"targetHandle": "port_3",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
@@ -8867,20 +8867,20 @@
|
|||||||
"id": "edge-contact-amesim_p4node2_5-port_4-amesim_pnl0002_5-port_1-1786949246412-0",
|
"id": "edge-contact-amesim_p4node2_5-port_4-amesim_pnl0002_5-port_1-1786949246412-0",
|
||||||
"source": "amesim_p4node2_5",
|
"source": "amesim_p4node2_5",
|
||||||
"target": "amesim_pnl0002_5",
|
"target": "amesim_pnl0002_5",
|
||||||
"sourceHandle": "port_4",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_1",
|
"targetHandle": "port_1",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": true
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "edge-contact-amesim_p4node2_6-port_1-amesim_pnl0002_5-port_2-1786949247647-0",
|
"id": "edge-contact-amesim_p4node2_6-port_1-amesim_pnl0002_5-port_2-1786949247647-0",
|
||||||
"source": "amesim_p4node2_6",
|
"source": "amesim_p4node2_6",
|
||||||
"target": "amesim_pnl0002_5",
|
"target": "amesim_pnl0002_5",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_3",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": true
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -8888,7 +8888,7 @@
|
|||||||
"source": "amesim_pnl0002_6",
|
"source": "amesim_pnl0002_6",
|
||||||
"target": "amesim_p4node2_6",
|
"target": "amesim_p4node2_6",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_4",
|
"targetHandle": "port_1",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
@@ -8898,7 +8898,7 @@
|
|||||||
"source": "amesim_pnl0002_6",
|
"source": "amesim_pnl0002_6",
|
||||||
"target": "amesim_p4node2_7",
|
"target": "amesim_p4node2_7",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_1",
|
"targetHandle": "port_3",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
@@ -8908,26 +8908,26 @@
|
|||||||
"source": "amesim_pnl0002_7",
|
"source": "amesim_pnl0002_7",
|
||||||
"target": "amesim_p4node2_7",
|
"target": "amesim_p4node2_7",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_4",
|
"targetHandle": "port_1",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": true
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "edge-contact-amesim_p4node2_8-port_1-amesim_pnl0002_7-port_2-1786949266454-0",
|
"id": "edge-contact-amesim_p4node2_8-port_1-amesim_pnl0002_7-port_2-1786949266454-0",
|
||||||
"source": "amesim_p4node2_8",
|
"source": "amesim_p4node2_8",
|
||||||
"target": "amesim_pnl0002_7",
|
"target": "amesim_pnl0002_7",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_3",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": true
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "edge-amesim_p4node2_2-port_3-amesim_pnl0001_14-port_2-1786949362056",
|
"id": "edge-amesim_p4node2_2-port_3-amesim_pnl0001_14-port_2-1786949362056",
|
||||||
"source": "amesim_p4node2_2",
|
"source": "amesim_p4node2_2",
|
||||||
"target": "amesim_pnl0001_14",
|
"target": "amesim_pnl0001_14",
|
||||||
"sourceHandle": "port_3",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -8937,7 +8937,7 @@
|
|||||||
"id": "edge-amesim_p4node2_3-port_3-amesim_pnl0001_15-port_2-1786949364233",
|
"id": "edge-amesim_p4node2_3-port_3-amesim_pnl0001_15-port_2-1786949364233",
|
||||||
"source": "amesim_p4node2_3",
|
"source": "amesim_p4node2_3",
|
||||||
"target": "amesim_pnl0001_15",
|
"target": "amesim_pnl0001_15",
|
||||||
"sourceHandle": "port_3",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -8947,7 +8947,7 @@
|
|||||||
"id": "edge-amesim_p4node2_4-port_3-amesim_pnl0001_16-port_2-1786949365198",
|
"id": "edge-amesim_p4node2_4-port_3-amesim_pnl0001_16-port_2-1786949365198",
|
||||||
"source": "amesim_p4node2_4",
|
"source": "amesim_p4node2_4",
|
||||||
"target": "amesim_pnl0001_16",
|
"target": "amesim_pnl0001_16",
|
||||||
"sourceHandle": "port_3",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -8957,7 +8957,7 @@
|
|||||||
"id": "edge-amesim_p4node2_5-port_3-amesim_pnl0001_17-port_2-1786949374542",
|
"id": "edge-amesim_p4node2_5-port_3-amesim_pnl0001_17-port_2-1786949374542",
|
||||||
"source": "amesim_p4node2_5",
|
"source": "amesim_p4node2_5",
|
||||||
"target": "amesim_pnl0001_17",
|
"target": "amesim_pnl0001_17",
|
||||||
"sourceHandle": "port_3",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -8967,7 +8967,7 @@
|
|||||||
"id": "edge-amesim_p4node2_6-port_3-amesim_pnl0001_18-port_2-1786949377180",
|
"id": "edge-amesim_p4node2_6-port_3-amesim_pnl0001_18-port_2-1786949377180",
|
||||||
"source": "amesim_p4node2_6",
|
"source": "amesim_p4node2_6",
|
||||||
"target": "amesim_pnl0001_18",
|
"target": "amesim_pnl0001_18",
|
||||||
"sourceHandle": "port_3",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -8977,7 +8977,7 @@
|
|||||||
"id": "edge-amesim_p4node2_7-port_3-amesim_pnl0001_19-port_2-1786949380063",
|
"id": "edge-amesim_p4node2_7-port_3-amesim_pnl0001_19-port_2-1786949380063",
|
||||||
"source": "amesim_p4node2_7",
|
"source": "amesim_p4node2_7",
|
||||||
"target": "amesim_pnl0001_19",
|
"target": "amesim_pnl0001_19",
|
||||||
"sourceHandle": "port_3",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -8987,7 +8987,7 @@
|
|||||||
"id": "edge-amesim_p4node2_8-port_3-amesim_pnl0001_20-port_2-1786949381164",
|
"id": "edge-amesim_p4node2_8-port_3-amesim_pnl0001_20-port_2-1786949381164",
|
||||||
"source": "amesim_p4node2_8",
|
"source": "amesim_p4node2_8",
|
||||||
"target": "amesim_pnl0001_20",
|
"target": "amesim_pnl0001_20",
|
||||||
"sourceHandle": "port_3",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -9018,9 +9018,9 @@
|
|||||||
"source": "amesim_pnpl01_2",
|
"source": "amesim_pnpl01_2",
|
||||||
"target": "amesim_pnch012_8",
|
"target": "amesim_pnch012_8",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_3",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": true
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -9038,9 +9038,9 @@
|
|||||||
"source": "amesim_pnpl01_4",
|
"source": "amesim_pnpl01_4",
|
||||||
"target": "amesim_pnch012_9",
|
"target": "amesim_pnch012_9",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_3",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": true
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -9068,9 +9068,9 @@
|
|||||||
"source": "amesim_pnpl01_6",
|
"source": "amesim_pnpl01_6",
|
||||||
"target": "amesim_pnch012_10",
|
"target": "amesim_pnch012_10",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_3",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": true
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -9098,9 +9098,9 @@
|
|||||||
"source": "amesim_pnpl01_8",
|
"source": "amesim_pnpl01_8",
|
||||||
"target": "amesim_pnch012_11",
|
"target": "amesim_pnch012_11",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_3",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": true
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -9128,9 +9128,9 @@
|
|||||||
"source": "amesim_pnpl01_10",
|
"source": "amesim_pnpl01_10",
|
||||||
"target": "amesim_pnch012_12",
|
"target": "amesim_pnch012_12",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_3",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": true
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -9158,9 +9158,9 @@
|
|||||||
"source": "amesim_pnpl01_12",
|
"source": "amesim_pnpl01_12",
|
||||||
"target": "amesim_pnch012_13",
|
"target": "amesim_pnch012_13",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_3",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": true
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -9188,9 +9188,9 @@
|
|||||||
"source": "amesim_pnpl01_14",
|
"source": "amesim_pnpl01_14",
|
||||||
"target": "amesim_pnch012_14",
|
"target": "amesim_pnch012_14",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_3",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": true
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -9218,9 +9218,9 @@
|
|||||||
"source": "amesim_pnpl01_16",
|
"source": "amesim_pnpl01_16",
|
||||||
"target": "amesim_pnch012_15",
|
"target": "amesim_pnch012_15",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_3",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": true
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -9237,7 +9237,7 @@
|
|||||||
"id": "edge-amesim_pnch012_15-port_2-amesim_pnrp17_1-port_1-1786949561970",
|
"id": "edge-amesim_pnch012_15-port_2-amesim_pnrp17_1-port_1-1786949561970",
|
||||||
"source": "amesim_pnch012_15",
|
"source": "amesim_pnch012_15",
|
||||||
"target": "amesim_pnrp17_1",
|
"target": "amesim_pnrp17_1",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_3",
|
||||||
"targetHandle": "port_1",
|
"targetHandle": "port_1",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -9247,7 +9247,7 @@
|
|||||||
"id": "edge-amesim_pnch012_14-port_2-amesim_pnrp17_2-port_1-1786949570722",
|
"id": "edge-amesim_pnch012_14-port_2-amesim_pnrp17_2-port_1-1786949570722",
|
||||||
"source": "amesim_pnch012_14",
|
"source": "amesim_pnch012_14",
|
||||||
"target": "amesim_pnrp17_2",
|
"target": "amesim_pnrp17_2",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_3",
|
||||||
"targetHandle": "port_1",
|
"targetHandle": "port_1",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -9257,7 +9257,7 @@
|
|||||||
"id": "edge-amesim_pnch012_13-port_2-amesim_pnrp17_3-port_1-1786949575918",
|
"id": "edge-amesim_pnch012_13-port_2-amesim_pnrp17_3-port_1-1786949575918",
|
||||||
"source": "amesim_pnch012_13",
|
"source": "amesim_pnch012_13",
|
||||||
"target": "amesim_pnrp17_3",
|
"target": "amesim_pnrp17_3",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_3",
|
||||||
"targetHandle": "port_1",
|
"targetHandle": "port_1",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -9267,7 +9267,7 @@
|
|||||||
"id": "edge-amesim_pnch012_12-port_2-amesim_pnrp17_4-port_1-1786949579000",
|
"id": "edge-amesim_pnch012_12-port_2-amesim_pnrp17_4-port_1-1786949579000",
|
||||||
"source": "amesim_pnch012_12",
|
"source": "amesim_pnch012_12",
|
||||||
"target": "amesim_pnrp17_4",
|
"target": "amesim_pnrp17_4",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_3",
|
||||||
"targetHandle": "port_1",
|
"targetHandle": "port_1",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -9277,7 +9277,7 @@
|
|||||||
"id": "edge-amesim_pnch012_11-port_2-amesim_pnrp17_5-port_1-1786949583304",
|
"id": "edge-amesim_pnch012_11-port_2-amesim_pnrp17_5-port_1-1786949583304",
|
||||||
"source": "amesim_pnch012_11",
|
"source": "amesim_pnch012_11",
|
||||||
"target": "amesim_pnrp17_5",
|
"target": "amesim_pnrp17_5",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_3",
|
||||||
"targetHandle": "port_1",
|
"targetHandle": "port_1",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -9287,7 +9287,7 @@
|
|||||||
"id": "edge-amesim_pnch012_10-port_2-amesim_pnrp17_6-port_1-1786949586596",
|
"id": "edge-amesim_pnch012_10-port_2-amesim_pnrp17_6-port_1-1786949586596",
|
||||||
"source": "amesim_pnch012_10",
|
"source": "amesim_pnch012_10",
|
||||||
"target": "amesim_pnrp17_6",
|
"target": "amesim_pnrp17_6",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_3",
|
||||||
"targetHandle": "port_1",
|
"targetHandle": "port_1",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -9297,7 +9297,7 @@
|
|||||||
"id": "edge-amesim_pnch012_9-port_2-amesim_pnrp17_7-port_1-1786949589317",
|
"id": "edge-amesim_pnch012_9-port_2-amesim_pnrp17_7-port_1-1786949589317",
|
||||||
"source": "amesim_pnch012_9",
|
"source": "amesim_pnch012_9",
|
||||||
"target": "amesim_pnrp17_7",
|
"target": "amesim_pnrp17_7",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_3",
|
||||||
"targetHandle": "port_1",
|
"targetHandle": "port_1",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -9307,7 +9307,7 @@
|
|||||||
"id": "edge-amesim_pnch012_8-port_2-amesim_pnrp17_8-port_1-1786949590677",
|
"id": "edge-amesim_pnch012_8-port_2-amesim_pnrp17_8-port_1-1786949590677",
|
||||||
"source": "amesim_pnch012_8",
|
"source": "amesim_pnch012_8",
|
||||||
"target": "amesim_pnrp17_8",
|
"target": "amesim_pnrp17_8",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_3",
|
||||||
"targetHandle": "port_1",
|
"targetHandle": "port_1",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -9667,7 +9667,7 @@
|
|||||||
"id": "edge-amesim_lmechn1_3-port_1-amesim_pnrp17_1-port_3-1786949849269",
|
"id": "edge-amesim_lmechn1_3-port_1-amesim_pnrp17_1-port_3-1786949849269",
|
||||||
"source": "amesim_lmechn1_3",
|
"source": "amesim_lmechn1_3",
|
||||||
"target": "amesim_pnrp17_1",
|
"target": "amesim_pnrp17_1",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_8",
|
||||||
"targetHandle": "port_3",
|
"targetHandle": "port_3",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -9677,7 +9677,7 @@
|
|||||||
"id": "edge-amesim_lmechn1_3-port_2-amesim_pnrp17_2-port_3-1786949850839",
|
"id": "edge-amesim_lmechn1_3-port_2-amesim_pnrp17_2-port_3-1786949850839",
|
||||||
"source": "amesim_lmechn1_3",
|
"source": "amesim_lmechn1_3",
|
||||||
"target": "amesim_pnrp17_2",
|
"target": "amesim_pnrp17_2",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_7",
|
||||||
"targetHandle": "port_3",
|
"targetHandle": "port_3",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -9687,7 +9687,7 @@
|
|||||||
"id": "edge-amesim_lmechn1_3-port_3-amesim_pnrp17_3-port_3-1786949852383",
|
"id": "edge-amesim_lmechn1_3-port_3-amesim_pnrp17_3-port_3-1786949852383",
|
||||||
"source": "amesim_lmechn1_3",
|
"source": "amesim_lmechn1_3",
|
||||||
"target": "amesim_pnrp17_3",
|
"target": "amesim_pnrp17_3",
|
||||||
"sourceHandle": "port_3",
|
"sourceHandle": "port_6",
|
||||||
"targetHandle": "port_3",
|
"targetHandle": "port_3",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -9697,7 +9697,7 @@
|
|||||||
"id": "edge-amesim_lmechn1_3-port_4-amesim_pnrp17_4-port_3-1786949854874",
|
"id": "edge-amesim_lmechn1_3-port_4-amesim_pnrp17_4-port_3-1786949854874",
|
||||||
"source": "amesim_lmechn1_3",
|
"source": "amesim_lmechn1_3",
|
||||||
"target": "amesim_pnrp17_4",
|
"target": "amesim_pnrp17_4",
|
||||||
"sourceHandle": "port_4",
|
"sourceHandle": "port_5",
|
||||||
"targetHandle": "port_3",
|
"targetHandle": "port_3",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -9707,7 +9707,7 @@
|
|||||||
"id": "edge-amesim_lmechn1_3-port_5-amesim_pnrp17_5-port_3-1786949858405",
|
"id": "edge-amesim_lmechn1_3-port_5-amesim_pnrp17_5-port_3-1786949858405",
|
||||||
"source": "amesim_lmechn1_3",
|
"source": "amesim_lmechn1_3",
|
||||||
"target": "amesim_pnrp17_5",
|
"target": "amesim_pnrp17_5",
|
||||||
"sourceHandle": "port_5",
|
"sourceHandle": "port_4",
|
||||||
"targetHandle": "port_3",
|
"targetHandle": "port_3",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -9717,7 +9717,7 @@
|
|||||||
"id": "edge-amesim_lmechn1_3-port_6-amesim_pnrp17_6-port_3-1786949862365",
|
"id": "edge-amesim_lmechn1_3-port_6-amesim_pnrp17_6-port_3-1786949862365",
|
||||||
"source": "amesim_lmechn1_3",
|
"source": "amesim_lmechn1_3",
|
||||||
"target": "amesim_pnrp17_6",
|
"target": "amesim_pnrp17_6",
|
||||||
"sourceHandle": "port_6",
|
"sourceHandle": "port_3",
|
||||||
"targetHandle": "port_3",
|
"targetHandle": "port_3",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -9727,7 +9727,7 @@
|
|||||||
"id": "edge-amesim_lmechn1_3-port_7-amesim_pnrp17_7-port_3-1786949896879",
|
"id": "edge-amesim_lmechn1_3-port_7-amesim_pnrp17_7-port_3-1786949896879",
|
||||||
"source": "amesim_lmechn1_3",
|
"source": "amesim_lmechn1_3",
|
||||||
"target": "amesim_pnrp17_7",
|
"target": "amesim_pnrp17_7",
|
||||||
"sourceHandle": "port_7",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_3",
|
"targetHandle": "port_3",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -9737,7 +9737,7 @@
|
|||||||
"id": "edge-amesim_lmechn1_3-port_8-amesim_pnrp17_8-port_3-1786949898922",
|
"id": "edge-amesim_lmechn1_3-port_8-amesim_pnrp17_8-port_3-1786949898922",
|
||||||
"source": "amesim_lmechn1_3",
|
"source": "amesim_lmechn1_3",
|
||||||
"target": "amesim_pnrp17_8",
|
"target": "amesim_pnrp17_8",
|
||||||
"sourceHandle": "port_8",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_3",
|
"targetHandle": "port_3",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
@@ -9778,7 +9778,7 @@
|
|||||||
"source": "amesim_lstp00a_1",
|
"source": "amesim_lstp00a_1",
|
||||||
"target": "amesim_lmechn1_4",
|
"target": "amesim_lmechn1_4",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_8",
|
"targetHandle": "port_1",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
@@ -9788,7 +9788,7 @@
|
|||||||
"source": "amesim_lstp00a_2",
|
"source": "amesim_lstp00a_2",
|
||||||
"target": "amesim_lmechn1_4",
|
"target": "amesim_lmechn1_4",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_7",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
@@ -9798,7 +9798,7 @@
|
|||||||
"source": "amesim_lstp00a_3",
|
"source": "amesim_lstp00a_3",
|
||||||
"target": "amesim_lmechn1_4",
|
"target": "amesim_lmechn1_4",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_6",
|
"targetHandle": "port_3",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
@@ -9808,7 +9808,7 @@
|
|||||||
"source": "amesim_lstp00a_4",
|
"source": "amesim_lstp00a_4",
|
||||||
"target": "amesim_lmechn1_4",
|
"target": "amesim_lmechn1_4",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_5",
|
"targetHandle": "port_4",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
@@ -9818,7 +9818,7 @@
|
|||||||
"source": "amesim_lstp00a_5",
|
"source": "amesim_lstp00a_5",
|
||||||
"target": "amesim_lmechn1_4",
|
"target": "amesim_lmechn1_4",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_4",
|
"targetHandle": "port_5",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
@@ -9828,7 +9828,7 @@
|
|||||||
"source": "amesim_lstp00a_6",
|
"source": "amesim_lstp00a_6",
|
||||||
"target": "amesim_lmechn1_4",
|
"target": "amesim_lmechn1_4",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_3",
|
"targetHandle": "port_6",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
@@ -9838,7 +9838,7 @@
|
|||||||
"source": "amesim_lstp00a_7",
|
"source": "amesim_lstp00a_7",
|
||||||
"target": "amesim_lmechn1_4",
|
"target": "amesim_lmechn1_4",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_7",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
@@ -9848,7 +9848,7 @@
|
|||||||
"source": "amesim_lstp00a_8",
|
"source": "amesim_lstp00a_8",
|
||||||
"target": "amesim_lmechn1_4",
|
"target": "amesim_lmechn1_4",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_1",
|
"targetHandle": "port_8",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
@@ -9978,9 +9978,9 @@
|
|||||||
"source": "amesim_pnl0002_1",
|
"source": "amesim_pnl0002_1",
|
||||||
"target": "amesim_p4node2_2",
|
"target": "amesim_p4node2_2",
|
||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_1",
|
"targetHandle": "port_3",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": true
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -9997,20 +9997,10 @@
|
|||||||
"id": "edge-amesim_p4node2_1-port_4-amesim_pnl0002_8-port_2-1786972824427",
|
"id": "edge-amesim_p4node2_1-port_4-amesim_pnl0002_8-port_2-1786972824427",
|
||||||
"source": "amesim_p4node2_1",
|
"source": "amesim_p4node2_1",
|
||||||
"target": "amesim_pnl0002_8",
|
"target": "amesim_pnl0002_8",
|
||||||
"sourceHandle": "port_4",
|
"sourceHandle": "port_3",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false,
|
"isContactEdge": false
|
||||||
"routePoints": [
|
|
||||||
{
|
|
||||||
"x": 19.87945415380198,
|
|
||||||
"y": 52.17994257325029
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x": 199.87521088533356,
|
|
||||||
"y": 52.17994257325029
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -10020,37 +10010,17 @@
|
|||||||
"sourceHandle": "port_2",
|
"sourceHandle": "port_2",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_2",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false,
|
"isContactEdge": false
|
||||||
"routePoints": [
|
|
||||||
{
|
|
||||||
"x": 80.32926682952949,
|
|
||||||
"y": 139.08364630171945
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x": 80.32926682952949,
|
|
||||||
"y": 181.8752425865067
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "edge-amesim_p4node2_8-port_4-amesim_pnl0002_8-port_1-1786972860850",
|
"id": "edge-amesim_p4node2_8-port_4-amesim_pnl0002_8-port_1-1786972860850",
|
||||||
"source": "amesim_p4node2_8",
|
"source": "amesim_p4node2_8",
|
||||||
"target": "amesim_pnl0002_8",
|
"target": "amesim_pnl0002_8",
|
||||||
"sourceHandle": "port_4",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_1",
|
"targetHandle": "port_1",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false,
|
"isContactEdge": false
|
||||||
"routePoints": [
|
|
||||||
{
|
|
||||||
"x": 21.87942245262885,
|
|
||||||
"y": 1602.5991575147043
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x": 199.87521088533356,
|
|
||||||
"y": 1602.5991575147043
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -10058,7 +10028,7 @@
|
|||||||
"source": "amesim_pnl0001_21",
|
"source": "amesim_pnl0001_21",
|
||||||
"target": "amesim_p4node2_5",
|
"target": "amesim_p4node2_5",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_4",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false
|
"isContactEdge": false
|
||||||
}
|
}
|
||||||
@@ -10098,19 +10068,9 @@
|
|||||||
"source": "amesim_pnl0001_27",
|
"source": "amesim_pnl0001_27",
|
||||||
"target": "amesim_p4node2_8",
|
"target": "amesim_p4node2_8",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_4",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false,
|
"isContactEdge": false
|
||||||
"routePoints": [
|
|
||||||
{
|
|
||||||
"x": -8.800000000000004,
|
|
||||||
"y": 1512
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x": -8.800000000000004,
|
|
||||||
"y": 1449.5999999999997
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -10138,19 +10098,9 @@
|
|||||||
"source": "amesim_pnl0001_26",
|
"source": "amesim_pnl0001_26",
|
||||||
"target": "amesim_p4node2_7",
|
"target": "amesim_p4node2_7",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_4",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false,
|
"isContactEdge": false
|
||||||
"routePoints": [
|
|
||||||
{
|
|
||||||
"x": -8.800000000000004,
|
|
||||||
"y": 1350
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x": -8.800000000000004,
|
|
||||||
"y": 1344
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -10178,27 +10128,17 @@
|
|||||||
"source": "amesim_pnl0001_25",
|
"source": "amesim_pnl0001_25",
|
||||||
"target": "amesim_p4node2_6",
|
"target": "amesim_p4node2_6",
|
||||||
"sourceHandle": "port_1",
|
"sourceHandle": "port_1",
|
||||||
"targetHandle": "port_2",
|
"targetHandle": "port_4",
|
||||||
"data": {
|
"data": {
|
||||||
"isContactEdge": false,
|
"isContactEdge": false
|
||||||
"routePoints": [
|
|
||||||
{
|
|
||||||
"x": -9.800000000000004,
|
|
||||||
"y": 1116
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"x": -9.800000000000004,
|
|
||||||
"y": 1054.8
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"simulation": {
|
"simulation": {
|
||||||
"t_start": 0,
|
"t_start": 0.0,
|
||||||
"t_stop": 10,
|
"t_stop": 10.0,
|
||||||
"step": 0.01,
|
"step": 0.01,
|
||||||
"max_step": 0.02,
|
"max_step": 1e+30,
|
||||||
"method": "BDF"
|
"method": "BDF"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,91 @@
|
|||||||
|
"""Compare identical native models with the old fixed-point, previous and current pipe solvers.
|
||||||
|
|
||||||
|
Run with Python 3.12 from the repository; generated programs/results stay in --output-dir.
|
||||||
|
The baseline ref is an explicit Git revision, never a frozen numerical result substituted for execution.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse
|
||||||
|
from hashlib import sha256
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import statistics
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
from app.main import compile_system_xml_network
|
||||||
|
from app.simulation.backends import simulation_config
|
||||||
|
from app.simulation.native_codegen import build as builder
|
||||||
|
from app.simulation.native_codegen.compiler import compile_native_program
|
||||||
|
from app.simulation.native_codegen.input import load_input
|
||||||
|
from app.simulation.native_codegen.runner import execute_native
|
||||||
|
|
||||||
|
OLD_ITERATION = '''double rough_limit=pipe_rough_limit(rr);
|
||||||
|
double base=area*p*cm/sqrt(T),q=sqrt(d/(length*.02))*base;
|
||||||
|
for(int i=0;i<(kind==0?64:16);i++) {
|
||||||
|
double next=sqrt(d/(length*pipe_friction_prepared(4*fabs(q)/den,rr,rough_limit)))*base;
|
||||||
|
if(fabs(next-q)<=fmax(1e-12,fabs(q)*1e-9)) return sign*next;
|
||||||
|
q=.5*(q+next);
|
||||||
|
}
|
||||||
|
return sign*q;'''
|
||||||
|
NEW_CALL = '''double base=area*p*cm/sqrt(T),K=pow(4*base/den,2)*d/length;
|
||||||
|
return sign*native_pipe_resistance(K,rr,den/4,NULL)*den/4;'''
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser=argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument('input',type=Path)
|
||||||
|
parser.add_argument('--output-dir',type=Path,required=True)
|
||||||
|
parser.add_argument('--baseline-ref',default='5d5a2e1')
|
||||||
|
parser.add_argument('--runs',type=int,default=3)
|
||||||
|
args=parser.parse_args()
|
||||||
|
if args.runs<1:parser.error('--runs must be positive')
|
||||||
|
out=args.output_dir.resolve();out.mkdir(parents=True,exist_ok=True)
|
||||||
|
if (out/'summary.json').exists():parser.error('Choose a fresh output directory')
|
||||||
|
revision=subprocess.check_output(['git','rev-parse',args.baseline_ref],cwd=ROOT,text=True).strip()
|
||||||
|
xml,doc=load_input(args.input)
|
||||||
|
program=compile_native_program(compile_system_xml_network(doc))
|
||||||
|
config=simulation_config(doc.simulation)
|
||||||
|
(out/'input.xml').write_bytes(xml)
|
||||||
|
(out/('input'+args.input.suffix)).write_bytes(args.input.read_bytes())
|
||||||
|
paths=subprocess.check_output(['git','ls-tree','-r','--name-only',revision,'native'],cwd=ROOT,text=True).splitlines()
|
||||||
|
variants={}
|
||||||
|
original_native=builder.NATIVE
|
||||||
|
try:
|
||||||
|
for variant in ('fixed-point','previous-newton','guarded-newton'):
|
||||||
|
directory=out/variant
|
||||||
|
for name in paths:
|
||||||
|
if variant=='guarded-newton':data=(ROOT/name).read_bytes()
|
||||||
|
else:data=subprocess.check_output(['git','show',f'{revision}:{name}'],cwd=ROOT)
|
||||||
|
target=directory/name;target.parent.mkdir(parents=True,exist_ok=True);target.write_bytes(data)
|
||||||
|
if variant=='fixed-point':
|
||||||
|
target=directory/'native/components/kernels.c';source=target.read_text()
|
||||||
|
if source.count(NEW_CALL)!=1:raise ValueError('Baseline pipe flow layout does not match the audited fixed-point substitution')
|
||||||
|
target.write_text(source.replace(NEW_CALL,OLD_ITERATION))
|
||||||
|
builder.NATIVE=directory/'native'
|
||||||
|
variants[variant]=builder.build_native(program,cache_dir=out/'cache')
|
||||||
|
finally:builder.NATIVE=original_native
|
||||||
|
rows=[]
|
||||||
|
for index in range(args.runs+1):
|
||||||
|
order=list(variants)
|
||||||
|
if index%2:order.reverse()
|
||||||
|
for variant in order:
|
||||||
|
data=execute_native(variants[variant],config,doc.simulation.sample_step,
|
||||||
|
run_dir=out/variant/('warmup' if index==0 else f'run-{index}'),timeout=120)
|
||||||
|
row={key:value for key,value in data.items() if key not in ('series','final','finalState')}
|
||||||
|
row.update(variant=variant,run=index)
|
||||||
|
rows.append(row);print(json.dumps(row,ensure_ascii=False),flush=True)
|
||||||
|
if not data['success']:raise RuntimeError(f'{variant} did not complete: {data["message"]}')
|
||||||
|
summary={'baselineRef':revision,'input':str(args.input.resolve()),
|
||||||
|
'inputSha256':sha256(args.input.read_bytes()).hexdigest(),'xmlSha256':sha256(xml).hexdigest(),
|
||||||
|
'settings':vars(config),'sampleStep':doc.simulation.sample_step,'rows':rows,
|
||||||
|
'variants':{name:{'sourceSha256':sha256((out/name/'native/components/kernels.c').read_bytes()).hexdigest(),
|
||||||
|
'buildKey':build.manifest['buildKey'],
|
||||||
|
'medianSolveSeconds':statistics.median(row['solveSeconds'] for row in rows if row['variant']==name and row['run']>0),
|
||||||
|
'medianProcessSeconds':statistics.median(row['processWallSeconds'] for row in rows if row['variant']==name and row['run']>0)}
|
||||||
|
for name,build in variants.items()}}
|
||||||
|
(out/'summary.json').write_text(json.dumps(summary,ensure_ascii=False,indent=2)+'\n')
|
||||||
|
print(json.dumps(summary['variants'],ensure_ascii=False,indent=2))
|
||||||
|
|
||||||
|
if __name__=='__main__':main()
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// Real browser/HTTP/native execution. No API routes or result data are mocked.
|
||||||
|
import { chromium } from '../../frontend/node_modules/playwright/index.mjs';
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { performance } from 'node:perf_hooks';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
|
const [input, output, baseURL = 'http://127.0.0.1:8011', runCount = '3'] = process.argv.slice(2);
|
||||||
|
if (!input || !output) throw new Error('Usage: node browser_native_simulation.mjs MODEL.json OUTPUT_DIR [URL] [RUNS]');
|
||||||
|
await fs.mkdir(output, { recursive: true });
|
||||||
|
const inputText = await fs.readFile(input, 'utf8');
|
||||||
|
const project = JSON.parse(inputText);
|
||||||
|
const curveNodeId = project.nodes.find(node => node.id === 'amesim_pnl0002_10')?.id
|
||||||
|
?? project.nodes.find(node => node.data.modelType === 'amesim_pnl0002')?.id;
|
||||||
|
if (!curveNodeId) throw new Error('Curve verification requires a PNL0002 component.');
|
||||||
|
await fs.writeFile(path.join(output, 'input.json'), inputText);
|
||||||
|
const browser = await chromium.launch({ headless: true });
|
||||||
|
const context = await browser.newContext({ viewport: { width: 1600, height: 1000 }, acceptDownloads: true });
|
||||||
|
const page = await context.newPage();
|
||||||
|
page.setDefaultTimeout(30000);
|
||||||
|
const errors = [];
|
||||||
|
page.on('pageerror', error => errors.push(String(error)));
|
||||||
|
const rows = [];
|
||||||
|
try {
|
||||||
|
await page.goto(baseURL);
|
||||||
|
await page.locator('input[type="file"][accept*=".json"]').setInputFiles(path.resolve(input));
|
||||||
|
await page.getByRole('textbox', { name: '工程', exact: true }).waitFor();
|
||||||
|
await page.waitForFunction(name => document.querySelector('input[aria-label="工程"]')?.value === name || [...document.querySelectorAll('input')].some(input => input.value === name), path.basename(input, path.extname(input)));
|
||||||
|
await page.screenshot({ path: path.join(output, 'model.png'), fullPage: true });
|
||||||
|
for (let i = 0; i <= Number(runCount); i++) {
|
||||||
|
const oldMarker = await page.evaluate(() => sessionStorage.getItem('system-simulation-flow:latest-result'));
|
||||||
|
const responsePromise = page.waitForResponse(response => response.url().endsWith('/api/system-xml/simulate-stream'), { timeout: 180000 });
|
||||||
|
const started = performance.now();
|
||||||
|
await page.getByRole('button', { name: '运行仿真', exact: true }).click();
|
||||||
|
const response = await responsePromise;
|
||||||
|
await response.finished();
|
||||||
|
if (!response.ok()) throw new Error(`Simulation HTTP ${response.status()}`);
|
||||||
|
const receivedMs = performance.now() - started;
|
||||||
|
await page.waitForFunction(old => {
|
||||||
|
const marker = sessionStorage.getItem('system-simulation-flow:latest-result');
|
||||||
|
return marker && marker !== old && JSON.parse(marker).storage === 'indexeddb';
|
||||||
|
}, oldMarker, { timeout: 180000 });
|
||||||
|
await page.getByRole('button', { name: '运行仿真', exact: true }).waitFor({ state: 'visible' });
|
||||||
|
await page.waitForFunction(() => !document.querySelector('button[aria-label="运行仿真"]')?.disabled);
|
||||||
|
await page.getByRole('tab', { name: /^结果/ }).click();
|
||||||
|
await page.getByRole('button', { name: '下载结果文件', exact: true }).waitFor();
|
||||||
|
const readyMs = performance.now() - started;
|
||||||
|
await page.waitForFunction(old => {
|
||||||
|
const marker = sessionStorage.getItem('system-simulation-flow:latest-result');
|
||||||
|
return marker && marker !== old && JSON.parse(marker).storage === 'indexeddb';
|
||||||
|
}, oldMarker);
|
||||||
|
const downloadPromise = page.waitForEvent('download');
|
||||||
|
await page.getByRole('button', { name: '下载结果文件', exact: true }).click();
|
||||||
|
await (await downloadPromise).saveAs(path.join(output, `run-${i}.simresult`));
|
||||||
|
const exported = JSON.parse(await fs.readFile(path.join(output, `run-${i}.simresult`), 'utf8'));
|
||||||
|
const result = exported.snapshot.result;
|
||||||
|
if (!result.success) throw new Error(result.message);
|
||||||
|
assert.equal(result.diagnostics.integration.rtol, 1e-8);
|
||||||
|
assert.equal(result.simulatedUntil, project.simulation.t_stop);
|
||||||
|
const row = { run: i, receivedMs, readyMs, sampleCount: result.series.time.length,
|
||||||
|
...result.diagnostics.native };
|
||||||
|
rows.push(row);
|
||||||
|
console.log(JSON.stringify(row));
|
||||||
|
if (i < Number(runCount)) await page.getByRole('tab', { name: '建模', exact: true }).click();
|
||||||
|
}
|
||||||
|
await page.getByRole('button', { name: '适应系统图窗口', exact: true }).click();
|
||||||
|
await page.locator(`.results-system-panel .react-flow__node[data-id=${JSON.stringify(curveNodeId)}]`).click();
|
||||||
|
await page.locator('.results-variable-list button').filter({ has: page.locator('small', { hasText: /^K$/ }) }).first().click();
|
||||||
|
await page.locator('.results-chart-panel .result-chart-window svg').first().waitFor();
|
||||||
|
const csvDownload = page.waitForEvent('download');
|
||||||
|
await page.getByRole('button', { name: '下载结果 CSV', exact: true }).click();
|
||||||
|
await (await csvDownload).saveAs(path.join(output, 'result.csv'));
|
||||||
|
await page.screenshot({ path: path.join(output, 'result.png'), fullPage: true });
|
||||||
|
await page.reload();
|
||||||
|
await page.getByRole('tab', { name: /^结果/ }).click();
|
||||||
|
await page.getByRole('button', { name: '下载结果文件', exact: true }).waitFor();
|
||||||
|
const restored = page.waitForEvent('download');
|
||||||
|
await page.getByRole('button', { name: '下载结果文件', exact: true }).click();
|
||||||
|
await (await restored).saveAs(path.join(output, 'restored.simresult'));
|
||||||
|
const restoredData = JSON.parse(await fs.readFile(path.join(output, 'restored.simresult'), 'utf8'));
|
||||||
|
const lastData = JSON.parse(await fs.readFile(path.join(output, `run-${runCount}.simresult`), 'utf8'));
|
||||||
|
assert.deepEqual(restoredData.snapshot.result, lastData.snapshot.result);
|
||||||
|
await fs.writeFile(path.join(output, 'summary.json'), JSON.stringify({ input: path.resolve(input), inputSha256: createHash('sha256').update(inputText).digest('hex'), baseURL, browser: browser.version(), rows, errors, restored: true, restoredResultIdentical: true, csvExported: true }, null, 2));
|
||||||
|
if (errors.length) throw new Error(`Browser errors: ${errors.join('\n')}`);
|
||||||
|
} catch (error) {
|
||||||
|
await page.screenshot({ path: path.join(output, 'failure.png'), fullPage: true });
|
||||||
|
await fs.writeFile(path.join(output, 'failure.txt'), `${error.stack}\nPage errors: ${JSON.stringify(errors)}\n${await page.locator('body').innerText()}`);
|
||||||
|
throw error;
|
||||||
|
} finally { await context.close(); await browser.close(); }
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
"""Compare real native/browser results to the frozen 72-curve Amesim reference.
|
||||||
|
|
||||||
|
Run with Python + NumPy. --plot additionally requires Matplotlib, available in
|
||||||
|
this workstation's system Python. Outputs stay in the requested artifact folder.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
import argparse
|
||||||
|
import csv
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
LIMITS = {'pressure': 250., 'temperature': .015, 'displacement': 2e-6,
|
||||||
|
'velocity': 1e-5, 'mass_flow': 3e-5}
|
||||||
|
UNITS = {'pressure': 'Pa', 'temperature': 'K', 'displacement': 'm',
|
||||||
|
'velocity': 'm/s', 'mass_flow': 'kg/s'}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument('result', type=Path)
|
||||||
|
parser.add_argument('--reference', type=Path, default=ROOT/'tests/baselines/simulation/test_mql_4/test-mql-4-amesim-reference.json')
|
||||||
|
parser.add_argument('--output-dir', type=Path, required=True)
|
||||||
|
parser.add_argument('--plot', action='store_true')
|
||||||
|
args = parser.parse_args()
|
||||||
|
ref = json.loads(args.reference.read_text())
|
||||||
|
data = json.loads(args.result.read_text())
|
||||||
|
if 'snapshot' in data:
|
||||||
|
data = data['snapshot']['result']
|
||||||
|
series = data['series']; times = np.array(series['time']); ref_times = np.array(ref['times'])
|
||||||
|
if not data['success'] or times[-1] < ref_times[-1] or times[0] > ref_times[0]:
|
||||||
|
raise ValueError('A completed result covering all reference times is required.')
|
||||||
|
if np.any(np.diff(times) < 0):
|
||||||
|
raise ValueError('Native sample times are not ordered.')
|
||||||
|
finite = all(np.all(np.isfinite(values)) for values in series.values())
|
||||||
|
rows = []; grouped = {}
|
||||||
|
for key, curve in ref['series'].items():
|
||||||
|
actual = np.interp(ref_times, times, series[key]); error = actual - curve['values']
|
||||||
|
worst = int(np.argmax(abs(error))); quantity = curve['quantity']
|
||||||
|
row = {'key': key, 'quantity': quantity, 'unit': UNITS[quantity],
|
||||||
|
'maxAbsoluteError': float(abs(error[worst])), 'rmsError': float(np.sqrt(np.mean(error**2))),
|
||||||
|
'worstTime': float(ref_times[worst]), 'limit': LIMITS[quantity],
|
||||||
|
'passed': bool(np.max(abs(error)) <= LIMITS[quantity])}
|
||||||
|
rows.append(row)
|
||||||
|
if quantity not in grouped or row['maxAbsoluteError'] > grouped[quantity]['maxAbsoluteError']:
|
||||||
|
grouped[quantity] = row
|
||||||
|
masses = np.array([v for k, v in series.items() if k.rsplit('.', 1)[-1] in ('m', 'm1', 'm2')])
|
||||||
|
total = np.sum(masses, axis=0)
|
||||||
|
mass_drift = float(np.max(abs(total-total[0])))
|
||||||
|
force_peaks = sorted(({'key': k, 'peakAbsolute': max(map(abs, v))} for k, v in series.items()
|
||||||
|
if k.startswith('amesim_lstp00a_') and k.endswith('.f')),
|
||||||
|
key=lambda row: row['peakAbsolute'], reverse=True)
|
||||||
|
summary = {'resultPath': str(args.result.resolve()),
|
||||||
|
'resultSha256': hashlib.sha256(args.result.read_bytes()).hexdigest(),
|
||||||
|
'referencePath': str(args.reference.resolve()),
|
||||||
|
'referenceSha256': hashlib.sha256(args.reference.read_bytes()).hexdigest(),
|
||||||
|
'referenceSource': ref['source'], 'freshAmesimRun': False,
|
||||||
|
'referenceCurveCount': len(rows), 'referenceTimeCount': len(ref_times),
|
||||||
|
'nativeSampleCount': len(times), 'allSamplesFinite': bool(finite),
|
||||||
|
'massStateCount': len(masses), 'maxTotalMassDriftKg': mass_drift,
|
||||||
|
'groupedWorstErrors': grouped, 'curves': rows, 'contactForcePeaksOutsideReferenceScope': force_peaks,
|
||||||
|
'passed': bool(finite and mass_drift <= 1e-10 and all(row['passed'] for row in rows))}
|
||||||
|
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
(args.output_dir/'comparison.json').write_text(json.dumps(summary, ensure_ascii=False, indent=2)+'\n')
|
||||||
|
with (args.output_dir/'curve-errors.csv').open('w', newline='') as output:
|
||||||
|
writer = csv.DictWriter(output, fieldnames=list(rows[0])); writer.writeheader(); writer.writerows(rows)
|
||||||
|
if args.plot:
|
||||||
|
import matplotlib
|
||||||
|
matplotlib.use('Agg')
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
fig, axes = plt.subplots(5, 2, figsize=(13, 15), constrained_layout=True)
|
||||||
|
for (quantity, row), (left, right) in zip(grouped.items(), axes):
|
||||||
|
key = row['key']; values = ref['series'][key]['values']
|
||||||
|
actual = np.interp(ref_times, times, series[key])
|
||||||
|
left.plot(times, series[key], linewidth=1, label='Native, rtol=1e-8')
|
||||||
|
left.plot(ref_times, values, '.', markersize=3, label='Saved Amesim reference')
|
||||||
|
left.set_title(key, fontsize=9); left.set_ylabel(UNITS[quantity]); left.legend(fontsize=7)
|
||||||
|
right.plot(ref_times, actual-values, '.-', linewidth=.8, markersize=3)
|
||||||
|
right.axhline(row['limit'], color='gray', linestyle='--', linewidth=.6)
|
||||||
|
right.axhline(-row['limit'], color='gray', linestyle='--', linewidth=.6)
|
||||||
|
right.set_title(f'{quantity}: sampled difference and acceptance limits', fontsize=9)
|
||||||
|
right.set_ylabel(UNITS[quantity])
|
||||||
|
for ax in (left, right): ax.grid(alpha=.2); ax.set_xlabel('Time [s]')
|
||||||
|
fig.suptitle('Four-branch model: 72 curves checked at 57 stored Amesim times\nWorst curve per quantity; contact force is outside this reference', fontsize=12)
|
||||||
|
fig.savefig(args.output_dir/'comparison.svg'); fig.savefig(args.output_dir/'comparison.png', dpi=140)
|
||||||
|
print(json.dumps({k: summary[k] for k in ('passed', 'nativeSampleCount', 'maxTotalMassDriftKg', 'groupedWorstErrors')}, indent=2))
|
||||||
|
if not summary['passed']: raise SystemExit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__': main()
|
||||||
@@ -0,0 +1,403 @@
|
|||||||
|
"""Audit the AME diagram against the corrected XML; emit an aligned browser project.
|
||||||
|
|
||||||
|
Run from the repository root with .venv/bin/python. Does not run Amesim or use
|
||||||
|
the stale eight-branch compiled/result cache inside the four-branch archive.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
from copy import deepcopy
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
if '--check' in sys.argv:
|
||||||
|
sys.dont_write_bytecode = True
|
||||||
|
import struct
|
||||||
|
import tarfile
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
from app.simulation.registry import get_component_model_spec
|
||||||
|
from tests.test_test_mql_ame_contract import (
|
||||||
|
_blocks, _required_element_text, _element_text, _resolve_globals,
|
||||||
|
_public_model_type, _expected_parameters, _value_fields,
|
||||||
|
)
|
||||||
|
|
||||||
|
OUT = ROOT / 'test/solver-newton-20260911/mql4'
|
||||||
|
AME = ROOT / 'tests/data/test_mql_4.ame'
|
||||||
|
XML = ROOT / 'tests/fixtures/amesim/test-mql-4-corrected.xml'
|
||||||
|
REFERENCE = ROOT / 'tests/baselines/simulation/test_mql_4/test-mql-4-amesim-reference.json'
|
||||||
|
PROJECT = ROOT / 'tests/data/test-mql-4-corrected.json'
|
||||||
|
|
||||||
|
|
||||||
|
def sha(path):
|
||||||
|
return hashlib.sha256(path.read_bytes()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def public_port(model, index):
|
||||||
|
if index == 0 and model in {'amesim_pnvo001', 'amesim_forc'}:
|
||||||
|
return 'res'
|
||||||
|
if index == 0 and model in {'amesim_step0', 'amesim_ud00'}:
|
||||||
|
return 'out'
|
||||||
|
return f'port_{index + 1}'
|
||||||
|
|
||||||
|
|
||||||
|
def parameter_key(values):
|
||||||
|
return tuple(sorted((key, format(float(value), '.12g')) for key, value in values.items()))
|
||||||
|
|
||||||
|
|
||||||
|
def audit_source():
|
||||||
|
with tarfile.open(AME) as archive:
|
||||||
|
def read(suffix):
|
||||||
|
names = [name for name in archive.getnames() if name.endswith(suffix)]
|
||||||
|
if len(names) != 1:
|
||||||
|
raise ValueError((suffix, names))
|
||||||
|
return archive.extractfile(names[0]).read().decode('latin1')
|
||||||
|
cir = read('.cir')
|
||||||
|
sim = read('.sim')
|
||||||
|
modelinfo = read('.modelinfo')
|
||||||
|
variables = read('.var')
|
||||||
|
perf = read('.ameperf')
|
||||||
|
result_name = next(name for name in archive.getnames() if name.endswith('.results'))
|
||||||
|
result_data = archive.extractfile(result_name).read()
|
||||||
|
result_points, result_columns = struct.unpack('<2i', result_data[:8])
|
||||||
|
member_hashes = {}
|
||||||
|
for suffix in ['.cir', '.sim', '.modelinfo', '.var', '.results']:
|
||||||
|
name = next(name for name in archive.getnames() if name.endswith(suffix))
|
||||||
|
data = archive.extractfile(name).read()
|
||||||
|
member_hashes[name] = {'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()}
|
||||||
|
globals_ = _resolve_globals(cir)
|
||||||
|
components = _blocks(cir, 'COMP')
|
||||||
|
lines = _blocks(cir, 'LINE')
|
||||||
|
nodes = {}
|
||||||
|
for kind, entities in [('component', components), ('line', lines)]:
|
||||||
|
for index, body in enumerate(entities):
|
||||||
|
submodel = _required_element_text(body, 'SUB_NAME')
|
||||||
|
if submodel == 'DIRECT':
|
||||||
|
continue
|
||||||
|
model = _public_model_type(submodel)
|
||||||
|
nodes[f'{kind}:{index}'] = {
|
||||||
|
'modelType': model,
|
||||||
|
'alias': _required_element_text(body, 'ALIAS'),
|
||||||
|
'parameters': _expected_parameters(body, model, globals_),
|
||||||
|
'sourceValues': {name: {'expression': value.expression, 'unit': value.unit}
|
||||||
|
for name, value in _value_fields(body).items()},
|
||||||
|
'position': _element_text(body, 'COMP_POS') or _element_text(body, 'LINE_POINTS'),
|
||||||
|
'geometry': _element_text(body, 'COMP_GEOMETRY'),
|
||||||
|
'outputType': _element_text(body, 'OUTPUT_TYPE'),
|
||||||
|
}
|
||||||
|
edges = set()
|
||||||
|
contacts = set()
|
||||||
|
for index, body in enumerate(components):
|
||||||
|
for port_index, port_body in enumerate(_blocks(body, 'COMP_PORT')):
|
||||||
|
if _required_element_text(port_body, 'PORT_CONNECT') != '1':
|
||||||
|
continue
|
||||||
|
for connection in _blocks(port_body, 'CONNECT'):
|
||||||
|
other = int(_required_element_text(connection, 'CONNECT_ENTITY_NUM'))
|
||||||
|
other_port = int(_required_element_text(connection, 'CONNECT_ENTITY_PORT'))
|
||||||
|
contacts.add(((index, port_index), (other, other_port)))
|
||||||
|
assert all((b, a) in contacts for a, b in contacts)
|
||||||
|
def endpoint(index, port):
|
||||||
|
key = f'component:{index}'
|
||||||
|
return key, public_port(nodes[key]['modelType'], port)
|
||||||
|
for a, b in contacts:
|
||||||
|
edges.add(tuple(sorted((endpoint(*a), endpoint(*b)))))
|
||||||
|
for index, body in enumerate(lines):
|
||||||
|
assert _required_element_text(body, 'LINE_START_TYPE') == '0'
|
||||||
|
assert _required_element_text(body, 'LINE_END_TYPE') == '0'
|
||||||
|
start = endpoint(int(_required_element_text(body, 'LINE_START_ENTITY')),
|
||||||
|
int(_required_element_text(body, 'LINE_START_PORT')))
|
||||||
|
end = endpoint(int(_required_element_text(body, 'LINE_END_ENTITY')),
|
||||||
|
int(_required_element_text(body, 'LINE_END_PORT')))
|
||||||
|
if _required_element_text(body, 'SUB_NAME') == 'DIRECT':
|
||||||
|
edges.add(tuple(sorted((start, end))))
|
||||||
|
else:
|
||||||
|
key = f'line:{index}'
|
||||||
|
output_type = _required_element_text(body, 'OUTPUT_TYPE')
|
||||||
|
assert output_type in {'1', '2'}
|
||||||
|
first, second = ('port_1', 'port_2') if output_type == '1' else ('port_2', 'port_1')
|
||||||
|
edges.add(tuple(sorted((start, (key, first)))))
|
||||||
|
edges.add(tuple(sorted((end, (key, second)))))
|
||||||
|
count = Counter(node['modelType'] for node in nodes.values())
|
||||||
|
states = sum(count[model] * size for model, size in {
|
||||||
|
'amesim_pnch012': 2, 'amesim_pnch023': 2, 'amesim_pnl0001': 2,
|
||||||
|
'amesim_pnl0002': 2, 'amesim_pnl0003': 4, 'amesim_mecmas21': 2,
|
||||||
|
}.items())
|
||||||
|
return nodes, sorted(edges), {
|
||||||
|
'archiveSha256': sha(AME), 'archiveBytes': AME.stat().st_size,
|
||||||
|
'members': member_hashes, 'globals': globals_,
|
||||||
|
'componentCount': len(components), 'modeledLineCount': len(nodes) - len(components),
|
||||||
|
'directLineCount': len(lines) - (len(nodes) - len(components)),
|
||||||
|
'contactCount': len(contacts) // 2, 'publicNodeCount': len(nodes),
|
||||||
|
'publicParameterCount': sum(len(node['parameters']) for node in nodes.values()),
|
||||||
|
'connectionCount': len(edges), 'typeCounts': dict(count),
|
||||||
|
'diagramDynamicStateCount': states,
|
||||||
|
'simulationFileRaw': sim, 'savedSimulationNumericRows': [[float(x) for x in row.split()] for row in sim.splitlines()],
|
||||||
|
'cachedModelInfo': modelinfo,
|
||||||
|
'cachedResultsSuitableForFourBranchComparison': False,
|
||||||
|
'cacheRejectionReason': 'Diagram has 64 states / four pistons; cached modelinfo has 132 states, .ameperf contains eight LSTP00A instances, and .results declares 1116 saved variables while the current .var has only 640 rows.',
|
||||||
|
'cachedResultHeader': {'pointCount': result_points, 'savedVariableCount': abs(result_columns)},
|
||||||
|
'variableRowCount': len(variables.splitlines()),
|
||||||
|
'cachedPerformanceContactInstances': sorted(set(re.findall(r'LSTP00A-([0-9]+)', perf))),
|
||||||
|
'cachedPistonAliases': sorted(set(re.findall(r'Data_Path=\S+@(pn_brp2[^\s]*)', variables))),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def xml_model(root=None):
|
||||||
|
if root is None:
|
||||||
|
root = ET.parse(XML).getroot()
|
||||||
|
nodes = {item.attrib['id']: {
|
||||||
|
'modelType': item.attrib['type'],
|
||||||
|
'modelVersion': item.attrib['modelVersion'],
|
||||||
|
'parameters': {p.attrib['name']: float(p.attrib['value']) for p in item.findall('Parameter')},
|
||||||
|
} for item in root.findall('./Components/Component')}
|
||||||
|
edges = [tuple(sorted((ep.attrib['component'], ep.attrib['port']) for ep in item.findall('Endpoint')))
|
||||||
|
for item in root.findall('./Connections/Connection')]
|
||||||
|
return nodes, edges, root
|
||||||
|
|
||||||
|
|
||||||
|
def find_mapping(first, first_edges, second, second_edges, *, ports=False, parameters=True):
|
||||||
|
"""Find an actual graph bijection, preserving node types and all parameters."""
|
||||||
|
def edge_label(node, port):
|
||||||
|
if not ports:
|
||||||
|
return ''
|
||||||
|
return port
|
||||||
|
def graph(nodes, edges):
|
||||||
|
adjacency = {key: defaultdict(Counter) for key in nodes}
|
||||||
|
for (a, ap), (b, bp) in edges:
|
||||||
|
adjacency[a][b][(edge_label(a, ap), edge_label(b, bp))] += 1
|
||||||
|
adjacency[b][a][(edge_label(b, bp), edge_label(a, ap))] += 1
|
||||||
|
return adjacency
|
||||||
|
a, b = graph(first, first_edges), graph(second, second_edges)
|
||||||
|
def signature(nodes, adj, key):
|
||||||
|
return (nodes[key]['modelType'], parameter_key(nodes[key]['parameters']) if parameters else (),
|
||||||
|
sum(sum(labels.values()) for labels in adj[key].values()))
|
||||||
|
candidates = {key: [other for other in second if signature(first, a, key) == signature(second, b, other)]
|
||||||
|
for key in first}
|
||||||
|
if any(not options for options in candidates.values()):
|
||||||
|
return None
|
||||||
|
mapping = {}
|
||||||
|
used = set()
|
||||||
|
def search():
|
||||||
|
if len(mapping) == len(first):
|
||||||
|
return dict(mapping)
|
||||||
|
key = min((key for key in first if key not in mapping),
|
||||||
|
key=lambda key: (-sum(neighbor in mapping for neighbor in a[key]),
|
||||||
|
len([other for other in candidates[key] if other not in used]), -len(a[key])))
|
||||||
|
for other in candidates[key]:
|
||||||
|
if other in used:
|
||||||
|
continue
|
||||||
|
if any(a[key].get(existing, Counter()) != b[other].get(target, Counter())
|
||||||
|
for existing, target in mapping.items()):
|
||||||
|
continue
|
||||||
|
mapping[key] = other
|
||||||
|
used.add(other)
|
||||||
|
result = search()
|
||||||
|
if result is not None:
|
||||||
|
return result
|
||||||
|
used.remove(other)
|
||||||
|
del mapping[key]
|
||||||
|
return None
|
||||||
|
return search()
|
||||||
|
|
||||||
|
|
||||||
|
def check_project_contract(project, xml_root):
|
||||||
|
"""Check generated port metadata and exact JSON/XML execution data, without writes.
|
||||||
|
|
||||||
|
This covers the observed nullable optional-field failure and shared physical
|
||||||
|
data, not a replacement for the complete frontend project parser.
|
||||||
|
"""
|
||||||
|
xml_nodes, xml_edges, _ = xml_model(xml_root)
|
||||||
|
json_nodes = {node['id']: node['data'] for node in project['nodes']}
|
||||||
|
if len(json_nodes) != len(project['nodes']) or json_nodes.keys() != xml_nodes.keys():
|
||||||
|
raise ValueError('JSON/XML component IDs differ or JSON contains duplicate IDs.')
|
||||||
|
for node_id, data in json_nodes.items():
|
||||||
|
expected = xml_nodes[node_id]
|
||||||
|
if (data['componentType'] != expected['modelType'] or
|
||||||
|
data['modelType'] != expected['modelType'] or
|
||||||
|
data['modelVersion'] != expected['modelVersion']):
|
||||||
|
raise ValueError(f'{node_id}: JSON/XML model type or version differs.')
|
||||||
|
if data['parameters'] != expected['parameters']:
|
||||||
|
raise ValueError(f'{node_id}: JSON/XML parameters differ.')
|
||||||
|
for port in data['ports']:
|
||||||
|
# frontend/src/App.tsx:isProjectPortDefinitionValue accepts omission,
|
||||||
|
# or the literal intoComponent. An explicit null rejects the import.
|
||||||
|
if ('positiveFlowDirection' in port and
|
||||||
|
port['positiveFlowDirection'] != 'intoComponent'):
|
||||||
|
raise ValueError(f"{node_id}.{port['name']}: omit undefined positiveFlowDirection instead of null.")
|
||||||
|
json_edges = []
|
||||||
|
for edge in project['edges']:
|
||||||
|
endpoints = []
|
||||||
|
for side in ['source', 'target']:
|
||||||
|
component, port = edge[side], edge[side + 'Handle']
|
||||||
|
if component not in json_nodes:
|
||||||
|
raise ValueError(f'Unknown connection component {component}.')
|
||||||
|
data = json_nodes[component]
|
||||||
|
active_ports = get_component_model_spec(data['modelType']).active_ports(data['parameters'])
|
||||||
|
if port not in {item.name for item in active_ports}:
|
||||||
|
raise ValueError(f'{component}.{port}: connection port is inactive or unknown.')
|
||||||
|
endpoints.append((component, port))
|
||||||
|
json_edges.append(tuple(sorted(endpoints)))
|
||||||
|
if Counter(json_edges) != Counter(xml_edges):
|
||||||
|
raise ValueError('JSON/XML connection endpoints differ.')
|
||||||
|
simulation = xml_root.find('Simulation').attrib
|
||||||
|
for key, xml_key in [('t_start', 'tStart'), ('t_stop', 'tStop'),
|
||||||
|
('step', 'sampleStep'), ('max_step', 'maxStep')]:
|
||||||
|
if project['simulation'][key] != float(simulation[xml_key]):
|
||||||
|
raise ValueError(f'JSON/XML simulation {key} differs.')
|
||||||
|
if project['simulation']['method'] != simulation['method']:
|
||||||
|
raise ValueError('JSON/XML integration methods differ.')
|
||||||
|
return {'valid': True, 'componentCount': len(json_nodes), 'connectionCount': len(json_edges),
|
||||||
|
'parameterCount': sum(len(data['parameters']) for data in json_nodes.values()),
|
||||||
|
'optionalPortFieldsValid': True, 'jsonXmlExecutionDataEqual': True}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_project(source, source_edges, target, target_edges, xml_root, mapping):
|
||||||
|
layout_source = ROOT / 'tests/fixtures/legacy/test-mql-8.json'
|
||||||
|
layout = json.loads(layout_source.read_text())
|
||||||
|
layout_nodes = {node['id']: node for node in layout['nodes']}
|
||||||
|
reverse_mapping = {value: key for key, value in mapping.items()}
|
||||||
|
nodes = []
|
||||||
|
for node_id, model in target.items():
|
||||||
|
source_node = source[reverse_mapping[node_id]]
|
||||||
|
template = layout_nodes.get(node_id)
|
||||||
|
if template is None:
|
||||||
|
template = next(node for node in layout['nodes'] if node['data']['modelType'] == model['modelType'])
|
||||||
|
node = deepcopy(template)
|
||||||
|
node['id'] = node_id
|
||||||
|
node['selected'] = False
|
||||||
|
node['data']['label'] = source_node['alias']
|
||||||
|
node['data']['parameters'] = source_node['parameters']
|
||||||
|
node['data']['modelVersion'] = model['modelVersion']
|
||||||
|
node['data']['ports'] = get_component_model_spec(model['modelType']).as_catalog_dict()['ports']
|
||||||
|
for port in node['data']['ports']:
|
||||||
|
if port.get('positiveFlowDirection') is None:
|
||||||
|
port.pop('positiveFlowDirection', None)
|
||||||
|
if model['modelType'] == 'amesim_p4node2':
|
||||||
|
node['data']['rotation'] = 180
|
||||||
|
node['data']['mirrored'] = False
|
||||||
|
if node_id not in layout_nodes:
|
||||||
|
# The extra closing pipe sits below the four retained branches.
|
||||||
|
node['position'] = {'x': 80.0, 'y': 900.0}
|
||||||
|
node['data']['rotation'] = 0
|
||||||
|
nodes.append(node)
|
||||||
|
original_by_pair = {}
|
||||||
|
for edge in layout['edges']:
|
||||||
|
pair = tuple(sorted((edge['source'], edge['target'])))
|
||||||
|
original_by_pair[pair] = edge
|
||||||
|
xml_by_pair = {}
|
||||||
|
for connection in xml_root.findall('./Connections/Connection'):
|
||||||
|
pair = tuple(sorted(ep.attrib['component'] for ep in connection.findall('Endpoint')))
|
||||||
|
xml_by_pair[pair] = connection
|
||||||
|
edges = []
|
||||||
|
changed_connections = []
|
||||||
|
for ((a, ap), (b, bp)) in source_edges:
|
||||||
|
a, b = mapping[a], mapping[b]
|
||||||
|
pair = tuple(sorted((a, b)))
|
||||||
|
existing = original_by_pair.get(pair)
|
||||||
|
edge = deepcopy(existing) if existing else {'id': 'ame-' + a + '-' + ap + '-' + b + '-' + bp}
|
||||||
|
if existing and existing['source'] == b:
|
||||||
|
a, ap, b, bp = b, bp, a, ap
|
||||||
|
old_connection = xml_by_pair[pair]
|
||||||
|
old_endpoints = {ep.attrib['component']: ep.attrib['port'] for ep in old_connection.findall('Endpoint')}
|
||||||
|
changed = old_endpoints != {a: ap, b: bp}
|
||||||
|
if changed:
|
||||||
|
changed_connections.append({'components': pair, 'before': old_endpoints, 'after': {a: ap, b: bp}})
|
||||||
|
edge.update({'source': a, 'target': b, 'sourceHandle': ap, 'targetHandle': bp})
|
||||||
|
edge.pop('selected', None)
|
||||||
|
if any(target[node_id]['modelType'] == 'amesim_p4node2' for node_id in [a, b]):
|
||||||
|
edge.setdefault('data', {}).pop('routePoints', None)
|
||||||
|
if changed or existing is None:
|
||||||
|
edge['data'] = {'isContactEdge': False}
|
||||||
|
edges.append(edge)
|
||||||
|
for endpoint in old_connection.findall('Endpoint'):
|
||||||
|
endpoint.attrib['port'] = {a: ap, b: bp}[endpoint.attrib['component']]
|
||||||
|
parameter_changes = []
|
||||||
|
for component in xml_root.findall('./Components/Component'):
|
||||||
|
node_id = component.attrib['id']
|
||||||
|
parameters = source[reverse_mapping[node_id]]['parameters']
|
||||||
|
for parameter in component.findall('Parameter'):
|
||||||
|
key = parameter.attrib['name']
|
||||||
|
before = float(parameter.attrib['value'])
|
||||||
|
after = parameters[key]
|
||||||
|
if abs(before - after) > max(1e-18, abs(after) * 1e-12):
|
||||||
|
parameter_changes.append({'component': node_id, 'parameter': key, 'before': before, 'after': after})
|
||||||
|
parameter.attrib['value'] = repr(after)
|
||||||
|
simulation = {'t_start': 0.0, 't_stop': 10.0, 'step': 0.01, 'max_step': 1e30, 'method': 'BDF'}
|
||||||
|
project = {'projectSchemaVersion': 1, 'name': 'test-mql-4-AME-aligned', 'nodes': nodes,
|
||||||
|
'edges': edges, 'simulation': simulation}
|
||||||
|
check_project_contract(project, xml_root)
|
||||||
|
PROJECT.write_text(json.dumps(project, indent=2, ensure_ascii=False) + '\n')
|
||||||
|
generated_xml = OUT / 'aligned-input.xml'
|
||||||
|
xml_root.attrib['name'] = project['name']
|
||||||
|
ET.indent(xml_root)
|
||||||
|
ET.ElementTree(xml_root).write(generated_xml, encoding='utf-8', xml_declaration=True)
|
||||||
|
XML.write_bytes(generated_xml.read_bytes())
|
||||||
|
# Check the delivered JSON itself, including every exact endpoint and number.
|
||||||
|
json_nodes = {node['id']: node['data'] for node in json.loads(PROJECT.read_text())['nodes']}
|
||||||
|
for source_id, project_id in mapping.items():
|
||||||
|
assert json_nodes[project_id]['parameters'] == source[source_id]['parameters']
|
||||||
|
expected_edges = {tuple(sorted(((mapping[a], ap), (mapping[b], bp)))) for ((a, ap), (b, bp)) in source_edges}
|
||||||
|
actual_edges = {tuple(sorted(((edge['source'], edge['sourceHandle']), (edge['target'], edge['targetHandle'])))) for edge in edges}
|
||||||
|
assert expected_edges == actual_edges
|
||||||
|
for edge in edges:
|
||||||
|
for side in ['source', 'target']:
|
||||||
|
data = json_nodes[edge[side]]
|
||||||
|
active_ports = get_component_model_spec(data['modelType']).active_ports(data['parameters'])
|
||||||
|
assert edge[side + 'Handle'] in {port.name for port in active_ports}
|
||||||
|
return {'path': str(PROJECT.relative_to(ROOT)), 'sha256': sha(PROJECT),
|
||||||
|
'xmlPath': str(generated_xml.relative_to(ROOT)), 'xmlSha256': sha(generated_xml),
|
||||||
|
'nodeCount': len(nodes), 'edgeCount': len(edges), 'parameterCount': sum(len(n['data']['parameters']) for n in nodes),
|
||||||
|
'allMappedParametersExactlyMatchAme': True, 'allMappedConnectionPortsExactlyMatchAme': True,
|
||||||
|
'parameterChangesFromLegacyXml': parameter_changes, 'connectionPortChangesFromLegacyXml': changed_connections,
|
||||||
|
'layoutMetadataSource': {'path': str(layout_source.relative_to(ROOT)), 'sha256': sha(layout_source)},
|
||||||
|
'simulation': simulation,
|
||||||
|
'simulationSettingsNote': 'AME archive stores historical fixed-step settings. This JSON uses native BDF; rtol is supplied by the backend and is not stored in the project. Initial validation used 1e-7; the backend default was subsequently changed to 1e-8. The earlier official Standard variable-step reference remains tolerance 1e-7. Inspect actual run diagnostics; solver algorithms are not identical.'}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description=__doc__)
|
||||||
|
parser.add_argument('--check', action='store_true', help='Validate delivered JSON/XML only; do not generate or write files.')
|
||||||
|
arguments = parser.parse_args()
|
||||||
|
if arguments.check:
|
||||||
|
print(json.dumps(check_project_contract(json.loads(PROJECT.read_text()), ET.parse(XML).getroot()), indent=2))
|
||||||
|
return
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
source, source_edges, audit = audit_source()
|
||||||
|
legacy_xml = OUT / 'legacy-corrected-input.xml'
|
||||||
|
if not legacy_xml.exists():
|
||||||
|
legacy_xml.write_bytes(XML.read_bytes())
|
||||||
|
target, target_edges, xml_root = xml_model()
|
||||||
|
mapping = find_mapping(source, source_edges, target, target_edges)
|
||||||
|
audit['correctedXml'] = {'path': str(XML.relative_to(ROOT)), 'sha256': sha(XML),
|
||||||
|
'parameterAndComponentTopologyIsomorphic': mapping is not None}
|
||||||
|
ref = json.loads(REFERENCE.read_text())
|
||||||
|
audit['reference'] = {'path': str(REFERENCE.relative_to(ROOT)), 'sha256': sha(REFERENCE),
|
||||||
|
'source': ref['source'], 'timeCount': len(ref['times']), 'seriesCount': len(ref['series']),
|
||||||
|
'ameHashMatches': ref['originalAmeSha256'] == sha(AME),
|
||||||
|
'xmlHashMatches': ref['xmlSha256'] == sha(XML),
|
||||||
|
'newAmesimRunPerformed': False}
|
||||||
|
(OUT / 'source-model.json').write_text(json.dumps({'nodes': source, 'edges': source_edges}, indent=2, ensure_ascii=False) + '\n')
|
||||||
|
if not mapping:
|
||||||
|
mapping = find_mapping(source, source_edges, target, target_edges, parameters=False)
|
||||||
|
if mapping:
|
||||||
|
audit['componentMapping'] = {key: {'alias': source[key]['alias'], 'projectId': value} for key, value in mapping.items()}
|
||||||
|
transformed = {tuple(sorted(((mapping[a], ap), (mapping[b], bp)))) for ((a, ap), (b, bp)) in source_edges}
|
||||||
|
target_set = set(target_edges)
|
||||||
|
audit['correctedXml']['exactPortDifferencesForTopologyMapping'] = {
|
||||||
|
'ameOnly': sorted(transformed - target_set), 'xmlOnly': sorted(target_set - transformed)}
|
||||||
|
if mapping:
|
||||||
|
audit['generatedProject'] = generate_project(source, source_edges, target, target_edges, xml_root, mapping)
|
||||||
|
audit['correctedXml']['scope'] = 'Input XML snapshot before this generation; generatedProject describes the delivered corrected files.'
|
||||||
|
audit_text = json.dumps(audit, indent=2, ensure_ascii=False) + '\n'
|
||||||
|
(OUT / 'model-audit.json').write_text(audit_text)
|
||||||
|
(ROOT / 'tests/model_audit/test-mql-4-audit.json').write_text(audit_text)
|
||||||
|
print(json.dumps({key: value for key, value in audit.items() if key not in {'componentMapping', 'members', 'globals'}}, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
"""Independent read of AME XML fields; no shared expected-parameter helper."""
|
||||||
|
from pathlib import Path
|
||||||
|
import ast, collections, hashlib, json, math, operator, re, sys, tarfile
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
ROOT=Path(__file__).resolve().parents[2]
|
||||||
|
sys.path.insert(0,str(ROOT))
|
||||||
|
from app.simulation.registry import get_component_model_spec
|
||||||
|
OUT=ROOT/'test/solver-newton-20260911/mql4/visual-recheck'
|
||||||
|
OUT.mkdir(parents=True,exist_ok=True)
|
||||||
|
with tarfile.open(ROOT/'tests/data/test_mql_4.ame') as archive:
|
||||||
|
cir=archive.extractfile('test_mql_4_.cir').read().decode('latin1')
|
||||||
|
# AME stores unescaped C-style && in visibility metadata, outside parameter values.
|
||||||
|
xml_text=re.sub(r'<VISIBILITY>.*?</VISIBILITY>', '<VISIBILITY/>', cir, flags=re.S)
|
||||||
|
xml_text=re.sub(r'&(?!amp;|lt;|gt;|quot;|apos;|#)', '&', xml_text)
|
||||||
|
root=ET.fromstring(xml_text)
|
||||||
|
BINARY={ast.Add:operator.add,ast.Sub:operator.sub,ast.Mult:operator.mul,ast.Div:operator.truediv,ast.Pow:operator.pow}
|
||||||
|
UNARY={ast.UAdd:operator.pos,ast.USub:operator.neg}
|
||||||
|
def evaluate(text,names):
|
||||||
|
def visit(node):
|
||||||
|
if isinstance(node,ast.Constant) and isinstance(node.value,(int,float)):return float(node.value)
|
||||||
|
if isinstance(node,ast.Name):return names[node.id]
|
||||||
|
if isinstance(node,ast.BinOp) and type(node.op) in BINARY:return BINARY[type(node.op)](visit(node.left),visit(node.right))
|
||||||
|
if isinstance(node,ast.UnaryOp) and type(node.op) in UNARY:return UNARY[type(node.op)](visit(node.operand))
|
||||||
|
raise ValueError(ast.dump(node))
|
||||||
|
return visit(ast.parse(text.strip().replace('^','**'),mode='eval').body)
|
||||||
|
variables={};pending={g.findtext('GLOB_PARAM_NAME').strip():g.findtext('VALUE').strip() for g in root.findall('.//GLOBALPARAM')}
|
||||||
|
while pending:
|
||||||
|
count=len(pending)
|
||||||
|
for key,value in list(pending.items()):
|
||||||
|
try:variables[key]=evaluate(value,variables)
|
||||||
|
except KeyError:continue
|
||||||
|
del pending[key]
|
||||||
|
assert len(pending)<count,pending
|
||||||
|
entities={}
|
||||||
|
for kind in ['COMP','LINE']:
|
||||||
|
for index,item in enumerate(root.findall('.//'+kind)):
|
||||||
|
model=item.findtext('./SUBMODEL/SUB_NAME')
|
||||||
|
if not model or model=='DIRECT':continue
|
||||||
|
fields={}
|
||||||
|
for tag in ['RPARAM','IPARAM','IVAR','EVAR']:
|
||||||
|
for field in item.findall('.//'+tag):
|
||||||
|
name=field.findtext('VARNAME');value=field.findtext('VALUE')
|
||||||
|
if name and value:
|
||||||
|
assert name not in fields,(item.findtext('ALIAS'),name)
|
||||||
|
fields[name.strip()]={'value':value.strip(),'unit':(field.findtext('UNITS') or '').strip()}
|
||||||
|
alias=item.findtext('ALIAS').strip()
|
||||||
|
assert alias not in entities
|
||||||
|
entities[alias]={'element':item,'fields':fields,'model':model,'source':kind+':'+str(index)}
|
||||||
|
# Explicit initial-state translations; all other public names are read verbatim.
|
||||||
|
state_names={
|
||||||
|
'MECMAS21':{'v0':'v1','x0':'x1'},'PNCH012':{'p0':'press','T0':'temp'},'PNCH023':{'p0':'press','T0':'temp'},
|
||||||
|
'PNL0001':{'p0':'p2','T0':'t2'},'PNL0002':{'p0':'pctr','T0':'tctr'},
|
||||||
|
'PNL0003':{'p1_0':'p1','T1_0':'t1','p2_0':'p2','T2_0':'t2'},
|
||||||
|
'STEP0':{'initial':'out0','final':'out1','time':'t0'},
|
||||||
|
}
|
||||||
|
unit_scale={('mm','m'):1e-3,('mm**2','m2'):1e-6,('L','m3'):1e-3,('N/mm','N/m'):1e3,('N/(mm/s)','N/(m/s)'):1e3}
|
||||||
|
unit_aliases={('null',''),('degree',''),('J/m**2/K/s','W/(m2*K)'),('N/m**2','Pa'),('m**2','m2'),('N/(m/s)**2','N/(m/s)^2')}
|
||||||
|
project=json.loads((ROOT/'tests/data/test-mql-4-corrected.json').read_text())
|
||||||
|
rows=[];issues=[];unit_counts=collections.Counter();extras=[]
|
||||||
|
for node in project['nodes']:
|
||||||
|
d=node['data'];entry=entities[d['label']];model=entry['model'];fields=entry['fields']
|
||||||
|
specs=get_component_model_spec(d['modelType']).parameter_by_name
|
||||||
|
for key,actual in d['parameters'].items():
|
||||||
|
source=state_names.get(model,{}).get(key,key)
|
||||||
|
rule='raw field'
|
||||||
|
if source in fields:
|
||||||
|
f=fields[source];raw=evaluate(f['value'],variables);source_unit=f['unit'];target_unit=specs[key].unit
|
||||||
|
if source in {'press','p1','p2','pctr'}:
|
||||||
|
assert (source_unit,target_unit)==('Pa','Pa')
|
||||||
|
expected=raw+101300.;rule='gauge Pa + 101300 Pa atmospheric pressure'
|
||||||
|
elif (source_unit,target_unit) in unit_scale:
|
||||||
|
expected=raw*unit_scale[source_unit,target_unit];rule='multiply by '+str(unit_scale[source_unit,target_unit])
|
||||||
|
elif source_unit==target_unit or (source_unit,target_unit) in unit_aliases:
|
||||||
|
expected=raw
|
||||||
|
else:raise AssertionError((source,source_unit,target_unit))
|
||||||
|
unit_counts[(source_unit,target_unit,rule)]+=1
|
||||||
|
row={'jsonId':node['id'],'ameAlias':d['label'],'sourceEntity':entry['source'],'parameter':key,'sourceField':source,
|
||||||
|
'rawExpression':f['value'],'rawValue':raw,'sourceUnit':source_unit,'targetUnit':target_unit,'conversion':rule,
|
||||||
|
'expected':expected,'actual':actual}
|
||||||
|
else:
|
||||||
|
if model=='PNCH012' and key in {'vol1','vol2','vol3','vol4','dvol1','dvol2','dvol3','dvol4'}:expected=0.;rule='public extra port-volume inputs default zero'
|
||||||
|
elif model=='PNVO001' and key=='opening0':expected=1.;rule='public fallback opening parameter; model signal port supplies actual command'
|
||||||
|
elif model=='FORC' and key=='direction':
|
||||||
|
geometry=entry['element'].findtext('COMP_GEOMETRY');assert geometry in {'2','8'}
|
||||||
|
expected=1. if geometry=='2' else -1.;rule='force direction from AME geometry '+geometry
|
||||||
|
elif model=='PNGD00' and key=='property_model':
|
||||||
|
codes={name:evaluate(fields[name]['value'],variables) for name in ['fluidType','eosType','gasSetting']}
|
||||||
|
assert codes=={'fluidType':12.,'eosType':6.,'gasSetting':1.},codes
|
||||||
|
expected=0.;rule='public helium model selection mapped from explicit AME fluid/eos/gas codes'
|
||||||
|
else:raise AssertionError((model,key))
|
||||||
|
row={'jsonId':node['id'],'ameAlias':d['label'],'sourceEntity':entry['source'],'parameter':key,'conversion':rule,'expected':expected,'actual':actual}
|
||||||
|
extras.append(row)
|
||||||
|
row['matches']=math.isclose(actual,expected,rel_tol=1e-12,abs_tol=1e-15)
|
||||||
|
if not row['matches']:issues.append(row)
|
||||||
|
rows.append(row)
|
||||||
|
result={'method':'Independent xml.etree XML parsing and arithmetic AST evaluation; does not import or call _expected_parameters or its source-field helpers.',
|
||||||
|
'nodeCount':len(project['nodes']),'parameterCount':len(rows),'directSourceFieldCount':len(rows)-len(extras),'publicExtraOrGeometryMappedCount':len(extras),
|
||||||
|
'unitConversionCounts':[{'source':a,'target':b,'rule':c,'count':n} for (a,b,c),n in unit_counts.items()],
|
||||||
|
'extraParameters':extras,'differences':issues,'parameters':rows}
|
||||||
|
(OUT/'independent-parameters.json').write_text(json.dumps(result,indent=2,ensure_ascii=False)+'\n')
|
||||||
|
print(json.dumps({k:v for k,v in result.items() if k not in ['parameters','extraParameters']},indent=2))
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
{
|
||||||
|
"source": "tests/data/test_mql_4.ame:test_mql_4_.cir COMP_PORT/PORT_POS (zero-based component indices)",
|
||||||
|
"archiveSha256": "99a071896671d634d651a7b21f487662dd7c122af8fd3d60ead404a5afa53ed4",
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"componentIndex": 47,
|
||||||
|
"alias": "pnnode4_16",
|
||||||
|
"geometry": 1,
|
||||||
|
"ports": {
|
||||||
|
"port_1": {
|
||||||
|
"x": 10.0,
|
||||||
|
"y": 19.0
|
||||||
|
},
|
||||||
|
"port_2": {
|
||||||
|
"x": 19.0,
|
||||||
|
"y": 10.0
|
||||||
|
},
|
||||||
|
"port_3": {
|
||||||
|
"x": 10.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"port_4": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 10.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"componentIndex": 48,
|
||||||
|
"alias": "pnnode4_17",
|
||||||
|
"geometry": 1,
|
||||||
|
"ports": {
|
||||||
|
"port_1": {
|
||||||
|
"x": 10.0,
|
||||||
|
"y": 19.0
|
||||||
|
},
|
||||||
|
"port_2": {
|
||||||
|
"x": 19.0,
|
||||||
|
"y": 10.0
|
||||||
|
},
|
||||||
|
"port_3": {
|
||||||
|
"x": 10.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"port_4": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 10.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"componentIndex": 49,
|
||||||
|
"alias": "pnnode4_18",
|
||||||
|
"geometry": 1,
|
||||||
|
"ports": {
|
||||||
|
"port_1": {
|
||||||
|
"x": 10.0,
|
||||||
|
"y": 19.0
|
||||||
|
},
|
||||||
|
"port_2": {
|
||||||
|
"x": 19.0,
|
||||||
|
"y": 10.0
|
||||||
|
},
|
||||||
|
"port_3": {
|
||||||
|
"x": 10.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"port_4": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 10.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"componentIndex": 50,
|
||||||
|
"alias": "pnnode4_19",
|
||||||
|
"geometry": 1,
|
||||||
|
"ports": {
|
||||||
|
"port_1": {
|
||||||
|
"x": 10.0,
|
||||||
|
"y": 19.0
|
||||||
|
},
|
||||||
|
"port_2": {
|
||||||
|
"x": 19.0,
|
||||||
|
"y": 10.0
|
||||||
|
},
|
||||||
|
"port_3": {
|
||||||
|
"x": 10.0,
|
||||||
|
"y": 1.0
|
||||||
|
},
|
||||||
|
"port_4": {
|
||||||
|
"x": 1.0,
|
||||||
|
"y": 10.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -9,7 +9,7 @@ from app.simulation.systems.network import SimulationNetwork
|
|||||||
|
|
||||||
|
|
||||||
def reference_data():
|
def reference_data():
|
||||||
return json.loads((Path(__file__).parent / 'data/native-python-reference.json').read_text(encoding='utf-8'))
|
return json.loads((Path(__file__).parent / 'baselines/native/native-python-reference.json').read_text(encoding='utf-8'))
|
||||||
|
|
||||||
|
|
||||||
def reference_network(case):
|
def reference_network(case):
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ from app.main import run_system_xml_simulation
|
|||||||
|
|
||||||
FIXTURE_PATH = (
|
FIXTURE_PATH = (
|
||||||
Path(__file__).resolve().parent
|
Path(__file__).resolve().parent
|
||||||
/ "data"
|
|
||||||
/ "fixtures"
|
/ "fixtures"
|
||||||
/ "high_stiffness_explicit_rk45.xml"
|
/ "high_stiffness_explicit_rk45.xml"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from app.simulation.config import SolverActivityTracker
|
|||||||
from app.system_xml import validate_system_xml_document
|
from app.system_xml import validate_system_xml_document
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[1]
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
FIXTURE = ROOT / "tests/data/native-skill-test.xml"
|
FIXTURE = ROOT / "tests/fixtures/native-skill-test.xml"
|
||||||
|
|
||||||
|
|
||||||
def network(xml=None):
|
def network(xml=None):
|
||||||
@@ -76,7 +76,8 @@ class NativeExecutionTests(unittest.TestCase):
|
|||||||
def test_cache_and_standalone_executable_without_python_path(self):
|
def test_cache_and_standalone_executable_without_python_path(self):
|
||||||
cached = build_native(self.program)
|
cached = build_native(self.program)
|
||||||
self.assertTrue(cached.cache_hit)
|
self.assertTrue(cached.cache_hit)
|
||||||
env = {**os.environ, "PATH": str(Path(os.environ["SystemRoot"]) / "System32")}
|
isolated_path = str(Path(os.environ["SystemRoot"]) / "System32") if os.name == "nt" else str(self.root / "no-path")
|
||||||
|
env = {**os.environ, "PATH": isolated_path}
|
||||||
r = subprocess.run([str(cached.executable), "--init"], env=env, cwd=self.root,
|
r = subprocess.run([str(cached.executable), "--init"], env=env, cwd=self.root,
|
||||||
capture_output=True, text=True, check=True)
|
capture_output=True, text=True, check=True)
|
||||||
self.assertEqual(len(json.loads(r.stdout)), 12)
|
self.assertEqual(len(json.loads(r.stdout)), 12)
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ sys.meta_path.insert(0, NoNumericalPython())
|
|||||||
from app.main import compile_system_xml_network
|
from app.main import compile_system_xml_network
|
||||||
from app.simulation.native_codegen.compiler import compile_native_program
|
from app.simulation.native_codegen.compiler import compile_native_program
|
||||||
from app.system_xml import validate_system_xml_document
|
from app.system_xml import validate_system_xml_document
|
||||||
doc = validate_system_xml_document(Path('tests/data/native-skill-test.xml').read_bytes()).document
|
doc = validate_system_xml_document(Path('tests/fixtures/native-skill-test.xml').read_bytes()).document
|
||||||
program = compile_native_program(compile_system_xml_network(doc))
|
program = compile_native_program(compile_system_xml_network(doc))
|
||||||
assert len(program.state_keys) == 12
|
assert len(program.state_keys) == 12
|
||||||
assert len(program.variables) == 175
|
assert len(program.variables) == 175
|
||||||
|
|||||||
@@ -152,8 +152,8 @@ int main(void) {
|
|||||||
self.assertLess(abs(sum(rates)),1e-10+sum(map(abs,rates))*1e-12)
|
self.assertLess(abs(sum(rates)),1e-10+sum(map(abs,rates))*1e-12)
|
||||||
|
|
||||||
def test_web_stream_completes_mql4_and_matches_amesim_reference(self):
|
def test_web_stream_completes_mql4_and_matches_amesim_reference(self):
|
||||||
reference=json.loads((ROOT/'tests/data/test-mql-4-amesim-reference.json').read_text())
|
reference=json.loads((ROOT/'tests/baselines/simulation/test_mql_4/test-mql-4-amesim-reference.json').read_text())
|
||||||
xml=(ROOT/'tests/data/test-mql-4-corrected.xml').read_bytes()
|
xml=(ROOT/'tests/fixtures/amesim/test-mql-4-corrected.xml').read_bytes()
|
||||||
events=[json.loads(line) for line in simulation_event_stream(xml)]
|
events=[json.loads(line) for line in simulation_event_stream(xml)]
|
||||||
self.assertFalse([e for e in events if e['event']=='error'])
|
self.assertFalse([e for e in events if e['event']=='error'])
|
||||||
self.assertTrue(any(e['event']=='progress' for e in events))
|
self.assertTrue(any(e['event']=='progress' for e in events))
|
||||||
@@ -161,7 +161,7 @@ int main(void) {
|
|||||||
self.assertTrue(result['success'],result['message'])
|
self.assertTrue(result['success'],result['message'])
|
||||||
self.assertEqual(result['simulatedUntil'],10)
|
self.assertEqual(result['simulatedUntil'],10)
|
||||||
self.assertEqual(result['diagnostics']['backend'],'native-c')
|
self.assertEqual(result['diagnostics']['backend'],'native-c')
|
||||||
self.assertEqual(result['diagnostics']['integration']['rtol'],1e-7)
|
self.assertEqual(result['diagnostics']['integration']['rtol'],1e-8)
|
||||||
self.assertEqual(result['diagnostics']['integration']['method'],'BDF')
|
self.assertEqual(result['diagnostics']['integration']['method'],'BDF')
|
||||||
self.assertEqual(result['diagnostics']['stateCount'],64)
|
self.assertEqual(result['diagnostics']['stateCount'],64)
|
||||||
# Bound the formerly stalled tiny-step failure by work, not machine time.
|
# Bound the formerly stalled tiny-step failure by work, not machine time.
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
"""Standalone pipe-root checks; only a C compiler and Python stdlib are needed."""
|
||||||
|
import ctypes
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import shlex
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
LAMINAR_END = 89.96829989
|
||||||
|
|
||||||
|
|
||||||
|
def reference_friction(reynolds, roughness):
|
||||||
|
"""Independent evaluation of the retained Darcy blend, without its slope."""
|
||||||
|
laminar = 64 / reynolds
|
||||||
|
if reynolds <= LAMINAR_END:
|
||||||
|
return laminar
|
||||||
|
smooth = (-1.8 * math.log10(6.9 / reynolds)) ** -2
|
||||||
|
turbulent = smooth
|
||||||
|
if roughness:
|
||||||
|
fully_rough = (-2 * math.log10(roughness / 3.7)) ** -2
|
||||||
|
weight = 1 / (1 + (180 / (reynolds * roughness)) ** 2)
|
||||||
|
turbulent = (1 - weight) * smooth + weight * fully_rough
|
||||||
|
transition = ((reynolds - LAMINAR_END) / 2741.96700831) ** 8.37293695
|
||||||
|
return (laminar + transition * turbulent) / (1 + transition)
|
||||||
|
|
||||||
|
|
||||||
|
def reference_root(constant, roughness):
|
||||||
|
if constant == 0:
|
||||||
|
return 0.0
|
||||||
|
low, high = 0.0, 1.0
|
||||||
|
while high * high * reference_friction(high, roughness) < constant:
|
||||||
|
high *= 2
|
||||||
|
for _ in range(120):
|
||||||
|
middle = low + (high - low) / 2
|
||||||
|
if middle == low or middle == high:
|
||||||
|
break
|
||||||
|
if middle * middle * reference_friction(middle, roughness) < constant:
|
||||||
|
low = middle
|
||||||
|
else:
|
||||||
|
high = middle
|
||||||
|
return low + (high - low) / 2
|
||||||
|
|
||||||
|
|
||||||
|
class PipeStatus(ctypes.Structure):
|
||||||
|
_fields_ = [
|
||||||
|
('converged', ctypes.c_int),
|
||||||
|
('iterations', ctypes.c_int),
|
||||||
|
('bisections', ctypes.c_int),
|
||||||
|
('relative_residual', ctypes.c_double),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class NativePipeSolverTests(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
command = shlex.split(os.environ.get('CC', ''))
|
||||||
|
if not command:
|
||||||
|
compiler = shutil.which('gcc') or shutil.which('clang')
|
||||||
|
if not compiler:
|
||||||
|
raise unittest.SkipTest('A native C compiler is required')
|
||||||
|
command = [compiler]
|
||||||
|
cls.directory = tempfile.TemporaryDirectory(prefix='native-pipe-solver-')
|
||||||
|
cls.addClassCleanup(cls.directory.cleanup)
|
||||||
|
cls.compiler = command
|
||||||
|
source = (ROOT / 'native/components/kernels.c').read_text()
|
||||||
|
cls.library = cls.build_library(source, 'ordinary')
|
||||||
|
|
||||||
|
# Fault injection only in the temporary test translation unit. The
|
||||||
|
# resistance equation and public production ABI have no test switches.
|
||||||
|
prepared = 'static double pipe_friction_prepared('
|
||||||
|
derivative = 'static double pipe_friction_derivative('
|
||||||
|
assert source.count(prepared) == source.count(derivative) == 1
|
||||||
|
injected = 'int test_pipe_fault_mode=0;\n' + source.replace(
|
||||||
|
prepared, 'static double pipe_friction_prepared_original(', 1)
|
||||||
|
injected = injected.replace('static double pipe_friction(double', r'''
|
||||||
|
static double pipe_friction_prepared(double re,double rr,double rough) {
|
||||||
|
if(test_pipe_fault_mode==5)return 0; /* No upper sign change. */
|
||||||
|
if(test_pipe_fault_mode==6)return re<2000 ? .01 : 100;
|
||||||
|
return pipe_friction_prepared_original(re,rr,rough);
|
||||||
|
}
|
||||||
|
static double pipe_friction(double''', 1)
|
||||||
|
injected = injected.replace(
|
||||||
|
derivative, 'static double pipe_friction_derivative_original(', 1)
|
||||||
|
injected = injected.replace('static double pipe_checked_solution(', r'''
|
||||||
|
static double pipe_friction_derivative(double re,double rr,double rough,double *df) {
|
||||||
|
double f=pipe_friction_derivative_original(re,rr,rough,df);
|
||||||
|
double slope=2*re*f+re*re*(*df);
|
||||||
|
if(test_pipe_fault_mode==1)*df=(1e6*slope-2*re*f)/(re*re);
|
||||||
|
if(test_pipe_fault_mode==2)*df=NAN;
|
||||||
|
if(test_pipe_fault_mode==3)*df=(-slope-2*re*f)/(re*re);
|
||||||
|
if(test_pipe_fault_mode==4)return NAN;
|
||||||
|
if(test_pipe_fault_mode==6){*df=0;return re<2000 ? .01 : 100;}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
static double pipe_checked_solution(''', 1)
|
||||||
|
cls.fault_library = cls.build_library(injected, 'faults')
|
||||||
|
cls.fault_mode = ctypes.c_int.in_dll(cls.fault_library, 'test_pipe_fault_mode')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def build_library(cls, source, name):
|
||||||
|
directory = Path(cls.directory.name)
|
||||||
|
source_path = directory / (name + '.c')
|
||||||
|
library_path = directory / (name + ('.dll' if os.name == 'nt' else '.so'))
|
||||||
|
source_path.write_text(source)
|
||||||
|
command = cls.compiler + [
|
||||||
|
'-std=c11', '-O3', '-Wall', '-Wextra', '-Werror',
|
||||||
|
'-ffp-contract=off', '-fno-fast-math', '-shared',
|
||||||
|
]
|
||||||
|
if os.name != 'nt':
|
||||||
|
command.append('-fPIC')
|
||||||
|
command += ['-I', str(ROOT / 'native/include'), str(source_path),
|
||||||
|
'-lm', '-o', str(library_path)]
|
||||||
|
run = subprocess.run(command, capture_output=True, text=True, timeout=60)
|
||||||
|
if run.returncode:
|
||||||
|
raise AssertionError(run.stderr)
|
||||||
|
library = ctypes.CDLL(str(library_path))
|
||||||
|
if os.name == 'nt':
|
||||||
|
import _ctypes
|
||||||
|
cls.addClassCleanup(_ctypes.FreeLibrary, library._handle)
|
||||||
|
library.native_pipe_resistance.argtypes = [
|
||||||
|
ctypes.c_double, ctypes.c_double, ctypes.c_double,
|
||||||
|
ctypes.POINTER(PipeStatus),
|
||||||
|
]
|
||||||
|
library.native_pipe_resistance.restype = ctypes.c_double
|
||||||
|
return library
|
||||||
|
|
||||||
|
def solve(self, constant, roughness=0, flow_scale=1, library=None):
|
||||||
|
# Nonzero sentinel fields ensure every call resets a reused status.
|
||||||
|
status = PipeStatus(1, 999, 999, -1)
|
||||||
|
result = (library or self.library).native_pipe_resistance(
|
||||||
|
constant, roughness, flow_scale, ctypes.byref(status))
|
||||||
|
return result, status
|
||||||
|
|
||||||
|
def assert_root(self, constant, roughness, result, status):
|
||||||
|
self.assertTrue(status.converged)
|
||||||
|
self.assertTrue(math.isfinite(result))
|
||||||
|
self.assertLessEqual(status.relative_residual, 1e-9)
|
||||||
|
expected = reference_root(constant, roughness)
|
||||||
|
self.assertLessEqual(abs(result - expected), 1e-12 + expected * 1e-9)
|
||||||
|
residual = abs(result * result * reference_friction(result, roughness) / constant - 1)
|
||||||
|
self.assertLessEqual(residual, 1e-9)
|
||||||
|
|
||||||
|
def test_independent_bisection_across_reynolds_roughness_and_flow_scales(self):
|
||||||
|
fallbacks = 0
|
||||||
|
maximum_iterations = 0
|
||||||
|
for exponent in range(151):
|
||||||
|
reynolds = 10 ** (-6 + exponent * .1)
|
||||||
|
for roughness in (0, 1e-5, 1e-4, 1e-3, 1e-2, .1):
|
||||||
|
constant = reynolds ** 2 * reference_friction(reynolds, roughness)
|
||||||
|
for scale in (1e-12, 1e-6, 1):
|
||||||
|
with self.subTest(reynolds=reynolds, roughness=roughness, scale=scale):
|
||||||
|
result, status = self.solve(constant, roughness, scale)
|
||||||
|
self.assert_root(constant, roughness, result, status)
|
||||||
|
self.assertLess(status.iterations, 20)
|
||||||
|
fallbacks += status.bisections
|
||||||
|
maximum_iterations = max(maximum_iterations, status.iterations)
|
||||||
|
self.assertGreater(fallbacks, 0)
|
||||||
|
self.assertGreater(maximum_iterations, 0)
|
||||||
|
|
||||||
|
def test_laminar_transition_neighbors_and_extreme_flow_scales(self):
|
||||||
|
for center in (LAMINAR_END, 1000, 2300, LAMINAR_END + 2741.96700831, 4000):
|
||||||
|
for reynolds in (math.nextafter(center, 0), center, math.nextafter(center, math.inf)):
|
||||||
|
for roughness in (0, 1e-5, .1):
|
||||||
|
constant = reynolds ** 2 * reference_friction(reynolds, roughness)
|
||||||
|
for scale in (1e-300, 1e-12, 1, 1e300):
|
||||||
|
with self.subTest(reynolds=reynolds, roughness=roughness, scale=scale):
|
||||||
|
result, status = self.solve(constant, roughness, scale)
|
||||||
|
self.assert_root(constant, roughness, result, status)
|
||||||
|
|
||||||
|
def test_zero_invalid_inputs_and_unrepresentable_values_fail_explicitly(self):
|
||||||
|
result, status = self.solve(0)
|
||||||
|
self.assertEqual(result, 0)
|
||||||
|
self.assertTrue(status.converged)
|
||||||
|
self.assertEqual(status.relative_residual, 0)
|
||||||
|
result, status = self.solve(64, 0, 1.7e308)
|
||||||
|
self.assertEqual(result, 1)
|
||||||
|
self.assertTrue(status.converged)
|
||||||
|
for constant, roughness, scale in (
|
||||||
|
(-1, 0, 1), (math.nan, 0, 1), (math.inf, 0, 1),
|
||||||
|
(1, -1, 1), (1, math.nan, 1), (1, math.inf, 1),
|
||||||
|
(1, 0, 0), (1, 0, -1), (1, 0, math.nan), (1, 0, math.inf),
|
||||||
|
(math.ulp(0.0), 0, 1), # A positive root rounds to zero.
|
||||||
|
(1e8, 3.7, 1), # Non-finite roughness limit.
|
||||||
|
(1e100, 0, 1), # Non-finite friction in bracket expansion.
|
||||||
|
(1e308, 0, 1), # Non-finite starting bound.
|
||||||
|
(128, 0, 1.7e308), (1e8, 0, 1.7e308),
|
||||||
|
):
|
||||||
|
with self.subTest(constant=constant, roughness=roughness, scale=scale):
|
||||||
|
result, status = self.solve(constant, roughness, scale)
|
||||||
|
self.assertTrue(math.isnan(result))
|
||||||
|
self.assertFalse(status.converged)
|
||||||
|
self.assertLessEqual(status.iterations, 128)
|
||||||
|
self.assertNotEqual(status.relative_residual, -1)
|
||||||
|
|
||||||
|
def test_inaccurate_or_unusable_newton_slopes_trigger_convergent_bisection(self):
|
||||||
|
try:
|
||||||
|
for mode in (1, 2, 3):
|
||||||
|
self.fault_mode.value = mode
|
||||||
|
for reynolds in (1000, 2800, 1e5, 1e9):
|
||||||
|
for roughness in (0, .1):
|
||||||
|
constant = reynolds ** 2 * reference_friction(reynolds, roughness)
|
||||||
|
with self.subTest(mode=mode, reynolds=reynolds, roughness=roughness):
|
||||||
|
result, status = self.solve(constant, roughness, library=self.fault_library)
|
||||||
|
self.assert_root(constant, roughness, result, status)
|
||||||
|
self.assertGreater(status.bisections, 0)
|
||||||
|
self.assertLess(status.iterations, 128)
|
||||||
|
finally:
|
||||||
|
self.fault_mode.value = 0
|
||||||
|
|
||||||
|
def test_nonfinite_equation_missing_bracket_and_float_stagnation_fail(self):
|
||||||
|
try:
|
||||||
|
for mode in (4, 5, 6):
|
||||||
|
self.fault_mode.value = mode
|
||||||
|
with self.subTest(mode=mode):
|
||||||
|
result, status = self.solve(1e6, library=self.fault_library)
|
||||||
|
self.assertTrue(math.isnan(result))
|
||||||
|
self.assertFalse(status.converged)
|
||||||
|
if mode == 4:
|
||||||
|
self.assertEqual(status.iterations, 1)
|
||||||
|
elif mode == 5:
|
||||||
|
self.assertEqual(status.iterations, 0)
|
||||||
|
else:
|
||||||
|
# A discontinuous equation has no valid root even when
|
||||||
|
# the bracket narrows to adjacent representable values.
|
||||||
|
self.assertGreater(status.bisections, 0)
|
||||||
|
self.assertLess(status.iterations, 128)
|
||||||
|
finally:
|
||||||
|
self.fault_mode.value = 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from copy import deepcopy
|
||||||
|
import subprocess
|
||||||
|
import unittest
|
||||||
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
|
from app.main import run_system_xml_simulation
|
||||||
|
from app.simulation.native_codegen.build import toolchain
|
||||||
|
from app.simulation.native_codegen.input import project_xml
|
||||||
|
from app.system_xml import validate_system_xml_document
|
||||||
|
from tests.test_amesim_pnch012_xml import amesim_pnch012_project
|
||||||
|
|
||||||
|
|
||||||
|
def pressure_project(value: float | str, unit: str = "bar") -> dict:
|
||||||
|
project = amesim_pnch012_project().model_dump(mode="json")
|
||||||
|
chamber = project["nodes"][0]["data"]
|
||||||
|
chamber["parameters"]["p0"] = value
|
||||||
|
chamber["parameterUnits"]["p0"] = unit
|
||||||
|
return project
|
||||||
|
|
||||||
|
|
||||||
|
class PressureUnitInputTests(unittest.TestCase):
|
||||||
|
def test_absolute_bar_expression_exports_si_without_atmospheric_offset(self) -> None:
|
||||||
|
project = pressure_project("= 0.25 + 0.75")
|
||||||
|
original = deepcopy(project)
|
||||||
|
xml = project_xml(project)
|
||||||
|
root = ET.fromstring(xml)
|
||||||
|
self.assertEqual(root.attrib["unitSystem"], "SI")
|
||||||
|
parameter = root.find(
|
||||||
|
"./Components/Component[@id='chamber_1']/Parameter[@name='p0']"
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(parameter)
|
||||||
|
self.assertEqual(float(parameter.attrib["value"]), 100000.0)
|
||||||
|
self.assertNotIn("unit", parameter.attrib)
|
||||||
|
report = validate_system_xml_document(xml)
|
||||||
|
self.assertTrue(report.valid, report.as_dict())
|
||||||
|
self.assertEqual(project, original)
|
||||||
|
|
||||||
|
def test_saved_numeric_values_are_already_si_with_absolute_bar_display(self) -> None:
|
||||||
|
for value in (250000.0, "250000"):
|
||||||
|
with self.subTest(value=value):
|
||||||
|
self.assertEqual(
|
||||||
|
project_xml(pressure_project(value)),
|
||||||
|
project_xml(pressure_project(250000.0, "Pa")),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_absolute_bar_and_existing_pressure_expression_units_are_equivalent(self) -> None:
|
||||||
|
expected = project_xml(pressure_project(250000.0, "Pa"))
|
||||||
|
for unit, expression in (
|
||||||
|
("bar", "2 + 0.5"),
|
||||||
|
("kPa", "200 + 50"),
|
||||||
|
("MPa", "0.2 + 0.05"),
|
||||||
|
("Pa", "200000 + 50000"),
|
||||||
|
):
|
||||||
|
with self.subTest(unit=unit):
|
||||||
|
self.assertEqual(project_xml(pressure_project(expression, unit)), expected)
|
||||||
|
|
||||||
|
def test_absolute_bar_and_pa_simulations_return_identical_si_pressure(self) -> None:
|
||||||
|
try:
|
||||||
|
toolchain()
|
||||||
|
except (OSError, RuntimeError, subprocess.SubprocessError) as exc:
|
||||||
|
self.skipTest(f"Native toolchain unavailable: {exc}")
|
||||||
|
absolute_bar = run_system_xml_simulation(
|
||||||
|
project_xml(pressure_project("2 + 0.5", "bar"))
|
||||||
|
)
|
||||||
|
pascals = run_system_xml_simulation(
|
||||||
|
project_xml(pressure_project(250000.0, "Pa"))
|
||||||
|
)
|
||||||
|
self.assertTrue(absolute_bar["success"], absolute_bar["message"])
|
||||||
|
self.assertTrue(pascals["success"], pascals["message"])
|
||||||
|
self.assertEqual(absolute_bar["series"], pascals["series"])
|
||||||
|
self.assertEqual(absolute_bar["series"]["time"], [0.0, 0.001, 0.002])
|
||||||
|
for pressure in absolute_bar["series"]["chamber_1.p"]:
|
||||||
|
self.assertAlmostEqual(pressure, 250000.0, places=6)
|
||||||
|
pressure_metadata = next(
|
||||||
|
variable for variable in absolute_bar["variables"]
|
||||||
|
if variable["key"] == "chamber_1.p"
|
||||||
|
)
|
||||||
|
self.assertEqual(pressure_metadata["unit"], "Pa")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -11,9 +11,9 @@ BASELINE_ROOT = REPOSITORY_ROOT / "tests" / "baselines" / "simulation"
|
|||||||
GIT_ATTRIBUTES_PATH = REPOSITORY_ROOT / ".gitattributes"
|
GIT_ATTRIBUTES_PATH = REPOSITORY_ROOT / ".gitattributes"
|
||||||
|
|
||||||
EXPECTED_ATTRIBUTE_RULES = {
|
EXPECTED_ATTRIBUTE_RULES = {
|
||||||
("tests/data/test-mql-8.xml", "text", "eol=lf"),
|
("tests/fixtures/legacy/test-mql-8.xml", "text", "eol=lf"),
|
||||||
("tests/data/test-mql-8.json", "text", "eol=lf"),
|
("tests/fixtures/legacy/test-mql-8.json", "text", "eol=lf"),
|
||||||
("tests/data/test_mql-full-branches-01-04.xml", "text", "eol=lf"),
|
("tests/baselines/simulation/test_mql_full_branches/sources/test_mql-full-branches-01-04.xml", "text", "eol=lf"),
|
||||||
("tests/baselines/simulation/**/*.json", "text", "eol=lf"),
|
("tests/baselines/simulation/**/*.json", "text", "eol=lf"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,325 @@
|
|||||||
|
"""Reproduce the eight-branch AME drawing -> editor model audit.
|
||||||
|
|
||||||
|
Run from the repository root with .venv/bin/python. The original inputs are
|
||||||
|
read only. Mapping uses typed graph isomorphism and endpoint edit cost, never
|
||||||
|
component display names; names are emitted only as evidence for review.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import Counter, defaultdict
|
||||||
|
from copy import deepcopy
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
import tarfile
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
from tests.test_test_mql_ame_contract import (
|
||||||
|
_blocks, _element_text as element, _expected_parameters,
|
||||||
|
_public_model_type, _resolve_globals, _value_fields,
|
||||||
|
_SOURCE_FIELD_BY_PARAMETER, _numeric_project_value,
|
||||||
|
)
|
||||||
|
from app.simulation.registry import get_component_model_spec
|
||||||
|
|
||||||
|
HERE = ROOT / 'test/solver-newton-20260911/mql8'
|
||||||
|
AME = ROOT / 'tests/data/test_mql.ame'
|
||||||
|
INPUT = ROOT / 'tests/fixtures/legacy/test-mql-8.json'
|
||||||
|
OUTPUT = ROOT / 'tests/data/test-mql-8-corrected.json'
|
||||||
|
|
||||||
|
|
||||||
|
def read_drawing():
|
||||||
|
with tarfile.open(AME) as archive:
|
||||||
|
cir = archive.extractfile('test_mql_.cir').read().decode('latin1')
|
||||||
|
sim = archive.extractfile('test_mql_.sim').read().decode('ascii')
|
||||||
|
model_info = archive.extractfile('test_mql_.modelinfo').read().decode('ascii')
|
||||||
|
globals_ = _resolve_globals(cir)
|
||||||
|
components, lines = _blocks(cir, 'COMP'), _blocks(cir, 'LINE')
|
||||||
|
entities, edges = {}, []
|
||||||
|
|
||||||
|
def add(key, body):
|
||||||
|
kind = _public_model_type(element(body, 'SUB_NAME'))
|
||||||
|
entities[key] = {
|
||||||
|
'modelType': kind, 'alias': element(body, 'ALIAS'),
|
||||||
|
'parameters': _expected_parameters(body, kind, globals_),
|
||||||
|
'fields': {k: vars(v) for k, v in _value_fields(body).items()},
|
||||||
|
'geometry': element(body, 'COMP_GEOMETRY'),
|
||||||
|
'outputType': element(body, 'OUTPUT_TYPE'),
|
||||||
|
'scopeId': element(body, 'CIRCUIT_SCOPE_ID'),
|
||||||
|
'drawingPorts': [{'position': element(p, 'PORT_POS'), 'face': element(p, 'PORT_FACE')}
|
||||||
|
for p in _blocks(body, 'COMP_PORT')],
|
||||||
|
}
|
||||||
|
|
||||||
|
def port(component, index):
|
||||||
|
kind = entities[component]['modelType']
|
||||||
|
if index == 0 and kind in ('amesim_forc', 'amesim_pnvo001'):
|
||||||
|
return 'res'
|
||||||
|
if index == 0 and kind in ('amesim_step0', 'amesim_ud00'):
|
||||||
|
return 'out'
|
||||||
|
return f'port_{index + 1}'
|
||||||
|
|
||||||
|
for index, body in enumerate(components):
|
||||||
|
add(f'component:{index}', body)
|
||||||
|
contacts = set()
|
||||||
|
for index, body in enumerate(components):
|
||||||
|
for pindex, pbody in enumerate(_blocks(body, 'COMP_PORT')):
|
||||||
|
if element(pbody, 'PORT_CONNECT') != '1':
|
||||||
|
continue
|
||||||
|
for conn in _blocks(pbody, 'CONNECT'):
|
||||||
|
other = int(element(conn, 'CONNECT_ENTITY_NUM'))
|
||||||
|
op = int(element(conn, 'CONNECT_ENTITY_PORT'))
|
||||||
|
contacts.add(((index, pindex), (other, op)))
|
||||||
|
for left, right in sorted(contacts):
|
||||||
|
assert (right, left) in contacts, 'Non-reciprocal component contact'
|
||||||
|
if left > right:
|
||||||
|
continue
|
||||||
|
a, b = f'component:{left[0]}', f'component:{right[0]}'
|
||||||
|
edges.append(((a, port(a, left[1])), (b, port(b, right[1])), 'contact'))
|
||||||
|
for index, body in enumerate(lines):
|
||||||
|
assert element(body, 'LINE_START_TYPE') == element(body, 'LINE_END_TYPE') == '0'
|
||||||
|
a, b = ('component:' + element(body, 'LINE_START_ENTITY'),
|
||||||
|
'component:' + element(body, 'LINE_END_ENTITY'))
|
||||||
|
left = a, port(a, int(element(body, 'LINE_START_PORT')))
|
||||||
|
right = b, port(b, int(element(body, 'LINE_END_PORT')))
|
||||||
|
if element(body, 'SUB_NAME') == 'DIRECT':
|
||||||
|
edges.append((left, right, f'direct-line:{index}'))
|
||||||
|
continue
|
||||||
|
key = f'line:{index}'
|
||||||
|
add(key, body)
|
||||||
|
output_type = element(body, 'OUTPUT_TYPE')
|
||||||
|
assert output_type in ('1', '2'), (key, output_type)
|
||||||
|
# A reversed two-port line's first physical endpoint is submodel port 2.
|
||||||
|
lp, rp = ('port_1', 'port_2') if output_type == '1' else ('port_2', 'port_1')
|
||||||
|
edges.extend(((left, (key, lp), key), ((key, rp), right, key)))
|
||||||
|
return entities, edges, globals_, sim, model_info
|
||||||
|
|
||||||
|
|
||||||
|
def match_graph(entities, ame_edges, project):
|
||||||
|
nodes = {n['id']: n for n in project['nodes']}
|
||||||
|
types = {**{k: v['modelType'] for k, v in entities.items()},
|
||||||
|
**{k: v['data']['modelType'] for k, v in nodes.items()}}
|
||||||
|
adjacency = {k: {} for k in types}
|
||||||
|
for (a, ap), (b, bp), _ in ame_edges:
|
||||||
|
assert b not in adjacency[a], 'Parallel entity edges need a multigraph matcher'
|
||||||
|
adjacency[a][b], adjacency[b][a] = (ap, bp), (bp, ap)
|
||||||
|
for edge in project['edges']:
|
||||||
|
a, b = edge['source'], edge['target']
|
||||||
|
assert b not in adjacency[a], 'Parallel editor edges need a multigraph matcher'
|
||||||
|
ap, bp = edge['sourceHandle'], edge['targetHandle']
|
||||||
|
adjacency[a][b], adjacency[b][a] = (ap, bp), (bp, ap)
|
||||||
|
colors = types.copy()
|
||||||
|
for _ in range(len(entities)):
|
||||||
|
signatures = {k: (types[k], colors[k], tuple(sorted(colors[n] for n in adjacency[k]))) for k in types}
|
||||||
|
vocabulary = {v: i for i, v in enumerate(sorted(set(signatures.values())))}
|
||||||
|
updated = {k: vocabulary[v] for k, v in signatures.items()}
|
||||||
|
if len(set(updated.values())) == len(set(colors.values())):
|
||||||
|
colors = updated
|
||||||
|
break
|
||||||
|
colors = updated
|
||||||
|
groups = defaultdict(lambda: [[], []])
|
||||||
|
for key in entities:
|
||||||
|
groups[colors[key]][0].append(key)
|
||||||
|
for key in nodes:
|
||||||
|
groups[colors[key]][1].append(key)
|
||||||
|
for ames, editors in groups.values():
|
||||||
|
assert len(ames) == len(editors), 'Typed topology differs'
|
||||||
|
# All remaining structural symmetries in this exact input preserve
|
||||||
|
# parameters, so selection within them does not hide parameter errors.
|
||||||
|
assert len({json.dumps(entities[k]['parameters'], sort_keys=True) for k in ames}) == 1
|
||||||
|
candidates = {a: tuple(editors) for ames, editors in groups.values() for a in ames}
|
||||||
|
best, best_cost, examined = None, math.inf, 0
|
||||||
|
mapping, used = {}, set()
|
||||||
|
|
||||||
|
def choices(a):
|
||||||
|
result = []
|
||||||
|
for b in candidates[a]:
|
||||||
|
if b in used:
|
||||||
|
continue
|
||||||
|
cost = 0
|
||||||
|
for previous, assigned in mapping.items():
|
||||||
|
ae, be = adjacency[a].get(previous), adjacency[b].get(assigned)
|
||||||
|
if bool(ae) != bool(be):
|
||||||
|
break
|
||||||
|
if ae:
|
||||||
|
cost += sum(x != y for x, y in zip(ae, be))
|
||||||
|
else:
|
||||||
|
result.append((cost, b))
|
||||||
|
return sorted(result)
|
||||||
|
|
||||||
|
def visit(cost):
|
||||||
|
nonlocal best, best_cost, examined
|
||||||
|
if cost >= best_cost:
|
||||||
|
return
|
||||||
|
if len(mapping) == len(entities):
|
||||||
|
examined += 1
|
||||||
|
best, best_cost = mapping.copy(), cost
|
||||||
|
return
|
||||||
|
remaining = [(a, choices(a)) for a in entities if a not in mapping]
|
||||||
|
if any(not options for _, options in remaining):
|
||||||
|
return
|
||||||
|
# Each current candidate cost is charged only for edges to assigned
|
||||||
|
# nodes, so this lower bound does not double count any edge.
|
||||||
|
if cost + sum(options[0][0] for _, options in remaining) >= best_cost:
|
||||||
|
return
|
||||||
|
a, options = min(remaining, key=lambda item: (len(item[1]),
|
||||||
|
-sum(n in mapping for n in adjacency[item[0]]), -len(adjacency[item[0]]), item[0]))
|
||||||
|
for extra, b in options:
|
||||||
|
mapping[a] = b
|
||||||
|
used.add(b)
|
||||||
|
visit(cost + extra)
|
||||||
|
used.remove(b)
|
||||||
|
del mapping[a]
|
||||||
|
visit(0)
|
||||||
|
assert best is not None, 'No typed topology isomorphism'
|
||||||
|
return best, {'minimumEndpointEdits': best_cost, 'improvingSolutions': examined,
|
||||||
|
'typedRefinementGroups': len(groups), 'parameterPreservingSymmetries': True}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
HERE.mkdir(parents=True, exist_ok=True)
|
||||||
|
original = json.loads(INPUT.read_text())
|
||||||
|
entities, edges, globals_, sim, model_info = read_drawing()
|
||||||
|
mapping, matching = match_graph(entities, edges, original)
|
||||||
|
corrected = deepcopy(original)
|
||||||
|
corrected['name'] = 'test-mql-8-corrected'
|
||||||
|
nodes = {n['id']: n for n in corrected['nodes']}
|
||||||
|
parameter_audit, parameter_changes = [], []
|
||||||
|
for source, target in mapping.items():
|
||||||
|
entity, node = entities[source], nodes[target]
|
||||||
|
spec = get_component_model_spec(entity['modelType'])
|
||||||
|
assert node['data']['modelVersion'] == spec.model_version
|
||||||
|
source_fields = _SOURCE_FIELD_BY_PARAMETER.get(entity['modelType'], {})
|
||||||
|
assert set(node['data']['parameters']) == set(entity['parameters'])
|
||||||
|
for parameter in spec.parameters:
|
||||||
|
name = parameter.name
|
||||||
|
expected = entity['parameters'][name]
|
||||||
|
actual = _numeric_project_value(node['data']['parameters'][name])
|
||||||
|
row = {'component': target, 'ameEntity': source, 'ameAlias': entity['alias'],
|
||||||
|
'parameter': name, 'unit': parameter.unit, 'old': actual, 'expected': expected,
|
||||||
|
'source': entity['fields'].get(source_fields.get(name, name)),
|
||||||
|
'matches': math.isclose(actual, expected, rel_tol=2e-14, abs_tol=1e-15)}
|
||||||
|
parameter_audit.append(row)
|
||||||
|
if not row['matches']:
|
||||||
|
parameter_changes.append(row)
|
||||||
|
node['data']['parameters'][name] = expected
|
||||||
|
desired = {}
|
||||||
|
for (a, ap), (b, bp), source in edges:
|
||||||
|
ja, jb = mapping[a], mapping[b]
|
||||||
|
key = frozenset((ja, jb))
|
||||||
|
assert key not in desired
|
||||||
|
desired[key] = ({ja: ap, jb: bp}, source)
|
||||||
|
connection_audit, connection_changes = [], []
|
||||||
|
cleared_contact_flags = []
|
||||||
|
for edge in corrected['edges']:
|
||||||
|
ends, source = desired.pop(frozenset((edge['source'], edge['target'])))
|
||||||
|
before = [(edge['source'], edge['sourceHandle']), (edge['target'], edge['targetHandle'])]
|
||||||
|
after = [(edge['source'], ends[edge['source']]), (edge['target'], ends[edge['target']])]
|
||||||
|
row = {'edgeId': edge['id'], 'ameSource': source, 'before': before, 'after': after, 'matches': before == after}
|
||||||
|
connection_audit.append(row)
|
||||||
|
if not row['matches']:
|
||||||
|
connection_changes.append(row)
|
||||||
|
edge['sourceHandle'], edge['targetHandle'] = after[0][1], after[1][1]
|
||||||
|
# A moved endpoint no longer certifies the old geometric contact.
|
||||||
|
# Keep its explicit connection visible; physical adjacency is intact.
|
||||||
|
if edge.get('data', {}).get('isContactEdge'):
|
||||||
|
edge['data']['isContactEdge'] = False
|
||||||
|
cleared_contact_flags.append(edge['id'])
|
||||||
|
assert not desired
|
||||||
|
# The corrected shared P4 symbol uses p1 up, p2 left, p3 down, p4 right.
|
||||||
|
# Read every drawing port to ensure the saved geometry needs exactly 180°.
|
||||||
|
display_audit, p4_nodes = [], set()
|
||||||
|
for source, target in mapping.items():
|
||||||
|
entity = entities[source]
|
||||||
|
if entity['modelType'] != 'amesim_p4node2':
|
||||||
|
continue
|
||||||
|
expected = [('10 19', '1'), ('19 10', '3'), ('10 1', '0'), ('1 10', '2')]
|
||||||
|
assert entity['geometry'] == '1'
|
||||||
|
assert [(p['position'], p['face']) for p in entity['drawingPorts']] == expected
|
||||||
|
data = nodes[target]['data']
|
||||||
|
display_audit.append({'component': target, 'ameEntity': source, 'ameAlias': entity['alias'],
|
||||||
|
'ameGeometry': entity['geometry'], 'drawingPorts': entity['drawingPorts'],
|
||||||
|
'previousRotation': data.get('rotation'), 'previousMirrored': data.get('mirrored'),
|
||||||
|
'rotation': 180, 'mirrored': False,
|
||||||
|
'portSides': {'port_1': 'bottom', 'port_2': 'right', 'port_3': 'top', 'port_4': 'left'}})
|
||||||
|
data['rotation'], data['mirrored'] = 180, False
|
||||||
|
p4_nodes.add(target)
|
||||||
|
cleared_route_points = []
|
||||||
|
for edge in corrected['edges']:
|
||||||
|
if edge['source'] in p4_nodes or edge['target'] in p4_nodes:
|
||||||
|
if 'routePoints' in edge.get('data', {}):
|
||||||
|
del edge['data']['routePoints']
|
||||||
|
cleared_route_points.append(edge['id'])
|
||||||
|
# The first five .sim values are recorded directly; no interpretation of
|
||||||
|
# undocumented solver-enumeration slots is needed for this correction.
|
||||||
|
sim_values = [float(x) for x in sim.splitlines()[0].split()]
|
||||||
|
corrected['simulation'].update(t_start=sim_values[0], t_stop=sim_values[1], step=sim_values[2], max_step=sim_values[3])
|
||||||
|
from app.main import ReactFlowProjectPayload, build_reactflow_system_xml, compile_reactflow_network
|
||||||
|
from app.simulation.native_codegen.compiler import compile_native_program
|
||||||
|
# Browser export resolves arithmetic editor expressions on a separate copy.
|
||||||
|
executable = deepcopy(corrected)
|
||||||
|
for node in executable['nodes']:
|
||||||
|
node['data']['parameters'] = {k: _numeric_project_value(v)
|
||||||
|
for k, v in node['data']['parameters'].items()}
|
||||||
|
payload = ReactFlowProjectPayload.model_validate(executable)
|
||||||
|
network = compile_reactflow_network(payload)
|
||||||
|
program = compile_native_program(network)
|
||||||
|
OUTPUT.write_text(json.dumps(corrected, ensure_ascii=False, indent=2) + '\n')
|
||||||
|
xml = build_reactflow_system_xml(payload)
|
||||||
|
(HERE / 'corrected.xml').write_bytes(xml if isinstance(xml, bytes) else xml.encode())
|
||||||
|
# The archive's parameter table and run input have matching row order.
|
||||||
|
with tarfile.open(AME) as archive:
|
||||||
|
param_lines = archive.extractfile('test_mql_.param').read().decode('latin1').splitlines()
|
||||||
|
data_lines = archive.extractfile('test_mql_.data').read().decode('latin1').splitlines()
|
||||||
|
assert len(param_lines) == len(data_lines)
|
||||||
|
run_parameters = {re.search(r'Data_Path=(\S+)', text).group(1): (i+1, data_lines[i])
|
||||||
|
for i,text in enumerate(param_lines) if 'Data_Path=' in text}
|
||||||
|
cache_checks, structural_fields = [], []
|
||||||
|
reverse_mapping = {v:k for k,v in mapping.items()}
|
||||||
|
from tests.test_test_mql_ame_contract import _evaluate_expression
|
||||||
|
for row in parameter_audit:
|
||||||
|
if row['source'] is None:
|
||||||
|
continue
|
||||||
|
entity = entities[reverse_mapping[row['component']]]
|
||||||
|
field = _SOURCE_FIELD_BY_PARAMETER.get(entity['modelType'], {}).get(row['parameter'], row['parameter'])
|
||||||
|
path = field + '@' + row['ameAlias']
|
||||||
|
if path not in run_parameters:
|
||||||
|
assert entity['modelType'] == 'amesim_lmechn1' and field == 'v1'
|
||||||
|
structural_fields.append(path)
|
||||||
|
continue
|
||||||
|
line, expression = run_parameters[path]
|
||||||
|
drawing_value = _evaluate_expression(row['source']['expression'], globals_)
|
||||||
|
run_value = _evaluate_expression(expression, globals_)
|
||||||
|
assert math.isclose(drawing_value, run_value, rel_tol=2e-14, abs_tol=1e-15), path
|
||||||
|
cache_checks.append({'dataPath': path, 'parameterDataLine': line,
|
||||||
|
'drawingValue': drawing_value, 'runValue': run_value})
|
||||||
|
audit = {
|
||||||
|
'sources': {str(p.relative_to(ROOT)): {'sha256': hashlib.sha256(p.read_bytes()).hexdigest(), 'bytes': p.stat().st_size} for p in (AME, INPUT)},
|
||||||
|
'output': str(OUTPUT.relative_to(ROOT)), 'globals': globals_, 'matching': matching,
|
||||||
|
'counts': {'components': len(entities), 'connections': len(edges), 'parameters': len(parameter_audit),
|
||||||
|
'changedParameters': len(parameter_changes), 'changedConnections': len(connection_changes),
|
||||||
|
'states': len(program.state_keys), 'outputs': len(program.variables)},
|
||||||
|
'simulation': {'old': original['simulation'], 'corrected': corrected['simulation'], 'ameRaw': sim,
|
||||||
|
'ameModelInfo': model_info, 'ameRtol': sim_values[4],
|
||||||
|
'note': 'BDF retained; AME solver enumeration is not equated with CVODE BDF. API rtol comes from the current backend default; this editor JSON does not carry rtol.'},
|
||||||
|
'mapping': [{'ameEntity': a, 'ameAlias': entities[a]['alias'], 'component': b,
|
||||||
|
'type': entities[a]['modelType'], 'lineOutputType': entities[a]['outputType']} for a,b in sorted(mapping.items())],
|
||||||
|
'parameterChanges': parameter_changes, 'connectionChanges': connection_changes,
|
||||||
|
'clearedGeometricContactFlags': cleared_contact_flags,
|
||||||
|
'p4DisplayAlignment': display_audit,
|
||||||
|
'clearedP4RoutePoints': cleared_route_points,
|
||||||
|
'archiveParameterCrossCheck': {'tableRows': len(param_lines), 'checked': len(cache_checks),
|
||||||
|
'drawingOnlyStructuralFields': structural_fields, 'checks': cache_checks},
|
||||||
|
'parameters': parameter_audit, 'connections': connection_audit,
|
||||||
|
'evaluationSchedule': program.evaluation_schedule,
|
||||||
|
}
|
||||||
|
(HERE / 'audit.json').write_text(json.dumps(audit, ensure_ascii=False, indent=2) + '\n')
|
||||||
|
print(json.dumps({'counts': audit['counts'], 'matching': matching,
|
||||||
|
'parameters': parameter_changes, 'connections': connection_changes,
|
||||||
|
'simulation': audit['simulation']}, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in new issue
Block a user