235 """Check one row object (continuation.py:352-371) and return it as a Row."""
236 if type(source_index)
is not int
or source_index < 0:
238 "input_invalid",
"the source index must be a non-negative integer"
240 if not isinstance(raw, dict):
241 raise RowsError(
"input_invalid",
"a row must be a JSON object")
242 identifier = raw.get(
"trial_id")
243 ids = (identifier,)
if type(identifier)
is int
else ()
245 def refuse(detail: str) -> RowsError:
246 return RowsError(
"input_invalid", detail, ids)
248 if type(identifier)
is not int
or not 0 <= identifier < _ID_LIMIT:
249 raise refuse(
"trial_id must be an integer from 0 to 2^64 - 2")
250 status = raw.get(
"status")
251 if type(status)
is not str
or status
not in STATUSES:
252 raise refuse(f
"unknown trial status {_clip(repr(status))}")
253 feasible = raw.get(
"feasible")
254 if type(feasible)
is not bool
or feasible != (status ==
"ok"):
255 raise refuse(
"feasible must be a boolean equal to (status is ok)")
256 if "objective" not in raw:
257 raise refuse(
"the row has no objective")
258 value = raw[
"objective"]
260 if value
is not None:
261 if type(value)
not in (int, float):
262 raise refuse(
"objective must be a finite number or null")
264 objective = float(value)
265 except OverflowError:
266 raise refuse(
"objective must be a finite number or null")
from None
267 if not math.isfinite(objective):
268 raise refuse(
"objective must be a finite number or null")
269 if objective
is None and status
in (
"ok",
"constraint_violation"):
270 raise refuse(f
"a {status} row needs a finite objective")
272 for key
in (
"parameters",
"metrics",
"backtest",
"space"):
273 if key
in raw
and not isinstance(raw[key], dict):
274 raise refuse(f
"{key} must be an object when it is present")
275 members[key] = raw.get(key)
276 space_hash = raw.get(
"space_hash")
277 if "space_hash" in raw
and type(space_hash)
is not str:
278 raise refuse(
"space_hash must be a string when it is present")
285 members[
"parameters"],
493def read_source(source: Any, index: int = 0) -> Source:
494 """Read one source into a Source whose rows are validated and in file order.
496 ``source`` is a path to a result JSON or a rows JSONL, an already-parsed
497 result dict, or an already-parsed list of rows. A result must keep every
498 trial (``trials_out`` all or absent) and its counts must agree with its rows,
499 else ``rows_incomplete``. A JSONL file holds one row object per non-blank line;
500 a bad line is refused with its 1-based line number. A source with no rows is
501 returned empty. Rows from several sources are told apart by ``index``.
503 if type(index)
is not int
or index < 0:
505 "input_invalid",
"the source index must be a non-negative integer"
507 if isinstance(source, dict):
508 return _result_source(source, index)
509 if isinstance(source, list):
510 return _array_source(source, index)
511 if isinstance(source, (str, os.PathLike)):
512 return _file_source(source, index)
514 "input_invalid",
"a source must be a path, a result object or a list of rows"
552def common_space(sources: Iterable[Source], passed_space: Any =
None) -> dict:
553 """Return the one recorded space shared by every row and result of ``sources``.
555 A row's own ``space`` and a result's ``space`` must all have the same canonical
556 form (which includes the objective, its direction and its constraints), else
557 ``space_mismatch`` naming the dimensions that differ and the offending rows.
558 When nothing records a space, ``passed_space`` (a recorded-space dict or a
559 StudySpec) is used; with none passed the call is refused as ``space_missing``.
560 A passed space is not compared with recorded ones. Rows or sources that record
561 no space are not checked against it.
564 for source
in sources:
565 if source.space
is not None:
566 holders.append((source.index, -1, source.space))
568 (row.source_index, row.trial_id, row.space)
569 for row
in source.rows
570 if row.space
is not None
573 if passed_space
is None:
576 "no row or result records its space and none was passed",
578 space = _passed(passed_space)
579 canonical_space(space)
580 return copy.deepcopy(space)
583 def form(space: Any) -> str:
585 text = json.dumps(space, sort_keys=
True)
586 except (TypeError, ValueError):
587 return canonical_space(space)
588 if text
not in forms:
589 forms[text] = canonical_space(space)
592 reference = min(holders, key=
lambda holder: holder[:2])
593 expected = form(reference[2])
594 apart = [holder
for holder
in holders
if form(holder[2]) != expected]
596 involved = sorted({reference[0], *(holder[0]
for holder
in apart)})
599 "rows or results record different spaces "
600 f
"(sources {', '.join(str(number) for number in involved)})",
601 [holder[:2]
for holder
in apart
if holder[1] >= 0],
602 _differing_names(expected, {form(holder[2])
for holder
in apart}),
604 return copy.deepcopy(reference[2])
611def check_context(rows: Iterable[Row]) ->
None:
612 """Refuse rows whose scoring context visibly differs (necessary, not sufficient).
614 Among rows with status ``ok`` that carry a ``backtest`` object, the tuple
615 (input_bars_processed, script_bars_processed, input_tf_seconds,
616 script_tf_seconds) must be equal; every row that carries a ``space_hash`` must
617 carry the same one. Equal counters and hashes do not prove the same data,
618 window, fixed inputs or strategy (study-spec.md:1018-1020). The refusal names
619 the rows as (source_index, trial_id) pairs, starting from the lowest pair.
623 (row, _counters(row))
625 if row.status ==
"ok" and row.backtest
is not None
628 reference, expected = min(scored, key=
lambda pair: _row_key(pair[0]))
629 apart = [row
for row, counters
in scored
if counters != expected]
633 for position, name
in enumerate(_COUNTERS)
635 counters[position] != expected[position]
for _, counters
in scored
640 "scored rows disagree on their backtest bar and timeframe counters",
641 [_row_key(row)
for row
in [reference, *apart]],
644 hashed = [row
for row
in rows
if row.space_hash
is not None]
646 reference = min(hashed, key=_row_key)
647 apart = [row
for row
in hashed
if row.space_hash != reference.space_hash]
651 "rows record different space_hash values",
652 [_row_key(row)
for row
in [reference, *apart]],
693def vector_key(parameters: dict, space: dict) -> str:
694 """Canonical JSON key of a parameter vector (continuation.py:112-140, 363).
696 Each value becomes a typed scalar by its dimension's kind in ``space``; reals
697 are binary64 hex with -0.0 taken as +0.0 and an int in a real dimension taken
698 as the same real. Equality is exact: no tolerance and no lattice snapping.
700 dimensions = space.get(
"parameters")
if isinstance(space, dict)
else None
701 if not isinstance(dimensions, dict):
702 raise RowsError(
"input_invalid",
"the space has no parameters object")
703 if not isinstance(parameters, dict)
or parameters.keys() != dimensions.keys():
704 given = set(parameters)
if isinstance(parameters, dict)
else set()
707 "the parameter names differ from the space dimensions",
708 names=sorted(given ^ set(dimensions)),
712 name: _typed(name, parameters[name], dimension)
713 for name, dimension
in dimensions.items()