GrammaticalEvolutionTools 1.1.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. getools/__init__.py +6 -0
  2. getools/agents/__init__.py +4 -0
  3. getools/agents/agent.py +212 -0
  4. getools/agents/agent_program.py +105 -0
  5. getools/evolution/__init__.py +10 -0
  6. getools/evolution/cross_over.py +110 -0
  7. getools/evolution/mutation.py +21 -0
  8. getools/grammars/__init__.py +5 -0
  9. getools/grammars/grammar.py +140 -0
  10. getools/grammars/grammar_node.py +150 -0
  11. getools/meta/__init__.py +5 -0
  12. getools/meta/base_node.py +1261 -0
  13. getools/meta/meta.py +273 -0
  14. getools/programs/__init__.py +3 -0
  15. getools/programs/base/__init__.py +4 -0
  16. getools/programs/base/program_node.py +644 -0
  17. getools/programs/base/program_tree.py +627 -0
  18. getools/programs/mods/__init__.py +0 -0
  19. getools/programs/mods/grammar/__init__.py +4 -0
  20. getools/programs/mods/grammar/grammar_program_addin.py +53 -0
  21. getools/programs/mods/grammar/node_converter.py +147 -0
  22. getools/programs/nodes/__init__.py +7 -0
  23. getools/programs/nodes/basic_nodes/__init__.py +6 -0
  24. getools/programs/nodes/basic_nodes/executable_node.py +14 -0
  25. getools/programs/nodes/basic_nodes/non_terminal_node.py +66 -0
  26. getools/programs/nodes/basic_nodes/root_node.py +37 -0
  27. getools/programs/nodes/basic_nodes/terminal_node.py +14 -0
  28. getools/programs/nodes/factor_nodes/__init__.py +6 -0
  29. getools/programs/nodes/factor_nodes/factor_node.py +34 -0
  30. getools/programs/nodes/factor_nodes/integer_node.py +20 -0
  31. getools/programs/nodes/factor_nodes/number_node.py +23 -0
  32. getools/programs/nodes/factor_nodes/rand_int_node.py +12 -0
  33. getools/programs/nodes/logic_nodes/__init__.py +5 -0
  34. getools/programs/nodes/logic_nodes/condition_node.py +101 -0
  35. getools/programs/nodes/logic_nodes/repeat_node.py +46 -0
  36. getools/programs/nodes/logic_nodes/sequential_node.py +52 -0
  37. getools/worlds/__init__.py +3 -0
  38. getools/worlds/base/__init__.py +8 -0
  39. getools/worlds/base/animation.py +28 -0
  40. getools/worlds/base/layout.py +68 -0
  41. getools/worlds/base/objects/__init__.py +4 -0
  42. getools/worlds/base/objects/mixins/__init__.py +3 -0
  43. getools/worlds/base/objects/mixins/reward.py +93 -0
  44. getools/worlds/base/objects/world_object.py +71 -0
  45. getools/worlds/base/position.py +92 -0
  46. getools/worlds/base/world.py +142 -0
  47. getools/worlds/grid_world/__init__.py +10 -0
  48. getools/worlds/grid_world/grid_layout.py +268 -0
  49. getools/worlds/grid_world/grid_position.py +8 -0
  50. getools/worlds/grid_world/grid_world.py +335 -0
  51. getools/worlds/grid_world/grid_world_agent.py +186 -0
  52. getools/worlds/grid_world/grid_world_animation.py +182 -0
  53. getools/worlds/grid_world/grid_world_object.py +49 -0
  54. getools/worlds/grid_world/grid_world_reward.py +24 -0
  55. getools/worlds/hex_world/__init__.py +0 -0
  56. getools/worlds/open_world/__init__.py +0 -0
  57. grammaticalevolutiontools-1.1.0a1.dist-info/METADATA +192 -0
  58. grammaticalevolutiontools-1.1.0a1.dist-info/RECORD +60 -0
  59. grammaticalevolutiontools-1.1.0a1.dist-info/WHEEL +4 -0
  60. grammaticalevolutiontools-1.1.0a1.dist-info/licenses/LICENSE +21 -0
