modify documents
This commit is contained in:
@@ -1,317 +0,0 @@
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError: # pragma: no cover
|
||||
import tomli as tomllib
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
AGENTS_ROOT = ROOT / ".codex" / "agents"
|
||||
SKILLS_ROOT = ROOT / ".codex" / "skills"
|
||||
|
||||
DESIGN_DOC = ROOT / "docs" / "ABAQUS_SUBROUTINE_AGENT_DESIGN.md"
|
||||
|
||||
COMMON_SKILL_SECTIONS = (
|
||||
"## Inputs",
|
||||
"## Workflow",
|
||||
"## Output Contract",
|
||||
"## Boundaries",
|
||||
"## Quality Gate",
|
||||
"## Handoff",
|
||||
)
|
||||
|
||||
SKILLS = {
|
||||
"abaqus-subroutine-requirements": (
|
||||
"Subroutine requirements",
|
||||
"ABAQUS-USUB-REQ-<FEATURE>-###",
|
||||
"Requirement Verification Matrix",
|
||||
"UMAT | VUMAT | UEL",
|
||||
"Do not implement Fortran code.",
|
||||
),
|
||||
"abaqus-subroutine-research": (
|
||||
"Research evidence",
|
||||
"official Abaqus documentation",
|
||||
"books, papers, and benchmark sources",
|
||||
"Separate verified facts from inference.",
|
||||
"Abaqus User Subroutines Reference Guide",
|
||||
),
|
||||
"abaqus-subroutine-formulation": (
|
||||
"Finite element formulation",
|
||||
"stress update",
|
||||
"consistent tangent",
|
||||
"state variables",
|
||||
"Do not design Fortran source layout.",
|
||||
),
|
||||
"abaqus-subroutine-numerical-review": (
|
||||
"Numerical review",
|
||||
"finite-difference tangent check",
|
||||
"state variable update",
|
||||
"stability risks",
|
||||
"pass-for-interface-definition",
|
||||
),
|
||||
"abaqus-subroutine-interface": (
|
||||
"Abaqus ABI Contract",
|
||||
"UMAT",
|
||||
"VUMAT",
|
||||
"UEL",
|
||||
"DDSDDE",
|
||||
"STATEV",
|
||||
),
|
||||
"abaqus-subroutine-test-models": (
|
||||
"TDD test model",
|
||||
"no-Abaqus",
|
||||
"tests/fortran/manifest.json",
|
||||
"references/<feature-id>/<model-id>/",
|
||||
"source hash",
|
||||
"msg/dat/log tail",
|
||||
),
|
||||
"abaqus-fortran-tdd": (
|
||||
"Fortran TDD",
|
||||
"RED -> GREEN -> VERIFY",
|
||||
"python scripts/validate_fortran.py",
|
||||
"Intel oneAPI",
|
||||
"ifx",
|
||||
"ifort",
|
||||
),
|
||||
"abaqus-subroutine-validation": (
|
||||
"Subroutine validation",
|
||||
"externally generated",
|
||||
"ready-for-comparison",
|
||||
"source hash",
|
||||
"msg/dat/log",
|
||||
"Do not change tolerance policies.",
|
||||
),
|
||||
"abaqus-subroutine-physics-sanity": (
|
||||
"Physics sanity",
|
||||
"global equilibrium",
|
||||
"reaction consistency",
|
||||
"stress/strain",
|
||||
"state variable",
|
||||
"energy/residual",
|
||||
),
|
||||
"abaqus-subroutine-readiness": (
|
||||
"Readiness audit",
|
||||
"Gate Evidence Inventory",
|
||||
"Known Limitations",
|
||||
"Validation Evidence",
|
||||
"Do not publish, deploy, package, tag, commit, or externally release anything unless the user explicitly asks.",
|
||||
),
|
||||
}
|
||||
|
||||
AGENT_SKILL_REFERENCES = {
|
||||
"coordinator-agent.toml": (
|
||||
"abaqus-subroutine-requirements",
|
||||
"abaqus-subroutine-test-models",
|
||||
"abaqus-subroutine-readiness",
|
||||
),
|
||||
"requirement-agent.toml": ("abaqus-subroutine-requirements",),
|
||||
"research-agent.toml": ("abaqus-subroutine-research", "fem-theory-query"),
|
||||
"formulation-agent.toml": ("abaqus-subroutine-formulation", "fem-theory-query"),
|
||||
"numerical-review-agent.toml": (
|
||||
"abaqus-subroutine-numerical-review",
|
||||
"fem-theory-query",
|
||||
),
|
||||
"io-definition-agent.toml": (
|
||||
"abaqus-subroutine-interface",
|
||||
"fem-theory-query",
|
||||
),
|
||||
"reference-model-agent.toml": (
|
||||
"abaqus-subroutine-test-models",
|
||||
"fem-theory-query",
|
||||
),
|
||||
"implementation-planning-agent.toml": (
|
||||
"abaqus-subroutine-formulation",
|
||||
"abaqus-subroutine-test-models",
|
||||
"abaqus-fortran-tdd",
|
||||
"fem-theory-query",
|
||||
),
|
||||
"implementation-agent.toml": ("abaqus-fortran-tdd",),
|
||||
"build-test-executor-agent.toml": (
|
||||
"abaqus-fortran-tdd",
|
||||
"abaqus-subroutine-validation",
|
||||
),
|
||||
"correction-agent.toml": ("abaqus-fortran-tdd",),
|
||||
"reference-verification-agent.toml": (
|
||||
"abaqus-subroutine-validation",
|
||||
"abaqus-subroutine-interface",
|
||||
),
|
||||
"physics-evaluation-agent.toml": (
|
||||
"abaqus-subroutine-physics-sanity",
|
||||
"fem-theory-query",
|
||||
),
|
||||
"release-agent.toml": ("abaqus-subroutine-readiness",),
|
||||
}
|
||||
|
||||
AGENT_REQUIRED_TERMS = {
|
||||
"coordinator-agent.toml": (
|
||||
"1. Subroutine requirements analysis",
|
||||
"2. Research evidence",
|
||||
"3. Finite element formulation",
|
||||
"4. Abaqus subroutine interface",
|
||||
"5. TDD test models",
|
||||
"6. Fortran implementation",
|
||||
"7. Subroutine validation",
|
||||
),
|
||||
"requirement-agent.toml": ("Subroutine requirements analysis", "ABAQUS-USUB-REQ"),
|
||||
"research-agent.toml": ("books, papers, official Abaqus manuals", "source reliability"),
|
||||
"formulation-agent.toml": ("stress update", "consistent tangent", "state variables"),
|
||||
"numerical-review-agent.toml": (
|
||||
"finite-difference tangent check",
|
||||
"algorithmic consistency",
|
||||
),
|
||||
"io-definition-agent.toml": ("Abaqus ABI Contract", "STRESS", "DDSDDE", "STATEV"),
|
||||
"reference-model-agent.toml": (
|
||||
"tests/fortran/manifest.json",
|
||||
"references/<feature-id>/<model-id>/",
|
||||
),
|
||||
"implementation-planning-agent.toml": (
|
||||
"Fortran source",
|
||||
"no-Abaqus driver",
|
||||
"RED -> GREEN -> VERIFY",
|
||||
),
|
||||
"implementation-agent.toml": ("Fortran source", "Intel oneAPI", "RED -> GREEN -> VERIFY"),
|
||||
"build-test-executor-agent.toml": (
|
||||
"python scripts/validate_fortran.py",
|
||||
"externally generated",
|
||||
),
|
||||
"correction-agent.toml": ("Fortran compile", "minimal correction"),
|
||||
"reference-verification-agent.toml": (
|
||||
"metadata.json",
|
||||
"source hash",
|
||||
"Abaqus version",
|
||||
"compiler version",
|
||||
),
|
||||
"physics-evaluation-agent.toml": ("global equilibrium", "stress/strain", "state variable"),
|
||||
"release-agent.toml": ("Gate Evidence Inventory", "Known Limitations"),
|
||||
}
|
||||
|
||||
|
||||
def parse_frontmatter(text):
|
||||
lines = text.splitlines()
|
||||
if not lines or lines[0] != "---":
|
||||
raise AssertionError("SKILL.md must start with YAML frontmatter")
|
||||
|
||||
fields = {}
|
||||
for line in lines[1:]:
|
||||
if line == "---":
|
||||
return fields
|
||||
key, sep, value = line.partition(":")
|
||||
if not sep:
|
||||
raise AssertionError(f"Invalid frontmatter line: {line}")
|
||||
fields[key.strip()] = value.strip()
|
||||
|
||||
raise AssertionError("SKILL.md frontmatter must be closed")
|
||||
|
||||
|
||||
class AbaqusSubroutineCodexConfigTests(unittest.TestCase):
|
||||
def test_abaqus_subroutine_skill_files_exist_with_metadata(self):
|
||||
for skill_name, body_terms in SKILLS.items():
|
||||
with self.subTest(skill=skill_name):
|
||||
skill_path = SKILLS_ROOT / skill_name / "SKILL.md"
|
||||
self.assertTrue(skill_path.exists(), f"{skill_name} SKILL.md is missing")
|
||||
text = skill_path.read_text(encoding="utf-8")
|
||||
fields = parse_frontmatter(text)
|
||||
|
||||
self.assertEqual(set(fields), {"name", "description"})
|
||||
self.assertEqual(fields["name"], skill_name)
|
||||
self.assertIn("Use when", fields["description"])
|
||||
self.assertIn("Abaqus", fields["description"])
|
||||
self.assertIn("User Subroutine", fields["description"])
|
||||
for section in COMMON_SKILL_SECTIONS:
|
||||
self.assertIn(section, text)
|
||||
self.assertIn("AGENTS.md", text)
|
||||
self.assertIn("docs/ABAQUS_SUBROUTINE_AGENT_DESIGN.md", text)
|
||||
for term in body_terms:
|
||||
self.assertIn(term, text)
|
||||
|
||||
def test_abaqus_subroutine_skills_have_ui_metadata(self):
|
||||
for skill_name in SKILLS:
|
||||
with self.subTest(skill=skill_name):
|
||||
metadata = SKILLS_ROOT / skill_name / "agents" / "openai.yaml"
|
||||
self.assertTrue(metadata.exists(), f"{skill_name} openai.yaml is missing")
|
||||
text = metadata.read_text(encoding="utf-8")
|
||||
self.assertIn("interface:", text)
|
||||
self.assertIn("display_name:", text)
|
||||
self.assertIn("short_description:", text)
|
||||
self.assertIn("default_prompt:", text)
|
||||
self.assertIn(f"${skill_name}", text)
|
||||
|
||||
def test_deprecated_fesa_skill_directories_are_removed(self):
|
||||
deprecated = sorted(p.name for p in SKILLS_ROOT.iterdir() if p.name.startswith("fesa-"))
|
||||
self.assertEqual([], deprecated)
|
||||
|
||||
def test_agents_reference_abaqus_subroutine_skills(self):
|
||||
for agent_file, skill_names in AGENT_SKILL_REFERENCES.items():
|
||||
with self.subTest(agent=agent_file):
|
||||
text = (AGENTS_ROOT / agent_file).read_text(encoding="utf-8")
|
||||
data = tomllib.loads(text)
|
||||
self.assertEqual(data["name"], agent_file.removesuffix(".toml"))
|
||||
self.assertEqual(data["model_reasoning_effort"], "extra high")
|
||||
self.assertIn("Abaqus User Subroutine", data["description"])
|
||||
instructions = data["developer_instructions"]
|
||||
self.assertIn("Skill references:", instructions)
|
||||
self.assertIn("Abaqus User Subroutine", instructions)
|
||||
for skill_name in skill_names:
|
||||
self.assertIn(f"${skill_name}", instructions)
|
||||
for term in AGENT_REQUIRED_TERMS[agent_file]:
|
||||
self.assertIn(term, instructions)
|
||||
|
||||
def test_agent_and_skill_text_no_longer_targets_fesa_cpp_solver_work(self):
|
||||
checked_paths = list(AGENTS_ROOT.glob("*.toml"))
|
||||
checked_paths += [SKILLS_ROOT / name / "SKILL.md" for name in SKILLS]
|
||||
checked_paths += list((SKILLS_ROOT / "harness-workflow").glob("SKILL.md"))
|
||||
checked_paths += list((SKILLS_ROOT / "harness-review").glob("SKILL.md"))
|
||||
checked_paths += list((ROOT / "docs").glob("**/README.md"))
|
||||
|
||||
forbidden_terms = (
|
||||
"FESA",
|
||||
"FESA solver",
|
||||
"FESA FEM",
|
||||
"FESA C++",
|
||||
"Nastran",
|
||||
"C++17/MSVC",
|
||||
"C++/MSVC",
|
||||
"HARNESS_ABAQUS_VALIDATION=run",
|
||||
"opt-in Abaqus",
|
||||
"Abaqus opt-in",
|
||||
"Abaqus execution is valid",
|
||||
"If explicitly configured, run",
|
||||
)
|
||||
for path in checked_paths:
|
||||
with self.subTest(path=path):
|
||||
text = path.read_text(encoding="utf-8")
|
||||
for forbidden in forbidden_terms:
|
||||
self.assertNotIn(forbidden, text)
|
||||
|
||||
def test_harness_skills_target_fortran_subroutine_workflow(self):
|
||||
for skill_name in ("harness-workflow", "harness-review"):
|
||||
with self.subTest(skill=skill_name):
|
||||
text = (SKILLS_ROOT / skill_name / "SKILL.md").read_text(encoding="utf-8")
|
||||
for term in (
|
||||
"Abaqus User Subroutine",
|
||||
"Fortran",
|
||||
"python scripts/validate_fortran.py",
|
||||
"python scripts/validate_reference_artifacts.py",
|
||||
"externally generated",
|
||||
):
|
||||
self.assertIn(term, text)
|
||||
|
||||
def test_design_doc_captures_user_subroutine_process(self):
|
||||
text = DESIGN_DOC.read_text(encoding="utf-8")
|
||||
for term in (
|
||||
"Abaqus User Subroutine development process",
|
||||
"1. Subroutine requirements analysis",
|
||||
"2. Books, papers, and research evidence",
|
||||
"3. Finite element formulation for implementation",
|
||||
"4. Subroutine input/output parameter definition",
|
||||
"5. TDD test model design",
|
||||
"6. Fortran code implementation",
|
||||
"7. Subroutine validation",
|
||||
):
|
||||
self.assertIn(term, text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,434 +0,0 @@
|
||||
import csv
|
||||
import contextlib
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import io
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_compare_extracted_csv():
|
||||
module_path = Path(__file__).resolve().parent / "compare_extracted_csv.py"
|
||||
spec = importlib.util.spec_from_file_location("compare_extracted_csv", module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def write_json(path: Path, payload: dict):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def write_csv(path: Path, rows: list[dict[str, str]]):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("w", newline="", encoding="utf-8") as handle:
|
||||
writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
|
||||
def write_text(path: Path, text: str):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
def metadata_payload() -> dict:
|
||||
return {
|
||||
"schema_version": "abaqus-user-subroutine-artifact-v1",
|
||||
"feature_id": "umat",
|
||||
"model_id": "single-element",
|
||||
"artifact_status": "ready-for-comparison",
|
||||
"abaqus": {"version": "2024", "precision": "double"},
|
||||
"compiler": {"vendor": "Intel oneAPI", "name": "ifx", "version": "2024"},
|
||||
"subroutine": {"entry_points": ["UMAT"], "source_files": []},
|
||||
"input_file": "model.inp",
|
||||
"outputs": {
|
||||
"tails": {
|
||||
"msg": "job.msg.tail.txt",
|
||||
"dat": "job.dat.tail.txt",
|
||||
"log": "job.log.tail.txt",
|
||||
"sta": "job.sta.tail.txt",
|
||||
},
|
||||
"csv": {"stresses": "extracted/stresses.csv"},
|
||||
},
|
||||
"extraction": {
|
||||
"source_odb": "job.odb",
|
||||
"tool": "Abaqus Python",
|
||||
"extracted_at": "2026-06-10T00:00:00+09:00",
|
||||
"csv_directory": "extracted",
|
||||
},
|
||||
"comparisons": {
|
||||
"stresses": {
|
||||
"reference_csv": "extracted/stresses.csv",
|
||||
"actual_csv": "extracted/stresses.csv",
|
||||
"required_columns": [
|
||||
"step",
|
||||
"frame",
|
||||
"instance",
|
||||
"element_label",
|
||||
"integration_point",
|
||||
"section_point",
|
||||
"output_position",
|
||||
"component",
|
||||
"coordinate_system",
|
||||
"unit",
|
||||
"value",
|
||||
],
|
||||
"key_columns": [
|
||||
"step",
|
||||
"frame",
|
||||
"instance",
|
||||
"element_label",
|
||||
"integration_point",
|
||||
"section_point",
|
||||
"output_position",
|
||||
"component",
|
||||
],
|
||||
"value_column": "value",
|
||||
"unit_column": "unit",
|
||||
"coordinate_system_column": "coordinate_system",
|
||||
"tolerance": {"absolute": 1.0e-8, "relative": 1.0e-6, "relative_floor": 1.0e-12},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def stress_rows(value: str = "100.0") -> list[dict[str, str]]:
|
||||
return [
|
||||
{
|
||||
"step": "Step-1",
|
||||
"frame": "1",
|
||||
"instance": "PART-1-1",
|
||||
"element_label": "1",
|
||||
"integration_point": "1",
|
||||
"section_point": "",
|
||||
"output_position": "INTEGRATION_POINT",
|
||||
"component": "S11",
|
||||
"coordinate_system": "GLOBAL",
|
||||
"unit": "MPa",
|
||||
"value": value,
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
def make_metadata_valid_for_artifact_validation(root: Path, model_dir: Path, payload: dict) -> dict:
|
||||
source = root / "src" / "fortran" / "abaqus" / "UMAT.for"
|
||||
source.parent.mkdir(parents=True, exist_ok=True)
|
||||
source.write_text(" subroutine umat()\n end\n", encoding="utf-8")
|
||||
source_hash = hashlib.sha256(source.read_bytes()).hexdigest()
|
||||
payload["subroutine"]["source_files"] = [
|
||||
{
|
||||
"path": "src/fortran/abaqus/UMAT.for",
|
||||
"language": "Fortran",
|
||||
"sha256": source_hash,
|
||||
}
|
||||
]
|
||||
for name in [
|
||||
"model.inp",
|
||||
"job.msg.tail.txt",
|
||||
"job.dat.tail.txt",
|
||||
"job.log.tail.txt",
|
||||
"job.sta.tail.txt",
|
||||
]:
|
||||
path = model_dir / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text("ok\n", encoding="utf-8")
|
||||
return payload
|
||||
|
||||
|
||||
def compare_stresses(reference_rows: list[dict[str, str]], actual_rows: list[dict[str, str]]) -> dict:
|
||||
compare = load_compare_extracted_csv()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
reference = root / "references" / "umat" / "single-element"
|
||||
actual = root / "external-results" / "umat" / "single-element"
|
||||
write_json(reference / "metadata.json", metadata_payload())
|
||||
write_csv(reference / "extracted" / "stresses.csv", reference_rows)
|
||||
write_csv(actual / "extracted" / "stresses.csv", actual_rows)
|
||||
|
||||
return compare.compare_metadata(
|
||||
reference / "metadata.json",
|
||||
actual,
|
||||
quantities=["stresses"],
|
||||
validate_artifacts=False,
|
||||
)
|
||||
|
||||
|
||||
def call_main_silently(compare, argv: list[str]) -> int:
|
||||
with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
|
||||
return compare.main(argv)
|
||||
|
||||
|
||||
class CompareExtractedCsvTests(unittest.TestCase):
|
||||
def test_quantity_passes_when_schema_keys_units_and_values_match_within_tolerance(self):
|
||||
compare = load_compare_extracted_csv()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
reference = root / "references" / "umat" / "single-element"
|
||||
actual = root / "external-results" / "umat" / "single-element"
|
||||
write_json(reference / "metadata.json", metadata_payload())
|
||||
write_csv(reference / "extracted" / "stresses.csv", stress_rows("100.0"))
|
||||
write_csv(actual / "extracted" / "stresses.csv", stress_rows("100.00000001"))
|
||||
|
||||
report = compare.compare_metadata(
|
||||
reference / "metadata.json",
|
||||
actual,
|
||||
quantities=["stresses"],
|
||||
validate_artifacts=False,
|
||||
)
|
||||
|
||||
self.assertEqual(report["overall_result"], "pass")
|
||||
self.assertEqual(report["quantities"][0]["result"], "pass")
|
||||
self.assertEqual(report["quantities"][0]["classification"], "N/A")
|
||||
self.assertEqual(report["quantities"][0]["compared_rows"], 1)
|
||||
|
||||
def test_missing_actual_csv_is_missing_generated_output(self):
|
||||
compare = load_compare_extracted_csv()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
reference = root / "references" / "umat" / "single-element"
|
||||
actual = root / "external-results" / "umat" / "single-element"
|
||||
write_json(reference / "metadata.json", metadata_payload())
|
||||
write_csv(reference / "extracted" / "stresses.csv", stress_rows("100.0"))
|
||||
|
||||
report = compare.compare_metadata(
|
||||
reference / "metadata.json",
|
||||
actual,
|
||||
quantities=["stresses"],
|
||||
validate_artifacts=False,
|
||||
)
|
||||
|
||||
self.assertEqual(report["overall_result"], "fail")
|
||||
self.assertEqual(report["quantities"][0]["classification"], "missing-generated-output")
|
||||
|
||||
def test_missing_required_column_is_schema_mismatch(self):
|
||||
compare = load_compare_extracted_csv()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
reference = root / "references" / "umat" / "single-element"
|
||||
actual = root / "external-results" / "umat" / "single-element"
|
||||
write_json(reference / "metadata.json", metadata_payload())
|
||||
row = stress_rows("100.0")[0]
|
||||
write_csv(reference / "extracted" / "stresses.csv", [row])
|
||||
actual_row = dict(row)
|
||||
actual_row.pop("coordinate_system")
|
||||
write_csv(actual / "extracted" / "stresses.csv", [actual_row])
|
||||
|
||||
report = compare.compare_metadata(
|
||||
reference / "metadata.json",
|
||||
actual,
|
||||
quantities=["stresses"],
|
||||
validate_artifacts=False,
|
||||
)
|
||||
|
||||
self.assertEqual(report["overall_result"], "fail")
|
||||
self.assertEqual(report["quantities"][0]["classification"], "schema-mismatch")
|
||||
|
||||
def test_missing_quantity_contract_is_upstream_contract(self):
|
||||
compare = load_compare_extracted_csv()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
reference = root / "references" / "umat" / "single-element"
|
||||
actual = root / "external-results" / "umat" / "single-element"
|
||||
payload = metadata_payload()
|
||||
payload["comparisons"].pop("stresses")
|
||||
write_json(reference / "metadata.json", payload)
|
||||
|
||||
report = compare.compare_metadata(
|
||||
reference / "metadata.json",
|
||||
actual,
|
||||
quantities=["stresses"],
|
||||
validate_artifacts=False,
|
||||
)
|
||||
|
||||
self.assertEqual(report["overall_result"], "fail")
|
||||
self.assertEqual(report["quantities"][0]["classification"], "upstream-contract")
|
||||
|
||||
def test_missing_comparisons_block_is_upstream_contract(self):
|
||||
compare = load_compare_extracted_csv()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
reference = root / "references" / "umat" / "single-element"
|
||||
actual = root / "external-results" / "umat" / "single-element"
|
||||
payload = metadata_payload()
|
||||
payload.pop("comparisons")
|
||||
write_json(reference / "metadata.json", payload)
|
||||
|
||||
report = compare.compare_metadata(reference / "metadata.json", actual, validate_artifacts=False)
|
||||
|
||||
self.assertEqual(report["overall_result"], "fail")
|
||||
self.assertEqual(report["quantities"][0]["classification"], "upstream-contract")
|
||||
|
||||
def test_incomplete_quantity_contract_is_upstream_contract(self):
|
||||
compare = load_compare_extracted_csv()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
reference = root / "references" / "umat" / "single-element"
|
||||
actual = root / "external-results" / "umat" / "single-element"
|
||||
payload = metadata_payload()
|
||||
payload["comparisons"]["stresses"].pop("value_column")
|
||||
write_json(reference / "metadata.json", payload)
|
||||
|
||||
report = compare.compare_metadata(
|
||||
reference / "metadata.json",
|
||||
actual,
|
||||
quantities=["stresses"],
|
||||
validate_artifacts=False,
|
||||
)
|
||||
|
||||
self.assertEqual(report["overall_result"], "fail")
|
||||
self.assertEqual(report["quantities"][0]["classification"], "upstream-contract")
|
||||
|
||||
def test_duplicate_header_is_schema_mismatch(self):
|
||||
compare = load_compare_extracted_csv()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
reference = root / "references" / "umat" / "single-element"
|
||||
actual = root / "external-results" / "umat" / "single-element"
|
||||
write_json(reference / "metadata.json", metadata_payload())
|
||||
write_csv(reference / "extracted" / "stresses.csv", stress_rows("100.0"))
|
||||
header = ",".join(list(stress_rows("100.0")[0]) + ["value"])
|
||||
row = ",".join(stress_rows("100.0")[0].values()) + ",100.0"
|
||||
write_text(actual / "extracted" / "stresses.csv", f"{header}\n{row}\n")
|
||||
|
||||
report = compare.compare_metadata(
|
||||
reference / "metadata.json",
|
||||
actual,
|
||||
quantities=["stresses"],
|
||||
validate_artifacts=False,
|
||||
)
|
||||
|
||||
self.assertEqual(report["overall_result"], "fail")
|
||||
self.assertEqual(report["quantities"][0]["classification"], "schema-mismatch")
|
||||
|
||||
def test_changed_row_key_is_id_mismatch(self):
|
||||
actual_row = dict(stress_rows("100.0")[0])
|
||||
actual_row["element_label"] = "2"
|
||||
|
||||
report = compare_stresses(stress_rows("100.0"), [actual_row])
|
||||
|
||||
self.assertEqual(report["overall_result"], "fail")
|
||||
self.assertEqual(report["quantities"][0]["classification"], "id-mismatch")
|
||||
self.assertEqual(report["quantities"][0]["missing_rows"], 1)
|
||||
self.assertEqual(report["quantities"][0]["extra_rows"], 1)
|
||||
|
||||
def test_unit_mismatch_is_unit_or_coordinate_mismatch(self):
|
||||
actual_row = dict(stress_rows("100.0")[0])
|
||||
actual_row["unit"] = "Pa"
|
||||
|
||||
report = compare_stresses(stress_rows("100.0"), [actual_row])
|
||||
|
||||
self.assertEqual(report["overall_result"], "fail")
|
||||
self.assertEqual(report["quantities"][0]["classification"], "unit-or-coordinate-mismatch")
|
||||
|
||||
def test_coordinate_system_mismatch_is_unit_or_coordinate_mismatch(self):
|
||||
actual_row = dict(stress_rows("100.0")[0])
|
||||
actual_row["coordinate_system"] = "LOCAL-1"
|
||||
|
||||
report = compare_stresses(stress_rows("100.0"), [actual_row])
|
||||
|
||||
self.assertEqual(report["overall_result"], "fail")
|
||||
self.assertEqual(report["quantities"][0]["classification"], "unit-or-coordinate-mismatch")
|
||||
|
||||
def test_nonfinite_value_is_nonfinite_result(self):
|
||||
report = compare_stresses(stress_rows("100.0"), stress_rows("nan"))
|
||||
|
||||
self.assertEqual(report["overall_result"], "fail")
|
||||
self.assertEqual(report["quantities"][0]["classification"], "nonfinite-result")
|
||||
|
||||
def test_value_outside_tolerance_is_tolerance_failure(self):
|
||||
report = compare_stresses(stress_rows("100.0"), stress_rows("101.0"))
|
||||
|
||||
self.assertEqual(report["overall_result"], "fail")
|
||||
self.assertEqual(report["quantities"][0]["classification"], "tolerance-failure")
|
||||
self.assertEqual(report["quantities"][0]["max_abs_error"], 1.0)
|
||||
self.assertEqual(report["quantities"][0]["result"], "fail")
|
||||
|
||||
def test_cli_writes_json_report_and_returns_zero_when_all_quantities_pass(self):
|
||||
compare = load_compare_extracted_csv()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
reference = root / "references" / "umat" / "single-element"
|
||||
actual = root / "external-results" / "umat" / "single-element"
|
||||
report_json = root / "build" / "reference-verification" / "umat-single-element.json"
|
||||
payload = make_metadata_valid_for_artifact_validation(root, reference, metadata_payload())
|
||||
write_json(reference / "metadata.json", payload)
|
||||
write_csv(reference / "extracted" / "stresses.csv", stress_rows("100.0"))
|
||||
write_csv(actual / "extracted" / "stresses.csv", stress_rows("100.00000001"))
|
||||
|
||||
exit_code = call_main_silently(
|
||||
compare,
|
||||
[
|
||||
"--metadata",
|
||||
str(reference / "metadata.json"),
|
||||
"--actual-root",
|
||||
str(actual),
|
||||
"--quantity",
|
||||
"stresses",
|
||||
"--report-json",
|
||||
str(report_json),
|
||||
],
|
||||
)
|
||||
|
||||
report = json.loads(report_json.read_text(encoding="utf-8"))
|
||||
|
||||
self.assertEqual(exit_code, 0)
|
||||
self.assertEqual(report["overall_result"], "pass")
|
||||
|
||||
def test_cli_writes_json_report_and_returns_one_when_a_quantity_fails(self):
|
||||
compare = load_compare_extracted_csv()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
reference = root / "references" / "umat" / "single-element"
|
||||
actual = root / "external-results" / "umat" / "single-element"
|
||||
report_json = root / "build" / "reference-verification" / "umat-single-element.json"
|
||||
payload = make_metadata_valid_for_artifact_validation(root, reference, metadata_payload())
|
||||
write_json(reference / "metadata.json", payload)
|
||||
write_csv(reference / "extracted" / "stresses.csv", stress_rows("100.0"))
|
||||
write_csv(actual / "extracted" / "stresses.csv", stress_rows("101.0"))
|
||||
|
||||
exit_code = call_main_silently(
|
||||
compare,
|
||||
[
|
||||
"--metadata",
|
||||
str(reference / "metadata.json"),
|
||||
"--actual-root",
|
||||
str(actual),
|
||||
"--quantity",
|
||||
"stresses",
|
||||
"--report-json",
|
||||
str(report_json),
|
||||
],
|
||||
)
|
||||
|
||||
report = json.loads(report_json.read_text(encoding="utf-8"))
|
||||
|
||||
self.assertEqual(exit_code, 1)
|
||||
self.assertEqual(report["overall_result"], "fail")
|
||||
|
||||
def test_cli_returns_two_for_invalid_arguments(self):
|
||||
compare = load_compare_extracted_csv()
|
||||
|
||||
self.assertEqual(call_main_silently(compare, []), 2)
|
||||
|
||||
def test_default_comparison_validates_reference_artifact_metadata(self):
|
||||
compare = load_compare_extracted_csv()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
reference = root / "references" / "umat" / "single-element"
|
||||
actual = root / "external-results" / "umat" / "single-element"
|
||||
write_json(reference / "metadata.json", metadata_payload())
|
||||
write_csv(reference / "extracted" / "stresses.csv", stress_rows("100.0"))
|
||||
write_csv(actual / "extracted" / "stresses.csv", stress_rows("100.0"))
|
||||
|
||||
report = compare.compare_metadata(reference / "metadata.json", actual)
|
||||
|
||||
self.assertEqual(report["overall_result"], "fail")
|
||||
self.assertEqual(report["quantities"][0]["classification"], "missing-reference-artifact")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,82 +0,0 @@
|
||||
import importlib.util
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
def load_fortran_toolchain():
|
||||
module_path = Path(__file__).resolve().parent / "fortran_toolchain.py"
|
||||
spec = importlib.util.spec_from_file_location("fortran_toolchain", module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class FortranToolchainTests(unittest.TestCase):
|
||||
def test_auto_prefers_ifx_over_ifort_when_both_are_on_path(self):
|
||||
fortran_toolchain = load_fortran_toolchain()
|
||||
|
||||
def fake_which(name):
|
||||
return f"C:\\oneapi\\{name}.exe" if name in {"ifx", "ifort"} else None
|
||||
|
||||
with patch.object(fortran_toolchain.shutil, "which", side_effect=fake_which):
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
toolchain = fortran_toolchain.resolve_toolchain()
|
||||
|
||||
self.assertEqual(toolchain.name, "ifx")
|
||||
self.assertEqual(toolchain.executable, "C:\\oneapi\\ifx.exe")
|
||||
self.assertIsNone(toolchain.env_script)
|
||||
|
||||
def test_auto_uses_oneapi_env_script_when_compiler_is_not_on_path(self):
|
||||
fortran_toolchain = load_fortran_toolchain()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
vars_bat = Path(tmp) / "setvars.bat"
|
||||
vars_bat.write_text("@echo off\n", encoding="utf-8")
|
||||
|
||||
with patch.object(fortran_toolchain.shutil, "which", return_value=None):
|
||||
with patch.object(fortran_toolchain, "ONEAPI_VARS_CANDIDATES", [vars_bat]):
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
toolchain = fortran_toolchain.resolve_toolchain()
|
||||
|
||||
self.assertEqual(toolchain.name, "ifx")
|
||||
self.assertEqual(toolchain.executable, "ifx")
|
||||
self.assertEqual(toolchain.env_script, vars_bat)
|
||||
|
||||
def test_explicit_compiler_preference_is_honored(self):
|
||||
fortran_toolchain = load_fortran_toolchain()
|
||||
|
||||
def fake_which(name):
|
||||
return f"C:\\oneapi\\{name}.exe"
|
||||
|
||||
with patch.object(fortran_toolchain.shutil, "which", side_effect=fake_which):
|
||||
with patch.dict(os.environ, {"HARNESS_FORTRAN_COMPILER": "ifort"}, clear=True):
|
||||
toolchain = fortran_toolchain.resolve_toolchain()
|
||||
|
||||
self.assertEqual(toolchain.name, "ifort")
|
||||
self.assertEqual(toolchain.executable, "C:\\oneapi\\ifort.exe")
|
||||
|
||||
def test_wrap_command_calls_oneapi_env_before_compiler(self):
|
||||
fortran_toolchain = load_fortran_toolchain()
|
||||
toolchain = fortran_toolchain.FortranToolchain(
|
||||
name="ifx",
|
||||
executable="ifx",
|
||||
env_script=Path(r"C:\Program Files (x86)\Intel\oneAPI\setvars.bat"),
|
||||
)
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"ComSpec": r"C:\Windows\System32\cmd.exe", "SystemRoot": r"C:\Windows"},
|
||||
clear=True,
|
||||
):
|
||||
command = fortran_toolchain.wrap_command(toolchain, ["ifx", "/nologo", "test.f90"])
|
||||
|
||||
self.assertIn(r"C:\Windows\System32\cmd.exe /d /s /c", command)
|
||||
self.assertIn('set "PATH=C:\\Windows\\System32;C:\\Windows"', command)
|
||||
self.assertIn('call "C:\\Program Files (x86)\\Intel\\oneAPI\\setvars.bat" intel64', command)
|
||||
self.assertIn("ifx /nologo test.f90", command)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,191 +0,0 @@
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def load_validate_reference_artifacts():
|
||||
module_path = Path(__file__).resolve().parent / "validate_reference_artifacts.py"
|
||||
spec = importlib.util.spec_from_file_location("validate_reference_artifacts", module_path)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
def write_json(path: Path, payload: dict):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
|
||||
|
||||
|
||||
def write_text(path: Path, text: str = "ok\n"):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(text, encoding="utf-8")
|
||||
|
||||
|
||||
def ready_metadata(source_hash: str) -> dict:
|
||||
return {
|
||||
"schema_version": "abaqus-user-subroutine-artifact-v1",
|
||||
"feature_id": "umat",
|
||||
"model_id": "single-element",
|
||||
"artifact_status": "ready-for-comparison",
|
||||
"abaqus": {"version": "2024", "precision": "double"},
|
||||
"compiler": {"vendor": "Intel oneAPI", "name": "ifx", "version": "2024"},
|
||||
"subroutine": {
|
||||
"entry_points": ["UMAT"],
|
||||
"source_files": [
|
||||
{
|
||||
"path": "src/fortran/abaqus/UMAT.for",
|
||||
"language": "Fortran",
|
||||
"sha256": source_hash,
|
||||
}
|
||||
],
|
||||
},
|
||||
"input_file": "model.inp",
|
||||
"outputs": {
|
||||
"tails": {
|
||||
"msg": "job.msg.tail.txt",
|
||||
"dat": "job.dat.tail.txt",
|
||||
"log": "job.log.tail.txt",
|
||||
"sta": "job.sta.tail.txt",
|
||||
},
|
||||
"csv": {"stresses": "extracted/stresses.csv"},
|
||||
},
|
||||
"extraction": {
|
||||
"source_odb": "job.odb",
|
||||
"tool": "Abaqus Python",
|
||||
"extracted_at": "2026-06-10T00:00:00+09:00",
|
||||
"csv_directory": "extracted",
|
||||
"script": "extraction/extract_odb_to_csv.py",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def create_ready_bundle(root: Path) -> tuple[Path, dict]:
|
||||
source = root / "src" / "fortran" / "abaqus" / "UMAT.for"
|
||||
write_text(source, " subroutine umat()\n end\n")
|
||||
source_hash = hashlib.sha256(source.read_bytes()).hexdigest()
|
||||
|
||||
model_dir = root / "references" / "umat" / "single-element"
|
||||
for name in [
|
||||
"model.inp",
|
||||
"job.msg.tail.txt",
|
||||
"job.dat.tail.txt",
|
||||
"job.log.tail.txt",
|
||||
"job.sta.tail.txt",
|
||||
"extracted/stresses.csv",
|
||||
"extraction/extract_odb_to_csv.py",
|
||||
]:
|
||||
write_text(model_dir / name)
|
||||
|
||||
return model_dir, ready_metadata(source_hash)
|
||||
|
||||
|
||||
class ValidateReferenceArtifactsTests(unittest.TestCase):
|
||||
def test_missing_references_directory_is_valid(self):
|
||||
validator = load_validate_reference_artifacts()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
self.assertEqual(validator.validate_root(Path(tmp)), [])
|
||||
|
||||
def test_draft_metadata_with_minimal_provenance_is_valid(self):
|
||||
validator = load_validate_reference_artifacts()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
write_json(
|
||||
root / "references" / "umat" / "single-element" / "metadata.json",
|
||||
{
|
||||
"schema_version": "abaqus-user-subroutine-artifact-v1",
|
||||
"feature_id": "umat",
|
||||
"model_id": "single-element",
|
||||
"artifact_status": "draft",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(validator.validate_root(root), [])
|
||||
|
||||
def test_ready_for_comparison_requires_declared_files(self):
|
||||
validator = load_validate_reference_artifacts()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
write_json(
|
||||
root / "references" / "umat" / "single-element" / "metadata.json",
|
||||
{
|
||||
"schema_version": "abaqus-user-subroutine-artifact-v1",
|
||||
"feature_id": "umat",
|
||||
"model_id": "single-element",
|
||||
"artifact_status": "ready-for-comparison",
|
||||
"abaqus": {"version": "2024", "precision": "double"},
|
||||
"compiler": {"vendor": "Intel oneAPI", "name": "ifx", "version": "2024"},
|
||||
"subroutine": {
|
||||
"entry_points": ["UMAT"],
|
||||
"source_files": [{"path": "src/fortran/abaqus/UMAT.for", "language": "Fortran", "sha256": "abc"}],
|
||||
},
|
||||
"input_file": "model.inp",
|
||||
"outputs": {
|
||||
"tails": {"msg": "job.msg.tail.txt", "dat": "job.dat.tail.txt", "log": "job.log.tail.txt"},
|
||||
"csv": {"stresses": "extracted/stresses.csv"},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
errors = validator.validate_root(root)
|
||||
|
||||
self.assertTrue(any("missing input_file" in error for error in errors))
|
||||
self.assertTrue(any("missing output tail sta" in error for error in errors))
|
||||
self.assertTrue(any("missing ready-for-comparison key extraction" in error for error in errors))
|
||||
self.assertTrue(any("missing csv output" in error for error in errors))
|
||||
self.assertTrue(any("missing source file" in error for error in errors))
|
||||
|
||||
def test_ready_for_comparison_accepts_external_bundle_without_abaqus_command(self):
|
||||
validator = load_validate_reference_artifacts()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
model_dir, payload = create_ready_bundle(root)
|
||||
payload["extraction"]["odb_sha256_file"] = "result.odb.sha256"
|
||||
write_text(model_dir / "result.odb.sha256", "0" * 64 + " job.odb\n")
|
||||
write_json(model_dir / "metadata.json", payload)
|
||||
|
||||
self.assertEqual(validator.validate_root(root), [])
|
||||
|
||||
def test_ready_for_comparison_checks_source_sha256(self):
|
||||
validator = load_validate_reference_artifacts()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
model_dir, payload = create_ready_bundle(root)
|
||||
payload["subroutine"]["source_files"][0]["sha256"] = "0" * 64
|
||||
write_json(model_dir / "metadata.json", payload)
|
||||
|
||||
errors = validator.validate_root(root)
|
||||
|
||||
self.assertTrue(any("sha256 mismatch" in error for error in errors))
|
||||
|
||||
def test_ready_for_comparison_rejects_csv_outside_extracted_directory(self):
|
||||
validator = load_validate_reference_artifacts()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
model_dir, payload = create_ready_bundle(root)
|
||||
payload["outputs"]["csv"] = {"stresses": "stresses.csv"}
|
||||
write_text(model_dir / "stresses.csv")
|
||||
write_json(model_dir / "metadata.json", payload)
|
||||
|
||||
errors = validator.validate_root(root)
|
||||
|
||||
self.assertTrue(any("csv output stresses must match extracted/*.csv" in error for error in errors))
|
||||
|
||||
def test_ready_for_comparison_checks_optional_odb_sha256_file(self):
|
||||
validator = load_validate_reference_artifacts()
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
root = Path(tmp)
|
||||
model_dir, payload = create_ready_bundle(root)
|
||||
payload["extraction"]["odb_sha256_file"] = "result.odb.sha256"
|
||||
write_text(model_dir / "result.odb.sha256", "not-a-sha\n")
|
||||
write_json(model_dir / "metadata.json", payload)
|
||||
|
||||
errors = validator.validate_root(root)
|
||||
|
||||
self.assertTrue(any("invalid odb_sha256_file" in error for error in errors))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user