chore: remove retired workflow and historical material

This commit is contained in:
KOKO\Mimi
2026-10-17 22:39:47 +09:00
parent 4bd19f5bf0
commit 774b80099c
768 changed files with 0 additions and 54164 deletions
-13
View File
@@ -1,13 +0,0 @@
# 개발 실행 진입점
이 영역은 명시적으로 요청된 검증 실행과 비교 도구를 제공한다. 솔버의 제품 runtime이나
자동 작업 승인·commit·push·release 절차를 소유하지 않는다.
- execute.py는 인자 없는 build/test 진입점이다. 과거 phase 인자, 자동 retry·내부 LLM·
파일명 TDD gate·대화 종료 hook을 복원하지 않는다.
- 명령 성공의 범위를 실제 실행한 검사로 제한한다. C++ 실행기가 Python 회귀나 별도
reference 비교까지 자동 수행한다고 보고하지 않는다.
- Config·preset·실행 명령은 신뢰가 필요한 실행 입력이다. shell=False나 timeout을
sandbox라고 설명하지 않는다.
- 변경 동작은 해당 Python 검사로 검증하고 문서만 수정하면 실행하지 않는다.
기능별 source/reference를 실행기 편의를 위해 rename·rewrite하지 않는다.
-59
View File
@@ -1,59 +0,0 @@
#!/usr/bin/env python3
"""Run the configured C/C++ build and tests once: python scripts/execute.py."""
import argparse
import sys
import time
from pathlib import Path
from msvc_harness.adapters.base import AdapterError
from msvc_harness.adapters.cmake import CMakeAdapter
from msvc_harness.adapters.msbuild import MSBuildAdapter
from msvc_harness.config import ConfigError, load_config
from msvc_harness.discovery import DiscoveryError, discover_project
from msvc_harness.models import ProjectKind
from msvc_harness.process import ValidationFailure, execute_plan
from msvc_harness.toolchain import ToolchainError, discover_toolchain
ROOT = Path(__file__).resolve().parents[1]
def run_validations(root: Path) -> int:
deadline = time.monotonic() + 1800
try:
config = load_config(root)
selection = discover_project(root, config).selection
if selection is None:
print("not configured: no CMake or MSBuild project found", file=sys.stderr)
return 2
tools = discover_toolchain(selection.kind, deadline=deadline)
adapter = CMakeAdapter() if selection.kind is ProjectKind.CMAKE else MSBuildAdapter()
plan = adapter.create_plan(root, selection, config, tools)
results = execute_plan(plan, root, deadline=deadline)
except (ConfigError, DiscoveryError, ToolchainError, AdapterError, ValidationFailure, OSError) as exc:
print(str(exc), file=sys.stderr)
return 1
for result in results:
print(f"{result.command.stage}: argv={list(result.command.argv)!r}; "
f"cwd={str(result.command.cwd)!r}; exit {result.returncode}")
if result.command.stage == "test":
output = (result.stdout + "\n" + result.stderr).strip()
tail = "\n".join(output.splitlines()[-8:])[-2000:]
if tail != output:
print("[test output tail]")
if tail:
print(tail)
print(f"Validation passed ({len(results)} commands).")
return 0
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.parse_args(argv)
return run_validations(ROOT)
if __name__ == "__main__":
raise SystemExit(main())
-15
View File
@@ -1,15 +0,0 @@
# Build와 test 실행기
이 package는 config·project 감지·toolchain 선택, adapter 명령 계획과 process 실행을 소유한다.
수치 acceptance나 Feature 상태, Git와 agent 인계는 소유하지 않는다.
- Explicit config와 metadata에 따라 CMake/MSBuild를 선택한다. 모호한 project 감지를
첫 번째 파일로 해결하거나 C++ source 존재만으로 별도 gate를 만들지 않는다.
- CMake 경로는 configure 이후 MSVC metadata와 CTest의 비어 있지 않은 JSON discovery를
검사한다. Preset 네 필드와 실제 binaryDir의 일치를 보존한다.
- Argv list와 shell=False를 사용하고 cwd를 검증한다. CLI의 전체 deadline과 step timeout
중 남은 범위를 적용한다. Backend side effect 전체를 격리한다고 주장하지 않는다.
- Nonzero·timeout·결과 검사 실패는 stage/argv/cwd와 출력 tail을 포함해 반환한다.
자동 retry·commit·push·phase 결과 파일 생성을 추가하지 않는다.
- Config precedence, ambiguous metadata, non-MSVC, empty discovery, timeout 경계,
backend exit와 runner exit 구분을 process seam으로 검증한다.
-3
View File
@@ -1,3 +0,0 @@
from .config import ConfigError, load_config
__all__ = ["ConfigError", "load_config"]
@@ -1,5 +0,0 @@
from .base import AdapterError, ValidationAdapter
from .cmake import CMakeAdapter
from .msbuild import MSBuildAdapter
__all__ = ["AdapterError", "CMakeAdapter", "MSBuildAdapter", "ValidationAdapter"]
-19
View File
@@ -1,19 +0,0 @@
from pathlib import Path
from typing import Protocol
from ..models import HarnessConfig, ProjectSelection, Toolchain, ValidationPlan
class AdapterError(RuntimeError):
pass
class ValidationAdapter(Protocol):
def create_plan(
self,
root: Path,
selection: ProjectSelection,
config: HarnessConfig,
tools: Toolchain,
) -> ValidationPlan:
raise NotImplementedError
-99
View File
@@ -1,99 +0,0 @@
from pathlib import Path
from ..models import (
CommandSpec,
HarnessConfig,
ProjectKind,
ProjectSelection,
ResultCheck,
ResultCheckKind,
Toolchain,
ValidationPlan,
ValidationStep,
)
from .base import AdapterError
class CMakeAdapter:
def create_plan(
self,
root: Path,
selection: ProjectSelection,
config: HarnessConfig,
tools: Toolchain,
) -> ValidationPlan:
if selection.kind is not ProjectKind.CMAKE:
raise AdapterError("CMakeAdapter requires a CMake project selection")
if tools.cmake is None:
raise AdapterError("CMake tool is required")
if tools.ctest is None:
raise AdapterError("CTest tool is required")
if config.cmake.configure_preset is None:
command_cwd = root
binary = config.cmake.binary_dir or root / ".harness/build"
configure = (
str(tools.cmake),
"-S",
str(config.cmake.source_dir),
"-B",
str(binary),
"-A",
"x64",
)
build = (str(tools.cmake), "--build", str(binary), "--config", "Debug")
discover = (
str(tools.ctest),
"--test-dir",
str(binary),
"-C",
"Debug",
"--show-only=json-v1",
)
test = (
str(tools.ctest),
"--test-dir",
str(binary),
"-C",
"Debug",
"--output-on-failure",
)
else:
command_cwd = config.cmake.source_dir
binary = config.cmake.binary_dir
if (
binary is None
or config.cmake.build_preset is None
or config.cmake.test_preset is None
):
raise AdapterError("CMake presets require binary, build, and test settings")
configure = (str(tools.cmake), "--preset", config.cmake.configure_preset)
build = (str(tools.cmake), "--build", "--preset", config.cmake.build_preset)
discover = (
str(tools.ctest),
"--preset",
config.cmake.test_preset,
"--show-only=json-v1",
)
test = (
str(tools.ctest),
"--preset",
config.cmake.test_preset,
"--output-on-failure",
)
return ValidationPlan(
ProjectKind.CMAKE,
(
ValidationStep(
CommandSpec(configure, command_cwd, "configure"),
(ResultCheck(ResultCheckKind.CMAKE_COMPILER_IS_MSVC, binary),),
),
ValidationStep(CommandSpec(build, command_cwd, "build")),
ValidationStep(
CommandSpec(discover, command_cwd, "test-discovery"),
(ResultCheck(ResultCheckKind.CTEST_HAS_TESTS),),
),
ValidationStep(CommandSpec(test, command_cwd, "test")),
),
)
-62
View File
@@ -1,62 +0,0 @@
from pathlib import Path
from ..models import (
CommandSpec,
HarnessConfig,
ProjectKind,
ProjectSelection,
Toolchain,
ValidationPlan,
ValidationStep,
)
from .base import AdapterError
def _test_argv(root: Path, command: tuple[str, ...]) -> tuple[str, ...]:
first = Path(command[0].replace("\\", "/"))
if first.is_absolute():
raise AdapterError(
"msbuild.testCommand executable must be repository-relative"
)
executable = (root / first).resolve()
try:
executable.relative_to(root.resolve())
except ValueError as exc:
raise AdapterError(
"msbuild.testCommand resolves outside the repository"
) from exc
return (str(executable), *command[1:])
class MSBuildAdapter:
def create_plan(
self,
root: Path,
selection: ProjectSelection,
config: HarnessConfig,
tools: Toolchain,
) -> ValidationPlan:
if selection.kind is not ProjectKind.MSBUILD:
raise AdapterError("MSBuildAdapter received a non-MSBuild project")
if not config.msbuild.test_command:
raise AdapterError(
"msbuild.testCommand is required for .sln/.vcxproj validation"
)
build = ValidationStep(
CommandSpec(
(
str(tools.msbuild),
str(selection.project_file),
"/m",
"/nologo",
f"/p:Configuration={config.msbuild.configuration}",
f"/p:Platform={config.msbuild.platform}",
),
root,
"build",
)
)
test = ValidationStep(
CommandSpec(_test_argv(root, config.msbuild.test_command), root, "test")
)
return ValidationPlan(ProjectKind.MSBUILD, (build, test))
-138
View File
@@ -1,138 +0,0 @@
import json
import sys
from pathlib import Path
from typing import Any
from .models import CMakeConfig, HarnessConfig, MSBuildConfig
class ConfigError(ValueError):
pass
def _reject_unknown(data: dict[str, Any], allowed: set[str], label: str) -> None:
unknown = sorted(set(data) - allowed)
if unknown:
raise ConfigError(f"{label} contains unexpected field: {unknown[0]}")
def _repo_path(root: Path, raw: str, label: str) -> Path:
candidate = Path(raw)
if candidate.is_absolute():
raise ConfigError(f"{label} must be repository-relative")
resolved = (root / candidate).resolve()
try:
resolved.relative_to(root)
except ValueError as exc:
raise ConfigError(f"{label} resolves outside the repository") from exc
return resolved
def _mapping(value: Any, label: str) -> dict[str, Any]:
if not isinstance(value, dict):
raise ConfigError(f"{label} must be an object")
return value
def _nonempty_string(value: Any, label: str) -> str:
if not isinstance(value, str) or not value:
raise ConfigError(f"{label} must be a non-empty string")
if any(ord(character) < 32 or ord(character) == 127 for character in value):
raise ConfigError(f"{label} must not contain control characters")
return value
def _string_array(value: Any, label: str) -> tuple[str, ...]:
if not isinstance(value, list):
raise ConfigError(f"{label} must be an array")
if not value:
raise ConfigError(f"{label} must be a non-empty string array")
return tuple(
_nonempty_string(item, f"{label}[{index}]")
for index, item in enumerate(value)
)
def load_config(root: Path) -> HarnessConfig:
root = root.resolve()
config_path = root / ".harness" / "config.json"
if config_path.is_file():
try:
data = json.loads(config_path.read_text(encoding="utf-8"))
except UnicodeDecodeError as exc:
raise ConfigError(
f"{config_path}: configuration must be valid UTF-8: {exc}"
) from exc
except json.JSONDecodeError as exc:
raise ConfigError(f"{config_path}: {exc}") from exc
else:
data = {"version": 1}
data = _mapping(data, "config")
_reject_unknown(data, {"version", "projectType", "cmake", "msbuild", "tdd"}, "config")
if data.get("version") != 1 or isinstance(data.get("version"), bool):
raise ConfigError("version must be 1")
project_type = data.get("projectType", "auto")
if project_type not in {"auto", "cmake", "msbuild"}:
raise ConfigError("projectType must be auto, cmake, or msbuild")
cmake_data = _mapping(data.get("cmake", {}), "cmake")
_reject_unknown(
cmake_data,
{"sourceDir", "binaryDir", "configurePreset", "buildPreset", "testPreset"},
"cmake",
)
source_dir = _repo_path(
root, _nonempty_string(cmake_data.get("sourceDir", "."), "cmake.sourceDir"), "cmake.sourceDir"
)
binary_dir = None
if "binaryDir" in cmake_data:
binary_dir = _repo_path(
root, _nonempty_string(cmake_data["binaryDir"], "cmake.binaryDir"), "cmake.binaryDir"
)
preset_keys = ("configurePreset", "buildPreset", "testPreset")
if any(key in cmake_data for key in preset_keys) and not (
all(key in cmake_data for key in preset_keys) and binary_dir is not None
):
raise ConfigError("cmake configurePreset, buildPreset, testPreset, and binaryDir must be specified together")
configure_preset = (
_nonempty_string(cmake_data["configurePreset"], "cmake.configurePreset")
if "configurePreset" in cmake_data
else None
)
build_preset = (
_nonempty_string(cmake_data["buildPreset"], "cmake.buildPreset")
if "buildPreset" in cmake_data
else None
)
test_preset = (
_nonempty_string(cmake_data["testPreset"], "cmake.testPreset")
if "testPreset" in cmake_data
else None
)
msbuild_data = _mapping(data.get("msbuild", {}), "msbuild")
_reject_unknown(msbuild_data, {"solution", "configuration", "platform", "testCommand"}, "msbuild")
solution = None
if "solution" in msbuild_data:
solution = _repo_path(
root, _nonempty_string(msbuild_data["solution"], "msbuild.solution"), "msbuild.solution"
)
configuration = _nonempty_string(msbuild_data.get("configuration", "Debug"), "msbuild.configuration")
platform = _nonempty_string(msbuild_data.get("platform", "x64"), "msbuild.platform")
test_command = (
_string_array(msbuild_data["testCommand"], "msbuild.testCommand")
if "testCommand" in msbuild_data
else None
)
if "tdd" in data:
print("Deprecated config field 'tdd' is ignored; it may be removed.", file=sys.stderr)
return HarnessConfig(
version=1,
project_type=project_type,
cmake=CMakeConfig(source_dir, binary_dir, configure_preset, build_preset, test_preset),
msbuild=MSBuildConfig(solution, configuration, platform, test_command),
)
-70
View File
@@ -1,70 +0,0 @@
from pathlib import Path
from .models import DiscoveryResult, HarnessConfig, ProjectKind, ProjectSelection
class DiscoveryError(RuntimeError):
pass
def _cmake_entry(source: Path) -> Path | None:
for name in ("CMakePresets.json", "CMakeUserPresets.json", "CMakeLists.txt"):
candidate = source / name
if candidate.is_file():
return candidate
return None
def _single_msbuild(root: Path, explicit: Path | None) -> Path:
if explicit is not None:
if explicit.is_file() and explicit.suffix.lower() in {".sln", ".vcxproj"}:
return explicit
raise DiscoveryError(f"configured MSBuild project is invalid: {explicit}")
solutions = sorted(root.glob("*.sln"))
if len(solutions) > 1:
raise DiscoveryError("multiple .sln files found; set msbuild.solution")
if solutions:
return solutions[0]
projects = sorted(root.glob("*.vcxproj"))
if len(projects) > 1:
raise DiscoveryError("multiple .vcxproj files found; set msbuild.solution")
if projects:
return projects[0]
raise DiscoveryError("no root .sln or .vcxproj was found")
def discover_project(root: Path, config: HarnessConfig) -> DiscoveryResult:
root = root.resolve()
if config.project_type == "cmake":
entry = _cmake_entry(config.cmake.source_dir)
if entry is None:
raise DiscoveryError(
f"no CMake project found in {config.cmake.source_dir}"
)
return DiscoveryResult(ProjectSelection(ProjectKind.CMAKE, entry))
if config.project_type == "msbuild":
project = _single_msbuild(root, config.msbuild.solution)
return DiscoveryResult(ProjectSelection(ProjectKind.MSBUILD, project))
entry = _cmake_entry(config.cmake.source_dir)
if entry is not None:
return DiscoveryResult(ProjectSelection(ProjectKind.CMAKE, entry))
solutions = sorted(root.glob("*.sln"))
if len(solutions) > 1:
raise DiscoveryError("multiple .sln files found; set msbuild.solution")
if solutions:
return DiscoveryResult(
ProjectSelection(ProjectKind.MSBUILD, solutions[0])
)
projects = sorted(root.glob("*.vcxproj"))
if len(projects) > 1:
raise DiscoveryError("multiple .vcxproj files found; set msbuild.solution")
if projects:
return DiscoveryResult(
ProjectSelection(ProjectKind.MSBUILD, projects[0])
)
return DiscoveryResult(None)
-91
View File
@@ -1,91 +0,0 @@
from dataclasses import dataclass
from enum import Enum
from pathlib import Path
class ProjectKind(Enum):
CMAKE = "cmake"
MSBUILD = "msbuild"
class ResultCheckKind(Enum):
CMAKE_COMPILER_IS_MSVC = "cmake_compiler_is_msvc"
CTEST_HAS_TESTS = "ctest_has_tests"
@dataclass(frozen=True)
class CMakeConfig:
source_dir: Path
binary_dir: Path | None = None
configure_preset: str | None = None
build_preset: str | None = None
test_preset: str | None = None
@dataclass(frozen=True)
class MSBuildConfig:
solution: Path | None = None
configuration: str = "Debug"
platform: str = "x64"
test_command: tuple[str, ...] | None = None
@dataclass(frozen=True)
class HarnessConfig:
version: int
project_type: str
cmake: CMakeConfig
msbuild: MSBuildConfig
@dataclass(frozen=True)
class ProjectSelection:
kind: ProjectKind
project_file: Path
@dataclass(frozen=True)
class DiscoveryResult:
selection: ProjectSelection | None
@dataclass(frozen=True)
class Toolchain:
installation: Path
msbuild: Path
cmake: Path | None = None
ctest: Path | None = None
@dataclass(frozen=True)
class CommandSpec:
argv: tuple[str, ...]
cwd: Path
stage: str
timeout_seconds: int = 7200
@dataclass(frozen=True)
class ResultCheck:
kind: ResultCheckKind
path: Path | None = None
@dataclass(frozen=True)
class ValidationStep:
command: CommandSpec
checks: tuple[ResultCheck, ...] = ()
@dataclass(frozen=True)
class ValidationPlan:
project_kind: ProjectKind
steps: tuple[ValidationStep, ...]
@dataclass(frozen=True)
class CommandResult:
command: CommandSpec
returncode: int
stdout: str
stderr: str
-140
View File
@@ -1,140 +0,0 @@
import json
import locale
import re
import subprocess
import time
from .models import CommandResult, ResultCheckKind
DIAGNOSTIC_LIMIT = 8000
class ValidationFailure(RuntimeError):
pass
def _decode(value):
if value is None:
return ""
if isinstance(value, str):
return value
for encoding in ("utf-8", locale.getpreferredencoding(False)):
try:
return value.decode(encoding)
except (UnicodeDecodeError, LookupError):
continue
return value.decode("utf-8", errors="replace")
def _check_result(check, result):
if check.kind is ResultCheckKind.CMAKE_COMPILER_IS_MSVC:
if check.path is None:
raise ValidationFailure("MSVC check is missing binaryDir")
files = sorted(check.path.glob("CMakeFiles/*/CMakeCXXCompiler.cmake"))
if not files:
raise ValidationFailure("CMake did not generate compiler metadata")
text = files[-1].read_text(encoding="utf-8", errors="replace")
if not re.search(
r'set\s*\(\s*CMAKE_CXX_COMPILER_ID\s+"MSVC"\s*\)', text
):
raise ValidationFailure("CMake selected a compiler other than MSVC")
elif check.kind is ResultCheckKind.CTEST_HAS_TESTS:
try:
payload = json.loads(result.stdout)
except json.JSONDecodeError as exc:
raise ValidationFailure("CTest discovery did not return JSON") from exc
if not isinstance(payload, dict):
raise ValidationFailure("CTest discovery JSON must be an object")
if not isinstance(payload.get("tests"), list) or not payload["tests"]:
raise ValidationFailure("CTest discovered no tests")
def _diagnostic(stdout, stderr):
diagnostic = (stdout + "\n" + stderr).rstrip()[-DIAGNOSTIC_LIMIT:]
return diagnostic.replace("\r\n", " | ").replace("\n", " | ")
def _command_context(argv, cwd):
return f"argv={list(argv)!r}; cwd={str(cwd)!r}"
def execute_plan(
plan,
root,
*,
env=None,
total_timeout_seconds=7200,
deadline=None,
run=subprocess.run,
clock=time.monotonic,
):
root = root.resolve()
if deadline is None:
deadline = clock() + total_timeout_seconds
results = []
for step in plan.steps:
cwd = step.command.cwd.resolve()
context = _command_context(step.command.argv, cwd)
try:
cwd.relative_to(root)
except ValueError as exc:
raise ValidationFailure(
f"{step.command.stage} working directory is outside the repository; "
f"{context}"
) from exc
remaining = deadline - clock()
if remaining <= 0:
raise ValidationFailure(
f"{step.command.stage} timed out before it could start; {context}"
)
timeout = min(step.command.timeout_seconds, remaining)
try:
completed = run(
list(step.command.argv),
cwd=cwd,
env=env,
capture_output=True,
shell=False,
timeout=timeout,
)
except subprocess.TimeoutExpired as exc:
stdout = _decode(exc.output)
stderr = _decode(exc.stderr)
diagnostic = _diagnostic(stdout, stderr)
message = (
f"{step.command.stage} timed out after {timeout} seconds; {context}"
)
if diagnostic:
message = f"{message}; output tail: {diagnostic}"
raise ValidationFailure(message) from exc
stdout = _decode(completed.stdout)
stderr = _decode(completed.stderr)
result = CommandResult(step.command, completed.returncode, stdout, stderr)
if completed.returncode != 0:
diagnostic = _diagnostic(stdout, stderr)
message = (
f"{step.command.stage} failed with exit code {completed.returncode}; "
f"{context}"
)
if diagnostic:
message = f"{message}; output tail: {diagnostic}"
raise ValidationFailure(message)
try:
for check in step.checks:
_check_result(check, result)
except ValidationFailure as exc:
diagnostic = _diagnostic(stdout, stderr)
message = (
f"{step.command.stage} result check failed: {exc}; {context}"
)
if diagnostic:
message = f"{message}; output tail: {diagnostic}"
raise ValidationFailure(message) from exc
results.append(result)
return tuple(results)
-169
View File
@@ -1,169 +0,0 @@
import os
import shutil
import subprocess
import time
from pathlib import Path
from .models import ProjectKind, Toolchain
VC_WORKLOAD = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"
VS_CMAKE_RELATIVE = Path(
"Common7/IDE/CommonExtensions/Microsoft/CMake/CMake/bin"
)
class ToolchainError(RuntimeError):
pass
def _decode(value):
if isinstance(value, str):
return value
return (value or b"").decode("utf-8", errors="replace")
def _first_path(result, label, argv, cwd):
text = _decode(result.stdout).strip()
if result.returncode != 0:
diagnostic = (
_decode(result.stdout) + "\n" + _decode(result.stderr)
).rstrip()[-8000:]
diagnostic = diagnostic.replace("\r\n", " | ").replace("\n", " | ")
message = (
f"{label} probe failed with exit code {result.returncode}; "
f"argv={argv!r}; cwd={str(cwd)!r}"
)
if diagnostic:
message = f"{message}; output tail: {diagnostic}"
raise ToolchainError(
f"{message}; install Visual Studio Desktop development with C++"
)
if not text:
raise ToolchainError(
f"{label} probe returned no path; argv={argv!r}; cwd={str(cwd)!r}; "
"install Visual Studio Desktop development with C++"
)
path = Path(text.splitlines()[0]).resolve()
if not path.exists():
diagnostic = (
_decode(result.stdout) + "\n" + _decode(result.stderr)
).rstrip()[-8000:]
diagnostic = diagnostic.replace("\r\n", " | ").replace("\n", " | ")
message = (
f"{label} probe returned nonexistent path with exit code 0; "
f"argv={argv!r}; cwd={str(cwd)!r}"
)
if diagnostic:
message = f"{message}; output tail: {diagnostic}"
raise ToolchainError(
f"{message}; install or repair Visual Studio Desktop development with C++"
)
return path
def _require_path(path, label):
if path is None or not path.exists():
raise ToolchainError(f"{label} not found")
return path.resolve()
def _remaining(deadline, clock, label):
if deadline is None:
return None
remaining = deadline - clock()
if remaining <= 0:
raise ToolchainError(f"{label} probe timed out before it could start")
return remaining
def _probe(run, argv, label, *, deadline, clock):
cwd = Path.cwd().resolve()
kwargs = {
"cwd": cwd,
"capture_output": True,
"check": False,
"shell": False,
}
remaining = _remaining(deadline, clock, label)
if remaining is not None:
kwargs["timeout"] = remaining
try:
return run(argv, **kwargs)
except subprocess.TimeoutExpired as exc:
raise ToolchainError(
f"{label} probe timed out; argv={argv!r}; cwd={str(cwd)!r}; "
"install or repair Visual Studio Desktop development with C++"
) from exc
def discover_toolchain(
kind: ProjectKind,
*,
env=None,
which=shutil.which,
run=subprocess.run,
deadline=None,
clock=time.monotonic,
) -> Toolchain:
environment = os.environ if env is None else env
vswhere_path = which("vswhere.exe")
if vswhere_path is None:
program_files = environment.get("ProgramFiles(x86)")
if program_files:
candidate = Path(program_files) / "Microsoft Visual Studio/Installer/vswhere.exe"
vswhere_path = str(candidate) if candidate.exists() else None
vswhere = _require_path(
Path(vswhere_path) if vswhere_path else None,
"vswhere.exe; install Visual Studio Desktop development with C++",
)
install_argv = [
str(vswhere), "-latest", "-products", "*", "-requires", VC_WORKLOAD,
"-property", "installationPath",
]
install_result = _probe(
run,
install_argv,
"Visual Studio installation",
deadline=deadline,
clock=clock,
)
probe_cwd = Path.cwd().resolve()
installation = _require_path(
_first_path(
install_result,
"Visual Studio installation",
install_argv,
probe_cwd,
),
"Visual Studio installation",
)
msbuild_argv = [
str(vswhere), "-latest", "-products", "*", "-requires", VC_WORKLOAD,
"-find", r"MSBuild\**\Bin\MSBuild.exe",
]
msbuild_result = _probe(
run,
msbuild_argv,
"MSBuild",
deadline=deadline,
clock=clock,
)
msbuild = _require_path(
_first_path(msbuild_result, "MSBuild", msbuild_argv, probe_cwd),
"MSBuild.exe",
)
if kind is not ProjectKind.CMAKE:
return Toolchain(installation=installation, msbuild=msbuild)
bundled_cmake_dir = installation / VS_CMAKE_RELATIVE
cmake = _require_path(
Path(which("cmake.exe")) if which("cmake.exe") else bundled_cmake_dir / "cmake.exe",
"cmake.exe",
)
ctest = _require_path(
Path(which("ctest.exe")) if which("ctest.exe") else bundled_cmake_dir / "ctest.exe",
"ctest.exe",
)
return Toolchain(installation=installation, msbuild=msbuild, cmake=cmake, ctest=ctest)
-306
View File
@@ -1,306 +0,0 @@
"""Measure whole native processes; reference comparison remains a separate step."""
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
import math
import os
from pathlib import Path
import platform
import shutil
import subprocess
import time
ROOT = Path(__file__).resolve().parents[2]
CASES = (
("hexa_nonlinearstatic", ROOT / "reference/hexa/hexa_nonlinearstatic.inp", 2.0),
("hexa_dynnonlinear", ROOT / "reference/hexa/hexa_dynnonlinear.inp", 100.0),
("shellNL-dyn-meshfine", ROOT / "reference/shellNL-dyn-meshfine/shellNL-dyn-meshfine.inp", 100.0),
)
THREAD_KEYS = ("MKL_NUM_THREADS", "MKL_DYNAMIC", "MKL_THREADING_LAYER", "MKL_CBWR",
"TBB_NUM_THREADS", "OMP_NUM_THREADS", "OMP_DYNAMIC", "OMP_PROC_BIND",
"OMP_PLACES", "KMP_AFFINITY", "KMP_BLOCKTIME", "FESA_NUM_THREADS")
def sha256(path):
if not path.is_file():
return None
digest = hashlib.sha256()
with path.open("rb") as stream:
for block in iter(lambda: stream.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def publish(path, value):
"""Expose only a fully serialized record; retain the last closed report."""
temporary = path.with_suffix(path.suffix + ".tmp")
temporary.write_text(json.dumps(value, indent=2, allow_nan=False) + "\n", encoding="utf-8")
os.replace(temporary, path)
def command_output(command):
try:
result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True,
errors="replace", timeout=30, check=False)
return {"command": command, "exit": result.returncode,
"output": (result.stdout + result.stderr).strip()}
except (OSError, subprocess.TimeoutExpired) as error:
return {"command": command, "exit": None, "error": str(error)}
def collect_environment(exe):
"""Capture actual build/runtime evidence outside every timed interval."""
build = next((p for p in exe.parents if (p / "CMakeCache.txt").is_file()), None)
cache = (build / "CMakeCache.txt").read_text(errors="replace") if build else None
result = {
"source_commit": command_output(["git", "rev-parse", "HEAD"]),
"source_status": command_output(["git", "status", "--porcelain"]),
"measurement_tool_sha256": sha256(Path(__file__)),
"comparator_sha256": {name: sha256(ROOT / "scripts/verification" / name)
for name in ("compare_hex8.py", "compare_hht_dynamic.py")},
"source_diff_sha256": hashlib.sha256(command_output(["git", "diff", "HEAD", "--", "include", "src", "cmake", "CMakeLists.txt", "CMakePresets.json"])["output"].encode()).hexdigest(),
"exe": str(exe), "exe_sha256": sha256(exe),
"os": platform.platform(), "cpu": platform.processor(),
"logical_processors": os.cpu_count(),
"thread_environment": {key: os.environ.get(key) for key in THREAD_KEYS},
"related_environment": {key: value for key, value in sorted(os.environ.items())
if key.startswith(("MKL_", "TBB_", "OMP_", "KMP_", "FESA_"))},
"runtime_path": os.environ.get("PATH"),
"thread_settings_note": "Environment requests; TBB uses runtime default (TBB_NUM_THREADS is not an enforced cap). Actual thread use requires profile evidence.",
"build_type_flags_dependencies": cache,
"build_files": {str(p): sha256(p) for p in ((build / "CMakeCache.txt", build / "build.ninja") if build else ())},
"runtime_dlls": {p.name: sha256(p) for p in sorted(exe.parent.glob("*.dll"))},
"compiler": command_output(["cl"]),
"vtune": command_output([os.environ.get("VTUNE_EXE", "vtune"), "-version"]),
}
if os.name == "nt":
import ctypes
class PowerStatus(ctypes.Structure):
_fields_ = [("ac_line", ctypes.c_ubyte), ("battery_flag", ctypes.c_ubyte),
("battery_percent", ctypes.c_ubyte), ("reserved", ctypes.c_ubyte),
("battery_life", ctypes.c_ulong), ("battery_full_life", ctypes.c_ulong)]
power = PowerStatus()
obtained = bool(ctypes.windll.kernel32.GetSystemPowerStatus(ctypes.byref(power)))
result["system_power"] = {"obtained": obtained,
"ac_line_status": power.ac_line if obtained else None,
"battery_flag": power.battery_flag if obtained else None}
result["power_scheme"] = command_output(["powercfg", "/getactivescheme"])
machine = command_output([
"powershell", "-NoProfile", "-Command",
"ConvertTo-Json -Depth 4 @{boot=(Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToString('o'); cpu=(Get-CimInstance Win32_Processor | Select-Object Name,NumberOfCores,NumberOfLogicalProcessors); battery=(Get-CimInstance Win32_Battery | Select-Object BatteryStatus); runtime=(Get-ChildItem -LiteralPath '"
+ str(exe.parent).replace("'", "''")
+ "' -Filter *.dll | ForEach-Object { @{name=$_.Name;version=$_.VersionInfo.FileVersion} })}"])
if machine.get("exit") == 0:
machine["output"] = json.loads(machine["output"])
result["machine_power_runtime_versions"] = machine
return result
def validate_result(case, scratch, result):
"""Reuse established inventory/finite prechecks without claiming comparison."""
name = "compare_hex8" if case.startswith("hexa_") else "compare_hht_dynamic"
spec = importlib.util.spec_from_file_location(name, ROOT / f"scripts/verification/{name}.py")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
if case.startswith("hexa_"):
model = module.input_inventory(scratch, case)
values = module.read_results(result, model)
module.check_result_values(values, model)
return {**values["evidence"], "physical_values_checked": True}
nodes, identity = module.input_inventory(scratch)
_, evidence = module.read_results(result, nodes, identity)
import h5py
with h5py.File(result, "r") as file:
if int(file["metadata"].attrs["schema_version"]) != 2:
raise ValueError("Shell performance requires schema v2")
source = file["metadata"].attrs["source_input_identity"]
if isinstance(source, bytes):
source = source.decode()
if source != f"path={scratch.absolute().as_posix()};content_identity={identity}":
raise ValueError("Shell result scratch path identity mismatch")
return evidence
def run_once(exe, original, case, directory, index, timeout=None):
directory.mkdir(parents=True, exist_ok=False)
scratch = directory / original.name
original_before = sha256(original)
shutil.copyfile(original, scratch)
scratch_before = sha256(scratch)
if original_before != scratch_before:
raise ValueError("Input copy differs from source bytes")
result = scratch.with_name(scratch.stem + "_results.h5")
native_path, stdout_path = directory / "native.json", directory / "stdout.txt"
command = [str(exe), str(scratch)]
state, native_exit, error, process = "completed", None, None, None
with stdout_path.open("wb") as output:
start = time.perf_counter()
try:
process = subprocess.Popen(command, cwd=directory, stdout=output, stderr=subprocess.STDOUT)
native_exit = process.wait(timeout=timeout)
except OSError as exception:
state, error = "launch-failed", str(exception)
except (KeyboardInterrupt, subprocess.TimeoutExpired) as exception:
state = "interrupted" if isinstance(exception, KeyboardInterrupt) else "timed-out"
error = str(exception)
if process is not None:
process.kill()
process.communicate()
native_exit = process.returncode
finally:
elapsed = time.perf_counter() - start
native = {
"case": case, "command": command, "cwd": str(directory), "native_exit": native_exit,
"execution_state": state, "original_sha256": original_before,
"scratch_sha256": scratch_before, "result_sha256": sha256(result),
"source_unchanged": sha256(original) == original_before,
"scratch_unchanged": sha256(scratch) == scratch_before, "result_exists": result.is_file(),
"original_sha256_after": sha256(original), "scratch_sha256_after": sha256(scratch),
"exe_sha256": sha256(exe), "elapsed_seconds": elapsed,
}
publish(native_path, native)
valid = (state == "completed" and native_exit == 0 and native["result_exists"]
and native["source_unchanged"] and native["scratch_unchanged"]
and math.isfinite(elapsed) and elapsed >= 0)
evidence, validation_state = None, "not-run"
if valid:
try:
evidence = validate_result(case, scratch, result)
validation_state = "passed"
except KeyboardInterrupt:
valid, validation_state = False, "interrupted"
error = "Result validation interrupted after native completion"
except Exception as exception:
valid, validation_state, error = False, "failed", str(exception)
row = {**native, "kind": "measurement",
"measurement_index": index, "original_input": str(original), "scratch_input": str(scratch),
"result": str(result), "native_record": str(native_path), "stdout_log": str(stdout_path),
"native_record_sha256": sha256(native_path),
"solver_log": str(scratch.with_name(scratch.stem + "_log.txt")),
"comparison_status": "pending", "comparison_report": None,
"execution_valid": valid, "result_precheck": evidence,
"result_validation_state": validation_state, "error": error}
publish(directory / "run.json", row)
return row
def measure(exe, output_dir, cases=CASES):
if output_dir.exists() and any(output_dir.iterdir()):
raise FileExistsError(f"Nonempty artifact directory: {output_dir}")
output_dir.mkdir(parents=True, exist_ok=True)
environment = collect_environment(exe)
report = {"schema": "fesa-performance-v1", "environment": environment, "cases": [],
"protocol": {"warmup_runs": 0, "measurement_runs": 1,
"cache_preparation": "no dedicated warmup or cache reset"},
"execution_complete": False}
path = output_dir / "measurements.json"
publish(path, report)
for case, original, target in cases:
case_report = {"case": case, "target_seconds": target, "runs": [],
"all_measured_runs_within_target": False}
report["cases"].append(case_report)
if collect_environment(exe) != environment:
report["error"] = "Source, binary, build or environment changed during measurement"
publish(path, report)
return 1
row = run_once(exe, original, case, output_dir / case / "run-1", 1)
native_valid = row["execution_valid"]
row["execution_valid"], row["within_target"] = False, False
row["environment_validation_state"] = "pending"
case_report["runs"].append(row)
publish(Path(row["native_record"]).parent / "run.json", row)
publish(path, report)
try:
unchanged = collect_environment(exe) == environment
except KeyboardInterrupt:
row["environment_validation_state"] = "interrupted"
row["error"] = "Environment validation interrupted after native completion"
except Exception as error:
row["environment_validation_state"] = "failed"
row["error"] = f"Environment validation failed after native completion: {error}"
else:
row["environment_validation_state"] = "passed" if unchanged else "changed"
row["execution_valid"] = native_valid and unchanged
row["within_target"] = row["execution_valid"] and row["elapsed_seconds"] <= target
if not unchanged:
row["error"] = "Source, binary, build or environment changed during native execution"
publish(Path(row["native_record"]).parent / "run.json", row)
case_report["all_measured_runs_within_target"] = row["within_target"]
publish(path, report)
print(f"{case} measurement 1: {row['elapsed_seconds']:.6f}s exit={row['native_exit']} valid={row['execution_valid']}", flush=True)
if not row["execution_valid"]:
return 1
report["execution_complete"] = True
publish(path, report)
return 0
def link_comparisons(output_dir):
"""Attach closed comparator reports by hashes without changing raw timing."""
path = output_dir / "measurements.json"
report = json.loads(path.read_text(encoding="utf-8"))
for case in report["cases"]:
for row in case["runs"]:
comparison_path = Path(row["native_record"]).parent / "comparison.json"
if not comparison_path.is_file():
continue
comparison = json.loads(comparison_path.read_text(encoding="utf-8"))
identities = comparison.get("identities", {})
native_path = Path(row["native_record"])
native = json.loads(native_path.read_text(encoding="utf-8"))
if (sha256(native_path) != row["native_record_sha256"]
or row["elapsed_seconds"] != native["elapsed_seconds"]
or sha256(Path(row["scratch_input"])) != row["scratch_sha256"]
or sha256(Path(row["result"])) != row["result_sha256"]
or identities.get("input", {}).get("sha256") != row["scratch_sha256"]
or identities.get("results", {}).get("sha256") != row["result_sha256"]
or comparison.get("case") != case["case"]):
raise ValueError("Comparison does not identify the recorded native run/result")
if case["case"].startswith("hexa_") and comparison.get("blocking_passed") and (
comparison.get("execution", {}).get("record_sha256") != row["native_record_sha256"]):
raise ValueError("Hex8 comparator did not verify this execution record")
row["comparison_status"] = "passed" if (
row["execution_valid"] and comparison.get("precheck", {}).get("passed")
and comparison.get("blocking_passed") and comparison.get("verdict") == "passed"
) else "failed"
row["comparison_report"] = str(comparison_path)
row["comparison_report_sha256"] = sha256(comparison_path)
report["comparisons_complete"] = (report["execution_complete"] and all(
row["comparison_status"] != "pending" for case in report["cases"] for row in case["runs"]))
report["all_comparisons_passed"] = (report["comparisons_complete"] and all(
row["comparison_status"] == "passed" for case in report["cases"] for row in case["runs"]))
publish(path, report)
return 0
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--exe", type=Path)
parser.add_argument("--output-dir", required=True, type=Path)
parser.add_argument("--link-comparisons", action="store_true",
help="Attach existing per-run comparison.json by native/input/result hashes.")
args = parser.parse_args()
if args.link_comparisons:
try:
return link_comparisons(args.output_dir.resolve())
except (OSError, ValueError, KeyError) as error:
print(f"comparison link failed: {error}", flush=True)
return 1
if args.exe is None:
parser.error("--exe is required for measurement")
if not args.exe.is_file():
parser.error("--exe must be an existing executable")
if args.output_dir.exists() and (not args.output_dir.is_dir() or any(args.output_dir.iterdir())):
parser.error("--output-dir must be new or empty")
try:
return measure(args.exe.resolve(), args.output_dir.resolve())
except (OSError, ValueError, KeyboardInterrupt) as error:
print(f"measurement failed: {error}", flush=True)
return 1
if __name__ == "__main__":
raise SystemExit(main())
-14
View File
@@ -1,14 +0,0 @@
# 외부 결과 비교 도구
이 영역은 승인된 reference와 FESA HDF5의 deterministic comparison을 소유한다.
솔버 실행, reference 생성·수정과 물리식 채택을 소유하지 않는다.
- 비교 전 필요한 source instance·label·component·step/frame을 일대일 대응한다.
누락·추가·중복·nonfinite와 schema 오류를 tolerance로 무시하지 않는다.
- Reference 값만으로 family scale을 만들고 approved near-zero·row·RMS·zero-scale 규칙을
적용한다. 임의 absolute floor·값 보정·nearest-time matching을 추가하지 않는다.
- 현재 HHT 비교기는 지정된 729-node 모델과 101 frames의 선택 절점 U/UR용이다.
다른 모델을 인자로 받는다는 이유로 범용 comparator라 설명하지 않는다.
- U blocking과 UR warning-only를 유지하고 warning도 결정적인 순서로 보고한다.
- Identity 변형·frame 누락·비유한 값·tolerance 경계·zero scale·report 재현성을 검증한다.
변위 비교 성공을 가속도·에너지·시간수렴 검증으로 확대하지 않는다.
-807
View File
@@ -1,807 +0,0 @@
"""Compare original Hex8 CSV/TXT results with Solid HDF5; never launch a solver.
This entry point is intentionally restricted to reference/hexa's fixed original
models. Identity/schema checks precede finite checks and the common tolerance.
Rounded HDF5 observables are compared; internal high+low solver gates are not
recomputed from them. Missing original modal Value data remain NOT RUN.
"""
import argparse
import csv
from decimal import Decimal, InvalidOperation
import hashlib
import json
import math
import os
from pathlib import Path
import re
from typing import Any
import h5py
import numpy as np
ROOT = Path(__file__).resolve().parents[2]
REFERENCE_DIRECTORY = ROOT / "reference/hexa"
STEP = "Step-1"
INSTANCE = "PART-1-1"
NODE_COUNT = 442
ELEMENT_COUNT = 300
POLICY = {"relative_tolerance": 0.05, "relative_rms_tolerance": 0.01,
"near_zero_ratio": 0.01, "zero_scale": "exact-zero"}
CASES = {
"hexa_linearstatic": "linear-static",
"hexa_nonlinearstatic": "nonlinear-static-full-newton",
"hexa_dynlinear": "hht-linear-dynamics",
"hexa_dynnonlinear": "hht-nonlinear-dynamics",
"hexa_freq": "linear-frequency",
}
# Working-tree bytes (Windows CRLF), not Git's normalized LF blobs.
SOURCE_SHA256 = {
"hexa_dynlinear.inp": "f8db81b8ea56b689ab0ad892e6a84b7c6a0162c9d3ff95328ea99722f3de078e",
"hexa_dynlinear_U.csv": "623c20489e8cc18d23fa1728563ce5a26059f321861514ef8569e3d204ef6cb5",
"hexa_dynnonlinear.inp": "1685f86f844e8020bf46e1747b949699ac9aff0d7570bd21ec95109399365ff9",
"hexa_dynnonlinear_U.csv": "f5832a24577f54838bb49dec86a0dc9668cf43e78953f718a9d3635e0f574226",
"hexa_freq.inp": "42cbcec2a8b738f8b6d2c1e13114b8840dccdde6fc3e6f8a43a3272e85d82347",
"hexa_freq.txt": "3d2dacbee13cd2cc8d3f7c1a38210611a72de41d5e31d169e636bf40e00d4370",
"hexa_linearstatic.inp": "67183909c5dc962a4d4b65ffb81f7bd65b72e89a5d015c8369a8e0226347a647",
"hexa_linearstatic_RF.csv": "3e04e71ec075ffef098cf4870d77f9c400557fccb375369da8bf67cf67c1e36b",
"hexa_linearstatic_S.csv": "8840a502b9d28a91e5685349301a56fed37d2ac62327b71304716b714db577e8",
"hexa_linearstatic_U.csv": "1ff49aea3ec601cfd796d6a2fb6a4f91be3fc3de9426cdf3744d981216d4bd5f",
"hexa_nonlinearstatic.inp": "7858254025b96c6bfe3fb6498605ecf9c473425c2bc3593f5fbdc6902fd7c6cb",
"hexa_nonlinearstatic_RF.csv": "3ba67a4fb75b2ce3edee13d35b2ea66ac7d52fc16e612c086ed1122a834fee7b",
"hexa_nonlinearstatic_S.csv": "a8866a730f0d65b1e3921d504d255b72158b4a2b9ee30fc691712d94982b07ba",
"hexa_nonlinearstatic_U.csv": "bd6c2fe2b2bb2ffb0bc2ea52f67513c3967a396b5707bbebc9533fde7be25ac6",
}
BASE_COLUMNS = ["ODB Name", "Step", "Frame", "Part Instance Name"]
TAIL_COLUMNS = ["X", "Y", "Z", "Section Name", "Material Name", "Section Point"]
COMPONENTS = {"U": ["U1", "U2", "U3"], "RF": ["RF1", "RF2", "RF3"],
"S": ["S11", "S22", "S33", "S12", "S13", "S23"]}
STRESS_DERIVED = ["Mises", "Max. Principal", "Max. Principal (Abs)",
"Mid. Principal", "Min. Principal", "Tresca", "Pressure", "Third Invariant"]
NATURAL_NODES = np.array([[-1, -1, -1], [1, -1, -1], [1, 1, -1], [-1, 1, -1],
[-1, -1, 1], [1, -1, 1], [1, 1, 1], [-1, 1, 1]])
NATURAL_POINTS = np.array([[-1, -1, -1], [1, -1, -1], [-1, 1, -1], [1, 1, -1],
[-1, -1, 1], [1, -1, 1], [-1, 1, 1], [1, 1, 1]]) / math.sqrt(3.0)
FRAME_PATTERN = re.compile(
r"Increment\s+(\d+):\s*Step Time\s*=\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)"
)
class ComparisonError(ValueError):
def __init__(self, code: str, message: str):
super().__init__(message)
self.code = code
def require(condition: bool, code: str, message: str):
if not condition:
raise ComparisonError(code, message)
def canonical(value: str) -> str:
"""Abaqus ASCII case folding only; distinct instances never coalesce."""
return value.translate(str.maketrans("abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"))
def label(value: str) -> int:
require(re.fullmatch(r"[0-9]+", value) is not None, "invalid-source-label", value)
number = int(value)
require(0 < number <= 2**63 - 1, "invalid-source-label", value)
return number
def lexical_absolute(path: Path) -> str:
"""Match C++ absolute.lexically_normal without resolving physical aliases."""
return Path(os.path.abspath(path)).as_posix()
def sha256(path: Path) -> str:
with path.open("rb") as stream:
return hashlib.file_digest(stream, "sha256").hexdigest()
def fnv1a64(raw: bytes) -> str:
value = 14695981039346656037
for byte in raw:
value = ((value ^ byte) * 1099511628211) & (2**64 - 1)
return f"fnv1a64:{value:016x}"
def input_inventory(path: Path, case: str) -> dict[str, Any]:
"""Read only the frozen single-part identity-instance original subset."""
require(case in CASES, "unknown-source-case", case)
require(sha256(path) == SOURCE_SHA256[f"{case}.inp"], "source-input-hash-mismatch", str(path))
nodes, elements, sets, boundaries = {}, {}, {}, []
keyword, options, in_assembly = "", {}, False
model = {"case": case, "nodes": nodes, "elements": elements, "node_sets": sets,
"section_line": None, "source_path": lexical_absolute(path),
"content_identity": fnv1a64(path.read_bytes())}
for line_number, raw in enumerate(path.read_text(encoding="utf-8-sig").splitlines(), 1):
line = raw.strip()
if not line or line.startswith("**"):
continue
if line.startswith("*"):
tokens = [token.strip() for token in line[1:].split(",")]
keyword = canonical(tokens[0])
options = {canonical(token.split("=", 1)[0]): token.split("=", 1)[1].strip()
for token in tokens[1:] if "=" in token}
if keyword == "ASSEMBLY":
in_assembly = True
elif keyword == "END ASSEMBLY":
in_assembly = False
elif keyword == "INSTANCE":
require(canonical(options["NAME"]) == INSTANCE and len(tokens) == 3,
"unsupported-source-instance", line)
elif keyword == "SOLID SECTION":
model["section_line"] = line_number
model["section_elset"] = options["ELSET"]
model["material"] = options["MATERIAL"]
elif keyword == "STEP":
require(options["NAME"] == STEP, "wrong-source-step", line)
continue
tokens = [token.strip() for token in line.split(",") if token.strip()]
if keyword == "NODE":
node = label(tokens[0])
require(node not in nodes and len(tokens) == 4, "source-node-inventory", line)
nodes[node] = tuple(float(value) for value in tokens[1:])
elif keyword == "ELEMENT":
element = label(tokens[0])
require(element not in elements and len(tokens) == 9 and options["TYPE"] == "C3D8",
"source-element-inventory", line)
elements[element] = tuple(label(value) for value in tokens[1:])
elif keyword == "NSET" and in_assembly:
name = canonical(options["NSET"])
require(canonical(options["INSTANCE"]) == INSTANCE, "wrong-source-instance", line)
sets.setdefault(name, []).extend(label(value) for value in tokens)
elif keyword == "BOUNDARY":
first, last = int(tokens[1]), int(tokens[2])
require(len(tokens) == 3 or float(tokens[3]) == 0.0, "unsupported-source-boundary", line)
boundaries.append((canonical(tokens[0]), first, last))
elif keyword == "DENSITY":
model["density"] = float(tokens[0])
elif keyword == "ELASTIC":
model["elastic"] = tuple(float(value) for value in tokens)
require(set(nodes) == set(range(1, NODE_COUNT + 1))
and set(elements) == set(range(1, ELEMENT_COUNT + 1)),
"source-model-inventory", "Expected original 442 nodes and 300 elements.")
constrained = set()
for name, first, last in boundaries:
require(name in sets and 1 <= first <= last <= 6, "source-boundary-identity", name)
constrained.update((node, component) for node in sets[name]
for component in range(first, min(last, 3) + 1))
model["constrained"] = constrained
return model
def point_coordinates(model, element: int, point: int):
xi = NATURAL_POINTS[point - 1]
shape = np.prod(1.0 + NATURAL_NODES * xi, axis=1) / 8.0
coordinates = np.array([model["nodes"][node] for node in model["elements"][element]])
terms = shape[:, None] * coordinates
return np.sum(terms, axis=0), np.sum(np.abs(terms), axis=0)
def read_reference(path: Path, case: str, quantity: str, model):
"""Validate all row identities before converting any physical values."""
require(quantity in COMPONENTS, "unknown-quantity", quantity)
dynamic = case.startswith("hexa_dyn")
require(not dynamic or quantity == "U", "wrong-reference-quantity", quantity)
extra = (["Element Label", "IntPt"] if quantity == "S" else ["Node Label"])
suffix = ([f"S-{name}" for name in STRESS_DERIVED] if quantity == "S"
else [f"{quantity}-Magnitude"])
headers = BASE_COLUMNS + extra + TAIL_COLUMNS + suffix + [f"{quantity}-{c}" for c in COMPONENTS[quantity]]
records = {}
with path.open(newline="", encoding="utf-8-sig") as stream:
reader = csv.reader(stream)
fields = [field.strip() for field in next(reader, [])]
require(len(fields) == len(set(fields)), "duplicate-reference-header", str(path))
require(set(fields) == set(headers), "wrong-reference-columns", str(fields))
for row_number, values in enumerate(reader, 2):
require(len(values) == len(fields), "wrong-reference-row-width", str(row_number))
row = dict(zip(fields, [value.strip() for value in values]))
require(row["ODB Name"].replace("\\", "/").rsplit("/", 1)[-1] == f"{case}.odb",
"wrong-reference-case", str(row_number))
require(row["Step"] == STEP, "wrong-reference-step", str(row_number))
require(canonical(row["Part Instance Name"]) == INSTANCE,
"wrong-reference-instance", str(row_number))
match = FRAME_PATTERN.fullmatch(row["Frame"])
require(match is not None, "invalid-reference-frame", str(row_number))
increment = int(match[1])
time = Decimal(match[2])
require(time.is_finite() and increment in (range(101) if dynamic else (10,))
and time == (Decimal(increment) / 10 if dynamic else Decimal(1)),
"wrong-reference-frame-time", str(row_number))
entity = label(row["Element Label" if quantity == "S" else "Node Label"])
point = label(row["IntPt"]) if quantity == "S" else 0
require(entity in model["elements" if quantity == "S" else "nodes"],
"extra-reference-entity", str(row_number))
require(quantity != "S" or 1 <= point <= 8, "wrong-reference-point", str(row_number))
require(row["Section Point"] == ""
and canonical(row["Material Name"]) == ("MATERIAL-1" if quantity == "S" else "NA")
and canonical(row["Section Name"]) == ("ASSEMBLY_PART-1-1_SET-1" if quantity == "S" else "NA"),
"wrong-reference-section", str(row_number))
key = (increment, entity, point)
require(key not in records, "duplicate-reference-identity", str(key))
records[key] = row
entities = model["elements" if quantity == "S" else "nodes"]
expected = {(n, entity, p) for n in (range(101) if dynamic else (10,))
for entity in entities for p in (range(1, 9) if quantity == "S" else (0,))}
require(records.keys() == expected, "reference-inventory", "Missing reference identities.")
return records
def reference_values(records, quantity, model):
result = {}
for key in sorted(records):
row = records[key]
values = [float(row[f"{quantity}-{component}"]) for component in COMPONENTS[quantity]]
coordinates = np.array([float(row[axis]) for axis in ("X", "Y", "Z")])
# Every exported numeric column is checked, including unused derived values.
derived = STRESS_DERIVED if quantity == "S" else ["Magnitude"]
other = [float(row[f"{quantity}-{name}"]) for name in derived]
require(all(math.isfinite(value) for value in values + other) and np.isfinite(coordinates).all(),
"nonfinite-reference-value", str(key))
if quantity == "S":
expected, scale = point_coordinates(model, key[1], key[2])
else:
expected = np.array(model["nodes"][key[1]])
scale = np.abs(expected)
budget = 16.0 * np.finfo(np.float32).eps * scale
require((np.abs(coordinates - expected) <= budget).all(),
"reference-coordinate-convention", str(key))
result[key] = values
return result
def evaluate_family(identity, result, reference):
require(len(result) == len(reference) == len(identity["rows"]) and len(result) > 0,
"scalar-inventory", identity["quantity"])
require(all(math.isfinite(value) for value in (*result, *reference)),
"nonfinite-comparison-value", "Comparison values must be finite.")
scale = max(abs(value) for value in reference)
band = POLICY["near_zero_ratio"] * scale
rows, error_norm = [], 0.0
for key, actual, expected in zip(identity["rows"], result, reference):
error = abs(actual - expected)
require(math.isfinite(error), "nonfinite-comparison-value", str(key))
error_norm = math.hypot(error_norm, error)
require(math.isfinite(error_norm), "nonfinite-comparison-value", str(key))
relative = None
if scale == 0.0:
branch, threshold, passed = "zero-scale-exact", 0.0, error == 0.0
elif abs(expected) <= band:
branch, threshold, passed = "near-zero", band, error <= band
else:
branch, threshold = "relative", POLICY["relative_tolerance"] * abs(expected)
relative = error / abs(expected)
require(math.isfinite(relative), "nonfinite-comparison-value", str(key))
passed = relative <= POLICY["relative_tolerance"]
rows.append({**key, "fesa_value": actual, "reference_value": expected,
"absolute_error": error, "relative_error": relative, "branch": branch,
"threshold": threshold, "passed": passed})
rows_passed = all(row["passed"] for row in rows)
absolute_rms = error_norm / math.sqrt(len(rows))
relative_rms = absolute_rms / scale if scale else None
require(relative_rms is None or math.isfinite(relative_rms), "nonfinite-comparison-value", "RMS")
rms_passed = rows_passed if not scale else relative_rms <= POLICY["relative_rms_tolerance"]
return {**{k: v for k, v in identity.items() if k != "rows"}, "impact": "blocking",
"row_count": len(rows), "reference_scale": scale, "near_zero_band": band,
"maximum_absolute_error": max(row["absolute_error"] for row in rows),
"absolute_rms": absolute_rms, "relative_rms": relative_rms,
"policy": dict(POLICY), "rows": rows, "rows_passed": rows_passed,
"rms_passed": rms_passed, "passed": rows_passed and rms_passed,
"diagnostic_code": "zero-reference-scale-nonzero-error" if not scale and not rows_passed else None}
def read_modal_reference(path: Path):
"""Read the supplied hexa_freq.txt Value=lambda, Freq=cycles/time format.
This artifact has no step field: its fixed case is bound to the sole Step-1
of the hash-checked hexa_freq.inp. Freq is finite auxiliary data; comparison
uses the printed Value directly, without frequency conversion or rounding.
"""
mapping = {}
for line in path.read_text(encoding="utf-8-sig").splitlines():
if not line.strip():
continue
match = re.fullmatch(
r"\s*Mode\s+([0-9]+):\s+Value\s*=\s*(\S+)\s+Freq\s*=\s*(\S+)\s+\(cycles/time\)\s*", line)
require(match is not None, "wrong-modal-reference-format", line)
mode = int(match[1])
require(mode in range(1, 11), "wrong-modal-mode", str(mode))
require(mode not in mapping, "duplicate-modal-mode", str(mode))
mapping[mode] = (match[2], match[3])
require(set(mapping) == set(range(1, 11)), "modal-mode-inventory", "Lowest ten modes required.")
rows = []
for mode in range(1, 11):
value, frequency = (float(token) for token in mapping[mode])
require(math.isfinite(value) and math.isfinite(frequency), "nonfinite-modal-reference", str(mode))
require(value >= 0 and frequency >= 0, "invalid-modal-reference-value", str(mode))
rows.append({"step": STEP, "mode": mode, "quantity": "eigenvalue",
"unit_dimensions": "1/time^2", "value": value})
require(all(a["value"] <= b["value"] for a, b in zip(rows, rows[1:])),
"wrong-modal-order", str(path))
return rows
def compare_modal(reference, result, quantity: str, unit: str):
"""Compare explicitly typed modal rows using the common tolerance.
Each row explicitly supplies step, mode, quantity, unit_dimensions and value.
Callers must obtain those semantics from a genuine supplied artifact first.
Eigenvalues and frequencies are never silently converted into one another.
"""
units = {"eigenvalue": "1/time^2", "frequency": "1/time"}
require(quantity in units and unit == units[quantity], "wrong-modal-unit", unit)
inventories = []
for rows in (reference, result):
mapping = {}
for row in rows:
require(row["quantity"] == quantity, "wrong-modal-quantity", str(row["mode"]))
require(row["unit_dimensions"] == unit, "wrong-modal-unit", str(row["mode"]))
require(row["step"] == STEP, "wrong-modal-step", str(row["mode"]))
mode = row["mode"]
require(type(mode) is int and mode in range(1, 11), "wrong-modal-mode", str(mode))
require(mode not in mapping, "duplicate-modal-mode", str(mode))
mapping[mode] = row["value"]
require(set(mapping) == set(range(1, 11)), "modal-mode-inventory", "Lowest ten modes required.")
inventories.append(mapping)
families = [evaluate_family(
{"identity": f"hexa_freq:{STEP}:mode-{mode}:{quantity}",
"case": "hexa_freq", "step": STEP, "mode": mode, "quantity": quantity,
"unit_dimensions": unit, "rows": [{"mode": mode, "component": quantity}]},
[inventories[1][mode]], [inventories[0][mode]]) for mode in range(1, 11)]
return {"families": families, "passed": all(family["passed"] for family in families)}
def text(value) -> str:
return value.decode("utf-8", errors="strict") if isinstance(value, bytes) else str(value)
def string_attribute(obj, name, expected=None):
require(name in obj.attrs, "missing-result-attribute", f"{obj.name}:{name}")
attr = obj.attrs.get_id(name)
string = h5py.check_string_dtype(attr.dtype)
require(attr.shape == () and string is not None and string.length is None
and string.encoding == "utf-8", "wrong-result-attribute-type", f"{obj.name}:{name}")
value = text(obj.attrs[name])
require(bool(value) and (expected is None or value == expected),
"wrong-result-attribute", f"{obj.name}:{name}={value}")
return value
def numeric_attribute(obj, name, dtype, expected):
require(name in obj.attrs, "missing-result-attribute", f"{obj.name}:{name}")
attr = obj.attrs.get_id(name)
require(attr.shape == () and attr.dtype == np.dtype(dtype),
"wrong-result-attribute-type", f"{obj.name}:{name}")
require(obj.attrs[name] == expected, "wrong-result-attribute", f"{obj.name}:{name}")
def dataset(file, path, shape):
require(path in file and isinstance(file[path], h5py.Dataset), "missing-result-dataset", path)
value = file[path]
require(value.shape == shape and value.chunks is None and value.compression is None,
"wrong-result-shape", path)
return value
def table(file, path, count, fields):
value = dataset(file, path, (count,))
require(value.dtype.names == tuple(name for name, _ in fields), "wrong-result-table-schema", path)
for name, dtype in fields:
actual = value.dtype[name]
if dtype == "string":
info = h5py.check_string_dtype(actual)
valid = info is not None and info.length is None and info.encoding == "utf-8"
else:
valid = actual == np.dtype(dtype)
require(valid, "wrong-result-table-schema", f"{path}:{name}")
return value[:]
def read_results(path: Path, model):
"""Read complete official schema/identities; defer physical finite checks."""
case = model["case"]
dynamic, nonlinear = case.startswith("hexa_dyn"), "nonlinear" in case
frequency = case == "hexa_freq"
arrays, frames, modal = [], {}, {}
with h5py.File(path, "r") as file:
require("metadata" in file and isinstance(file["metadata"], h5py.Group),
"missing-result-metadata", str(path))
metadata = file["metadata"]
numeric_attribute(metadata, "schema_version", "<u8", 1)
for name, expected in {
"feature_id": "hex8-structural-analysis", "analysis_procedure": CASES[case],
"element_formulation": "FESA-Hex8", "integration_rule": "gauss-2x2x2",
"mass_formulation": "lumped-reference", "source_step_name": STEP,
"unit_system_label": "user-consistent-unspecified",
"coordinate_convention": "global-cartesian; reference-strain; current-cauchy-stress",
"component_order": "node-major; UX,UY,UZ",
"source_input_identity": f"path={model['source_path']};content_identity={model['content_identity']}",
}.items():
string_attribute(metadata, name, expected)
string_attribute(metadata, "solver_version")
nodes = table(file, "model/nodes", NODE_COUNT, [
("internal_node_id", "<u8"), ("instance_name", "string"),
("source_label", "string"), ("coordinates", ("<f8", (3,)))])
node_map = {}
for i, row in enumerate(nodes):
node = label(text(row["source_label"]))
require(int(row["internal_node_id"]) == i and canonical(text(row["instance_name"])) == INSTANCE,
"wrong-result-node-identity", str(i))
require(node not in node_map and node in model["nodes"], "result-node-inventory", str(node))
node_map[node] = i
require(node_map.keys() == model["nodes"].keys(), "result-node-inventory", str(path))
elements = table(file, "model/elements", ELEMENT_COUNT, [
("internal_element_id", "<u8"), ("instance_name", "string"), ("source_label", "string"),
("source_element_type", "string"), ("internal_formulation", "string"),
("node_internal_ids", ("<u8", (8,))), ("solid_section_internal_id", "<u8"),
("material_internal_id", "<u8")])
element_map = {}
for i, row in enumerate(elements):
element = label(text(row["source_label"]))
require(int(row["internal_element_id"]) == i and canonical(text(row["instance_name"])) == INSTANCE,
"wrong-result-element-identity", str(i))
require(element not in element_map and element in model["elements"],
"result-element-inventory", str(element))
require(text(row["source_element_type"]) == "C3D8"
and text(row["internal_formulation"]) == "FESA-Hex8"
and tuple(row["node_internal_ids"]) == tuple(node_map[n] for n in model["elements"][element])
and row["solid_section_internal_id"] == 0 and row["material_internal_id"] == 0,
"wrong-result-element-connectivity", str(element))
element_map[element] = i
materials = table(file, "model/solid/materials", 1, [
("internal_material_id", "<u8"), ("name", "string"), ("youngs_modulus", "<f8"),
("poisson_ratio", "<f8"), ("density_present", "u1"), ("density", "<f8")])
material = materials[0]
require(material["internal_material_id"] == 0 and canonical(text(material["name"])) == "MATERIAL-1"
and material["density_present"] == 1, "wrong-result-material-identity", str(path))
sections = table(file, "model/solid/sections", 1, [
("internal_section_id", "<u8"), ("source_file", "string"), ("source_line", "<u8"),
("source_elset", "string"), ("material_internal_id", "<u8")])
section = sections[0]
require(section["internal_section_id"] == 0 and section["material_internal_id"] == 0
and text(section["source_file"]) == model["source_path"]
and section["source_line"] == model["section_line"]
and canonical(text(section["source_elset"])) == canonical(model["section_elset"]),
"wrong-result-section-identity", str(path))
points = table(file, "model/solid_integration_points", ELEMENT_COUNT * 8, [
("internal_element_id", "<u8"), ("point_id", "u1"),
("natural_coordinates", ("<f8", (3,))), ("reference_coordinates", ("<f8", (3,)))])
for i, row in enumerate(points):
require(row["internal_element_id"] == i // 8 and row["point_id"] == i % 8 + 1,
"wrong-result-point-identity", str(i))
mask = dataset(file, "model/nodal_constraint_mask", (NODE_COUNT, 3))
require(mask.dtype == np.dtype("u1"), "wrong-result-mask-type", str(path))
string_attribute(mask, "component_names", "UX,UY,UZ")
mask = mask[:]
expected_mask = np.array([[(node, c) in model["constrained"] for c in (1, 2, 3)]
for node in node_map], dtype="u1")
require(np.array_equal(mask, expected_mask), "wrong-result-constraint-identity", str(path))
require("steps" in file and set(file["steps"]) == {STEP}, "wrong-result-step", str(path))
step = file[f"steps/{STEP}"]
expected_step_groups = {"eigen"} if frequency else ({"frames", "accepted_frames"} if dynamic or nonlinear else {"frames"})
require(set(step) == expected_step_groups, "wrong-result-step-inventory", str(path))
if frequency:
modal = read_modal_tables(file, arrays, int((mask == 0).sum()))
else:
count = 101 if dynamic else (10 if nonlinear else 1)
accepted = None
if dynamic or nonlinear:
coordinate = "time" if dynamic else "load_factor"
flag = "is_initial" if dynamic else "is_factor_one"
accepted = table(file, f"steps/{STEP}/accepted_frames", count, [
("frame_index", "<u8"), ("increment_index", "<u8"), (coordinate, "<f8"),
("iteration_count", "<u8"), (flag, "u1")])
for frame, row in enumerate(accepted):
increment = frame if dynamic else frame + 1
value = increment * 0.1
expected_flag = int(frame == (0 if dynamic else count - 1))
require(row["frame_index"] == frame and row["increment_index"] == increment
and row[coordinate] == value and row[flag] == expected_flag
and (row["iteration_count"] == 0 if dynamic and frame == 0
else 1 <= row["iteration_count"] <= 100),
"wrong-result-frame-schedule", str(frame))
frame_root = f"steps/{STEP}/frames"
require(set(file[frame_root]) == {str(i) for i in range(count)},
"result-frame-inventory", str(path))
for frame in range(count):
increment = frame if dynamic else (frame + 1 if nonlinear else 10)
value = increment * 0.1 if dynamic or nonlinear else 1.0
frames[increment] = read_frame(file, frame, increment, value, dynamic, nonlinear, arrays)
# Identity and schema checks above deliberately finish before physical checks.
arrays.extend([("model/nodes/coordinates", nodes["coordinates"]),
("model/points/natural", points["natural_coordinates"]),
("model/points/reference", points["reference_coordinates"]),
("model/material", np.array([material[n] for n in ("youngs_modulus", "poisson_ratio", "density")]))])
evidence = {"source_input_identity": string_attribute(metadata, "source_input_identity"),
"source_node_count": NODE_COUNT, "source_element_count": ELEMENT_COUNT,
"accepted_frame_count": len(frames), "constrained_scalar_count": int(mask.sum()),
"result_node_map": node_map, "result_element_map": element_map}
return {"frames": frames, "modal": modal, "nodes": nodes, "points": points,
"material": material, "arrays": arrays, "mask": mask, "evidence": evidence}
def physical_dataset(file, path, shape, names, units, location, frame=None,
increment=None, coordinate=None, dynamic=False, extra=None):
value = dataset(file, path, shape)
require(value.dtype == np.dtype("<f8"), "wrong-result-value-type", path)
attributes = {"component_names": names, "unit_dimensions": units,
"coordinate_system": "global-cartesian", "location": location}
if frame is not None:
attributes["step_name"] = STEP
attributes.update(extra or {})
for name, expected in attributes.items():
string_attribute(value, name, expected)
if frame is not None:
numeric_attribute(value, "frame_index", "<u8", frame)
numeric_attribute(value, "increment_index", "<u8", increment)
numeric_attribute(value, "time" if dynamic else "load_factor", "<f8", coordinate)
return value[()]
def read_frame(file, frame, increment, coordinate, dynamic, nonlinear, arrays):
root = f"steps/{STEP}/frames/{frame}"
require(set(file[root]) == {"nodal", "solid", "global"}, "wrong-result-frame-inventory", root)
definitions = {
"nodal/displacement": ((NODE_COUNT, 3), "U1,U2,U3", "length", "nodal", {}),
"nodal/reaction": ((NODE_COUNT, 3), "RF1,RF2,RF3", "force", "nodal", {}),
"nodal/residual": ((NODE_COUNT, 3), "R1,R2,R3", "force", "nodal", {}),
"solid/stress": ((ELEMENT_COUNT, 8, 6), "S11,S22,S33,S12,S13,S23", "force/length^2",
"integration-point", {"stress_measure": "cauchy"}),
"solid/strain": ((ELEMENT_COUNT, 8, 6), "E11,E22,E33,E12,E13,E23", "dimensionless",
"integration-point", {"strain_measure": "green-lagrange" if nonlinear else "infinitesimal",
"shear_convention": "tensor"}),
"global/strain_energy": ((), "scalar", "force*length", "global",
{"definition": "sum(W*detJ0)" if nonlinear else "0.5*u^T*K0*u"}),
}
if dynamic:
definitions.update({
"nodal/velocity": ((NODE_COUNT, 3), "V1,V2,V3", "length/time", "nodal", {}),
"nodal/acceleration": ((NODE_COUNT, 3), "A1,A2,A3", "length/time^2", "nodal", {"acceleration_kind": "physical"}),
"nodal/algorithmic_acceleration": ((NODE_COUNT, 3), "A1,A2,A3", "length/time^2", "nodal", {"acceleration_kind": "algorithmic"}),
"global/kinetic_energy": ((), "scalar", "force*length", "global", {"definition": "0.5*v^T*M*v"}),
"global/external_work": ((), "scalar", "force*length", "global", {"definition": "sum(0.5*(F_old+F_new)*delta_u)"}),
"global/energy_balance_error": ((), "scalar", "force*length", "global", {"definition": "T+U-T0-U0-Wext"}),
"global/linear_momentum": ((3,), "P1,P2,P3", "mass*length/time", "global", {}),
"global/angular_momentum": ((3,), "L1,L2,L3", "mass*length^2/time", "global", {"origin": "global0"}),
})
for group in ("nodal", "solid", "global"):
expected = {key.split("/", 1)[1] for key in definitions if key.startswith(group + "/")}
require(set(file[f"{root}/{group}"]) == expected, "wrong-result-quantity-inventory", root + "/" + group)
values = {}
for quantity, (shape, names, units, location, extra) in definitions.items():
path = root + "/" + quantity
values[quantity] = physical_dataset(file, path, shape, names, units, location,
frame, increment, coordinate, dynamic, extra)
arrays.append((path, values[quantity]))
return values
def read_modal_tables(file, arrays, free_count):
root = f"steps/{STEP}/eigen"
require(set(file[root]) == {"modes", "frequencies", "vectors", "verification"},
"wrong-modal-result-inventory", root)
modes = table(file, root + "/modes", 10, [("mode_id", "<u8"), ("eigenvalue", "<f8"),
("classification", "u1"), ("cluster_id", "<u8")])
for name, component, units in (("modes", "eigenvalue", "1/time^2"),
("frequencies", "frequency", "1/time")):
obj = file[root + "/" + name]
for attr, expected in {"component_names": component, "unit_dimensions": units,
"coordinate_system": "global-cartesian", "location": "global"}.items():
string_attribute(obj, attr, expected)
frequencies = table(file, root + "/frequencies", sum(row["eigenvalue"] >= 0 for row in modes),
[("mode_id", "<u8"), ("frequency", "<f8")])
require(list(modes["mode_id"]) == list(range(1, 11)) and (modes["classification"] <= 1).all()
and modes["cluster_id"][0] == 1
and all(b in (a, a + 1) for a, b in zip(modes["cluster_id"], modes["cluster_id"][1:])),
"wrong-modal-mode", root)
require(list(frequencies["mode_id"]) == [row["mode_id"] for row in modes if row["eigenvalue"] >= 0],
"wrong-modal-frequency-inventory", root)
vectors = physical_dataset(file, root + "/vectors", (10, NODE_COUNT, 3),
"UX,UY,UZ", "mass-normalized", "nodal",
extra={"normalization": "phi^T*M*phi=1"})
verification = root + "/verification"
require(set(file[verification]) == {"pairs", "eta_gram", "max_eta_orth"},
"wrong-modal-verification-inventory", verification)
pair_object = file[verification + "/pairs"]
require(isinstance(pair_object, h5py.Dataset) and len(pair_object.shape) == 1,
"wrong-modal-pair-shape", verification + "/pairs")
pair_count = pair_object.shape[0]
require(10 <= pair_count <= free_count, "wrong-modal-pair-count", str(pair_count))
pairs = table(file, verification + "/pairs", pair_count, [
("sorted_rank", "<u8"), ("solver_column", "<u8"), ("eigenvalue", "<f8"),
("modal_mass", "<f8"), ("rayleigh_quotient", "<f8"), ("eta_r", "<f8"),
("eta_norm", "<f8"), ("eta_energy", "<f8")])
require(list(pairs["sorted_rank"]) == list(range(1, pair_count + 1))
and len(set(pairs["solver_column"])) == pair_count
and (pairs["solver_column"] < pair_count).all(), "wrong-modal-pair-identity", verification)
for name in ("eta_gram", "max_eta_orth"):
value = dataset(file, verification + "/" + name, ())
require(value.dtype == np.dtype("<f8"), "wrong-result-value-type", name)
arrays.append((verification + "/" + name, value[()]))
arrays.extend([(root + "/vectors", vectors), (root + "/lambda", modes["eigenvalue"]),
(root + "/frequency", frequencies["frequency"])])
arrays.extend([(verification + "/" + name, pairs[name]) for name in pairs.dtype.names[2:]])
return {"modes": modes, "frequencies": frequencies, "pairs": pairs}
def check_result_values(result, model):
for path, values in result["arrays"]:
require(np.isfinite(values).all(), "nonfinite-result-value", path)
for row in result["nodes"]:
node = label(text(row["source_label"]))
require(tuple(row["coordinates"]) == model["nodes"][node], "wrong-result-coordinate", str(node))
material = result["material"]
require(tuple(material[name] for name in ("youngs_modulus", "poisson_ratio")) == model["elastic"]
and material["density"] == model["density"], "wrong-result-material-value", model["case"])
for i, row in enumerate(result["points"]):
element = list(result["evidence"]["result_element_map"])[i // 8]
expected, scale = point_coordinates(model, element, i % 8 + 1)
require(np.array_equal(row["natural_coordinates"], NATURAL_POINTS[i % 8])
and (np.abs(row["reference_coordinates"] - expected) <= 16 * np.finfo(float).eps * scale).all(),
"result-coordinate-convention", str(i))
for increment, values in result["frames"].items():
require((values["nodal/reaction"][result["mask"] == 0] == 0.0).all(),
"nonzero-free-result-reaction", str(increment))
if model["case"].startswith("hexa_dyn") and increment == 0:
require((values["nodal/displacement"] == 0.0).all() and (values["nodal/velocity"] == 0.0).all(),
"nonzero-initial-result-state", str(increment))
if result["modal"]:
modes, frequencies, pairs = (result["modal"][key] for key in ("modes", "frequencies", "pairs"))
require((np.diff(modes["eigenvalue"]) >= 0).all() and (np.diff(pairs["eigenvalue"]) >= 0).all(),
"wrong-modal-order", model["case"])
for row in frequencies:
expected = math.sqrt(modes[row["mode_id"] - 1]["eigenvalue"]) / (2 * math.pi)
require(row["frequency"] == expected, "wrong-modal-frequency", str(row["mode_id"]))
def compare(case: str, input_path: Path, results_path: Path, reference_directory: Path = REFERENCE_DIRECTORY,
execution_record: Path | None = None):
report = {"schema": "hex8-reference-comparison-v1", "case": case, "policy": dict(POLICY),
"identities": {}, "precheck": {"passed": False}, "families": [],
"blocking_failures": [], "blocking_passed": False, "verdict": "failed"}
try:
require(case in CASES, "unknown-source-case", case)
quantities = ["U"] if case.startswith("hexa_dyn") else (["U", "RF", "S"] if case != "hexa_freq" else [])
paths = {"input": input_path, "results": results_path}
paths.update({quantity: reference_directory / f"{case}_{quantity}.csv" for quantity in quantities})
modal_path = reference_directory / "hexa_freq.txt"
if case == "hexa_freq" and modal_path.exists():
paths["Value"] = modal_path
for name, path in paths.items():
report["identities"][name] = {"path": lexical_absolute(path), "sha256": sha256(path)}
model = input_inventory(input_path, case)
report["input_copy_matches_approved_bytes"] = True
report["approved_input_sha256"] = SOURCE_SHA256[f"{case}.inp"]
report["execution"] = {"verified": False, "scope": "file comparison only; execution record not supplied"}
if execution_record is not None:
execution = json.loads(execution_record.read_text(encoding="utf-8-sig"))
command = execution.get("command")
require(execution.get("case") == case and execution.get("native_exit") == 0
and type(execution.get("native_exit")) is int
and execution.get("original_sha256") == report["identities"]["input"]["sha256"]
and execution.get("scratch_sha256") == report["identities"]["input"]["sha256"]
and execution.get("result_sha256") == report["identities"]["results"]["sha256"]
and execution.get("source_unchanged") is True
and execution.get("scratch_unchanged") is True
and execution.get("result_exists") is True
and isinstance(command, list) and len(command) == 2
and all(isinstance(item, str) for item in command)
and Path(lexical_absolute(Path(command[1]))) == Path(lexical_absolute(input_path)),
"invalid-execution-evidence", str(execution_record))
report["execution"] = {"verified": True, "native_exit": 0, "command": command,
"record_path": lexical_absolute(execution_record),
"record_sha256": sha256(execution_record)}
records = {}
modal_reference = None
if "Value" in paths:
require(report["identities"]["Value"]["sha256"] == SOURCE_SHA256["hexa_freq.txt"],
"reference-hash-mismatch", str(modal_path))
modal_reference = read_modal_reference(modal_path)
for quantity in quantities:
require(report["identities"][quantity]["sha256"] == SOURCE_SHA256[f"{case}_{quantity}.csv"],
"reference-hash-mismatch", str(paths[quantity]))
records[quantity] = read_reference(paths[quantity], case, quantity, model)
result = read_results(results_path, model)
references = {quantity: reference_values(rows, quantity, model) for quantity, rows in records.items()}
check_result_values(result, model)
report["precheck"] = {"passed": True, **result["evidence"],
"reference_row_counts": {q: len(rows) for q, rows in records.items()},
"coordinate_check": "auxiliary; fixed identity; 16*float32-eps uncancelled source-coordinate budget"}
# Internal maps are useful to validation but unnecessary in the public report.
del report["precheck"]["result_node_map"], report["precheck"]["result_element_map"]
if case == "hexa_freq":
if modal_reference is None:
report["verdict"] = "not-run"
report["reason"] = "Original modal Value reference is absent; no format/quantity/unit assumption made."
return report
actual = [{"step": STEP, "mode": int(row["mode_id"]), "quantity": "eigenvalue",
"unit_dimensions": "1/time^2", "value": float(row["eigenvalue"])}
for row in result["modal"]["modes"]]
modal = compare_modal(modal_reference, actual, "eigenvalue", "1/time^2")
report["precheck"]["reference_row_counts"]["Value"] = len(modal_reference)
report["modal_reference"] = {"format": "Mode n: Value = lambda Freq = f (cycles/time)",
"step_binding": "sole Step-1 of approved hexa_freq.inp",
"quantity": "eigenvalue", "unit_dimensions": "1/time^2",
"frequency_role": "finite auxiliary; no conversion"}
report["families"] = modal["families"]
for family in report["families"]:
family["reference_sha256"] = report["identities"]["Value"]["sha256"]
report["blocking_failures"] = [f["identity"] for f in modal["families"] if not f["passed"]]
report["blocking_passed"] = modal["passed"]
report["verdict"] = "passed" if modal["passed"] else "failed"
return report
families = []
for increment in sorted(result["frames"]):
if not case.startswith("hexa_dyn") and increment != 10:
continue
values = result["frames"][increment]
for quantity in quantities:
actual, expected, identities = [], [], []
for key in sorted(references[quantity]):
if key[0] != increment:
continue
_, entity, point = key
index = result["evidence"]["result_element_map" if quantity == "S" else "result_node_map"][entity]
row = values[{"U": "nodal/displacement", "RF": "nodal/reaction", "S": "solid/stress"}[quantity]][index]
if quantity == "S":
row = row[point - 1]
for c, component in enumerate(COMPONENTS[quantity]):
if quantity == "RF" and (entity, c + 1) not in model["constrained"]:
continue
identities.append({"instance": INSTANCE, "component": component,
"element_label" if quantity == "S" else "node_label": entity,
**({"integration_point": point} if quantity == "S" else {})})
actual.append(float(row[c]))
expected.append(references[quantity][key][c])
identity = {"identity": f"{case}:{STEP}:increment-{increment}:{quantity}-global",
"case": case, "step": STEP, "increment": increment, "quantity": quantity,
"unit_dimensions": {"U": "length", "RF": "force", "S": "force/length^2"}[quantity],
"coordinate_system": "global-cartesian", "rows": identities,
"reference_sha256": report["identities"][quantity]["sha256"]}
if case.startswith("hexa_dyn"):
identity.update({"hdf5_time": increment * 0.1,
"reference_decimal_time": str(Decimal(increment) / 10)})
families.append(evaluate_family(identity, actual, expected))
report["families"] = families
report["blocking_failures"] = [f["identity"] for f in families if not f["passed"]]
report["blocking_passed"] = not report["blocking_failures"]
report["verdict"] = "passed" if report["blocking_passed"] else "failed"
except (ComparisonError, OSError, ValueError, InvalidOperation, UnicodeError, KeyError, TypeError) as error:
report["precheck"] = {"passed": False, "code": getattr(error, "code", "invalid-comparison-input"),
"message": str(error)}
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--case", choices=CASES, required=True)
parser.add_argument("--input", type=Path, required=True, help="Actual byte-identical scratch input read by the solver.")
parser.add_argument("--results", type=Path, required=True)
parser.add_argument("--references", type=Path, default=REFERENCE_DIRECTORY)
parser.add_argument("--execution-record", type=Path, help="Closed native CLI record with input/result hashes; never executed here.")
parser.add_argument("--report", type=Path, required=True)
args = parser.parse_args()
protected = {args.input.resolve(), args.results.resolve()}
if args.execution_record is not None:
protected.add(args.execution_record.resolve())
protected.update(path.resolve() for path in args.references.glob("*"))
protected.update(path.resolve() for path in REFERENCE_DIRECTORY.glob("*"))
report_path = args.report.resolve()
hardlink_alias = args.report.exists() and any(
path.exists() and args.report.samefile(path) for path in protected)
if report_path in protected or hardlink_alias:
parser.error("The report path must not overwrite an input, reference or result.")
report = compare(args.case, args.input, args.results, args.references, args.execution_record)
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(json.dumps(report, sort_keys=True, indent=2, allow_nan=False) + "\n", encoding="utf-8")
print(json.dumps({"case": args.case, "verdict": report["verdict"], "precheck": report["precheck"],
"blocking_failure_count": len(report["blocking_failures"]), "report": str(args.report)}, allow_nan=False))
return 2 if not report["precheck"]["passed"] or report["verdict"] == "not-run" else (0 if report["blocking_passed"] else 1)
if __name__ == "__main__":
raise SystemExit(main())
-436
View File
@@ -1,436 +0,0 @@
"""Compare the approved 101-frame HHT reference without modifying artifacts.
Run with ``uv run --with h5py python -B scripts/verification/compare_hht_dynamic.py``.
Only the documented displacement/rotation reference quantities are compared;
this is not an independent validation of inertia, energy, or convergence.
"""
from __future__ import annotations
import argparse
import csv
from decimal import Decimal, InvalidOperation
import hashlib
import json
import math
from pathlib import Path
import re
from typing import Any
import h5py
import numpy as np
ROOT = Path(__file__).resolve().parents[2]
CASE = "shellNL-dyn-meshfine"
REFERENCE_DIRECTORY = ROOT / "reference" / CASE
NODE_LABELS = (2, 21, 22, 69, 70, 165, 309, 442, 586)
INSTANCE = "PART-1-1"
STEP = "Step-1"
NODE_COUNT = 729
FRAME_COUNT = 101
INCREMENT = 0.1
COMPONENTS = ("U-U1", "U-U2", "U-U3", "UR-UR1", "UR-UR2", "UR-UR3")
CSV_COLUMNS = ("Frame", "Part Instance Name", "Node Label", *COMPONENTS)
FRAME_PATTERN = re.compile(
r"Increment\s+(\d+):\s*Step Time\s*=\s*"
r"([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)"
)
POLICY = {
"policy_id": "common-reference-tolerance",
"near_zero_ratio": 0.01,
"relative_tolerance": 0.05,
"relative_rms_tolerance": 0.01,
}
class ComparisonError(ValueError):
"""An identity/schema/finite failure that must precede tolerance evaluation."""
def __init__(self, code: str, message: str):
super().__init__(message)
self.code = code
def require(condition: bool, code: str, message: str) -> None:
if not condition:
raise ComparisonError(code, message)
def positive_label(text: str) -> int:
require(bool(re.fullmatch(r"\d+", text)), "invalid-node-label", repr(text))
label = int(text)
require(0 < label <= 2**63 - 1, "invalid-node-label", repr(text))
return label
def canonical_instance(text: str) -> str:
return text.translate(str.maketrans("abcdefghijklmnopqrstuvwxyz", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"))
def input_inventory(path: Path) -> tuple[dict[int, tuple[float, ...]], str]:
"""Read identity and schedule authority from this specific approved INP."""
raw = path.read_bytes()
nodes: dict[int, tuple[float, ...]] = {}
parts: list[str] = []
instances: list[tuple[str, str]] = []
steps: list[dict[str, str]] = []
dynamic_rows: list[tuple[float, float]] = []
keyword = ""
in_part = False
for line_number, line in enumerate(raw.decode("utf-8-sig").splitlines(), 1):
text = line.strip()
if not text or text.startswith("**"):
continue
fields = [field.strip() for field in text.split(",")]
if text.startswith("*"):
keyword = fields[0].upper()
parameters = dict(
(name.upper(), value.strip())
for field in fields[1:] if "=" in field
for name, value in [field.split("=", 1)]
)
if keyword == "*PART":
in_part = True
parts.append(canonical_instance(parameters.get("NAME", "")))
elif keyword == "*END PART":
in_part = False
elif keyword == "*INSTANCE":
instances.append((canonical_instance(parameters.get("NAME", "")),
canonical_instance(parameters.get("PART", ""))))
elif keyword == "*STEP":
steps.append(parameters)
elif keyword == "*DYNAMIC":
require([field.upper() for field in fields] == ["*DYNAMIC", "DIRECT"],
"wrong-input-procedure", f"line {line_number}")
continue
if keyword == "*NODE":
require(in_part and len(fields) == 4, "invalid-input-node", f"line {line_number}")
label = positive_label(fields[0])
require(label not in nodes, "duplicate-input-node", str(label))
coordinates = tuple(float(value) for value in fields[1:])
require(all(math.isfinite(value) for value in coordinates),
"nonfinite-input-node", str(label))
nodes[label] = coordinates
elif keyword == "*DYNAMIC":
if fields[-1] == "":
fields.pop()
require(len(fields) == 2, "wrong-input-schedule", f"line {line_number}")
dynamic_rows.append((float(fields[0]), float(fields[1])))
require(parts == ["PART-1"] and instances == [(INSTANCE, "PART-1")],
"wrong-input-instance", "Expected the original single identity instance.")
require(len(nodes) == NODE_COUNT and set(NODE_LABELS) <= nodes.keys(),
"wrong-input-node-inventory", f"Expected {NODE_COUNT} source nodes.")
require(len(steps) == 1 and steps[0].get("NAME") == STEP
and steps[0].get("NLGEOM", "").upper() == "YES"
and steps[0].get("INC") == "1000", "wrong-input-step", str(steps))
require(dynamic_rows == [(INCREMENT, 10.0)], "wrong-input-schedule", str(dynamic_rows))
value = 14695981039346656037
for byte in raw:
value = ((value ^ byte) * 1099511628211) & (2**64 - 1)
return nodes, f"fnv1a64:{value:016x}"
def read_reference(path: Path) -> dict[tuple[int, int], tuple[float, ...]]:
rows: dict[tuple[int, int], tuple[float, ...]] = {}
with path.open(encoding="utf-8-sig", newline="") as stream:
reader = csv.reader(stream)
header = [field.strip() for field in next(reader, [])]
require(len(header) == len(set(header)), "duplicate-reference-column", str(header))
require(set(CSV_COLUMNS) <= set(header), "missing-reference-column", str(header))
for line_number, fields in enumerate(reader, 2):
if not fields or all(not field.strip() for field in fields):
continue
require(len(fields) == len(header), "invalid-reference-row", f"line {line_number}")
row = dict(zip(header, (field.strip() for field in fields)))
match = FRAME_PATTERN.fullmatch(row["Frame"])
require(match is not None, "invalid-reference-frame", f"line {line_number}")
increment = int(match.group(1))
time = Decimal(match.group(2))
require(time.is_finite() and time == Decimal(increment) / Decimal(10),
"wrong-reference-time", f"line {line_number}: {row['Frame']}")
require(canonical_instance(row["Part Instance Name"]) == INSTANCE,
"wrong-reference-instance", f"line {line_number}")
label = positive_label(row["Node Label"])
key = (increment, label)
require(key not in rows, "duplicate-reference-row", str(key))
values = tuple(float(row[component]) for component in COMPONENTS)
require(all(math.isfinite(value) for value in values),
"nonfinite-reference-value", str(key))
rows[key] = values
expected = {(frame, label) for frame in range(FRAME_COUNT) for label in NODE_LABELS}
require(rows.keys() == expected, "reference-row-inventory",
f"missing={sorted(expected - rows.keys())}; extra={sorted(rows.keys() - expected)}")
return rows
def text_value(value: Any) -> str:
return value.decode("utf-8") if isinstance(value, bytes) else str(value)
def string_attribute(obj: Any, name: str, expected: str | None = None) -> str:
require(name in obj.attrs, "missing-hdf5-attribute", f"{obj.name}@{name}")
attribute = obj.attrs.get_id(name)
string = h5py.check_string_dtype(attribute.dtype)
require(attribute.shape == () and string is not None and string.length is None
and string.encoding == "utf-8", "wrong-hdf5-attribute-type", f"{obj.name}@{name}")
value = text_value(obj.attrs[name])
require(bool(value) and (expected is None or value == expected),
"wrong-hdf5-attribute-value", f"{obj.name}@{name}: {value!r}")
return value
def numeric_attribute(obj: Any, name: str, dtype: str, expected: int | float) -> None:
require(name in obj.attrs, "missing-hdf5-attribute", f"{obj.name}@{name}")
attribute = obj.attrs.get_id(name)
require(attribute.shape == () and attribute.dtype == np.dtype(dtype),
"wrong-hdf5-attribute-type", f"{obj.name}@{name}")
value = obj.attrs[name]
require(math.isfinite(value) and value == expected, "wrong-hdf5-attribute-value",
f"{obj.name}@{name}: {value!r}")
def dataset(file: h5py.File, path: str, shape: tuple[int, ...]) -> h5py.Dataset:
require(path in file and isinstance(file[path], h5py.Dataset), "missing-hdf5-dataset", path)
result = file[path]
require(result.shape == shape and result.chunks is None and result.compression is None,
"wrong-hdf5-dataset-layout", path)
return result
def read_results(path: Path, nodes: dict[int, tuple[float, ...]],
content_identity: str) -> tuple[list[np.ndarray], dict[str, Any]]:
"""Precheck every result node/frame/component, then project by source key."""
projected: list[np.ndarray] = []
with h5py.File(path, "r") as file:
require("metadata" in file and isinstance(file["metadata"], h5py.Group),
"missing-hdf5-metadata", "/metadata")
metadata = file["metadata"]
for name, expected in {
"feature_id": "hht-implicit-dynamics", "analysis_procedure": "hht-implicit-dynamics",
"mass_formulation": "nodal-lumped-kinetic", "internal_formulation": "FESA-MITC4-total-lagrangian",
"initial_state": "zero-q-v-step-load-0+",
"acceleration_convention": "physical-endpoint-and-auxiliary-separated",
"source_step_name": STEP, "step_name": STEP, "unit_system_label": "consistent-input-units",
}.items():
string_attribute(metadata, name, expected)
string_attribute(metadata, "solver_version")
source_identity = string_attribute(metadata, "source_input_identity")
require(source_identity.endswith(";content_identity=" + content_identity),
"wrong-input-content-identity", source_identity)
require("schema_version" in metadata.attrs, "missing-hdf5-attribute", "/metadata@schema_version")
version_attribute = metadata.attrs.get_id("schema_version")
require(version_attribute.shape == () and version_attribute.dtype == np.dtype("<u8"),
"wrong-hdf5-attribute-type", "/metadata@schema_version")
schema_version = int(metadata.attrs["schema_version"])
require(schema_version in (1, 2), "wrong-hdf5-attribute-value", "/metadata@schema_version")
if schema_version == 2:
string_attribute(metadata, "time_integration_formulation",
"reduced-coordinate-HHT-with-physical-rate-reconstruction")
elif "time_integration_formulation" in metadata.attrs:
string_attribute(metadata, "time_integration_formulation", "full-coordinate-HHT")
for name, dtype, value in (
("schema_version", "<u8", schema_version), ("increment_count", "<u8", 100),
("maximum_increments", "<u8", 1000), ("alpha", "<f8", -0.05),
("beta", "<f8", 0.275625), ("gamma", "<f8", 0.55),
("initial_increment", "<f8", INCREMENT), ("time_period", "<f8", 10.0),
):
numeric_attribute(metadata, name, dtype, value)
table = dataset(file, "model/nodes", (NODE_COUNT,))
require(table.dtype.names == ("internal_node_id", "instance_name", "source_label", "coordinates"),
"wrong-node-schema", str(table.dtype))
require(table.dtype["internal_node_id"] == np.dtype("<u8")
and table.dtype["coordinates"] == np.dtype(("<f8", (3,))),
"wrong-node-schema", str(table.dtype))
for name in ("instance_name", "source_label"):
string = h5py.check_string_dtype(table.dtype[name])
require(string is not None and string.length is None and string.encoding == "utf-8",
"wrong-node-schema", name)
node_indices: dict[int, int] = {}
for index, node in enumerate(table[:]):
require(int(node["internal_node_id"]) == index, "wrong-internal-node-identity", str(index))
instance = canonical_instance(text_value(node["instance_name"]))
label = positive_label(text_value(node["source_label"]))
require(instance == INSTANCE, "wrong-result-instance", instance)
require(label not in node_indices, "duplicate-result-node", str(label))
require(label in nodes, "extra-result-node", str(label))
require(np.isfinite(node["coordinates"]).all(), "nonfinite-result-node", str(label))
require(tuple(node["coordinates"]) == nodes[label], "wrong-result-coordinate", str(label))
node_indices[label] = index
require(node_indices.keys() == nodes.keys(), "result-node-inventory", "Source node inventories differ.")
require("steps" in file and isinstance(file["steps"], h5py.Group)
and set(file["steps"]) == {STEP}, "wrong-result-step", "Expected one accepted step.")
accepted = dataset(file, f"steps/{STEP}/accepted_frames", (FRAME_COUNT,))
expected_fields = (("frame_index", "<u8"), ("increment_index", "<u8"),
("time", "<f8"), ("iteration_count", "<u8"), ("is_initial", "u1"))
require(accepted.dtype.names == tuple(name for name, _ in expected_fields)
and all(accepted.dtype[name] == np.dtype(dtype) for name, dtype in expected_fields),
"wrong-frame-table-schema", str(accepted.dtype))
frame_path = f"steps/{STEP}/frames"
require(frame_path in file and isinstance(file[frame_path], h5py.Group)
and set(file[frame_path]) == {str(i) for i in range(FRAME_COUNT)},
"result-frame-inventory", "Missing, extra, or noncanonical frame group.")
selection = [node_indices[label] for label in NODE_LABELS]
for frame, row in enumerate(accepted[:]):
scheduled_time = float(frame) * INCREMENT
require(int(row["frame_index"]) == frame and int(row["increment_index"]) == frame
and math.isfinite(row["time"]) and float(row["time"]) == scheduled_time
and int(row["is_initial"]) == int(frame == 0)
and 0 <= int(row["iteration_count"]) <= 100
and (frame != 0 or (int(row["iteration_count"]) == 0 and not np.signbit(row["time"]))),
"wrong-result-frame-time", str(frame))
displacement = dataset(file, f"{frame_path}/{frame}/nodal/displacement", (NODE_COUNT, 6))
require(displacement.dtype == np.dtype("<f8"), "wrong-displacement-type", str(frame))
for name, value in {
"component_names": "UX,UY,UZ,URX,URY,URZ",
"component_unit_dimensions": "length,length,length,1,1,1",
"coordinate_system": "global-cartesian", "location": "nodal",
"step_name": STEP, "source_step_name": STEP,
"rotation_representation": "total-rotation-vector-chart",
"quantity_semantics": "global-translation-and-total-rotation-vector",
}.items():
string_attribute(displacement, name, value)
for name, dtype, value in (
("schema_version", "<u8", schema_version), ("frame_index", "<u8", frame),
("increment_index", "<u8", frame), ("time", "<f8", scheduled_time),
("is_initial", "u1", int(frame == 0)),
):
numeric_attribute(displacement, name, dtype, value)
if schema_version == 2:
auxiliary = dataset(file, f"{frame_path}/{frame}/nodal/algorithmic_acceleration", (NODE_COUNT, 6))
require(auxiliary.dtype == np.dtype("<f8"), "wrong-auxiliary-type", str(frame))
for name, value in {
"component_names": "AUX1,AUX2,AUX3,AUX4,AUX5,AUX6",
"component_unit_dimensions": "length/time^2,length/time^2,length/time^2,1/time^2,1/time^2,1/time^2",
"coordinate_system": "fixed-initial-global-chart", "location": "nodal",
"step_name": STEP, "source_step_name": STEP,
"rotation_representation": "total-rotation-vector-chart",
"quantity_semantics": "full-chart-HHT-filter-combination-not-predictor-driver",
}.items():
string_attribute(auxiliary, name, value)
for name, dtype, value in (
("schema_version", "<u8", schema_version), ("frame_index", "<u8", frame),
("increment_index", "<u8", frame), ("time", "<f8", scheduled_time),
("is_initial", "u1", int(frame == 0)),
):
numeric_attribute(auxiliary, name, dtype, value)
require(np.isfinite(auxiliary[:]).all(), "nonfinite-auxiliary-value", str(frame))
values = displacement[:]
require(np.isfinite(values).all(), "nonfinite-result-value", str(frame))
if frame == 0:
require((values == 0.0).all(), "nonzero-initial-displacement", "Frame zero must have q0=0.")
projected.append(values[selection])
return projected, {"source_input_identity": source_identity, "source_node_count": NODE_COUNT,
"accepted_frame_count": FRAME_COUNT, "checked_result_scalars": NODE_COUNT * FRAME_COUNT * 6,
"projected_node_labels": list(NODE_LABELS)}
def evaluate_family(increment: int, quantity: str, result: list[float],
reference: list[float]) -> dict[str, Any]:
"""Apply the common policy exactly, including zero-scale and family RMS."""
require(len(result) == len(reference) and len(result) > 0, "scalar-inventory", quantity)
require(all(math.isfinite(value) for value in (*result, *reference)),
"nonfinite-comparison-value", quantity)
scale = max(abs(value) for value in reference)
band = POLICY["near_zero_ratio"] * scale
rows = []
error_norm = 0.0
for index, (actual, expected) in enumerate(zip(result, reference)):
error = abs(actual - expected)
require(math.isfinite(error), "nonfinite-comparison-value", quantity)
error_norm = math.hypot(error_norm, error)
require(math.isfinite(error_norm), "nonfinite-comparison-value", quantity)
relative_error = None
if scale == 0.0:
branch, threshold, passed = "zero-scale-exact", 0.0, error == 0.0
elif abs(expected) <= band:
branch, threshold, passed = "near-zero", band, error <= band
else:
branch = "relative"
threshold = POLICY["relative_tolerance"] * abs(expected)
relative_error = error / abs(expected)
require(math.isfinite(relative_error), "nonfinite-comparison-value", quantity)
passed = relative_error <= POLICY["relative_tolerance"]
rows.append({"instance": INSTANCE, "node_label": NODE_LABELS[index // 3],
"component": f"{quantity}{index % 3 + 1}", "fesa_value": actual,
"reference_value": expected, "absolute_error": error,
"relative_error": relative_error, "branch": branch,
"threshold": threshold, "passed": passed})
rows_passed = all(row["passed"] for row in rows)
rms = error_norm / math.sqrt(len(rows)) / scale if scale else None
require(rms is None or math.isfinite(rms), "nonfinite-comparison-value", quantity)
rms_passed = rows_passed if scale == 0.0 else rms <= POLICY["relative_rms_tolerance"]
return {"identity": f"{quantity}-global:{STEP}:increment-{increment}", "case": CASE,
"step": STEP, "increment": increment, "hdf5_time": float(increment) * INCREMENT,
"reference_decimal_time": str(Decimal(increment) / Decimal(10)),
"quantity": quantity, "components": [f"{quantity}{i}" for i in (1, 2, 3)],
"unit_dimension": "length" if quantity == "U" else "rad",
"coordinate_system": "global-cartesian" if quantity == "U" else "fixed-initial-global-total-rotation-vector",
"impact": "blocking" if quantity == "U" else "warning-only", "row_count": len(rows),
"reference_scale": scale, "near_zero_band": band, "rows": rows,
"rows_passed": rows_passed, "relative_rms": rms, "rms_passed": rms_passed,
"diagnostic_code": "zero-reference-scale-nonzero-error" if scale == 0.0 and not rows_passed else None,
"passed": rows_passed and rms_passed}
def compare(input_path: Path, reference_path: Path, results_path: Path) -> dict[str, Any]:
report: dict[str, Any] = {
"schema": "hht-reference-comparison-v1", "case": CASE, "policy": dict(POLICY),
"identities": {}, "precheck": {"passed": False}, "families": [],
"blocking_failures": [], "warnings": [], "blocking_passed": False, "verdict": "failed",
}
try:
for name, path in (("input", input_path), ("reference", reference_path), ("results", results_path)):
report["identities"][name] = {"path": str(path.absolute())}
with path.open("rb") as stream:
report["identities"][name]["sha256"] = hashlib.file_digest(stream, "sha256").hexdigest()
nodes, content_identity = input_inventory(input_path)
reference = read_reference(reference_path)
result, evidence = read_results(results_path, nodes, content_identity)
families = []
for frame in range(FRAME_COUNT):
for quantity, first in (("U", 0), ("UR", 3)):
actual = [float(value) for row in result[frame] for value in row[first:first + 3]]
expected = [value for label in NODE_LABELS for value in reference[(frame, label)][first:first + 3]]
families.append(evaluate_family(frame, quantity, actual, expected))
report["precheck"] = {"passed": True, "reference_row_count": len(reference),
"reference_frame_count": FRAME_COUNT, **evidence}
report["families"] = families
for family in families:
if family["passed"]:
continue
if family["impact"] == "blocking":
report["blocking_failures"].append(family["identity"])
else:
if not family["rows_passed"]:
report["warnings"].append({"family": family["identity"], "code": "row-tolerance-failure"})
if not family["rms_passed"]:
report["warnings"].append({"family": family["identity"], "code": "relative-rms-failure"})
report["blocking_passed"] = not report["blocking_failures"]
report["verdict"] = "passed" if report["blocking_passed"] else "failed"
except (ComparisonError, OSError, ValueError, InvalidOperation, UnicodeError) as error:
report["precheck"] = {"passed": False, "code": getattr(error, "code", "invalid-comparison-input"),
"message": str(error)}
return report
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--input", type=Path, default=REFERENCE_DIRECTORY / f"{CASE}.inp")
parser.add_argument("--reference", type=Path, default=REFERENCE_DIRECTORY / f"{CASE} displacements.csv")
parser.add_argument("--results", type=Path)
parser.add_argument("--report", type=Path, required=True)
args = parser.parse_args()
results = args.results or args.input.with_name(args.input.stem + "_results.h5")
if args.report.resolve() in {args.input.resolve(), args.reference.resolve(), results.resolve()}:
parser.error("The report path must not overwrite an input, reference, or solver result.")
report = compare(args.input, args.reference, results)
args.report.parent.mkdir(parents=True, exist_ok=True)
args.report.write_text(json.dumps(report, indent=2, sort_keys=True, allow_nan=False) + "\n", encoding="utf-8")
print(json.dumps({"verdict": report["verdict"], "precheck": report["precheck"],
"blocking_failure_count": len(report["blocking_failures"]),
"warning_count": len(report["warnings"]), "report": str(args.report)}, allow_nan=False))
return 2 if not report["precheck"]["passed"] else (0 if report["blocking_passed"] else 1)
if __name__ == "__main__":
raise SystemExit(main())