Skip to content

pygx.geno

Program genome and genotypes.

geno

Program genome and genotypes.

Program genome (DNA) is a representation for encoding actions for manipulating symbolic objects. Genotypes (pygx.DNASpec) are the specification on how to generate them. Genotypes are separated from their corresponding hyper values (pygx.HyperValue) which generate client-side objects, in the aim to decouple the algorithms that generate genomes from the ones that consume them. As a result, the algorithms can be applied on different user programs for optimization.

.. graphviz:: :align: center

digraph genotypes {
  node [shape="box"];
  edge [arrowtail="empty" arrowhead="none" dir="back" style="dashed"];
  dna_spec [label="DNASpec" href="dna_spec.html"]
  space [label="Space" href="space_class.html"];
  dp [label="DecisionPoint" href="decision_point.html"];
  choices [label="Choices" href="choices.html"];
  float [label="Float" href="float.html"];
  custom [label="CustomDecisionPoint" href="custom_decision_point.html"];
  dna [label="DNA", href="dna.html"]
  dna_spec -> space;
  dna_spec -> dp;
  space -> dp [arrowtail="diamond" style="none" label="elements"];
  dp -> choices;
  choices -> space [arrowtail="diamond" style="none" label="candidates"];
  dp -> float;
  dp -> custom;
  dna -> dna [arrowtail="diamond" style="none" label="children"];
  dna -> dna_spec [arrowhead="normal" dir="forward" style="none"
                   label="spec"];
}

Genotypes map 1:1 to hyper primitives as the following:

+-------------------------------------+--------------------------------------+ | Genotype class | Hyper class | +=====================================+======================================+ |pg.DNASpec | pg.hyper.HyperValue | +-------------------------------------+--------------------------------------+ |pg.geno.Space | pg.hyper.ObjectTemplate | +-------------------------------------+--------------------------------------+ |pg.geno.DecisionPoint | pg.hyper.HyperPrimitive | +-------------------------------------+--------------------------------------+ |pg.geno.Choices | pg.hyper.Choices | | | (pg.oneof, pg.manyof)| +-------------------------------------+--------------------------------------+ |pg.geno.Float | pg.floatv | +-------------------------------------+--------------------------------------+ |pg.geno.CustomDecisionPoint | pg.hyper.CustomHyper | | | (pg.evolve) | +-------------------------------------+--------------------------------------+

DNA

DNA(
    value: None | int | float | str | list[Any] | tuple[Any] = None,
    children: list[DNA] | None = None,
    spec: DNASpec | None = None,
    metadata: dict[str, Any] | None = None,
    *,
    allow_partial: bool = False,
    **kwargs: Any
)

Bases: Object

The genome of a symbolic object relative to its search space.

DNA is a hierarchical structure - each DNA node has a value, and a list of child DNA nodes. The root node represents the genome that encodes an entire object relative to its space. The value of a DNA node could be None, an integer, a float number or a string, dependening on its specification (pg.DNASpec). A valid DNA has a form of the following.

