Text Generation
PEFT
Safetensors
English
lora
qwen2.5
civil-nuclear
reactor-physics
reactor-kinetics
synthetic-data
vers3dynamics
conversational
Instructions to use ciaochris/Vers3Dynamics-Civil-Reactor-Expert-3B with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use ciaochris/Vers3Dynamics-Civil-Reactor-Expert-3B with PEFT:
from peft import PeftModel from transformers import AutoModelForCausalLM base_model = AutoModelForCausalLM.from_pretrained("Qwen/Qwen2.5-3B-Instruct") model = PeftModel.from_pretrained(base_model, "ciaochris/Vers3Dynamics-Civil-Reactor-Expert-3B") - Notebooks
- Google Colab
- Kaggle
File size: 11,911 Bytes
9c2a086 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 | """Numeric-correctness evaluation for the civil reactor model.
Why this exists
---------------
The repository's existing benchmarks score assistant-token negative
log-likelihood / perplexity. The project's own independent eval showed that
metric rewards format memorisation that does not transfer: an adapter can score
"53% better" on a template-sharing benchmark while making the model *worse* than
the base on open-ended content. Perplexity is the wrong yardstick for a model
whose whole point is producing *correct numbers*.
This harness measures the thing that actually matters: does the stated number
match what the audited engine computes, within a declared tolerance? Ground
truth is not hand-written; it is computed at run time from the same engines the
answer is supposed to agree with (via ``civil_tools``), so the suite cannot
drift from the physics and there is nothing to keep in sync by hand.
Three modes
-----------
* ``--self-check`` (default): recompute every item's ground truth and confirm
the suite's own ``reference_answer`` contains it within tolerance. Needs no
model and runs in CI; it validates both the suite and the scorer.
* ``--answers FILE``: score a JSONL of model outputs (``{"id", "answer"}``) and
report per-category and overall accuracy.
* ``--endpoint-url URL``: query an OpenAI-compatible local endpoint for each
prompt, then score. Uses only stdlib ``urllib`` — no extra dependency.
A number is scored, not prose. Extraction masks isotope labels (``Xe-135``,
``I-135``, ``U-235``) so a mass number is never mistaken for an answer, then
compares every remaining number in the text against the engine value with
``math.isclose``. A relative tolerance travels with each item, because "within
2%" means different things for a period (~50 s) and an inventory (~1e15 /cm^3).
"""
from __future__ import annotations
import argparse
import json
import math
import re
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from civil_safety import read_jsonl
from civil_tools import call_tool
ROOT = Path(__file__).resolve().parent
DEFAULT_SUITE = ROOT / "eval_numeric.jsonl"
# A signed integer/decimal/scientific literal. Kept deliberately strict so it
# does not swallow stray punctuation.
NUMBER_RE = re.compile(r"[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?")
# Element-mass isotope labels (Xe-135, I-135, U-235, Np-239). Masked before
# number extraction so "-135" in "Xe-135" is never read as the answer -135.
ISOTOPE_RE = re.compile(r"[A-Za-z]{1,2}-\d{1,3}\b")
def mask_isotopes(text: str) -> str:
"""Blank out element-mass labels so their mass numbers are not extracted."""
return ISOTOPE_RE.sub(" ", text)
def extract_numbers(text: str) -> list[float]:
"""Every numeric literal in ``text`` (isotope labels masked out first)."""
cleaned = mask_isotopes(text)
values: list[float] = []
for match in NUMBER_RE.finditer(cleaned):
token = match.group()
try:
values.append(float(token))
except ValueError:
continue
return values
def answer_matches(
expected: float, text: str, *, rel_tol: float, abs_tol: float
) -> tuple[bool, float | None]:
"""True iff some number in ``text`` is close to ``expected``.
Returns the matched value too, for reporting. ``abs_tol`` covers values at
or near zero, where a relative tolerance is meaningless.
"""
for value in extract_numbers(text):
if math.isclose(value, expected, rel_tol=rel_tol, abs_tol=abs_tol):
return True, value
return False, None
def ground_truth(item: dict[str, Any]) -> float:
"""Engine-computed reference value for an item's ``check`` spec."""
check = item.get("check")
if not isinstance(check, dict):
raise ValueError(f"{item.get('id', '<no id>')}: missing 'check' object")
tool = check.get("tool")
field_name = check.get("field")
args = check.get("args", {})
result = call_tool(str(tool), args)
if not result.ok:
raise ValueError(
f"{item.get('id')}: ground-truth tool '{tool}' failed: {result.error}"
)
if field_name not in result.values:
raise ValueError(
f"{item.get('id')}: tool '{tool}' has no field '{field_name}' "
f"(has: {', '.join(result.values)})"
)
return result.values[str(field_name)]
@dataclass
class ScoreRow:
id: str
category: str
expected: float
passed: bool
matched: float | None
detail: str = ""
@dataclass
class Report:
rows: list[ScoreRow] = field(default_factory=list)
@property
def total(self) -> int:
return len(self.rows)
@property
def passed(self) -> int:
return sum(1 for r in self.rows if r.passed)
@property
def accuracy(self) -> float:
return self.passed / self.total if self.total else 0.0
def by_category(self) -> dict[str, tuple[int, int]]:
acc: dict[str, list[int]] = {}
for row in self.rows:
bucket = acc.setdefault(row.category, [0, 0])
bucket[0] += int(row.passed)
bucket[1] += 1
return {k: (v[0], v[1]) for k, v in sorted(acc.items())}
def to_dict(self) -> dict[str, Any]:
return {
"total": self.total,
"passed": self.passed,
"accuracy": self.accuracy,
"by_category": {
k: {"passed": p, "total": t} for k, (p, t) in self.by_category().items()
},
"failures": [
{
"id": r.id,
"category": r.category,
"expected": r.expected,
"matched": r.matched,
"detail": r.detail,
}
for r in self.rows
if not r.passed
],
}
def render(self) -> str:
lines = ["Numeric-correctness report", "=" * 34]
for category, (p, t) in self.by_category().items():
lines.append(f" {category:34s} {p:3d}/{t:<3d} {(p / t if t else 0):6.1%}")
lines.append("-" * 34)
lines.append(
f" {'OVERALL':34s} {self.passed:3d}/{self.total:<3d} {self.accuracy:6.1%}"
)
failures = [r for r in self.rows if not r.passed]
if failures:
lines.append("\nFailures:")
for r in failures:
got = "no numeric match" if r.matched is None else f"got {r.matched:g}"
lines.append(
f" - {r.id} ({r.category}): expected {r.expected:g}, {got}. {r.detail}"
)
return "\n".join(lines)
def _tolerances(item: dict[str, Any]) -> tuple[float, float]:
check = item.get("check", {})
return float(check.get("rel_tol", 0.02)), float(check.get("abs_tol", 1e-9))
def score_answers(items: list[dict[str, Any]], answers: dict[str, str]) -> Report:
"""Score model answers (id -> answer text) against engine ground truth."""
report = Report()
for item in items:
item_id = str(item.get("id", "<no id>"))
category = str(item.get("category", "uncategorized"))
expected = ground_truth(item)
rel_tol, abs_tol = _tolerances(item)
text = answers.get(item_id)
if text is None:
report.rows.append(
ScoreRow(item_id, category, expected, False, None, "no answer supplied")
)
continue
passed, matched = answer_matches(
expected, text, rel_tol=rel_tol, abs_tol=abs_tol
)
report.rows.append(ScoreRow(item_id, category, expected, passed, matched))
return report
def self_check(items: list[dict[str, Any]]) -> Report:
"""Confirm each item's reference_answer embeds its engine ground truth."""
answers = {}
for item in items:
ref = item.get("reference_answer")
if not isinstance(ref, str) or not ref.strip():
raise ValueError(
f"{item.get('id')}: self-check needs a 'reference_answer' string"
)
answers[str(item.get("id"))] = ref
return score_answers(items, answers)
def _query_endpoint(url: str, prompt: str, *, system: str, timeout: float) -> str:
"""POST one chat completion to an OpenAI-compatible endpoint (stdlib only)."""
import urllib.error
import urllib.request
payload = json.dumps(
{
"model": "vers3dynamics-civil-reactor-expert",
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": prompt},
],
"temperature": 0.0,
"max_tokens": 512,
}
).encode("utf-8")
request = urllib.request.Request(
url, data=payload, headers={"Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(request, timeout=timeout) as response:
body = json.loads(response.read().decode("utf-8"))
except (urllib.error.URLError, TimeoutError) as exc: # pragma: no cover - network
raise RuntimeError(f"endpoint request failed: {exc}") from exc
return str(body["choices"][0]["message"]["content"])
_ENDPOINT_SYSTEM = (
"You are Vers3Dynamics Civil Reactor Expert. Answer civil-nuclear questions "
"with explicit numbers and units. Stay civil-only."
)
def collect_endpoint_answers(
items: list[dict[str, Any]], url: str, *, timeout: float = 60.0
) -> dict[str, str]:
answers: dict[str, str] = {}
for item in items:
answers[str(item.get("id"))] = _query_endpoint(
url, str(item.get("prompt", "")), system=_ENDPOINT_SYSTEM, timeout=timeout
)
return answers
def _load_answers_file(path: Path) -> dict[str, str]:
answers: dict[str, str] = {}
for row in read_jsonl(path):
answers[str(row.get("id"))] = str(row.get("answer", ""))
return answers
def _main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(
description="Numeric-correctness eval against the physics engines."
)
parser.add_argument(
"--suite", type=Path, default=DEFAULT_SUITE, help="Eval suite JSONL."
)
parser.add_argument(
"--self-check", action="store_true", help="Validate suite + scorer (no model)."
)
parser.add_argument(
"--answers", type=Path, help="JSONL of model answers ({id, answer})."
)
parser.add_argument(
"--endpoint-url", help="OpenAI-compatible endpoint to query per prompt."
)
parser.add_argument("--report", type=Path, help="Write the JSON report here.")
parser.add_argument(
"--min-accuracy",
type=float,
default=None,
help="Fail (exit 1) below this accuracy.",
)
args = parser.parse_args(argv)
items = read_jsonl(args.suite)
if not items:
print(f"empty suite: {args.suite}", file=sys.stderr)
return 2
if args.answers:
report = score_answers(items, _load_answers_file(args.answers))
elif args.endpoint_url:
report = score_answers(
items, collect_endpoint_answers(items, args.endpoint_url)
)
else:
report = self_check(items) # default mode
print(report.render())
if args.report:
args.report.write_text(json.dumps(report.to_dict(), indent=2), encoding="utf-8")
print(f"\nwrote {args.report}")
if args.min_accuracy is not None and report.accuracy < args.min_accuracy:
print(
f"\nFAIL: accuracy {report.accuracy:.1%} < required {args.min_accuracy:.1%}",
file=sys.stderr,
)
return 1
# In self-check mode, any failure is a broken suite and must be non-zero.
if not args.answers and not args.endpoint_url and report.passed != report.total:
return 1
return 0
if __name__ == "__main__":
raise SystemExit(_main())
|