79CLASSES = (SCORED_OK, SCORED_INFEASIBLE, UNSCORABLE, NEVER_EVALUATED)
363 """Build the dimension called ``name`` from its recorded-space entry.
365 The entry is checked the way the study spec checks a dimension: integer
366 bounds and a positive integer step with ``low <= high``; real bounds with an
367 optional positive step; a log dimension has positive bounds, and a log
368 integer step 1 while a log real has no step; a stepped real must have at
369 most 2^53 ordinals. A dimension with exactly one legal value is constant.
370 A categorical keeps the first of equal choices. Anything else raises
371 ``NeighbourhoodError("neighbourhood_input_invalid")``.
374 def refuse(reason: str) -> NeighbourhoodError:
376 "input_invalid", f
"dimension {_clip(name)}: {reason}", names=(name,)
379 if not isinstance(spec, dict):
380 raise refuse(
"the recorded entry must be an object")
381 kind = spec.get(
"kind")
382 log = spec.get(
"log",
False)
383 if kind
in (
"integer",
"real")
and type(log)
is not bool:
384 raise refuse(
"log must be a boolean")
385 if kind ==
"integer":
386 low, high, step = spec.get(
"low"), spec.get(
"high"), spec.get(
"step")
387 if type(low)
is not int
or type(high)
is not int:
388 raise refuse(
"integer bounds must be integers")
389 if type(step)
is not int
or step < 1:
390 raise refuse(
"the integer step must be a positive integer")
392 raise refuse(
"low exceeds high")
393 if log
and (low < 1
or step != 1):
394 raise refuse(
"a log integer needs positive bounds and step 1")
395 if (high - low) // step == 0:
398 rule =
"ratio_rounded" if log
else "step"
400 name=name, kind=kind, rule=rule, low=low, high=high, step=step, log=log
403 low, high = _number(spec.get(
"low")), _number(spec.get(
"high"))
404 if low
is None or high
is None:
405 raise refuse(
"real bounds must be finite numbers")
407 raise refuse(
"low exceeds high")
408 step = spec.get(
"step")
411 if step
is None or step <= 0:
412 raise refuse(
"the real step must be a positive finite number")
413 if log
and (low <= 0
or step
is not None):
414 raise refuse(
"a log real needs positive bounds and no step")
418 last =
None if step
is None else 0
420 rule =
"ratio" if log
else "fraction"
423 count = _finite_parameter_cardinality(
428 if count
is None or issues:
429 raise refuse(
"the stepped real has no usable lattice (over 2^53)")
431 rule =
"constant" if last == 0
else "ordinal"
442 if kind ==
"boolean":
443 return Dimension(name=name, kind=kind, rule=
"other_value")
444 if kind ==
"categorical":
445 listed = spec.get(
"choices")
446 if not isinstance(listed, list)
or not listed:
447 raise refuse(
"choices must be a non-empty list")
450 for choice
in listed:
451 if type(choice)
not in (str, int, float, bool)
or (
452 type(choice)
is float
and not math.isfinite(choice)
454 raise refuse(
"every choice must be a finite scalar")
455 marker = (type(choice), choice)
456 if marker
not in seen:
458 choices.append(choice)
459 rule =
"constant" if len(choices) == 1
else "other_choices"
460 return Dimension(name=name, kind=kind, rule=rule, choices=tuple(choices))
461 raise refuse(
"the kind must be integer, real, boolean or categorical")
694def prepare(sources: Any, *, space: Any =
None) -> Study:
695 """Read the sources and index their rows; see ``neighbourhood`` for refusals.
697 ``sources`` is a non-empty list; an item is a path to a complete result JSON
698 or a rows JSONL, a parsed result, or a parsed list of rows. The position in
699 the list is the source index. ``space`` is a recorded-space dict or a
700 ``StudySpec`` and is used only when no row or result records its space.
702 if not isinstance(sources, (list, tuple))
or not sources:
705 "sources must be a non-empty list of result files, results or row lists",
709 for index, item
in enumerate(sources):
711 read.append(read_source(item, index))
712 except RowsError
as error:
713 raise _from_rows_error(error, index)
from None
714 return Study(read, space)
996 fraction: Any =
None,
999 max_work: Any =
None,
1002 """Describe the neighbourhoods of the given centres from finished rows.
1004 ``sources`` is a non-empty list; an item is a path to a complete result JSON
1005 or a rows JSONL, a parsed result, or a parsed list of rows, and its position
1006 in the list is its source index. ``centres`` is a list of (source, trial_id)
1007 pairs, each a row with status ok (``top_ids`` returns such a list). ``band``
1008 is ``{"mode": "absolute" | "relative", "value": v}``: a scored ok neighbour
1009 is near the best iff its gap to the best ok objective is at most ``v`` or
1010 ``v`` times the absolute best. ``fraction`` (continuous reals) and ``ratio``
1011 (log dimensions) are needed only when a varying dimension uses them.
1012 ``pair_k`` must be passed: it has no default, and None refuses with
1013 ``neighbourhood_parameter_missing``. That many pair maps per centre are
1014 built from the pairs with the most not-near neighbours, and an explicit 0
1015 builds none. ``max_work`` bounds the rows plus the lookups. ``space`` is
1016 used only when no row or result records one. No default exists for the
1017 band, the fraction, the ratio, ``pair_k`` or ``max_work``.
1019 The result is a JSON-ready dict (see ``docs/neighbourhood.md``). Refusals
1020 raise ``NeighbourhoodError`` with one of ``CODES`` and, when several defects
1021 exist, report the first in this order: caller parameters, reading the
1022 sources, space and direction, context, dimensions, rows against the space,
1023 duplicate conflicts, no ok row, no varying dimension, missing fraction or
1024 ratio, a relative band at a zero reference, centres, work above
1027 centre_ids, band, fraction, ratio, pair_k, max_work = _check_parameters(
1028 centres, band, fraction, ratio, pair_k, max_work
1030 study =
prepare(sources, space=space)
1031 _require_varying(study)
1032 _require_rules(study, fraction, ratio)
1033 if band[
"mode"] ==
"relative" and study.reference == 0.0:
1036 "a relative band needs a reference value other than zero",
1038 study.bind(band, fraction, ratio)
1039 chosen = _centre_rows(study, centre_ids)
1040 values = [study.row_values(row)
for row
in chosen]
1043 name: study.dimensions[name].neighbours(vector[name], fraction, ratio)
1044 for name
in study.varying
1046 for vector
in values
1048 axis = sum(len(item.values)
for centre
in moves
for item
in centre.values())
1049 _admit(study, axis, max_work)
1051 _profile(study, vector, centre)
for vector, centre
in zip(values, moves)
1053 maps: list = [[]
for _
in chosen]
1056 from .
import sensitivity
1059 sensitivity.plan_pairs(study, vector, profile, pair_k)
1060 for vector, profile
in zip(values, profiles)
1062 pair_lookups = sum(plan.lookups
for centre
in plans
for plan
in centre)
1063 _admit(study, axis + pair_lookups, max_work)
1065 sensitivity.pair_maps(study, vector, centre)
1066 for vector, centre
in zip(values, plans)
1069 _centre_result(study, row, centre, profile, pair_maps)
1070 for row, centre, profile, pair_maps
in zip(chosen, moves, profiles, maps)
1072 lookups = axis + pair_lookups
1074 "contract": CONTRACT,
1076 "direction": study.direction,
1078 "objective": study.reference,
1079 "source": study.reference_id[0],
1080 "trial_id": study.reference_id[1],
1082 "band": dict(study.band),
1084 "fraction": fraction,
1088 name: study.dimensions[name].rule
for name
in study.varying
1091 "sources": [dict(item)
for item
in study.summaries],
1092 "duplicate_rows": study.duplicate_rows,
1093 "constant_dimensions": list(study.constants),
1094 "missing_vectors": _missing_vectors(study, list(zip(values, results))),
1096 "rows": study.row_count,
1098 "work": study.row_count + lookups,
1099 "max_work": max_work,
1101 "input_sha256": study.input_sha256,
1102 "interpretation": INTERPRETATION,