Add decision core: autojev/sft_numeric.py
Browse files- autojev/sft_numeric.py +276 -0
autojev/sft_numeric.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Executable numeric labels, with expression structures reserved by split."""
|
| 2 |
+
|
| 3 |
+
from collections.abc import Callable, Mapping
|
| 4 |
+
from dataclasses import dataclass
|
| 5 |
+
from fractions import Fraction
|
| 6 |
+
import random
|
| 7 |
+
|
| 8 |
+
from autojev.types import Content, Example, JSONValue
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@dataclass(frozen=True)
|
| 12 |
+
class Expression:
|
| 13 |
+
op: str
|
| 14 |
+
value: Fraction = Fraction(0)
|
| 15 |
+
left: "Expression | None" = None
|
| 16 |
+
right: "Expression | None" = None
|
| 17 |
+
|
| 18 |
+
def evaluate(self) -> Fraction:
|
| 19 |
+
if self.op == "number":
|
| 20 |
+
return self.value
|
| 21 |
+
if self.left is None or self.right is None:
|
| 22 |
+
raise ValueError("An operation needs two operands")
|
| 23 |
+
a, b = self.left.evaluate(), self.right.evaluate()
|
| 24 |
+
if self.op == "+":
|
| 25 |
+
return a + b
|
| 26 |
+
if self.op == "-":
|
| 27 |
+
return a - b
|
| 28 |
+
if self.op == "*":
|
| 29 |
+
return a * b
|
| 30 |
+
if self.op == "/":
|
| 31 |
+
return a / b
|
| 32 |
+
raise ValueError(self.op)
|
| 33 |
+
|
| 34 |
+
def render(self) -> str:
|
| 35 |
+
if self.op == "number":
|
| 36 |
+
return str(self.value) if self.value >= 0 else f"({self.value})"
|
| 37 |
+
if self.left is None or self.right is None:
|
| 38 |
+
raise ValueError("An operation needs two operands")
|
| 39 |
+
return f"({self.left.render()} {self.op} {self.right.render()})"
|
| 40 |
+
|
| 41 |
+
def json(self) -> dict[str, JSONValue]:
|
| 42 |
+
if self.op == "number":
|
| 43 |
+
return {"op": self.op, "value": str(self.value)}
|
| 44 |
+
if self.left is None or self.right is None:
|
| 45 |
+
raise ValueError("An operation needs two operands")
|
| 46 |
+
return {"op": self.op, "left": self.left.json(), "right": self.right.json()}
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def number(value: Fraction | int) -> Expression:
|
| 50 |
+
return Expression("number", Fraction(value))
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def operation(op: str, a: Expression, b: Expression) -> Expression:
|
| 54 |
+
return Expression(op, left=a, right=b)
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def composed(value: Fraction, rng: random.Random, split: str, variant: int) -> Expression:
|
| 58 |
+
"""Build an exact value without sharing expression-tree templates across splits."""
|
| 59 |
+
a, b = Fraction(rng.randint(2, 41)), Fraction(rng.randint(2, 23))
|
| 60 |
+
n, op = number, operation
|
| 61 |
+
forms: tuple[Callable[[], Expression], ...]
|
| 62 |
+
if split == "train":
|
| 63 |
+
forms = (
|
| 64 |
+
lambda: op("+", n(value - a), n(a)),
|
| 65 |
+
lambda: op("-", n(value + a), n(a)),
|
| 66 |
+
lambda: op("*", n(value / a), n(a)),
|
| 67 |
+
lambda: op("/", n(value * a), n(a)),
|
| 68 |
+
)
|
| 69 |
+
elif split == "calibration":
|
| 70 |
+
forms = (
|
| 71 |
+
lambda: op("-", op("+", n(value + b - a), n(a)), n(b)),
|
| 72 |
+
lambda: op("/", op("-", n(value * b + a), n(a)), n(b)),
|
| 73 |
+
lambda: op("+", op("*", n((value - b) / a), n(a)), n(b)),
|
| 74 |
+
)
|
| 75 |
+
elif split == "confirmation":
|
| 76 |
+
forms = (
|
| 77 |
+
lambda: op("*", op("+", n(value / b - a), n(a)), n(b)),
|
| 78 |
+
lambda: op("-", op("*", n((value + b) / a), n(a)), n(b)),
|
| 79 |
+
lambda: op("+", op("-", n(value + a - 1 / b), n(a)), op("/", n(1), n(b))),
|
| 80 |
+
)
|
| 81 |
+
else:
|
| 82 |
+
raise ValueError(f"Unknown split: {split}")
|
| 83 |
+
expression = forms[variant % len(forms)]()
|
| 84 |
+
if expression.evaluate() != value:
|
| 85 |
+
raise ValueError("Expression construction changed its value")
|
| 86 |
+
return expression
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
WORDS = {
|
| 90 |
+
"EN": {
|
| 91 |
+
"intro": "Compare the specified first and second quantities in this operations record. Use exact arithmetic; a/b is a fraction. Square brackets are inclusive bounds. No unstated relationship between records is implied.",
|
| 92 |
+
"first": "First quantity", "second": "Second quantity", "record": "record", "unit": "unit",
|
| 93 |
+
"missing": "not recorded; may be any rational value", "conversion": "Explicit conversion",
|
| 94 |
+
"other": "Separate records; none changes the two queried records", "context": "Operations context",
|
| 95 |
+
"question": "Which relationship is established for the first quantity relative to the second? If multiple relationships remain possible, choose unknown.",
|
| 96 |
+
"less": "The first quantity is smaller.", "equal": "The quantities are equal.",
|
| 97 |
+
"greater": "The first quantity is larger.", "unknown": "The relationship is not determined.",
|
| 98 |
+
"percent": "Percentage computation: the expression gives the base amount; multiply by the stated percentage / 100.",
|
| 99 |
+
"rate": "Rate computation: the expression gives the total amount; divide by the stated duration in hours.",
|
| 100 |
+
"duration": "duration in hours", "percentage": "percentage", "calculation": "calculation",
|
| 101 |
+
"archive": "An additional archived measurement, independent of the queried quantities", "speaker": "Operator",
|
| 102 |
+
},
|
| 103 |
+
"DE": {
|
| 104 |
+
"intro": "Vergleiche die angegebene erste und zweite Größe in diesem Betriebsprotokoll. Rechne exakt; a/b ist ein Bruch. Eckige Klammern geben inklusive Grenzen an. Zwischen Einträgen bestehen keine unausgesprochenen Beziehungen.",
|
| 105 |
+
"first": "Erste Größe", "second": "Zweite Größe", "record": "Eintrag", "unit": "Einheit",
|
| 106 |
+
"missing": "nicht erfasst; jeder rationale Wert ist möglich", "conversion": "Explizite Umrechnung",
|
| 107 |
+
"other": "Separate Einträge; keiner verändert die beiden abgefragten Einträge", "context": "Betriebskontext",
|
| 108 |
+
"question": "Welche Beziehung der ersten zur zweiten Größe ist belegt? Wenn mehrere Beziehungen möglich bleiben, wähle unknown.",
|
| 109 |
+
"less": "Die erste Größe ist kleiner.", "equal": "Die Größen sind gleich.",
|
| 110 |
+
"greater": "Die erste Größe ist größer.", "unknown": "Die Beziehung ist nicht bestimmt.",
|
| 111 |
+
"percent": "Prozentrechnung: Der Ausdruck ergibt den Grundwert; multipliziere ihn mit dem angegebenen Prozentsatz / 100.",
|
| 112 |
+
"rate": "Ratenberechnung: Der Ausdruck ergibt die Gesamtmenge; dividiere sie durch die angegebene Dauer in Stunden.",
|
| 113 |
+
"duration": "Dauer in Stunden", "percentage": "Prozentsatz", "calculation": "Berechnung",
|
| 114 |
+
"archive": "Eine zusätzliche archivierte Messung, unabhängig von den abgefragten Größen", "speaker": "Sachbearbeiter",
|
| 115 |
+
},
|
| 116 |
+
"ES": {
|
| 117 |
+
"intro": "Compara la primera y la segunda cantidad indicadas en este registro operativo. Calcula exactamente; a/b es una fracción. Los corchetes indican límites inclusivos. No se supone ninguna relación no indicada entre registros.",
|
| 118 |
+
"first": "Primera cantidad", "second": "Segunda cantidad", "record": "registro", "unit": "unidad",
|
| 119 |
+
"missing": "no registrada; puede tener cualquier valor racional", "conversion": "Conversión explícita",
|
| 120 |
+
"other": "Registros separados; ninguno modifica los dos registros consultados", "context": "Contexto operativo",
|
| 121 |
+
"question": "¿Qué relación queda establecida entre la primera cantidad y la segunda? Si siguen siendo posibles varias relaciones, elige unknown.",
|
| 122 |
+
"less": "La primera cantidad es menor.", "equal": "Las cantidades son iguales.",
|
| 123 |
+
"greater": "La primera cantidad es mayor.", "unknown": "No se puede determinar la relación.",
|
| 124 |
+
"percent": "Cálculo porcentual: la expresión indica la cantidad base; multiplícala por el porcentaje indicado / 100.",
|
| 125 |
+
"rate": "Cálculo de tasa: la expresión indica la cantidad total; divídela por la duración indicada en horas.",
|
| 126 |
+
"duration": "duración en horas", "percentage": "porcentaje", "calculation": "cálculo",
|
| 127 |
+
"archive": "Una medición archivada adicional, independiente de las cantidades consultadas", "speaker": "Operador",
|
| 128 |
+
},
|
| 129 |
+
"FR": {
|
| 130 |
+
"intro": "Comparez la première et la deuxième quantité indiquées dans ce relevé opérationnel. Calculez exactement ; a/b est une fraction. Les crochets désignent des bornes inclusives. Aucune relation non précisée entre les relevés n'est supposée.",
|
| 131 |
+
"first": "Première quantité", "second": "Deuxième quantité", "record": "relevé", "unit": "unité",
|
| 132 |
+
"missing": "non enregistrée ; toute valeur rationnelle est possible", "conversion": "Conversion explicite",
|
| 133 |
+
"other": "Relevés distincts ; aucun ne modifie les deux relevés demandés", "context": "Contexte opérationnel",
|
| 134 |
+
"question": "Quelle relation est établie entre la première quantité et la deuxième ? Si plusieurs relations restent possibles, choisissez unknown.",
|
| 135 |
+
"less": "La première quantité est inférieure.", "equal": "Les quantités sont égales.",
|
| 136 |
+
"greater": "La première quantité est supérieure.", "unknown": "La relation ne peut pas être déterminée.",
|
| 137 |
+
"percent": "Calcul de pourcentage : l'expression donne la quantité de base ; multipliez-la par le pourcentage indiqué / 100.",
|
| 138 |
+
"rate": "Calcul de débit : l'expression donne la quantité totale ; divisez-la par la durée indiquée en heures.",
|
| 139 |
+
"duration": "durée en heures", "percentage": "pourcentage", "calculation": "calcul",
|
| 140 |
+
"archive": "Une mesure archivée supplémentaire, indépendante des quantités demandées", "speaker": "Opérateur",
|
| 141 |
+
},
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def compare(a: tuple[Fraction, Fraction] | None, b: tuple[Fraction, Fraction] | None) -> str:
|
| 146 |
+
if a is None or b is None:
|
| 147 |
+
return "unknown"
|
| 148 |
+
if a[1] < b[0]:
|
| 149 |
+
return "less"
|
| 150 |
+
if a[0] > b[1]:
|
| 151 |
+
return "greater"
|
| 152 |
+
if a[0] == a[1] == b[0] == b[1]:
|
| 153 |
+
return "equal"
|
| 154 |
+
return "unknown"
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
def numeric_example(slot: Mapping[str, JSONValue]) -> Example:
|
| 158 |
+
"""Return a machine-checkable decision. Only the rendered state enters the model."""
|
| 159 |
+
seed, split, phenomenon = int(str(slot["seed"])), str(slot["split"]), str(slot["phenomenon"])
|
| 160 |
+
rng = random.Random(seed)
|
| 161 |
+
words = WORDS[str(slot["language"])]
|
| 162 |
+
desired = str(slot["desired_label"])
|
| 163 |
+
denominator = rng.choice((2, 3, 4, 5, 8, 10)) if phenomenon == "fractions" else 1
|
| 164 |
+
center = Fraction(rng.randint(100, 5000), denominator)
|
| 165 |
+
if phenomenon == "signed_values":
|
| 166 |
+
center *= rng.choice((-1, 1))
|
| 167 |
+
gap = Fraction(rng.randint(1, 17), denominator)
|
| 168 |
+
proposed_width = Fraction(rng.randint(1, 9), denominator)
|
| 169 |
+
labels = ("less", "equal", "greater", "unknown")
|
| 170 |
+
first_label = labels[(labels.index(desired) - int(str(slot["variant"]))) % 4]
|
| 171 |
+
pair = (first_label, labels[(labels.index(first_label) + 1) % 4])
|
| 172 |
+
width = proposed_width if phenomenon == "bounds" and "equal" not in pair else Fraction(0)
|
| 173 |
+
a = (center - width, center + width)
|
| 174 |
+
# Keep a family counterfactual local: the second record's decisive amount or availability changes.
|
| 175 |
+
offset = {"less": 2 * width + gap, "equal": Fraction(0), "greater": -2 * width - gap,
|
| 176 |
+
"unknown": Fraction(0)}[desired]
|
| 177 |
+
optional_width = rng.choice((Fraction(0), Fraction(0), gap / 2))
|
| 178 |
+
b_width = max(width, optional_width) if desired != "equal" else Fraction(0)
|
| 179 |
+
if desired == "unknown":
|
| 180 |
+
b_width = max(width, gap)
|
| 181 |
+
b: tuple[Fraction, Fraction] | None = (center + offset - b_width, center + offset + b_width)
|
| 182 |
+
if phenomenon == "missing_information" and desired == "unknown":
|
| 183 |
+
b = None
|
| 184 |
+
label = compare(a, b)
|
| 185 |
+
if label != desired:
|
| 186 |
+
raise ValueError(f"Requested {desired}; computed {label}")
|
| 187 |
+
shape = rng.randrange(12)
|
| 188 |
+
template = f"{split}:{phenomenon}:{shape % (4 if split == 'train' else 3)}"
|
| 189 |
+
units = rng.choice((("m", "cm", Fraction(100)), ("kg", "g", Fraction(1000)),
|
| 190 |
+
("h", "min", Fraction(60)), ("L", "mL", Fraction(1000))))
|
| 191 |
+
quantity_units = (units[0], units[1]) if phenomenon == "unit_conversion" else (words["unit"], words["unit"])
|
| 192 |
+
factors = (Fraction(1), 1 / units[2]) if phenomenon == "unit_conversion" else (Fraction(1), Fraction(1))
|
| 193 |
+
records: list[str] = []
|
| 194 |
+
specs: list[JSONValue] = []
|
| 195 |
+
record_ids = (f"R{rng.randint(10000, 49999)}", f"R{rng.randint(50000, 99999)}")
|
| 196 |
+
for index, interval in enumerate((a, b)):
|
| 197 |
+
record_rng = random.Random(seed + 100 + index)
|
| 198 |
+
prefix = f"{record_ids[index]} · {words['unit']}: {quantity_units[index]}"
|
| 199 |
+
if interval is None:
|
| 200 |
+
records.append(f"{prefix} · {words['calculation']}: {words['missing']}")
|
| 201 |
+
specs.append({"id": record_ids[index], "unit": quantity_units[index], "parameter": None,
|
| 202 |
+
"bounds": None, "factor": str(factors[index])})
|
| 203 |
+
continue
|
| 204 |
+
parameter = record_rng.randint(2, 19) if phenomenon == "rates" else record_rng.choice((5, 10, 15, 20, 25, 40, 60, 75))
|
| 205 |
+
expressions: list[Expression] = []
|
| 206 |
+
for endpoint in interval:
|
| 207 |
+
if expressions and interval[0] == interval[1]:
|
| 208 |
+
expressions.append(expressions[0])
|
| 209 |
+
continue
|
| 210 |
+
value = endpoint / factors[index]
|
| 211 |
+
if phenomenon == "rates":
|
| 212 |
+
value *= parameter
|
| 213 |
+
elif phenomenon == "percentages":
|
| 214 |
+
value *= Fraction(100, parameter)
|
| 215 |
+
expression = composed(value, record_rng, split, shape)
|
| 216 |
+
expressions.append(expression)
|
| 217 |
+
rendered = expressions[0].render() if interval[0] == interval[1] else f"[{expressions[0].render()}, {expressions[1].render()}]"
|
| 218 |
+
description = f"{prefix} · {words['calculation']}: {rendered}"
|
| 219 |
+
if phenomenon in ("rates", "percentages"):
|
| 220 |
+
description += f" · {words['duration' if phenomenon == 'rates' else 'percentage']}: {parameter}"
|
| 221 |
+
records.append(description)
|
| 222 |
+
multiplier = Fraction(1, parameter) if phenomenon == "rates" else Fraction(parameter, 100) if phenomenon == "percentages" else Fraction(1)
|
| 223 |
+
calculated = tuple(expr.evaluate() * multiplier * factors[index] for expr in expressions)
|
| 224 |
+
if calculated != interval:
|
| 225 |
+
raise ValueError("Rendered expression, conversion and reference interval differ")
|
| 226 |
+
specs.append({"id": record_ids[index], "unit": quantity_units[index],
|
| 227 |
+
"parameter": parameter if phenomenon in ("rates", "percentages") else None,
|
| 228 |
+
"expressions": [expr.json() for expr in expressions],
|
| 229 |
+
"factor": str(factors[index]), "multiplier": str(multiplier),
|
| 230 |
+
"bounds": [str(value) for value in calculated]})
|
| 231 |
+
# Numeric long contexts exercise distraction, not repeated narrative padding.
|
| 232 |
+
length = str(slot["length"])
|
| 233 |
+
auxiliary: list[str] = []
|
| 234 |
+
rng = random.Random(seed + 200)
|
| 235 |
+
for index in range({"short": 0, "medium": 4, "long": 20}[length]):
|
| 236 |
+
identifier = f"S{rng.randint(100000, 999999)}-{index}"
|
| 237 |
+
left, right = rng.randint(2, 7000), rng.randint(2, 7000)
|
| 238 |
+
auxiliary.append(f"{identifier}: {words['archive']}; {words['calculation']}: {left} + {right}; {words['unit']}: {words['unit']}.")
|
| 239 |
+
rng.shuffle(records)
|
| 240 |
+
all_records = records + auxiliary
|
| 241 |
+
rng.shuffle(all_records)
|
| 242 |
+
header = f"{words['intro']}\n{words['context']}: {slot['domain']}.\n{words['first']}: {record_ids[0]}; {words['second']}: {record_ids[1]}."
|
| 243 |
+
if phenomenon == "unit_conversion":
|
| 244 |
+
header += f"\n{words['conversion']}: 1 {units[0]} = {units[2]} {units[1]}."
|
| 245 |
+
if phenomenon in ("percentages", "rates"):
|
| 246 |
+
header += "\n" + words["percent" if phenomenon == "percentages" else "rate"]
|
| 247 |
+
style = str(slot["style"])
|
| 248 |
+
if style == "table":
|
| 249 |
+
body = "\n".join("| " + item.replace(" · ", " | ") + " |" for item in all_records)
|
| 250 |
+
elif style == "dialogue":
|
| 251 |
+
body = "\n".join(f"{words['speaker']} {1 + i % 2}: {item}" for i, item in enumerate(all_records))
|
| 252 |
+
elif style == "bullet_list":
|
| 253 |
+
body = "\n".join("- " + item for item in all_records)
|
| 254 |
+
elif style == "form":
|
| 255 |
+
body = "\n".join(f"{words['record']} {i + 1}: {item}" for i, item in enumerate(all_records))
|
| 256 |
+
elif style == "report":
|
| 257 |
+
body = "\n\n".join(all_records)
|
| 258 |
+
elif style == "message":
|
| 259 |
+
body = "; ".join(all_records)
|
| 260 |
+
else:
|
| 261 |
+
raise ValueError(style)
|
| 262 |
+
options = slot["option_labels"]
|
| 263 |
+
if not isinstance(options, list) or set(map(str, options)) != {"less", "equal", "greater", "unknown"}:
|
| 264 |
+
raise ValueError("Numeric criteria must contain the four relation labels")
|
| 265 |
+
criteria: dict[str, Content | None] = {str(key): words[str(key)] for key in options}
|
| 266 |
+
source: dict[str, JSONValue] = dict(slot)
|
| 267 |
+
source.update({"generator": "python-executable-numeric", "template_id": template,
|
| 268 |
+
"numeric_spec": {"records": specs, "first": record_ids[0], "second": record_ids[1],
|
| 269 |
+
"phenomenon": phenomenon, "unit_conversion": [units[0], units[1], str(units[2])] if phenomenon == "unit_conversion" else None},
|
| 270 |
+
"reference_calculation": {"a": [str(value) for value in a],
|
| 271 |
+
"b": [str(value) for value in b] if b is not None else None,
|
| 272 |
+
"label": label}})
|
| 273 |
+
return {"id": str(slot["id"]), "family": str(slot["family"]), "suite": "sft2-numeric",
|
| 274 |
+
"state": header + "\n" + (words["other"] + ":\n" if auxiliary else "") + body,
|
| 275 |
+
"question": {"type": "choice", "instructions": words["question"], "criteria": criteria},
|
| 276 |
+
"label": label, "target": label, "source": source}
|