Counting

In this recipe, we use a “counting” cogent to illustrate basic usage of a few library features:

  • Elaboration: information, accessible via IOContainer.e.feature_name, that is computed each cycle (during the DP Elaboration phase)

    • create_elaborator: a convenience function for creating an Elaborator based upon kwargs that associate feature_name with a function to compute the associated value

  • Termination Check: a function that (during the DP TerminationCheck phase) determines if the DP should end

  • Arguments: name=value pairs supplied to a DP that are accessible via IOContainer.a.name

  • Logging: using the standard Python logging library

    • Debug output: using the stringify function can improve readability of logs, as well as debug output (e.g., printing a DP)

Detailed Run Summary

Let’s focus on line 142, assuming starting_val is 2

  1. The value 2 is accessible via the counting_start argument (@ io.a.counter_start)

  2. DP reinit (state.counter -> None); DP run!

    1. DP Cycle 1

      1. Elaboration: the union is taken of all the key-value pairs produced across all elaborators (which is just one in this example), yielding {'is_initialized': False, 'is_perfect': False, 'as_str': 'None'}

      2. TerminationCheck: if any check returns True (including selection of a terminal operator, by default), the DP stops; but not yet (since found_perfect finds that io.e.is_perfect is False)

      3. Propose: the operators are mutually exclusive (by design) and so init is proposed as the only action

      4. Rank: only one action, so init is selected

      5. Apply: init changes state.counter -> 2

    2. Cycle 2

      1. Elaboration: io.e -> {'is_initialized': True, 'is_perfect': False, 'as_str': '2'}

      2. TerminationCheck: False (still going!)

      3. Propose: only increment is proposed

      4. Rank: only one action, so increment is selected

      5. Apply: increment changes state.counter -> 3

    3. Cycle 3

      1. Elaboration: io.e -> {'is_initialized': True, 'is_perfect': False, 'as_str': '3'}

      2. TerminationCheck: False (still going!)

      3. Propose: only increment is proposed

      4. Rank: only one action, so increment is selected

      5. Apply: increment changes state.counter -> 4

    4. Cycle 4

      1. Elaboration: io.e -> {'is_initialized': True, 'is_perfect': True, 'as_str': '4'}

      2. TerminationCheck: True (done!)

  3. DP run complete; Cogent gate check (done!)

  4. Arguments (counting_start) removed

Code

  1"""
  2Illustrates a simple counting agent that
  3uses a termination check based upon the
  4result of an elaborator
  5"""
  6
  7import logging
  8from dataclasses import dataclass
  9from math import sqrt
 10from typing import cast
 11
 12from cognition import (
 13    Cogent,
 14    DecisionProcess,
 15    IOContainer,
 16    Operator,
 17    create_elaborator,
 18    stringify,
 19)
 20
 21# ===
 22
 23# Example of enabling logging to the console (stdout),
 24# capturing only those entries that rise to
 25# the level of a warning (rare)
 26
 27handler = logging.StreamHandler()  # options exist for files, http, etc.
 28handler.setFormatter(
 29    logging.Formatter(
 30        "%(levelname)s\t%(name)s\t%(asctime)s\t%(message)s"
 31    )  # more fields exist
 32)
 33
 34logger = logging.getLogger("cognition")  # can be set to a sub-package for focus
 35logger.setLevel(logging.WARNING)  # set to DEBUG for details
 36logger.addHandler(handler)
 37
 38# ===
 39
 40
 41@dataclass
 42class CountingState:
 43    """
 44    keeps track of where we are in the
 45    counting process (init, or current value)
 46    """
 47
 48    counter: int | None = None
 49    """
 50    current value: None signals to init,
 51    otherwise current count
 52    """
 53
 54
 55@stringify("is_perfect")  # easier-to-read in logs/debug output
 56def is_perfect(cs: CountingState, _io: IOContainer) -> bool:
 57    """example elaboration: is the counter value a perfect number?"""
 58
 59    return (cs.counter is not None) and (sqrt(cs.counter) % 1 == 0)
 60
 61
 62# ===
 63
 64# note that the dp state "initializer" in this
 65# case is just the class constructor
 66counting_cogent = Cogent(DecisionProcess(CountingState))
 67
 68# these could very well be methods of the state class,
 69# but for illustration, this will make available three
 70# keys on io.e.kwarg that automatically update each cycle
 71# during the elaboration phase
 72counting_cogent.dp.add_elaborator(
 73    create_elaborator(
 74        "examples",  # only visible in logs/debug output
 75        is_initialized=lambda cs, _: cs.counter is not None,
 76        is_perfect=is_perfect,
 77        as_str=lambda cs, _: str(cs.counter),  # unused (showing any result type)
 78    )
 79)
 80
 81
 82@counting_cogent.dp.termination_check
 83@stringify("found_perfect")
 84def found_perfect(cs: CountingState, io: IOContainer) -> bool:
 85    """
 86    utilizes elaboration to detect custom termination logic:
 87    in this case that a perfect number has been achieved
 88    """
 89
 90    print(f"Terminate?: {cs.counter=} ({io.e.is_perfect=})")
 91
 92    return cast(bool, io.e.is_perfect)
 93
 94
 95# ===
 96
 97
 98@counting_cogent.dp.operator("init")
 99class InitOperator(Operator[CountingState]):
100    """
101    sets the initial counter value
102    based upon a supplied argument
103    """
104
105    def can_perform(self, _state: CountingState, io: IOContainer) -> bool:
106        return not cast(bool, io.e.is_initialized)
107
108    def perform(self, state: CountingState, io: IOContainer) -> None:
109        start_val = cast(int, io.a.counter_start)
110
111        print(f"Initialize: {start_val}")
112
113        state.counter = start_val
114
115
116@counting_cogent.dp.operator("increment")
117class IncrementOperator(Operator[CountingState]):
118    """
119    Increments the counter
120    """
121
122    def can_perform(self, _state: CountingState, io: IOContainer) -> bool:
123        return cast(bool, io.e.is_initialized)
124
125    def perform(self, state: CountingState, _io: IOContainer) -> None:
126        assert state.counter is not None  # for type checking
127
128        new_val = state.counter + 1
129        print(f"Increment: {state.counter} -> {new_val}")
130        state.counter = new_val
131
132
133# ===
134
135examples = [1, 2, 5, 11, 101]
136
137for starting_val in examples:
138    print(f"== {starting_val=} ==")
139
140    # when running the agent, kwarg passed to the
141    # agent appear as io.a.kwarg
142    output = cast(int, counting_cogent(counter_start=starting_val).dp.state.counter)
143
144    # print(counting_cogent.dp) # uncomment to see lots of debug details about the DP
145
146    print()
147    print(f"The first perfect square ≥ {starting_val} is {output} ({sqrt(output)=}).")
148    print()