getools/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ from .meta import BaseNode, BaseNodeMeta, InheritingNodeMeta, WorldAnimation
2
+ from .worlds.base import World
3
+ from .agents import Agent, AgentProgramTree
4
+
5
+
6
+ __all__ = []
@@ -0,0 +1,4 @@
1
+ from .agent import Agent
2
+ from .agent_program import AgentProgramTree
3
+
4
+ __all__ = ['Agent', 'AgentProgramTree']
@@ -0,0 +1,212 @@
1
+ from .agent_program import AgentProgramTree
2
+ from ..grammars import Grammar
3
+
4
+ from numbers import Number
5
+ from uuid import uuid4
6
+
7
+ from typing import Type, TYPE_CHECKING
8
+ import warnings
9
+
10
+ if TYPE_CHECKING:
11
+ from ..worlds import World
12
+
13
+ class Agent:
14
+
15
+ @classmethod
16
+ def default_grammar(cls) -> Grammar:
17
+ return cls._default_grammar
18
+
19
+ @classmethod
20
+ def default_program_cls(cls) -> Type[AgentProgramTree]:
21
+ mangled = f"_{cls.__name__}__default_program_cls"
22
+
23
+ if not hasattr(cls, mangled):
24
+ default_grammar = cls.default_grammar()
25
+ if default_grammar:
26
+ class AgentProgram(AgentProgramTree):
27
+ _grammar = default_grammar
28
+ pass
29
+ setattr(cls, mangled, AgentProgram)
30
+
31
+ return getattr(cls, mangled, None)
32
+
33
+ _default_grammar = None
34
+ _requires_world = False
35
+
36
+ # - - Exceptions - -
37
+
38
+ class WorldNotSetError(RuntimeError):
39
+ pass
40
+
41
+ class AlreadyInWorldError(RuntimeError):
42
+ pass
43
+
44
+ class AssignedProgramError(RuntimeError):
45
+ pass
46
+
47
+ class NotAssignedProgramError(RuntimeError):
48
+ pass
49
+
50
+ # - - Initialization - -
51
+
52
+ def __init__(self, program: AgentProgramTree = None, autogen=True):
53
+ self._world: World = None
54
+ self._program: AgentProgramTree = None
55
+ self._uuid = uuid4() # just to make hashable
56
+
57
+ self._score = 0
58
+ self._num_actions = 0
59
+
60
+ if program:
61
+ self._assert_valid_program(program)
62
+ self._set_program(program)
63
+ elif autogen:
64
+ program_cls = self.default_program_cls()
65
+ if not program_cls:
66
+ warnings.warn(
67
+ "`autogen` set to True, but no default " \
68
+ "grammar found for " \
69
+ f"{type(self).__name__}. Could not create a program.",
70
+ UserWarning
71
+ )
72
+ else:
73
+ self._set_program(program_cls())
74
+
75
+ # - - Assertions - -
76
+
77
+ def _assert_not_in_world(self):
78
+ if self.assigned_to_world():
79
+ raise Agent.AlreadyInWorldError(
80
+ "Cannot add agent to world because agent is already in another world. "
81
+ "Remove agent from that world first. "
82
+ )
83
+
84
+ def _assert_not_assigned_program(self):
85
+ if self._program:
86
+ raise Agent.AssignedProgramError(
87
+ "Cannot set program when agent is already assigned a program. "
88
+ "Please remove old program first."
89
+ )
90
+
91
+ def _assert_valid_program(self, program):
92
+ if not isinstance(program, AgentProgramTree):
93
+ raise TypeError('program must be an instance of AgentProgramTree.')
94
+ if program.agent and program.agent is not self:
95
+ raise ValueError(
96
+ "Cannot bind program to this agent when program is already "
97
+ "bound to another agent. Unbind the program first."
98
+ )
99
+
100
+ # - - World Internal Access - -
101
+
102
+ def _set_world(self, world: 'World'):
103
+ self._assert_not_in_world()
104
+
105
+ self._world = world
106
+
107
+ def _clear_world(self):
108
+ self._world = None
109
+
110
+ # - - Helpers - -
111
+
112
+ def _set_program(self, program: AgentProgramTree):
113
+ self._program = program
114
+ if program:
115
+ program._set_agent(self)
116
+
117
+ def _remove_program(self) -> AgentProgramTree:
118
+ program = self._program
119
+ program._set_agent(None)
120
+ self._program = None
121
+
122
+ return program
123
+
124
+ def _replace_program(self, new_program: AgentProgramTree) -> AgentProgramTree:
125
+ old = self._remove_program()
126
+ try:
127
+ self._set_program(new_program)
128
+ except (TypeError, ValueError) as e:
129
+ self._set_program(old)
130
+ raise e
131
+
132
+ return old
133
+
134
+ # - - Public - -
135
+
136
+ def reset(self, program: AgentProgramTree = None):
137
+ if program:
138
+ self._assert_valid_program(program)
139
+ self._replace_program(program)
140
+
141
+ if self._program and self._program.running():
142
+ self._program.kill()
143
+
144
+ self._num_actions = 0
145
+ self._score = 0
146
+
147
+ def copy_program(self):
148
+ return self._program.copy()
149
+
150
+ # - - Running Program - -
151
+
152
+ def execute_program(self, n=1):
153
+ """
154
+ Resets the program and runs to completion once.
155
+ """
156
+ if self._program.running():
157
+ self._program.kill()
158
+
159
+ self._program.run(n=n)
160
+
161
+ def tick(self, loop: bool=True):
162
+ """
163
+ Runs the program through the next executable node. Exits if run and no nodes are left.
164
+ If loop is True (default), then the program will restart rather than exiting.
165
+ """
166
+ if self._requires_world and not self.assigned_to_world():
167
+ raise Agent.WorldNotSetError("Cannot run program without agent being assigned a world unless this is a worldless agent.")
168
+
169
+ status = self._program.tick()
170
+ if status == AgentProgramTree.Status.EXITED and loop:
171
+ self._program.tick()
172
+
173
+ def give_reward(self, amount):
174
+ self._score += amount
175
+
176
+ # - - Listeners - -
177
+
178
+ def _on_action_taken(self):
179
+ self._record_action()
180
+
181
+ # - - Operations - -
182
+
183
+ def _record_action(self):
184
+ self._num_actions += 1
185
+
186
+ # - - Getters - -
187
+
188
+ def assigned_to_world(self):
189
+ return self._world is not None
190
+
191
+ @property
192
+ def program(self) -> AgentProgramTree:
193
+ return self._program
194
+
195
+ @property
196
+ def score(self) -> Number:
197
+ return self._score
198
+
199
+ @property
200
+ def num_actions(self) -> int:
201
+ return self._num_actions
202
+
203
+ # - - Other Methods - -
204
+
205
+ def __hash__(self):
206
+ return hash(self._uuid)
207
+
208
+ def __lt__(self, other):
209
+ if not isinstance(other, Agent):
210
+ raise TypeError("Cannot compare Agent to non-Agent")
211
+
212
+ return self.score < other.score
@@ -0,0 +1,105 @@
1
+ from ..programs.base.program_tree import ProgramTree
2
+ from ..programs.mods.grammar import GrammarProgramAddin
3
+
4
+ from typing import TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from ..agents import Agent
8
+
9
+
10
+ class AgentProgramTree(ProgramTree, GrammarProgramAddin):
11
+
12
+ @classmethod
13
+ def _assert_agent_valid(cls, agent):
14
+ grammar = cls.get_grammar()
15
+ if not isinstance(agent, grammar.target_agent_type):
16
+ raise TypeError(
17
+ "Cannot bind tree to an agent with which " \
18
+ "it is not compatible"
19
+ )
20
+
21
+ # - - Exceptions - -
22
+
23
+ class MissingAgentError(RuntimeError):
24
+ """Exception raised when attempting to run a program not attached to an an agent."""
25
+ pass
26
+
27
+ class BoundToAgentError(RuntimeError):
28
+ """Exception raised for attempted operations requiring the program to be unbound from an agent when it is not."""
29
+ pass
30
+
31
+ # - - Local Assertions - -
32
+
33
+ def _assert_editable(self):
34
+ if self.bound_to_agent():
35
+ raise AgentProgramTree.BoundToAgentError(
36
+ "Cannot modify a program while it is bound to an agent. "
37
+ "Please remove the program from the agent first or create "
38
+ "a copy to modify. "
39
+ )
40
+ ProgramTree._assert_editable(self)
41
+
42
+ def _assert_runnable(self):
43
+ if not self.bound_to_agent():
44
+ raise AgentProgramTree.MissingAgentError(
45
+ "Cannot run program when it is not bound to an agent."
46
+ )
47
+ ProgramTree._assert_runnable(self)
48
+
49
+ # - - Initialization - -
50
+
51
+ def __init__(self, root=None, autofill=True):
52
+ self._agent = None
53
+ ProgramTree.__init__(self, root, autofill=autofill)
54
+
55
+ def _verify_and_set_root(self, root):
56
+ GrammarProgramAddin._verify_and_set_root(self, root)
57
+
58
+ # - - Agent Access - -
59
+
60
+ def _set_agent(self, agent):
61
+ """Sets the agent instance to which this program is bound.
62
+
63
+ This method should typically only be called by an instance of
64
+ :py:class:`~.agents.Agent` itself to establish the binding.
65
+
66
+ Parameters
67
+ ----------
68
+ agent : Agent
69
+ The agent instance to bind the program to.
70
+ """
71
+ self._assert_agent_valid(agent)
72
+ self._agent = agent
73
+
74
+ # - - Public Methods
75
+
76
+ def bound_to_agent(self) -> bool:
77
+ """Checks if the program is currently bound to an agent.
78
+
79
+ Returns
80
+ -------
81
+ bool
82
+ :py:obj:`True` if an :py:class:`~.agents.Agent` instance is attached to the program
83
+ (:py:attr:`~.ProgramTree._agent` is not :py:obj:`None`), :py:obj:`False` otherwise.
84
+ """
85
+ return self._agent is not None
86
+
87
+ def is_editable(self):
88
+ return ProgramTree.is_editable(self) and \
89
+ not self.bound_to_agent()
90
+
91
+ def is_runnable(self):
92
+ return ProgramTree.is_runnable() and \
93
+ self.bound_to_agent()
94
+
95
+ @property
96
+ def agent(self) -> 'Agent':
97
+ """The agent instance to which this program is currently attached.
98
+
99
+ Returns
100
+ -------
101
+ Agent or None
102
+ The attached :py:class:`~.agents.Agent` instance, or :py:obj:`None`
103
+ if no agent is currently bound.
104
+ """
105
+ return self._agent
@@ -0,0 +1,10 @@
1
+ from .cross_over import pick_compatible_nodes_same_type_only, \
2
+ pick_compatible_nodes_any_valid_replacement, \
3
+ cross_over_programs
4
+ from .mutation import mutate_terminals, replace_random_branch
5
+
6
+
7
+ __all__ = ['pick_compatible_nodes_same_type_only',
8
+ 'pick_compatible_nodes_any_valid_replacement',
9
+ 'cross_over_programs', 'mutate_terminals',
10
+ 'replace_random_branch']
@@ -0,0 +1,110 @@
1
+ from ..programs.nodes.basic_nodes import TerminalNode
2
+ from ..programs import ProgramTree, ProgramNode
3
+ import random
4
+
5
+ from typing import Literal, Type, Tuple
6
+
7
+
8
+ CrossOverOption = Literal['same', 'any']
9
+
10
+ def pick_compatible_nodes_same_type_only(
11
+ program1: ProgramTree, program2: ProgramTree,
12
+ exclude: list[Type[ProgramNode]] = None,
13
+ exclude_children_of_roots: bool = False
14
+ ) -> Tuple[ProgramNode, ProgramNode]:
15
+
16
+ incompatible_types = tuple(exclude or [])
17
+
18
+ # Get all eligible node types from program1 (excluding root and incompatible types)
19
+ eligible_types = set()
20
+ possible_node1s = []
21
+
22
+ for node_type in program1.types_iter():
23
+ if not issubclass(node_type, incompatible_types) \
24
+ and node_type in program2.node_types:
25
+ eligible_types.add(node_type)
26
+ possible_node1s.extend(program1.get_nodes_by_type(node_type))
27
+
28
+ # if no matching types, return None, None
29
+ if not eligible_types:
30
+ return None, None
31
+
32
+ # attempt to pick a matching type
33
+ nodes_picked = False
34
+
35
+ while not nodes_picked:
36
+ possible_node1s = [node for node in possible_node1s \
37
+ if type(node) in eligible_types \
38
+ and node is not program1._root]
39
+
40
+ if not possible_node1s:
41
+ return None, None
42
+
43
+ # choose a random node from program1
44
+ node1: ProgramNode = random.choice(possible_node1s)
45
+ node1_child_of_root = node1._parent is program1.root
46
+
47
+ # get a possible list of nodes from program2
48
+ possible_node2s = [node for node in program2.node_iter(type=type(node1)) \
49
+ if type(node) == type(node1) \
50
+ and node is not program2._root \
51
+ and (not exclude_children_of_roots \
52
+ or not node1_child_of_root \
53
+ or node._parent is not program2.root) \
54
+ ]
55
+
56
+ # if no matches, remove the type and try again
57
+ if not possible_node2s:
58
+ eligible_types.remove(type(node1))
59
+ continue
60
+
61
+ # once a compatible type is found, choose node2
62
+ node2 = random.choice(possible_node2s)
63
+
64
+ nodes_picked = True
65
+
66
+ return node1, node2
67
+
68
+
69
+ def pick_compatible_nodes_any_valid_replacement(program1: ProgramTree, program2: ProgramTree) -> Tuple[ProgramNode, ProgramNode]:
70
+ """Not Implemented"""
71
+ # TODO: Implement this function
72
+ picked = False
73
+ incompatible_types = set([TerminalNode])
74
+ possible_node1s = []
75
+ while not picked:
76
+ possible_node1s = [node for node in (possible_node1s or program1.node_iter()) \
77
+ if not isinstance(node, tuple(incompatible_types))]
78
+ node1: ProgramNode = random.choice(possible_node1s)
79
+
80
+ return NotImplemented
81
+
82
+
83
+ def cross_over_programs(program1: ProgramTree, program2: ProgramTree,
84
+ cross_over_option: CrossOverOption = 'same') -> list[ProgramTree]:
85
+
86
+ # copy the nodes as to not modify the originals
87
+ program1 = program1.copy()
88
+ program2 = program2.copy()
89
+
90
+ # select a pair of compatible nodes.
91
+ # excludes terminals and prevents children of root nodes
92
+ # from being swapped with each other (root nodes excluded automatically)
93
+ if cross_over_option == 'same':
94
+ node1, node2 = pick_compatible_nodes_same_type_only(
95
+ program1, program2, exclude=[TerminalNode],
96
+ exclude_children_of_roots=True
97
+ )
98
+ elif cross_over_option == 'any':
99
+ raise NotImplementedError("The 'any' option is currently under development.")
100
+ # pick_compatible_nodes_any_valid_replacement(program1, program2)
101
+
102
+ if not node1 or not node2: # if no match was found
103
+ return []
104
+
105
+ # swap the nodes in each tree
106
+ program1.replace_node(node1, node2.copy())
107
+ program2.replace_node(node2, node1)
108
+
109
+ # return a list of the newly created nodes
110
+ return [program1, program2]
@@ -0,0 +1,21 @@
1
+ from ..programs import ProgramTree
2
+ import random
3
+
4
+ def mutate_terminals(program:ProgramTree, num_mutations, terminal_types:list[type]):
5
+ possible_nodes = [node for node in program.nodes if type(node) \
6
+ in terminal_types]
7
+
8
+ num_mutations = min(num_mutations, len(possible_nodes))
9
+ for k in range(num_mutations):
10
+ node = random.choice(possible_nodes)
11
+ if node in program.nodes:
12
+ program.replace_node(node)
13
+ possible_nodes.remove(node)
14
+ else:
15
+ print('error')
16
+
17
+ def replace_random_branch(program: ProgramTree, possible_node_types:list[type]):
18
+ possible_nodes = [n for n in program.nodes if type(n) in possible_node_types]
19
+ node = random.choice(possible_nodes)
20
+ program.replace_node(node) # randomly replaces node by default
21
+
@@ -0,0 +1,5 @@
1
+ from .grammar import Grammar
2
+ from .grammar_node import GrammarNode, OutOfContextError
3
+ from ..programs.mods.grammar.node_converter import as_grammar_node
4
+
5
+ __all__ = ['Grammar', 'GrammarNode', 'OutOfContextError', 'as_grammar_node']
@@ -0,0 +1,140 @@
1
+ from .grammar_node import GrammarNode
2
+
3
+ from typing import Type, TYPE_CHECKING
4
+ import warnings
5
+
6
+ if TYPE_CHECKING:
7
+ from ..agents import Agent
8
+
9
+ class Grammar:
10
+
11
+ class MultipleRootsException(ValueError):
12
+ def __init__(self):
13
+ super(Grammar.MultipleRootsException, self).__init__("Multiple Root Nodes Defined within Grammar.")
14
+
15
+ class NoRootDefinedException(ValueError):
16
+ def __init__(self):
17
+ super(Grammar.NoRootDefinedException, self).__init__("No Root Nodes Defined within Grammar.")
18
+
19
+ current_grammar = None
20
+
21
+ def __init__(self, target_agent_type: 'Type[Agent]' = None,
22
+ warnings=True):
23
+ self._roots: list[Type[GrammarNode]] = []
24
+ self._all_node_classes: dict[str, Type[GrammarNode]] = {}
25
+ self._concrete_types: set[Type[GrammarNode]] = set()
26
+ self._abstract_types: set[Type[GrammarNode]] = set()
27
+
28
+ self._target_agent_type = target_agent_type
29
+ self._warnings = warnings
30
+ self._is_valid = False
31
+
32
+ def __enter__(self):
33
+ Grammar.current_grammar = self
34
+ return self
35
+
36
+ def __exit__(self, exc_type, exc_value, traceback):
37
+ Grammar.current_grammar = None
38
+
39
+ if exc_type is None:
40
+ self._validate()
41
+ self._is_valid = True
42
+
43
+ return self._is_valid
44
+
45
+ def _validate(self):
46
+ if len(self._roots) == 0:
47
+ raise Grammar.NoRootDefinedException()
48
+ elif len(self._roots) > 1:
49
+ raise Grammar.MultipleRootsException()
50
+
51
+ for cls in self._abstract_types.union(self._concrete_types):
52
+ self._all_node_classes[cls.__name__] = cls
53
+
54
+ # Make sure all possible children are also in the Grammar
55
+ seen_nodes = set()
56
+ for node_cls in self._all_node_classes.values():
57
+ try:
58
+ node_cls._resolve_possible_children()
59
+
60
+ except KeyError as e:
61
+ raise LookupError(f"Error resolving possible children for {node_cls.__name__}. " + \
62
+ "Child not defined within the scope of the Grammar.") from e
63
+
64
+ for chld_cls in node_cls._ALL_POSSIBLE_CHILDREN:
65
+
66
+ error = None
67
+ if chld_cls is self.root:
68
+ error = "Root node class"
69
+ if chld_cls in self._abstract_types:
70
+ error = "Abstract node class"
71
+ if error is not None:
72
+ raise ValueError(f"{error} cannot be listed as a possible child of another class. Class at fault: {node_cls.__name__} referencing {chld_cls.__name__}")
73
+
74
+ seen_nodes.update(node_cls._ALL_POSSIBLE_CHILDREN)
75
+
76
+ if self._warnings:
77
+ # Raise a warning if abstract classes exist that weren't used to make concrete classes
78
+ for abstract_class in self._abstract_types:
79
+ implemented = False
80
+ for cls in self._concrete_types:
81
+ if issubclass(cls, abstract_class):
82
+ implemented = True
83
+ break
84
+ if not implemented:
85
+ warnings.warn(f"Abstract class {abstract_class.__name__} was defined but no concrete classes were ever created from it. " +
86
+ "Please note that all concrete classes must have no parameters other than self in their __init__ function, " + \
87
+ "otherwise they will still be considered abstract classes.",
88
+ UserWarning)
89
+
90
+ # Raise a warning if a concrete class (besides the root) is not a possible child of any other class
91
+ for node_cls in self._concrete_types:
92
+ if node_cls is self.root:
93
+ continue
94
+
95
+ if node_cls not in seen_nodes:
96
+ warnings.warn(f"{node_cls.__name__} is not listed as a possible child type for any other node in its grammar.",
97
+ UserWarning)
98
+
99
+
100
+ # --------------------
101
+
102
+ def register(self, cls: Type[GrammarNode]):
103
+ """Register a node class inside the grammar."""
104
+
105
+ cls._GRAMMAR = self
106
+
107
+ if cls.is_abstract_class():
108
+ self._abstract_types.add(cls)
109
+ else:
110
+ if cls._IS_ROOT:
111
+ self._roots.append(cls)
112
+
113
+ self._concrete_types.add(cls)
114
+
115
+ def get_class(self, cls_name: str) -> Type[GrammarNode]:
116
+ return self._all_node_classes[cls_name]
117
+
118
+ @property
119
+ def is_valid(self) -> bool:
120
+ return self._is_valid
121
+
122
+ @property
123
+ def root(self) -> Type[GrammarNode]:
124
+ return self._roots[0]
125
+
126
+ @property
127
+ def warnings_enabled(self) -> bool:
128
+ return self._warnings
129
+
130
+ @property
131
+ def abstract_classes(self) -> set[Type[GrammarNode]]:
132
+ return self._abstract_types.copy()
133
+
134
+ @property
135
+ def valid_node_classes(self) -> set[Type[GrammarNode]]:
136
+ return self._concrete_types.copy()
137
+
138
+ @property
139
+ def target_agent_type(self) -> 'Type[Agent]':
140
+ return self._target_agent_type