+--------------------------------------+-----------------+-----------------------------+ | Hyper value type | Possible values | Child nodes | | (DNASpec type) | | | +======================================+=================+=============================+ |pg.hyper.ObjectTemplate | None |DNA of child decision points | |(pg.geno.Space) |(elements > 1) |(Choices/Float) in the | | | |template. | +--------------------------------------+-----------------+-----------------------------+ | |None |Children of elements[0] | | |(elements == 1 | | | |and | | | |elements[0] | | | |num_choices > 1) | | +--------------------------------------+-----------------+-----------------------------+ | |int |Children of: | | |(elements == 1 |elements[0][0] | | |and | | | |elements[0] | | | |num_choices ==1) | | +--------------------------------------+-----------------+-----------------------------+ | |float |Empty | | |(elements == 1 | | | |and | | | |elements[0] | | | |is geno.Float) | | +--------------------------------------+-----------------+-----------------------------+ |pg.oneof |int |Children of Space | |(pg.geno.Choices) |(candidate index |for the chosen candidate | | |as choice) | | +--------------------------------------+-----------------+-----------------------------+ |pg.manyof |None |DNA of each chosen candidate | |(:class:pg.geno.Choices) |(num_choices > 1 | | +--------------------------------------+-----------------+-----------------------------+ | |int |Children of chosen candidate | | |(num_choices==1) | | +--------------------------------------+-----------------+-----------------------------+ |[pg.floatv][pygx.hyper.floatv] |float |Empty | |([pg.geno.Float][pygx.geno.Float] ) | | | +--------------------------------------+-----------------+-----------------------------+ |[pg.hyper.CustomHyper][pygx.hyper.CustomHyper] |string |User defined. | |([pg.geno.CustomDecisionPoint`][pygx.geno.CustomDecisionPoint])|(serialized | | | | object) | | +--------------------------------------+-----------------+-----------------------------+

DNA can also be represented in a compact form - a tree of numbers/strs, formally defined as::

:= empty | : = | | | := | | := int := float := str := [, , ...] := (, , ... )

Thus DNA can be constructed by nested structures of list, tuple and numbers. The numeric value for DNA can be integer (as index of choice) or float (the value itself will used as decoded value).

Examples::

# Empty DNA. This may be generated by an empty template. DNA()

# A DNA of a nested choice of depth 3. DNA(0, 0, 0)

# A DNA of three choices at the same level, # positioned at 0, 1, 2, each choice has value 0. DNA([0, 0, 0])

# A DNA of two choices (one two-level conditional, # one non-conditional), position 0 is with choice path: 0 -> [0, 1], # while [0, 1] means it's a multi-choice, decodable by Sublist or Subset. # position 1 is a single non-conditional choice: 0. DNA([(0, [0, 1]), 0])

# A DNA with custom decision point whose encoding # is defined by the user. DNA('abc')

Constructor.

Parameters:

Name Type Description Default
value None | int | float | str | list[Any] | tuple[Any]

Value for current node.

None
children list[DNA] | None

Child DNA(s).

None
spec DNASpec | None

DNA spec that constraint current node.

None
metadata dict[str, Any] | None

Optional dict as controller metadata for the DNA.

None
allow_partial bool

If True, allow the object to be partial.

False
**kwargs Any

keyword arguments that will be passed through to symbolic.Object.

{}
Source code in pygx/geno/_base.py
def __init__(
    self,
    value: None | int | float | str | list[Any] | tuple[Any] = None,
    # Set MISSING_VALUE to use default from pg_typing.
    children: list['DNA'] | None = None,
    spec: DNASpec | None = None,
    metadata: dict[str, Any] | None = None,
    *,
    allow_partial: bool = False,
    **kwargs: Any,
):
    """Constructor.

    Args:
      value: Value for current node.
      children: Child DNA(s).
      spec: DNA spec that constraint current node.
      metadata: Optional dict as controller metadata for the DNA.
      allow_partial: If True, allow the object to be partial.
      **kwargs: keyword arguments that will be passed through to
        symbolic.Object.
    """
    value, children, metadata = self._parse_value_and_children(
        value, children, metadata, spec
    )
    super().__init__(
        value=value,
        children=children,
        metadata=metadata or {},
        allow_partial=allow_partial,
        **kwargs,
    )

    self._decision_by_id_cache = None
    self._named_decisions = None
    self._userdata = AttributeDict()
    self._cloneable_metadata_keys = set()
    self._cloneable_userdata_keys = set()
    self._spec = None
    if spec:
        self.use_spec(spec)

userdata property

userdata: AttributeDict

Gets user data.

spec property

spec: DNASpec | None

Returns DNA spec of current DNA.

parent_dna property

parent_dna: Optional[DNA]

Returns parent DNA.

root property

root: DNA

Returns the DNA root.

is_subchoice property

is_subchoice: bool

Returns True if current DNA is a subchoice of a multi-choice.

multi_choice_spec property

multi_choice_spec: Optional[DecisionPoint]

Returns the multi-choice spec for child DNAs.

Returns:

Type Description
Optional[DecisionPoint]

If the children of this DNA are decisions of a multi-choice's subchoices,

Optional[DecisionPoint]

return the multi-choice spec (pg.geno.Choices). Otherwise returns None.

is_multi_choice_container property

is_multi_choice_container: bool

Returns True if the children of this DNA are multi-choice subchoices.

literal_value property

literal_value: str | int | float | list[str | int | float]

Returns the literal value represented by current DNA.

decision_ids property

decision_ids: list[KeyPath]

Returns decision IDs.

named_decisions property

named_decisions: dict[str, Union[DNA, list[DNA]]]

Returns a dict of name to the named DNA in the sub-tree.

is_leaf property

is_leaf: bool

Returns whether the current node is a leaf node.

on_sym_bound

on_sym_bound()

Event that is triggered when any symbolic member changes.

Source code in pygx/geno/_base.py
def on_sym_bound(self):
    """Event that is triggered when any symbolic member changes."""
    super().on_sym_bound()
    self._decision_by_id_cache = None
    self._named_decisions = None

set_metadata

set_metadata(key: str, value: Any, cloneable: bool = False) -> DNA

Set metadata associated with a key.

Metadata associated with the DNA will be persisted and carried over across processes, which is different the userdata. (See set_userdata for more details.)

Parameters:

Name Type Description Default
key str

Key for the metadata.

required
value Any

Value for the metadata.

required
cloneable bool

If True, the key/value will be propagated during clone.

False

Returns:

Type Description
DNA

Self.

Source code in pygx/geno/_base.py
def set_metadata(
    self, key: str, value: Any, cloneable: bool = False
) -> 'DNA':
    """Set metadata associated with a key.

    Metadata associated with the DNA will be persisted and carried over across
    processes, which is different the `userdata`. (See `set_userdata` for more
    details.)

    Args:
      key: Key for the metadata.
      value: Value for the metadata.
      cloneable: If True, the key/value will be propagated during clone.

    Returns:
      Self.
    """
    self.metadata.sym_rebind(  # pyright: ignore[reportAttributeAccessIssue]
        {key: value}, raise_on_no_change=False, skip_notification=True
    )
    if cloneable:
        self._cloneable_metadata_keys.add(key)
    return self

set_userdata

set_userdata(key: str, value: Any, cloneable: bool = False) -> DNA

Sets user data associated with a key.

User data associated with the DNA will live only within current process, and is not carried over during serialization/deserialization, which is different from DNA metadata. (See set_metadata for more details.)

Parameters:

Name Type Description Default
key str

Key of the user data.

required
value Any

Value of the user data.

required
cloneable bool

If True, the key/value will be carry over to the cloned DNA.

False

Returns:

Type Description
DNA

Self.

Source code in pygx/geno/_base.py
def set_userdata(
    self, key: str, value: Any, cloneable: bool = False
) -> 'DNA':
    """Sets user data associated with a key.

    User data associated with the DNA will live only within current process,
    and is not carried over during serialization/deserialization, which is
    different from DNA metadata. (See `set_metadata` for more details.)

    Args:
      key: Key of the user data.
      value: Value of the user data.
      cloneable: If True, the key/value will be carry over to the cloned DNA.

    Returns:
      Self.
    """
    self._userdata[key] = value
    if cloneable:
        self._cloneable_userdata_keys.add(key)
    return self

use_spec

use_spec(spec: DNASpec) -> DNA

Use a DNA spec for this node and children recursively.

Parameters:

Name Type Description Default
spec DNASpec

DNA spec.

required

Returns:

Type Description
DNA

Self.

Raises:

Type Description
ValueError

current DNA tree does not conform to the DNA spec.

Source code in pygx/geno/_base.py
def use_spec(self, spec: DNASpec) -> 'DNA':
    """Use a DNA spec for this node and children recursively.

    Args:
      spec: DNA spec.

    Returns:
      Self.

    Raises:
      ValueError: current DNA tree does not conform to the DNA spec.
    """
    if not isinstance(spec, DNASpec):
        raise ValueError(
            f"Argument 'spec' must be a `pg.DNASpec` object. "
            f'Encountered: {spec!r}.'
        )

    if self._spec is spec:
        return self

    def _use_spec_for_child_choices(spec: DNASpec, children: list[DNA]):
        """Use spec for child choices."""
        assert spec.is_categorical, spec
        if spec.num_choices != len(children):
            raise ValueError(
                f'Number of choices ({spec.num_choices}) does not match with '
                f'the number of child values (len(children)). '
                f'Spec: {spec!r}, Children: {children!r}.'
            )

        for i, child in enumerate(children):
            subchoice = spec.subchoice(i)
            child.use_spec(subchoice)

        child_values = cast(list[int], [c.value for c in children])
        if spec.sorted and sorted(child_values) != child_values:
            raise ValueError(
                f'Child values {child_values!r} are not sorted. Spec: {spec!r}.'
            )
        if spec.distinct and len(set(child_values)) != len(child_values):
            raise ValueError(
                f'Child values {child_values!r} are not distinct. Spec: {spec!r}.'
            )

    # Skip dummy DNA specs.
    while spec.is_space and len(spec.elements) == 1:
        spec = spec.elements[0]

    if spec.is_space:
        # Multiple value composition.
        if self.value is not None:
            raise ValueError(
                f'DNA value type mismatch. Value: {self.value}, Spec: {spec!r}.'
            )
        if len(spec.elements) != len(self.children):
            raise ValueError(
                f'Length of DNA child values ({len(self.children)}) is different '
                f'from the number of elements ({len(spec.elements)}) '
                f'in Spec: {spec!r}.'
            )
        for i, elem_spec in enumerate(spec.elements):
            self.children[i].use_spec(elem_spec)
    elif spec.is_categorical:
        if spec.num_choices == 1:
            # Single choice.
            if not isinstance(self.value, int):
                raise ValueError(
                    f'DNA value type mismatch. Value: {self.value}, Spec: {spec!r}.'
                )
            if self.value >= len(spec.candidates):
                raise ValueError(
                    f'Value of DNA is out of range according to the DNA spec. '
                    f'Value: {self.value}, Spec: {spec!r}.'
                )
            chosen_candidate = spec.candidates[self.value]
            assert chosen_candidate.is_space, chosen_candidate

            # Empty template in chosen candidate.
            if not chosen_candidate.elements and self.children:
                raise ValueError(
                    f'There is no DNA spec for child DNA values. '
                    f'Child values: {self.children}.'
                )

            # None-empty template in chosen candidate.
            if len(chosen_candidate.elements) > 1:
                # Children are DNA of multiple encoders in chosen composition.
                if len(chosen_candidate.elements) != len(self.children):
                    raise ValueError(
                        f'Number of elements in child templates '
                        f'({len(chosen_candidate.elements)}) does not match with '
                        f'the length of children ({len(self.children)}) from DNA: '
                        f'{self!r}, Spec: {chosen_candidate}.'
                    )
                for i, elem_spec in enumerate(chosen_candidate.elements):
                    self.children[i].use_spec(elem_spec)
            elif len(chosen_candidate.elements) == 1:
                # Children are multiple choices of the only encoder
                # in chosen composition.
                sub_spec = chosen_candidate
                while sub_spec.is_space and len(sub_spec.elements) == 1:
                    sub_spec = sub_spec.elements[0]
                if (
                    sub_spec.is_numerical
                    or sub_spec.is_custom_decision_point
                ):
                    if len(self.children) != 1:
                        raise ValueError(
                            f'Encountered more than 1 value.'
                            f'Child value: {self.children}, Spec: {sub_spec}.'
                        )
                    self.children[0].use_spec(sub_spec)
                else:
                    assert sub_spec.is_categorical, sub_spec
                    _use_spec_for_child_choices(sub_spec, self.children)
        else:
            # Multiple choices.
            if self.value is not None:
                raise ValueError(
                    f'Cannot apply multi-choice DNA spec on '
                    f'value {self.value}: {spec!r}.'
                )
            _use_spec_for_child_choices(spec, self.children)
    elif spec.is_numerical:
        if not isinstance(self.value, float):
            raise ValueError(
                f'DNA value type mismatch. Value: {self.value}, '
                f'Spec: {spec!r}.'
            )
        if self.value < spec.min_value:
            raise ValueError(
                f'DNA value should be no less than {spec.min_value}. '
                f'Encountered {self.value}, Spec: {spec!r}.'
            )
        if self.value > spec.max_value:
            raise ValueError(
                f'DNA value should be no greater than {spec.max_value}. '
                f'Encountered {self.value}, Spec: {spec!r}.'
            )
    else:
        assert spec.is_custom_decision_point, spec
        if not isinstance(self.value, str):
            raise ValueError(
                f'DNA value type mismatch, Value: {self.value!r}, Spec: {spec!r}.'
            )
    self._spec = spec
    return self

parse classmethod

parse(
    json_value: int | float | str | list[Any] | tuple[Any] | None,
    spec: DNASpec | None = None,
) -> DNA

Parse DNA from a nested structure of numbers.

Deprecated: use DNA.__init__ instead.

Parameters:

Name Type Description Default
json_value int | float | str | list[Any] | tuple[Any] | None

A nested structure of numbers.

required
spec DNASpec | None

DNA spec that will be applied to current DNA tree.

None

Returns:

Type Description
DNA

an instance of DNA object.

Raises:

Type Description
ValueError

Bad format for json_value or parsed DNA does not conform to the DNA spec.

Source code in pygx/geno/_base.py
@classmethod
def parse(
    cls,
    json_value: (
        int  # As a single chosen index.
        | float  # As a single chosen value.
        | str  # As a custom genome.
        | list[Any]  # As multi-choice. (coexisting)
        | tuple[Any]  # As a conditional choice.
        | None  # An empty DNA.
    ),
    spec: DNASpec | None = None,
) -> 'DNA':
    """Parse DNA from a nested structure of numbers.

    Deprecated: use `DNA.__init__` instead.

    Args:
      json_value: A nested structure of numbers.
      spec: DNA spec that will be applied to current DNA tree.

    Returns:
      an instance of DNA object.

    Raises:
      ValueError: Bad format for json_value or parsed DNA does not conform to
        the DNA spec.
    """
    return DNA(json_value, spec=spec)

from_dict classmethod

from_dict(
    dict_repr: dict[Any, Any],
    dna_spec: DNASpec,
    use_ints_as_literals: bool = False,
) -> DNA

Create a DNA from its dictionary representation.

Parameters:

Name Type Description Default
dict_repr dict[Any, Any]

The dictionary representation of the DNA. The keys should be either strings as the decision point ID or DNASpec objects. The values should be either numeric or literal values for the decisions. For inactive decisions, their ID/spec should either be absent from the dictionary, or use None as their values.

required
dna_spec DNASpec

The DNASpec that applies to the DNA.

required
use_ints_as_literals bool

If True, when an integer is encountered for a dictinonary value, treat it as the literal value. Otherwise, always treat it as a candidate index.

False

Returns:

Type Description
DNA

A DNA object.

Source code in pygx/geno/_base.py
@classmethod
def from_dict(
    cls,
    dict_repr: dict[Any, Any],
    dna_spec: DNASpec,
    use_ints_as_literals: bool = False,
) -> 'DNA':
    """Create a DNA from its dictionary representation.

    Args:
      dict_repr: The dictionary representation of the DNA.
        The keys should be either strings as the decision point ID
        or DNASpec objects. The values should be either numeric or literal
        values for the decisions.
        For inactive decisions, their ID/spec should either be absent from the
        dictionary, or use None as their values.
      dna_spec: The DNASpec that applies to the DNA.
      use_ints_as_literals: If True, when an integer is encountered for
        a dictinonary value, treat it as the literal value.
        Otherwise, always treat it as a candidate index.

    Returns:
      A DNA object.
    """

    def _get_decision(spec: DNASpec):
        """Gets the decision for DNASpec."""
        decision = dict_repr.get(spec.id, None)
        if decision is None:
            decision = dict_repr.get(spec, None)

        if decision is None and spec.name:
            decision = dict_repr.get(spec.name, None)
            # A spec can result in multiple decision points (e.g. multi-choices)
            # therefore we always pop up the next single decision if a name
            # is associated with multiple decisions.
            if isinstance(decision, list):
                dict_repr[spec.name] = decision[1:]
                decision = decision[0] if decision else None
        return decision

    def _choice_index(subchoice, value: int | float | str) -> int:
        """Gets the index of choice value based on its spec."""
        if isinstance(value, int) and not use_ints_as_literals:
            index = value
            if index < 0 or index >= len(subchoice.candidates):
                identifier = subchoice.name or subchoice.id
                raise ValueError(
                    f"Candidate index out of range at choice '{identifier}'. Index="
                    f'{index}, Number of candidates={len(subchoice.candidates)}.'
                )
        else:
            index = subchoice.candidate_index(value)
        return index

    def _make_dna(spec: DNASpec) -> DNA:
        """Lookup DNA value from parameter values according to DNA spec."""
        if spec.is_space:
            children = [_make_dna(elem) for elem in spec.elements]
            return DNA(None, children)
        else:
            if spec.is_categorical:
                children = []
                for choice_idx in range(spec.num_choices):
                    subchoice = spec.subchoice(choice_idx)
                    value = _get_decision(subchoice)

                    # It's possible that the decisions for multiple choices are
                    # collapsed as a single entry in the dictionary. e.g: {'x': [0, 1]}.
                    # In this case, we will make another attempt to get the decision
                    # from the parent spec entry.
                    if value is None and subchoice.is_subchoice:
                        parent_decisions = _get_decision(spec)
                        if parent_decisions is not None:
                            assert (
                                len(parent_decisions) == spec.num_choices
                            ), (parent_decisions, spec)
                            value = parent_decisions[choice_idx]

                    if value is None:
                        identifier = subchoice.name or subchoice.id
                        raise ValueError(
                            f"Value for '{identifier}' is not found in "
                            f'the dictionary {dict_repr!r}.'
                        )

                    if isinstance(value, DNA):
                        children.append(value)
                    else:
                        choice_index = _choice_index(subchoice, value)
                        subspace_dna = _make_dna(
                            subchoice.candidates[choice_index]
                        )
                        children.append(
                            DNA(
                                choice_index,
                                [subspace_dna] if subspace_dna else [],
                            )
                        )
                return DNA(None, children)
            elif spec.is_numerical or spec.is_custom_decision_point:
                value = _get_decision(spec)
                if value is None:
                    raise ValueError(
                        f"Value for '{spec.name or spec.id}' is not found "
                        f'in the dictionary {dict_repr!r}.'
                    )
                if isinstance(value, DNA):
                    value = value.value
                if spec.is_numerical:
                    if value < spec.min_value:
                        raise ValueError(
                            f"The decision for '{spec.name or spec.id}' should "
                            f'be no less than {spec.min_value}. Encountered {value}.'
                        )
                    if value > spec.max_value:
                        raise ValueError(
                            f"The decision for '{spec.name or spec.id}' should "
                            f'be no greater than {spec.max_value}. Encountered {value}.'
                        )
                else:
                    if not isinstance(value, str):
                        raise ValueError(
                            f"The decision for '{spec.name or spec.id}' should "
                            f'be a string. Encountered {value}.'
                        )
                return DNA(value, None)
            else:
                raise NotImplementedError(
                    'Should never happen.'
                )  # pragma: no cover

    dna = _make_dna(dna_spec)
    return dna.use_spec(dna_spec)

to_dict

to_dict(
    key_type: str = "id",
    value_type: str = "value",
    multi_choice_key: str = "subchoice",
    include_inactive_decisions: bool = False,
    filter_fn: Callable[[DecisionPoint], bool] | None = None,
) -> dict[
    DecisionPoint | str,
    Union[None, DNA, float, int, str, list[DNA], list[int], list[str]],
]

Returns the dict representation of current DNA.

Parameters:

Name Type Description Default
key_type str

Key type in returned dictionary. Acceptable values are:

  • 'id': Use the ID (canonical location) of each decision point as key. This is the default behavior.
  • 'name_or_id': Use the name of each decision point as key if it's present, otherwise use ID as key. When the name of a decision point is presented, it is guaranteed not to clash with other decision points' names or IDs.
  • 'dna_spec': Use the DNASpec object of each decision point as key.
'id'
value_type str

Value type for choices in returned dictionary. Acceptable values are:

  • 'value': Use the index of the chosen candidate for Choices, and use the float number for Float. This is the default behavior.
  • 'dna': Use DNA for all decision points.
  • 'choice': Use '{index}/{num_candidates}' for the chosen candidate for Choices, and the chosen float number for Float.
  • 'literal': Use the literal value for the chosen candidate for Choices, and the chosen float number for Float. If the literal value for the Choices decision point is not present, fall back to the '{index}/{num_candidates}' format.
  • 'choice_and_literal': Use '{index}/{num_candidates} ({literal})' for the chosen candidate for Choices and then chosen float number for Float. If the literal value for the Choices decision point is not present, fall back to the '{index}/{num_candidates}' format.
'value'
multi_choice_key str

'subchoice', 'parent', or 'both'. If 'subchoice', each subchoice will insert a key into the dict. If 'parent', subchoices of a multi-choice will share the parent spec as key, its value will be a list of decisions from the subchoices. If 'both', the dict will contain both the keys for subchoices and the key for the parent multi-choice.

'subchoice'
include_inactive_decisions bool

If True, inactive decisions from the search space will be added to the dict with value None. Otherwise they will be absent in the dict.

False
filter_fn Callable[[DecisionPoint], bool] | None

Decision point filter. If None, all the decision points will be included in the dict. Otherwise only the decision points that pass the filter (returns True) will be included.

None

Returns:

Type Description
dict[DecisionPoint | str, Union[None, DNA, float, int, str, list[DNA], list[int], list[str]]]

A dictionary of requested key type to value type mapped from the DNA.

Raises:

Type Description
ValueError

argument key_type or value_type is not valid.

RuntimeError

If DNA is not associated with a DNASpec.

Source code in pygx/geno/_base.py
def to_dict(
    self,
    key_type: str = 'id',
    value_type: str = 'value',
    multi_choice_key: str = 'subchoice',
    include_inactive_decisions: bool = False,
    filter_fn: Callable[[DecisionPoint], bool] | None = None,
) -> dict[
    DecisionPoint | str,
    Union[None, 'DNA', float, int, str, list['DNA'], list[int], list[str]],
]:
    """Returns the dict representation of current DNA.

    Args:
      key_type: Key type in returned dictionary. Acceptable values are:

        * 'id': Use the ID (canonical location) of each decision point as key.
          This is the default behavior.
        * 'name_or_id': Use the name of each decision point as key if it's
          present, otherwise use ID as key. When the name of a decision
          point is presented, it is guaranteed not to clash with other
          decision points' names or IDs.
        * 'dna_spec': Use the DNASpec object of each decision point as key.
      value_type: Value type for choices in returned dictionary.
        Acceptable values are:

        * 'value': Use the index of the chosen candidate for `Choices`, and
          use the float number for `Float`. This is the default behavior.
        * 'dna': Use `DNA` for all decision points.
        * 'choice': Use '{index}/{num_candidates}' for the chosen candidate
          for `Choices`, and the chosen float number for `Float`.
        * 'literal': Use the literal value for the chosen candidate for
          `Choices`, and the chosen float number for `Float`. If the literal
          value for the `Choices` decision point is not present, fall back
          to the '{index}/{num_candidates}' format.
        * 'choice_and_literal': Use '{index}/{num_candidates} ({literal})'
          for the chosen candidate for `Choices` and then chosen float number
          for `Float`. If the literal value for the `Choices` decision point
          is not present, fall back to the '{index}/{num_candidates}' format.

      multi_choice_key: 'subchoice', 'parent', or 'both'. If 'subchoice', each
        subchoice will insert a key into the dict. If 'parent', subchoices of
        a multi-choice will share the parent spec as key, its value will be
        a list of decisions from the subchoices. If 'both', the dict will
        contain both the keys for subchoices and the key for the parent
        multi-choice.
      include_inactive_decisions: If True, inactive decisions from the search
        space will be added to the dict with value None. Otherwise they will
        be absent in the dict.
      filter_fn: Decision point filter. If None, all the decision points will be
        included in the dict. Otherwise only the decision points that pass
        the filter (returns True) will be included.

    Returns:
      A dictionary of requested key type to value type mapped from the DNA.

    Raises:
      ValueError: argument `key_type` or `value_type` is not valid.
      RuntimeError: If DNA is not associated with a DNASpec.
    """
    if key_type not in ['id', 'name_or_id', 'dna_spec']:
        raise ValueError(
            f"'key_type' must be either 'id', 'name_or_id' "
            f"or 'dna_spec'. Encountered: {key_type!r}."
        )

    if value_type not in [
        'dna',
        'value',
        'choice',
        'literal',
        'choice_and_literal',
    ]:
        raise ValueError(
            f"'value_type' must be either 'dna', 'value', 'choice' "
            f"'literal' or 'choice_and_literal'. "
            f'Encountered: {value_type!r}.'
        )

    if multi_choice_key not in ['subchoice', 'parent', 'both']:
        raise ValueError(
            f"'multi_choice_key' must be either 'subchoice', 'parent', or "
            f"'both'. Encountered: {multi_choice_key!r}."
        )

    multi_choice_use_parent_as_key = multi_choice_key != 'subchoice'
    multi_choice_use_subchoice_as_key = multi_choice_key != 'parent'
    filter_fn = filter_fn or (lambda x: True)

    self._ensure_dna_spec()
    dict_repr = dict()

    def _needs_subchoice_key(subchoice):
        return multi_choice_use_subchoice_as_key and (
            not multi_choice_use_parent_as_key
            or (key_type != 'name_or_id' or subchoice.name is None)
        )

    def _key(spec: 'DecisionPoint'):
        if key_type == 'id':
            return spec.id.path
        elif key_type == 'name_or_id':
            return spec.name if spec.name else spec.id.path
        else:
            return spec

    def _put(key, value):
        if key in dict_repr:
            accumulated = dict_repr[key]
            if not isinstance(accumulated, list):
                accumulated = [accumulated]
            accumulated.append(value)
            value = accumulated
        dict_repr[key] = value
        return value

    def _dump_node(dna: DNA):
        """Dump node value to dict representation."""
        if isinstance(dna.spec, DecisionPoint) and filter_fn(dna.spec):
            key = _key(dna.spec)
            value = None
            if dna.spec.is_categorical and dna.value is not None:
                if value_type == 'dna':
                    value = dna
                elif value_type == 'value':
                    value = dna.value
                else:
                    value = dna.spec.format_candidate(
                        dna.value, display_format=value_type
                    )

                if dna.spec.is_subchoice:
                    # Append multi-choice values into parent's key.
                    if multi_choice_use_parent_as_key:
                        parent_spec = dna.spec.parent_spec
                        assert isinstance(parent_spec, DecisionPoint)
                        _put(_key(parent_spec), value)

                    # Insert subchoice in its own key.
                    if _needs_subchoice_key(dna.spec):
                        _put(key, value)
                else:
                    _put(key, value)
            elif dna.spec.is_numerical or dna.spec.is_custom_decision_point:
                if value_type == 'dna':
                    value = dna
                else:
                    value = dna.value
                _put(key, value)

        for child_dna in dna.children:
            _dump_node(child_dna)

    _dump_node(self)
    if not include_inactive_decisions:
        return dict_repr

    result = dict()
    assert self.spec is not None
    for dp in self.spec.decision_points:
        if not filter_fn(dp):
            continue
        if dp.is_categorical and dp.is_subchoice:
            if multi_choice_use_parent_as_key:
                if dp.subchoice_index == 0:
                    parent_spec = dp.parent_spec
                    assert isinstance(parent_spec, DecisionPoint)
                    k = _key(parent_spec)
                    result[k] = dict_repr.get(k, None)
            if _needs_subchoice_key(dp):
                k = _key(dp)
                result[k] = dict_repr.get(k, None)
        else:
            k = _key(dp)
            result[k] = dict_repr.get(k, None)
    return result

from_numbers classmethod

from_numbers(dna_values: list[int | float | str], dna_spec: DNASpec) -> DNA

Create a DNA from a flattened list of dna values.

Parameters:

Name Type Description Default
dna_values list[int | float | str]

A list of DNA values.

required
dna_spec DNASpec

DNASpec that interprets the dna values.

required

Returns:

Type Description
DNA

A DNA object.

Source code in pygx/geno/_base.py
@classmethod
def from_numbers(
    cls, dna_values: list[int | float | str], dna_spec: DNASpec
) -> 'DNA':
    """Create a DNA from a flattened list of dna values.

    Args:
      dna_values: A list of DNA values.
      dna_spec: DNASpec that interprets the dna values.

    Returns:
      A DNA object.
    """
    index = 0
    n_values = len(dna_values)

    def _next_decision():
        nonlocal index
        if index >= n_values:
            raise ValueError(
                f'The input {dna_values!r} is too short for {dna_spec!r}.'
            )
        decision = dna_values[index]
        index += 1
        return decision

    def _bind_decisions(dna_spec):
        value = None
        children = None
        if dna_spec.is_space:
            children = [_bind_decisions(elem) for elem in dna_spec.elements]
        elif dna_spec.is_categorical:
            if dna_spec.num_choices == 1:
                value = _next_decision()
                assert isinstance(value, int)
                if value < 0 or value >= len(dna_spec.candidates):
                    raise ValueError(
                        f'Candidate index out of range at choice '
                        f"'{dna_spec.name or dna_spec.id}'. Index={value}, "
                        f'Number of candidates={len(dna_spec.candidates)}.'
                    )
                children = [_bind_decisions(dna_spec.candidates[value])]
            else:
                children = [
                    _bind_decisions(spec) for spec in dna_spec.choice_specs
                ]
        else:
            value = _next_decision()
        # Skip `spec=` on inner constructions: the final `use_spec` on the
        # root DNA recurses through the whole tree, so attaching spec at
        # each level would pay the dispatch cost twice.
        return DNA(value, children)

    dna = _bind_decisions(dna_spec)
    if index != n_values:
        raise ValueError(
            f'The input {dna_values!r} is too long for {dna_spec!r}. '
            f'Remaining: {dna_values[index:]!r}.'
        )
    dna.use_spec(dna_spec)
    return dna

to_numbers

to_numbers(
    flatten: bool = True,
) -> list[int | float | str] | Nestable[int | float | str]

Returns a (maybe) nested structure of numbers as decisions.

Parameters:

Name Type Description Default
flatten bool

If True, the hierarchy of the numbers will not be preserved. Decisions will be returned as a flat list in DFS order. Otherwise, a nestable structure of numbers will be returned.

True

Returns:

Type Description
list[int | float | str] | Nestable[int | float | str]

A flat list or a hierarchical structure of numbers as the decisions made for each decision point.

Source code in pygx/geno/_base.py
def to_numbers(
    self,
    flatten: bool = True,
) -> list[int | float | str] | wire.Nestable[int | float | str]:
    """Returns a (maybe) nested structure of numbers as decisions.

    Args:
      flatten: If True, the hierarchy of the numbers will not be preserved.
        Decisions will be returned as a flat list in DFS order. Otherwise, a
        nestable structure of numbers will be returned.

    Returns:
      A flat list or a hierarchical structure of numbers as the decisions made
        for each decision point.
    """
    if flatten:
        decisions: list[int | float | str] = (
            [self.value] if self.value is not None else []
        )
        for c in self.children:
            decisions.extend(
                cast(list[int | float | str], c.to_numbers(flatten))
            )
        return decisions
    else:
        if self.value is None:
            return [c.to_numbers(flatten) for c in self.children]
        elif not self.children:
            return self.value
        elif len(self.children) == 1:
            child = self.children[0].to_numbers(flatten)
            if isinstance(child, tuple):
                return tuple([self.value, list(child)])
            else:
                return (self.value, child)
        else:
            assert len(self.children) > 1
            return (
                self.value,
                [c.to_numbers(flatten) for c in self.children],
            )

from_fn classmethod

from_fn(
    dna_spec: DNASpec,
    generator_fn: Callable[[DecisionPoint], Union[list[int], float, str, DNA]],
) -> DNA

Generate a DNA with user generator function.

Parameters:

Name Type Description Default
dna_spec DNASpec

The DNASpec for the DNA.

required
generator_fn Callable[[DecisionPoint], Union[list[int], float, str, DNA]]

A callable object with signature:

(decision_point) -> decision

The decision_point is a Choices object or a Float object. The returned decision should be:

  • a list of integer or a DNA object for a Choices decision point. When a DNA is returned, it will be used as the DNA for the entire sub-tree, hence generate_fn will not be called on sub-decision points.
  • a float or a DNA object for a Float decision point.
  • a string or a DNA object for a CustomDecisionPoint.
required

Returns:

Type Description
DNA

A DNA generated from the user function.

Source code in pygx/geno/_base.py
@classmethod
def from_fn(
    cls,
    dna_spec: DNASpec,
    generator_fn: Callable[
        ['DecisionPoint'], Union[list[int], float, str, 'DNA']
    ],
) -> 'DNA':
    """Generate a DNA with user generator function.

    Args:
      dna_spec: The DNASpec for the DNA.
      generator_fn: A callable object with signature:

        `(decision_point) -> decision`

        The decision_point is a `Choices` object or a `Float` object.
        The returned decision should be:

         * a list of integer or a DNA object for a `Choices` decision point.
           When a DNA is returned, it will be used as the DNA for the entire
           sub-tree, hence `generate_fn` will not be called on sub-decision
           points.
         * a float or a DNA object for a Float decision point.
         * a string or a DNA object for a CustomDecisionPoint.

    Returns:
      A DNA generated from the user function.
    """
    if not isinstance(dna_spec, DNASpec):
        raise TypeError(
            f"Argument 'dna_spec' should be DNASpec type. "
            f'Encountered {dna_spec}.'
        )

    if dna_spec.is_space:
        # Generate values for Space.
        children = []
        for child_spec in dna_spec.elements:
            children.append(DNA.from_fn(child_spec, generator_fn))
        if len(children) == 1:
            return children[0]
        dna = DNA(None, children)
    elif dna_spec.is_categorical:
        assert isinstance(dna_spec, DecisionPoint), dna_spec
        decision = generator_fn(dna_spec)
        if isinstance(decision, DNA):
            dna = decision
        else:
            decision = cast(Any, decision)
            if len(decision) != dna_spec.num_choices:
                raise ValueError(
                    f'Number of DNA child values does not match the number of '
                    f'choices. Child values: {decision!r}, '
                    f'Choices: {dna_spec.num_choices}, '
                    f'Location: {dna_spec.location.path}.'
                )
            children = []
            for i, choice in enumerate(decision):
                choice_location = topology.KeyPath(i, dna_spec.location)
                if not isinstance(choice, int):
                    raise ValueError(
                        f'Choice value should be int. Encountered: {choice}, '
                        f'Location: {choice_location.path}.'
                    )
                if choice >= len(dna_spec.candidates):
                    raise ValueError(
                        f'Choice out of range. Value: {choice}, '
                        f'Candidates: {len(dna_spec.candidates)}, '
                        f'Location: {choice_location.path}.'
                    )
                child_dna = DNA.from_fn(
                    dna_spec.candidates[choice], generator_fn
                )
                children.append(DNA(choice, [child_dna]))
            dna = DNA(None, children)
    else:
        assert isinstance(dna_spec, DecisionPoint), dna_spec
        decision = generator_fn(dna_spec)
        if isinstance(decision, DNA):
            dna = decision
        else:
            dna = DNA(decision)
    dna_spec.validate(dna)
    return dna

sym_jsonify

sym_jsonify(compact: bool = True, type_info: bool = True, **kwargs: Any) -> Any

Convert DNA to JSON object.

Parameters:

Name Type Description Default
compact bool

Whether use compact form. If compact, the nested number structure in DNA.parse will be used, otherwise members will be rendered out as regular symbolic Object.

True
type_info bool

If True, type information will be included in output, otherwise only the compact value is emitted (as a proper WireValue — tuples in their __tuple__ marker form; metadata is not attached). Applicable when compact is set to True. Note this parameter predates and shadows the to_json(type_info=...) dump option (#519) — a threaded type_info=False selects this compact value-only form.

True
**kwargs Any

Keyword arguments that will be passed to symbolic.Object if compact is False.

{}

Returns:

Type Description
Any

JSON representation of DNA.

Source code in pygx/geno/_base.py
def sym_jsonify(
    self, compact: bool = True, type_info: bool = True, **kwargs: Any
) -> Any:
    """Convert DNA to JSON object.

    Args:
      compact: Whether use compact form. If compact, the nested number structure
        in DNA.parse will be used, otherwise members will be rendered out as
        regular symbolic Object.
      type_info: If True, type information will be included in output, otherwise
        only the compact value is emitted (as a proper WireValue — tuples
        in their `__tuple__` marker form; `metadata` is not attached).
        Applicable when compact is set to True. Note this parameter
        predates and shadows the `to_json(type_info=...)` dump option
        (#519) — a threaded `type_info=False` selects this compact
        value-only form.
      **kwargs: Keyword arguments that will be passed to symbolic.Object if
        compact is False.

    Returns:
      JSON representation of DNA.
    """
    if not compact:
        json_value = super().sym_jsonify(**kwargs)
        assert isinstance(json_value, dict), json_value
        if self._cloneable_metadata_keys:
            json_value['_cloneable_metadata_keys'] = list(
                self._cloneable_metadata_keys
            )
        return json_value

    value = self._compact_value()
    if type_info:
        json_value = {
            wire.WireConvertible.TYPE_NAME_KEY: (
                self.__class__.__serialization_key__
            ),
            'format': 'compact',
            'value': symbolic.to_json(value),
        }
        # NOTE(daiyip): For now, we only attach metadata from the root node for
        # faster serialization/deserialization speed. This should be revised if
        # metadata for child DNA is used.
        if self.metadata:
            json_value['metadata'] = symbolic.to_json(self.metadata)

        if self._cloneable_metadata_keys:
            json_value['_cloneable_metadata_keys'] = list(
                self._cloneable_metadata_keys
            )
        return json_value
    else:
        # WireValue-clean (#531 review M2): the compact value converts
        # through `symbolic.to_json` so a raw tuple never escapes into a
        # wire payload (its `__tuple__` marker form emits instead). The
        # raw form stays available internally via `_compact_value`.
        return symbolic.to_json(value)

from_json classmethod

from_json(
    json_value: dict[str, Any],
    *,
    allow_partial: bool = False,
    root_path: KeyPath | None = None,
    **kwargs: Any
) -> DNA

Class method that load a DNA from a JSON value.

Parameters:

Name Type Description Default
json_value dict[str, Any]

Input JSON value, only JSON dict is acceptable.

required
allow_partial bool

Whether to allow elements of the list to be partial.

False
root_path KeyPath | None

KeyPath of loaded object in its object tree.

None
**kwargs Any

Keyword arguments that will be passed to symbolic.from_json.

{}

Returns:

Type Description
DNA

A DNA object.

Source code in pygx/geno/_base.py
@classmethod
def from_json(
    cls,
    json_value: dict[str, Any],
    *,
    allow_partial: bool = False,
    root_path: topology.KeyPath | None = None,
    **kwargs: Any,
) -> 'DNA':
    """Class method that load a DNA from a JSON value.

    Args:
      json_value: Input JSON value, only JSON dict is acceptable.
      allow_partial: Whether to allow elements of the list to be partial.
      root_path: KeyPath of loaded object in its object tree.
      **kwargs: Keyword arguments that will be passed to symbolic.from_json.

    Returns:
      A DNA object.
    """
    cloneable_metadata_keys = json_value.pop(
        '_cloneable_metadata_keys', None
    )
    if json_value.get('format', None) == 'compact':
        # NOTE(daiyip): DNA.parse already validates the input.
        dna = DNA.parse(
            symbolic.from_json(json_value.get('value'), **kwargs)
        )
        if 'metadata' in json_value:
            dna.sym_rebind(
                metadata=symbolic.from_json(
                    json_value.get('metadata'), **kwargs
                ),
                raise_on_no_change=False,
                skip_notification=True,
            )
    else:
        dna = super().from_json(
            json_value,
            allow_partial=allow_partial,
            root_path=root_path,
            **kwargs,
        )  # pytype: disable=bad-return-type
        assert isinstance(dna, DNA)
    if cloneable_metadata_keys:
        dna._cloneable_metadata_keys = set(cloneable_metadata_keys)  # pylint: disable=protected-access
    return dna

get

get(
    key: Union[int, slice, str, KeyPath, DecisionPoint], default: Any = None
) -> Union[Any, None, DNA, list[Optional[DNA]]]

Get an immediate child DNA or DNA in the sub-tree.

Source code in pygx/geno/_base.py
def get(
    self,
    key: Union[int, slice, str, topology.KeyPath, 'DecisionPoint'],
    default: Any = None,
) -> Union[Any, None, 'DNA', list[Optional['DNA']]]:
    """Get an immediate child DNA or DNA in the sub-tree."""
    try:
        return self[key]
    except KeyError:
        return default

next_dna

next_dna() -> Optional[DNA]

Get the next DNA in the spec.

Source code in pygx/geno/_base.py
def next_dna(self) -> Optional['DNA']:
    """Get the next DNA in the spec."""
    self._ensure_dna_spec()
    assert self.spec is not None
    return self.spec.next_dna(self)

iter_dna

iter_dna()

Iterate DNA of the space starting from self.

Source code in pygx/geno/_base.py
def iter_dna(self):
    """Iterate DNA of the space starting from self."""
    self._ensure_dna_spec()
    assert self.spec is not None
    return self.spec.iter_dna(self)

format

format(
    compact: bool = False,
    verbose: bool = True,
    root_indent: int = 0,
    *,
    list_wrap_threshold: int = 80,
    as_dict: bool = False,
    **kwargs
)

Customize format method for DNA for more compact representation.

Source code in pygx/geno/_base.py
def format(
    self,
    compact: bool = False,
    verbose: bool = True,
    root_indent: int = 0,
    *,
    list_wrap_threshold: int = 80,
    as_dict: bool = False,
    **kwargs,
):
    """Customize format method for DNA for more compact representation."""
    if as_dict and self.spec:
        details = formatting.format(
            self.to_dict(value_type='choice_and_literal'),
            False,
            verbose,
            root_indent,
            **kwargs,
        )
        s = f'DNA({details})'
    else:
        kwargs['list_wrap_threshold'] = list_wrap_threshold

        if not verbose:
            s = super().format(False, verbose, root_indent, **kwargs)
        elif self.is_leaf:
            s = f'DNA({self.value!r})'
        else:
            rep = formatting.format(
                self._compact_value(),
                compact,
                verbose,
                root_indent,
                **kwargs,
            )
            if rep and rep[0] == '(':
                # NOTE(daiyip): for conditional choice from the root,
                # we don't want to keep duplicate round bracket.
                s = f'DNA{rep}'
            else:
                s = f'DNA({rep})'
    return s

parameters

parameters(use_literal_values: bool = False) -> dict[str, str]

Returns parameters for this DNA to emit based on its spec.

Deprecated: use to_dict instead.

Parameters:

Name Type Description Default
use_literal_values bool

If True, use literal values from DNASpec for Choices, otherwise returns '{choice}/{num_candidates} ({literal})'. Otherwise returns '{choice}/{num_candidates}'.

False

Returns:

Type Description
dict[str, str]

Dict of parameter names to their values mapped from this DNA.

Raises:

Type Description
RuntimeError

If DNA is not associated with a DNASpec.

Source code in pygx/geno/_base.py
def parameters(self, use_literal_values: bool = False) -> dict[str, str]:
    """Returns parameters for this DNA to emit based on its spec.

    Deprecated: use `to_dict` instead.

    Args:
      use_literal_values: If True, use literal values from DNASpec for Choices,
        otherwise returns '{choice}/{num_candidates} ({literal})'. Otherwise
        returns '{choice}/{num_candidates}'.

    Returns:
      Dict of parameter names to their values mapped from this DNA.

    Raises:
      RuntimeError: If DNA is not associated with a DNASpec.
    """
    value_type = 'choice_and_literal' if use_literal_values else 'choice'
    return cast(dict[str, str], self.to_dict(value_type=value_type))

from_parameters classmethod

from_parameters(
    parameters: dict[str, Any],
    dna_spec: DNASpec,
    use_literal_values: bool = False,
) -> DNA

Create DNA from parameters based on DNASpec.

Deprecated: use from_dict instead.

Parameters:

Name Type Description Default
parameters dict[str, Any]

A 1-depth dict of parameter names to parameter values.

required
dna_spec DNASpec

DNASpec to interpret the parameters.

required
use_literal_values bool

If True, parameter values are literal values from DNASpec.

False

Returns:

Type Description
DNA

DNA instance bound with the DNASpec.

Raises:

Type Description
ValueError

If parameters are not aligned with DNA spec.

Source code in pygx/geno/_base.py
@classmethod
def from_parameters(
    cls,
    parameters: dict[str, Any],
    dna_spec: DNASpec,
    use_literal_values: bool = False,
) -> 'DNA':
    """Create DNA from parameters based on DNASpec.

    Deprecated: use `from_dict` instead.

    Args:
      parameters: A 1-depth dict of parameter names to parameter values.
      dna_spec: DNASpec to interpret the parameters.
      use_literal_values: If True, parameter values are literal values from
        DNASpec.

    Returns:
      DNA instance bound with the DNASpec.

    Raises:
      ValueError: If parameters are not aligned with DNA spec.
    """
    del use_literal_values
    return cls.from_dict(parameters, dna_spec)

AttributeDict

Bases: dict

A dictionary that allows attribute access.

ConditionalKey

ConditionalKey(index: int, num_choices: int)

Key used in pygx.KeyPath to represent conditional element.

For example, a[=1].b means a.b when a == 1. a[0][=0][0] means a[0][0] when a[0] == 0.

Source code in pygx/geno/_base.py
def __init__(self, index: int, num_choices: int):
    self._index = index
    self._num_choices = num_choices

index property

index: int

Return selected index of current condition.

num_choices property

num_choices: int

Returns number of choices of current condition.

DecisionPoint

DecisionPoint(
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any
)

Bases: DNASpec

Base class for decision points.

Child classes:

Source code in pygx/symbolic/_object.py
@_filter_typing_traceback_on_raise
def __init__(
    self,
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: topology.KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any,
):
    """Create an Object instance.

    ``pg.Object`` synthesizes a keyword-only ``__init__``. Subclasses
    that need positional arguments must override ``__init__``
    explicitly and translate to keyword arguments before forwarding
    to ``super().__init__(**kwargs)``.

    Args:
      allow_partial: If True, the object can be partial.
      sealed: If True, seal the object from future modification (unless under
        a `pg.as_sealed(False)` context manager). If False, treat the object as
        unsealed. If None, it's determined by `cls.__sym_options__.frozen`.
      root_path: The symbolic path for current object. By default it's None,
        which indicates that newly constructed object does not have a parent.
      explicit_init: Should set to `True` when `__init__` is called via
        `pg.Object.__init__` instead of `super().__init__`.
      **kwargs: key/value arguments that align with the schema. All required
        keys in the schema must be specified, and values should be acceptable
        according to their value spec.

    Raises:
      KeyError: When required key(s) are missing.
      ValueError: When value(s) are not acceptable by their value spec.
    """
    # Placeholder for Google-internal usage instrumentation.

    cls = self.__class__
    # The ever-instantiated latch `apply_schema`'s post-instance guard
    # reads (#432: `update_schema` is only legal before instances exist).
    # Own-`__dict__`, latched on the construction ATTEMPT — mirroring the
    # native core, whose `__native_ctor_meta__` (the equivalent marker) is
    # created on the first instance op. One dict read per construct.
    meta = cls.__sym_meta__
    if not meta.instantiated:
        meta.instantiated = True
    if sealed is None:
        sealed = cls.__sym_options__.frozen

    if not isinstance(allow_partial, bool):
        raise TypeError(
            f"Expect bool type for argument 'allow_partial' in "
            f'symbolic.Object.__init__ but encountered {allow_partial}.'
        )

    # Fast path: const-keyed schema.
    if cls._sym_init_fields is not None and self._init_fast_path(
        kwargs,
        allow_partial,
        sealed,
        root_path,
        explicit_init,
    ):
        return

    # Slow path: handles varargs and schemas with non-const keys.

    # Model before-hook (#516) — the fast path was not taken
    # (`_sym_init_fields is None`), so the hook has not run yet.
    if cls._sym_preinit_overridden:
        kwargs = _apply_preinit(cls, kwargs)

    # We delay the seal attempt until members are all set.
    super().__init__(
        allow_partial=allow_partial,
        accessor_writable=cls.__sym_options__.attr_write,
        sealed=sealed,
        root_path=root_path,
        init_super=not explicit_init,
    )

    # Fill field_args and init_args from **kwargs.
    _, unmatched_keys = cls.__schema__.resolve(list(kwargs.keys()))
    # Fields opted out of `__init__` via `enable_init=False` resolve
    # against the schema but must still be rejected as kwargs.
    get_field = cls.__schema__.get_field
    unmatched_keys = list(unmatched_keys) + [
        k
        for k in kwargs
        if (f := get_field(k)) is not None and not f.enable_init
    ]
    if unmatched_keys:
        _screen_unexpected_init_keys(cls, kwargs, unmatched_keys)

    # Declared aliases are RESERVED wire spellings (#517, owner ruling):
    # an open (varkw) schema must not accept an extra kwarg equal to a
    # field's alias — the wire would become ambiguous. (Const-keyed
    # schemas never reach here with an alias-spelled kwarg; their
    # unknown-key screen already raised — or, under `varkw='ignore'`,
    # raised for alias keys before dropping the rest.)
    if cls._sym_alias_maps is not None:
        alias_to_name = cls._sym_alias_maps[0]
        for k in kwargs:
            if k in alias_to_name:
                raise TypeError(
                    f'{cls.__name__}.__init__() got keyword argument '
                    f'{k!r}, which is reserved as the alias of field '
                    f'{alias_to_name[k]!r}.'
                )

    # All field values come from kwargs — `pg.Object.__init__` is
    # keyword-only.
    field_args = dict(kwargs)

    # Check missing arguments when partial binding is disallowed.
    if not base.accepts_partial(self):
        missing_args = []
        for field in self.__class__.__schema__.fields.values():
            if (
                not field.value.has_default
                and isinstance(field.key, pg_typing.ConstStrKey)
                and field.key not in field_args
            ):
                missing_args.append(str(field.key))
        if missing_args:
            arg_phrase = formatting.auto_plural(
                len(missing_args), 'argument'
            )
            keys_str = formatting.comma_delimited_str(missing_args)
            raise TypeError(
                f'{self.__class__.__name__}.__init__() missing {len(missing_args)} '
                f'required {arg_phrase}: {keys_str}.'
            )

    # NOTE(daiyip): Accessor writable is honored by `Object.__setattr__` so
    # we can always make `_sym_attributes` accessor writable. This prevents
    # a child object's attribute access from being changed when it's
    # attached to a parent whose symbolic attributes are not writable.
    # Per-field validation is gated by `field.enable_validation` inside
    # `Schema.apply` / `Dict._formalized_value` — so a class-wide
    # `validate=False` (resolved into each field) and per-field overrides
    # both flow through naturally here.
    vars(self)['_sym_attributes'] = pg_dict.Dict(
        field_args,  # pyright: ignore[reportArgumentType]
        value_spec=self.__class__.sym_fields,
        allow_partial=allow_partial,
        sealed=sealed,
        accessor_writable=True,
        root_path=root_path,
        as_object_attributes_container=True,
        under_sym_mode=cls.__sym_options__.sym,
    )
    self._sym_attributes.sym_setparent(self)
    # Seed `init=False` fields from their defaults before `on_sym_post_init`
    # runs so `on_sym_ready`/`on_sym_bound` overrides start from a known baseline.
    _seed_non_symbolic_fields(self, cls._non_symbolic_fields)
    # Model after-validator (#516): fires before any lifecycle hook can
    # observe the object, gated like `on_sym_ready` (concrete only).
    if cls._sym_validate_overridden:
        abstract = (
            self._allow_partial and self.sym_partial
        ) or self.sym_puresymbolic
        if not abstract:
            self.on_sym_validate()
    self.on_sym_post_init()
    self.sym_seal(sealed)

DNASpec

DNASpec(
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any
)

Bases: Object

Base class for DNA specifications (genotypes).

A DNASpec object describes the rules and tips for generating a DNA.

Concrete decision points are the following:

All DecisionPoints provide hints for DNA generator as a reference when generating values, it needs to be serializable to pass between client and servers.

All DNASpec types allow user to attach their data via set_userdata method, it's aimed to be used within the same process, thus not required to be serializable.

Source code in pygx/symbolic/_object.py
@_filter_typing_traceback_on_raise
def __init__(
    self,
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: topology.KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any,
):
    """Create an Object instance.

    ``pg.Object`` synthesizes a keyword-only ``__init__``. Subclasses
    that need positional arguments must override ``__init__``
    explicitly and translate to keyword arguments before forwarding
    to ``super().__init__(**kwargs)``.

    Args:
      allow_partial: If True, the object can be partial.
      sealed: If True, seal the object from future modification (unless under
        a `pg.as_sealed(False)` context manager). If False, treat the object as
        unsealed. If None, it's determined by `cls.__sym_options__.frozen`.
      root_path: The symbolic path for current object. By default it's None,
        which indicates that newly constructed object does not have a parent.
      explicit_init: Should set to `True` when `__init__` is called via
        `pg.Object.__init__` instead of `super().__init__`.
      **kwargs: key/value arguments that align with the schema. All required
        keys in the schema must be specified, and values should be acceptable
        according to their value spec.

    Raises:
      KeyError: When required key(s) are missing.
      ValueError: When value(s) are not acceptable by their value spec.
    """
    # Placeholder for Google-internal usage instrumentation.

    cls = self.__class__
    # The ever-instantiated latch `apply_schema`'s post-instance guard
    # reads (#432: `update_schema` is only legal before instances exist).
    # Own-`__dict__`, latched on the construction ATTEMPT — mirroring the
    # native core, whose `__native_ctor_meta__` (the equivalent marker) is
    # created on the first instance op. One dict read per construct.
    meta = cls.__sym_meta__
    if not meta.instantiated:
        meta.instantiated = True
    if sealed is None:
        sealed = cls.__sym_options__.frozen

    if not isinstance(allow_partial, bool):
        raise TypeError(
            f"Expect bool type for argument 'allow_partial' in "
            f'symbolic.Object.__init__ but encountered {allow_partial}.'
        )

    # Fast path: const-keyed schema.
    if cls._sym_init_fields is not None and self._init_fast_path(
        kwargs,
        allow_partial,
        sealed,
        root_path,
        explicit_init,
    ):
        return

    # Slow path: handles varargs and schemas with non-const keys.

    # Model before-hook (#516) — the fast path was not taken
    # (`_sym_init_fields is None`), so the hook has not run yet.
    if cls._sym_preinit_overridden:
        kwargs = _apply_preinit(cls, kwargs)

    # We delay the seal attempt until members are all set.
    super().__init__(
        allow_partial=allow_partial,
        accessor_writable=cls.__sym_options__.attr_write,
        sealed=sealed,
        root_path=root_path,
        init_super=not explicit_init,
    )

    # Fill field_args and init_args from **kwargs.
    _, unmatched_keys = cls.__schema__.resolve(list(kwargs.keys()))
    # Fields opted out of `__init__` via `enable_init=False` resolve
    # against the schema but must still be rejected as kwargs.
    get_field = cls.__schema__.get_field
    unmatched_keys = list(unmatched_keys) + [
        k
        for k in kwargs
        if (f := get_field(k)) is not None and not f.enable_init
    ]
    if unmatched_keys:
        _screen_unexpected_init_keys(cls, kwargs, unmatched_keys)

    # Declared aliases are RESERVED wire spellings (#517, owner ruling):
    # an open (varkw) schema must not accept an extra kwarg equal to a
    # field's alias — the wire would become ambiguous. (Const-keyed
    # schemas never reach here with an alias-spelled kwarg; their
    # unknown-key screen already raised — or, under `varkw='ignore'`,
    # raised for alias keys before dropping the rest.)
    if cls._sym_alias_maps is not None:
        alias_to_name = cls._sym_alias_maps[0]
        for k in kwargs:
            if k in alias_to_name:
                raise TypeError(
                    f'{cls.__name__}.__init__() got keyword argument '
                    f'{k!r}, which is reserved as the alias of field '
                    f'{alias_to_name[k]!r}.'
                )

    # All field values come from kwargs — `pg.Object.__init__` is
    # keyword-only.
    field_args = dict(kwargs)

    # Check missing arguments when partial binding is disallowed.
    if not base.accepts_partial(self):
        missing_args = []
        for field in self.__class__.__schema__.fields.values():
            if (
                not field.value.has_default
                and isinstance(field.key, pg_typing.ConstStrKey)
                and field.key not in field_args
            ):
                missing_args.append(str(field.key))
        if missing_args:
            arg_phrase = formatting.auto_plural(
                len(missing_args), 'argument'
            )
            keys_str = formatting.comma_delimited_str(missing_args)
            raise TypeError(
                f'{self.__class__.__name__}.__init__() missing {len(missing_args)} '
                f'required {arg_phrase}: {keys_str}.'
            )

    # NOTE(daiyip): Accessor writable is honored by `Object.__setattr__` so
    # we can always make `_sym_attributes` accessor writable. This prevents
    # a child object's attribute access from being changed when it's
    # attached to a parent whose symbolic attributes are not writable.
    # Per-field validation is gated by `field.enable_validation` inside
    # `Schema.apply` / `Dict._formalized_value` — so a class-wide
    # `validate=False` (resolved into each field) and per-field overrides
    # both flow through naturally here.
    vars(self)['_sym_attributes'] = pg_dict.Dict(
        field_args,  # pyright: ignore[reportArgumentType]
        value_spec=self.__class__.sym_fields,
        allow_partial=allow_partial,
        sealed=sealed,
        accessor_writable=True,
        root_path=root_path,
        as_object_attributes_container=True,
        under_sym_mode=cls.__sym_options__.sym,
    )
    self._sym_attributes.sym_setparent(self)
    # Seed `init=False` fields from their defaults before `on_sym_post_init`
    # runs so `on_sym_ready`/`on_sym_bound` overrides start from a known baseline.
    _seed_non_symbolic_fields(self, cls._non_symbolic_fields)
    # Model after-validator (#516): fires before any lifecycle hook can
    # observe the object, gated like `on_sym_ready` (concrete only).
    if cls._sym_validate_overridden:
        abstract = (
            self._allow_partial and self.sym_partial
        ) or self.sym_puresymbolic
        if not abstract:
            self.on_sym_validate()
    self.on_sym_post_init()
    self.sym_seal(sealed)

is_space abstractmethod property

is_space: bool

Returns True if current node is a sub-space.

is_categorical abstractmethod property

is_categorical: bool

Returns True if current node is a categorical choice.

is_subchoice abstractmethod property

is_subchoice: bool

Returns True if current node is a subchoice of a multi-choice.

is_numerical abstractmethod property

is_numerical: bool

Returns True if current node is numerical decision.

is_custom_decision_point abstractmethod property

is_custom_decision_point: bool

Returns True if current node is a custom decision point.

decision_points abstractmethod property

decision_points: list[DecisionPoint]

Returns all decision points in their declaration order.

decision_ids property

decision_ids: list[KeyPath]

Returns decision IDs.

named_decision_points property

named_decision_points: dict[str, Union[DecisionPoint, list[DecisionPoint]]]

Returns all named decision points in their declaration order.

space_size abstractmethod property

space_size: int

Returns the size of the search space. Use -1 for infinity.

parent_spec property

parent_spec: Optional[DNASpec]

Returns parent spec. None if spec is root.

parent_choice property

parent_choice: Optional[DecisionPoint]

Returns the parent choice of current space.

id property

id: KeyPath

Returns a path of locations from the root as the ID for current node.

userdata property

userdata: AttributeDict

Gets user data.

on_sym_bound

on_sym_bound()

Event that is triggered when object is modified.

Source code in pygx/geno/_base.py
def on_sym_bound(self):
    """Event that is triggered when object is modified."""
    super().on_sym_bound()
    self._id = None
    self._named_decision_points_cache = None
    self._decision_point_by_id_cache = None
    self._userdata = AttributeDict()

on_sym_path_change

on_sym_path_change(old_path, new_path)

Event that is triggered when path changes.

Source code in pygx/geno/_base.py
def on_sym_path_change(self, old_path, new_path):
    """Event that is triggered when path changes."""
    super().on_sym_path_change(old_path, new_path)
    # We invalidate the ID cache and decision_point_by_id cache
    # when the ancestor hierarchy changes, which will force the ID and
    # the cache to be recomputed upon usage.
    self._id = None
    self._decision_point_by_id_cache = None

validate abstractmethod

validate(dna: DNA) -> None

Validate whether a DNA value conforms to this spec.

Source code in pygx/geno/_base.py
@abc.abstractmethod
def validate(self, dna: 'DNA') -> None:
    """Validate whether a DNA value conforms to this spec."""

first_dna

first_dna(attach_spec: bool = True) -> DNA

Returns the first DNA in the spec.

Source code in pygx/geno/_base.py
def first_dna(self, attach_spec: bool = True) -> 'DNA':
    """Returns the first DNA in the spec."""
    dna = self.next_dna(None, attach_spec)
    assert dna is not None
    return dna

next_dna

next_dna(dna: Optional[DNA] = None, attach_spec: bool = True) -> Optional[DNA]

Returns the next DNA in the space represented by this spec.

Parameters:

Name Type Description Default
dna Optional[DNA]

The DNA whose next will be returned. If None, next_dna will return the first DNA.

None
attach_spec bool

If True, current spec will be attached to the returned DNA.

True

Returns:

Type Description
Optional[DNA]

The next DNA or None if there is no next DNA.

Source code in pygx/geno/_base.py
def next_dna(
    self, dna: Optional['DNA'] = None, attach_spec: bool = True
) -> Optional['DNA']:
    """Returns the next DNA in the space represented by this spec.

    Args:
      dna: The DNA whose next will be returned. If None, `next_dna` will return
        the first DNA.
      attach_spec: If True, current spec will be attached to the returned DNA.

    Returns:
      The next DNA or None if there is no next DNA.
    """
    dna = self._next_dna(dna)
    if attach_spec and dna is not None:
        dna.use_spec(self)
    return dna

random_dna

random_dna(
    random_generator: ModuleType | Random | None = None,
    attach_spec: bool = True,
    previous_dna: Optional[DNA] = None,
) -> DNA

Returns a random DNA based on current spec.

Parameters:

Name Type Description Default
random_generator ModuleType | Random | None

An optional Random object. If None, the global random module will be used.

None
attach_spec bool

If True, current spec will be attached to the returned DNA.

True
previous_dna Optional[DNA]

An optional DNA representing previous DNA. This field might be useful for generating stateful random DNAs.

None

Returns:

Type Description
DNA

A random DNA based on current spec.

Source code in pygx/geno/_base.py
def random_dna(
    self,
    random_generator: types.ModuleType | random.Random | None = None,
    attach_spec: bool = True,
    previous_dna: Optional['DNA'] = None,
) -> 'DNA':
    """Returns a random DNA based on current spec.

    Args:
      random_generator: An optional Random object. If None, the global random
        module will be used.
      attach_spec: If True, current spec will be attached to the returned DNA.
      previous_dna: An optional DNA representing previous DNA. This field might
        be useful for generating stateful random DNAs.

    Returns:
      A random DNA based on current spec.
    """
    random_generator = random_generator or random
    dna = self._random_dna(random_generator, previous_dna)
    if attach_spec:
        dna.use_spec(self)
    return dna

iter_dna

iter_dna(dna: Optional[DNA] = None, attach_spec: bool = True) -> Iterator[DNA]

Iterate the DNA in the space represented by this spec.

Parameters:

Name Type Description Default
dna Optional[DNA]

An optional DNA as the start point (exclusive) for iteration.

None
attach_spec bool

If True, the DNASpec will be attached to the DNA returned.

True

Yields:

Type Description
DNA

The next DNA according to the spec.

Source code in pygx/geno/_base.py
def iter_dna(
    self, dna: Optional['DNA'] = None, attach_spec: bool = True
) -> Iterator['DNA']:
    """Iterate the DNA in the space represented by this spec.

    Args:
      dna: An optional DNA as the start point (exclusive) for iteration.
      attach_spec: If True, the DNASpec will be attached to the DNA returned.

    Yields:
      The next DNA according to the spec.
    """
    while True:
        dna = self.next_dna(dna, attach_spec)
        if dna is None:
            break
        yield dna

get

get(
    name_or_id: KeyPath | str, default: Any = None
) -> Union[DecisionPoint, list[DecisionPoint]]

Get decision point(s) by name or ID.

Source code in pygx/geno/_base.py
def get(
    self, name_or_id: topology.KeyPath | str, default: Any = None
) -> Union['DecisionPoint', list['DecisionPoint']]:
    """Get decision point(s) by name or ID."""
    try:
        return self[name_or_id]
    except KeyError:
        return default

set_userdata

set_userdata(key: str, value: Any) -> None

Sets user data.

User data can be used for storing state associated with the DNASpec, and is not persisted across processes or during serialization. Use hints to carry persistent objects for the DNASpec.

Parameters:

Name Type Description Default
key str

Key of the user data.

required
value Any

Value of the user data.

required
Source code in pygx/geno/_base.py
def set_userdata(self, key: str, value: Any) -> None:
    """Sets user data.

    User data can be used for storing state associated with the DNASpec, and
    is not persisted across processes or during serialization. Use `hints` to
    carry persistent objects for the DNASpec.

    Args:
      key: Key of the user data.
      value: Value of the user data.
    """
    self._userdata[key] = value

from_json classmethod

from_json(json_value, *args, **kwargs) -> Object

Override from_json for backward compatibility with serialized data.

Source code in pygx/geno/_base.py
@classmethod
def from_json(cls, json_value, *args, **kwargs) -> symbolic.Object:
    """Override from_json for backward compatibility with serialized data."""
    assert isinstance(json_value, dict)
    json_value.pop('userdata', None)
    return super().from_json(json_value, *args, **kwargs)

Choices

Choices(num_choices: int = 1, *, candidates: list[Space], **kwargs)

Bases: DecisionPoint

Represents a single or multiple choices from a list of candidates.

Example::

# Create a single choice with a nested subspace for candidate 2 (0-based). pg.geno.oneof([ pg.geno.constant(), pg.geno.constant(), pg.geno.space([ pg.geno.floatv(0.1, 1.0) pg.geno.manyof(2, [ pg.geno.constant(), pg.geno.constant(), pg.geno.constant() ]) ]) ])

See also: pygx.geno.oneof, pygx.geno.manyof.

Source code in pygx/geno/_categorical.py
def __init__(
    self,
    num_choices: int = 1,
    *,
    candidates: list['Space'],
    **kwargs,
):
    super().__init__(
        **dict(num_choices=num_choices, candidates=candidates),
        **kwargs,
    )

choice_specs property

choice_specs: list[Choices]

Returns all choice specs.

is_categorical property

is_categorical: bool

Returns True if current node is a categorical choice.

is_subchoice property

is_subchoice: bool

Returns if current choice is a subchoice of a multi-choice.

is_numerical property

is_numerical: bool

Returns True if current node is numerical decision.

is_custom_decision_point property

is_custom_decision_point: bool

Returns True if current node is a custom decision point.

decision_points property

decision_points: list[DecisionPoint]

Returns all decision points in their declaration order.

Returns:

Type Description
list[DecisionPoint]

All decision points in current space. For multi-choices, the sub-choice

list[DecisionPoint]

objects will be returned. Users can call spec.parent_choice to access

list[DecisionPoint]

the parent multi-choice node.

space_size property

space_size: int

Returns the search space size.

subchoice

subchoice(index: int) -> Choices

Returns spec for choice i.

Source code in pygx/geno/_categorical.py
def subchoice(self, index: int) -> 'Choices':
    """Returns spec for choice i."""
    if self.is_subchoice:
        raise ValueError(
            f"'subchoice' should not be called on a subchoice of a "
            f'multi-choice. Encountered: {self!r}.'
        )
    if self.num_choices == 1:
        return self
    assert self._subchoice_specs is not None
    return self._subchoice_specs[index]

format_candidate

format_candidate(
    index: int, display_format: str = "choice_and_literal"
) -> str | int | float

Get a formatted candidate value by index.

Parameters:

Name Type Description Default
index int

The index of the candidate to format.

required
display_format str

One of 'choice', 'literal' and 'choice_and_literal' as the output format for human consumption.

'choice_and_literal'

Returns:

Type Description
str | int | float

A int, float or string that represent the candidate based on the display format.

Source code in pygx/geno/_categorical.py
def format_candidate(
    self, index: int, display_format: str = 'choice_and_literal'
) -> str | int | float:
    """Get a formatted candidate value by index.

    Args:
      index: The index of the candidate to format.
      display_format: One of 'choice', 'literal' and 'choice_and_literal' as
        the output format for human consumption.

    Returns:
      A int, float or string that represent the candidate based on the
        display format.
    """
    if display_format not in ['choice', 'literal', 'choice_and_literal']:
        raise ValueError(
            f"`display_format` must be either 'choice', 'literal', or "
            f"'choice_and_literal'. Encountered: {display_format!r}."
        )
    if self.literal_values:
        if display_format == 'literal':
            return self.literal_values[index]
        elif display_format == 'choice_and_literal':
            return f'{index}/{len(self.candidates)} ({self.literal_values[index]})'
    return f'{index}/{len(self.candidates)}'

candidate_index

candidate_index(choice_value: str | int | float) -> int

Returns the candidate index of a choice value.

Parameters:

Name Type Description Default
choice_value str | int | float

Choice value can be: a (integer, float or string) as a candidate's literal value, or a text in the format "/" or a text in the format "/ ()".

required

Returns:

Type Description
int

The index of chosen candidate.

Raises:

Type Description
ValueError

choice_value is not a valid index or it does not match any candidate's literal value.

Source code in pygx/geno/_categorical.py
def candidate_index(self, choice_value: str | int | float) -> int:
    """Returns the candidate index of a choice value.

    Args:
      choice_value: Choice value can be:
        a (integer, float or string) as a candidate's literal value,
        or a text in the format "<index>/<num_candidates>"
        or a text in the format "<index>/<num_candidates> (<literal>)".

    Returns:
      The index of chosen candidate.

    Raises:
      ValueError: `choice_value` is not a valid index or it does not match
        any candidate's literal value.
    """

    def _index_from_literal(value: str | int | float) -> int:
        index = self._literal_index.get(value)
        if index is None:
            raise ValueError(
                f'There is no candidate in {self!r} with literal value {value!r}.'
            )
        return index

    index = None
    literal = None
    num_candidates = len(self.candidates)
    if isinstance(choice_value, str):
        r = _CHOICE_AND_LITERAL_REGEX.match(choice_value)
        if r:
            index = int(r.group(1))
            num_candidates = int(r.group(2))
            literal = r.group(3)
        else:
            r = _CHOICE_REGEX.match(choice_value)
            if r:
                index = int(r.group(1))
                num_candidates = int(r.group(2))
            else:
                index = _index_from_literal(choice_value)
    elif isinstance(choice_value, (int, float)):
        index = _index_from_literal(choice_value)
    else:
        raise ValueError(
            f"The value for Choice '{self.id}' should be either an integer, "
            f'a float or a string. Encountered: {choice_value!r}.'
        )
    if index < -len(self.candidates) or index >= len(self.candidates):
        raise ValueError(
            f"Candidate index out of range at choice '{self.name or self.id}'. "
            f'Index={index}, Number of candidates={len(self.candidates)}.'
        )
    if num_candidates != len(self.candidates):
        raise ValueError(
            f'Number of candidates ({num_candidates}) for Choice '
            f"'{self.name or self.id}' does not match with "
            f'DNASpec ({len(self.candidates)}).'
        )
    if literal is not None and (
        not self.literal_values
        or literal != str(self.literal_values[index])
    ):
        raise ValueError(
            f'The literal value from the input ({literal!r}) does not match '
            f'with the literal value ({self.literal_values[index]!r}) from the '  # pyright: ignore[reportOptionalSubscript]
            f'chosen candidate (index={index}).'
        )
    return index

validate

validate(dna: DNA) -> None

Validate whether a DNA value conforms to this spec.

Source code in pygx/geno/_categorical.py
def validate(self, dna: DNA) -> None:
    """Validate whether a DNA value conforms to this spec."""
    if self.num_choices == 1:
        # For single choice, choice is decoded from dna.value.
        # children should be decoded by chosen candidate.
        if dna.value is None and dna.children:
            raise ValueError(
                f'Expect an integer for a single choice, but encountered a list '
                f'({dna.to_numbers(flatten=False)}). '
                f'Location: {self.location.path}.'
            )
        if not isinstance(dna.value, int):
            raise ValueError(
                f'Expect an integer for a single choice, but encountered: '
                f'{dna.value!r}, Location: {self.location.path}.'
            )
        if dna.value >= len(self.candidates):
            raise ValueError(
                f'Choice out of range. Value: {dna.value}, '
                f'Candidates: {len(self.candidates)}, '
                f'Location: {self.location.path}.'
            )
        chosen = self.candidates[dna.value]
        if chosen.is_constant and dna.children:
            raise ValueError(
                f'Child DNA ({dna.children!r}) provided while there is no child '
                f'decision point.'
            )
        elif not chosen.is_constant and not dna.children:
            raise ValueError(
                f'No child DNA provided for child space {chosen!r}.'
            )
        chosen.validate(DNA(None, dna.children))
    else:
        # For multiple choices, choice is encoded in DNA.children.
        # dna.value could be None (Choices as the only encoder in root template)
        # or int (parent choice).
        if len(dna.children) != self.num_choices:
            raise ValueError(
                f'Number of DNA child values does not match the number of choices. '
                f'Child values: {dna.children!r}, Choices: {self.num_choices}, '
                f'Location: {self.location.path}.'
            )
        if self.distinct or self.sorted:
            sub_dna_values = [s.value for s in dna]
            if self.distinct and len(set(sub_dna_values)) != len(
                dna.children
            ):
                raise ValueError(
                    f'DNA child values should be distinct. '
                    f'Encountered: {sub_dna_values}, Location: {self.location.path}.'
                )
            if self.sorted and sorted(sub_dna_values) != sub_dna_values:
                raise ValueError(
                    f'DNA child values should be sorted. '
                    f'Encountered: {sub_dna_values}, Location: {self.location.path}.'
                )
        for i, sub_dna in enumerate(dna):
            sub_location = topology.KeyPath(i, self.location)
            if not isinstance(sub_dna.value, int):
                raise ValueError(
                    f'Choice value should be int. Encountered: {sub_dna.value}, '
                    f'Location: {sub_location.path}.'
                )
            if sub_dna.value >= len(self.candidates):
                raise ValueError(
                    f'Choice out of range. Value: {sub_dna.value}, '
                    f'Candidates: {len(self.candidates)}, '
                    f'Location: {sub_location.path}.'
                )
            self.candidates[sub_dna.value].validate(
                DNA(None, sub_dna.children)
            )

format

format(
    compact: bool = True,
    verbose: bool = True,
    root_indent: int = 0,
    show_id: bool = True,
    **kwargs
)

Format this object.

Source code in pygx/geno/_categorical.py
def format(
    self,
    compact: bool = True,
    verbose: bool = True,
    root_indent: int = 0,
    show_id: bool = True,
    **kwargs,
):
    """Format this object."""
    if not compact:
        return super().format(compact, verbose, root_indent, **kwargs)

    def _indent(text, indent):
        return ' ' * 2 * indent + text

    s = [f'Choices(num_choices={self.num_choices}, [\n']
    if not self.literal_values:
        for i, candidate in enumerate(self.candidates):
            candidate_str = candidate.format(
                compact, verbose, root_indent + 1, **kwargs
            )
            s.append(_indent(f'({i}): {candidate_str}\n', root_indent + 1))
    else:
        assert len(self.literal_values) == len(self.candidates)
        for i, (candidate, literal_value) in enumerate(
            zip(self.candidates, self.literal_values)
        ):
            if not candidate.is_constant:
                value_str = candidate.format(
                    compact, verbose, root_indent + 1, **kwargs
                )
            else:
                value_str = literal_value
            s.append(_indent(f'({i}): {value_str}\n', root_indent + 1))
    s.append(_indent(']', root_indent))
    kvlist: list[tuple[str, Any, Any]]
    if show_id:
        kvlist = [('id', str(self.id), "''")]
    else:
        kvlist = []
    additionl_properties = formatting.kvlist_str(
        kvlist
        + [
            ('name', self.name, None),
            ('distinct', self.distinct, True),
            ('sorted', self.sorted, False),
            ('hints', self.hints, None),
            ('subchoice_index', self.subchoice_index, None),
        ],
        compact=False,
        root_indent=root_indent,
    )
    if additionl_properties:
        s.append(', ')
        s.append(additionl_properties)
    s.append(')')
    return ''.join(s)

CustomDecisionPoint

CustomDecisionPoint(
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any
)

Bases: DecisionPoint

Represents a user-defined decision point.

Example::

decision_point = pg.geno.custom()

See also: pygx.geno.CustomDecisionPoint.

Source code in pygx/symbolic/_object.py
@_filter_typing_traceback_on_raise
def __init__(
    self,
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: topology.KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any,
):
    """Create an Object instance.

    ``pg.Object`` synthesizes a keyword-only ``__init__``. Subclasses
    that need positional arguments must override ``__init__``
    explicitly and translate to keyword arguments before forwarding
    to ``super().__init__(**kwargs)``.

    Args:
      allow_partial: If True, the object can be partial.
      sealed: If True, seal the object from future modification (unless under
        a `pg.as_sealed(False)` context manager). If False, treat the object as
        unsealed. If None, it's determined by `cls.__sym_options__.frozen`.
      root_path: The symbolic path for current object. By default it's None,
        which indicates that newly constructed object does not have a parent.
      explicit_init: Should set to `True` when `__init__` is called via
        `pg.Object.__init__` instead of `super().__init__`.
      **kwargs: key/value arguments that align with the schema. All required
        keys in the schema must be specified, and values should be acceptable
        according to their value spec.

    Raises:
      KeyError: When required key(s) are missing.
      ValueError: When value(s) are not acceptable by their value spec.
    """
    # Placeholder for Google-internal usage instrumentation.

    cls = self.__class__
    # The ever-instantiated latch `apply_schema`'s post-instance guard
    # reads (#432: `update_schema` is only legal before instances exist).
    # Own-`__dict__`, latched on the construction ATTEMPT — mirroring the
    # native core, whose `__native_ctor_meta__` (the equivalent marker) is
    # created on the first instance op. One dict read per construct.
    meta = cls.__sym_meta__
    if not meta.instantiated:
        meta.instantiated = True
    if sealed is None:
        sealed = cls.__sym_options__.frozen

    if not isinstance(allow_partial, bool):
        raise TypeError(
            f"Expect bool type for argument 'allow_partial' in "
            f'symbolic.Object.__init__ but encountered {allow_partial}.'
        )

    # Fast path: const-keyed schema.
    if cls._sym_init_fields is not None and self._init_fast_path(
        kwargs,
        allow_partial,
        sealed,
        root_path,
        explicit_init,
    ):
        return

    # Slow path: handles varargs and schemas with non-const keys.

    # Model before-hook (#516) — the fast path was not taken
    # (`_sym_init_fields is None`), so the hook has not run yet.
    if cls._sym_preinit_overridden:
        kwargs = _apply_preinit(cls, kwargs)

    # We delay the seal attempt until members are all set.
    super().__init__(
        allow_partial=allow_partial,
        accessor_writable=cls.__sym_options__.attr_write,
        sealed=sealed,
        root_path=root_path,
        init_super=not explicit_init,
    )

    # Fill field_args and init_args from **kwargs.
    _, unmatched_keys = cls.__schema__.resolve(list(kwargs.keys()))
    # Fields opted out of `__init__` via `enable_init=False` resolve
    # against the schema but must still be rejected as kwargs.
    get_field = cls.__schema__.get_field
    unmatched_keys = list(unmatched_keys) + [
        k
        for k in kwargs
        if (f := get_field(k)) is not None and not f.enable_init
    ]
    if unmatched_keys:
        _screen_unexpected_init_keys(cls, kwargs, unmatched_keys)

    # Declared aliases are RESERVED wire spellings (#517, owner ruling):
    # an open (varkw) schema must not accept an extra kwarg equal to a
    # field's alias — the wire would become ambiguous. (Const-keyed
    # schemas never reach here with an alias-spelled kwarg; their
    # unknown-key screen already raised — or, under `varkw='ignore'`,
    # raised for alias keys before dropping the rest.)
    if cls._sym_alias_maps is not None:
        alias_to_name = cls._sym_alias_maps[0]
        for k in kwargs:
            if k in alias_to_name:
                raise TypeError(
                    f'{cls.__name__}.__init__() got keyword argument '
                    f'{k!r}, which is reserved as the alias of field '
                    f'{alias_to_name[k]!r}.'
                )

    # All field values come from kwargs — `pg.Object.__init__` is
    # keyword-only.
    field_args = dict(kwargs)

    # Check missing arguments when partial binding is disallowed.
    if not base.accepts_partial(self):
        missing_args = []
        for field in self.__class__.__schema__.fields.values():
            if (
                not field.value.has_default
                and isinstance(field.key, pg_typing.ConstStrKey)
                and field.key not in field_args
            ):
                missing_args.append(str(field.key))
        if missing_args:
            arg_phrase = formatting.auto_plural(
                len(missing_args), 'argument'
            )
            keys_str = formatting.comma_delimited_str(missing_args)
            raise TypeError(
                f'{self.__class__.__name__}.__init__() missing {len(missing_args)} '
                f'required {arg_phrase}: {keys_str}.'
            )

    # NOTE(daiyip): Accessor writable is honored by `Object.__setattr__` so
    # we can always make `_sym_attributes` accessor writable. This prevents
    # a child object's attribute access from being changed when it's
    # attached to a parent whose symbolic attributes are not writable.
    # Per-field validation is gated by `field.enable_validation` inside
    # `Schema.apply` / `Dict._formalized_value` — so a class-wide
    # `validate=False` (resolved into each field) and per-field overrides
    # both flow through naturally here.
    vars(self)['_sym_attributes'] = pg_dict.Dict(
        field_args,  # pyright: ignore[reportArgumentType]
        value_spec=self.__class__.sym_fields,
        allow_partial=allow_partial,
        sealed=sealed,
        accessor_writable=True,
        root_path=root_path,
        as_object_attributes_container=True,
        under_sym_mode=cls.__sym_options__.sym,
    )
    self._sym_attributes.sym_setparent(self)
    # Seed `init=False` fields from their defaults before `on_sym_post_init`
    # runs so `on_sym_ready`/`on_sym_bound` overrides start from a known baseline.
    _seed_non_symbolic_fields(self, cls._non_symbolic_fields)
    # Model after-validator (#516): fires before any lifecycle hook can
    # observe the object, gated like `on_sym_ready` (concrete only).
    if cls._sym_validate_overridden:
        abstract = (
            self._allow_partial and self.sym_partial
        ) or self.sym_puresymbolic
        if not abstract:
            self.on_sym_validate()
    self.on_sym_post_init()
    self.sym_seal(sealed)

is_categorical property

is_categorical: bool

Returns True if current node is a categorical choice.

is_subchoice property

is_subchoice: bool

Returns True if current node is a subchoice of a multi-choice.

is_numerical property

is_numerical: bool

Returns True if current node is numerical decision.

is_custom_decision_point property

is_custom_decision_point: bool

Returns True if current node is a custom decision point.

decision_points property

decision_points: list[DecisionPoint]

Returns all decision points in their declaration order.

space_size property

space_size: int

Returns the size of the search space. Use -1 for infinity.

validate

validate(dna: DNA) -> None

Validate whether a DNA value conforms to this spec.

Source code in pygx/geno/_custom.py
def validate(self, dna: DNA) -> None:
    """Validate whether a DNA value conforms to this spec."""
    if not isinstance(dna.value, str):
        raise ValueError(
            f'CustomDecisionPoint expects string type DNA. '
            f'Encountered: {dna!r}, Location: {self.location.path}.'
        )

sym_jsonify

sym_jsonify(**kwargs: Any) -> WireValue

Overrides sym_jsonify to exclude non-serializable fields.

Source code in pygx/geno/_custom.py
def sym_jsonify(self, **kwargs: Any) -> wire.WireValue:
    """Overrides sym_jsonify to exclude non-serializable fields."""
    exclude_keys = kwargs.pop('exclude_keys', [])
    exclude_keys.extend(['random_dna_fn', 'next_dna_fn'])
    return super().sym_jsonify(exclude_keys=exclude_keys, **kwargs)

format

format(
    compact: bool = True,
    verbose: bool = True,
    root_indent: int = 0,
    show_id: bool = True,
    **kwargs
)

Format this object.

Source code in pygx/geno/_custom.py
def format(
    self,
    compact: bool = True,
    verbose: bool = True,
    root_indent: int = 0,
    show_id: bool = True,
    **kwargs,
):
    """Format this object."""
    if not compact:
        return super().format(compact, verbose, root_indent, **kwargs)
    kvlist: list[tuple[str, Any, Any]]
    if show_id:
        kvlist = [('id', str(self.id), "''")]
    else:
        kvlist = []
    details = formatting.kvlist_str(
        kvlist
        + [
            ('hyper_type', self.hyper_type, None),
            ('name', self.name, None),
            ('hints', self.hints, None),
        ]
    )
    return f'{self.__class__.__name__}({details})'

Deduping

Deduping(generator: DNAGenerator, **kwargs)

Bases: DNAGenerator

Deduping generator.

A deduping generator can be applied on another generator to dedup its proposed DNAs.

Hash function

By default, the hash function is the symbolic hash of the DNA, which returns the same hash when the decisions from the DNA are the same.

For example::

pg.geno.Deduping(pg.geno.Random())

will only generate unique DNAs. When hash_fn is specified, it allows the user to compute the hash for a DNA.

For example::

pg.geno.Deduping(pg.geno.Random(), hash_fn=lambda dna: sum(dna.to_numbers()))

will dedup based on the sum of all decision values.

Number of duplicates

An optional max_duplicates can be provided by the user to allow a few duplicates.

For example::

pg.geno.Deduping(pg.geno.Random(), max_duplicates=5)

Note: for inner DNAGenerators that requires user feedback, duplication accounting is based on DNAs that are fed back to the DNAGenerator, not proposed ones.

Automatic reward computation

Automatic reward computation will be enabled when auto_reward_fn is provided AND when the inner generator takes feedback. It allows users to compute the reward for new duplicates (which exceed the max_duplicates limit) by aggregating rewards from previous duplicates. Such DNAs will be fed back to the DNAGenerator without client's evaluation (supported by pg.sample through the 'reward' metadata).

For example::

pg.geno.Deduping(pg.algo.evolution.regularized_evolution(), auto_reward_fn=lambda rs: sum(rs) / len(rs))

Source code in pygx/geno/_deduping.py
def __init__(self, generator: DNAGenerator, **kwargs):
    super().__init__(**dict(generator=generator), **kwargs)

DNAGenerator

DNAGenerator(
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any
)

Bases: Object

Base class for DNA generator.

A DNA generator is an object that produces pygx.DNA, and optionally takes feedback from the caller to improve its future proposals.

To implement a DNA generator, the user must implement the _propose method, and can optionally override the _setup, _feedback and _replay methods.

  • Making proposals (Required): This method defines what to return as the next DNA from the generator, users MUST override the _propose method to implement this logic. _propose can raise StopIteration when no more DNA can be produced.

  • Custom setup (Optional): Usually a DNAGenerator subclass has its internal state, which can be initialized when the search space definition is attached to the DNAGenerator. To do so, the user can override the _setup method, in which we can access the search space definition (DNASpec object) via self.dna_spec.

  • Taking feedback (Optional): A DNAGenerator may take feedbacks from the caller on the fitness of proposed DNA to improve future proposals. The fitness is measured by a reward (a float number as the measure of a single objective, or a tuple of float numbers as the measure for multiple objectives). The user should override the _feedback method to implement such logics. If the reward is for multiple objectives. The user should override the multi_objective property to return True.

  • State recovery (Optional): DNAGenerator was designed with distributed computing in mind, in which a process can be preempted or killed unexpectedly. Therefore, a DNAGenerator should be able to recover its state from historical proposals and rewards. The recover method was introduced for such purpose, whose default implementation is to replay the history through the _feedback method. If the user has a custom replay logic other than _feedback, they should override the _replay method. In some use cases, the user may want to implement their own checkpointing logic. In such cases, the user can override the recover method as a no-op. As aside note, the recover method will be called by the tuning backend (see tuning.py) after setup but before propose.

See also:

Source code in pygx/symbolic/_object.py
@_filter_typing_traceback_on_raise
def __init__(
    self,
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: topology.KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any,
):
    """Create an Object instance.

    ``pg.Object`` synthesizes a keyword-only ``__init__``. Subclasses
    that need positional arguments must override ``__init__``
    explicitly and translate to keyword arguments before forwarding
    to ``super().__init__(**kwargs)``.

    Args:
      allow_partial: If True, the object can be partial.
      sealed: If True, seal the object from future modification (unless under
        a `pg.as_sealed(False)` context manager). If False, treat the object as
        unsealed. If None, it's determined by `cls.__sym_options__.frozen`.
      root_path: The symbolic path for current object. By default it's None,
        which indicates that newly constructed object does not have a parent.
      explicit_init: Should set to `True` when `__init__` is called via
        `pg.Object.__init__` instead of `super().__init__`.
      **kwargs: key/value arguments that align with the schema. All required
        keys in the schema must be specified, and values should be acceptable
        according to their value spec.

    Raises:
      KeyError: When required key(s) are missing.
      ValueError: When value(s) are not acceptable by their value spec.
    """
    # Placeholder for Google-internal usage instrumentation.

    cls = self.__class__
    # The ever-instantiated latch `apply_schema`'s post-instance guard
    # reads (#432: `update_schema` is only legal before instances exist).
    # Own-`__dict__`, latched on the construction ATTEMPT — mirroring the
    # native core, whose `__native_ctor_meta__` (the equivalent marker) is
    # created on the first instance op. One dict read per construct.
    meta = cls.__sym_meta__
    if not meta.instantiated:
        meta.instantiated = True
    if sealed is None:
        sealed = cls.__sym_options__.frozen

    if not isinstance(allow_partial, bool):
        raise TypeError(
            f"Expect bool type for argument 'allow_partial' in "
            f'symbolic.Object.__init__ but encountered {allow_partial}.'
        )

    # Fast path: const-keyed schema.
    if cls._sym_init_fields is not None and self._init_fast_path(
        kwargs,
        allow_partial,
        sealed,
        root_path,
        explicit_init,
    ):
        return

    # Slow path: handles varargs and schemas with non-const keys.

    # Model before-hook (#516) — the fast path was not taken
    # (`_sym_init_fields is None`), so the hook has not run yet.
    if cls._sym_preinit_overridden:
        kwargs = _apply_preinit(cls, kwargs)

    # We delay the seal attempt until members are all set.
    super().__init__(
        allow_partial=allow_partial,
        accessor_writable=cls.__sym_options__.attr_write,
        sealed=sealed,
        root_path=root_path,
        init_super=not explicit_init,
    )

    # Fill field_args and init_args from **kwargs.
    _, unmatched_keys = cls.__schema__.resolve(list(kwargs.keys()))
    # Fields opted out of `__init__` via `enable_init=False` resolve
    # against the schema but must still be rejected as kwargs.
    get_field = cls.__schema__.get_field
    unmatched_keys = list(unmatched_keys) + [
        k
        for k in kwargs
        if (f := get_field(k)) is not None and not f.enable_init
    ]
    if unmatched_keys:
        _screen_unexpected_init_keys(cls, kwargs, unmatched_keys)

    # Declared aliases are RESERVED wire spellings (#517, owner ruling):
    # an open (varkw) schema must not accept an extra kwarg equal to a
    # field's alias — the wire would become ambiguous. (Const-keyed
    # schemas never reach here with an alias-spelled kwarg; their
    # unknown-key screen already raised — or, under `varkw='ignore'`,
    # raised for alias keys before dropping the rest.)
    if cls._sym_alias_maps is not None:
        alias_to_name = cls._sym_alias_maps[0]
        for k in kwargs:
            if k in alias_to_name:
                raise TypeError(
                    f'{cls.__name__}.__init__() got keyword argument '
                    f'{k!r}, which is reserved as the alias of field '
                    f'{alias_to_name[k]!r}.'
                )

    # All field values come from kwargs — `pg.Object.__init__` is
    # keyword-only.
    field_args = dict(kwargs)

    # Check missing arguments when partial binding is disallowed.
    if not base.accepts_partial(self):
        missing_args = []
        for field in self.__class__.__schema__.fields.values():
            if (
                not field.value.has_default
                and isinstance(field.key, pg_typing.ConstStrKey)
                and field.key not in field_args
            ):
                missing_args.append(str(field.key))
        if missing_args:
            arg_phrase = formatting.auto_plural(
                len(missing_args), 'argument'
            )
            keys_str = formatting.comma_delimited_str(missing_args)
            raise TypeError(
                f'{self.__class__.__name__}.__init__() missing {len(missing_args)} '
                f'required {arg_phrase}: {keys_str}.'
            )

    # NOTE(daiyip): Accessor writable is honored by `Object.__setattr__` so
    # we can always make `_sym_attributes` accessor writable. This prevents
    # a child object's attribute access from being changed when it's
    # attached to a parent whose symbolic attributes are not writable.
    # Per-field validation is gated by `field.enable_validation` inside
    # `Schema.apply` / `Dict._formalized_value` — so a class-wide
    # `validate=False` (resolved into each field) and per-field overrides
    # both flow through naturally here.
    vars(self)['_sym_attributes'] = pg_dict.Dict(
        field_args,  # pyright: ignore[reportArgumentType]
        value_spec=self.__class__.sym_fields,
        allow_partial=allow_partial,
        sealed=sealed,
        accessor_writable=True,
        root_path=root_path,
        as_object_attributes_container=True,
        under_sym_mode=cls.__sym_options__.sym,
    )
    self._sym_attributes.sym_setparent(self)
    # Seed `init=False` fields from their defaults before `on_sym_post_init`
    # runs so `on_sym_ready`/`on_sym_bound` overrides start from a known baseline.
    _seed_non_symbolic_fields(self, cls._non_symbolic_fields)
    # Model after-validator (#516): fires before any lifecycle hook can
    # observe the object, gated like `on_sym_ready` (concrete only).
    if cls._sym_validate_overridden:
        abstract = (
            self._allow_partial and self.sym_partial
        ) or self.sym_puresymbolic
        if not abstract:
            self.on_sym_validate()
    self.on_sym_post_init()
    self.sym_seal(sealed)

multi_objective property

multi_objective: bool

If True, current DNA generator supports multi-objective optimization.

needs_feedback property

needs_feedback: bool

Returns True if the DNAGenerator needs feedback.

num_proposals property

num_proposals

Get number of proposals that are already produced.

num_feedbacks property

num_feedbacks

Get number of proposals whose feedback are provided.

setup

setup(dna_spec: DNASpec) -> None

Setup DNA spec.

Source code in pygx/geno/_dna_generator.py
def setup(self, dna_spec: DNASpec) -> None:
    """Setup DNA spec."""
    self._dna_spec = dna_spec
    self._num_proposals = 0
    self._num_feedbacks = 0
    self._setup()

propose

propose() -> DNA

Propose a DNA to evaluate.

Source code in pygx/geno/_dna_generator.py
def propose(self) -> DNA:
    """Propose a DNA to evaluate."""
    dna = self._propose()
    self._num_proposals += 1
    return dna

feedback

feedback(dna: DNA, reward: float | tuple[float]) -> None

Feedback a completed trial to the algorithm.

Parameters:

Name Type Description Default
dna DNA

a DNA object.

required
reward float | tuple[float]

reward for the DNA. It is a float if self.multi_objective returns False, otherwise it's a tuple of floats.

required
Source code in pygx/geno/_dna_generator.py
def feedback(self, dna: DNA, reward: float | tuple[float]) -> None:
    """Feedback a completed trial to the algorithm.

    Args:
      dna: a DNA object.
      reward: reward for the DNA. It is a float if `self.multi_objective`
        returns False, otherwise it's a tuple of floats.
    """
    if self.needs_feedback:
        if self.multi_objective and isinstance(reward, float):
            reward = (reward,)
        elif not self.multi_objective and isinstance(reward, tuple):
            if len(reward) != 1:
                raise ValueError(
                    f'{self!r} is single objective, but the reward {reward!r} '
                    f'contains multiple objectives.'
                )
            reward = reward[0]
        self._feedback(dna, reward)
    self._num_feedbacks += 1

recover

recover(history: Iterable[tuple[DNA, None | float | tuple[float]]]) -> None

Recover states by replaying the proposal history.

NOTE: recover will always be called before first propose and could be called multiple times if there are multiple source of history, e.g: trials from a previous study and existing trials from current study.

Parameters:

Name Type Description Default
history Iterable[tuple[DNA, None | float | tuple[float]]]

An iterable object that consists of historically proposed DNA with its reward. The reward will be None if it is not yet provided (via feedback).

required
Source code in pygx/geno/_dna_generator.py
def recover(
    self, history: Iterable[tuple[DNA, None | float | tuple[float]]]
) -> None:
    """Recover states by replaying the proposal history.

    NOTE: `recover` will always be called before first `propose` and could be
    called multiple times if there are multiple source of history, e.g: trials
    from a previous study and existing trials from current study.

    Args:
      history: An iterable object that consists of historically proposed DNA
        with its reward. The reward will be None if it is not yet provided
        (via feedback).
    """
    for i, (dna, reward) in enumerate(history):
        self._replay(i, dna, reward)
        self._num_proposals += 1
        if reward is not None:
            self._num_feedbacks += 1

Float

Float(min_value: float, max_value: float, **kwargs)

Bases: DecisionPoint

Represents the genotype for a float-value genome.

Example::

# Create a float decision point within range [0.1, 1.0]. decision_point = pg.geno.floatv(0.1, 1.0)

See also: pygx.geno.floatv.

Source code in pygx/geno/_numerical.py
def __init__(self, min_value: float, max_value: float, **kwargs):
    super().__init__(
        **dict(min_value=min_value, max_value=max_value),
        **kwargs,
    )

is_categorical property

is_categorical: bool

Returns True if current node is a categorical choice.

is_subchoice property

is_subchoice: bool

Returns True if current node is a subchoice of a multi-choice.

is_numerical property

is_numerical: bool

Returns True if current node is numerical decision.

is_custom_decision_point property

is_custom_decision_point: bool

Returns True if current node is a custom decision point.

decision_points property

decision_points: list[DecisionPoint]

Returns all decision points in their declaration order.

space_size property

space_size: int

Returns the size of the search space. Use -1 for infinity.

on_sym_bound

on_sym_bound()

Custom logics to validate value.

Source code in pygx/geno/_numerical.py
def on_sym_bound(self):
    """Custom logics to validate value."""
    super().on_sym_bound()
    if self.min_value > self.max_value:
        raise ValueError(
            f"Argument 'min_value' ({self.min_value}) should be no greater "
            f"than 'max_value' ({self.max_value})."
        )
    if self.scale in ['log', 'rlog'] and self.min_value <= 0:
        raise ValueError(
            f"'min_value' must be positive when `scale` is {self.scale!r}. "
            f'encountered: {self.min_value}.'
        )

validate

validate(dna: DNA) -> None

Validate whether a DNA value conforms to this spec.

Source code in pygx/geno/_numerical.py
def validate(self, dna: DNA) -> None:
    """Validate whether a DNA value conforms to this spec."""
    if not isinstance(dna.value, float):
        raise ValueError(
            f'Expect float value. Encountered: {dna.value}, '
            f'Location: {self.location.path}.'
        )
    if dna.value < self.min_value:
        raise ValueError(
            f'DNA value should be no less than {self.min_value}. '
            f'Encountered {dna.value}, Location: {self.location.path}.'
        )
    if dna.value > self.max_value:
        raise ValueError(
            f'DNA value should be no greater than {self.max_value}. '
            f'Encountered {dna.value}, Location: {self.location.path}.'
        )
    if dna.children:
        raise ValueError(
            f'Float DNA should have no children. '
            f'Encountered: {dna.children!r}, Location: {self.location.path}.'
        )

format

format(
    compact: bool = True,
    verbose: bool = True,
    root_indent: int = 0,
    show_id: bool = True,
    **kwargs
)

Format this object.

Source code in pygx/geno/_numerical.py
def format(
    self,
    compact: bool = True,
    verbose: bool = True,
    root_indent: int = 0,
    show_id: bool = True,
    **kwargs,
):
    """Format this object."""
    if not compact:
        return super().format(compact, verbose, root_indent, **kwargs)
    kvlist: list[tuple[str, Any, Any]]
    if show_id:
        kvlist = [('id', str(self.id), "''")]
    else:
        kvlist = []
    details = formatting.kvlist_str(
        kvlist
        + [
            ('name', self.name, None),
            ('min_value', self.min_value, None),
            ('max_value', self.max_value, None),
            ('scale', self.scale, None),
            ('hints', self.hints, None),
        ]
    )
    return f'{self.__class__.__name__}({details})'

Random

Random(seed: int | None = None, **kwargs)

Bases: DNAGenerator

Random DNA generator.

Source code in pygx/geno/_random.py
def __init__(self, seed: int | None = None, **kwargs):
    super().__init__(**dict(seed=seed), **kwargs)

Space

Space(elements: list[DecisionPoint] | None = None, **kwargs)

Bases: DNASpec

Represents a search space that consists of a list of decision points.

Example::

# Create a constant space. space = pg.geno.Space([])

# Create a space with one categorical decision point # and a float decision point space = pg.geno.space([ pg.geno.oneof([ pg.geno.constant(), pg.geno.constant(), ]), pg.geno.floatv(0.0, 1.0) ])

See also: pygx.geno.constant.

Source code in pygx/geno/_space.py
def __init__(
    self,
    elements: list[DecisionPoint] | None = None,
    **kwargs,
):
    # `elements=None` means "use schema default" (an empty list). The
    # schema's deep-copy of the default avoids the mutable-default
    # sharing problem.
    if elements is not None:
        kwargs['elements'] = elements
    super().__init__(**kwargs)

is_space property

is_space: bool

Returns True if current node is a sub-space.

is_categorical property

is_categorical: bool

Returns True if current node is a categorical choice.

is_subchoice property

is_subchoice: bool

Returns True if current node is a subchoice of a multi-choice.

is_numerical property

is_numerical: bool

Returns True if current node is numerical decision.

is_custom_decision_point property

is_custom_decision_point: bool

Returns True if current node is a custom decision point.

is_constant property

is_constant: bool

Returns whether this template is constant.

A constant Space does not have any genetic encoders.

decision_points property

decision_points: list[DecisionPoint]

Returns all decision points in their declaration order.

space_size property

space_size: int

Returns the size of the search space. Use -1 for infinity.

validate

validate(dna: DNA) -> None

Validate whether a DNA value conforms to this spec.

Source code in pygx/geno/_space.py
def validate(self, dna: DNA) -> None:  # pytype: disable=signature-mismatch
    """Validate whether a DNA value conforms to this spec."""
    if not self.elements and (dna.value is not None or dna.children):
        raise ValueError(
            f'Extra DNA values encountered: {dna!r}, '
            f'Location: {self.location.path}.'
        )

    if len(self.elements) == 1:
        self.elements[0].validate(dna)
    else:
        if len(dna.children) != len(self.elements):
            raise ValueError(
                f'Number of child values in DNA ({len(dna.children)}) does not '
                f'match the number of elements ({len(self.elements)}). Child '
                f'values: {dna.children!r}, Location: {self.location.path}.'
            )
        for i, elem in enumerate(self.elements):
            # `DNA.__getitem__` is union-typed (`DNA | list[...] | None`),
            # but for a multi-element Space the i-th child is always a
            # `DNA` once the length-check above passes.
            elem.validate(dna[i])  # pyright: ignore[reportArgumentType]

format

format(
    compact: bool = True, verbose: bool = True, root_indent: int = 0, **kwargs
)

Format this object.

Source code in pygx/geno/_space.py
def format(
    self,
    compact: bool = True,
    verbose: bool = True,
    root_indent: int = 0,
    **kwargs,
):
    """Format this object."""
    if not compact:
        return super().format(compact, verbose, root_indent, **kwargs)

    if not self.elements:
        return 'Space()'

    def _indent(text, indent):
        return ' ' * 2 * indent + text

    s = ['Space({\n']
    for i, elem in enumerate(self.elements):
        elem_str = elem.format(
            compact, verbose, root_indent + 1, show_id=False, **kwargs
        )
        s.append(
            _indent(
                f"{i} = '{elem.location.path}': {elem_str}\n",
                root_indent + 1,
            )
        )
    s.append(_indent('}', root_indent))
    s.append(')')
    return ''.join(s)

Sweeping

Sweeping(
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any
)

Bases: DNAGenerator

Sweeping (Grid Search) DNA generator.

Source code in pygx/symbolic/_object.py
@_filter_typing_traceback_on_raise
def __init__(
    self,
    *,
    allow_partial: bool = False,
    sealed: bool | None = None,
    root_path: topology.KeyPath | None = None,
    explicit_init: bool = False,
    **kwargs: Any,
):
    """Create an Object instance.

    ``pg.Object`` synthesizes a keyword-only ``__init__``. Subclasses
    that need positional arguments must override ``__init__``
    explicitly and translate to keyword arguments before forwarding
    to ``super().__init__(**kwargs)``.

    Args:
      allow_partial: If True, the object can be partial.
      sealed: If True, seal the object from future modification (unless under
        a `pg.as_sealed(False)` context manager). If False, treat the object as
        unsealed. If None, it's determined by `cls.__sym_options__.frozen`.
      root_path: The symbolic path for current object. By default it's None,
        which indicates that newly constructed object does not have a parent.
      explicit_init: Should set to `True` when `__init__` is called via
        `pg.Object.__init__` instead of `super().__init__`.
      **kwargs: key/value arguments that align with the schema. All required
        keys in the schema must be specified, and values should be acceptable
        according to their value spec.

    Raises:
      KeyError: When required key(s) are missing.
      ValueError: When value(s) are not acceptable by their value spec.
    """
    # Placeholder for Google-internal usage instrumentation.

    cls = self.__class__
    # The ever-instantiated latch `apply_schema`'s post-instance guard
    # reads (#432: `update_schema` is only legal before instances exist).
    # Own-`__dict__`, latched on the construction ATTEMPT — mirroring the
    # native core, whose `__native_ctor_meta__` (the equivalent marker) is
    # created on the first instance op. One dict read per construct.
    meta = cls.__sym_meta__
    if not meta.instantiated:
        meta.instantiated = True
    if sealed is None:
        sealed = cls.__sym_options__.frozen

    if not isinstance(allow_partial, bool):
        raise TypeError(
            f"Expect bool type for argument 'allow_partial' in "
            f'symbolic.Object.__init__ but encountered {allow_partial}.'
        )

    # Fast path: const-keyed schema.
    if cls._sym_init_fields is not None and self._init_fast_path(
        kwargs,
        allow_partial,
        sealed,
        root_path,
        explicit_init,
    ):
        return

    # Slow path: handles varargs and schemas with non-const keys.

    # Model before-hook (#516) — the fast path was not taken
    # (`_sym_init_fields is None`), so the hook has not run yet.
    if cls._sym_preinit_overridden:
        kwargs = _apply_preinit(cls, kwargs)

    # We delay the seal attempt until members are all set.
    super().__init__(
        allow_partial=allow_partial,
        accessor_writable=cls.__sym_options__.attr_write,
        sealed=sealed,
        root_path=root_path,
        init_super=not explicit_init,
    )

    # Fill field_args and init_args from **kwargs.
    _, unmatched_keys = cls.__schema__.resolve(list(kwargs.keys()))
    # Fields opted out of `__init__` via `enable_init=False` resolve
    # against the schema but must still be rejected as kwargs.
    get_field = cls.__schema__.get_field
    unmatched_keys = list(unmatched_keys) + [
        k
        for k in kwargs
        if (f := get_field(k)) is not None and not f.enable_init
    ]
    if unmatched_keys:
        _screen_unexpected_init_keys(cls, kwargs, unmatched_keys)

    # Declared aliases are RESERVED wire spellings (#517, owner ruling):
    # an open (varkw) schema must not accept an extra kwarg equal to a
    # field's alias — the wire would become ambiguous. (Const-keyed
    # schemas never reach here with an alias-spelled kwarg; their
    # unknown-key screen already raised — or, under `varkw='ignore'`,
    # raised for alias keys before dropping the rest.)
    if cls._sym_alias_maps is not None:
        alias_to_name = cls._sym_alias_maps[0]
        for k in kwargs:
            if k in alias_to_name:
                raise TypeError(
                    f'{cls.__name__}.__init__() got keyword argument '
                    f'{k!r}, which is reserved as the alias of field '
                    f'{alias_to_name[k]!r}.'
                )

    # All field values come from kwargs — `pg.Object.__init__` is
    # keyword-only.
    field_args = dict(kwargs)

    # Check missing arguments when partial binding is disallowed.
    if not base.accepts_partial(self):
        missing_args = []
        for field in self.__class__.__schema__.fields.values():
            if (
                not field.value.has_default
                and isinstance(field.key, pg_typing.ConstStrKey)
                and field.key not in field_args
            ):
                missing_args.append(str(field.key))
        if missing_args:
            arg_phrase = formatting.auto_plural(
                len(missing_args), 'argument'
            )
            keys_str = formatting.comma_delimited_str(missing_args)
            raise TypeError(
                f'{self.__class__.__name__}.__init__() missing {len(missing_args)} '
                f'required {arg_phrase}: {keys_str}.'
            )

    # NOTE(daiyip): Accessor writable is honored by `Object.__setattr__` so
    # we can always make `_sym_attributes` accessor writable. This prevents
    # a child object's attribute access from being changed when it's
    # attached to a parent whose symbolic attributes are not writable.
    # Per-field validation is gated by `field.enable_validation` inside
    # `Schema.apply` / `Dict._formalized_value` — so a class-wide
    # `validate=False` (resolved into each field) and per-field overrides
    # both flow through naturally here.
    vars(self)['_sym_attributes'] = pg_dict.Dict(
        field_args,  # pyright: ignore[reportArgumentType]
        value_spec=self.__class__.sym_fields,
        allow_partial=allow_partial,
        sealed=sealed,
        accessor_writable=True,
        root_path=root_path,
        as_object_attributes_container=True,
        under_sym_mode=cls.__sym_options__.sym,
    )
    self._sym_attributes.sym_setparent(self)
    # Seed `init=False` fields from their defaults before `on_sym_post_init`
    # runs so `on_sym_ready`/`on_sym_bound` overrides start from a known baseline.
    _seed_non_symbolic_fields(self, cls._non_symbolic_fields)
    # Model after-validator (#516): fires before any lifecycle hook can
    # observe the object, gated like `on_sym_ready` (concrete only).
    if cls._sym_validate_overridden:
        abstract = (
            self._allow_partial and self.sym_partial
        ) or self.sym_puresymbolic
        if not abstract:
            self.on_sym_validate()
    self.on_sym_post_init()
    self.sym_seal(sealed)

manyof

manyof(
    num_choices: int,
    candidates: list[DNASpec],
    distinct: bool = True,
    sorted: bool = False,
    literal_values: list[str | int | float] | None = None,
    hints: Any = None,
    location: str | KeyPath = KeyPath(),
    name: str | None = None,
) -> Choices

Returns a multi-choice specification.

It creates the genotype for pygx.manyof.

Example::

spec = pg.geno.manyof(2, [ pg.geno.constant(), pg.geno.constant(), pg.geno.oneof([ pg.geno.constant(), pg.geno.constant() ]) ])

Parameters:

Name Type Description Default
num_choices int

Number of choices.

required
candidates list[DNASpec]

A list of pg.geno.Space objects as the candidates.

required
distinct bool

If True, the decisions for the multiple choices should be distinct from each other (based on the value of selected indices).

True
sorted bool

If Ture, the decisions returned should be sorted by the values of selected indices.

False
literal_values list[str | int | float] | None

An optional list of string, integer, or float as the literal values for the candidates for display purpose.

None
hints Any

An optional hint object.

None
location str | KeyPath

A pg.KeyPath object that indicates the location of the decision point.

KeyPath()
name str | None

An optional global unique name for identifying this decision point.

None

Returns:

Type Description
Choices

A pg.geno.Choices object.

See also:

Source code in pygx/geno/_categorical.py
def manyof(
    num_choices: int,
    candidates: list[DNASpec],
    distinct: bool = True,
    sorted: bool = False,  # pylint: disable=redefined-builtin
    literal_values: list[str | int | float] | None = None,
    hints: Any = None,
    location: str | topology.KeyPath = topology.KeyPath(),
    name: str | None = None,
) -> Choices:
    """Returns a multi-choice specification.

    It creates the genotype for [`pygx.manyof`][pygx.hyper.manyof].

    Example::

      spec = pg.geno.manyof(2, [
          pg.geno.constant(),
          pg.geno.constant(),
          pg.geno.oneof([
              pg.geno.constant(),
              pg.geno.constant()
          ])
      ])

    Args:
      num_choices: Number of choices.
      candidates: A list of ``pg.geno.Space`` objects as the candidates.
      distinct: If True, the decisions for the multiple choices should be
        distinct from each other (based on the value of selected indices).
      sorted: If Ture, the decisions returned should be sorted by the
        values of selected indices.
      literal_values: An optional list of string, integer, or float as the
        literal values for the candidates for display purpose.
      hints: An optional hint object.
      location: A ``pg.KeyPath`` object that indicates the location of the
        decision point.
      name: An optional global unique name for identifying this decision
        point.

    Returns:
      A ``pg.geno.Choices`` object.

    See also:

      * [`pygx.geno.constant`][pygx.geno.constant]
      * [`pygx.geno.oneof`][pygx.geno.oneof]
      * [`pygx.geno.floatv`][pygx.geno.floatv]
    """
    normalized_candidates = []
    for c in candidates:
        if isinstance(c, DecisionPoint):
            c = Space(elements=[c])
        normalized_candidates.append(c)
    return Choices(
        num_choices=num_choices,
        candidates=normalized_candidates,
        distinct=distinct,
        sorted=sorted,
        literal_values=literal_values,
        hints=hints,
        location=location,
        name=name,
    )

oneof

oneof(
    candidates: list[DNASpec],
    literal_values: list[str | int | float] | None = None,
    hints: Any = None,
    location: str | KeyPath = KeyPath(),
    name: str | None = None,
) -> Choices

Returns a single choice specification.

It creates the genotype for pygx.oneof.

Example::

spec = pg.geno.oneof([ pg.geno.constant(), pg.geno.oneof([ pg.geno.constant(), pg.geno.constant() ]) ])

Parameters:

Name Type Description Default
candidates list[DNASpec]

A list of pg.geno.Space objects as the candidates.

required
literal_values list[str | int | float] | None

An optional list of string, integer, or float as the literal values for the candidates for display purpose.

None
hints Any

An optional hint object.

None
location str | KeyPath

A pg.KeyPath object that indicates the location of the decision point.

KeyPath()
name str | None

An optional global unique name for identifying this decision point.

None

Returns:

Type Description
Choices

A pg.geno.Choices object.

See also:

Source code in pygx/geno/_categorical.py
def oneof(
    candidates: list[DNASpec],
    literal_values: list[str | int | float] | None = None,
    hints: Any = None,
    location: str | topology.KeyPath = topology.KeyPath(),
    name: str | None = None,
) -> Choices:
    """Returns a single choice specification.

    It creates the genotype for [`pygx.oneof`][pygx.hyper.oneof].

    Example::

      spec = pg.geno.oneof([
          pg.geno.constant(),
          pg.geno.oneof([
              pg.geno.constant(),
              pg.geno.constant()
          ])
      ])

    Args:
      candidates: A list of ``pg.geno.Space`` objects as the candidates.
      literal_values: An optional list of string, integer, or float as the
        literal values for the candidates for display purpose.
      hints: An optional hint object.
      location: A ``pg.KeyPath`` object that indicates the location of the
        decision point.
      name: An optional global unique name for identifying this decision
        point.

    Returns:
      A ``pg.geno.Choices`` object.

    See also:

      * [`pygx.geno.constant`][pygx.geno.constant]
      * [`pygx.geno.manyof`][pygx.geno.manyof]
      * [`pygx.geno.floatv`][pygx.geno.floatv]
    """
    return manyof(
        1,
        candidates,
        literal_values=literal_values,
        hints=hints,
        location=location,
        name=name,
    )

custom

custom(
    hyper_type: str | None = None,
    next_dna_fn: Callable[[DNA | None], DNA | None] | None = None,
    random_dna_fn: Callable[[Any], DNA] | None = None,
    hints: Any = None,
    location: KeyPath = KeyPath(),
    name: str | None = None,
) -> CustomDecisionPoint

Returns a custom decision point.

It creates the genotype for subclasses of pygx.hyper.CustomHyper.

Example::

spec = pg.geno.custom('my_hyper', hints='some hints')

Parameters:

Name Type Description Default
hyper_type str | None

An optional display type for the custom decision point.

None
next_dna_fn Callable[[DNA | None], DNA | None] | None

An optional callable for computing the next DNA for current decision point.

None
random_dna_fn Callable[[Any], DNA] | None

An optional callable for computing a random DNA for current decision point.

None
hints Any

An optional hint object.

None
location KeyPath

A pg.KeyPath object that indicates the location of the decision point.

KeyPath()
name str | None

An optional global unique name for identifying this decision point.

None

Returns:

Type Description
CustomDecisionPoint

A pg.geno.CustomDecisionPoint object.

See also:

Source code in pygx/geno/_custom.py
def custom(
    hyper_type: str | None = None,
    next_dna_fn: Callable[[DNA | None], DNA | None] | None = None,
    random_dna_fn: Callable[[Any], DNA] | None = None,
    hints: Any = None,
    location: topology.KeyPath = topology.KeyPath(),
    name: str | None = None,
) -> CustomDecisionPoint:
    """Returns a custom decision point.

    It creates the genotype for subclasses of [`pygx.hyper.CustomHyper`][pygx.hyper.CustomHyper].

    Example::

      spec = pg.geno.custom('my_hyper', hints='some hints')

    Args:
      hyper_type: An optional display type for the custom decision point.
      next_dna_fn: An optional callable for computing the next DNA for current
        decision point.
      random_dna_fn: An optional callable for computing a random DNA for current
        decision point.
      hints: An optional hint object.
      location: A ``pg.KeyPath`` object that indicates the location of the
        decision point.
      name: An optional global unique name for identifying this decision
        point.

    Returns:
      A ``pg.geno.CustomDecisionPoint`` object.

    See also:

      * [`pygx.geno.constant`][pygx.geno.constant]
      * [`pygx.geno.oneof`][pygx.geno.oneof]
      * [`pygx.geno.manyof`][pygx.geno.manyof]
      * [`pygx.geno.floatv`][pygx.geno.floatv]
    """
    return CustomDecisionPoint(
        hyper_type=hyper_type,
        next_dna_fn=next_dna_fn,
        random_dna_fn=random_dna_fn,
        hints=hints,
        location=location,
        name=name,
    )

dna_generator

dna_generator(func: Callable[[DNASpec], Iterator[DNA]]) -> type[DNAGenerator]

Decorator that converts a generation function to a DNAGenerator class.

Example::

# A DNA generator that reads DNA from file.

def from_file(filepath): @pg.geno.dna_generator def file_based_dna_generator(dna_spec): dna_list = pg.load(filepath) for dna in dna_list: dna.use_spec(dna_spec) yield dna

return file_based_dna_generator

See also: pygx.DNAGenerator

Parameters:

Name Type Description Default
func Callable[[DNASpec], Iterator[DNA]]

the generation function in signature: (DNASpec) -> Iterator[DNA]

required

Returns:

Type Description
type[DNAGenerator]

A DNAGenerator class.

Source code in pygx/geno/_dna_generator.py
def dna_generator(
    func: Callable[[DNASpec], Iterator[DNA]],
) -> type['DNAGenerator']:
    """Decorator that converts a generation function to a DNAGenerator class.

    Example::

      # A DNA generator that reads DNA from file.

      def from_file(filepath):
        @pg.geno.dna_generator
        def file_based_dna_generator(dna_spec):
          dna_list = pg.load(filepath)
          for dna in dna_list:
            dna.use_spec(dna_spec)
            yield dna

        return file_based_dna_generator

    See also: [`pygx.DNAGenerator`][pygx.geno.DNAGenerator]

    Args:
      func: the generation function in signature:
        `(DNASpec) -> Iterator[DNA]`

    Returns:
      A DNAGenerator class.
    """

    class SimpleDNAGenerator(DNAGenerator):
        """Simple DNA generator."""

        def _setup(self):
            self._iterator = func(self.dna_spec)  # pyright: ignore[reportArgumentType]
            self._error = None

        def _propose(self) -> DNA:
            if self._error is not None:
                raise ValueError(
                    f'Error happened earlier: {self._error}'
                ) from self._error
            try:
                return next(self._iterator)
            except Exception as e:
                if not isinstance(e, StopIteration):
                    self._error = e
                raise

    return SimpleDNAGenerator

floatv

floatv(
    min_value: float,
    max_value: float,
    scale: str | None = None,
    hints: Any = None,
    location: KeyPath = KeyPath(),
    name: str | None = None,
) -> Float

Returns a Float specification.

It creates the genotype for pygx.floatv.

Example::

spec = pg.geno.floatv(0.0, 1.0)

Parameters:

Name Type Description Default
min_value float

The lower bound of decision.

required
max_value float

The upper bound of decision.

required
scale str | None

An optional string as the scale of the range. Supported values are None, 'linear', 'log', and 'rlog'. If None, the feasible space is unscaled. If linear, the feasible space is mapped to [0, 1] linearly. If log, the feasible space is mapped to [0, 1] logarithmically with formula x -> log(x / min) / log(max / min). If rlog, the feasible space is mapped to [0, 1] "reverse" logarithmically, resulting in values close to max_value spread out more than the points near the min_value, with formula: x -> 1.0 - log((max + min - x) / min) / log (max / min). min_value must be positive if scale is not None. Also, it depends on the search algorithm to decide whether this information is used or not.

None
hints Any

An optional hint object.

None
location KeyPath

A pg.KeyPath object that indicates the location of the decision point.

KeyPath()
name str | None

An optional global unique name for identifying this decision point.

None

Returns:

Type Description
Float

A pg.geno.Float object.

See also:

Source code in pygx/geno/_numerical.py
def floatv(
    min_value: float,
    max_value: float,
    scale: str | None = None,
    hints: Any = None,
    location: topology.KeyPath = topology.KeyPath(),
    name: str | None = None,
) -> Float:
    """Returns a Float specification.

    It creates the genotype for [`pygx.floatv`][pygx.hyper.floatv].

    Example::

      spec = pg.geno.floatv(0.0, 1.0)

    Args:
      min_value: The lower bound of decision.
      max_value: The upper bound of decision.
      scale: An optional string as the scale of the range. Supported values
        are None, 'linear', 'log', and 'rlog'.
        If None, the feasible space is unscaled.
        If `linear`, the feasible space is mapped to [0, 1] linearly.
        If `log`, the feasible space is mapped to [0, 1] logarithmically with
          formula `x -> log(x / min) / log(max / min)`.
        If `rlog`, the feasible space is mapped to [0, 1] "reverse"
          logarithmically, resulting in values close to `max_value` spread
          out more than the points near the `min_value`, with formula:
          x -> 1.0 - log((max + min - x) / min) / log (max / min).
        `min_value` must be positive if `scale` is not None.
        Also, it depends on the search algorithm to decide whether this
        information is used or not.
      hints: An optional hint object.
      location: A ``pg.KeyPath`` object that indicates the location of the
        decision point.
      name: An optional global unique name for identifying this decision
        point.

    Returns:
      A ``pg.geno.Float`` object.

    See also:

      * [`pygx.geno.constant`][pygx.geno.constant]
      * [`pygx.geno.oneof`][pygx.geno.oneof]
      * [`pygx.geno.manyof`][pygx.geno.manyof]
    """
    return Float(
        min_value=min_value,
        max_value=max_value,
        scale=scale,
        hints=hints,
        location=location,
        name=name,
    )

random_dna

random_dna(
    dna_spec: DNASpec,
    random_generator: None | ModuleType | Random = None,
    attach_spec: bool = True,
    previous_dna: DNA | None = None,
) -> DNA

Generates a random DNA from a DNASpec.

Example::

spec = pg.geno.space([ pg.geno.oneof([ pg.geno.constant(), pg.geno.constant(), pg.geno.constant() ]), pg.geno.floatv(0.1, 0.2) ])

print(pg.random_dna(spec)) # DNA([2, 0.1123])

Parameters:

Name Type Description Default
dna_spec DNASpec

a DNASpec object.

required
random_generator None | ModuleType | Random

a Python random generator.

None
attach_spec bool

If True, attach the DNASpec to generated DNA.

True
previous_dna DNA | None

An optional DNA representing previous DNA. This field might be useful for generating stateful random DNAs.

None

Returns:

Type Description
DNA

A DNA object.

Source code in pygx/geno/_random.py
def random_dna(
    dna_spec: DNASpec,
    random_generator: None | types.ModuleType | random.Random = None,
    attach_spec: bool = True,
    previous_dna: DNA | None = None,
) -> DNA:
    """Generates a random DNA from a DNASpec.

    Example::

      spec = pg.geno.space([
          pg.geno.oneof([
              pg.geno.constant(),
              pg.geno.constant(),
              pg.geno.constant()
          ]),
          pg.geno.floatv(0.1, 0.2)
      ])

      print(pg.random_dna(spec))
      # DNA([2, 0.1123])

    Args:
      dna_spec: a DNASpec object.
      random_generator: a Python random generator.
      attach_spec: If True, attach the DNASpec to generated DNA.
      previous_dna: An optional DNA representing previous DNA. This field might
          be useful for generating stateful random DNAs.

    Returns:
      A DNA object.
    """
    return dna_spec.random_dna(
        random_generator or random, attach_spec, previous_dna
    )

constant

constant() -> Space

Returns an constant candidate of Choices.

Example::

spec = pg.geno.constant()

Returns:

Type Description
Space

a constant pg.geno.Space object.

See also:

Source code in pygx/geno/_space.py
def constant() -> Space:
    """Returns an constant candidate of Choices.

    Example::

      spec = pg.geno.constant()

    Returns:
      a constant ``pg.geno.Space`` object.

    See also:

      * [`pygx.geno.oneof`][pygx.geno.oneof]
      * [`pygx.geno.manyof`][pygx.geno.manyof]
      * [`pygx.geno.floatv`][pygx.geno.floatv]
    """
    return Space()

options: show_root_heading: false show_root_toc_entry: false members_order: source heading_level: 2