rl-language 0.1.20__py3-none-win_amd64.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.
- command_line/__init__.py +15 -0
- command_line/utils.py +81 -0
- loader/__init__.py +10 -0
- loader/simulation.py +291 -0
- ml/__init__.py +12 -0
- ml/action_oracle.py +61 -0
- ml/raylib/__init__.py +0 -0
- ml/raylib/action_mask.py +201 -0
- ml/raylib/environment.py +286 -0
- ml/raylib/module_config.py +188 -0
- ml/training_data.py +121 -0
- ml/transformer.py +267 -0
- ml/utils.py +16 -0
- rl_language-0.1.20.data/data/Scripts/rlc-lsp.exe +0 -0
- rl_language-0.1.20.data/data/Scripts/rlc.exe +0 -0
- rl_language-0.1.20.data/data/lib/rlc/stdlib/action.rl +349 -0
- rl_language-0.1.20.data/data/lib/rlc/stdlib/algorithms/equal.rl +34 -0
- rl_language-0.1.20.data/data/lib/rlc/stdlib/bounded_arg.rl +157 -0
- rl_language-0.1.20.data/data/lib/rlc/stdlib/collections/graph.rl +179 -0
- rl_language-0.1.20.data/data/lib/rlc/stdlib/collections/vector.rl +166 -0
- rl_language-0.1.20.data/data/lib/rlc/stdlib/machine_learning.rl +61 -0
- rl_language-0.1.20.data/data/lib/rlc/stdlib/math/numeric.rl +23 -0
- rl_language-0.1.20.data/data/lib/rlc/stdlib/none.rl +12 -0
- rl_language-0.1.20.data/data/lib/rlc/stdlib/serialization/print.rl +34 -0
- rl_language-0.1.20.data/data/lib/rlc/stdlib/serialization/to_byte_vector.rl +168 -0
- rl_language-0.1.20.data/data/lib/rlc/stdlib/string.rl +437 -0
- rl_language-0.1.20.data/data/lib/runtime.lib +0 -0
- rl_language-0.1.20.data/data/lib/site-packages/impl/action.py +112 -0
- rl_language-0.1.20.data/data/lib/site-packages/impl/fix_ray.py +26 -0
- rl_language-0.1.20.data/data/lib/site-packages/impl/learn.py +206 -0
- rl_language-0.1.20.data/data/lib/site-packages/impl/play.py +64 -0
- rl_language-0.1.20.data/data/lib/site-packages/impl/probs.py +75 -0
- rl_language-0.1.20.data/data/lib/site-packages/impl/solve.py +47 -0
- rl_language-0.1.20.data/data/lib/site-packages/impl/test.py +53 -0
- rl_language-0.1.20.dist-info/METADATA +33 -0
- rl_language-0.1.20.dist-info/RECORD +41 -0
- rl_language-0.1.20.dist-info/WHEEL +5 -0
- rl_language-0.1.20.dist-info/entry_points.txt +8 -0
- rl_language-0.1.20.dist-info/top_level.txt +4 -0
- test/__init__.py +9 -0
- test/example_test.py +11 -0
command_line/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file is part of the RLC project.
|
|
3
|
+
#
|
|
4
|
+
# RLC is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation.
|
|
5
|
+
#
|
|
6
|
+
# RLC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
7
|
+
#
|
|
8
|
+
# You should have received a copy of the GNU General Public License along with RLC. If not, see <https://www.gnu.org/licenses/>.
|
|
9
|
+
#
|
|
10
|
+
from .utils import (
|
|
11
|
+
make_rlc_argparse,
|
|
12
|
+
load_simulation_from_args,
|
|
13
|
+
load_dataset,
|
|
14
|
+
load_network,
|
|
15
|
+
)
|
command_line/utils.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file is part of the RLC project.
|
|
3
|
+
#
|
|
4
|
+
# RLC is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation.
|
|
5
|
+
#
|
|
6
|
+
# RLC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
7
|
+
#
|
|
8
|
+
# You should have received a copy of the GNU General Public License along with RLC. If not, see <https://www.gnu.org/licenses/>.
|
|
9
|
+
#
|
|
10
|
+
from shutil import which
|
|
11
|
+
import argparse
|
|
12
|
+
from loader import Simulation, compile
|
|
13
|
+
import pickle
|
|
14
|
+
from ml import RLCTransformer
|
|
15
|
+
import torch
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_network(path, ntokens, device):
|
|
19
|
+
if path == "":
|
|
20
|
+
return RLCTransformer(ntokens=ntokens, device=device)
|
|
21
|
+
return torch.load(path)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def load_dataset(path):
|
|
25
|
+
to_return = []
|
|
26
|
+
with open(path, "rb") as f:
|
|
27
|
+
while True:
|
|
28
|
+
try:
|
|
29
|
+
loaded = pickle.load(f)
|
|
30
|
+
for state, actions_and_scores in loaded:
|
|
31
|
+
to_return.append((state, actions_and_scores))
|
|
32
|
+
except Exception as e:
|
|
33
|
+
break
|
|
34
|
+
return to_return
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def make_rlc_argparse(name, description):
|
|
38
|
+
parser = argparse.ArgumentParser(name, description=description)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"source_file",
|
|
41
|
+
type=str,
|
|
42
|
+
help="path to .rl source file, or to a python wrapper obtained with rlc --python",
|
|
43
|
+
)
|
|
44
|
+
parser.add_argument(
|
|
45
|
+
"--include",
|
|
46
|
+
"-i",
|
|
47
|
+
type=str,
|
|
48
|
+
nargs="*",
|
|
49
|
+
help="path to folder where rl files can be found",
|
|
50
|
+
default=[],
|
|
51
|
+
)
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"--rlc",
|
|
54
|
+
"-c",
|
|
55
|
+
type=str,
|
|
56
|
+
nargs="?",
|
|
57
|
+
help="path to rlc compiler",
|
|
58
|
+
default="rlc",
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
"--runtime",
|
|
62
|
+
"-rt",
|
|
63
|
+
type=str,
|
|
64
|
+
default="",
|
|
65
|
+
nargs="?",
|
|
66
|
+
help="path to runtime library",
|
|
67
|
+
)
|
|
68
|
+
return parser
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def load_simulation_from_args(args, optimize=False):
|
|
72
|
+
assert (
|
|
73
|
+
which(args.rlc) is not None
|
|
74
|
+
), "could not find executable {}, use --rlc <path_to_rlc> to configure it".format(
|
|
75
|
+
args.rlc
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
if args.source_file.endswith(".py"):
|
|
79
|
+
return Simulation(args.source_file)
|
|
80
|
+
else:
|
|
81
|
+
return compile(args.source_file, args.rlc, args.include, args.runtime, optimized=optimize)
|
loader/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file is part of the RLC project.
|
|
3
|
+
#
|
|
4
|
+
# RLC is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation.
|
|
5
|
+
#
|
|
6
|
+
# RLC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
7
|
+
#
|
|
8
|
+
# You should have received a copy of the GNU General Public License along with RLC. If not, see <https://www.gnu.org/licenses/>.
|
|
9
|
+
#
|
|
10
|
+
from .simulation import Simulation, Action, State, compile
|
loader/simulation.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2024 Massimo Fioravanti
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
from importlib import import_module, machinery, util
|
|
17
|
+
import os
|
|
18
|
+
import inspect
|
|
19
|
+
from math import log2, ceil, floor
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from collections import defaultdict
|
|
22
|
+
from tempfile import TemporaryDirectory
|
|
23
|
+
from subprocess import run
|
|
24
|
+
from ctypes import Structure, Array
|
|
25
|
+
from sys import stdout, stderr
|
|
26
|
+
|
|
27
|
+
loaded_libs = {}
|
|
28
|
+
|
|
29
|
+
def import_file(name, file_path):
|
|
30
|
+
if name in loaded_libs:
|
|
31
|
+
return loaded_libs[name]
|
|
32
|
+
loader = machinery.SourceFileLoader(name, file_path)
|
|
33
|
+
spec = util.spec_from_loader(name, loader)
|
|
34
|
+
mod = util.module_from_spec(spec)
|
|
35
|
+
loader.exec_module(mod)
|
|
36
|
+
loaded_libs[name] = mod
|
|
37
|
+
return mod
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class Action:
|
|
41
|
+
def __init__(self, action, simulation):
|
|
42
|
+
self.action = action
|
|
43
|
+
self.simulation = simulation
|
|
44
|
+
self.functions = simulation.module.functions
|
|
45
|
+
|
|
46
|
+
def __str__(self) -> str:
|
|
47
|
+
return self.simulation.to_python_string(self.functions.to_string(self.action))
|
|
48
|
+
|
|
49
|
+
def to_bytes(self) -> bytes:
|
|
50
|
+
result = self.functions.as_byte_vector(self.action)
|
|
51
|
+
real_content = []
|
|
52
|
+
for i in range(getattr(result, "__size")):
|
|
53
|
+
real_content.append(getattr(result, "__data")[i] + 128)
|
|
54
|
+
return bytes(real_content)
|
|
55
|
+
|
|
56
|
+
def can_run(self, state) -> bool:
|
|
57
|
+
return self.functions.can_apply(self.action, state.state).value
|
|
58
|
+
|
|
59
|
+
def run(self, state):
|
|
60
|
+
self.functions.apply(self.action, state.state)
|
|
61
|
+
return state
|
|
62
|
+
|
|
63
|
+
def copy(self):
|
|
64
|
+
return Action(self.state.copy(), self.simulation)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ActionType:
|
|
68
|
+
def __init__(self, action_type, action_id, simulation):
|
|
69
|
+
self.simulation = simulation
|
|
70
|
+
self.action_type = action_type
|
|
71
|
+
self.action_id = action_id
|
|
72
|
+
|
|
73
|
+
def get_arg_min_maxs(self):
|
|
74
|
+
return [(0, 3), (0, 3)]
|
|
75
|
+
|
|
76
|
+
def materialize(self, *args):
|
|
77
|
+
empty_action = Action(self.simulation.any_action(), self.simulation)
|
|
78
|
+
current_action = self.action_type()
|
|
79
|
+
for (name, type), value in zip(getattr(current_action, "_fields_"), args):
|
|
80
|
+
setattr(current_action, name, value)
|
|
81
|
+
|
|
82
|
+
self.simulation.module.functions.assign(empty_action.action, current_action)
|
|
83
|
+
return empty_action
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class State:
|
|
87
|
+
def __init__(self, simulation, state):
|
|
88
|
+
self.simulation = simulation
|
|
89
|
+
self.state = state
|
|
90
|
+
|
|
91
|
+
def copy(self):
|
|
92
|
+
return State(self.simulation, self.state.copy())
|
|
93
|
+
|
|
94
|
+
def is_done(self) -> bool:
|
|
95
|
+
return self.state.resume_index == -1
|
|
96
|
+
|
|
97
|
+
def __str__(self) -> str:
|
|
98
|
+
return self.simulation.to_python_string(
|
|
99
|
+
self.simulation.module.functions.to_string(self.state)
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def from_string(self, string: str) -> bool:
|
|
103
|
+
rl_string = self.simulation.to_rl_string(str)
|
|
104
|
+
return self.simulation.module.functions.from_string(self.state, rl_string)
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def actions(self) -> [Action]:
|
|
108
|
+
return self.simulation.get_actions()
|
|
109
|
+
|
|
110
|
+
def as_byte_vector(self):
|
|
111
|
+
result = self.simulation.module.functions.as_byte_vector(self.state)
|
|
112
|
+
real_content = []
|
|
113
|
+
for i in range(getattr(result, "__size")):
|
|
114
|
+
real_content.append(getattr(result, "__data")[i] + 128)
|
|
115
|
+
return bytes(real_content)
|
|
116
|
+
|
|
117
|
+
def from_byte_vector(self, byte_vector):
|
|
118
|
+
vector = self.simulation.module.VectorTint8_tT()
|
|
119
|
+
for byte in byte_vector:
|
|
120
|
+
self.simulation.module.functions.append(vector, byte - 128)
|
|
121
|
+
self.simulation.module.functions.from_byte_vector(self.state, vector)
|
|
122
|
+
|
|
123
|
+
def write_binary(self, path: str):
|
|
124
|
+
with open(path, mode="wb") as file:
|
|
125
|
+
file.write(self.as_byte_vector())
|
|
126
|
+
file.flush()
|
|
127
|
+
|
|
128
|
+
def load_binary(self, path: str):
|
|
129
|
+
with open(path, mode="rb") as file:
|
|
130
|
+
bytes = file.read()
|
|
131
|
+
self.from_byte_vector(bytes)
|
|
132
|
+
|
|
133
|
+
def load(self, path: str) -> bool:
|
|
134
|
+
with open(path, mode="r") as file:
|
|
135
|
+
bytes = file.read()
|
|
136
|
+
return self.from_string(bytes)
|
|
137
|
+
|
|
138
|
+
def score(self):
|
|
139
|
+
return self.state.score
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Simulation:
|
|
143
|
+
def __init__(self, wrapper: str, tmp_dir=None):
|
|
144
|
+
self.wrapper_path = wrapper
|
|
145
|
+
self.module = import_file("sim", wrapper)
|
|
146
|
+
self.tmp_dir = tmp_dir
|
|
147
|
+
|
|
148
|
+
if "play" in self.module.actionToAnyFunctionType:
|
|
149
|
+
self.any_action = self.module.actionToAnyFunctionType["play"]
|
|
150
|
+
any_action_union = getattr(self.any_action(), "content")
|
|
151
|
+
alternatives = getattr(any_action_union, "_fields_")
|
|
152
|
+
|
|
153
|
+
self.actions = [
|
|
154
|
+
ActionType(action_type, i, self)
|
|
155
|
+
for (i, (_, action_type)) in enumerate(alternatives)
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
def get_class(self, name):
|
|
159
|
+
return getattr(self.module, name)
|
|
160
|
+
|
|
161
|
+
def get_all_actions(self):
|
|
162
|
+
action = self.module.AnyGameAction()
|
|
163
|
+
actions = self.module.functions.enumerate(action)
|
|
164
|
+
result = []
|
|
165
|
+
for i in range(self.module.functions.size(actions)):
|
|
166
|
+
copy = self.module.AnyGameAction()
|
|
167
|
+
self.module.functions.assign(copy, self.module.functions.get(actions, i).contents)
|
|
168
|
+
result.append(copy)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
def valid_actions(self, state):
|
|
174
|
+
# Convert NumPy arrays to nested tuples to make them hashable.
|
|
175
|
+
x = []
|
|
176
|
+
for action in self.get_all_actions():
|
|
177
|
+
if self.module.functions.can_apply_impl(action, state):
|
|
178
|
+
x.append(action)
|
|
179
|
+
return x
|
|
180
|
+
|
|
181
|
+
def can_apply_action(self, action, state):
|
|
182
|
+
return self.module.functions.can_apply_impl(action, state)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def start(self, name: str, *args):
|
|
186
|
+
return State(self, getattr(self.module.functions, name)(*args))
|
|
187
|
+
|
|
188
|
+
def parse_action(self, action: str) -> Action:
|
|
189
|
+
any_action = self.module.actionToAnyFunctionType["play"]()
|
|
190
|
+
rl_string = self.to_rl_string(action)
|
|
191
|
+
if self.module.functions.from_string(any_action, rl_string):
|
|
192
|
+
return Action(any_action, self)
|
|
193
|
+
else:
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def parse_actions_from_binary_buffer(self, actions: bytes) -> [list]:
|
|
197
|
+
vector = self._bytes_to_byte_vector(actions)
|
|
198
|
+
any_action = self.module.actionToAnyFunctionType["play"]()
|
|
199
|
+
vec = self.module.functions.parse_actions(any_action, vector)
|
|
200
|
+
to_return = []
|
|
201
|
+
for i in range(getattr(vec, "__size")):
|
|
202
|
+
to_return.append(getattr(vec, "__data")[i])
|
|
203
|
+
return to_return
|
|
204
|
+
|
|
205
|
+
def _bytes_to_byte_vector(self, byte_vector: bytes):
|
|
206
|
+
vector = self.module.VectorTint8_tT()
|
|
207
|
+
for byte in byte_vector:
|
|
208
|
+
self.module.functions.append(vector, byte)
|
|
209
|
+
return vector
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def action_names(self) -> [str]:
|
|
213
|
+
return [name for name in self.module.actions.keys()]
|
|
214
|
+
|
|
215
|
+
def dump(self):
|
|
216
|
+
for name in self.action_names:
|
|
217
|
+
print(name)
|
|
218
|
+
|
|
219
|
+
def to_rl_string(self, string):
|
|
220
|
+
return self.module.rl_s__strlit_r_String(string)
|
|
221
|
+
|
|
222
|
+
def to_python_string(self, string):
|
|
223
|
+
first_character = getattr(getattr(string, "__data"), "__data")
|
|
224
|
+
return self.module.cast(first_character, self.module.c_char_p).value.decode(
|
|
225
|
+
"utf-8"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
def action_from_string(self, string: str) -> Action:
|
|
229
|
+
rl_string = self.to_rl_string(str)
|
|
230
|
+
action = self.any_action()
|
|
231
|
+
if not self.module.functions.from_string(action, rl_string):
|
|
232
|
+
return None
|
|
233
|
+
return Action(action, self)
|
|
234
|
+
|
|
235
|
+
def action_from_byte_vector(self, byte_vector) -> Action:
|
|
236
|
+
action = self.any_action()
|
|
237
|
+
vector = self.module.VectorTint8_tT()
|
|
238
|
+
for byte in byte_vector:
|
|
239
|
+
self.module.functions.append(vector, byte - 128)
|
|
240
|
+
if not self.module.functions.parse_action_optimized(action, vector, 0):
|
|
241
|
+
return None
|
|
242
|
+
return Action(action, self)
|
|
243
|
+
|
|
244
|
+
def __enter__(self):
|
|
245
|
+
return self
|
|
246
|
+
|
|
247
|
+
def cleanup(self):
|
|
248
|
+
self.__exit__()
|
|
249
|
+
|
|
250
|
+
def __exit__(self, *args):
|
|
251
|
+
import ctypes
|
|
252
|
+
import _ctypes
|
|
253
|
+
libHandle = self.module.lib._handle
|
|
254
|
+
|
|
255
|
+
if self.tmp_dir is not None:
|
|
256
|
+
if os.name == "nt":
|
|
257
|
+
del self.module
|
|
258
|
+
_ctypes.FreeLibrary(libHandle)
|
|
259
|
+
self.tmp_dir.cleanup()
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def functions(self):
|
|
263
|
+
return self.module.functions
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def compile(source, rlc_compiler="rlc", rlc_includes=[], rlc_runtime_lib="", optimized=True):
|
|
267
|
+
include_args = []
|
|
268
|
+
for arg in rlc_includes:
|
|
269
|
+
include_args.append("-i")
|
|
270
|
+
include_args.append(arg)
|
|
271
|
+
tmp_dir = TemporaryDirectory(ignore_cleanup_errors=True)
|
|
272
|
+
assert (
|
|
273
|
+
run(
|
|
274
|
+
[
|
|
275
|
+
rlc_compiler,
|
|
276
|
+
source,
|
|
277
|
+
"--python",
|
|
278
|
+
"-o",
|
|
279
|
+
Path(tmp_dir.name) / Path("wrapper.py"),
|
|
280
|
+
"-O2" if optimized else "",
|
|
281
|
+
]
|
|
282
|
+
+ include_args
|
|
283
|
+
).returncode
|
|
284
|
+
== 0
|
|
285
|
+
)
|
|
286
|
+
lib_name = "lib.dll" if os.name == "nt" else "lib.so"
|
|
287
|
+
args = [rlc_compiler, source, "--shared", "-o", Path(tmp_dir.name) / Path(lib_name), "-O2" if optimized else ""]
|
|
288
|
+
if rlc_runtime_lib != "":
|
|
289
|
+
args = args + ["--runtime-lib", rlc_runtime_lib]
|
|
290
|
+
assert run(args + include_args).returncode == 0
|
|
291
|
+
return Simulation(str(Path(tmp_dir.name) / Path("wrapper.py")), tmp_dir)
|
ml/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#
|
|
2
|
+
# This file is part of the RLC project.
|
|
3
|
+
#
|
|
4
|
+
# RLC is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License version 2 as published by the Free Software Foundation.
|
|
5
|
+
#
|
|
6
|
+
# RLC is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
|
7
|
+
#
|
|
8
|
+
# You should have received a copy of the GNU General Public License along with RLC. If not, see <https://www.gnu.org/licenses/>.
|
|
9
|
+
#
|
|
10
|
+
from .training_data import TrainingData
|
|
11
|
+
from .transformer import RLCTransformer, run_once, next_probabilities
|
|
12
|
+
from .action_oracle import ActionOracle
|
ml/action_oracle.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2024 Massimo Fioravanti
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
#
|
|
16
|
+
from loader import State
|
|
17
|
+
import torch
|
|
18
|
+
from ml import RLCTransformer, run_once
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ActionOracle:
|
|
22
|
+
def __init__(self, transformer: RLCTransformer, action_table):
|
|
23
|
+
self.transformer = transformer
|
|
24
|
+
self.action_table = action_table
|
|
25
|
+
|
|
26
|
+
def __call__(self, states: State):
|
|
27
|
+
non_null_states = [state for state in states if state is not None]
|
|
28
|
+
tensor = (
|
|
29
|
+
torch.IntTensor(
|
|
30
|
+
[[int(x) for x in state.as_byte_vector()] for state in non_null_states]
|
|
31
|
+
)
|
|
32
|
+
.t()
|
|
33
|
+
.to(self.transformer.device)
|
|
34
|
+
)
|
|
35
|
+
result_bytes = run_once(
|
|
36
|
+
self.transformer.model,
|
|
37
|
+
tensor,
|
|
38
|
+
True,
|
|
39
|
+
3,
|
|
40
|
+
self.transformer.device,
|
|
41
|
+
self.transformer.ntokens,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
result = []
|
|
45
|
+
index = 0
|
|
46
|
+
for state in states:
|
|
47
|
+
if state is None:
|
|
48
|
+
result.append(None)
|
|
49
|
+
else:
|
|
50
|
+
num = result_bytes[0, index]
|
|
51
|
+
num = num % len(self.action_table)
|
|
52
|
+
if num >= len(self.action_table) or num < 0:
|
|
53
|
+
result.append(None)
|
|
54
|
+
else:
|
|
55
|
+
action = self.action_table[num]
|
|
56
|
+
if not action.can_run(state):
|
|
57
|
+
result.append(None)
|
|
58
|
+
else:
|
|
59
|
+
result.append(action)
|
|
60
|
+
index = index + 1
|
|
61
|
+
return result
|
ml/raylib/__init__.py
ADDED
|
File without changes
|
ml/raylib/action_mask.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
from ray.rllib.core.rl_module.rl_module import RLModule, RLModuleConfig
|
|
2
|
+
from ray.rllib.policy.sample_batch import SampleBatch
|
|
3
|
+
import torch
|
|
4
|
+
from ray.rllib.utils.torch_utils import FLOAT_MIN
|
|
5
|
+
from ray.rllib.algorithms.ppo.torch.ppo_torch_rl_module import PPOTorchRLModule
|
|
6
|
+
import gymnasium as gym
|
|
7
|
+
from ray.rllib.utils.annotations import override
|
|
8
|
+
from ray.rllib.core.columns import Columns
|
|
9
|
+
from ray.rllib.utils.typing import TensorType
|
|
10
|
+
|
|
11
|
+
from typing import Dict, Optional, Tuple
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ActionMaskRLMBase(RLModule):
|
|
15
|
+
@override(RLModule)
|
|
16
|
+
def __init__(self, config: RLModuleConfig):
|
|
17
|
+
# If observation space is not of type `Dict` raise an error.
|
|
18
|
+
if not isinstance(config.observation_space, gym.spaces.dict.Dict):
|
|
19
|
+
raise ValueError(
|
|
20
|
+
"This RLModule requires the environment to provide a "
|
|
21
|
+
"`gym.spaces.Dict` observation space of the form: \n"
|
|
22
|
+
" {'action_mask': Box(0.0, 1.0, shape=(self.action_space.n,)),"
|
|
23
|
+
" 'observation_space': self.observation_space}"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# While the environment holds an observation space that contains, both,
|
|
27
|
+
# the action mask and the original observation space, the 'RLModule'
|
|
28
|
+
# receives only the `"observation"` element of the space, but not the
|
|
29
|
+
# action mask.
|
|
30
|
+
self.observation_space_with_mask = config.observation_space
|
|
31
|
+
config.observation_space = config.observation_space["observations"]
|
|
32
|
+
|
|
33
|
+
# Keeps track if observation specs have been checked already.
|
|
34
|
+
self._checked_observations = False
|
|
35
|
+
|
|
36
|
+
# The PPORLModule, in its constructor will build networks for the original
|
|
37
|
+
# observation space (i.e. without the action mask).
|
|
38
|
+
super().__init__(config)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _check_batch(batch):
|
|
43
|
+
"""Check whether the batch contains the required keys."""
|
|
44
|
+
if "action_mask" not in batch[SampleBatch.OBS]:
|
|
45
|
+
raise ValueError(
|
|
46
|
+
"Action mask not found in observation. This model requires "
|
|
47
|
+
"the environment to provide observations that include an "
|
|
48
|
+
"action mask (i.e. an observation space of the Dict space "
|
|
49
|
+
"type that looks as follows: \n"
|
|
50
|
+
"{'action_mask': Box(0.0, 1.0, shape=(self.action_space.n,)),"
|
|
51
|
+
"'observations': <observation_space>}"
|
|
52
|
+
)
|
|
53
|
+
if "observations" not in batch[SampleBatch.OBS]:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
"Observations not found in observation.This model requires "
|
|
56
|
+
"the environment to provide observations that include a "
|
|
57
|
+
" (i.e. an observation space of the Dict space "
|
|
58
|
+
"type that looks as follows: \n"
|
|
59
|
+
"{'action_mask': Box(0.0, 1.0, shape=(self.action_space.n,)),"
|
|
60
|
+
"'observations': <observation_space>}"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TorchActionMaskRLM(ActionMaskRLMBase, PPOTorchRLModule):
|
|
65
|
+
@override(PPOTorchRLModule)
|
|
66
|
+
def setup(self):
|
|
67
|
+
super().setup()
|
|
68
|
+
# We need to reset here the observation space such that the
|
|
69
|
+
# super`s (`PPOTorchRLModule`) observation space is the
|
|
70
|
+
# original space (i.e. without the action mask) and `self`'s
|
|
71
|
+
# observation space contains the action mask.
|
|
72
|
+
self.config.observation_space = self.observation_space_with_mask
|
|
73
|
+
|
|
74
|
+
@override(PPOTorchRLModule)
|
|
75
|
+
def setup(self):
|
|
76
|
+
super().setup()
|
|
77
|
+
# We need to reset here the observation space such that the
|
|
78
|
+
# super`s (`PPOTorchRLModule`) observation space is the
|
|
79
|
+
# original space (i.e. without the action mask) and `self`'s
|
|
80
|
+
# observation space contains the action mask.
|
|
81
|
+
self.config.observation_space = self.observation_space_with_mask
|
|
82
|
+
|
|
83
|
+
@override(PPOTorchRLModule)
|
|
84
|
+
def _forward_inference(
|
|
85
|
+
self, batch: Dict[str, TensorType], **kwargs
|
|
86
|
+
) -> Dict[str, TensorType]:
|
|
87
|
+
# Preprocess the original batch to extract the action mask.
|
|
88
|
+
action_mask, batch = self._preprocess_batch(batch)
|
|
89
|
+
# Run the forward pass.
|
|
90
|
+
outs = super()._forward_inference(batch, **kwargs)
|
|
91
|
+
# Mask the action logits and return.
|
|
92
|
+
return self._mask_action_logits(outs, action_mask)
|
|
93
|
+
|
|
94
|
+
@override(PPOTorchRLModule)
|
|
95
|
+
def _forward_exploration(
|
|
96
|
+
self, batch: Dict[str, TensorType], **kwargs
|
|
97
|
+
) -> Dict[str, TensorType]:
|
|
98
|
+
# Preprocess the original batch to extract the action mask.
|
|
99
|
+
action_mask, batch = self._preprocess_batch(batch)
|
|
100
|
+
# Run the forward pass.
|
|
101
|
+
outs = super()._forward_exploration(batch, **kwargs)
|
|
102
|
+
# Mask the action logits and return.
|
|
103
|
+
return self._mask_action_logits(outs, action_mask)
|
|
104
|
+
|
|
105
|
+
@override(PPOTorchRLModule)
|
|
106
|
+
def _forward_train(
|
|
107
|
+
self, batch: Dict[str, TensorType], **kwargs
|
|
108
|
+
) -> Dict[str, TensorType]:
|
|
109
|
+
# Preprocess the original batch to extract the action mask.
|
|
110
|
+
action_mask, batch = self._preprocess_batch(batch)
|
|
111
|
+
# Run the forward pass.
|
|
112
|
+
outs = super()._forward_train(batch, **kwargs)
|
|
113
|
+
# Mask the action logits and return.
|
|
114
|
+
return self._mask_action_logits(outs, action_mask)
|
|
115
|
+
|
|
116
|
+
@override(PPOTorchRLModule)
|
|
117
|
+
def _compute_values(self, batch: Dict[str, TensorType], device):
|
|
118
|
+
# Preprocess the batch to extract the `observations` to `Columns.OBS`.
|
|
119
|
+
_, batch = self._preprocess_batch(batch)
|
|
120
|
+
# Call the super's method to compute values for GAE.
|
|
121
|
+
return super()._compute_values(batch, device)
|
|
122
|
+
|
|
123
|
+
def _preprocess_batch(
|
|
124
|
+
self, batch: Dict[str, TensorType], **kwargs
|
|
125
|
+
) -> Tuple[TensorType, Dict[str, TensorType]]:
|
|
126
|
+
"""Extracts observations and action mask from the batch
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
batch: A dictionary containing tensors (at least `Columns.OBS`)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
A tuple with the action mask tensor and the modified batch containing
|
|
133
|
+
the original observations.
|
|
134
|
+
"""
|
|
135
|
+
# Check observation specs for action mask and observation keys.
|
|
136
|
+
self._check_batch(batch)
|
|
137
|
+
|
|
138
|
+
# Extract the available actions tensor from the observation.
|
|
139
|
+
action_mask = batch[Columns.OBS].pop("action_mask")
|
|
140
|
+
|
|
141
|
+
# Modify the batch for the `PPORLModule`'s `forward` method, i.e.
|
|
142
|
+
# pass only `"obs"` into the `forward` method.
|
|
143
|
+
batch[Columns.OBS] = batch[Columns.OBS].pop("observations")
|
|
144
|
+
|
|
145
|
+
# Return the extracted action mask and the modified batch.
|
|
146
|
+
return action_mask, batch
|
|
147
|
+
|
|
148
|
+
def _check_batch(self, batch: Dict[str, TensorType]) -> Optional[ValueError]:
|
|
149
|
+
"""Assert that the batch includes action mask and observations.
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
batch: A dicitonary containing tensors (at least `Columns.OBS`) to be
|
|
153
|
+
checked.
|
|
154
|
+
|
|
155
|
+
Raises:
|
|
156
|
+
`ValueError` if the column `Columns.OBS` does not contain observations
|
|
157
|
+
and action mask.
|
|
158
|
+
"""
|
|
159
|
+
if not self._checked_observations:
|
|
160
|
+
if "action_mask" not in batch[Columns.OBS]:
|
|
161
|
+
raise ValueError(
|
|
162
|
+
"No action mask found in observation. This `RLModule` requires "
|
|
163
|
+
"the environment to provide observations that include an "
|
|
164
|
+
"action mask (i.e. an observation space of the Dict space "
|
|
165
|
+
"type that looks as follows: \n"
|
|
166
|
+
"{'action_mask': Box(0.0, 1.0, shape=(self.action_space.n,)),"
|
|
167
|
+
"'observations': self.observation_space}"
|
|
168
|
+
)
|
|
169
|
+
if "observations" not in batch[Columns.OBS]:
|
|
170
|
+
raise ValueError(
|
|
171
|
+
"No observations found in observation. This 'RLModule` requires "
|
|
172
|
+
"the environment to provide observations that include the original "
|
|
173
|
+
"observations under a key `'observations'` in a dict (i.e. an "
|
|
174
|
+
"observation space of the Dict space type that looks as follows: \n"
|
|
175
|
+
"{'action_mask': Box(0.0, 1.0, shape=(self.action_space.n,)),"
|
|
176
|
+
"'observations': <observation_space>}"
|
|
177
|
+
)
|
|
178
|
+
self._checked_observations = True
|
|
179
|
+
|
|
180
|
+
def _mask_action_logits(
|
|
181
|
+
self, batch: Dict[str, TensorType], action_mask: TensorType
|
|
182
|
+
) -> Dict[str, TensorType]:
|
|
183
|
+
"""Masks the action logits for the output of `forward` methods
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
batch: A dictionary containing tensors (at least action logits).
|
|
187
|
+
action_mask: A tensor containing the action mask for the current
|
|
188
|
+
observations.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
A modified batch with masked action logits for the action distribution
|
|
192
|
+
inputs.
|
|
193
|
+
"""
|
|
194
|
+
# Convert action mask into an `[0.0][-inf]`-type mask.
|
|
195
|
+
inf_mask = torch.clamp(torch.log(action_mask), min=FLOAT_MIN)
|
|
196
|
+
|
|
197
|
+
# Mask the logits.
|
|
198
|
+
batch[Columns.ACTION_DIST_INPUTS] += inf_mask
|
|
199
|
+
|
|
200
|
+
# Return the batch with the masked action logits.
|
|
201
|
+
return batch
|