diff --git a/irlc/__init__.py b/irlc/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a5273e7ad3ca01ceff509fd51046bd4f52d95482 --- /dev/null +++ b/irlc/__init__.py @@ -0,0 +1,261 @@ +""" Source code for 02466, Introduction to reinforcement learning and control, offered at DTU """ +__version__ = "0.0.1" + +# Do not import Matplotlib (or imports which import matplotlib) in case you have to run in headless mode. +import shutil +import inspect +import lzma, pickle + +import gymnasium +import numpy as np +import os + +# Global imports from across the API. Allows imports like +# > from irlc import Agent, train +from irlc.utils.irlc_plot import main_plot as main_plot +from irlc.utils.irlc_plot import plot_trajectory as plot_trajectory +try: + from irlc.ex01.agent import Agent as Agent, train as train + from irlc.ex09.rl_agent import TabularAgent, ValueAgent +except ImportError: + pass +from irlc.utils.player_wrapper import interactive as interactive +from irlc.utils.lazylog import LazyLog # This one is unclear. Is it required? +from irlc.utils.timer import Timer + + +def get_irlc_base(): + dir_path = os.path.dirname(os.path.realpath(__file__)) + return dir_path + +def get_students_base(): + return os.path.join(get_irlc_base(), "../../../02465students/") + + +def pd2latex_(pd, index=False, escape=False, column_spec=None, **kwargs): # You can add column specs. + for c in pd.columns: + if pd[c].values.dtype == 'float64' and all(pd[c].values - np.round(pd[c].values)==0): + pd[c] = pd[c].astype(int) + ss = pd.to_latex(index=index, escape=escape, **kwargs) + return fix_bookstabs_latex_(ss,column_spec=column_spec) + +def fix_bookstabs_latex_(ss, linewidth=True, first_column_left=True, column_spec=None): + to_tabular_x = linewidth + + if to_tabular_x: + ss = ss.replace("tabular", "tabularx") + lines = ss.split("\n") + hd = lines[0].split("{") + if column_spec is None: + adj = (('l' if to_tabular_x else 'l') if first_column_left else 'C') + ("".join(["C"] * (len(hd[-1][:-1]) - 1))) + else: + adj = column_spec + + # adj = ( ('l' if to_tabular_x else 'l') if first_column_left else 'C') + ("".join(["C"] * (len(hd[-1][:-1])-1))) + if linewidth: + lines[0] = "\\begin{tabularx}{\\linewidth}{" + adj + "}" + else: + lines[0] = "\\begin{tabular}{" + adj.lower() + "}" + + ss = '\n'.join(lines) + return ss + +def plotenv(env : gymnasium.Env): + """ + Given a Gymnasium environment instance, this function will plot the environment as a matplotlib image. Remember to call ``plt.show()`` to actually see the image. + + For this function to work, you must create the environment with :python:`render_mode='human'`. + + .. note:: + + This function may not work for all gymnasium environments, however, it will work for most environments we use in this course. + + :param env: The environment to plot. + """ + + from PIL import Image + import matplotlib.pyplot as plt + if hasattr(env, 'render_mode') and not env.render_mode == 'rgb_array': + env.render_mode, rmt = 'rgb_array', env.render_mode + frame = env.render() + if hasattr(env, 'render_mode') and not env.render_mode == 'rgb_array': + env.render_mode = rmt + + im = Image.fromarray(frame) + + plt.figure(figsize=(16, 16)) + plt.imshow(im) + plt.axis('off') + plt.tight_layout() + + + + +def _savepdf_env(file, env): + from PIL import Image + import matplotlib.pyplot as plt + if hasattr(env, 'render_mode') and not env.render_mode == 'rgb_array': + env.render_mode, rmt = 'rgb_array', env.render_mode + frame = env.render() + if hasattr(env, 'render_mode') and not env.render_mode == 'rgb_array': + env.render_mode = rmt + + im = Image.fromarray(frame) + snapshot_base = file + if snapshot_base.endswith(".png"): + sf = snapshot_base[:-4] + fext = 'png' + else: + fext = 'pdf' + if snapshot_base.endswith(".pdf"): + sf = snapshot_base[:-4] + else: + sf = snapshot_base + + sf = f"{sf}.{fext}" + dn = os.path.dirname(sf) + if len(dn) > 0 and not os.path.isdir(dn): + os.makedirs(dn) + print("Saving snapshot of environment to", os.path.abspath(sf)) + if fext == 'png': + im.save(sf) + from irlc import _move_to_output_directory + _move_to_output_directory(sf) + else: + plt.figure(figsize=(16, 16)) + plt.imshow(im) + plt.axis('off') + plt.tight_layout() + from irlc import savepdf + savepdf(sf, verbose=True) + # plt.show() + + + +def savepdf(pdf, verbose=False, watermark=False, env=None): + """ + Convenience function for saving PDFs. Just call it after you have created your plot as ``savepdf('my_file.pdf')`` + to save a PDF of the plot. + You can also pass an environment, in which case the environment will be stored to a pdf file. + + + :param pdf: The file to save to, for instance ``"my_pdf.pdf"`` + :param verbose: Print output destination (optional) + :param watermark: Include a watermark (optional) + :return: Full path of the created PDF. + """ + if env is not None: + _savepdf_env(pdf, env) + return + + import matplotlib.pyplot as plt + pdf = os.path.normpath(pdf.strip()) + pdf = pdf+".pdf" if not pdf.endswith(".pdf") else pdf + + if os.sep in pdf: + pdf = os.path.abspath(pdf) + else: + pdf = os.path.join(os.getcwd(), "pdf", pdf) + if not os.path.isdir(os.path.dirname(pdf)): + os.makedirs(os.path.dirname(pdf)) + + + + # filename = None + stack = inspect.stack() + modules = [inspect.getmodule(s[0]) for s in inspect.stack()] + files = [m.__file__ for m in modules if m is not None] + if any( [f.endswith("RUN_OUTPUT_CAPTURE.py") for f in files] ): + return + + # for s in stack: + # print(s) + # print(stack) + # for k in range(len(stack)-1, -1, -1): + # frame = stack[k] + # module = inspect.getmodule(frame[0]) + # filename = module.__file__ + # print(filename) + # if not any([filename.endswith(f) for f in ["pydev_code_executor.py", "pydevd.py", "_pydev_execfile.py", "pydevconsole.py", "pydev_ipython_console.py"] ]): + # # print("breaking c. debugger", filename) + # break + # if any( [filename.endswith(f) for f in ["pydevd.py", "_pydev_execfile.py"]]): + # print("pdf path could not be resolved due to debug mode being active in pycharm", filename) + # return + # print("Selected filename", filename) + # wd = os.path.dirname(filename) + # pdf_dir = wd +"/pdf" + # if filename.endswith("_RUN_OUTPUT_CAPTURE.py"): + # return + # if not os.path.isdir(pdf_dir): + # os.mkdir(pdf_dir) + wd = os.getcwd() + irlc_base = os.path.dirname(__file__) + if False: + pass + else: + plt.savefig(fname=pdf) + outf = os.path.normpath(os.path.abspath(pdf)) + print("> [savepdf]", pdf + (f" [full path: {outf}]" if verbose else "")) + + return outf + + +def _move_to_output_directory(file): + """ + Hidden function: Move file given file to static output dir. + """ + if not is_this_my_computer(): + return + CDIR = os.path.dirname(os.path.realpath(__file__)).replace('\\', '/') + shared_output_dir = CDIR + "/../../shared/output" + shutil.copy(file, shared_output_dir + "/"+ os.path.basename(file) ) + + +def bmatrix(a): + if False: + return a.__str__() + else: + np.set_printoptions(suppress=True) + """Returns a LaTeX bmatrix + :a: numpy array + :returns: LaTeX bmatrix as a string + """ + if len(a.shape) > 2: + raise ValueError('bmatrix can at most display two dimensions') + lines = str(a).replace('[', '').replace(']', '').splitlines() + rv = [r'\begin{bmatrix}'] + rv += [' ' + ' & '.join(l.split()) + r'\\' for l in lines] + rv += [r'\end{bmatrix}'] + return '\n'.join(rv) + + +def is_this_my_computer(): + CDIR = os.path.dirname(os.path.realpath(__file__)).replace('\\', '/') + return os.path.exists(CDIR + "/../../Exercises") + +def cache_write(object, file_name, only_on_professors_computer=False, verbose=True, protocol=-1): # -1 is default protocol. Fix crash issue with large files. + if only_on_professors_computer and not is_this_my_computer(): + """ Probably for your own good :-). """ + return + + dn = os.path.dirname(file_name) + if not os.path.exists(dn): + os.mkdir(dn) + if verbose: print("Writing cache...", file_name) + with lzma.open(file_name, 'wb') as f: + pickle.dump(object, f) + # compress_pickle.dump(object, f, compression="lzma", protocol=protocol) + if verbose: + print("Done!") + + +def cache_exists(file_name): + return os.path.exists(file_name) + +def cache_read(file_name): + if os.path.exists(file_name): + with lzma.open(file_name, 'rb') as f: + return pickle.load(f) + else: + return None diff --git a/irlc/__pycache__/__init__.cpython-311.pyc b/irlc/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..97a0dcea2a082392c4fe14d86fedd5de91689215 Binary files /dev/null and b/irlc/__pycache__/__init__.cpython-311.pyc differ diff --git a/irlc/car/__init__.py b/irlc/car/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/car/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/car/car_model.py b/irlc/car/car_model.py new file mode 100644 index 0000000000000000000000000000000000000000..d99168458fe1e71303acb48540bead0f934357a9 --- /dev/null +++ b/irlc/car/car_model.py @@ -0,0 +1,304 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# from irlc.car.car_viewer import CarViewer +from irlc.car.car_viewer import CarViewerPygame +import numpy as np +import sympy as sym +from scipy.optimize import Bounds +from gymnasium.spaces import Box +from irlc.car.sym_map import SymMap, wrap_angle +from irlc.ex03.control_model import ControlModel +from irlc.ex03.control_cost import SymbolicQRCost +from irlc.ex04.discrete_control_model import DiscreteControlModel +from irlc.ex04.control_environment import ControlEnvironment +# from irlc.ex03.control_specification import ControlSpecification + +""" +class MySpecification(): + def get_bounds(self): + return bounds + + def get_cost(self): + pass + + def sym_f(self): + return ... + + def simulate(self): + # Simulate using RK4. + + pass + + +spec = MySpecification() +model = Model(spec) +model.simulate(...) + + + +""" + + +class SymbolicBicycleModel(ControlModel): + metadata = { + 'render.modes': ['human', 'rgb_array'], + 'video.frames_per_second': 30 + } + def __init__(self, map_width=0.8, simple_bounds=None, cost=None, hot_start=False, verbose=True): + s = """ + Coordinate system of the car: + State x consist of + x[0] = Vx (speed in direction of the car body) + x[1] = Vy (speed perpendicular to car body) + x[2] = wz (Yaw rate; how fast the car is turning) + x[3] = e_psi (Angle of rotation between car body and centerline) + x[4] = s (How far we are along the track) + x[5] = e_y (Distance between car body and closest point on centerline) + + Meanwhile the actions are + u[0] : Angle between wheels and car body (i.e. are we steering to the right or to the left) + u[1] : Engine force (applied to the rear wheels, i.e. accelerates car) + """ + if verbose: + print(s) + # if simple_bounds is None: + # simple_bounds = dict() + self.map = SymMap(width=map_width) + self.v_max = 3.0 + + self.viewer = None # rendering + self.hot_start = hot_start + # self.observation_space = Box(low=np.asarray([-np.inf, -np.inf, -np.inf, -np.inf, -np.inf, -map_width], dtype=float), + # high=np.asarray([v_max, np.inf, np.inf, np.inf, np.inf, map_width]), dtype=float) + # self.action_space = Box(low=np.asarray([-0.5, -1]), high=np.asarray([0.5, 1]), dtype=float) + + # xl = np.zeros((6,)) + # xl[4] = self.map.TrackLength + # simple_bounds = {'x0': Bounds([-np.inf, -np.inf, -np.inf, -np.inf, -np.inf, -map_width], [v_max, np.inf, np.inf, np.inf, np.inf, map_width]), + # 'xF': Bounds(list(xl), list(xl)), **simple_bounds} + # n = 6 + # d = 2 + # if cost is None: + # cost = SymbolicQRCost(Q=np.zeros((6,6)), R=np.eye(2)*10, qc=0*1.) + # bounds = dict(x_low=[-np.inf, -np.inf, -np.inf, -np.inf, -np.inf, -map_width], x_high=[self.v_max, np.inf, np.inf, np.inf, np.inf, map_width], + # u_low=[-0.5, -1], u_high=[0.5, 1]) + + super().__init__() + + def get_cost(self) -> SymbolicQRCost: + return SymbolicQRCost(Q=np.zeros((6,6)), R=np.eye(2)*10, qc=1.*0) + + def x_bound(self) -> Box: + return Box(np.asarray([-np.inf, -np.inf, -np.inf, -np.inf, -np.inf, -self.map.width]), + np.asarray([self.v_max, np.inf, np.inf, np.inf, np.inf, self.map.width])) + + def u_bound(self) -> Box: + return Box(np.asarray([-0.5, -1]),np.asarray([0.5, 1])) + + def render(self, x, render_mode='human'): + if self.viewer == None: + self.viewer = CarViewerPygame(self) + + self.viewer.update(self.x_curv2x_XY(x)) + return self.viewer.blit(render_mode=render_mode) + # return self.viewer.render(return_rgb_array=mode == 'rgb_array') + + def close(self): + if self.viewer is not None: + self.viewer.close() + + def x_curv2x_XY(self, x_curv): + ''' + Utility function for converting x (including velocities, etc.) from local (curvilinear) coordinates to global XY position. + ''' + Xc, Yc, vangle = self.map.getGlobalPosition(s=x_curv[4], ey=x_curv[5], epsi=x_curv[3]) + dglob = np.asarray([x_curv[0], x_curv[1], x_curv[2], vangle, Xc, Yc]) + return dglob + + def sym_f(self, x, u, t=None, curvelinear_coordinates=True, curvature_s=None): + ''' + Create derivative function + + \dot{x} = f(x, u) + + We will both create it in curvelinear coordinates or normal (global) coordinates. + ''' + # Vehicle Parameters + m = 1.98 + lf = 0.125 + lr = 0.125 + Iz = 0.024 + Df = 0.8 * m * 9.81 / 2.0 + Cf = 1.25 + Bf = 1.0 + Dr = 0.8 * m * 9.81 / 2.0 + Cr = 1.25 + Br = 1.0 + + vx = x[0] + vy = x[1] + wz = x[2] + if curvelinear_coordinates: + epsi = x[3] + s = x[4] + ey = x[5] + else: + psi = x[3] + + delta = u[0] + a = u[1] + + alpha_f = delta - sym.atan2(vy + lf * wz, vx) + alpha_r = -sym.atan2(vy - lf * wz, vx) + + # Compute lateral force at front and rear tire + Fyf = 2 * Df * sym.sin(Cf * sym.atan(Bf * alpha_f)) + Fyr = 2 * Dr * sym.sin(Cr * sym.atan(Br * alpha_r)) + + d_vx = (a - 1 / m * Fyf * sym.sin(delta) + wz * vy) + d_vy = (1 / m * (Fyf * sym.cos(delta) + Fyr) - wz * vx) + d_wz = (1 / Iz * (lf * Fyf * sym.cos(delta) - lr * Fyr)) + + if curvelinear_coordinates: + cur = self.map.sym_curvature(s) + d_epsi = (wz - (vx * sym.cos(epsi) - vy * sym.sin(epsi)) / (1 - cur * ey) * cur) + d_s = ((vx * sym.cos(epsi) - vy * sym.sin(epsi)) / (1 - cur * ey)) + """ + Compute derivative of e_y here (d_ey). See paper for details. + """ + d_ey = (vx * sym.sin(epsi) + vy * sym.cos(epsi)) # Old ex here ! b ! b + # implement the ODE governing ey (distane from center of road) in curveliner coordinates + xp = [d_vx, d_vy, d_wz, d_epsi, d_s, d_ey] + + else: + d_psi = wz + d_X = ((vx * sym.cos(psi) - vy * sym.sin(psi))) + d_Y = (vx * sym.sin(psi) + vy * sym.cos(psi)) + + xp = [d_vx, d_vy, d_wz, d_psi, d_X, d_Y] + return xp + + def fix_angles(self, x): + # fix angular component of x + if x.size == self.state_size: + x[3] = wrap_angle(x[3]) + elif x.shape[1] == self.state_size: + x[:,3] = wrap_angle(x[:,3]) + return x + + +class DiscreteCarModel(DiscreteControlModel): + def __init__(self, dt=0.1, cost=None, **kwargs): + model = SymbolicBicycleModel(**kwargs) + # self.observation_space = model.observation_space + # self.action_space = model.action_space + # n = 6 + # d = 2 + # if cost is None: + # from irlc.ex04.cost_discrete import DiscreteQRCost + # cost = DiscreteQRCost(Q=np.zeros((model.state_size, model.state_size)), R=np.eye(model.action_size)) + super().__init__(model=model, dt=dt, cost=cost) + # self.cost = cost + self.map = model.map + + +class CarEnvironment(ControlEnvironment): + def __init__(self, Tmax=10, noise_scale=1.0, cost=None, max_laps=10, hot_start=False, render_mode=None, **kwargs): + discrete_model = DiscreteCarModel(cost=cost, hot_start=hot_start, **kwargs) + super().__init__(discrete_model, Tmax=Tmax, render_mode=render_mode) + self.map = discrete_model.map + self.noise_scale = noise_scale + self.cost = cost + self.completed_laps = 0 + self.max_laps = max_laps + + def simple_bounds(self): + simple_bounds = {'x': Bounds(self.observation_space.low, self.observation_space.high), + 't0': Bounds([0], [0]), + 'u': Bounds(self.action_space.low, self.action_space.high)} + return simple_bounds + + """ We add a bit of noise for backward compatibility. """ + def step(self, u): + # We don't want to render the car before we have added jitter (below). These lines therefore disable rendering + self.render_mode, rmt_ = None, self.render_mode + xp, cost, terminated, truncated, info = super().step(u) + self.render_mode = rmt_ + + x = xp + if hasattr(self, 'seed') and self.seed is not None and not callable(self.seed): + np.random.seed(self.seed) + + noise_vx = np.maximum(-0.05, np.minimum(np.random.randn() * 0.01, 0.05)) + noise_vy = np.maximum(-0.1, np.minimum(np.random.randn() * 0.01, 0.1)) + noise_wz = np.maximum(-0.05, np.minimum(np.random.randn() * 0.005, 0.05)) + if True: #self.noise_scale > 0: + x[0] = x[0] + 0.03 * noise_vx #* self.noise_scale + x[1] = x[1] + 0.03 * noise_vy #* self.noise_scale + x[2] = x[2] + 0.03 * noise_wz #* self.noise_scale + + if x[4] > self.map.TrackLength: + self.completed_laps += 1 + x[4] -= self.map.TrackLength + + done = self.completed_laps >= self.max_laps + if x[4] < 0: + assert(False) + if self.render_mode == 'human': + self.render() + return x, cost, done, False, info + + def L(self, x): + ''' + Implement whether we have obtained the terminal condition. see eq. 4 in "Autonomous Racing using LMPC" + + :param x: + :return: + ''' + return x[4] > self.map.TrackLength + + def epoch_reset(self, x): + ''' + After completing one epoch, i.e. when L(x) == True, reset the x-vector using this method to + restart the epoch. In practice, take one more lap on the track. + + :param x: + :return: + ''' + x = x.copy() + x[4] -= self.map.TrackLength + return x + + def _get_initial_state(self): + x0 = np.zeros((6,)) + if self.discrete_model.continuous_model.hot_start: + x0[0] = 0.5 # Start velocity is 0.5 + # self.render() + return x0 + +if __name__ == "__main__": + # car = SymbolicBicycleModel() + # car.render(car.reset()) + # sleep(2.0) + # car.close() + # print("Hello world") + env = CarEnvironment(render_mode='human') + env.metadata['video.frames_per_second'] = 10000 + # from irlc import VideoMonitor + # env = wrappers.Monitor(env, "carvid2", force=True, video_callable=lambda episode_id: True) + # env = VideoMonitor(env) + env.reset() + import time + t0 = time.time() + n = 300 + for _ in range(n): + u = env.action_space.sample() + # print(u) + # u *= 0 + u[0] = 0 + u[1] = 0.01 + s, cost, done, truncated, info = env.step(u) + # print(s) + # sleep(5) + env.close() + tpf = (time.time()- t0)/n + print("TPF", tpf, "fps", 1/tpf) diff --git a/irlc/car/car_viewer.py b/irlc/car/car_viewer.py new file mode 100644 index 0000000000000000000000000000000000000000..0952d40241b9eda29df6319d8482f7d9abb697b8 --- /dev/null +++ b/irlc/car/car_viewer.py @@ -0,0 +1,51 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# from pyglet.shapes import Rectangle, Circle +# from irlc.utils.pyglet_rendering import PygletViewer, PolygonOutline, GroupedElement +import pygame +from irlc.utils.graphics_util_pygame import UpgradedGraphicsUtil +import numpy as np + +track_outline = (0, 0, 0) +track_middle = (220, 25, 25) + +class CarViewerPygame(UpgradedGraphicsUtil): + def __init__(self, car): + + n = int(10 * (car.map.PointAndTangent[-1, 3] + car.map.PointAndTangent[-1, 4])) + center = [car.map.getGlobalPosition(i * 0.1, 0) for i in range(n)] + outer = [car.map.getGlobalPosition(i * 0.1, -car.map.width) for i in range(n)] + inner = [car.map.getGlobalPosition(i * 0.1, car.map.width) for i in range(n)] + fudge = 0.2 + xs, ys = zip(*outer) + super().__init__(screen_width=1000, xmin=min(xs) - fudge, xmax=max(xs) + fudge, + ymax=min(ys) - fudge, ymin=max(ys) + fudge, title="Racecar environment") + self.center = center + self.outer = outer + self.inner = inner + # Load ze sprite. + from irlc.utils.graphics_util_pygame import Object + self.car = Object("car.png", image_width=90) + + + def render(self): + green = (126, 200, 80) + track = (144,)*3 + self.draw_background(background_color=green) + + self.polygon("safd", self.outer, fillColor=track, outlineColor=track_outline, width=3) + self.polygon("in", self.inner, fillColor=green, outlineColor=track_outline, width=3) + self.polygon("in", self.center, fillColor=None, filled=False, outlineColor=(100, 100, 100), width=5) + # Now draw the pretty car. + x, y, psi = self.xglob[4], self.xglob[5], self.xglob[3] + xy = self.fixxy((x,y)) + # self.car.rect.move() + self.car.rect.center = xy + # self.car.rect.center = xy[1] + + self.car.rotate(psi / (2*np.pi) * 360) + # self.car.rotate(45) + self.car.blit(self.surf) + self.circle("in", (x,y), 4, fillColor=(255, 0, 0)) # drawn on the center of the car. + + def update(self, xglob): + self.xglob = xglob diff --git a/irlc/car/sym_map.py b/irlc/car/sym_map.py new file mode 100644 index 0000000000000000000000000000000000000000..0142042dd9d769c2456009e2706edf6826f30221 --- /dev/null +++ b/irlc/car/sym_map.py @@ -0,0 +1,450 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import pdb +import matplotlib.pyplot as plt +import numpy as np +import numpy.linalg as la +import sympy as sym + +""" +This is a bunch of pretty awful code to define a map and compute useful quantities like tangents, etc. +Defining a map is pretty straight forward (it consist of circle archs and lines), but +don't try to read on. +""" +class SymMap: + def plot(self, show=False): + PointAndTangent, TrackLength, extra = self.spec2PointAndTangent(self.spec) + for i in range(PointAndTangent.shape[0]-1): + extra_ = extra[i] + if 'CenterX' in extra_: + CenterX, CenterY = extra_['CenterX'], extra_['CenterY'] + angle, spanAng = extra_['angle'], extra_['spanAng'] + r = self.spec[i,1] + direction = 1 if r >= 0 else -1 + + # Plotting. Ignore this + plt.plot(CenterX, CenterY, 'ro') + tt = np.linspace(angle, angle + direction * spanAng) + plt.plot(CenterX + np.cos(tt) * np.abs(r), CenterY + np.abs(r) * np.sin(tt), 'r-') + + x, y = PointAndTangent[:, 0], PointAndTangent[:, 1] + plt.plot(x, y, '.-') + print(np.sum(np.sum(np.abs(self.PointAndTangent - PointAndTangent)))) + + if show: + plt.show() + ''' + Format: + PointAndTangent = [x, + y, + psi: angle of tangent vector at the last point of segment, + total-distance-travelled, + segment-length, curvature] + + Also creates a symbolic expression to evaluate track position. + ''' + def spec2PointAndTangent(self, spec): + # also create a symbolic piecewise expression to evaluate the curvature as a function of track length location. + + # spec = self.spec + # PointAndTangent = self.PointAndTangent.copy() + PointAndTangent = np.zeros((spec.shape[0] + 1, 6)) + extra = [] + + N = spec.shape[0] + segment_s_cur = 0 # Distance travelled to start of segment (s-coordinate). + angle_prev = 0 # Angle of the tangent vector at the starting point of the segment + x_prev, y_prev = 0, 0 # x,y coordinate of last point of previous segment. + for i in range(N): + l, r = spec[i,0], spec[i,1] # Length of segment and radius of curvature + ang = angle_prev # Angle of the tangent vector at the starting point of the segment + + if r == 0.0: # If the current segment is a straight line + x = x_prev + l * np.cos(ang) # x coordinate of the last point of the segment + y = y_prev + l * np.sin(ang) # y coordinate of the last point of the segment + psi = ang # Angle of the tangent vector at the last point of the segment + curvature = 0 + extra_ = {} + else: + direction = 1 if r >= 0 else -1 + CenterX = x_prev + np.abs(r) * np.cos(ang + direction * np.pi / 2) # x coordinate center of circle + CenterY = y_prev + np.abs(r) * np.sin(ang + direction * np.pi / 2) # y coordinate center of circle + spanAng = l / np.abs(r) # Angle spanned by the circle + psi = wrap_angle(ang + spanAng * np.sign(r)) # Angle of the tangent vector at the last point of the segment + angleNormal = wrap_angle((direction * np.pi / 2 + ang)) + angle = -(np.pi - np.abs(angleNormal)) * (sign(angleNormal)) + x = CenterX + np.abs(r) * np.cos(angle + direction * spanAng) # x coordinate of the last point of the segment + y = CenterY + np.abs(r) * np.sin(angle + direction * spanAng) # y coordinate of the last point of the segment + curvature = 1/r + + extra_ = {'CenterX': CenterX, + 'CenterY': CenterY, + 'angle': angle, + 'direction': direction, + 'spanAng': spanAng} + + extra.append(extra_) + NewLine = np.array([x, y, psi, segment_s_cur, l, curvature]) + PointAndTangent[i, :] = NewLine # Write the new info + x_prev, y_prev, angle_prev = PointAndTangent[i, 0], PointAndTangent[i, 1], PointAndTangent[i, 2] + segment_s_cur += l + + xs = PointAndTangent[-2, 0] + ys = PointAndTangent[-2, 1] + xf = 0 + yf = 0 + psif = 0 + + l = np.sqrt((xf - xs) ** 2 + (yf - ys) ** 2) + + NewLine = np.array([xf, yf, psif, PointAndTangent[-2, 3] + PointAndTangent[-2, 4], l, 0]) + PointAndTangent[-1, :] = NewLine + TrackLength = PointAndTangent[-1, 3] + PointAndTangent[-1, 4] + + return PointAndTangent, TrackLength, extra + + + """map object + Attributes: + getGlobalPosition: convert position from (s, ey) to (X,Y) + """ + def __init__(self, width): + """Initialization + width: track width + Modify the vector spec to change the geometry of the track + """ + self.width = width + self.halfWidth = 0.4 + self.slack = 0.45 + lengthCurve = 3.5 # 3.0 + straight = 1.0 + spec = np.array([[1.0, 0], + [lengthCurve, lengthCurve / np.pi], + # Note s = 1 * np.pi / 2 and r = -1 ---> Angle spanned = np.pi / 2 + [straight, 0], + [lengthCurve / 2, -lengthCurve / np.pi], + [straight, 0], + [lengthCurve, lengthCurve / np.pi], + [lengthCurve / np.pi * 2 + 1.0, 0], + [lengthCurve / 2, lengthCurve / np.pi]]) + + + PointAndTangent, TrackLength, extra = self.spec2PointAndTangent(spec) + self.PointAndTangent = PointAndTangent + self.TrackLength = TrackLength + self.spec = spec + + + ''' + Creates a symbolic expression for the curvature + +def Curvature(s, PointAndTangent): + """curvature computation + s: curvilinear abscissa at which the curvature has to be evaluated + PointAndTangent: points and tangent vectors defining the map (these quantities are initialized in the map object) + """ + TrackLength = PointAndTangent[-1,3]+PointAndTangent[-1,4] + + # In case on a lap after the first one + while (s > TrackLength): + s = s - TrackLength + + # Given s \in [0, TrackLength] compute the curvature + # Compute the segment in which system is evolving + index = np.all([[s >= PointAndTangent[:, 3]], [s < PointAndTangent[:, 3] + PointAndTangent[:, 4]]], axis=0) + + i = int(np.where(np.squeeze(index))[0]) + curvature = PointAndTangent[i, 5] + + return curvature + + ''' + def sym_curvature(self, s): + s = s - self.TrackLength * sym.floor(s / self.TrackLength) + n = self.PointAndTangent.shape[0] + pw = [] + for i in range(n): + pw.append( (self.PointAndTangent[i,5], s - (self.PointAndTangent[i, 3] + self.PointAndTangent[i, 4]) <= 0) ) + p = sym.Piecewise(*pw) + return p + + def getGlobalPosition(self, s, ey, epsi=None, vangle_true=None): + """coordinate transformation from curvilinear reference frame (e, ey) to inertial reference frame (X, Y) + (s, ey): position in the curvilinear reference frame + """ + # wrap s along the track + # while (s > self.TrackLength): + # s = s - self.TrackLength + s = np.mod(s, self.TrackLength) + + # Compute the segment in which system is evolving + PointAndTangent = self.PointAndTangent + + index = np.all([[s >= PointAndTangent[:, 3]], [s < PointAndTangent[:, 3] + PointAndTangent[:, 4]]], axis=0) + dx = np.where(np.squeeze(index)) + if len(dx) < 1: + a = 234 + raise Exception("bad") + try: + i = int(np.where(np.squeeze(index))[0]) + except Exception as e: + print(e) + + + if PointAndTangent[i, 5] == 0.0: # If segment is a straight line + # Extract the first final and initial point of the segment + xf = PointAndTangent[i, 0] + yf = PointAndTangent[i, 1] + xs = PointAndTangent[i - 1, 0] + ys = PointAndTangent[i - 1, 1] + psi = PointAndTangent[i, 2] + + # Compute the segment length + deltaL = PointAndTangent[i, 4] + reltaL = s - PointAndTangent[i, 3] + + # Do the linear combination + x = (1 - reltaL / deltaL) * xs + reltaL / deltaL * xf + ey * np.cos(psi + np.pi / 2) + y = (1 - reltaL / deltaL) * ys + reltaL / deltaL * yf + ey * np.sin(psi + np.pi / 2) + if epsi is not None: + vangle = psi + epsi + else: + r = 1 / PointAndTangent[i, 5] # Extract curvature + ang = PointAndTangent[i - 1, 2] # Extract angle of the tangent at the initial point (i-1) + # Compute the center of the arc + direction = 1 if r >= 0 else -1 + # if r >= 0: + # direction = 1 + # else: + # direction = -1 + + CenterX = PointAndTangent[i - 1, 0] + np.abs(r) * np.cos(ang + direction * np.pi / 2) # x coordinate center of circle + CenterY = PointAndTangent[i - 1, 1] + np.abs(r) * np.sin(ang + direction * np.pi / 2) # y coordinate center of circle + + spanAng = (s - PointAndTangent[i, 3]) / (np.pi * np.abs(r)) * np.pi + + angleNormal = wrap_angle(direction * np.pi / 2 + ang) + + angle = -(np.pi - np.abs(angleNormal)) * (sign(angleNormal)) + + x = CenterX + (np.abs(r) - direction * ey) * np.cos(angle + direction * spanAng) # x coordinate of the last point of the segment + y = CenterY + (np.abs(r) - direction * ey) * np.sin(angle + direction * spanAng) # y coordinate of the last point of the segment + + if epsi is not None: + vangle = epsi + direction * spanAng + PointAndTangent[i - 1, 2] + + if epsi is None: + return x,y + else: + vangle = wrap_angle(vangle) + if vangle_true is not None: + vangle_true = wrap_angle(vangle_true) + # vangle, vangle_true = np.unwrap([vangle, vangle_true]) + if err(vangle - vangle_true, exception=False) > 1e-3: # debug code + print([vangle_true, vangle]) + print("Bad angle, delta: ", vangle - vangle_true) + raise Exception("bad angle") + return x, y, vangle + + def getLocalPosition(self, x, y, psi): + """coordinate transformation from inertial reference frame (X, Y) to curvilinear reference frame (s, ey) + (X, Y): position in the inertial reference frame + """ + PointAndTangent = self.PointAndTangent + CompletedFlag = 0 + + for i in range(0, PointAndTangent.shape[0]): + if CompletedFlag == 1: + break + + if PointAndTangent[i, 5] == 0.0: # If segment is a straight line + # Extract the first final and initial point of the segment + xf = PointAndTangent[i, 0] + yf = PointAndTangent[i, 1] + xs = PointAndTangent[i - 1, 0] + ys = PointAndTangent[i - 1, 1] + + psi_unwrap = np.unwrap([PointAndTangent[i - 1, 2], psi])[1] + epsi = psi_unwrap - PointAndTangent[i - 1, 2] + # Check if on the segment using angles + if (la.norm(np.array([xs, ys]) - np.array([x, y]))) == 0: + s = PointAndTangent[i, 3] + ey = 0 + CompletedFlag = 1 + + elif (la.norm(np.array([xf, yf]) - np.array([x, y]))) == 0: + s = PointAndTangent[i, 3] + PointAndTangent[i, 4] + ey = 0 + CompletedFlag = 1 + else: + if np.abs(computeAngle( [x,y] , [xs, ys], [xf, yf])) <= np.pi/2 and np.abs(computeAngle( [x,y] , [xf, yf], [xs, ys])) <= np.pi/2: + v1 = np.array([x,y]) - np.array([xs, ys]) + angle = computeAngle( [xf,yf] , [xs, ys], [x, y]) + s_local = la.norm(v1) * np.cos(angle) + s = s_local + PointAndTangent[i, 3] + ey = la.norm(v1) * np.sin(angle) + + if np.abs(ey)<= self.width: + CompletedFlag = 1 + + else: + xf = PointAndTangent[i, 0] + yf = PointAndTangent[i, 1] + xs = PointAndTangent[i - 1, 0] + ys = PointAndTangent[i - 1, 1] + + r = 1 / PointAndTangent[i, 5] # Extract curvature + direction = 1 if r >= 0 else -1 + # if r >= 0: + # direction = 1 + # else: + # direction = -1 + ang = PointAndTangent[i - 1, 2] # Extract angle of the tangent at the initial point (i-1) + + # Compute the center of the arc + CenterX = xs + np.abs(r) * np.cos(ang + direction * np.pi / 2) # x coordinate center of circle + CenterY = ys + np.abs(r) * np.sin(ang + direction * np.pi / 2) # y coordinate center of circle + + # Check if on the segment using angles + if (la.norm(np.array([xs, ys]) - np.array([x, y]))) == 0: + ey = 0 + psi_unwrap = np.unwrap([ang, psi])[1] + epsi = psi_unwrap - ang + s = PointAndTangent[i, 3] + CompletedFlag = 1 + elif (la.norm(np.array([xf, yf]) - np.array([x, y]))) == 0: + s = PointAndTangent[i, 3] + PointAndTangent[i, 4] + ey = 0 + psi_unwrap = np.unwrap([PointAndTangent[i, 2], psi])[1] + epsi = psi_unwrap - PointAndTangent[i, 2] + CompletedFlag = 1 + else: + arc1 = PointAndTangent[i, 4] * PointAndTangent[i, 5] + arc2 = computeAngle([xs, ys], [CenterX, CenterY], [x, y]) + if np.sign(arc1) == np.sign(arc2) and np.abs(arc1) >= np.abs(arc2): + v = np.array([x, y]) - np.array([CenterX, CenterY]) + s_local = np.abs(arc2)*np.abs(r) + s = s_local + PointAndTangent[i, 3] + ey = -np.sign(direction) * (la.norm(v) - np.abs(r)) + psi_unwrap = np.unwrap([ang + arc2, psi])[1] + epsi = psi_unwrap - (ang + arc2) + + if np.abs(ey) <= self.width: + CompletedFlag = 1 + + if epsi>1.0: + raise Exception("epsi very large; car in wrong direction") + pdb.set_trace() + + if CompletedFlag == 0: + s = 10000 + ey = 10000 + epsi = 10000 + + print("Error!! POINT OUT OF THE TRACK!!!! <==================") + raise Exception("car outside track") + # pdb.set_trace() + + return s, ey, epsi, CompletedFlag + + + def curvature_and_angle(self, s): + """curvature computation + s: curvilinear abscissa at which the curvature has to be evaluated + PointAndTangent: points and tangent vectors defining the map (these quantities are initialized in the map object) + """ + PointAndTangent = self.PointAndTangent + TrackLength = PointAndTangent[-1, 3] + PointAndTangent[-1, 4] + + # In case on a lap after the first one + while (s > TrackLength): + s = s - TrackLength + + # Given s \in [0, TrackLength] compute the curvature + # Compute the segment in which system is evolving + index = np.all([[s >= PointAndTangent[:, 3]], [s < PointAndTangent[:, 3] + PointAndTangent[:, 4]]], axis=0) + i = int(np.where(np.squeeze(index))[0]) + curvature = PointAndTangent[i, 5] + angle = PointAndTangent[i, 4] # tangent angle of path + return curvature, angle, i + + + +# ====================================================================================================================== +# ====================================================================================================================== +# ====================================== Internal utilities functions ================================================== +# ====================================================================================================================== +# ====================================================================================================================== +def computeAngle(point1, origin, point2): + # The orientation of this angle matches that of the coordinate system. Tha is why a minus sign is needed + v1 = np.array(point1) - np.array(origin) + v2 = np.array(point2) - np.array(origin) + + dot = v1[0] * v2[0] + v1[1] * v2[1] # dot product between [x1, y1] and [x2, y2] + det = v1[0] * v2[1] - v1[1] * v2[0] # determinant + angle = np.arctan2(det, dot) # atan2(y, x) or atan2(sin, cos) + return angle + +''' +This is used because np.sign(a) return 0 when a=0, which is pretty stupid. +''' +def sign(a): + return 1 if a >= 0 else -1 + +def wrap_angle(angle): + return np.mod(angle+np.pi, 2 * np.pi) - np.pi + +''' +Compute difference of these two vectors taking into account the angular component wraps +''' +def xy_diff(x,y): + dx = x-y + if len(dx.shape) == 1: + dx[3] = wrap_angle(dx[3]) + else: + dx[:,3] = wrap_angle(dx[:,3]) + return dx + + +def unityTestChangeOfCoordinates(map, ClosedLoopData): + """For each point in ClosedLoopData change (X, Y) into (s, ey) and back to (X, Y) to check accurancy + """ + TestResult = 1 + for i in range(0, ClosedLoopData.x.shape[0]): + xdat = ClosedLoopData.x + xglobdat = ClosedLoopData.x_glob + + s, ey, epsi, _ = map.getLocalPosition(x=xglobdat[i, 4], y=xglobdat[i, 5], psi=xglobdat[i, 3]) + v1 = np.array([epsi, s, ey]) + v2 = np.array(xdat[i, 3:6]) + x,y,vangle = np.array(map.getGlobalPosition(s=v1[1], ey=v1[2],epsi=v1[0], vangle_true=xglobdat[i,3] )) + v3 = np.array([ vangle, x, y]) + v4 = np.array( [wrap_angle( xglobdat[i, 3] )] + xglobdat[i, 4:6].tolist() ) + # print(i) + if np.abs( wrap_angle( xglobdat[i, 3] ) - vangle ) > 0.1: + print("BAD") + raise Exception("bad angle test result") + + if np.dot(v3 - v4, v3 - v4) > 0.00000001: + TestResult = 0 + print("ERROR", v1, v2, v3, v4) + # pdb.set_trace() + v1 = np.array(map.getLocalPosition(xglobdat[i, 4], xglobdat[i, 5])) + v2 = np.array(xdat[i, 4:6]) + v3 = np.array(map.getGlobalPosition(v1[0], v1[1])) + v4 = np.array([xglobdat[i, 4], xglobdat[i, 5]]) + print(np.dot(v3 - v4, v3 - v4)) + # pdb.set_trace() + + if TestResult == 1: + print("Change of coordinates test passed!") + + +def err(x, exception=True, tol=1e-5, message="Error too large!"): + er = np.mean(np.abs(x).flat) + if er > tol: + print(message) + print(x) + print(er) + if exception: + raise Exception(message) + return er diff --git a/irlc/ex00/__init__.py b/irlc/ex00/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8239917af89716ec33217e0ca7a897d67aaef65c --- /dev/null +++ b/irlc/ex00/__init__.py @@ -0,0 +1,2 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""This directory contains the exercises for week 0.""" diff --git a/irlc/ex00/fruit_homework.py b/irlc/ex00/fruit_homework.py new file mode 100644 index 0000000000000000000000000000000000000000..c2538c5645d8f95b480c9591f5cd5a6a964f2334 --- /dev/null +++ b/irlc/ex00/fruit_homework.py @@ -0,0 +1,119 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +def add(a, b): + """ This function shuold return the sum of a and b. I.e. if print(add(2,3)) should print '5'. """ + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + +def misterfy(animals): + """ + Given a list of animals like animals=["cat", "wolf", "elephans"], this function should return + a list like ["mr cat", "mr wolf", "mr elephant"] """ + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + +def mean_value(p_dict): + """ + Given a dictionary of the form: {x: probability_of_x, ...} compute the mean value of + x, i.e. sum_i x_i * p(x_i). The recommended way is to use list comprehension and not numpy. + Hint: Look at the .items() method and the build-in sum(my_list) method. """ + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + +def fruits_ordered(order_dict): + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + +class BasicFruitShop: + """ This is a simple class that represents a fruit-shop. + You instantiate it with a dictionary of prices """ + def __init__(self, name, prices): + """ prices is a dictionary of the form {fruit_name: cost}. For instance + prices = {'apple': 5, 'orange': 6} """ + self.name = name + self.prices = prices + + def cost(self, fruit): + """ Return the cost in pounds of the fruit with name 'fruit'. It uses the self.prices variable + to get the price. + You don't need to do exception handling here. """ + # TODO: 1 lines missing. + raise NotImplementedError("Return cost of fruit as a floating point number") + +class OnlineFruitShop(BasicFruitShop): + def price_of_order(self, order): + """ + order_dict = {'apple': 5, 'pear': 2, ...} where the numbers are the quantity ordered. + + Hints: Dictionary comprehension like: + > for fruit, pounds in order_dict.items() + > self.getCostPerPound(fruit) allows you to get cost of a fruit + > the total is sum of {pounds} * {cost_per_pound} + """ + # TODO: 1 lines missing. + raise NotImplementedError("return the total cost of the order") + + +def shop_smart(order, fruit_shops): + """ + order_dict: dictionary {'apple': 3, ...} of fruits and the pounds ordered + fruitShops: List of OnlineFruitShops + + Hints: + > Remember there is a s.price_of_order method + > Use this method to first make a list containing the cost of the order at each fruit shop + > List has form [cost1, cost2], then find the index of the smallest value (the list has an index-function) + > return fruitShops[lowest_index]. + """ + # TODO: 2 lines missing. + raise NotImplementedError("Implement function body") + return best_shop + + +if __name__ == '__main__': + "This code runs when you invoke the script from the command line (but not otherwise)" + + """ Quesion 1: Lists and basic data types """ + print("add(2,5) function should return 7, and it returned", add(2, 5)) + + animals = ["cat", "giraffe", "wolf"] + print("The nice animals are", misterfy(animals)) + + """ + This problem represents the probabilities of a loaded die as a dictionary such that + > p(roll=3) = p_dict[3] = 0.15. + """ + p_die = {1: 0.20, + 2: 0.10, + 3: 0.15, + 4: 0.05, + 5: 0.10, + 6: 0.40} + print("Mean roll of die, sum_{i=1}^6 i * p(i) =", mean_value(p_die)) + + order = {'apples': 1.0, + 'oranges': 3.0} + print("The different fruits in the fruit-order is", fruits_ordered(order)) + + """ Part B: A simple class """ + price1 = {"apple": 4, "pear": 8, 'orange': 10} + shop1 = BasicFruitShop("Alis Funky Fruits", price1) + + price2 = {'banana': 9, "apple": 5, "pear": 7, 'orange': 11} + shop2 = BasicFruitShop("Hansen Fruit Emporium", price2) + + fruit = "apple" + print("The cost of", fruit, "in", shop1.name, "is", shop1.cost(fruit)) + print("The cost of", fruit, "in", shop2.name, "is", shop2.cost(fruit)) + + """ Part C: Class inheritance """ + price_of_fruits = {'apples': 2, 'oranges': 1, 'pears': 1.5, 'mellon': 10} + shopA = OnlineFruitShop('shopA', price_of_fruits) + print("The price of the given order in shopA is", shopA.price_of_order(order)) + + """ Part C: Using classes """ + shopB = OnlineFruitShop('shopB', {'apples': 1.0, 'oranges': 5.0}) + + shops = [shopA, shopB] + print("For the order", order, " the best shop is", shop_smart(order, shops).name) + order = {'apples': 3.0} # test with a new order. + print("For the order", order, " the best shop is", shop_smart(order, shops).name) diff --git a/irlc/ex01/__init__.py b/irlc/ex01/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..51d06d4927f23b2f23bf8b39f2b235f268d55ca8 --- /dev/null +++ b/irlc/ex01/__init__.py @@ -0,0 +1,2 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""This directory contains the exercises for week 1.""" diff --git a/irlc/ex01/__pycache__/__init__.cpython-311.pyc b/irlc/ex01/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..31afb3e3eb30d987d8b13ffdca6fad503c42634e Binary files /dev/null and b/irlc/ex01/__pycache__/__init__.cpython-311.pyc differ diff --git a/irlc/ex01/__pycache__/agent.cpython-311.pyc b/irlc/ex01/__pycache__/agent.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..38d6cdd2e0550830a1f3c3198566c6e9aa1b77b5 Binary files /dev/null and b/irlc/ex01/__pycache__/agent.cpython-311.pyc differ diff --git a/irlc/ex01/__pycache__/inventory_environment.cpython-311.pyc b/irlc/ex01/__pycache__/inventory_environment.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c931bc5d63994b85ecd853b1a4743d0e84ee3b9c Binary files /dev/null and b/irlc/ex01/__pycache__/inventory_environment.cpython-311.pyc differ diff --git a/irlc/ex01/agent.py b/irlc/ex01/agent.py new file mode 100644 index 0000000000000000000000000000000000000000..093e841165a2d21e3cd6ad8f78e11fb66f34ff52 --- /dev/null +++ b/irlc/ex01/agent.py @@ -0,0 +1,385 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""The Agent class. + +References: + [Her24] Tue Herlau. Sequential decision making. (Freely available online), 2024. +""" +import typing +import itertools +import os +import sys +from collections import OrderedDict, namedtuple +import numpy as np +from tqdm import tqdm +from irlc.utils.common import load_time_series, log_time_series +from irlc.utils.irlc_plot import existing_runs +import shutil +from gymnasium import Env +from dataclasses import dataclass + +class Agent: + r"""The main agent class. See (Her24, Subsection 4.4.3) for additional details. + + To use the agent class, you should first create an environment. In this case we will just create an instance of the + ``InventoryEnvironment`` (see (Her24, Subsection 4.2.3)) + + :Example: + + .. runblock:: pycon + + >>> from irlc import Agent # You can import directly from top-level package + >>> import numpy as np + >>> np.random.seed(42) # Fix the seed for reproduciability + >>> from irlc.ex01.inventory_environment import InventoryEnvironment + >>> env = InventoryEnvironment() # Create an instance of the environment + >>> agent = Agent(env) # Create an instance of the agent. + >>> s0, info0 = env.reset() # Always call reset to start the environment + >>> a0 = agent.pi(s0, k=0, info=info0) # Tell the agent to compute action $a_{k=0}$ + >>> print(f"In state {s0=}, the agent took the action {a0=}") + """ + + def __init__(self, env: Env): + """Instantiate the Agent class. + + The agent is given the openai gym environment it must interact with. This allows the agent to know what the + action and observation space is. + + :param env: The openai gym ``Env`` instance the agent should interact with. + """ + self.env = env + + def pi(self, s, k : int, info : typing.Optional[dict] =None): + r"""Evaluate the Agent's policy (i.e., compute the action the agent want to take) at time step ``k`` in state ``s``. + + This correspond to the environment being in a state evaluating :math:`x_k`, and the function should compute the next + action the agent wish to take: + + .. math:: + u_k = \mu_k(x_k) + + This means that ``s`` = :math:`x_k` and ``k`` = :math:`k =\{0, 1, ...\}`. The function should return an action that lies in the action-space + of the environment. + + The info dictionary: + The ``info``-dictionary contains possible extra information returned from the environment, for instance when calling the ``s, info = env.reset()`` function. + The main use in this course is in control, where the dictionary contains a value ``info['time_seconds']`` (which corresponds to the simulation time :math:`t` in seconds). + + We will also use the info dictionary to let the agent know certain actions are not available. This is done by setting the ``info['mask']``-key. + Note that this is only relevant for reinforcement learning, and you should see the documentation/exercises for reinforcement learning for additional details. + + The default behavior of the agent is to return a random action. An example: + + .. runblock:: pycon + + >>> from irlc.pacman.pacman_environment import PacmanEnvironment + >>> from irlc import Agent + >>> env = PacmanEnvironment() + >>> s, info = env.reset() + >>> agent = Agent(env) + >>> agent.pi(s, k=0, info=info) # get a random action + >>> agent.pi(s, k=0) # If info is not specified, all actions are assumed permissible. + + + :param s: Current state the environment is in. + :param timestep: Current time + :return: The action the agent want to take in the given state at the given time. By default the agent returns a random action + """ + if info is None or 'mask' not in info: + return self.env.action_space.sample() + else: + """ In the case where the actions available in each state differ, openAI deals with that by specifying a + ``mask``-entry in the info-dictionary. The mask can then be passed on to the + env.action_space.sample-function to make sure we don't sample illegal actions. I consider this the most + difficult and annoying thing about openai gym.""" + if info['mask'].max() > 1: + raise Exception("Bad mask!") + return self.env.action_space.sample(mask=info['mask']) + + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + r"""Implement this function if the agent has to learn (be trained). + + Note that you only have to implement this function from week 7 onwards -- before that, we are not interested in control methods that learn. + + The agent takes a number of input arguments. You should imagine that + + * ``s`` is the current state :math:`x_k`` + * ``a`` is the action the agent took in state ``s``, i.e. ``a`` :math:`= u_k = \mu_k(x_k)` + * ``r`` is the reward the the agent got from that action + * ``sp`` (s-plus) is the state the environment then transitioned to, i.e. ``sp`` :math:`= x_{k+1}` + * '``done`` tells the agent if the environment has stopped + * ``info_s`` is the information-dictionary returned by the environment as it transitioned to ``s`` + * ``info_sp`` is the information-dictionary returned by the environment as it transitioned to ``sp``. + + The following example will hopefully clarify it by showing how you would manually call the train-function once: + + :Example: + + .. runblock:: pycon + + >>> from irlc.ex01.inventory_environment import InventoryEnvironment # import environment + >>> from irlc import Agent + >>> env = InventoryEnvironment() # Create an instance of the environment + >>> agent = Agent(env) # Create an instance of the agent. + >>> s, info_s = env.reset() # s is the current state + >>> a = agent.pi(s, k=0, info=info_s) # The agent takes an action + >>> sp, r, done, _, info_sp = env.step(a) # Environment updates + >>> agent.train(s, a, r, sp, done, info_s, info_sp) # How the training function is called + + + In control and dynamical programming, please recall that the reward is equal to minus the cost. + + :param s: Current state :math:`x_k` + :param a: Action taken :math:`u_k` + :param r: Reward obtained by taking action :math:`a_k` in state :math:`x_k` + :param sp: The state that the environment transitioned to :math:`{\\bf x}_{k+1}` + :param info_s: The information dictionary corresponding to ``s`` returned by ``env.reset`` (when :math:`k=0`) and otherwise ``env.step``. + :param info_sp: The information-dictionary corresponding to ``sp`` returned by ``env.step`` + :param done: Whether environment terminated when transitioning to ``sp`` + :return: None + """ + pass + + def __str__(self): + """**Optional:** A unique name for this agent. Used for labels when plotting, but can be kept like this.""" + return super().__str__() + + def extra_stats(self) -> dict: + """**Optional:** Implement this function if you wish to record extra information from the ``Agent`` while training. + + You can safely ignore this method as it will only be used for control theory to create nicer plots """ + return {} + +fields = ('time', 'state', 'action', 'reward') +Trajectory = namedtuple('Trajectory', fields + ("env_info",)) + +# Experiment using a dataclass. +@dataclass +class Stats: + episode: int + episode_length: int + accumulated_reward: float + + total_steps: int + trajectory : Trajectory = None + agent_stats : dict = None + + @property + def average_reward(self): + return self.accumulated_reward / self.episode_length + +# s = Stats(episode=0, episode_length=5, accumulated_reward=4, total_steps=2, trajectory=Trajectory()) + + +def train(env, + agent=None, + experiment_name=None, + num_episodes=1, + verbose=True, + reset=True, # If True we will call env.reset() upon episode start. + max_steps=1e10, + max_runs=None, + return_trajectory=True, # Return the current trajectories as a list + resume_stats=None, # Resume stat collection from last save. + log_interval=1, # Only log every log_interval steps. Reduces size of log files. + delete_old_experiments=False, # Remove the old experiments folder. Useful while debugging a model (or to conserve disk space) + seed=None, # Attempt to set the seed of the random number generator to produce reproducible results. + ): + """This function implements the main training loop as described in (Her24, Subsection 4.4.4). + + The loop will simulate the interaction between agent `agent` and the environment `env`. + The function has a lot of special functionality, so it is useful to consider the common cases. An example: + + >>> stats, _ = train(env, agent, num_episodes=2) + + Simulate interaction for two episodes (i.e. environment terminates two times and is reset). + `stats` will be a list of length two containing information from each run + + >>> stats, trajectories = train(env, agent, num_episodes=2, return_Trajectory=True) + + `trajectories` will be a list of length two containing information from the two trajectories. + + >>> stats, _ = train(env, agent, experiment_name='experiments/my_run', num_episodes=2) + + Save `stats`, and trajectories, to a file which can easily be loaded/plotted (see course software for examples of this). + The file will be time-stamped so using several calls you can repeat the same experiment (run) many times. + + >>> stats, _ = train(env, agent, experiment_name='experiments/my_run', num_episodes=2, max_runs=10) + + As above, but do not perform more than 10 runs. Useful for repeated experiments. + + :param env: An openai-Gym ``Env`` instance (the environment) + :param agent: An ``Agent`` instance + :param experiment_name: The outcome of this experiment will be saved in a folder with this name. This will allow you to run multiple (repeated) experiment and visualize the results in a single plot, which is very important in reinforcement learning. + :param num_episodes: Number of episodes to simulate + :param verbose: Display progress bar + :param reset: Call ``env.reset()`` before simulation start. Default is ``True``. This is only useful in very rare cases. + :param max_steps: Terminate if this many steps have elapsed (for non-terminating environments) + :param max_runs: Maximum number of repeated experiments (requires ``experiment_name``) + :param return_trajectory: Return trajectories list (Off by default since it might consume lots of memory) + :param resume_stats: Resume stat collection from last run (this requires the ``experiment_name`` variable to be set) + :param log_interval: Log stats less frequently than each episode. Useful if you want to run really long experiments. + :param delete_old_experiments: If true, old saved experiments will be deleted. This is useful during debugging. + :param seed: An integer. The random number generator of the environment will be reset to this seed allowing for reproducible results. + :return: A list where each element corresponds to each (started) episode. The elements are dictionaries, and contain the statistics for that episode. + """ + + from irlc import cache_write + from irlc import cache_read + saveload_model = False + # temporal_policy = None + save_stats = True + if agent is None: + print("[train] No agent was specified. Using irlc.Agent(env) (this agent selects actions at random)") + agent = Agent(env) + + if delete_old_experiments and experiment_name is not None and os.path.isdir(experiment_name): + shutil.rmtree(experiment_name) + + if experiment_name is not None and max_runs is not None and existing_runs(experiment_name) >= max_runs: + stats, recent = load_time_series(experiment_name=experiment_name) + if return_trajectory: + trajectories = cache_read(recent+"/trajectories.pkl") + else: + trajectories = [] + return stats, trajectories + stats = [] + steps = 0 + ep_start = 0 + resume_stats = saveload_model if resume_stats is None else resume_stats + + recent = None + if resume_stats: + stats, recent = load_time_series(experiment_name=experiment_name) + if recent is not None: + ep_start, steps = stats[-1]['Episode']+1, stats[-1]['Steps'] + + trajectories = [] + # include_metadata = len(inspect.getfullargspec(agent.train).args) >= 7 + break_outer = False + + with tqdm(total=num_episodes, disable=not verbose, file=sys.stdout, mininterval=int(num_episodes/100) if num_episodes>100 else None) as tq: + for i_episode in range(num_episodes): + if break_outer: + break + info_s = {} + if reset or i_episode > 0: + if seed is not None: + s, info_s = env.reset(seed=seed) + seed = None + else: + s, info_s = env.reset() + elif hasattr(env, "s"): # This is doing what, exactly? Perhaps save/load of agent? + s = env.s + elif hasattr(env, 'state'): + s = env.state + else: + s = env.model.s + # time = 0 + reward = [] + trajectory = Trajectory(time=[], state=[], action=[], reward=[], env_info=[]) + k = 0 # initial state k. + for _ in itertools.count(): + # policy is always temporal + a = agent.pi(s, k, info_s) # if temporal_policy else agent.pi(s) + k = k + 1 + sp, r, terminated, truncated, info_sp = env.step(a) + done = terminated or truncated + + if info_sp is not None and 'mask' in info_sp and info_sp['mask'].max() > 1: + print("bad") + + agent.train(s, a, r, sp, done, info_s, info_sp) + + if return_trajectory: + trajectory.time.append(np.asarray(info_s['time_seconds'] if 'time_seconds' in info_s else steps)) #np.asarray(time)) + trajectory.state.append(s) + trajectory.action.append(a) + trajectory.reward.append(np.asarray(r)) + trajectory.env_info.append(info_s) + + reward.append(r) + steps += 1 + # time += info_sp['dt'] if 'dt' in info_sp else 1 + # time += 1 + + if done or steps >= max_steps: + trajectory.state.append(sp) + trajectory.env_info.append(info_sp) + trajectory.time.append(np.asarray(info_sp['time_seconds'] if 'time_seconds' in info_s else steps)) + break_outer = steps >= max_steps + break + s = sp + info_s = info_sp + if return_trajectory: + try: + from irlc.ex04.control_environment import ControlEnvironment + if isinstance(env, ControlEnvironment): # TODO: this is too hacky. States/actions should be lists, and subsequent methods should stack. + trajectory = Trajectory(**{field: np.stack([np.asarray(x_) for x_ in getattr(trajectory, field)]) for field in fields}, env_info=trajectory.env_info) + # else: + # trajectory = Trajectory(**{field: np.stack([np.asarray(x_) for x_ in getattr(trajectory, field)]) for field in fields}, env_info=trajectory.env_info) + + except Exception as e: + pass + + trajectories.append(trajectory) + if (i_episode + 1) % log_interval == 0: + stats.append({"Episode": i_episode + ep_start, + "Accumulated Reward": sum(reward), + # "Average Reward": np.mean(reward), # Not sure we need this anymore. + "Length": len(reward), + "Steps": steps, # Useful for deep learning applications. This should be kept, or week 13 will have issues. + **agent.extra_stats()}) + + rate = int(num_episodes / 100) + if rate > 0 and i_episode % rate == 0: + tq.set_postfix(ordered_dict=OrderedDict(list(OrderedDict(stats[-1]).items())[:5])) if len(stats) > 0 else None + tq.update() + + sys.stderr.flush() + + if resume_stats and save_stats and recent is not None: + os.remove(recent+"/log.txt") + + if experiment_name is not None and save_stats: + path = log_time_series(experiment=experiment_name, list_obs=stats) + if return_trajectory: + cache_write(trajectories, path+"/trajectories.pkl") + + print(f"Training completed. Logging {experiment_name}: '{', '.join( stats[0].keys()) }'") + + for i, t in enumerate(trajectories): + from collections import defaultdict + nt = defaultdict(lambda: []) + if t.env_info is not None and t.env_info[1] is not None and "supersample" in t.env_info[1]: + for f in fields: + for k, ei in enumerate(t.env_info): + if 'supersample' not in ei: + continue + z = ei['supersample'].__getattribute__(f).T + if k == 0: + pass + else: + z = z[1:] + nt[f].append(z) + + for f in fields: + nt[f] = np.concatenate([z for z in nt[f]],axis=0) + traj2 = Trajectory(**nt, env_info=[]) + trajectories[i] = traj2 + + # for k, t in enumerate(stats): + # if k < len(trajectories): + # stats[k]['trajectory'] = trajectories[k] + # Turn this into a single episodes-list (refactor later) + return stats, trajectories + + +if __name__ == "__main__": + # Use the trajectories here. + from irlc.ex01.inventory_environment import InventoryEnvironment + env = InventoryEnvironment(N=10) + stats, traj = train(env, Agent(env)) + print(stats) + s = Stats(episode=1, episode_length=2, accumulated_reward=4, total_steps=4, trajectory=None, agent_stats={}) + print(s) diff --git a/irlc/ex01/bobs_friend.py b/irlc/ex01/bobs_friend.py new file mode 100644 index 0000000000000000000000000000000000000000..0d515d8e5e2f8186451fe37b03aa8b83ea7f66ed --- /dev/null +++ b/irlc/ex01/bobs_friend.py @@ -0,0 +1,59 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import gymnasium +import numpy as np +from gymnasium.spaces.discrete import Discrete +from irlc.ex01.agent import Agent, train + +class BobFriendEnvironment(gymnasium.Env): + def __init__(self, x0=20): + self.x0 = x0 + self.action_space = Discrete(2) # Possible actions {0, 1} + + def reset(self): + # TODO: 1 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + return self.s, {} + + def step(self, a): + # TODO: 9 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + return s_next, reward, terminated, False, {} + +class AlwaysAction_u0(Agent): + def pi(self, s, k, info=None): + """This agent should always take action u=0.""" + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + +class AlwaysAction_u1(Agent): + def pi(self, s, k, info=None): + """This agent should always take action u=1.""" + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + +if __name__ == "__main__": + # Part A: + env = BobFriendEnvironment() + x0, _ = env.reset() + print(f"Initial amount of money is x0 = {x0} (should be 20 kroner)") + print("Lets put it in the bank, we should end up in state x1=22 and get a reward of 2 kroner") + x1, reward, _, _, _ = env.step(0) + print("we got", x1, reward) + # Since we reset the environment, we should get the same result as before: + env.reset() + x1, reward, _, _, _ = env.step(0) + print("(once more) we got", x1, reward, "(should be the same as before)") + + env.reset() # We must call reset -- the environment has possibly been changed! + print("Lets lend it to our friend -- what happens will now be random") + x1, reward, _, _, _ = env.step(1) + print("we got", x1, reward) + + # Part B: + stats, _ = train(env, AlwaysAction_u0(env), num_episodes=1000) + average_u0 = np.mean([stat['Accumulated Reward'] for stat in stats]) + + stats, _ = train(env, AlwaysAction_u1(env), num_episodes=1000) + average_u1 = np.mean([stat['Accumulated Reward'] for stat in stats]) + print(f"Average reward while taking action u=0 was {average_u0} (should be 2)") + print(f"Average reward while taking action u=1 was {average_u1} (should be 4)") diff --git a/irlc/ex01/chess.py b/irlc/ex01/chess.py new file mode 100644 index 0000000000000000000000000000000000000000..935e1fc1c4c40d121bcf249eb00b17e11e618c82 --- /dev/null +++ b/irlc/ex01/chess.py @@ -0,0 +1,99 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""This file contains code for the Chess Tournament problem.""" +import numpy as np +from gymnasium.spaces.discrete import Discrete +from gymnasium import Env + +class ChessTournament(Env): + """The ChessTournament gymnasium-environment which simulate a chess tournament. + + In the problem, a chess tournament ends when a player wins two games in a row. The results + of each game are -1, 0, 1 corresponding to a loss, draw and win for player 1. See: + https://www.youtube.com/watch?v=5UQU1oBpAic + + To implement this, we define the step-function such that one episode of the environment corresponds to playing + a chess tournament to completion. Once the environment completes, it returns a reward of +1 if the player won + the tournament, and otherwise 0. + + Each step therefore corresponds to playing a single game in the tournament. + To implement this, we use a state corresponding to the sequence of games in the tournament: + + >>> self.s = [0, -1, 1, 0, 0, 1] + + In the self.step(action)-function, we ignore the action, simulate the outcome of a single game, + and append the outcome to self.s. We then compute whether the tournament has completed, and if so + a reward of 1 if we won. + """ + + def __init__(self, p_draw=3 / 4, p_win=2 / 3): + self.action_space = Discrete(1) + self.p_draw = p_draw + self.p_win = p_win + self.s = [] # A chess tournament is a sequence of won/lost games s = [0, -1, 1, 0, ...] + + def reset(self): + """Reset the tournament environment to begin to simulate a new tournament. + + After each episode is complete, this function will reset :python:`self.s` and return the current state s and an empty dictionary. + :return: + - s - The initial state (what is it?) + - info - An empty dictionary, ``{}`` + """ + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + return self.s, {} + + def step(self, action): + """Play a single game in the current tournament + + The variable action is required by gymnasium but it is not used since no (player) actions occur in this problem. + + The step-method should update `self.state` to be the next (new) state, compute the reward, and determine whether + the environment has terminated (:python:`done`). + + :param action: This input is required by gymnasium but it is not used in this case. + :return: A tuple of the form :python:`(new_state, reward, done, False, {})` + """ + game_outcome = None # should be -1, 0, or 1 depending on outcome of single game. + ## TODO: Oy veh, the following 7 lines below have been permuted. Uncomment, rearrange to the correct order and remove the error. + #------------------------------------------------------------------------------------------------------------------------------- + # else: + # else: + # game_outcome = 1 + # if np.random.rand() < self.p_win: + # game_outcome = -1 + # game_outcome = 0 + # if np.random.rand() < self.p_draw: + raise NotImplementedError("Compute game_outcome here") + self.s.append(game_outcome) + + #done = True if the tournament has ended otherwise false. Compute using s. + # TODO: 1 lines missing. + raise NotImplementedError("Compute 'done', whether the tournament has ended.") + # r = ... . Compute reward. Let r=1 if we won the tournament otherwise 0. + # TODO: 1 lines missing. + raise NotImplementedError("Compute the reward 'r' here.") + return self.s, r, done, False, {} + +def main(): + """The main method of the chess-game problem. + + This function will simulate T tournament games and estimate average win probability for player 1 as p_win (answer to riddle) and also + the average length. Note the later should be a 1-liner, but would require non-trivial computations to solve + analytically. Please see the :class:`gymnasium.Env` class for additional details. + """ + T = 5000 + from irlc import train, Agent + env = ChessTournament() + # Compute stats using the train function. Simulate the tournament for a total of T=10'000 episodes. + # TODO: 1 lines missing. + raise NotImplementedError("Compute stats here using train(env, ...). Use num_episodes.") + p_win = np.mean([st['Accumulated Reward'] for st in stats]) + avg_length = np.mean([st['Length'] for st in stats]) + + print("Agent: Estimated chance I won the tournament: ", p_win) + print("Agent: Average tournament length", avg_length) + + +if __name__ == "__main__": + main() diff --git a/irlc/ex01/inventory_environment.py b/irlc/ex01/inventory_environment.py new file mode 100644 index 0000000000000000000000000000000000000000..a4601596baf8e487d7048eb800c9df42a434a8f5 --- /dev/null +++ b/irlc/ex01/inventory_environment.py @@ -0,0 +1,71 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +from gymnasium.spaces.discrete import Discrete +from gymnasium import Env +from irlc.ex01.agent import Agent, train + +class InventoryEnvironment(Env): + def __init__(self, N=2): + self.N = N # planning horizon + self.action_space = Discrete(3) # Possible actions {0, 1, 2} + self.observation_space = Discrete(3) # Possible observations {0, 1, 2} + + def reset(self): + self.s = 0 # reset initial state x0=0 + self.k = 0 # reset time step k=0 + return self.s, {} # Return the state we reset to (and an empty dict) + + def step(self, a): + w = np.random.choice(3, p=(.1, .7, .2)) # Generate random disturbance + # TODO: 5 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + return s_next, reward, terminated, False, {} # return transition information + +class RandomAgent(Agent): + def pi(self, s, k, info=None): + """ Return action to take in state s at time step k """ + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + + +def simplified_train(env: Env, agent: Agent) -> float: + s, _ = env.reset() + J = 0 # Accumulated reward for this rollout + for k in range(1000): + ## TODO: Oy veh, the following 7 lines below have been permuted. Uncomment, rearrange to the correct order and remove the error. + #------------------------------------------------------------------------------------------------------------------------------- + # if terminated or truncated: + # sp, r, terminated, truncated, metadata = env.step(a) + # a = agent.pi(s, k) + # s = sp + # J += r + # agent.train(s, a, sp, r, terminated) + # break + raise NotImplementedError("Remove this exception after the above lines have been uncommented and rearranged.") + return J + +def run_inventory(): + env = InventoryEnvironment() + agent = RandomAgent(env) + stats, _ = train(env,agent,num_episodes=1,verbose=False) # Perform one rollout. + print("Accumulated reward of first episode", stats[0]['Accumulated Reward']) + # I recommend inspecting 'stats' in a debugger; why do you think it is a list of length 1? + + stats, _ = train(env, agent, num_episodes=1000,verbose=False) # do 1000 rollouts + avg_reward = np.mean([stat['Accumulated Reward'] for stat in stats]) + print("[RandomAgent class] Average cost of random policy J_pi_random(0)=", -avg_reward) + # Try to inspect stats again in a debugger here. How long is the list now? + + stats, _ = train(env, Agent(env), num_episodes=1000,verbose=False) # Perform 1000 rollouts using Agent class + avg_reward = np.mean([stat['Accumulated Reward'] for stat in stats]) + print("[Agent class] Average cost of random policy J_pi_random(0)=", -avg_reward) + + """ Second part: Using the simplified training method. I.e. do not use train() below. + You can find some pretty strong hints about what goes on in simplified_train in the lecture slides for today. """ + avg_reward_simplified_train = np.mean( [simplified_train(env, agent) for i in range(1000)]) + print("[simplified train] Average cost of random policy J_pi_random(0) =", -avg_reward_simplified_train) + + + +if __name__ == "__main__": + run_inventory() diff --git a/irlc/ex01/pacman_hardcoded.py b/irlc/ex01/pacman_hardcoded.py new file mode 100644 index 0000000000000000000000000000000000000000..62547565232907e67c339e90463d1c7a9cd6f121 --- /dev/null +++ b/irlc/ex01/pacman_hardcoded.py @@ -0,0 +1,60 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.pacman.pacman_environment import PacmanEnvironment +from irlc import Agent, train, savepdf + + +# Maze layouts can be specified using a string. +layout = """ +%%%%%%%%%% +%P.......% +%.%%%%%%.% +%.% %.% +%.% %.% +%.% %.% +%.% %.% +%.%%%%%%.% +%........% +%%%%%%%%%% +""" + +# This is our first agent. Note it inherits from the Agent class. Use <ctrl>+click in pycharm to navigate to code definitions -- +# this is a very useful habbit when you work with other peoples code in general, and object-oriented code in particular. +class GoAroundAgent(Agent): + def pi(self, x, k, info=None): + """ Collect all dots in the maze in the smallest amount of time. + This function should return an action, check the output of the code below to see what actions you can potentially + return. + Remember Pacman only have to solve this single maze, so don't make the function general. + + Hints: + - Insert a breakpoint in the function. Try to write self.env and self.env.action_space.actions in the interpreter. Where did self.env get set? + - Remember that k is the current step number. + - Ignore the info dictionary; you can probably also ignore the state x. + - The function should return a string (the actions are strings such as 'North') + """ + # TODO: 7 lines missing. + raise NotImplementedError("Implement function body") + return 'West' + +if __name__ == "__main__": + # Create an environment with the given layout. animate_movement is just for a nicer visualization. + env = PacmanEnvironment(layout_str=layout, render_mode='human') + # This creates a visualization (Note this makes the environment slower) which can help us see what Pacman does + # This create the GoAroundAgent-instance + agent = GoAroundAgent(env) + # Uncomment the following line to input actions instead of the agent using the keyboard: + # env, agent = interactive(env, agent) + s, info = env.reset() # Reset (and start) the environment + + savepdf("pacman_roundabout.pdf", env=env) # Saves a snapshot of the start layout + # The next two lines display two ways to get the available actions. The 'canonical' way using the + # env.action_space, and a way particular to Pacman by using the s.A() function on the state. + # You can read more about the functions in the state in project 1. + # print("Available actions at start:", env.action_space.actions) # This will list the available actions. + print("Alternative way of getting actions:", s.A()) # See also project description + + # Simulate the agent for one episode + stats, _ = train(env, agent, num_episodes=1) + # Print your obtained score. + print("Your obtained score was", stats[0]['Accumulated Reward']) + env.close() # When working with visualizations, call env.close() to close windows it may have opened. " diff --git a/irlc/ex02/__init__.py b/irlc/ex02/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..97bfecdc3daf77e00a1987adf3a7d3dba98c5aa4 --- /dev/null +++ b/irlc/ex02/__init__.py @@ -0,0 +1,2 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""This directory contains the exercises for week 2.""" diff --git a/irlc/ex02/__pycache__/__init__.cpython-311.pyc b/irlc/ex02/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4c0e4230c872e1b4fd8be12ef0dc7e0d21ce5642 Binary files /dev/null and b/irlc/ex02/__pycache__/__init__.cpython-311.pyc differ diff --git a/irlc/ex02/__pycache__/dp.cpython-311.pyc b/irlc/ex02/__pycache__/dp.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4a183932fe284d48d4142290e953c73af60cfb31 Binary files /dev/null and b/irlc/ex02/__pycache__/dp.cpython-311.pyc differ diff --git a/irlc/ex02/__pycache__/dp_model.cpython-311.pyc b/irlc/ex02/__pycache__/dp_model.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d9cd5bd2a1e51fd4f90d8072be63d1e1bea7167 Binary files /dev/null and b/irlc/ex02/__pycache__/dp_model.cpython-311.pyc differ diff --git a/irlc/ex02/__pycache__/graph_traversal.cpython-311.pyc b/irlc/ex02/__pycache__/graph_traversal.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..539851022997c8a7f7a92f9e87749c2a08229cad Binary files /dev/null and b/irlc/ex02/__pycache__/graph_traversal.cpython-311.pyc differ diff --git a/irlc/ex02/__pycache__/inventory.cpython-311.pyc b/irlc/ex02/__pycache__/inventory.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5f7d3211a68b3c18faebe51fd3aa2a340afe6df Binary files /dev/null and b/irlc/ex02/__pycache__/inventory.cpython-311.pyc differ diff --git a/irlc/ex02/dp.py b/irlc/ex02/dp.py new file mode 100644 index 0000000000000000000000000000000000000000..853d188f68d0127c1dfc228704825364868b5ed2 --- /dev/null +++ b/irlc/ex02/dp.py @@ -0,0 +1,71 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [Her24] Tue Herlau. Sequential decision making. (Freely available online), 2024. +""" +from irlc.ex02.graph_traversal import SmallGraphDP +from irlc.ex02.dp_model import DPModel + +def DP_stochastic(model: DPModel): + """ + Implement the stochastic DP algorithm. The implementation follows (Her24, Algorithm 1). + Once you are done, you should be able to call the function as: + + .. runblock:: pycon + + >>> from irlc.ex02.graph_traversal import SmallGraphDP + >>> from irlc.ex02.dp import DP_stochastic + >>> model = SmallGraphDP(t=5) # Instantiate the small graph with target node 5 + >>> J, pi = DP_stochastic(model) + >>> print(pi[0][2]) # Action taken in state ``x=2`` at time step ``k=0``. + + :param model: An instance of :class:`irlc.ex02.dp_model.DPModel` class. This represents the problem we wish to solve. + :return: + - ``J`` - A list of of cost function so that ``J[k][x]`` represents :math:`J_k(x)` + - ``pi`` - A list of dictionaries so that ``pi[k][x]`` represents :math:`\mu_k(x)` + """ + + """ + In case you run into problems, I recommend following the hints in (Her24, Subsection 6.2.1) and focus on the + case without a noise term; once it works, you can add the w-terms. When you don't loop over noise terms, just specify + them as w = None in env.f and env.g. + """ + N = model.N + J = [{} for _ in range(N + 1)] + pi = [{} for _ in range(N)] + J[N] = {x: model.gN(x) for x in model.S(model.N)} + for k in range(N-1, -1, -1): + for x in model.S(k): + """ + Update pi[k][x] and Jstar[k][x] using the general DP algorithm given in (Her24, Algorithm 1). + If you implement it using the pseudo-code, I recommend you define Q (from the algorithm) as a dictionary like the J-function such that + + > Q[u] = Q_u (for all u in model.A(x,k)) + + Then you find the u with the lowest value of Q_u, i.e. + + > umin = arg_min_u Q[u] + + (for help, google: `python find key in dictionary with minimum value'). + Then you can use this to update J[k][x] = Q_umin and pi[k][x] = umin. + """ + # TODO: 4 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + """ + After the above update it should be the case that: + + J[k][x] = J_k(x) + pi[k][x] = pi_k(x) + """ + return J, pi + + +if __name__ == "__main__": # Test dp on small graph given in (Her24, Subsection 6.2.1) + print("Testing the deterministic DP algorithm on the small graph environment") + model = SmallGraphDP(t=5) # Instantiate the small graph with target node 5 + J, pi = DP_stochastic(model) + # Print all optimal cost functions J_k(x_k) + for k in range(len(J)): + print(", ".join([f"J_{k}({i}) = {v:.1f}" for i, v in J[k].items()])) + print(f"Cost of shortest path when starting in node 2 is: {J[0][2]=} (and should be 4.5)") diff --git a/irlc/ex02/dp_agent.py b/irlc/ex02/dp_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..7e49efd69dacab5359d6fa208c5dce5b125f4ba5 --- /dev/null +++ b/irlc/ex02/dp_agent.py @@ -0,0 +1,44 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex01.agent import Agent +from irlc.ex02.dp import DP_stochastic +from irlc import train +import numpy as np + + +class DynamicalProgrammingAgent(Agent): + """ + This is an agent which plan using dynamical programming. + """ + def __init__(self, env, model=None): + super().__init__(env) + self.J, self.pi_ = DP_stochastic(model) + + def pi(self, s, k, info=None): + if k >= len(self.pi_): + raise Exception("k >= N; I have not planned this far!") + ## TODO: Half of each line of code in the following 1 lines have been replaced by garbage. Make it work and remove the error. + #---------------------------------------------------------------------------------------------------------------------------- + # action = se???????????? + raise NotImplementedError("Get the action according to the DP policy.") + return action + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): # Do nothing; this is DP so no learning takes place. + pass + + +def main(): + from irlc.ex01.inventory_environment import InventoryEnvironment + from irlc.ex02.inventory import InventoryDPModel + + env = InventoryEnvironment(N=3) + inventory_model = InventoryDPModel(N=3) + agent = DynamicalProgrammingAgent(env, model=inventory_model) + stats, _ = train(env, agent, num_episodes=5000) + + s, _ = env.reset() # Get initial state + Er = np.mean([stat['Accumulated Reward'] for stat in stats]) + print("Estimated reward using trained policy and MC rollouts", Er) + print("Reward as computed using DP", -agent.J[0][s]) + +if __name__ == "__main__": + main() diff --git a/irlc/ex02/dp_model.py b/irlc/ex02/dp_model.py new file mode 100644 index 0000000000000000000000000000000000000000..88dd27cf84b58b7865b613750336b8326dfb11e0 --- /dev/null +++ b/irlc/ex02/dp_model.py @@ -0,0 +1,185 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np + +class DPModel: + r""" The Dynamical Programming model class + + The purpose of this class is to translate a dynamical programming problem, defined by the equations, + + .. math:: + + x_{k+1} & = f_k(x_k, u_k, w_k) \\ + \text{cost} & = g_k(x_k, u_k, w_k) \\ + \text{terminal cost} & = g_N(x_N) \\ + \text{Noise disturbances:} \quad w_k & \sim P_W(w_k | x_k, u_k) \\ + \text{State/action spaces:} \quad & \mathcal A_k(x_k), \mathcal S_k + + into a single python object which we can then use for planning. + + .. Note:: + + This is the first time many of you encounter a class. If so, you might wonder why you can't just implement + the functions as usual, i.e. ``def f(x, k, ...):``, ``def g(x, k, ...):``, + as regular python function and just let that be it? + + The reason is that we want to pass all these function (which taken together represents a planning problem) + to planning methods such as the DP-algorithm (see the function :func:`~irlc.ex02.dp.DP_stochastic`) + all at once. + It is not very convenient to pass the functions one at a time -- instead we collect them into a class and simply call the function as + + >>> from irlc.ex02.inventory import InventoryDPModel + >>> from irlc.ex02.dp import DP_stochastic + >>> model = InventoryDPModel() # Intialize the model + >>> J, pi = DP_stochastic(model) # All functions are passed to DP_stochastic + + + + To actually use the model, you need to extend it and implement the methods. The basic recipe for this is something like:: + + class MyDPModel(DPModel): + def f(self, x, u, w, k): # Note the `self`-variable. You can use it to access class variables such as`self.N`. + return x + u - w # Just an example + def S(self, k): + return [0, 1, 2] # State space S_k = {0, 1, 2} + # Implement the other functions A, g, gN and Pw here. + + + You should take a look at :func:`~irlc.ex02.inventory.InventoryDPModel` for a concrete example. + Once the functions have been implemented, you can call them as: + + .. runblock:: pycon + + >>> from irlc.ex02.inventory import InventoryDPModel + >>> model = InventoryDPModel(N=5) # Plan on a horizon of 5 + >>> print("State space S_2", model.S(2)) + >>> model.f(x=1, u=2, w=1, k=0) # Just an example. You don't have to use named arguments, although it helps on readability. + >>> model.A(1, k=2) # Action space A_1(2), i.e. the actions available at time step k=1 in state 2. + + """ + def __init__(self, N): + """ + Called when the DP Model is initialized. By default, it simply stores the planning horizon ``N`` + + :param N: The planning horizon in the DP problem :math:`N` + """ + self.N = N # Store the planning horizon. + + def f(self, x, u, w, k: int): + """ + Implements the transition function :math:`x_{k+1} = f_k(x, u, w)` and returns the next state :math:`x_{k+1}` + + :param x: The state :math:`x_k` + :param u: The action taken :math:`u_k` + :param w: The random noise disturbance :math:`w_k` + :param k: The current time step :math:`k` + :return: The state the environment (deterministically) transitions to, i.e. :math:`x_{k+1}` + """ + raise NotImplementedError("Return f_k(x,u,w)") + + def g(self, x, u, w, k: int) -> float: + """ + Implements the cost function :math:`c = g_k(x, u, w)` and returns the cost :math:`c` + + :param x: The state :math:`x_k` + :param u: The action taken :math:`u_k` + :param w: The random noise disturbance :math:`w_k` + :param k: The current time step :math:`k` + :return: The cost (as a ``float``) incurred by the environment, i.e. :math:`g_k(x, u, w)` + """ + raise NotImplementedError("Return g_k(x,u,w)") + + def gN(self, x) -> float: + """ + Implements the terminal cost function :math:`c = g_N(x)` and returns the terminal cost :math:`c`. + + :param x: A state seen at the last time step :math:`x_N` + :return: The terminal cost (as a ``float``) incurred by the environment, i.e. :math:`g_N(x)` + """ + raise NotImplementedError("Return g_N(x)") + + def S(self, k: int): + """ + Computes the state space :math:`\mathcal S_k` at time step :math:`k`. + In other words, this function returns a set of all states the system can possibly be in at time step :math:`k`. + + .. Note:: + I think the cleanest implementation is one where this function returns a python ``set``. However, it won't matter + if the function returns a ``list`` or ``tuple`` instead. + + :param k: The current time step :math:`k` + :return: The state space (as a ``list`` or ``set``) available at time step ``k``, i.e. :math:`\mathcal S_k` + """ + raise NotImplementedError("Return state space as set S_k = {x_1, x_2, ...}") + + def A(self, x, k: int): + """ + Computes the action space :math:`\mathcal A_k(x)` at time step :math:`k` in state `x`. + + In other words, this function returns a ``set`` of all actions the agent can take in time step :math:`k`. + + .. Note:: + An example where the actions depend on the state is chess (in this case, the state is board position, and the actions are the legal moves) + + :param k: The current time step :math:`k` + :param x: The state we want to compute the actions in :math:`x_k` + :return: The action space (as a ``list`` or ``set``) available at time step ``k``, i.e. :math:`\mathcal A_k(x_k)` + """ + raise NotImplementedError("Return action space as set A(x_k) = {u_1, u_2, ...}") + + def Pw(self, x, u, k: int): + """ + Returns the random noise disturbances and their probability. In other words, this function implements the distribution: + + .. math:: + + P_k(w_k | x_k, u_k) + + To implement this distribution, we must keep track of both the possible values of the noise disturbances :math:`w_k` + as well as the (numerical) value of their probability :math:`p(w_k| ...)`. + + To do this, the function returns a dictionary of the form ``P = {w1: p_w1, w2: p_w2, ...}`` where + + - The keys ``w`` represents random noise disturbances + - the values ``P[w]`` represents their probability (i.e. a ``float``) + + This can hopefully be made more clear with the Inventory environment: + + .. runblock:: pycon + + >>> from irlc.ex02.inventory import InventoryDPModel + >>> model = InventoryDPModel(N=5) # Plan on a horizon of 5 + >>> print("Random noise disturbances in state x=1 using action u=0 is:", model.Pw(x=1, u=0, k=0)) + >>> for w, pw in model.Pw(x=1, u=0, k=0).items(): # Iterate and print: + ... print(f"p_k({w}|x, u) =", pw) + + + :param x: The state :math:`x_k` + :param u: The action taken :math:`u_k` + :param k: The current time step :math:`k` + :return: A dictionary representing the distribution of random noise disturbances :math:`P_k(w |x_k, u_k)` of the form ``{..., w_i: pw_i, ...}`` such that ``pw_i = P_k(w_i | x, u)`` + """ + # Compute and return the random noise disturbances here. + # As an example: + return {'w_dummy': 1/3, 42: 2/3} # P(w_k="w_dummy") = 1/3, P(w_k =42)=2/3. + + def w_rnd(self, x, u, k): + """ + This helper function computes generates a random noise disturbance using the function + :func:`irlc.ex02.dp_model.DPModel.Pw`, i.e. it returns a sample: + + .. math:: + w \sim P_k(x_k, u_k) + + This will be useful for simulating the model. + + .. Note:: + You don't have to implement or change this function. + + :param x: The state :math:`x_k` + :param u: The action taken :math:`u_k` + :param k: The current time step :math:`k` + :return: A random noise disturbance :math:`w` distributed as :math:`P_k(x_k, u_k)` + """ + pW = self.Pw(x, u, k) + w, pw = zip(*pW.items()) # seperate w and p(w) + return np.random.choice(a=w, p=pw) diff --git a/irlc/ex02/flower_store.py b/irlc/ex02/flower_store.py new file mode 100644 index 0000000000000000000000000000000000000000..35a4712bdd7f06688b8eaebc539e37b804e331ea --- /dev/null +++ b/irlc/ex02/flower_store.py @@ -0,0 +1,27 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex02.inventory import InventoryDPModel +from irlc.ex02.dp import DP_stochastic +import numpy as np + +# TODO: Code has been removed from here. +raise NotImplementedError("Insert your solution and remove this error.") + +def a_get_policy(N: int, c: float, x0 : int) -> int: + # TODO: Code has been removed from here. + raise NotImplementedError("Insert your solution and remove this error.") + return u + +def b_prob_one(N : int, x0 : int) -> float: + # TODO: Code has been removed from here. + raise NotImplementedError("Insert your solution and remove this error.") + return pr_empty + + +if __name__ == "__main__": + model = InventoryDPModel() + pi = [{s: 0 for s in model.S(k)} for k in range(model.N)] + x0 = 0 + c = 0.5 + N = 3 + print(f"a) The policy choice for {c=} is {a_get_policy(N, c,x0)} should be 1") + print(f"b) The probability of ending up with a single element in the inventory is {b_prob_one(N, x0)} and should be 0.492") diff --git a/irlc/ex02/graph_traversal.py b/irlc/ex02/graph_traversal.py new file mode 100644 index 0000000000000000000000000000000000000000..4fd25aabae0fd409d0051a734960577764871417 --- /dev/null +++ b/irlc/ex02/graph_traversal.py @@ -0,0 +1,67 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [Her24] Tue Herlau. Sequential decision making. (Freely available online), 2024. +""" +import numpy as np +from irlc.ex02.dp_model import DPModel + +r""" +Graph of shortest path problem of (Her24, Subsection 5.1.1) +""" +G222 = {(1, 2): 6, (1, 3): 5, (1, 4): 2, (1, 5): 2, + (2, 3): .5, (2, 4): 5, (2, 5): 7, + (3, 4): 1, (3, 5): 5, (4, 5): 3} + +def symG(G): + """ make a graph symmetric. I.e. if it contains edge (a,b) with cost z add edge (b,a) with cost c """ + G.update({(b, a): l for (a, b), l in G.items()}) +symG(G222) + +class SmallGraphDP(DPModel): + r""" Implement the small-graph example in (Her24, Subsection 5.1.1). t is the terminal node. """ + def __init__(self, t, G=None): + self.G = G.copy() if G is not None else G222.copy() + self.G[(t,t)] = 0 # make target vertex absorbing + self.t = t # target vertex in graph + self.nodes = {node for edge in self.G for node in edge} # set of all nodes + super(SmallGraphDP, self).__init__(N=len(self.nodes)-1) + + def f(self, x, u, w, k): + if (x,u) in self.G: + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + else: + raise Exception("Nodes are not connected") + + def g(self, x, u, w, k): + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + + def gN(self, x): + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + + def S(self, k): + return self.nodes + + def A(self, x, k): + return {j for (i,j) in self.G if i == x} + +def main(): + t = 5 # target node + model = SmallGraphDP(t=t) + x0 = 1 # starting node + k = 0 + w = 0 # irrelevant. + u = 2 # as an example. + print(f"{model.f(x0, u, w, k)=} (should be 2)") + print(f"{model.g(x0, u, w, k)=} (should be 6)") + print(f"{model.gN(x0)=} (should be np.inf)") + print(f"{model.S(k)=}", "(should be {1, 2, 3, 4, 5})") + print(f"{model.A(x0, k)=}", "(should be {2, 3, 4, 5})") + print("Run the tests to check your implementation.") + +if __name__ == '__main__': + main() diff --git a/irlc/ex02/inventory.py b/irlc/ex02/inventory.py new file mode 100644 index 0000000000000000000000000000000000000000..74c8eb8a8c8b2fb0580d6d2d6e71f3223f5f6940 --- /dev/null +++ b/irlc/ex02/inventory.py @@ -0,0 +1,44 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" +Implements the inventory-control problem from (Her24, Subsection 5.1.2). + +References: + [Her24] Tue Herlau. Sequential decision making. (Freely available online), 2024. +""" +from irlc.ex02.dp_model import DPModel +from irlc.ex02.dp import DP_stochastic + +class InventoryDPModel(DPModel): + def __init__(self, N=3): + super().__init__(N=N) + + def A(self, x, k): # Action space A_k(x) + return {0, 1, 2} + + def S(self, k): # State space S_k + return {0, 1, 2} + + def g(self, x, u, w, k): # Cost function g_k(x,u,w) + return u + (x + u - w) ** 2 + + def f(self, x, u, w, k): # Dynamics f_k(x,u,w) + return max(0, min(2, x + u - w )) + + def Pw(self, x, u, k): # Distribution over random disturbances + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + + def gN(self, x): + return 0 + +def main(): + inv = InventoryDPModel() + J,pi = DP_stochastic(inv) + print(f"Inventory control optimal policy/value functions") + for k in range(inv.N): + print(", ".join([f" J_{k}(x_{k}={i}) = {J[k][i]:.2f}" for i in inv.S(k)] ) ) + for k in range(inv.N): + print(", ".join([f"pi_{k}(x_{k}={i}) = {pi[k][i]}" for i in inv.S(k)] ) ) + +if __name__ == "__main__": + main() diff --git a/irlc/ex03/__init__.py b/irlc/ex03/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..01980cab4517ee3de7eb4bdc269e9927949d4ecb --- /dev/null +++ b/irlc/ex03/__init__.py @@ -0,0 +1,2 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""This directory contains the exercises for week 3.""" diff --git a/irlc/ex03/__pycache__/__init__.cpython-311.pyc b/irlc/ex03/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e5b4f16c809b017449ec5929b058171c20265711 Binary files /dev/null and b/irlc/ex03/__pycache__/__init__.cpython-311.pyc differ diff --git a/irlc/ex03/__pycache__/basic_pendulum.cpython-311.pyc b/irlc/ex03/__pycache__/basic_pendulum.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8aea6b565bd95c52b88ab61c24199f4f2fc19207 Binary files /dev/null and b/irlc/ex03/__pycache__/basic_pendulum.cpython-311.pyc differ diff --git a/irlc/ex03/__pycache__/control_cost.cpython-311.pyc b/irlc/ex03/__pycache__/control_cost.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fbd4fa86d1447a69fa82333373f93802475e67a2 Binary files /dev/null and b/irlc/ex03/__pycache__/control_cost.cpython-311.pyc differ diff --git a/irlc/ex03/__pycache__/control_model.cpython-311.pyc b/irlc/ex03/__pycache__/control_model.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..30de5a19f87513667a5d380652c9bb051c97ccc2 Binary files /dev/null and b/irlc/ex03/__pycache__/control_model.cpython-311.pyc differ diff --git a/irlc/ex03/basic_pendulum.py b/irlc/ex03/basic_pendulum.py new file mode 100644 index 0000000000000000000000000000000000000000..817e511daec05c0a99bb422443c2b04c72a7369b --- /dev/null +++ b/irlc/ex03/basic_pendulum.py @@ -0,0 +1,39 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import sympy as sym +import numpy as np +from irlc.ex03.control_model import ControlModel +from irlc.ex03.control_cost import SymbolicQRCost +from gymnasium.spaces import Box + +class BasicPendulumModel(ControlModel): + def sym_f(self, x, u, t=None): + g = 9.82 + l = 1 + m = 2 + theta_dot = x[1] # Parameterization: x = [theta, theta'] + theta_dot_dot = g / l * sym.sin(x[0]) + 1 / (m * l ** 2) * u[0] + return [theta_dot, theta_dot_dot] + + def get_cost(self) -> SymbolicQRCost: + return SymbolicQRCost(Q=np.eye(2), R=np.eye(1)) + + def u_bound(self) -> Box: + return Box(np.asarray([-10]), np.asarray([10])) + + def x0_bound(self) -> Box: + return Box(np.asarray( [np.pi, 0] ), np.asarray( [np.pi, 0])) + +if __name__ == "__main__": + p = BasicPendulumModel() + print(p) + + from irlc.ex04.discrete_control_model import DiscreteControlModel + model = BasicPendulumModel() + discrete_pendulum = DiscreteControlModel(model, dt=0.5) # Using a discretization time step: 0.5 seconds. + x0 = model.x0_bound().low # Get the initial state: x0 = [np.pi, 0]. + u0 = [0] # No action. Note the action must be a list. + x1 = discrete_pendulum.f(x0, u0) + print(x1) + print("Now, lets compute the Euler step manually to confirm") + x1_manual = x0 + 0.5 * model.f(x0, u0, 0) + print(x1_manual) diff --git a/irlc/ex03/control_cost.py b/irlc/ex03/control_cost.py new file mode 100644 index 0000000000000000000000000000000000000000..43d1c794a50fc8974d7b94c26b395ae58c98d538 --- /dev/null +++ b/irlc/ex03/control_cost.py @@ -0,0 +1,289 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [Her24] Tue Herlau. Sequential decision making. (Freely available online), 2024. +""" +import sympy as sym +import numpy as np + + +def mat(x): # Helper function. + return sym.Matrix(x) if x is not None else x + + +class SymbolicQRCost: + """ + This class represents the cost function for a continuous-time model. In the simulations, we are going to assume + that the cost function takes the form: + + .. math:: + \int_{t_0}^{t_F} c(x(t), u(t)) dt + c_F(x_F) + + And this class will specifically implement the two functions :math:`c` and :math:`c_F`. They will be assumed to have the quadratic form: + + .. math:: + c(x, u) & = \\frac{1}{2} x^T Q x + \\frac{1}{2} u^T R u + u^T H x + q^T x + r^T u + q_0, \\\\ + c_F(x_F) & = \\frac{1}{2} x_F^T Q_F x_F + q_F^T x_F + q_{0,F}. + + So what all of this boils down to is that the class just need to store a bunch of matrices and vectors. + + You can add and scale cost-functions + ********************************************************** + + A slightly smart thing about the cost functions are that you can add and scale them. The following provides an + example: + + .. runblock:: pycon + + >>> from irlc.ex03.control_cost import SymbolicQRCost + >>> import numpy as np + >>> cost1 = SymbolicQRCost(np.eye(2), np.zeros(1) ) # Set Q = I, R = 0 + >>> cost2 = SymbolicQRCost(np.ones((2,2)), np.zeros(1) ) # Set Q = 2x2 matrices of 1's, R = 0 + >>> print(cost1.Q) # Will be the identity matrix. + >>> cost = cost1 * 3 + cost2 * 2 + >>> print(cost.Q) # Will be 3 x I + 2 + + """ + + def __init__(self, Q, R, q=None, qc=None, r=None, H=None, QN=None, qN=None, qcN=None): + """ + The constructor can be used to manually create a cost function. You will rarely want to call the constructor + directly but instead use the helper methods (see class documentation). + What the class basically does is that it stores the input parameters as fields. In other words, you can access the quadratic + term of the cost function, :math:`\\frac{1}{2}x^T Q x`, as ``cost.Q``. + + :param Q: The matrix :math:`Q` + :param R: The matrix :math:`R` + :param q: The vector :math:`q` + :param qc: The constant :math:`q_0` + :param r: The vector :math:`r` + :param H: The matrix :math:`H` + :param QN: The terminal cost matrix :math:`Q_N` + :param qN: The terminal cost vector :math:`q_N` + :param qcN: The terminal cost constant :math:`q_{0,N}` + """ + + n = Q.shape[0] + d = R.shape[0] + self.Q = Q + self.R = R + self.q = np.zeros( (n,)) if q is None else q + self.qc = 0 if qc == None else qc + self.r = np.zeros( (d,)) if r is None else r + self.H = np.zeros((d,n)) if H is None else H + self.QN = np.zeros((n,n)) if QN is None else QN + self.qN = np.zeros((n,)) if qN is None else qN + self.qcN = 0 if qcN == None else qcN + self.flds = ('Q', 'R', 'q', 'qc', 'r', 'H', 'QN', 'qN', 'qcN') + self.flds_term = ('QN', 'qN', 'qcN') + + self.c_numpy = None + self.cF_numpy = None + + + @classmethod + def zero(cls, state_size, action_size): + """ + Creates an all-zero cost function, i.e. all terms :math:`Q`, :math:`R` are set to zer0. + + .. runblock:: pycon + + >>> from irlc.ex03.control_cost import SymbolicQRCost + >>> cost = SymbolicQRCost.zero(2, 1) + >>> cost.Q # 2x2 zero matrix + >>> cost.R # 1x1 zero matrix. + + :param state_size: Dimension of the state vector :math:`n` + :param action_size: Dimension of the action vector :math:`d` + :return: A ``SymbolicQRCost`` with all zero terms. + """ + + return cls(Q=np.zeros( (state_size,state_size)), R=np.zeros((action_size,action_size)) ) + + + def sym_c(self, x, u, t=None): + """ + Evaluate the (instantaneous) part of the function :math:`c(x,u, t)`. An example: + + .. runblock:: pycon + + >>> from irlc.ex03.control_cost import SymbolicQRCost + >>> import numpy as np + >>> cost = SymbolicQRCost(np.eye(2), np.eye(1)) # Set Q = I, R = 0 + >>> cost.sym_c(x = np.asarray([1,2]), u=np.asarray([0])) # should return 0.5 * x^T Q x = 0.5 * (1 + 4) + + :param x: The state :math:`x(t)` + :param u: The action :math:`u(t)` + :param t: The current time step :math:`t` (this will be ignored) + :return: A ``sympy`` symbolic expression corresponding to the instantaneous cost. + """ + u = sym.Matrix(u) + x = sym.Matrix(x) + c = 1 / 2 * (x.transpose() @ self.Q @ x) + 1 / 2 * (u.transpose() @ self.R @ u) + u.transpose() @ self.H @ x + sym.Matrix(self.q).transpose() @ x + sym.Matrix(self.r).transpose() @ u + sym.Matrix([[self.qc]]) + assert c.shape == (1,1) + return c[0,0] + + + def sym_cf(self, t0, tF, x0, xF): + """ + Evaluate the terminal (constant) term in the cost function :math:`c_F(t_0, t_F, x_0, x_F)`. An example: + + .. runblock:: pycon + + >>> from irlc.ex03.control_cost import SymbolicQRCost + >>> import numpy as np + >>> cost = SymbolicQRCost(np.eye(2), np.zeros(1), QN=np.eye(2)) # Set Q = I, R = 0 + >>> cost.sym_cf(0, 0, 0, xF=2*np.ones((2,))) # should return 0.5 * xF^T * xF = 0.5 * 8 + + :param t0: Starting time :math:`t_0` (not used) + :param tF: Stopping time :math:`t_F` (not used) + :param x0: Initial state :math:`x_0` (not used) + :param xF: Termi lanstate :math:`x_F` (**this one is used**) + :return: A ``sympy`` symbolic expression corresponding to the terminal cost. + """ + xF = sym.Matrix(xF) + c = 0.5 * xF.transpose() @ self.QN @ xF + xF.transpose() @ sym.Matrix(self.qN) + sym.Matrix([[self.qcN]]) + assert c.shape == (1,1) + return c[0,0] + + def discretize(self, dt): + """ + Discretize the cost function so it is suitable for a discrete control problem. See (Her24, Subsection 13.1.5) for more information. + + :param dt: The discretization time step :math:`\Delta` + :return: An :class:`~irlc.ex04.cost_discrete.DiscreteQRCost` instance corresponding to a discretized version of this cost function. + """ + from irlc.ex04.discrete_control_cost import DiscreteQRCost + return DiscreteQRCost(**{f: self.__getattribute__(f) * (1 if f in self.flds_term else dt) for f in self.flds} ) + + + def __add__(self, c): + return SymbolicQRCost(**{k: self.__dict__[k] + c.__dict__[k] for k in self.flds}) + + def __mul__(self, c): + return SymbolicQRCost(**{k: self.__dict__[k] * c for k in self.flds}) + + def __str__(self): + title = "Continuous-time cost function" + label1 = "Non-zero terms in c(x, u)" + label2 = "Non-zero terms in c_F(x)" + terms1 = [s for s in self.flds if s not in self.flds_term] + terms2 = self.flds_term + return _repr_cost(self, title, label1, label2, terms1, terms2) + + def goal_seeking_terminal_cost(self, xF_target, QF=None): + """ + Create a cost function which is minimal when the terminal state :math:`x_F` is equal to a goal state :math:`x_F^*`. + Concretely, it will return a cost function of the form + + .. math:: + c_F(x_F) = \\frac{1}{2} (x_F^* - x_F)^\\top Q_F (x_F^* - x_F) + + .. runblock:: pycon + + >>> from irlc.ex03.control_cost import SymbolicQRCost + >>> import numpy as np + >>> cost = SymbolicQRCost.zero(2, 1) + >>> cost += cost.goal_seeking_terminal_cost(xF_target=np.ones((2,))) + >>> print(cost.qN) + >>> print(cost) + + :param xF_target: Target state :math:`x_F^*` + :param QF: Cost matrix :math:`Q_F` + :return: A ``SymbolicQRCost`` object corresponding to the goal-seeking cost function + """ + if QF is None: + QF = np.eye(xF_target.size) + QF, qN, qcN = targ2matrices(xF_target, Q=QF) + return SymbolicQRCost(Q=self.Q*0, R=self.R*0, QN=QF, qN=qN, qcN=qcN) + + def goal_seeking_cost(self, x_target, Q=None): + """ + Create a cost function which is minimal when the state :math:`x` is equal to a goal state :math:`x^*`. + Concretely, it will return a cost function of the form + + .. math:: + c(x, u) = \\frac{1}{2} (x^* - x)^\\top Q (x^* - x) + + .. runblock:: pycon + + >>> from irlc.ex03.control_cost import SymbolicQRCost + >>> import numpy as np + >>> cost = SymbolicQRCost.zero(2, 1) + >>> cost += cost.goal_seeking_cost(x_target=np.ones((2,))) + >>> print(cost.q) + >>> print(cost) + + :param x_target: Target state :math:`x^*` + :param Q: Cost matrix :math:`Q` + :return: A ``SymbolicQRCost`` object corresponding to the goal-seeking cost function + """ + if Q is None: + Q = np.eye(x_target.size) + Q, q, qc = targ2matrices(x_target, Q=Q) + return SymbolicQRCost(Q=Q, R=self.R*0, q=q, qc=qc) + + def term(self, Q=None, R=None,r=None): + dd = {} + lc = locals() + for f in self.flds: + if f in lc and lc[f] is not None: + dd[f] = lc[f] + else: + dd[f] = self.__getattribute__(f)*0 + return SymbolicQRCost(**dd) + + @property + def state_size(self): + return self.Q.shape[0] + + @property + def action_size(self): + return self.R.shape[0] + + + +def _repr_cost(cost, title, label1, label2, terms1, terms2): + self = cost + def _get(flds, label): + d = {s: self.__dict__[s] for s in flds if np.sum(np.sum(self.__dict__[s] != 0)) != 0} + out = "" + if len(d) > 0: + # out = "" + out += f"> {label}:\n" + for s, m in d.items(): + mm = f"{m}" + if len(mm.splitlines()) > 1: + mm = "\n" + mm + out += f" * {s} = {mm}\n" + + return d, out + + nz_c, o1 = _get([s for s in terms1], label1) + out = "" + out += f"{title}:\n" + out += o1 + nz_term, o2 = _get(terms2, label2) + out += o2 + if len(nz_c) + len(nz_term) == 0: + print("All terms in the cost-function are zero.") + return out + + +def targ2matrices(t, Q=None): # Helper function + """ + Given a target vector :math:`t` and a matrix :math:`Q` this function returns cost-matrices suitable for implementing: + + .. math:: + \\frac{1}{2} * (x - t)^Q (x - t) = \\frac{1}{2} * x^T Q x + 1/2 * t^T * t - x * t + + :param t: + :param Q: + :return: + """ + n = t.size + if Q is None: + Q = np.eye(n) + + return Q, -1/2 * (Q @ t + t @ Q.T), 1/2 * t @ Q @ t diff --git a/irlc/ex03/control_model.py b/irlc/ex03/control_model.py new file mode 100644 index 0000000000000000000000000000000000000000..ed57c858c46b0ca694810407ac2854df6b6f0c59 --- /dev/null +++ b/irlc/ex03/control_model.py @@ -0,0 +1,423 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [Her24] Tue Herlau. Sequential decision making. (Freely available online), 2024. +""" +from collections import defaultdict +import tabulate +import sympy as sym +import numpy as np +import matplotlib.pyplot as plt +from gymnasium.spaces import Box +from irlc.ex03.control_cost import SymbolicQRCost + + +class ControlModel: + r"""Represents the continious time model of a control environment. + + + See (Her24, Section 13.2) for a top-level description. + + The model represents the physical system we are simulating and can be considered a control-equivalent of the + :class:`irlc.ex02.dp_model.DPModel`. The class must keep track of the following: + + .. math:: + \frac{dx}{dt} = f(x, u, t) + + And the cost-function which is defined as an integral + + .. math:: + c_F(t_0, t_F, x(t_0), x(t_F)) + \int_{t_0}^{t_F} c(t, x, u) dt + + as well as constraints and boundary conditions on :math:`x`, :math:`u` and the initial conditions state :math:`x(t_0)`. + this course, the cost function will always be quadratic, and can be accessed as ``model.get_cost``. + + If you want to implement your own model, the best approach is to start with an existing model and modify it for + your needs. The overall idea is that you implement the dynamics,``sym_f``, and the cost function ``get_cost``, + and optionally define bounds as needed. + """ + state_labels = None # Labels (as lists) used for visualizations. + action_labels = None # Labels (as lists) used for visualizations. + + def __init__(self): + """ + The cost must be an instance of :class:`irlc.ex04.cost_continuous.SymbolicQRCost`. + Bounds is a dictionary but otherwise optional; the model should give it a default value. + + :param cost: A quadratic cost function + :param dict bounds: A dictionary of boundary constraints. + """ + if self.state_labels is None: + self.state_labels = [f'x{i}' for i in range(self.state_size)] + if self.action_labels is None: + self.action_labels = [f'u{i}' for i in range(self.action_size)] + + t = sym.symbols("t") + x = sym.symbols(f"x0:{self.state_size}") + u = sym.symbols(f"u0:{self.action_size}") + try: + f = self.sym_f(x, u, t) + except Exception as e: + print("control_model.py> There is a problem with the way you have specified the dynamics. The function sym_f must accept lists as inputs") + raise e + if len(f) != len(x): + print("control_model.py> Your function ControlModel.sym_f must output a list of symbolic expressions.") + assert len(f) == len(x) + + self._f_np = sym.lambdify((x, u, t), self.sym_f(x, u, t)) + + def x0_bound(self) -> Box: + r"""The bound on the initial state :math:`\mathbf{x}_0`. + + The default bound is ``Box(0, 0, shape=(self.state_size,))``, i.e. :math:`\mathbf{x}_0 = 0`. + + :return: An appropriate gymnasium Box instance. + """ + return Box(0, 0, shape=(self.state_size,)) + + def xF_bound(self) -> Box: + r"""The bound on the terminal state :math:`\mathbf{x}_F`. + + :return: An appropriate gymnasium Box instance. + """ + return Box(-np.inf, np.inf, shape=(self.state_size,)) + + def x_bound(self) -> Box: + r"""The bound on all other states :math:`\mathbf{x}(t)`. + + :return: An appropriate gymnasium Box instance. + """ + return Box(-np.inf, np.inf, shape=(self.state_size,)) + + def u_bound(self) -> Box: + r"""The bound on the terminal state :math:`\mathbf{u}(t)`. + + :return: An appropriate gymnasium Box instance. + """ + return Box(-np.inf, np.inf, shape=(self.action_size,)) + + def t0_bound(self) -> Box: + r"""The bound on the initial time :math:`\mathbf{t}_0`. + + I have included this bound for completeness: In practice, there is no reason why you should change it + from the default bound is ``Box(0, 0, shape=(1,))``, i.e. :math:`\mathbf{t}_0 = 0`. + + :return: An appropriate gymnasium Box instance. + """ + return Box(0, 0, shape=(1,)) + + def tF_bound(self) -> Box: + r"""The bound on the final time :math:`\mathbf{t}_F`, i.e. when the environment terminates. + + :return: An appropriate gymnasium Box instance. + """ + return Box(-np.inf, np.inf, shape=(1,)) + + def get_cost(self) -> SymbolicQRCost: + raise NotImplementedError("When you implement the model, you must implement the get_cost() function.\nfor instance, use return SymbolicQRCost(Q=np.eye(n), R=np.eye(d))") + + def sym_f(self, x, u, t=None): + """ + The symbolic (``sympy``) version of the dynamics :math:`f(x, u, t)`. This is the main place where you specify + the dynamics when you build a new model. you should look at concrete implementations of models for specifics. + + :param x: A list of symbolic expressions ``['x0', 'x1', ..]`` corresponding to :math:`x` + :param u: A list of symbolic expressions ``['u0', 'u1', ..]`` corresponding to :math:`u` + :param t: A single symbolic expression corresponding to the time :math:`t` (seconds) + :return: A list of symbolic expressions ``[f0, f1, ...]`` of the same length as ``x`` where each element is a coordinate of :math:`f` + """ + raise NotImplementedError("Implement a function which return the environment dynamics f(x,u,t) as a sympy exression") + + def f(self, x, u, t=0) -> np.ndarray: + r"""Evaluate the dynamics. + + This function will evaluate the dynamics. In other words, it will evaluate :math:`\mathbf{f}` in the following expression: + + .. math:: + + \dot{\mathbf{x}} = \mathbf{f}(\mathbf{x}, \mathbf{u}, t) + + :param x: A numpy ndarray corresponding to the state + :param u: A numpy ndarray corresponding to the control + :param t: A :python:`float` corresponding to the time. + :return: The time derivative of the state, :math:`\mathbf{x}(t)`. + """ + return np.asarray( self._f_np(x, u, t) ) + + + def simulate(self, x0, u_fun, t0, tF, N_steps=1000, method='rk4'): + """ + Used to simulate the effect of a policy on the model. By default, it uses + Runge-Kutta 4 (RK4) with a fine discretization -- this is slow, but in nearly all cases exact. See (Her24, Algorithm 18) for more information. + + The input argument ``u_fun`` should be a function which returns a list or tuple with same dimension as + ``model.action_space``, :math:`d`. + + :param x0: The initial state of the simulation. Must be a list of floats of same dimension as ``env.observation_space``, :math:`n`. + :param u_fun: Can be either: + - Either a policy function that can be called as ``u_fun(x, t)`` and returns an action ``u`` in the ``action_space`` + - A single action (i.e. a list of floats of same length as the action space). The model will be simulated with a constant action in this case. + :param float t0: Starting time :math:`t_0` + :param float tF: Stopping time :math:`t_F`; the model will be simulated for :math:`t_F - t_0` seconds + :param int N_steps: Steps :math:`N` in the RK4 simulation + :param str method: Simulation method. Either ``'rk4'`` (default) or ``'euler'`` + :return: + - xs - A numpy ``ndarray`` of dimension :math:`(N+1)\\times n` containing the observations :math:`x` + - us - A numpy ``ndarray`` of dimension :math:`(N+1)\\times d` containing the actions :math:`u` + - ts - A numpy ``ndarray`` of dimension :math:`(N+1)` containing the corresponding times :math:`t` (seconds) + """ + + u_fun = ensure_policy(u_fun) + tt = np.linspace(t0, tF, N_steps+1) # Time grid t_k = tt[k] between t0 and tF. + xs = [ np.asarray(x0) ] + us = [ u_fun(x0, t0 )] + for k in range(N_steps): + Delta = tt[k+1] - tt[k] + tn = tt[k] + xn = xs[k] + un = us[k] # ensure the action u is a vector. + unp = u_fun(xn, tn + Delta) + if method == 'rk4': + """ Implementation of RK4 here. See: (Her24, Algorithm 18) """ + k1 = np.asarray(self.f(xn, un, tn)) + k2 = np.asarray(self.f(xn + Delta * k1/2, u_fun(xn, tn+Delta/2), tn+Delta/2)) + k3 = np.asarray(self.f(xn + Delta * k2/2, u_fun(xn, tn+Delta/2), tn+Delta/2)) + k4 = np.asarray(self.f(xn + Delta * k3, u_fun(xn, tn + Delta), tn+Delta)) + xnp = xn + 1/6 * Delta * (k1 + 2*k2 + 2*k3 + k4) + elif method == 'euler': + xnp = xn + Delta * np.asarray(self.f(xn, un, tn)) + else: + raise Exception("Bad integration method", method) + xs.append(xnp) + us.append(unp) + xs = np.stack(xs, axis=0) + us = np.stack(us, axis=0) + return xs, us, tt + + @property + def state_size(self): + """ + This field represents the dimensionality of the state-vector :math:`n`. Use it as ``model.state_size`` + :return: Dimensionality of the state vector :math:`x` + """ + return self.get_cost().state_size + # return len(list(self.bounds['x_low'])) + + @property + def action_size(self): + """ + This field represents the dimensionality of the action-vector :math:`d`. Use it as ``model.action_size`` + :return: Dimensionality of the action vector :math:`u` + """ + return self.get_cost().action_size + # return len(list(self.bounds['u_low'])) + + def render(self, x, render_mode="human"): + """ + Responsible for rendering the state. You don't have to worry about this function. + + :param x: State to render + :param str render_mode: Rendering mode. Select ``"human"`` for a visualization. + :return: Either none or a ``ndarray`` for plotting. + """ + raise NotImplementedError() + + def close(self): + pass + + def phi_x(self, x : list) -> list: + r"""Coordinate transformation of the state when the model is discretized. + + This function specifies the coordinate transformation :math:`x_k = \Phi_x(x(t_k))` which is applied to the environment when it is + discretized. It should accept a list of symbols, corresponding to :math:`x`, and return a new list + of symbols corresponding to the (discrete) coordinates. + + :param x: A list of symbols ``[x0, x1, ..., xn]`` corresponding to :math:`\mathbf{x}(t)` + :return: A new list of symbols corresponding to the discrete coordinates :math:`\mathbf{x}_k`. + """ + return x + + def phi_x_inv(self, x: list) -> list: + r"""Inverse of coordinate transformation for the state. + + This function should specify the inverse of the coordinate transformation :math:`\Phi_x`, i.e. :math:`\Phi_x^{-1}`. + In other words, it has to map from the discrete coordinates to the continuous-time coordinates: :math:`x(t) = \Phi_x^{-1}(x_k)`. + + :param x: A list of symbols ``[x0, x1, ..., xn]`` corresponding to :math:`\mathbf{x}_k` + :return: A new list of symbols corresponding to the continuous-time coordinates :math:`\mathbf{x}(t)`. + """ + return x + + def phi_u(self, u: list) -> list: + r"""Coordinate transformation of the action when the model is discretized. + + This function specifies the coordinate transformation :math:`x_k = \Phi_x(x(t_k))` which is applied to the environment when it is + discretized. It should accept a list of symbols, corresponding to :math:`x`, and return a new list + of symbols corresponding to the (discrete) coordinates. + + :param x: A list of symbols ``[x0, x1, ..., xn]`` corresponding to :math:`\mathbf{x}(t)` + :return: A new list of symbols corresponding to the discrete coordinates :math:`\mathbf{x}_k`. + """ + return u + + def phi_u_inv(self, u: list) -> list: + r"""Inverse of coordinate transformation for the action. + + This function should specify the inverse of the coordinate transformation :math:`\Phi_u`, i.e. :math:`\Phi_u^{-1}`. + In other words, it has to map from the discrete coordinates to the continuous-time coordinates: :math:`u(t) = \Phi_u^{-1}(u_k)`. + + :param x: A list of symbols ``[u0, u1, ..., ud]`` corresponding to :math:`\mathbf{u}_k` + :return: A new list of symbols corresponding to the continuous-time coordinates :math:`\mathbf{u}(t)`. + """ + return u + + def __str__(self): + """ + Return a string representation of the model. This is a potentially helpful way to summarize the content of the + model. You can use it as: + + .. runblock:: pycon + + >>> from irlc.ex04.model_pendulum import SinCosPendulumModel + >>> model = SinCosPendulumModel() + >>> print(model) + + :return: A string containing the details of the model. + """ + split = "-"*20 + s = [f"{self.__class__}"] + ['='*50] + s += ["Dynamics:", split] + t = sym.symbols("t") + x = sym.symbols(f"x0:{self.state_size}") + u = sym.symbols(f"u0:{self.action_size}") + + s += [typeset_eq(x, u, self.sym_f(x, u, t) )] + + s += ["Cost:", split, str(self.get_cost())] + + dd = defaultdict(list) + bounds = [ ('x', self.x_bound()), ('x0', self.x0_bound()), ('xF', self.xF_bound()), + ('u', self.u_bound()), + ('t0', self.t0_bound()), ('tF', self.tF_bound())] + + for v, box in bounds: + if (box.low == -np.inf).all() and (box.high == np.inf).all(): + continue + dd['low'].append(box.low_repr) + dd['variable'].append("<= " + v + " <=") + dd['high'].append(box.high_repr) + + if len(dd) > 0: + s += ["Bounds:", split] + s += [tabulate.tabulate(dd, headers='keys')] + else: + s += ['No bounds are applied to the x and u-variables.'] + return "\n".join(s) + + +def symv(s, n): + """ + Returns a vector of symbolic functions. For instance if s='x' and n=3 then it will return + [x0,x1,x2] + where x0,..,x2 are symbolic variables. + """ + return sym.symbols(" ".join(["%s%i," % (s, i) for i in range(n)])) + +def ensure_policy(u): + """ + Ensure u corresponds to a policy function with input arguments u(x, t) + """ + if callable(u): + return lambda x, t: np.asarray(u(x,t)).reshape((-1,)) + else: + return lambda x, t: np.asarray(u).reshape((-1,)) + +def plot_trajectory(x_res, tt, lt='k-', ax=None, labels=None, legend=None): + M = x_res.shape[1] + if labels is None: + labels = [f"x_{i}" for i in range(M)] + + if ax is None: + if M == 2: + a = 234 + if M == 3: + r = 1 + c = 3 + else: + r = 2 if M > 1 else 1 + c = (M + 1) // 2 + + H = 2*r if r > 1 else 3 + W = 6*c + # if M == 2: + # W = 12 + f, ax = plt.subplots(r,c, figsize=(W,H)) + if M == 1: + ax = np.asarray([ax]) + print(M,r,c) + + for i in range(M): + if len(ax) <= i: + print("issue!") + + a = ax.flat[i] + a.plot(tt, x_res[:, i], lt, label=legend) + + a.set_xlabel("Time/seconds") + a.set_ylabel(labels[i]) + # a.set_title(labels[i]) + a.grid(True) + if legend is not None and i == 0: + a.legend() + # if i == M: + plt.tight_layout() + return ax + +def make_space_above(axes, topmargin=1.0): + """ increase figure size to make topmargin (in inches) space for + titles, without changing the axes sizes""" + fig = axes.flatten()[0].figure + s = fig.subplotpars + w, h = fig.get_size_inches() + + figh = h - (1-s.top)*h + topmargin + fig.subplots_adjust(bottom=s.bottom*h/figh, top=1-topmargin/figh) + fig.set_figheight(figh) + +def typeset_eq(x, u, f): + def ascii_vector(ls): + ml = max(map(len, ls)) + ls = [" " * (ml - len(s)) + s for s in ls] + ls = ["[" + s + "]" for s in ls] + return "\n".join(ls) + + v = [str(z) for z in f] + + def cstack(ls: list): + # ls = [l.splitlines() for l in ls] + height = max([len(l) for l in ls]) + widths = [len(l[0]) for l in ls] + + for k in range(len(ls)): + missing2 = (height - len(ls[k])) // 2 + missing1 = (height - len(ls[k]) - missing2) + tpad = [" " * widths[k]] * missing1 + bpad = [" " * widths[k]] * missing2 + ls[k] = tpad + ls[k] + bpad + + r = [""] * len(ls[0]) + for w in range(len(ls)): + for h in range(len(ls[0])): + r[h] += ls[w][h] + + return r + + xx = [str(x) for x in x] + uu = [str(u) for u in u] + xx = ascii_vector(xx).splitlines() + uu = ascii_vector(uu).splitlines() + cm = cstack([xx, [", "], uu]) + eq = cstack([["f("], cm, [")"]]) + eq = cstack([[" "], eq, [" = "], ascii_vector(v).splitlines()]) + return "\n".join(eq) diff --git a/irlc/ex03/inventory_evaluation.py b/irlc/ex03/inventory_evaluation.py new file mode 100644 index 0000000000000000000000000000000000000000..c5d7eda4076d4dde269bb4b8ad7bd4c4e4c0d6f2 --- /dev/null +++ b/irlc/ex03/inventory_evaluation.py @@ -0,0 +1,26 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex02.inventory import InventoryDPModel + +def a_expected_items_next_day(x : int, u : int) -> float: + model = InventoryDPModel() + expected_number_of_items = None + # TODO: Code has been removed from here. + raise NotImplementedError("Insert your solution and remove this error.") + return expected_number_of_items + + +def b_evaluate_policy(pi : list, x0 : int) -> float: + # TODO: Code has been removed from here. + raise NotImplementedError("Insert your solution and remove this error.") + return J_pi_x0 + +if __name__ == "__main__": + model = InventoryDPModel() + # Create a policy that always buy an item if the inventory is empty. + pi = [{s: 1 if s == 0 else 0 for s in model.S(k)} for k in range(model.N)] + x = 0 + u = 1 + x0 = 1 + a_expected_items_next_day(x=0, u=1) + print(f"Given inventory is {x=} and we buy {u=}, the expected items on day k=1 is {a_expected_items_next_day(x, u)} and should be 0.1") + print(f"Evaluation of policy is {b_evaluate_policy(pi, x0)} and should be 2.7") diff --git a/irlc/ex03/kuramoto.py b/irlc/ex03/kuramoto.py new file mode 100644 index 0000000000000000000000000000000000000000..e20844efe1ed2359c423df0cca48094993258fa7 --- /dev/null +++ b/irlc/ex03/kuramoto.py @@ -0,0 +1,123 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [Her24] Tue Herlau. Sequential decision making. (Freely available online), 2024. +""" +import sympy as sym +from irlc.ex03.control_model import ControlModel +from irlc.ex03.control_cost import SymbolicQRCost +import numpy as np +from irlc import savepdf +from gymnasium.spaces import Box + + +class KuramotoModel(ControlModel): + r""" + The Kuramoto model. It implements the following dynamics: + + .. math:: + + \dot{x}(t) = u(t) +\cos(x(t)) + + I.e. the state and control variables are both one-dimensional. The cost function is simply: + + .. math:: + + c(t) = \frac{1}{2}x(t)^2 + \frac{1}{2}u(t)^2 + + This is a QR cost with :math:`Q=R=1`. + """ + def u_bound(self) -> Box: + return Box(-2, 2, shape=(1,)) + + def x0_bound(self) -> Box: + return Box(0, 0, shape=(1,)) + + def get_cost(self) -> SymbolicQRCost: + """ + Create a cost-object. The code defines a quadratic cost (with the given matrices) and allows easy computation + of derivatives, etc. There are automatic ways to discretize the cost so you don't have to bother with that. + See the online documentation for further details. + """ + return SymbolicQRCost(Q=np.zeros((1, 1)), R=np.ones((1,1))) + + def sym_f(self, x: list, u: list, t=None): + r""" Return a symbolic expression representing the Kuramoto model. + The inputs x, u are themselves *lists* of symbolic variables (insert breakpoint and check their value). + you have to use them to create a symbolic object representing f, and return it as a list. That is, you are going to return + + .. codeblock:: python + + return [f_val] + + where ``f_val`` is the symbolic expression corresponding to the dynamics, i.e. :math:`u(t) + \cos( x(t))`. + Note you can use trigonometric functions like ``sym.cos``. + """ + # TODO: 1 lines missing. + raise NotImplementedError("Implement symbolic expression as a singleton list here") + # define the symbolic expression + return symbolic_f_list + + +def f(x, u): + """ Implement the kuramoto osscilator model's dynamics, i.e. f such that dx/dt = f(x,u). + The answer should be returned as a singleton list. """ + cmodel = KuramotoModel() + # TODO: 1 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + # Use the ContiniousKuramotoModel to compute f(x,u). If in doubt, insert a breakpoint and let pycharms autocomplete + # guide you. See my video to Exercise 2 for how to use the debugger. Don't forget to specify t (for instance t=0). + # Note that sympys error messages can be a bit unforgiving. + return f_value + +def rk4_simulate(x0, u, t0, tF, N=1000): + """ + Implement the RK4 algorithm (Her24, Algorithm 18). + In this function, x0 and u are constant numpy ndarrays. I.e. u is not a function, which simplify the RK4 + algorithm a bit. + + The function you want to integrate, f, is already defined above. You can likewise assume f is not a function of + time. t0 and tF play the same role as in the algorithm. + + The function should return a numpy ndarray xs of dimension (N,) (containing all the x-values) and a numpy ndarray + tt containing the corresponding time points. + + Hints: + * Call f as in f(x, u). You defined f earlier in this exercise. + """ + tt = np.linspace(t0, tF, N+1) # Time grid t_k = tt[k] between t0 and tF. + xs = [ x0 ] + f(x0, u) # This is how you can call f. + for k in range(N): + x_next = None # Obtain x_next = x_{k+1} using a single RK4 step. + # Remember to insert breakpoints and use the console to examine what the various variables are. + # TODO: 7 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + xs.append(x_next) + xs = np.stack(xs, axis=0) + return xs, tt + +if __name__ == "__main__": + # Create a symbolic model corresponding to the Kuramoto model: + # Evaluate the dynamics dx / dt = f(x, u). + + print("Value of f(x,u) in x=2, u=0.3", f([2], [0.3])) + print("Value of f(x,u) in x=0, u=1", f([0], [1])) + + cmodel = KuramotoModel() + print(cmodel) + x0 = cmodel.x0_bound().low # Get the starting state x0. We exploit that the bound on x0 is an equality constraint. + u = 1.3 + xs, ts = rk4_simulate(x0, [u], t0=0, tF=20, N=100) + xs_true, us_true, ts_true = cmodel.simulate(x0, u_fun=u, t0=0, tF=20, N_steps=100) + """You should generally use cmodel.simulate(...) to simulate the environment. Note that u_fun in the simulate + function can be set to a constant. Use this compute numpy ndarrays corresponding to the time, x and u values. + """ + # Plot the exact simulation of the environment + import matplotlib.pyplot as plt + plt.plot(ts_true, xs_true, 'k.-', label='RK4 state sequence x(t) (using model.simulate)') + plt.plot(ts, xs, 'r-', label='RK4 state sequence x(t) (using your code)') + plt.legend() + #savepdf('kuramoto_rk4') + plt.show(block=False) diff --git a/irlc/ex03/toy_2d_control.py b/irlc/ex03/toy_2d_control.py new file mode 100644 index 0000000000000000000000000000000000000000..187dd0369b3f8077cce422d58ec975a79d889144 --- /dev/null +++ b/irlc/ex03/toy_2d_control.py @@ -0,0 +1,23 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import sympy as sym +from irlc.ex03.control_model import ControlModel +from irlc.ex03.control_cost import SymbolicQRCost +import numpy as np + +class Toy2DControl(ControlModel): + def get_cost(self): + # You get the cost-function for free because it can be anything as far as this problem is concerned. + return SymbolicQRCost(Q=np.eye(2), R=np.eye(1)) + + # TODO: 2 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + +def toy_simulation(u0 : float, T : float) -> float: + # TODO: 4 lines missing. + raise NotImplementedError("Create a Toy2dControl instance and use model.simulate(..) to get the final state.") + return wT + +if __name__ == "__main__": + x0 = np.asarray([np.pi/2, 0]) + wT = toy_simulation(u0=0.4, T=5) + print(f"Starting in x0=[pi/2, 0], after T=5 seconds the system is an an angle {wT=} (should be 1.265)") diff --git a/irlc/ex04/__init__.py b/irlc/ex04/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d084853c8794e0bebd41758f6e27fbf152bc134f --- /dev/null +++ b/irlc/ex04/__init__.py @@ -0,0 +1,20 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""This directory contains the exercises for week 4.""" + +speech = """ +Fate has ordained that the men who went to the moon to explore in peace will stay on the moon to rest in peace. + +These brave men, Neil Armstrong and Edwin Aldrin, know that there is no hope for their recovery. But they also know that there is hope for mankind in their sacrifice. + +These two men are laying down their lives in mankind’s most noble goal: the search for truth and understanding. + +They will be mourned by their families and friends; they will be mourned by their nation; they will be mourned by the people of the world; they will be mourned by a Mother Earth that dared send two of her sons into the unknown. + +In their exploration, they stirred the people of the world to feel as one; in their sacrifice, they bind more tightly the brotherhood of man. + +In ancient days, men looked at stars and saw their heroes in the constellations. In modern times, we do much the same, but our heroes are epic men of flesh and blood. + +Others will follow, and surely find their way home. Man’s search will not be denied. But these men were the first, and they will remain the foremost in our hearts. + +For every human being who looks up at the moon in the nights to come will know that there is some corner of another world that is forever mankind. +""" diff --git a/irlc/ex04/__pycache__/__init__.cpython-311.pyc b/irlc/ex04/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3164efb4748bc0ed4f1b2e1ee9f97c20e21d294b Binary files /dev/null and b/irlc/ex04/__pycache__/__init__.cpython-311.pyc differ diff --git a/irlc/ex04/__pycache__/control_environment.cpython-311.pyc b/irlc/ex04/__pycache__/control_environment.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d907d0d5b866124103b4679a0addcac96838cba2 Binary files /dev/null and b/irlc/ex04/__pycache__/control_environment.cpython-311.pyc differ diff --git a/irlc/ex04/__pycache__/discrete_control_cost.cpython-311.pyc b/irlc/ex04/__pycache__/discrete_control_cost.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a34b7db9927788c8e5d7bb03c37ab974e150ae1d Binary files /dev/null and b/irlc/ex04/__pycache__/discrete_control_cost.cpython-311.pyc differ diff --git a/irlc/ex04/__pycache__/discrete_control_model.cpython-311.pyc b/irlc/ex04/__pycache__/discrete_control_model.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5c4477150f85d1c7b05871d85cef6908932f7aee Binary files /dev/null and b/irlc/ex04/__pycache__/discrete_control_model.cpython-311.pyc differ diff --git a/irlc/ex04/__pycache__/model_linear_quadratic.cpython-311.pyc b/irlc/ex04/__pycache__/model_linear_quadratic.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..421272b3d5c7dc0c25f34823cd4b2c1862685ce6 Binary files /dev/null and b/irlc/ex04/__pycache__/model_linear_quadratic.cpython-311.pyc differ diff --git a/irlc/ex04/__pycache__/model_pendulum.cpython-311.pyc b/irlc/ex04/__pycache__/model_pendulum.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fdff94ba6b6693e07f04de94d8bd44222e68cf01 Binary files /dev/null and b/irlc/ex04/__pycache__/model_pendulum.cpython-311.pyc differ diff --git a/irlc/ex04/control_environment.py b/irlc/ex04/control_environment.py new file mode 100644 index 0000000000000000000000000000000000000000..ad44fe94c41231d206d591a981b9b91199f4ee6a --- /dev/null +++ b/irlc/ex04/control_environment.py @@ -0,0 +1,171 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import gymnasium as gym +import numpy as np +from irlc.ex03.control_model import ensure_policy +from irlc.ex04.discrete_control_model import DiscreteControlModel + + +class ControlEnvironment(gym.Env): + """ + Helper class to convert a discretized model into an environment. + See the ``__init__`` function for how to create a new environment using this class. Once an environment has been + created, you can use it as any other gym environment: + + .. runblock:: pycon + + >>> from irlc.ex04.model_pendulum import GymSinCosPendulumEnvironment + >>> env = GymSinCosPendulumEnvironment(Tmax=4) # Specify we want it to run for a maximum of 4 seconds + >>> env.reset() # Reset both the time and state variable + >>> u = env.action_space.sample() + >>> next_state, cost, done, truncated, info = env.step(u) + >>> print("Current state: ", env.state) + >>> print("Current time", env.time) + + In this example, tell the environment to terminate after 4 seconds using ``Tmax`` (after which ``done=True``) + + .. Note:: + The ``step``-method will use the (nearly exact) RK4 method to integrate the enviorent over a timespan of ``dt``, + and **not** use the approximate ``model.f(x_k,u_k, k)``-method in the discrete environment which is based on + Euler discretization. + This is the correct behavior since we want the environment to reflect what happens in the real world and not + our apprixmation method. + """ + metadata = { + 'render.modes': ['human', 'rgb_array'], + 'video.frames_per_second': 30 + } + action_space = None + observation_space = None + + def __init__(self, discrete_model: DiscreteControlModel, Tmax=None, supersample_trajectory=False, render_mode=None): + """ + Creates a new instance. You should use this in conjunction with a discrete model to build a new class. An example: + + .. runblock:: pycon + + >>> from irlc.ex04.model_pendulum import DiscreteSinCosPendulumModel + >>> from irlc.ex04.control_environment import ControlEnvironment + >>> from gymnasium.spaces import Box + >>> import numpy as np + >>> class MyGymSinCosEnvironment(ControlEnvironment): + ... def __init__(self, Tmax=5): + ... discrete_model = DiscreteSinCosPendulumModel() + ... self.action_space = Box(low=-np.inf, high=np.inf, shape=(1,), dtype=np.float64) + ... self.observation_space = Box(low=-np.inf, high=np.inf, shape=(3,), dtype=np.float64) + ... super().__init__(discrete_model, Tmax=Tmax) + >>> + >>> env = MyGymSinCosEnvironment() + >>> env.reset() + >>> env.step(env.action_space.sample()) + + :param discrete_model: The discrete model the environment is based on + :param Tmax: Time in seconds until the environment terminates (``step`` returns ``done=True``) + :param supersample_trajectory: Used to create nicer (smooth) trajectories. Don't worry about it. + :param render_mode: If ``human`` the environment will be rendered (inherited from ``Env``) + """ + self.dt = discrete_model.dt # Discretization time + self.state = None # the current state + self.time = 0 # Current global time index + self.discrete_model = discrete_model + self.Tmax = Tmax + + # Try to guess action/observation spaces unless they are already defined. + if self.observation_space is None: + self.observation_space = gym.spaces.Box(-np.inf, np.inf, shape=(discrete_model.state_size,) ) + + if self.action_space is None: + u_bound = self.discrete_model.continuous_model.u_bound() + self.action_space = gym.spaces.Box(low=np.asarray(self.discrete_model.phi_u(u_bound.low)), + high=np.asarray(self.discrete_model.phi_u(u_bound.high)), + dtype=np.float64, + ) + self.state_labels = discrete_model.state_labels + self.action_labels = discrete_model.action_labels + self.supersample_trajectory = supersample_trajectory + self.render_mode = render_mode + + + def step(self, u): + """ + This works similar to the gym ``Env.step``-function. ``u`` is an action in the action-space, + and the environment will then assume we (constantly) apply the action ``u`` from the current time step, :math:`t_k`, until + the next time step :math:`t_{k+1} = t_k + \Delta`, where :math:`\Delta` is equal to ``env.model.dt``. + + During this period, the next state is computed using the relatively exact RK4 simulation, and the incurred cost will be + computed using Riemann integration. + + .. math:: + \int_{t_k}^{t_k+\Delta} c(x(t), u(t)) dt + + .. Note:: + The gym environment requires that we return a cost. The reward will therefore be equal to minus the (integral) of the cost function. + + In case the environment terminates, the reward will include the terminal cost. :math:`c_F`. + + :param u: The action we apply :math:`u` + :return: + - ``state`` - the state we arrive in + - ``reward`` - (minus) the total (integrated) cost incurred in this time period. + - ``done`` - ``True`` if the environment has finished, i.e. we reached ``env.Tmax``. + - ``truncated`` - ``True`` if the environment was forced to terminated prematurely. Assume it is ``False`` and ignore it. + - ``info`` - A dictionary of potential extra information. Includes ``info['time_seconds']``, which is the current time after the step function has completed. + """ + + def clip_action(self, u): + return np.clip(u, a_max=self.action_space.high, a_min=self.action_space.low) + + u = clip_action(self, u) + self.discrete_model.continuous_model._u_prev = u # for rendering. + if not ((self.action_space.low <= u).all() and (u <= self.action_space.high).all()): # u not in self.action_space: + raise Exception("Action", u, "not contained in action space", self.action_space) + # N=20 is a bit arbitrary; should probably be a parameter to the environment. + xx, uu, tt = self.discrete_model.simulate2(x0=self.state, policy=ensure_policy(u), t0=self.time, tF=self.time + self.discrete_model.dt, N=20) + self.state = xx[-1] + self.time = tt[-1] + cc = [self.discrete_model.cost.c(x, u, k=None) for x, u in zip(xx[:-1], uu[:-1])] + done = False + if self.time + self.discrete_model.dt/2 > self.Tmax: + cc[-1] += self.discrete_model.cost.cN(xx[-1]) + done = True + info = {'dt': self.discrete_model.dt, 'time_seconds': self.time} # Allow the train() function to figure out the simulation time step size + if self.supersample_trajectory: # This is only for nice visualizations. + from irlc.ex01.agent import Trajectory + traj = Trajectory(time=tt, state=xx.T, action=uu.T, reward=np.asarray(cc), env_info=[]) + info['supersample'] = traj # Supersample the trajectory + reward = -sum(cc) # To be compatible with openai gym we return the reward as -cost. + if not ( (self.observation_space.low <= self.state).all() and (self.state <= self.observation_space.high).all() ): #self.state not in self.observation_space: + print("> state", self.state) + print("> observation space", self.observation_space) + raise Exception("State no longer in observation space", self.state) + if self.render_mode == "human": # as in gym's carpole + self.render() + + return self.state, reward, done, False, info + + def reset(self): + """ + Reset the environment to the initial state. This will by default be `the value computed using `self.discrete_model.reset()``. + + :return: + - ``state`` - The initial state the environment has been reset to + - ``info`` - A dictionary with extra information, in this case that time begins at 0 seconds. + """ + self.state = self._get_initial_state() + self.time = 0 # Reset internal time (seconds) + if self.render_mode == "human": + self.render() + return self.state, {'time_seconds': self.time} + + def _get_initial_state(self) -> np.ndarray: + # This helper function returns an initial state. It will be used by the reset() function, and it is this function + # you should overwrite if you want to reset to a state which is not implied by the bounds. + if (self.discrete_model.continuous_model.x0_bound().low == self.discrete_model.continuous_model.x0_bound().high).all(): + return np.asarray(self.discrete_model.phi_x(self.discrete_model.continuous_model.x0_bound().low)) + else: + raise Exception("Since bounds do not agree I cannot return initial state.") + + def render(self): + return self.discrete_model.render(x=self.state, render_mode=self.render_mode) + + def close(self): + self.discrete_model.close() diff --git a/irlc/ex04/discrete_control_cost.py b/irlc/ex04/discrete_control_cost.py new file mode 100644 index 0000000000000000000000000000000000000000..f9ad90dc6958aed6b8d57bdac22355c8c3b14612 --- /dev/null +++ b/irlc/ex04/discrete_control_cost.py @@ -0,0 +1,195 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" +Quadratic cost functions +""" +import numpy as np +from irlc.ex03.control_cost import targ2matrices + +def nz(X,a,b=None): + return np.zeros((a,) if b is None else (a,b)) if X is None else X + +class DiscreteQRCost: #(DiscreteCost): + """ + This class represents the cost function for a discrete-time model. In the simulations, we are going to assume + that the cost function takes the form: + + .. math:: + \sum_{k=0}^{N-1} c_k(x_k, u_k) + c_N(x_N) + + + And this class will specifically implement the two functions :math:`c` and :math:`c_N`. + They will be assumed to have the quadratic form: + + .. math:: + c_k(x_k, u_k) & = \\frac{1}{2} x_k^T Q x_k + \\frac{1}{2} u_k^T R u_k + u^T_k H x_k + q^T x_k + r^T u_k + q_0, \\\\ + c_N(x_N) & = \\frac{1}{2} x_N^T Q_N x_N + q_N^T x_N + q_{0,N}. + + So what all of this boils down to is that the class just need to store a bunch of matrices and vectors. + + You can add and scale cost-functions + ********************************************************** + + A slightly smart thing about the cost functions are that you can add and scale them. The following provides an + example: + + .. runblock:: pycon + + >>> from irlc.ex04.discrete_control_cost import DiscreteQRCost + >>> import numpy as np + >>> cost1 = DiscreteQRCost(np.eye(2), np.zeros(1) ) # Set Q = I, R = 0 + >>> cost2 = DiscreteQRCost(np.ones((2,2)), np.zeros(1) ) # Set Q = 2x2 matrices of 1's, R = 0 + >>> print(cost1.Q) # Will be the identity matrix. + >>> cost = cost1 * 3 + cost2 * 2 + >>> print(cost.Q) # Will be 3 x I + 2 + + """ + def __init__(self, Q, R, H=None,q=None,r=None,qc=0, QN=None, qN=None,qcN=0): + n, d = Q.shape[0], R.shape[0] + self.QN, self.qN = nz(QN,n,n), nz(qN,n) + self.Q, self.q = nz(Q, n, n), nz(q, n) + self.R, self.H, self.r = nz(R, d, d), nz(H, d, n), nz(r, d) + self.qc, self.qcN = qc, qcN + self.flds_term = ['QN', 'qN', 'qcN'] + self.flds = ['Q', 'q', 'R', 'H', 'r', 'qc'] + self.flds_term + + def c(self, x, u, k=None, compute_gradients=False): + """ + Evaluate the (instantaneous) part of the function :math:`c_k(x_k,u_k)`. An example: + + .. runblock:: pycon + + >>> from irlc.ex04.discrete_control_cost import DiscreteQRCost + >>> import numpy as np + >>> cost = DiscreteQRCost(np.eye(2), np.eye(1)) # Set Q = I, R = 0 + >>> cost.c(x = np.asarray([1,2]), u=np.asarray([0]), compute_gradients=False) # should return 0.5 * x^T Q x = 0.5 * (1 + 4) + + The function can also return the derivates of the cost function if ``compute_derivates=True`` + + :param x: The state :math:`x_k` + :param u: The action :math:`u_k` + :param k: The time step :math:`k` (this will be ignored) + :param compute_gradients: if ``True`` the function will compute gradients and Hessians. + :return: + - ``c`` - The cost as a ``float`` + - ``c_x`` - The derivative with respect to :math:`x` + """ + c = 1/2 * (x @ self.Q @ x) + 1/2 * (u @ self.R @ u) + u @ self.H @ x + self.q @ x + self.r @ u + self.qc + c_x = 1/2 * (self.Q + self.Q.T) @ x + self.q + c_u = 1 / 2 * (self.R + self.R.T) @ u + self.r + c_ux = self.H + c_xx = self.Q + c_uu = self.R + if compute_gradients: + # this is useful for MPC when we apply an optimizer rather than LQR (iLQR) + return c, c_x, c_u, c_xx, c_ux, c_uu + else: + return c + + def cN(self, x, compute_gradients=False): + """ + Evaluate the terminal (constant) term in the cost function :math:`c_N(x_N)`. An example: + + .. runblock:: pycon + + >>> from irlc.ex04.discrete_control_cost import DiscreteQRCost + >>> import numpy as np + >>> cost = DiscreteQRCost(np.eye(2), np.zeros(1), QN=np.eye(2)) # Set Q = I, R = 0 + >>> c, Jx, Jxx = cost.cN(x=2*np.ones((2,)), compute_gradients=True) + >>> c # should return 0.5 * x_N^T * x_N = 0.5 * 8 + + :param x: Terminal state :math:`x_N` + :param compute_gradients: if ``True`` the function will compute gradients and Hessians of the cost function. + :return: The last (terminal) part of the cost-function :math:`c_N` + """ + J = 1/2 * (x @ self.QN @ x) + self.qN @ x + self.qcN + if compute_gradients: + J_x = 1 / 2 * (self.QN + self.QN.T) @ x + self.qN + return J, J_x, self.QN + else: + return J + + def __add__(self, c): + return DiscreteQRCost(**{k: self.__dict__[k] + c.__dict__[k] for k in self.flds}) + + def __mul__(self, c): + return DiscreteQRCost(**{k: self.__dict__[k] * c for k in self.flds}) + + def __str__(self): + title = "Discrete-time cost function" + label1 = "Non-zero terms in c_k(x_k, u_k)" + label2 = "Non-zero terms in c_N(x_N)" + terms1 = [s for s in self.flds if s not in self.flds_term] + terms2 = self.flds_term + from irlc.ex03.control_cost import _repr_cost + return _repr_cost(self, title, label1, label2, terms1, terms2) + + @classmethod + def zero(cls, state_size, action_size): + """ + Creates an all-zero cost function, i.e. all terms :math:`Q`, :math:`R` are set to zero. + + .. runblock:: pycon + + >>> from irlc.ex04.discrete_control_cost import DiscreteQRCost + >>> cost = DiscreteQRCost.zero(2, 1) + >>> cost.Q # 2x2 zero matrix + >>> cost.R # 1x1 zero matrix. + + :param state_size: Dimension of the state vector :math:`n` + :param action_size: Dimension of the action vector :math:`d` + :return: A ``DiscreteQRCost`` with all zero terms. + """ + return cls(Q=np.zeros((state_size, state_size)), R=np.zeros((action_size, action_size))) + + def goal_seeking_terminal_cost(self, xN_target, QN=None): + """ + Create a discrete cost function which is minimal when the final state :math:`x_N` is equal to a goal state :math:`x_N^*`. + Concretely, it will return a cost function of the form + + .. math:: + c_N(x_N) = \\frac{1}{2} (x^*_N - x_N)^\\top Q (x^*_N - x_N) + + .. runblock:: pycon + + >>> from irlc.ex04.discrete_control_cost import DiscreteQRCost + >>> import numpy as np + >>> cost = DiscreteQRCost.zero(2, 1) + >>> cost += cost.goal_seeking_terminal_cost(xN_target=np.ones((2,))) + >>> print(cost.qN) + >>> print(cost) + + :param xN_target: Target state :math:`x_N^*` + :param Q: Cost matrix :math:`Q` + :return: A ``DiscreteQRCost`` object corresponding to the goal-seeking cost function + """ + + if QN is None: + QN = np.eye(xN_target.size) + QN, qN, qcN = targ2matrices(xN_target, Q=QN) + return DiscreteQRCost(Q=QN*0, R=self.R*0, QN=QN, qN=qN, qcN=qcN) + + def goal_seeking_cost(self, x_target, Q=None): + """ + Create a discrete cost function which is minimal when the state :math:`x_k` is equal to a goal state :math:`x_k^*`. + Concretely, it will return a cost function of the form + + .. math:: + c_k(x_k, u_k) = \\frac{1}{2} (x^*_k - x_k)^\\top Q (x^*_k - x_k) + + .. runblock:: pycon + + >>> from irlc.ex04.discrete_control_cost import DiscreteQRCost + >>> import numpy as np + >>> cost = DiscreteQRCost.zero(2, 1) + >>> cost += cost.goal_seeking_cost(x_target=np.ones((2,))) + >>> print(cost.q) + >>> print(cost) + + :param x_target: Target state :math:`x_k^*` + :param Q: Cost matrix :math:`Q` + :return: A ``DiscreteQRCost`` object corresponding to the goal-seeking cost function + """ + if Q is None: + Q = np.eye(x_target.size) + Q, q, qc = targ2matrices(x_target, Q=Q) + return DiscreteQRCost(Q=Q, R=self.R*0, q=q, qc=qc) diff --git a/irlc/ex04/discrete_control_model.py b/irlc/ex04/discrete_control_model.py new file mode 100644 index 0000000000000000000000000000000000000000..085a78243784661039f6950079d67259aa774a1d --- /dev/null +++ b/irlc/ex04/discrete_control_model.py @@ -0,0 +1,346 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [Her24] Tue Herlau. Sequential decision making. (Freely available online), 2024. +""" +from irlc.ex03.control_model import ControlModel +import sympy as sym +import numpy as np +import sys +from irlc.ex03.control_model import ensure_policy +# Patch sympy with mapping to numpy functions. +sympy_modules_ = ['numpy', {'atan': np.arctan, 'atan2': np.arctan2, 'atanh': np.arctanh}, 'sympy'] + +class DiscreteControlModel: + """ + A discretized model. To create a model of this type, first specify a symbolic model, then pass it along to the constructor. + Since the symbolic model will specify the dynamics as a symbolic function, the discretized model can automatically discretize it + and create functions for computing derivatives. + + The class will also discretize the cost. Note that it is possible to specify coordinate transformations. + """ + state_labels = None + action_labels = None + + "This field represents the :class:`~irlc.ex04.continuous_time_model.ContinuousSymbolicModel` the discrete model is derived from." + continuous_model = None + + def __init__(self, model: ControlModel, dt: float, cost=None, discretization_method=None): + """ + Create the discretized model. + + :param model: The continuous-time model to discretize. + :param dt: Discretization timestep :math:`\Delta` + :param cost: If this parameter is not specified, the cost will be derived (discretized) automatically from ``model`` + :param discretization_method: Can be either ``'Euler'`` (default) or ``'ei'`` (exponential integration). The later will assume that the model is a linear. + """ + self.dt = dt + self.continuous_model = model + if discretization_method is None: + from irlc.ex04.model_linear_quadratic import LinearQuadraticModel + if isinstance(model, LinearQuadraticModel): + discretization_method = 'Ei' + else: + discretization_method = 'Euler' + self.discretization_method = discretization_method.lower() + + """ Initialize symbolic variables representing inputs and actions. """ + + uc = sym.symbols(f"uc:{model.action_size}") + xc = sym.symbols(f"xc:{model.state_size}") + + # xd, ud = self.sym_continious_xu2discrete_xu(xc, uc) + xd, ud = model.phi_x(xc), model.phi_u(uc) + + x = sym.symbols(f"x:{len(xd)}") + u = sym.symbols(f"u:{len(ud)}") + + """ x_next is a symbolic variable representing x_{k+1} = f_k(x_k, u_k) """ + x_next = self._f_discrete_sym(x, u, dt=dt) + """ compute the symbolic derivate of x_next wrt. z = (x,u): d x_{k+1}/dz """ + dy_dz = sym.Matrix([[sym.diff(f, zi) for zi in list(x) + list(u)] for f in x_next]) + """ Define (numpy) functions giving next state and the derivatives """ + self._f_z_np = sym.lambdify((tuple(x), tuple(u)), dy_dz, modules=sympy_modules_) + # Create a numpy function corresponding to the discretized model x_{k+1} = f_discrete(x_k, u_k) + self._f_np = sym.lambdify((tuple(x), tuple(u)), x_next, modules=sympy_modules_) + self._n = len(x) + self._d = len(u) + + # Make action/state transformation + # xc_, uc_ = self.sym_discrete_xu2continious_xu(x, u) + # self.discrete_states2continious_states = sym.lambdify( (x,), xc_, modules=sympy_modules_) # probably better to make these individual + # self.discrete_actions2continious_actions = sym.lambdify( (u,), uc_, modules=sympy_modules_) # probably better to make these individual + + self.phi_x_inv = sym.lambdify( (x,), model.phi_x_inv(x), modules=sympy_modules_) + self.phi_u_inv = sym.lambdify( (u,), model.phi_u_inv(u), modules=sympy_modules_) + + # xd, ud = self.sym_continious_xu2discrete_xu(xc, uc) + # self.continious_states2discrete_states = sym.lambdify((xc,), xd, modules=sympy_modules_) + # self.continious_actions2discrete_actions = sym.lambdify((uc,), ud, modules=sympy_modules_) + + self.phi_x = sym.lambdify((xc,), model.phi_x(xc), modules=sympy_modules_) + self.phi_u = sym.lambdify((uc,), model.phi_u(uc), modules=sympy_modules_) + + # set labels + if self.state_labels is None: + self.state_labels = self.continuous_model.state_labels + + if self.action_labels is None: + self.action_labels = self.continuous_model.action_labels + + if cost is None: + self.cost = model.get_cost().discretize(dt=dt) + else: + self.cost = cost + + @property + def state_size(self): + """ + The dimension of the state vector :math:`x`, i.e. :math:`n` + :return: Dimension of the state vector :math:`n` + """ + return self._n + + @property + def action_size(self): + """ + The dimension of the action vector :math:`u`, i.e. :math:`d` + :return: Dimension of the action vector :math:`d` + """ + return self._d + + def _f_discrete_sym(self, xs, us, dt): + """ + This is a helper function. It computes the discretized dynamics as a symbolic object: + + .. math:: + x_{k+1} = f_k(x_k, u_k, t_k) + + The parameters corresponds to states and actions and are lists of the form ``[x0, x1, ..]`` and ``[u0, u1, ..]`` + where each element is a symbolic expression. The function returns a list of the form ``[f0, f1, ..]`` where + each element is a symbolic expression corresponding to a coordinate of :math:`f_k`. + + :param xs: List of symbolic expressions corresponding to the coordinates of :math:`x_k` + :param us: List of symbolic expressions corresponding to the coordinates of :math:`x_u` + :param dt: A symbolic expressions corresponding to :math:`t_k` + :return: A list of symbolic expressions corresponding to the coordinates of :math:`f_k` + """ + # xc, uc = self.sym_discrete_xu2continious_xu(xs, us) + xc, uc = self.continuous_model.phi_x_inv(xs), self.continuous_model.phi_u_inv(us) + if self.discretization_method == 'euler': + xdot = self.continuous_model.sym_f(x=xc, u=uc) + xnext = [x_ + xdot_ * dt for x_, xdot_ in zip(xc, xdot)] + elif self.discretization_method == 'ei': # Assume the continuous model is linear; a bit hacky, but use exact Exponential integration in that case + A = self.continuous_model.A + B = self.continuous_model.B + d = self.continuous_model.d + """ These are the matrices of the continuous-time problem. + > dx/dt = Ax + Bu + d + and should be discretized using the exact integration technique (see (Her24, Subsection 13.1.3) and (Her24, Subsection 13.1.6)); + the precise formula you should implement is given in (Her24, eq. (13.19)) + + Remember the output matrix should be symbolic (see Euler integration for examples) but you can assume there are no variable transformations for simplicity. + """ + from scipy.linalg import expm, inv + """ + expm computes the matrix exponential: + > expm(A) = exp(A) + inv computes the inverse of a matrix inv(A) = A^{-1}. + """ + Ad = expm(A * dt) + n = Ad.shape[0] + d = d.reshape( (len(B),1) ) if d is not None else np.zeros( (n, 1) ) + Bud = B @ sym.Matrix(uc) + (sym.zeros(len(B),1) if d is None else d) + x_next = sym.Matrix(Ad) @ sym.Matrix(xc) + dt * phi1(A * dt) @ Bud + xnext = list(x_next) + else: + raise Exception("Unknown discreetization method", self.discretization_method) + # xnext, _ = self.sym_continious_xu2discrete_xu(xnext, uc) + xnext = self.continuous_model.phi_x(xnext) + return xnext + + def simulate2(self, x0, policy, t0, tF, N=1000): + policy3 = lambda x, t: self.phi_u_inv(ensure_policy(policy)(x, t)) + x, u, t = self.continuous_model.simulate(self.phi_x_inv(x0), policy3, t0, tF, N_steps=N, method='rk4') + # transform to discrete representations using phi. + xd = np.stack( [np.asarray(self.phi_x(x_)).reshape((-1,)) for x_ in x ] ) + ud = np.stack( [np.asarray(self.phi_u(u_)).reshape((-1,)) for u_ in u] ) + return xd, ud, t + + def f(self, x, u, k=0): + """ + This function implements the dynamics :math:`f_k(x_k, u_k)` of the model. They can be evaluated as: + + .. runblock:: pycon + + >>> from irlc.ex04.model_pendulum import DiscreteSinCosPendulumModel + >>> model = DiscreteSinCosPendulumModel() + >>> x = [0, 1, 0.4] + >>> u = [1] + >>> print(model.f(x,u) ) # Computes x_{k+1} = f_k(x_k, u_k) + + The model will by default be Euler discretized: + + .. math:: + + x_{k+1} = f_k(x_k, u_k) = x_k + \Delta f(x_k, u_k) + + except :python:`LinearQuadraticModel` which will be discretized using Exponential Integration by default. + + + :param x: The state as a numpy array + :param u: The action as a numpy array + :param k: The time step as an integer (currently this has no effect) + :return: The next state :math:`x_{x+1}` as a numpy array. + """ + fx = np.asarray( self._f_np(x, u) ) + return fx + # if compute_jacobian: + # assert False + # # J = self._f_z_np(x, u) + # return fx, J[:, :self.state_size], J[:, self.state_size:] + # else: + # return fx + + + def f_jacobian(self, x, u, k=0): + """Compute the Jacobians of the discretized dynamics. + + The function will compute the two Jacobian derives of the discrete dynamics :math:`f_k` with respect to :math:`x` and :math:`u`: + + .. math:: + J_x f_k(x,u), \quad J_u f_k(x, u) + + .. runblock:: pycon + + >>> from irlc.ex04.model_pendulum import DiscreteSinCosPendulumModel + >>> model = DiscreteSinCosPendulumModel() + >>> x = [0, 1, 0.4] + >>> u = [0] + >>> f, Jx, Ju = model.f(x,u) + >>> Jx, Ju = model.f_jacobian(x,u) + >>> print("Jacobian Jx is\\n", Jx) + >>> print("Jacobian Ju is\\n", Ju) + + + :param x: The state as a numpy array + :param u: The action as a numpy array + :param k: The time step as an integer (currently this has no effect) + :return: The two Jacobians computed wrt. :math:`x` and :math:`u`. + """ + J = self._f_z_np(x, u) + return J[:, :self.state_size], J[:, self.state_size:] + + + def render(self, x=None, render_mode="human"): + return self.continuous_model.render(x=self.phi_x_inv(x), render_mode=render_mode) + + # def sym_continious_xu2discrete_xu(self, x, u): + # """ + # This (optional) function handle coordinate transformations. + # ``x`` and ``u`` are lists of symbolic expressions (the state and action), and the function then computes and return + # the forward coordinate transformation (from continuous coordinates to discrete): + # + # .. math:: + # x_k & = \phi_x(x) \\\\ + # u_k & = \phi_u(u) + # + # :param x: Continuous state + # :param u: Continuous action + # :return: + # - ``x_k`` - Transformed (discrete) state + # - ``u_k`` - Transformed (discrete) action + # """ + # return x, u + + # def sym_discrete_xu2continious_xu(self, x_k, u_k): + # """ + # This (optional) function handle coordinate transformations. + # ``x_k`` and ``u_k`` are lists of symbolic expressions (the state and action), and the function then computes and return + # the **backward** coordinate transformation (from discrete coordinates to continuous coordinates): + # + # .. math:: + # x & = \phi^{-1}_x(x_k) \\\\ + # u & = \phi^{-1}_u(u_k) + # + # :param x_k: discrete state + # :param u_k: discrete action + # :return: + # - ``x`` - Transformed (Continuous) state + # - ``u`` - Transformed (Continuous) action + # """ + # return x_k, u_k + + def close(self): + self.continuous_model.close() + + def __str__(self): + """ + Return a string representation of the model. This is a potentially helpful way to summarize the content of the + model. You can use it as: + + .. runblock:: pycon + + >>> from irlc.ex04.model_pendulum import DiscreteSinCosPendulumModel + >>> model = DiscreteSinCosPendulumModel() + >>> print(model) + + :return: A string containing the details of the model. + """ + split = "-"*20 + s = [f"{self.__class__}"] + ['='*50] + s += [f"Dynamics (after discretization with Delta = {self.dt}):", split] + t = sym.symbols("t") + x = sym.symbols(f"x0:{self.state_size}") + u = sym.symbols(f"u0:{self.action_size}") + + # x = symv("x", self.state_size) + # u = symv("u", self.action_size) + # s += [f"f_k({x}, {u}) = {str(self.f_discrete_sym(x, u, self.dt))}", ''] + + f = self._f_discrete_sym(x, u, self.dt) + + # x = sym.symbols(f"x0:{self.state_size}") + # u = sym.symbols(f"u0:{self.action_size}") + from irlc.ex03.control_model import typeset_eq + + s += [typeset_eq(x, u, f)] + + # print(typeset_eq(x, u, f)) + + + s += ["Continuous-time dynamics:", split] + # xc = symv("x", self.continuous_model.state_size) + # uc = symv("u", self.continuous_model.action_size) + xc = sym.symbols(f"x:{self.continuous_model.state_size}") + uc = sym.symbols(f"u:{self.continuous_model.action_size}") + + s += [f"f_k({x}, {u}) = {str(self.continuous_model.sym_f(xc, uc))}", ''] + s += ["Variable transformations:", split] + # self.continious_states2discrete_states(xc) + xd, ud = self.continuous_model.phi_x(xc), self.continuous_model.phi_u(uc) + s += [f' * phi_x( x(t) ) -> x_k = {xd}'] + s += [f' * phi_u( u(t) ) -> u_k = {ud}', ''] + s += ["Cost:", split, str(self.cost)] + return "\n".join(s) + + +def phi1(A): + """ This is a helper functions which computes + .. math:: + A^{-1} (e^A - I) + + and importantly deals with potential numerical instability in the expression. + """ + from scipy.linalg import expm + from math import factorial + if np.linalg.cond(A) < 1 / sys.float_info.epsilon: + return np.linalg.solve(A, expm(A) - np.eye( len(A) ) ) + else: + C = np.zeros_like(A) + for k in range(1, 20): + dC = np.linalg.matrix_power(A, k - 1) / factorial(k) + C += dC + assert sum( np.abs(dC.flat)) < 1e-10 + return C diff --git a/irlc/ex04/discrete_kuramoto.py b/irlc/ex04/discrete_kuramoto.py new file mode 100644 index 0000000000000000000000000000000000000000..50edc127f74c221805c2bbf40c3f56b3fe7ad3e9 --- /dev/null +++ b/irlc/ex04/discrete_kuramoto.py @@ -0,0 +1,101 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex04.discrete_control_model import DiscreteControlModel +from irlc.ex04.control_environment import ControlEnvironment +import numpy as np +from irlc import train, Agent, savepdf +import matplotlib.pyplot as plt +from irlc.ex03.kuramoto import KuramotoModel, f + + +def fk(x,u): + """ Computes the discrete (Euler 1-step integrated) version of the Kuromoto update with discretization time dt=0.5,i.e. + + x_{k+1} = f_k(x,u). + + Look at dmodel.f for inspiration. As usual, use a debugger and experiment. Note you have to specify input arguments as lists, + and the function should return a numpy ndarray. + """ + dmodel = DiscreteControlModel(KuramotoModel(), dt=0.5) # this is how we discretize the Kuramoto model. + # TODO: 1 lines missing. + raise NotImplementedError("Compute Euler discretized dynamics here using the dmodel.") + return f_euler + +def dfk_dx(x,u): + """ Computes the derivative of the (Euler 1-step integrated) version of the Kuromoto update with discretization time dt=0.5, + i.e. if + + .. math:: + + x_{k+1} = f_k(x,u) + + this function should return + + .. math:: + + \frac{\partial f_k}{\partial x } + + (i.e. the Jacobian with respect to x) as a numpy matrix. + Look at dmodel.f for inspiration, and note it has an input argument that is relevant. + As usual, use a debugger and experiment. Note you have to specify input arguments as lists, + and the function should return a two-dimensional numpy ndarray. + + """ + dmodel = DiscreteControlModel(KuramotoModel(), dt=0.5) + # the function dmodel.f accept various parameters. Perhaps their name can give you an idea? + # TODO: 1 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + return f_euler_derivative + + +if __name__ == "__main__": + # Part 1: Making a model + cmodel = KuramotoModel() + print(cmodel) + # Computing f_k + dmodel = DiscreteControlModel(KuramotoModel(), dt=0.5) + print(dmodel) # This will print details about the discrete model. + + print("The Euler-discretized version, f_k(x,u) = x + Delta f(x,u), is") + print("f_k(x=0,u=0) =", fk([0], [0])) + print("f_k(x=1,u=0.3) =", fk([1], [0.3])) + + # Computing df_k / dx (The Jacobian). + print("The derivative of the Euler discretized version wrt. x is:") + print("df_k/dx(x=0,u=0) =", dfk_dx([0], [0])) + + # Part 2: The environment and simulation: + env = ControlEnvironment(dmodel, Tmax=20) # An environment that runs for 20 seconds. + u = 1.3 # Action to take in each time step. + + ts_step = [] # Current time (according to the environment, i.e. in increments of dt. + xs_step = [] # x_k using the env.step-function in the enviroment. + + x, _ = env.reset() # Get starting state. + ts_step.append(env.time) # env.time keeps track of the clock-time in the environment. + xs_step.append(x) # Initialize with first state + + # Use + # > next_x, cost, terminated, truncated, metadata = env.step([u]) + # to simulate a single step. + for _ in range(10000): + # TODO: 1 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + xs_step.append(next_x) + ts_step.append(env.time) # This is how you get the current time (in seconds) from the environment. + + if terminated: # Obtain 'terminated' from the step-function. It will be true when Tmax=20 seconds has passed. + break + + x0 = cmodel.x0_bound().low # Get the starting state x0. We exploit that the bound on x0 is an equality constraint. + xs_rk4, us_rk4, ts_rk4 = cmodel.simulate(x0, u_fun=u, t0=0, tF=20, N_steps=100) + + plt.plot(ts_rk4, xs_rk4, 'k-', label='RK4 (nearly exact)') + plt.plot(ts_step, xs_step, 'ro', label='RK4 (step-function in environment)') + + # Use the train-function to plot the result of simulating a random agent. + stats, trajectories = train(env, Agent(env), return_trajectory=True) + plt.plot(trajectories[0].time, trajectories[0].state, label='x(t) when using a random action sequence from agent') + plt.legend() + savepdf('kuramoto_step') + plt.show(block=False) + print("The total cost obtained using random actions", -stats[0]['Accumulated Reward']) diff --git a/irlc/ex04/locomotive.py b/irlc/ex04/locomotive.py new file mode 100644 index 0000000000000000000000000000000000000000..10b0a1494bb38cce9731295a9ad591dfe574d834 --- /dev/null +++ b/irlc/ex04/locomotive.py @@ -0,0 +1,105 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex04.discrete_control_model import DiscreteControlModel +from irlc.ex04.control_environment import ControlEnvironment +from irlc.ex04.model_harmonic import HarmonicOscilatorModel +import numpy as np +from irlc.utils.graphics_util_pygame import UpgradedGraphicsUtil +from gymnasium.spaces import Box + +class LocomotiveModel(HarmonicOscilatorModel): + state_labels = ["x(t)", "v(t)"] + action_labels = ["u(t)"] + + + viewer = None + metadata = { + 'render.modes': ['human', 'rgb_array'], + 'video.frames_per_second': 20 + } + + def __init__(self, m=1., slope=0.0, target=0): + """ + Slope is the uphill slope of the train (in degrees). E.g. slope=15 makes it harder for the engine. + + :param m: + :param slope: + """ + self.target = target + self.slope = slope + super().__init__(m=m, k=0., drag=-np.sin(slope/360*2*np.pi) * m * 9.82) + + def x0_bound(self) -> Box: + return Box(np.asarray([-1, 0]), np.asarray([-1,0])) + + def u_bound(self) -> Box: + return Box(np.asarray([-100]), np.asarray([100])) # Min and Max engine power. + + def render(self, x, render_mode="human"): + """ Initialize a viewer and update the states. """ + if self.viewer is None: + self.viewer = LocomotiveViewer(self) + self.viewer.update(x, self.target) + import time + time.sleep(0.05) + return self.viewer.blit(render_mode=render_mode) + + def close(self): + if self.viewer is not None: + self.viewer.close() + +class DiscreteLocomotiveModel(DiscreteControlModel): + def __init__(self, *args, dt=0.1, **kwargs): + model = LocomotiveModel(*args, **kwargs) + super().__init__(model=model, dt=dt) + +class LocomotiveEnvironment(ControlEnvironment): + def __init__(self, *args, dt=0.1, Tmax=5, render_mode=None, **kwargs): + model = DiscreteLocomotiveModel(*args, dt=dt, **kwargs) + # self.dt = model.dt + super().__init__(discrete_model=model, Tmax=Tmax, render_mode=render_mode) + + +class LocomotiveViewer(UpgradedGraphicsUtil): + def __init__(self, train): + self.train = train + width = 1100 + self.scale = width / 4 + self.dw = self.scale * 0.1 + super().__init__(screen_width=width, xmin=-width / 2, xmax=width / 2, ymin=-width / 5, ymax=width / 5, title='Locomotive environment') + from irlc.utils.graphics_util_pygame import Object + self.locomotive = Object("locomotive.png", image_width=90, graphics=self) + + def render(self): + # fugly rendering code. + dw = self.dw + scale = self.scale + train = self.train + red = (200, 40, 40) + from irlc.utils.graphics_util_pygame import rotate_around + ptrack = [(-2 * scale, -dw / 2*0), + (-2 * scale, dw / 2), + (2 * scale, dw / 2), + (2 * scale, -dw / 2*0)] + ptrack.append( ptrack[-1]) + ptrack = rotate_around(ptrack,(0,0), -self.train.slope) + self.draw_background(background_color=(255, 255, 255)) + self.polygon("asdf", coords=ptrack, fillColor=(int(.7 * 255),) * 3, filled=True) + self.locomotive.surf.get_height() + self.locomotive.rotate(self.train.slope) + p0 = (0,0) + self.locomotive.move_center_to_xy( *rotate_around( (self.scale * self.x[0], -self.locomotive.surf.get_height()//2), p0, -self.train.slope)) + self.locomotive.blit(self.surf) + xx = 0*self.scale * self.x[0] + triangle = [(train.target * scale - dw / 2+ xx, dw/2), (train.target * scale + xx, -0*dw / 2), + (train.target * scale + dw / 2 + xx, dw/2)] + triangle = rotate_around(triangle, p0, -self.train.slope) + ddw = dw/2 + xx = self.scale * self.x[0] + trainloc = [(xx- ddw / 2, -ddw / 2), ( xx, -0 * ddw / 2), (xx + ddw / 2, -ddw / 2)] + trainloc = rotate_around(trainloc, p0, -self.train.slope) + self.trg = self.polygon("", coords=trainloc, fillColor=red, filled=True) + self.trg = self.polygon("", coords=triangle, fillColor=red, filled=True) + + def update(self, x, xstar): + self.x = x #*self.scale + self.xstar = xstar diff --git a/irlc/ex04/model_harmonic.py b/irlc/ex04/model_harmonic.py new file mode 100644 index 0000000000000000000000000000000000000000..198e529d5953df1be967a9c48ecd53786beb808a --- /dev/null +++ b/irlc/ex04/model_harmonic.py @@ -0,0 +1,113 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex04.model_linear_quadratic import LinearQuadraticModel +from irlc.ex04.discrete_control_model import DiscreteControlModel +from irlc.ex04.control_environment import ControlEnvironment +import numpy as np +from irlc.utils.graphics_util_pygame import UpgradedGraphicsUtil + +""" +Simulate a Harmonic oscillator governed by equations: + +d^2 x1 / dt^2 = -k/m x1 + u(x1, t) + +where x1 is the position and u is our externally applied force (the control) +k is the spring constant and m is the mass. See: + +https://en.wikipedia.org/wiki/Simple_harmonic_motion#Dynamics + +for more details. +In the code, we will re-write the equations as: + +dx/dt = f(x, u), u = u_fun(x, t) + +where x = [x1,x2] is now a vector and f is a function of x and the current control. +here, x1 is the position (same as x in the first equation) and x2 is the velocity. + +The function should return ts, xs, C + +where ts is the N time points t_0, ..., t_{N-1}, xs is a corresponding list [ ..., [x_1(t_k),x_2(t_k)], ...] and C is the cost. +""" + +class HarmonicOscilatorModel(LinearQuadraticModel): + metadata = { + 'render.modes': ['human', 'rgb_array'], + 'video.frames_per_second': 20 + } + """ + See: https://books.google.dk/books?id=tXZDAAAAQBAJ&pg=PA147&lpg=PA147&dq=boeing+747+flight+0.322+model+longitudinal+flight&source=bl&ots=L2RpjCAWiZ&sig=ACfU3U2m0JsiHmUorwyq5REcOj2nlxZkuA&hl=en&sa=X&ved=2ahUKEwir7L3i6o3qAhWpl4sKHQV6CdcQ6AEwAHoECAoQAQ#v=onepage&q=boeing%20747%20flight%200.322%20model%20longitudinal%20flight&f=false + """ + def __init__(self, k=1., m=1., drag=0.0, Q=None, R=None): + self.k = k + self.m = m + A = [[0, 1], + [-k/m, 0]] + + B = [[0], [1/m]] + d = [[0], [drag/m]] + + A, B, d = np.asarray(A), np.asarray(B), np.asarray(d) + if Q is None: + Q = np.eye(2) + if R is None: + R = np.eye(1) + self.viewer = None + super().__init__(A=A, B=B, Q=Q, R=R, d=d) + + def render(self, x, render_mode="human"): + """ Render the environment. You don't have to understand this code. """ + if self.viewer is None: + self.viewer = HarmonicViewer(xstar=0) # target: x=0. + self.viewer.update(x) + import time + time.sleep(0.05) + return self.viewer.blit(render_mode=render_mode) + + def close(self): + if self.viewer is not None: + self.viewer.close() + + +class DiscreteHarmonicOscilatorModel(DiscreteControlModel): + def __init__(self, dt=0.1, discretization_method=None, **kwargs): + model = HarmonicOscilatorModel(**kwargs) + super().__init__(model=model, dt=dt, discretization_method=discretization_method) + + +class HarmonicOscilatorEnvironment(ControlEnvironment): + def __init__(self, Tmax=80, supersample_trajectory=False, render_mode=None, **kwargs): + model = DiscreteHarmonicOscilatorModel(**kwargs) + self.dt = model.dt + super().__init__(discrete_model=model, Tmax=Tmax, render_mode=render_mode, supersample_trajectory=supersample_trajectory) + + def _get_initial_state(self) -> np.ndarray: + return np.asarray([1, 0]) + +class HarmonicViewer(UpgradedGraphicsUtil): + def __init__(self, xstar = 0): + self.xstar = xstar + width = 1100 + self.scale = width / 6 + self.dw = self.scale * 0.1 + super().__init__(screen_width=width, xmin=-width / 2, xmax=width / 2, ymin=-width / 5, ymax=width / 5, title='Harmonic Osscilator') + + def render(self): + self.draw_background(background_color=(255, 255, 255)) + dw = self.dw + self.rectangle(color=(0,0,0), x=-dw//2, y=-dw//2, width=dw, height=dw) + xx = np.linspace(0, 1) + y = np.sin(xx * 2 * np.pi * 5) * 0.1*self.scale * 0.5 + + for i in range(len(xx) - 1): + self.line("asfasf", here=(xx[i] * self.x[0] * self.scale, y[i]), there=(xx[i + 1] * self.x[0] * self.scale, y[i+1]), + color=(0,0,0), width=2) + self.circle("asdf", pos=( self.x[0] * self.scale, 0), r=dw, fillColor=(0,0,0)) + self.circle("asdf", pos=( self.x[0] * self.scale, 0), r=dw*0.9, fillColor=(int(.7 * 255),) * 3) + + def update(self, x): + self.x = x + +if __name__ == "__main__": + from irlc import train + env = HarmonicOscilatorEnvironment(render_mode='human') + # train(env, NullAgent(env), num_episodes=1, max_steps=200) + # env.close() diff --git a/irlc/ex04/model_linear_quadratic.py b/irlc/ex04/model_linear_quadratic.py new file mode 100644 index 0000000000000000000000000000000000000000..912c2bbe9e6fdcbb783cfb4c330e4e496f5c7fdd --- /dev/null +++ b/irlc/ex04/model_linear_quadratic.py @@ -0,0 +1,29 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import sympy as sym +from irlc.ex03.control_model import ControlModel +from irlc.ex03.control_cost import SymbolicQRCost +from gymnasium.spaces import Box + +class LinearQuadraticModel(ControlModel): + """ + Implements a model with update equations + + dx/dt = Ax + Bx + d + Cost = integral_0^{t_F} (1/2 x^T Q x + 1/2 u^T R u + q' x + qc) dt + """ + def __init__(self, A, B, Q, R, q=None, qc=None, d=None): + self._cost = SymbolicQRCost(R=R, Q=Q, q=q, qc=qc) + self.A, self.B, self.d = A, B, d + super().__init__() + + def sym_f(self, x, u, t=None): + xp = sym.Matrix(self.A) * sym.Matrix(x) + sym.Matrix(self.B) * sym.Matrix(u) + if self.d is not None: + xp += sym.Matrix(self.d) + return [x for xr in xp.tolist() for x in xr] + + def x0_bound(self) -> Box: + return Box(0, 0, shape=(self.state_size,)) + + def get_cost(self): + return self._cost diff --git a/irlc/ex04/model_pendulum.py b/irlc/ex04/model_pendulum.py new file mode 100644 index 0000000000000000000000000000000000000000..2777afce25b45f89986db02ea772f892c6b269d6 --- /dev/null +++ b/irlc/ex04/model_pendulum.py @@ -0,0 +1,164 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import sympy as sym +from irlc.ex03.control_model import ControlModel +from irlc.ex03.control_cost import SymbolicQRCost +from irlc.ex04.discrete_control_model import DiscreteControlModel +import gymnasium as gym +from gymnasium.spaces.box import Box +from irlc.ex04.control_environment import ControlEnvironment +import numpy as np + +""" +SEE: https://github.com/openai/gym/blob/master/gym/envs/classic_control/cartpole.py +https://github.com/openai/gym/blob/master/gym/envs/classic_control/pendulum.py +""" +class PendulumModel(ControlModel): + state_labels= [r"$\theta$", r"$\frac{d \theta}{dt}$"] + action_labels = ['Torque $u$'] + x_upright, x_down = np.asarray([0.0, 0.0]), np.asarray([np.pi, 0.0]) + + def __init__(self, l=1., m=.8, friction=0.0, max_torque=6.0, transform_coordinates=False): + self.l, self.m, self.max_torque = l, m, max_torque + assert not transform_coordinates + super().__init__() + self.friction = friction + self._u_prev = None # For rendering + self.cp_render = {} + assert friction == 0.0 + + def sym_f(self, x, u, t=None): + l, m = self.l, self.m + g = 9.82 + theta_dot = x[1] # Parameterization: x = [theta, theta'] + theta_dot_dot = g/l * sym.sin(x[0]) + 1/(m*l**2) * u[0] + return [theta_dot, theta_dot_dot] + + def get_cost(self) -> SymbolicQRCost: + return SymbolicQRCost(R=np.ones((1, 1)), Q=np.eye(2)) + + def tF_bound(self) -> Box: + return Box(0.5, 4, shape=(1,)) + + def t0_bound(self) -> Box: + return Box(0, 0, shape=(1,)) + + def x_bound(self) -> Box: + return Box(np.asarray( [-2 * np.pi, -np.inf]), np.asarray( [2 * np.pi, np.inf]) ) + + def u_bound(self) -> Box: + return Box(np.asarray([-self.max_torque]), np.asarray([self.max_torque])) + + def x0_bound(self) -> Box: + return Box(np.asarray( [np.pi, 0] ), np.asarray( [np.pi, 0])) + + def xF_bound(self) -> Box: + return Box(np.asarray([0, 0]), np.asarray([0, 0])) + + # def close(self): + # if self.cp_render is not None: + # self.cp_render.close() + + # def render(self, x, render_mode="human"): + # if self.cp_render is None: + # self.cp_render = gym.make("Pendulum-v1", render_mode=render_mode) # environment only used for rendering + # self.cp_render.max_time_limit = 10000 + # self.cp_render.reset() + # + # self.cp_render.unwrapped.last_u = float(self._u_prev) if self._u_prev is not None else self._u_prev + # self.cp_render.unwrapped.state = np.asarray(x) + # return self.cp_render.render() + + + def close(self): + for r in self.cp_render.values(): + r.close() + + def render(self, x, render_mode="human"): + if render_mode not in self.cp_render: # is None or self.cp_render[1] != render_mode: + # if self.cp_render is not None: + # self.cp_render.close() + + self.cp_render[render_mode] = gym.make("Pendulum-v1", render_mode=render_mode) # environment only used for rendering. Change to v1 in gym 0.26. + # self.cp_render[render_mode].render_mode = render_mode + self.cp_render[render_mode].max_time_limit = 10000 + self.cp_render[render_mode].reset() + self.cp_render[render_mode].unwrapped.state = np.asarray(x) # environment is wrapped + self.cp_render[render_mode].unwrapped.last_u = self._u_prev[0] if self._u_prev is not None else None + return self.cp_render[render_mode].render() + +class SinCosPendulumModel(PendulumModel): + def phi_x(self, x): + theta, theta_dot = x[0], x[1] + return [sym.sin(theta), sym.cos(theta), theta_dot] + + def phi_x_inv(self, x): + sin_theta, cos_theta, theta_dot = x[0], x[1], x[2] + theta = sym.atan2(sin_theta, cos_theta) # Obtain angle theta from sin(theta),cos(theta) + return [theta, theta_dot] + + def phi_u(self, u): + return [sym.atanh(u[0] / self.max_torque)] + + def phi_u_inv(self, u): + return [sym.tanh(u[0]) * self.max_torque] + + def u_bound(self) -> Box: + return Box(np.asarray([-np.inf]), np.asarray([np.inf])) + +def _pendulum_cost(model): + from irlc.ex04.discrete_control_cost import DiscreteQRCost + Q = np.eye(model.state_size) + Q[0, 1] = Q[1, 0] = model.l + Q[0, 0] = Q[1, 1] = model.l ** 2 + Q[2, 2] = 0.0 + R = np.array([[0.1]]) * 10 + c0 = DiscreteQRCost(Q=np.zeros((model.state_size,model.state_size)), R=R) + c0 = c0 + c0.goal_seeking_cost(Q=Q, x_target=model.x_upright) + c0 = c0 + c0.goal_seeking_terminal_cost(xN_target=model.x_upright) * 1000 + return c0 * 2 + + +class DiscreteSinCosPendulumModel(DiscreteControlModel): + state_labels = ['$\sin(\\theta)$', '$\cos(\\theta)$', '$\\dot{\\theta}$'] # Check if this escape character works. + action_labels = ['Torque $u$'] + + def __init__(self, dt=0.02, cost=None, **kwargs): + model = SinCosPendulumModel(**kwargs) + self.max_torque = model.max_torque + # self.transform_actions = transform_actions + super().__init__(model=model, dt=dt, cost=cost) + self.x_upright = np.asarray(self.phi_x(model.x_upright)) + self.l = model.l # Pendulum length + if cost is None: + cost = _pendulum_cost(self) + self.cost = cost + + +class ThetaPendulumEnvironment(ControlEnvironment): + def __init__(self, Tmax=5, render_mode=None): + dt = 0.02 + discrete_model = DiscreteControlModel(PendulumModel(), dt=dt) + super().__init__(discrete_model, Tmax=Tmax, render_mode=render_mode) + +class GymSinCosPendulumEnvironment(ControlEnvironment): + def __init__(self, *args, Tmax=5, supersample_trajectory=False, render_mode=None, **kwargs): + discrete_model = DiscreteSinCosPendulumModel(*args, **kwargs) + self.action_space = Box(low=-np.inf, high=np.inf, shape=(discrete_model.action_size,), dtype=float) + self.observation_space = Box(low=-np.inf, high=np.inf, shape=(discrete_model.state_size,), dtype=float) + super().__init__(discrete_model, Tmax=Tmax, supersample_trajectory=supersample_trajectory, render_mode=render_mode) + +if __name__ == "__main__": + model = SinCosPendulumModel(l=1, m=1) + print(str(model)) + print(f"Pendulum with l={model.l}, m={model.m}") + x = [1,2] + u = [0] # Input state/action. + # x_dot = ... + # TODO: 1 lines missing. + raise NotImplementedError("Compute dx/dt = f(x, u, t=0) here using the model-class defined above") + # x_dot_numpy = ... + # TODO: 1 lines missing. + raise NotImplementedError("Compute dx/dt = f(x, u, t=0) here using numpy-expressions you write manually.") + + print(f"Using model-class: dx/dt = f(x, u, t) = {x_dot}") + print(f"Using numpy: dx/dt = f(x, u, t) = {x_dot_numpy}") diff --git a/irlc/ex04/pid.py b/irlc/ex04/pid.py new file mode 100644 index 0000000000000000000000000000000000000000..440228e8588d8c188beec60ff8f3fc0a73e1672c --- /dev/null +++ b/irlc/ex04/pid.py @@ -0,0 +1,60 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [Her24] Tue Herlau. Sequential decision making. (Freely available online), 2024. +""" +from irlc import savepdf +import numpy as np +import matplotlib.pyplot as plt +from irlc.ex04.locomotive import LocomotiveEnvironment + +class PID: + def __init__(self, dt, Kp, Ki, Kd, target): + self.Kp = Kp + self.Ki = Ki + self.Kd = Kd + self.dt = dt # discretization time + self.target = target # target, in our case just a number. + self.I = 0 # Internal variables for integral/derivative terms; use these or define your own. + self.e_prior = 0 # Previous value of the error. Used in the derivative term. Remember to update it in the pi-function. + + def reset(self): + self.I = 0 + self.e_prior = 0 + + def pi(self, x): + """ + Policy for the PID class. x is always a scalar (float) and the output u is a scalar. + Should implement (Her24, Algorithm 19) + + :param x: Input state (float) + :return: Action to take (float) + """ + # TODO: 6 lines missing. + raise NotImplementedError("Compute u here.") + return u + + +def pid_explicit(): + env = LocomotiveEnvironment(m=70, slope=0, dt=0.05, Tmax=15) + pid = PID(dt=0.05, Kp=40, Kd=0, Ki=0, target=0) + # Compute the first action using PID control: + print(f"When x_0 = 1 then the first action is u_0 = {pid.pi(x=1)} (and should be u_0 = -40.0)") + x0, _ = env.reset() + x = [x0] + for _ in range(200): # Simulate for 200 steps, i.e. 0.05 * 200 seconds. + x_cur = x[-1] # x is the last state [position, velocity]. Note that you only need to pass position to your PID controller. + # TODO: 1 lines missing. + raise NotImplementedError("Compute action here using the pid class.") + u = np.clip(u, -100, 100) # clip actions. + xp_, reward, done, truncated, _ = env.step(u) + x.append(xp_) + + x = np.stack(x) + plt.plot(x[:,0], 'k-', label="PID state trajectory") + savepdf("pid_basic") + plt.show(block=False) + +if __name__ == "__main__": + pid_explicit() diff --git a/irlc/ex04/pid_car.py b/irlc/ex04/pid_car.py new file mode 100644 index 0000000000000000000000000000000000000000..84627fd578c9601b4acf1a26ea79bd2bb63be21f --- /dev/null +++ b/irlc/ex04/pid_car.py @@ -0,0 +1,61 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +from irlc import savepdf +from irlc.ex04.pid import PID +from irlc import Agent +from irlc.ex04.control_environment import ControlEnvironment + +class PIDCarAgent(Agent): + def __init__(self, env: ControlEnvironment, v_target=0.5, use_both_x5_x3=True): + """ + Define two pid controllers: One for the angle, the other for the velocity. + + self.pid_angle = PID(dt=self.discrete_model.dt, Kp=x, ...) + self.pid_velocity = PID(dt=self.discrete_model.dt, Kp=z, ...) + + I did not use Kd/Ki, however you need to think a little about the targets. + """ + # self.pid_angle = ... + ## TODO: Half of each line of code in the following 2 lines have been replaced by garbage. Make it work and remove the error. + #---------------------------------------------------------------------------------------------------------------------------- + # self.pid_angle = PID(dt=env.discrete_m?????????????????????????????????????? + # self.pid_velocity = PID(dt=env.discrete_mod??????????????????????????????????????????? + raise NotImplementedError("Define PID controllers here.") + self.use_both_x5_x3 = use_both_x5_x3 # Using both x3+x5 seems to make it a little easier to get a quick lap time, but you can just use x5 to begin with. + super().__init__(env) + + def pi(self, x, k, info=None): + """ + Call PID controller. The steering angle controller should initially just be based on + x[5] (distance to the centerline), but you can later experiment with a linear combination of x5 and x3 as input. + + Hints: + - To control the velocity, you should use x[0], the velocity of the car in the direction of the car. + - Remember to start out with a low value of v_target, then tune the controller and look at the animation. + - You can access the pid controllers as self.pid_angle(x_input) + - Remember the function must return a 2d numpy ndarray. + """ + + # TODO: 2 lines missing. + raise NotImplementedError("Compute action here. No clipping necesary.") + return u + + +if __name__ == "__main__": + from irlc.ex01.agent import train + from irlc.car.car_model import CarEnvironment + import matplotlib.pyplot as plt + + env = CarEnvironment(noise_scale=0,Tmax=30, max_laps=1, render_mode='human') + agent = PIDCarAgent(env, v_target=1, use_both_x5_x3=True) # I recommend lowering v_target to make the problem simpler. + + stats, trajectories = train(env, agent, num_episodes=1, return_trajectory=True) + env.close() + t = trajectories[0] + plt.clf() + plt.plot(t.state[:,0], label="velocity" ) + plt.plot(t.state[:,5], label="s (distance to center)" ) + plt.xlabel("Time/seconds") + plt.legend() + savepdf("pid_car_agent") + plt.show() diff --git a/irlc/ex04/pid_locomotive_agent.py b/irlc/ex04/pid_locomotive_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..bb2083c454f86465e7edc4d055518898f88fcc5c --- /dev/null +++ b/irlc/ex04/pid_locomotive_agent.py @@ -0,0 +1,70 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +import matplotlib.pyplot as plt +from irlc.ex04.locomotive import LocomotiveEnvironment +from irlc.ex04.pid_car import PID +from irlc import Agent, train +from irlc import savepdf +from irlc.ex04.control_environment import ControlEnvironment + +class PIDLocomotiveAgent(Agent): + def __init__(self, env: ControlEnvironment, dt, Kp=1.0, Ki=0.0, Kd=0.0, target=0): + # self.pid = PID(dt=...) + # TODO: 1 lines missing. + raise NotImplementedError("Make a pid instance here.") + super().__init__(env) + + def pi(self, x, k, info=None): + # TODO: 1 lines missing. + raise NotImplementedError("Get the correct action using self.pid.pi(...). Same as previous exercise") + u = np.clip(u, self.env.action_space.low[0], self.env.action_space.high[0]) # Clip actions to ensure u is in the action space + return np.asarray([u]) # Must return actions as numpy ndarrays. + +def fixplt(): + plt.legend() + plt.grid('on') + plt.box(False) + # plt.ylim([-dd, dd]) + plt.xlabel('Time/seconds') + plt.ylabel('$x(t)$') + +def pid_locomotive(): + dt = .08 + m = 70 + Tmax=15 + + env = LocomotiveEnvironment(m=m, slope=0, dt=dt, Tmax=Tmax, render_mode='human') + Kp = 40 + agent = PIDLocomotiveAgent(env, dt=dt, Kp=Kp, Ki=0, Kd=0, target=0) + stats, traj = train(env, agent, return_trajectory=True) + plt.plot(traj[0].time, traj[0].state[:, 0], '-', label=f"$K_p={40}$") + fixplt() + savepdf('pid_locomotive_Kp') + plt.show() + + # Now include a derivative term: + Kp = 40 + for Kd in [10, 50, 100]: + agent = PIDLocomotiveAgent(env, dt=dt, Kp=Kp, Ki=0, Kd=Kd, target=0) + stats, traj = train(env, agent, return_trajectory=True) + plt.plot(traj[0].time, traj[0].state[:, 0], '-', label=f"$K_p={Kp}, K_d={Kd}$") + fixplt() + savepdf('pid_locomotive_Kd') + plt.show() + env.close() + + # Derivative test: Include a slope term. For fun, let's also change the target. + env = LocomotiveEnvironment(m=m, slope=2, dt=dt, Tmax=20, target=1, render_mode='human') + for Ki in [0, 10]: + agent = PIDLocomotiveAgent(env, dt=dt, Kp=40, Ki=Ki, Kd=50, target=1) + stats, traj = train(env, agent, return_trajectory=True) + x = traj[0].state + tt = traj[0].time + plt.plot(tt, x[:, 0], '-', label=f"$K_p={Kp}, K_i={Ki}, K_d={Kd}$") + fixplt() + savepdf('pid_locomotive_Ki') + plt.show() + env.close() + +if __name__ == '__main__': + pid_locomotive() diff --git a/irlc/ex04/pid_lunar.py b/irlc/ex04/pid_lunar.py new file mode 100644 index 0000000000000000000000000000000000000000..7af982d9bf3e50d575e9c275f6b8318977531e6d --- /dev/null +++ b/irlc/ex04/pid_lunar.py @@ -0,0 +1,136 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" +For information about the Apollo 11 lunar lander see: +https://eli40.com/lander/02-debrief/ + +For code for the Gym LunarLander environment see: + +https://github.com/openai/gym/blob/master/gym/envs/box2d/lunar_lander.py + +This particular controller is inspired by: + +https://github.com/wfleshman/PID_Control/blob/master/pid.py + +However, I had better success with different parameters for the PID controller. +""" +import gymnasium as gym +import matplotlib.pyplot as plt +import numpy as np +from irlc import train +from irlc.ex04.pid import PID +from irlc import Agent +from irlc.ex04 import speech +from irlc import savepdf +from gymnasium.envs.box2d.lunar_lander import FPS + +class ApolloLunarAgent(Agent): + def __init__(self, env, dt, Kp_altitude=18, Kd_altitude=13, Kp_angle=-18, Kd_angle=-18): + """ Set up PID parameters for the two controllers (one controlling the altitude, another the angle of the lander) """ + self.Kp_altitude = Kp_altitude + self.Kd_altitude = Kd_altitude + self.Kp_angle = Kp_angle + self.Kd_angle = Kd_angle + self.error_angle = [] + self.error_altitude = [] + self.dt = dt + super().__init__(env) + + def pi(self, x, k, info=None): + """ From documentation: https://github.com/openai/gym/blob/master/gym/envs/box2d/lunar_lander.py + x (list): The state. Attributes: + x[0] is the horizontal coordinate + x[1] is the vertical coordinate + x[2] is the horizontal speed + x[3] is the vertical speed + x[4] is the angle + x[5] is the angular speed + x[6] 1 if first leg has contact, else 0 + x[7] 1 if second leg has contact, else 0 + + Your implementation should follow what happens in: + + https://github.com/wfleshman/PID_Control/blob/master/pid.py + + I.e. you have to compute the target for the angle and altitude as done in the code (and explained in the documentation. + Note the target for the PID controllers is 0. + """ + if k == 0: + """ At time t=0 we set up the two PID controllers. You don't have to change these lines. """ + self.pid_alt = PID(dt=self.dt, Kp=self.Kp_altitude, Kd=self.Kd_altitude, Ki=0, target=0) + self.pid_ang = PID(dt=self.dt, Kp=self.Kp_angle, Kd=self.Kd_angle, Ki=0, target=0) + + """ Compute the PID control signals using two calls to the PID controllers such as: """ + # alt_adj = self.pid_alt.pi(...) + # ang_adj = self.pid_ang.pi(...) + """ You need to specify the inputs to the controllers. Look at the code in the link above and implement a comparable control rule. + The inputs you give to the controller will be simple functions of the coordinates of x, i.e. x[0], x[1], and so on. + """ + # TODO: 2 lines missing. + raise NotImplementedError("Compute the alt_adj and ang_adj as in the gitlab repo (see code comment).") + + u = np.array([alt_adj, ang_adj]) + u = np.clip(u, -1, +1) + + # If the legs are on the ground we made it, kill engines + if (x[6] or x[7]): + u[:] = 0 + # Record stats. + self.error_altitude.append(self.pid_alt.e_prior) + self.error_angle.append(self.pid_ang.e_prior) + return u + +def get_lunar_lander(env): + dt = 1/FPS # Get time discretization from environment. + spars = ['Kp_altitude', 'Kd_altitude', 'Kp_angle', 'Kd_angle'] + def x2pars(x2): + return {spars[i]: x2[i] for i in range(4)} + x_opt = np.asarray([52.23302414, 34.55938593, -80.68722976, -38.04571655]) + agent = ApolloLunarAgent(env, dt=dt, **x2pars(x_opt)) + return agent + +def lunar_single_mission(): + env = gym.make('LunarLanderContinuous-v2', render_mode='human') + env._max_episode_steps = 1000 # We don't want it to time out. + + agent = get_lunar_lander(env) + stats, traj = train(env, agent, return_trajectory=True, num_episodes=1) + env.close() + if traj[0].reward[-1] == 100: + print("A small step for man, a giant leap for mankind!") + elif traj[0].reward[-1] == -100: + print(speech) + else: + print("Environment timed out and the lunar module is just kind of floating around") + + states = np.stack(traj[0].state) + plt.plot(states[:, 0], label='x') + plt.plot(states[:, 1], label='y') + plt.plot(states[:, 2], label='vx') + plt.plot(states[:, 3], label='vy') + plt.plot(states[:, 4], label='theta') + plt.plot(states[:, 5], label='vtheta') + plt.legend() + plt.grid() + plt.ylim(-1.1, 1.1) + plt.title('PID Control') + plt.ylabel('Value') + plt.xlabel('Steps') + savepdf("pid_lunar_trajectory") + plt.show(block=False) + +def lunar_average_performance(): + env = gym.make('LunarLanderContinuous-v2', render_mode=None) # Set render_mode = 'human' to see what it does. + env._max_episode_steps = 1000 # To avoid the environment timing out after just 200 steps + + agent = get_lunar_lander(env) + stats, traj = train(env, agent, return_trajectory=True, num_episodes=20) + env.close() + + n_won = sum([np.sum(t.reward[-1] == 100) for t in traj]) + n_lost = sum([np.sum(t.reward[-1] == -100) for t in traj]) + print("Successfull landings: ", n_won, "of 20") + print("Unsuccessfull landings: ", n_lost, "of 20") + +if __name__ == "__main__": + lunar_single_mission() + lunar_average_performance() diff --git a/irlc/ex04/pid_pendulum.py b/irlc/ex04/pid_pendulum.py new file mode 100644 index 0000000000000000000000000000000000000000..82e865b853b734b3003f42a04df6cba43f3b9a2e --- /dev/null +++ b/irlc/ex04/pid_pendulum.py @@ -0,0 +1,74 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +import matplotlib.pyplot as plt +np.random.seed(32) +from irlc import Agent, savepdf +from irlc.ex04.pid import PID +from irlc.ex01.agent import train + +class PIDPendulumAgent(Agent): + def __init__(self, env, dt, Kp=1.0, Ki=0.0, Kd=0.0, target_angle=0): + """ Balance_to_x0 = True implies the agent should also try to get the cartpole to x=0 (i.e. center). + If balance_to_x0 = False implies it is good enough for the agent to get the cart upright. + """ + self.pid = PID(dt=dt, Kp = Kp, Ki=Ki, Kd=Kd, target=target_angle) + super().__init__(env) + + def pi(self, x, k, info=None): + """ Compute action using self.pid. YCartpoleou have to think about the inputs as they will depend on + whether balance_to_x0 is true or not. """ + # TODO: 2 lines missing. + raise NotImplementedError("Implement function body") + return u + + +def get_offbalance_pendulum(waiting_steps=30): + from irlc.ex04.model_pendulum import ThetaPendulumEnvironment + env = ThetaPendulumEnvironment(Tmax=10, render_mode='human') + + env.reset() + env.state[0] = 0 + env.state[1] = 0 + for _ in range(waiting_steps): # Simulate the environment for 30 steps to get things out of balance. + env.step(1) + return env + +def plot_trajectory(trajectory): + t = trajectory + plt.plot(t.time, t.state[:,0], label="Angle $\\theta$" ) + plt.plot(t.time, t.state[:,1], label="Angular speed $\\cdot{\\theta}$") + plt.xlabel("Time") + plt.legend() + + +target_angle = np.pi/6 # The target angle for the second task in the pendulum problem. +if __name__ == "__main__": + """ + First task: Bring the balance upright from a slightly off-center position. + For this task, we do not care about the x-position, only the angle theta which should be 0 (upright) + """ + env = get_offbalance_pendulum(30) + ## TODO: Half of each line of code in the following 1 lines have been replaced by garbage. Make it work and remove the error. + #---------------------------------------------------------------------------------------------------------------------------- + # agent = PIDPendulumAgent(env, dt=env.?????????????????????????????????????? + raise NotImplementedError("Define your agent here (including parameters)") + _, trajectories = train(env, agent, num_episodes=1, return_trajectory=True, reset=False) # Note reset=False to maintain initial conditions. + env.close() + plot_trajectory(trajectories[0]) + savepdf("pid_pendulumA") + plt.show() + + """ + Second task: We will now try to get to a target angle of target_angle=np.pi/6. + """ + env = get_offbalance_pendulum(30) + ## TODO: Half of each line of code in the following 1 lines have been replaced by garbage. Make it work and remove the error. + #---------------------------------------------------------------------------------------------------------------------------- + # agent = PIDPendulumAgent(env, dt=env.dt,????????????????????????????????????????? + raise NotImplementedError("Define your agent here (include the target_angle parameter to the agent!)") + _, trajectories = train(env, agent, num_episodes=1, return_trajectory=True, reset=False) # Note reset=False to maintain initial conditions. + env.close() + plot_trajectory(trajectories[0]) + print("Final state is x(t_F) =", trajectories[0].state[-1], f"goal [{target_angle:.2f}, 0]") + savepdf("pid_pendulumB") + plt.show() diff --git a/irlc/ex05/__init__.py b/irlc/ex05/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..23f7751213a745f38295d78a273a61b6a1ce7ffc --- /dev/null +++ b/irlc/ex05/__init__.py @@ -0,0 +1,2 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""This directory contains the exercises for week 5.""" diff --git a/irlc/ex05/__pycache__/__init__.cpython-311.pyc b/irlc/ex05/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1b9bf1d95f19da97a2af54288ce60d0d9100ed11 Binary files /dev/null and b/irlc/ex05/__pycache__/__init__.cpython-311.pyc differ diff --git a/irlc/ex05/__pycache__/direct.cpython-311.pyc b/irlc/ex05/__pycache__/direct.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..76f0a425273c0209db1eb6913f557682f56014ee Binary files /dev/null and b/irlc/ex05/__pycache__/direct.cpython-311.pyc differ diff --git a/irlc/ex05/direct.py b/irlc/ex05/direct.py new file mode 100644 index 0000000000000000000000000000000000000000..b38379afda4c16bc99e554214e5bff7e8f96d3c6 --- /dev/null +++ b/irlc/ex05/direct.py @@ -0,0 +1,370 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [Her24] Tue Herlau. Sequential decision making. (Freely available online), 2024. +""" +from irlc.ex03.control_model import ControlModel +import numpy as np +import sympy as sym +import sys +from scipy.optimize import Bounds, minimize +from scipy.interpolate import interp1d +from irlc.ex03.control_model import symv +from irlc.ex04.discrete_control_model import sympy_modules_ +from irlc import Timer +from tqdm import tqdm + +def bounds2fun(t0 : float, tF : float, bounds : Bounds): + """ + Given start and end times [t0, tF] and a scipy Bounds object with upper/lower bounds on some variable x, i.e. so that: + + > bounds.lb <= x <= bounds.ub + + this function returns a new function f such that f(t0) equals bounds.lb and f(tF) = bounds.ub and + f(t) interpolates between the uppower/lower bounds linearly, i.e. + + > bounds.lb <= f(t) <= bounds.ub + + The function will return a numpy ``ndarray``. + """ + return interp1d(np.asarray([t0, tF]), np.stack([np.reshape(b, (-1,)) for b in bounds], axis=1)) + +def direct_solver(model, options): + """ + Main direct solver method, see (Her24, Algorithm 21). Given a list of options of length S, the solver performers collocation + using the settings found in the dictionary options[i], and use the result of options[i] to initialize collocation on options[i+1]. + + This iterative refinement scheme is required to obtain good overall solutions. + + :param model: A ContinuousTimeModel instance + :param options: An options-structure. This is a list of dictionaries of options for each collocation iteration + :return: A list of solutions, one for each collocation step. The last will be the 'best' solution (highest N) + + """ + if isinstance(options, dict): + options = [options] + solutions = [] # re-use result of current solutions to initialize next with a higher value of N + for i, opt in enumerate(options): + optimizer_options = opt['optimizer_options'] # to be passed along to minimize() + if i == 0 or "guess" in opt: + # No solutions-function is given. Re-calculate by linearly interpreting bounds (see (Her24, Subsection 15.3.4)) + guess = opt['guess'] + guess['u'] = bounds2fun(guess['t0'],guess['tF'],guess['u']) if isinstance(guess['u'], list) else guess['u'] + guess['x'] = bounds2fun(guess['t0'],guess['tF'],guess['x']) if isinstance(guess['x'], list) else guess['x'] + else: + """ For an iterative solver ((Her24, Subsection 15.3.4)), initialize the guess at iteration i to be the solution at iteration i-1. + The guess consists of a guess for t0, tF (just numbers) as well as x, u (state/action trajectories), + the later two being functions. The format of the guess is just a dictionary (you have seen several examples) + i.e. + + > guess = {'t0': (number), 'tF': (number), 'x': (function), 'u': (function)} + + and you can get the solution by using solutions[i - 1]['fun']. (insert a breakpoint and check the fields) + """ + # TODO: 1 lines missing. + raise NotImplementedError("Define guess = {'t0': ..., ...} here.") + N = opt['N'] + print(f"{i}> Collocation starting with grid-size N={N}") + sol = collocate(model, N=N, optimizer_options=optimizer_options, guess=guess, verbose=opt.get('verbose', False)) + solutions.append(sol) + + print("Was collocation success full at each iteration?") + for i, s in enumerate(solutions): + print(f"{i}> Success? {s['solver']['success']}") + return solutions + +def collocate(model : ControlModel, N=25, optimizer_options=None, guess : dict = None, verbose=True): + r""" + Performs collocation by discretizing the model using a grid-size of N and optimize to find the optimal solution. + The 'model' should be a ControlModel instance, optimizer_options contains options for the optimizer, and guess + is a dictionary used to initialize the optimizer containing keys:: + + guess = {'t0': Start time (float), + 'tF': Terminal time (float), + 'x': A *function* which takes time as input and return a guess for x(t), + 'u': A *function* which takes time as input and return a guess for u(t), + } + + So for instance + + .. code-block:: python + + guess['x'](0.5) + + will return the state :math:`\mathbf x(0.5)` as a numpy ndarray. + + The overall structure of the optimization procedure is as follows: + + #. Define the following variables. They will all be lists: + - ``z``: Variables to be optimized over. Each element ``z[k]`` is a symbolic variable. This will allow us to compute derivatives. + - ``z0``: A list of numbers representing the initial guess. Computed using 'guess' (above) + - ``z_lb``, ``z_ub``: Lists of numbers representting the upper/lower bounds on z. Use bound-methods in :class:`irlc.ex03.control_model.ControlModel` to get these. + #. Create a symbolic expression representing the cost-function J + This is defined using the symbolic variables similar to the toy-problem we saw last week. This allows us to compute derivatives of the cost + #. Create *symbolic* expressions representing all constraints + The lists ``Iineq`` and ``Ieq`` contains *lists* of constraints. The solver will ensure that for any i:: + + Ieq[i] == 0 + + and:: + + Iineq[i] <= 0 + + This allows us to just specify each element in 'eqC' and 'ineqC' as a single symbolic expression. Once more, we use symbolic expressions so + derivatives can be computed automatically. The most important constraints are in 'eqC', as these must include the collocation-constraints (see algorithm in notes) + #. Compile all symbolic expressions into a format useful for the optimizer + The optimizer accepts numpy functions, so we turn all symbolic expressions and derivatives into numpy (similar to the example last week). + It is then fed into the optimizer and, fingers crossed, the optimizer spits out a value 'z*', which represents the optimal values. + + #. Unpack z: + The value 'z*' then has to be unpacked and turned into function u*(t) and x*(t) (as in the notes). These functions can then be put into the + solution-dictionary and used to initialize the next guess (or assuming we terminate, these are simply our solution). + + :param model: A :class:`irlc.ex03.control_model.ControlModel` instance + :param N: The number of collocation knot points :math:`N` + :param optimizer_options: Options for the scipy optimizer. You can ignore this. + :param guess: A dictionary containing the initial guess. See the online documentation. + :param verbose: Whether to print out extra details during the run. Useful only for debugging. + :return: A dictionary containing the solution. It is compatible with the :python:`guess` datastructure . + """ + timer = Timer(start=True) + cost = model.get_cost() + t0, tF = sym.symbols("t0"), sym.symbols("tF") + ts = t0 + np.linspace(0, 1, N) * (tF-t0) # N points linearly spaced between [t0, tF] TODO: Convert this to a list. + xs, us = [], [] + for i in range(N): + xs.append(list(symv("x_%i_" % i, model.state_size))) + us.append(list(symv("u_%i_" % i, model.action_size))) + + ''' (1) Construct guess z0, all simple bounds [z_lb, z_ub] for the problem and collect all symbolic variables as z ''' + # sb = model.simple_bounds() # get simple inequality boundaries in problem (v_lb <= v <= v_ub) + z = [] # list of all *symbolic* variables in the problem + # These lists contain the guess z0 and lower/upper bounds (list-of-numbers): z_lb[k] <= z0[k] <= z_ub[k]. + # They should be lists of *numbers*. + z0, z_lb, z_ub = [], [], [] + ts_eval = sym.lambdify((t0, tF), ts.tolist(), modules='numpy') + for k in range(N): + x_low = list(model.x0_bound().low if k == 0 else (model.xF_bound().low if k == N - 1 else model.x_bound().low)) + x_high = list(model.x0_bound().high if k == 0 else (model.xF_bound().high if k == N - 1 else model.x_bound().high)) + u_low, u_high = list(model.u_bound().low), list(model.u_bound().high) + + tk = ts_eval(guess['t0'], guess['tF'])[k] + """ In these lines, update z, z0, z_lb, and z_ub with values corresponding to xs[k], us[k]. + The values are all lists; i.e. z[j] (symbolic) has guess z0[j] (float) and bounds z_lb[j], z_ub[j] (floats) """ + # TODO: 2 lines missing. + raise NotImplementedError("Updates for x_k, u_k") + + """ Update z, z0, z_lb, and z_ub with bounds/guesses corresponding to t0 and tF (same format as above). """ + # z, z0, z_lb, z_ub = z+[t0], z0+[guess['t0']], z_lb+[model.bounds['t0_low']], z_ub+[model.bounds['t0_high']] + # TODO: 2 lines missing. + raise NotImplementedError("Updates for t0, tF") + assert len(z) == len(z0) == len(z_lb) == len(z_ub) + if verbose: + print(f"z={z}\nz0={np.asarray(z0).round(1).tolist()}\nz_lb={np.asarray(z_lb).round(1).tolist()}\nz_ub={np.asarray(z_ub).round(1).tolist()}") + print(">>> Trapezoid collocation of problem") # problem in this section + fs, cs = [], [] # lists of symbolic variables corresponding to f_k and c_k, see (Her24, Algorithm 20). + for k in range(N): + """ Update both fs and cs; these are lists of symbolic expressions such that fs[k] corresponds to f_k and cs[k] to c_k in the slides. + Use the functions env.sym_f and env.sym_c """ + # fs.append( symbolic variable corresponding to f_k; see env.sym_f). similarly update cs.append(env.sym_c(...) ). + ## TODO: Half of each line of code in the following 2 lines have been replaced by garbage. Make it work and remove the error. + #---------------------------------------------------------------------------------------------------------------------------- + # fs.append(model.sym_f(x=????????????????????????? + # cs.append(cost.sym_c(x=x???????????????????????? + raise NotImplementedError("Compute f[k] and c[k] here (see slides) and add them to above lists") + + J = cost.sym_cf(x0=xs[0], t0=t0, xF=xs[-1], tF=tF) # terminal cost; you need to update this variable with all the cs[k]'s. + Ieq, Iineq = [], [] # all symbolic equality/inequality constraints are stored in these lists + for k in range(N - 1): + # Update cost function ((Her24, eq. (15.15))). Use the above defined symbolic expressions ts, hk and cs. + # TODO: 2 lines missing. + raise NotImplementedError("Update J here") + # Set up equality constraints. See (Her24, eq. (15.18)). + for j in range(model.state_size): + """Create all collocation equality-constraints here and add them to Ieq. I.e. + + xs[k+1] - xs[k] = 0.5 h_k (f_{k+1} + f_k) + + Note we have to create these coordinate-wise which is why we loop over j. + """ + ## TODO: Half of each line of code in the following 1 lines have been replaced by garbage. Make it work and remove the error. + #---------------------------------------------------------------------------------------------------------------------------- + # Ieq.append((xs[k+1][j] - xs[k][j])?????????????????????????????????? + raise NotImplementedError("Update collocation constraints here") + """ + To solve problems with dynamical path constriants like Brachiostone, update Iineq here to contain the + inequality constraint model.sym_h(...) <= 0. For the other problems this can simply be left blank """ + if hasattr(model, 'sym_h'): + # TODO: 1 lines missing. + raise NotImplementedError("Update symbolic path-dependent constraint h(x,u,t)<=0 here") + + print(">>> Creating objective and derivative...") + timer.tic("Building symbolic objective") + J_fun = sym.lambdify([z], J, modules='numpy') # create a python function from symbolic expression + # To compute the Jacobian, you can use sym.derive_by_array(J, z) to get the correct symbolic expression, then use sym.lamdify (as above) to get a numpy function. + ## TODO: Half of each line of code in the following 1 lines have been replaced by garbage. Make it work and remove the error. + #---------------------------------------------------------------------------------------------------------------------------- + # J_jac = sym.lambdify([z], sym.deri??????????????????????????????????? + raise NotImplementedError("Jacobian of J. See how this is computed for equality/inequality constratins for help.") + if verbose: + print(f"{Ieq=}\n{Iineq=}\n{J=}") + timer.toc() + print(">>> Differentiating equality constraints..."), timer.tic("Differentiating equality constraints") + constraints = [] + for eq in tqdm(Ieq, file=sys.stdout): # don't write to error output. + constraints.append(constraint2dict(eq, z, type='eq')) + timer.toc() + print(">>> Differentiating inequality constraints"), timer.tic("Differentiating inequality constraints") + constraints += [constraint2dict(ineq, z, type='ineq') for ineq in Iineq] + timer.toc() + + c_viol = sum(abs(np.minimum(z_ub - np.asarray(z0), 0))) + sum(abs(np.maximum(np.asarray(z_lb) - np.asarray(z0), 0))) + if c_viol > 0: # check if: z_lb <= z0 <= z_ub. Violations only serious if large + print(f">>> Warning! Constraint violations found of total magnitude: {c_viol:4} before optimization") + + print(">>> Running optimizer..."), timer.tic("Optimizing") + z_B = Bounds(z_lb, z_ub) + res = minimize(J_fun, x0=z0, method='SLSQP', jac=J_jac, constraints=constraints, options=optimizer_options, bounds=z_B) + # Compute value of equality constraints to check violations + timer.toc() + eqC_fun = sym.lambdify([z], Ieq) + eqC_val_ = eqC_fun(res.x) + eqC_val = np.zeros((N - 1, model.state_size)) + + x_res = np.zeros((N, model.state_size)) + u_res = np.zeros((N, model.action_size)) + t0_res = res.x[-2] + tF_res = res.x[-1] + + m = model.state_size + model.action_size + for k in range(N): + dx = res.x[k * m:(k + 1) * m] + if k < N - 1: + eqC_val[k, :] = eqC_val_[k * model.state_size:(k + 1) * model.state_size] + x_res[k, :] = dx[:model.state_size] + u_res[k, :] = dx[model.state_size:] + + # Generate solution structure + ts_numpy = ts_eval(t0_res, tF_res) + # make linear interpolation similar to (Her24, eq. (15.22)) + ufun = interp1d(ts_numpy, np.transpose(u_res), kind='linear') + # Evaluate function values fk points (useful for debugging but not much else): + f_eval = sym.lambdify((t0, tF, xs, us), fs) + fs_numpy = f_eval(t0_res, tF_res, x_res, u_res) + fs_numpy = np.asarray(fs_numpy) + + r""" Interpolate to get x(t) as described in (Her24, eq. (15.26)). The function should accept both lists and numbers for t.""" + x_fun = lambda t_new: np.stack([trapezoid_interpolant(ts_numpy, np.transpose(x_res), np.transpose(fs_numpy), t_new=t) for t in np.reshape(np.asarray(t_new), (-1,))], axis=1) + + if verbose: + newt = np.linspace(ts_numpy[0], ts_numpy[-1], len(ts_numpy)-1) + print( x_fun(newt) ) + + sol = { + 'grid': {'x': x_res, 'u': u_res, 'ts': ts_numpy, 'fs': fs_numpy}, + 'fun': {'x': x_fun, 'u': ufun, 'tF': tF_res, 't0': t0_res}, + 'solver': res, + 'eqC_val': eqC_val, + 'inputs': {'z': z, 'z0': z0, 'z_lb': z_lb, 'z_ub': z_ub}, + } + print(timer.display()) + return sol + +def trapezoid_interpolant(ts : list, xs : list, fs : list, t_new=None): + r""" + This function implements (Her24, eq. (15.26)) to evaluate :math:`\mathbf{x}(t)` at a point :math:`t =` ``t_new``. + + The other inputs represent the output of the direct optimization procedure. I.e., ``ts`` is a list of length + :math:`N+1` corresponding to :math:`t_k`, ``xs`` is a list of :math:`\mathbf x_k`, and ``fs`` is a list corresponding + to :math:`\mathbf f_k`. To implement the method, you should first determine which :math:`k` the new time point ``t_new`` + corresponds to, i.e. where :math:`t_k \leq t_\text{new} < t_{k+1}`. + + + :param ts: List of time points ``[.., t_k, ..]`` + :param xs: List of numpy ndarrays ``[.., x_k, ...]`` + :param fs: List of numpy ndarrays ``[.., f_k, ...]`` + :param t_new: The time point we should evaluate the function in. + :return: The state evaluated at time ``t_new``, i.e. :math:`\mathbf x(t_\text{new})`. + """ + # TODO: 3 lines missing. + raise NotImplementedError("Determine the time index k here so that ts[k] <= t_new < ts[k+1].") + + ts = np.asarray(ts) + tau = t_new - ts[k] + hk = ts[k + 1] - ts[k] + r""" + Make interpolation here. Should be a numpy array of dimensions [xs.shape[0], len(I)] + What the code does is that for each t in ts, we work out which knot-point interval the code falls within. I.e. + insert a breakpoint and make sure you understand what e.g. the code tau = t_new - ts[I] does. + + Given this information, we can recover the relevant (evaluated) knot-points as for instance + fs[:,I] and those at the next time step as fs[:,I]. With this information, the problem is simply an + implementation of (Her24, eq. (15.26)), i.e. + + > x_interp = xs[:,I] + tau * fs[:,I] + (...) + + """ + ## TODO: Half of each line of code in the following 1 lines have been replaced by garbage. Make it work and remove the error. + #---------------------------------------------------------------------------------------------------------------------------- + # x_interp = xs[:, k] + tau * fs[:, k] + (tau ???????????????????????????????????????????? + raise NotImplementedError("Insert your solution and remove this error.") + return x_interp + + +def constraint2dict(symb, all_vars, type='eq'): + ''' Turn constraints into a dict with type, fun, and jacobian field. ''' + if type == "ineq": symb = -1 * symb # To agree with sign convention in optimizer + + f = sym.lambdify([all_vars], symb, modules=sympy_modules_) + # np.atan = np.arctan # Monkeypatch numpy to contain atan. Passing "numpy" does not seem to fix this. + jac = sym.lambdify([all_vars], sym.derive_by_array(symb, all_vars), modules=sympy_modules_) + eq_cons = {'type': type, + 'fun': f, + 'jac': jac} + return eq_cons + +def get_opts(N, ftol=1e-6, guess=None, verbose=False): # helper function to instantiate options objet. + d = {'N': N, + 'optimizer_options': {'maxiter': 1000, + 'ftol': ftol, + 'iprint': 1, + 'disp': True, + 'eps': 1.5e-8}, # 'eps': 1.4901161193847656e-08, + 'verbose': verbose} + if guess: + d['guess'] = guess + return d + +def guess(model : ControlModel): + def mfin(z): + return [z_ if np.isfinite(z_) else 0 for z_ in z] + xL = mfin(model.x0_bound().low) + xU = mfin(model.xF_bound().high) + tF = 10 if not np.isfinite(model.tF_bound().high[0]) else model.tF_bound().high[0] + gs = {'t0': 0, + 'tF': tF, + 'x': [xL, xU], + 'u': [mfin(model.u_bound().low), mfin(model.u_bound().high)]} + return gs + + +def run_direct_small_problem(): + from irlc.ex04.model_pendulum import SinCosPendulumModel + model = SinCosPendulumModel() + """ + Test out implementation on a very small grid. The overall solution will be fairly bad, + but we can print out the various symbolic expressions + + We use verbose=True to get debug-information. + """ + print("Solving with a small grid, N=5") + options = [get_opts(N=5, ftol=1e-3, guess=guess(model), verbose=True)] + solutions = direct_solver(model, options) + return model, solutions + + +if __name__ == "__main__": + from irlc.ex05.direct_plot import plot_solutions + model, solutions = run_direct_small_problem() + plot_solutions(model, solutions, animate=False, pdf="direct_pendulum_small") diff --git a/irlc/ex05/direct_agent.py b/irlc/ex05/direct_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..e8cbca2f99735e27da5518d5f686194c82211f71 --- /dev/null +++ b/irlc/ex05/direct_agent.py @@ -0,0 +1,77 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex05.direct import direct_solver, get_opts, guess +from irlc.ex04.model_pendulum import SinCosPendulumModel +from irlc.ex04.discrete_control_model import DiscreteControlModel +from irlc.ex04.control_environment import ControlEnvironment +from irlc import train +from irlc import Agent +import numpy as np +import matplotlib.pyplot as plt +from irlc import savepdf +from irlc.ex05.direct_plot import plot_solutions + +class DirectAgent(Agent): + def __init__(self, env: ControlEnvironment, options=None): + cmod = env.discrete_model.continuous_model # Get the continuous-time model for planning + + if options is None: + options = [get_opts(N=10, ftol=1e-3, guess=guess(cmod), verbose=False), + get_opts(N=60, ftol=1e-6, verbose=False) + ] + solutions = direct_solver(cmod, options) + + # The next 3 lines are for plotting purposes. You can ignore them. + self.x_grid = np.stack([env.discrete_model.phi_x(x) for x in solutions[-1]['grid']['x']]) + self.u_grid = np.stack([env.discrete_model.phi_u(u) for u in solutions[-1]['grid']['u']]) + self.ts_grid = np.stack(solutions[-1]['grid']['ts']) + # set self.ufun equal to the solution (policy) function. You can get it by looking at `solutions` computed above + self.solutions = solutions + # TODO: 1 lines missing. + raise NotImplementedError("set self.ufun = solutions[....][somethingsomething] (insert a breakpoint, it should be self-explanatory).") + super().__init__(env) + + def pi(self, x, k, info=None): + """ Return the action given x and t. As a hint, you will only use t, and self.ufun computed a few lines above""" + # TODO: 7 lines missing. + raise NotImplementedError("Implement function body") + return u + +def train_direct_agent(animate=True, plot=False): + from irlc.ex04.model_pendulum import PendulumModel + model = PendulumModel() + """ + Test out implementation on a fairly small grid. Note this will work fairly terribly. + """ + guess = {'t0': 0, + 'tF': 4, + 'x': [np.asarray([0, 0]), np.asarray([np.pi, 0])], + 'u': [np.asarray([0]), np.asarray([0])]} + + options = [get_opts(N=10, ftol=1e-3, guess=guess), + get_opts(N=20, ftol=1e-3), + get_opts(N=80, ftol=1e-6) + ] + + dmod = DiscreteControlModel(model=model, dt=0.1) # Discretize the pendulum model. Used for creating the environment. + denv = ControlEnvironment(discrete_model=dmod, Tmax=4, render_mode='human' if animate else None) + agent = DirectAgent(denv, options=options) + denv.Tmax = agent.solutions[-1]['fun']['tF'] # Specify max runtime of the environment. Must be based on the Agent's solution. + stats, traj = train(denv, agent=agent, num_episodes=1, return_trajectory=True) + + if plot: + from irlc import plot_trajectory + plot_trajectory(traj[0], env=denv) + savepdf("direct_agent_pendulum") + plt.show() + + return stats, traj, agent + +if __name__ == "__main__": + stats, traj, agent = train_direct_agent(animate=True, plot=True) + print("Obtained cost", -stats[0]['Accumulated Reward']) + + # Let's try to plot the state-vectors for the two models. They are not going to agree that well. + plt.plot(agent.ts_grid, agent.x_grid, 'r-', label="Direct solver prediction") + plt.plot(traj[0].time, traj[0].state, 'k-', label='Simulation') + plt.legend() + plt.show() diff --git a/irlc/ex05/direct_brachistochrone.py b/irlc/ex05/direct_brachistochrone.py new file mode 100644 index 0000000000000000000000000000000000000000..2aaf14e8dc0257fe29f5c5beebaac14ef659b5cd --- /dev/null +++ b/irlc/ex05/direct_brachistochrone.py @@ -0,0 +1,59 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +import matplotlib.pyplot as plt +from irlc import savepdf +from irlc.ex05.model_brachistochrone import ContiniouBrachistochrone +from irlc.ex05.direct import direct_solver, get_opts, guess +from irlc.ex05.direct_plot import plot_solutions + +def plot_brachistochrone_solutions(model, solutions, out=None): + plot_solutions(model, solutions, animate=False, pdf=out) + for index, sol in enumerate(solutions): + x_res = sol['grid']['x'] + plt.figure(figsize=(5,5)) + plt.plot( x_res[:,0], x_res[:,1]) + xF = model.bounds['xF_low'] + plt.plot([0, 0], [0, xF[1]], 'r-') + plt.plot([0, xF[0]], [xF[1], xF[1]], 'r-') + # plt.title("Curve in x/y plane") + plt.xlabel("$x$-position") + plt.ylabel("$y$-position") + if model.h is not None: + # add dynamical constraint. + xc = np.linspace(0, model.x_dist) + yc = -xc/2 - model.h + plt.plot(xc, yc, 'k-', linewidth=2) + plt.grid() + # plt.gca().invert_yaxis() + plt.gca().axis('equal') + if out: + savepdf(f"{out}_{index}") + plt.show() + pass + +def compute_unconstrained_solutions(): + model = ContiniouBrachistochrone(h=None, x_dist=1) + options = [get_opts(N=10, ftol=1e-3, guess=guess(model)), + get_opts(N=30, ftol=1e-6)] + # solve without constraints + solutions = direct_solver(model, options) + return model, solutions + +def compute_constrained_solutions(): + model_h = ContiniouBrachistochrone(h=0.1, x_dist=1) + options = [get_opts(N=10, ftol=1e-3, guess=guess(model_h)), + get_opts(N=30, ftol=1e-6)] + solutions_h = direct_solver(model_h, options) + return model_h, solutions_h + +if __name__ == "__main__": + """ + For further information see: + http://www.hep.caltech.edu/~fcp/math/variationalCalculus/variationalCalculus.pdf + """ + model, solutions = compute_unconstrained_solutions() + plot_brachistochrone_solutions(model, solutions[-1:], out="brachi") + + # solve with dynamical (sloped planc) constraint at height of h. + model_h, solutions_h = compute_constrained_solutions() + plot_brachistochrone_solutions(model_h, solutions_h[-1:], out="brachi_h") diff --git a/irlc/ex05/direct_cartpole_kelly.py b/irlc/ex05/direct_cartpole_kelly.py new file mode 100644 index 0000000000000000000000000000000000000000..1bf026828d35bae29ddf09ddae26b282b138d592 --- /dev/null +++ b/irlc/ex05/direct_cartpole_kelly.py @@ -0,0 +1,56 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [Kel17] Matthew Kelly. An introduction to trajectory optimization: how to do your own direct collocation. SIAM Review, 59(4):849–904, 2017. (See kelly2017.pdf). +""" +from irlc.ex05.direct import guess +from irlc.ex05.model_cartpole import CartpoleModel +from irlc.ex03.control_cost import SymbolicQRCost +from irlc.ex05.direct import direct_solver, get_opts +import numpy as np +from gymnasium.spaces import Box + +class KellyCartpoleModel(CartpoleModel): + """Completes the Cartpole swingup task in exactly 2 seconds. + + The only changes to the original cartpole model is the inclusion of a new bound on ``tf_bound(self)``, + to limit the end-time to :math:`t_F = 2`, and an updated cost function so that :math:`Q=0` and :math:`R=I`. + """ + def get_cost(self) -> SymbolicQRCost: + # TODO: 2 lines missing. + raise NotImplementedError("Construct and return a new cost-function here.") + + def tF_bound(self) -> Box: + # TODO: 2 lines missing. + raise NotImplementedError("Implement the bound on tF here") + +def make_cartpole_kelly17(): + """ + Creates Cartpole problem. Details about the cost function can be found in (Kel17, Section 6) + and details about the physical parameters can be found in (Kel17, Appendix E, table 3). + """ + # this will generate a different carpole environment with an emphasis on applying little force u. + duration = 2.0 + maxForce = 20 + model = KellyCartpoleModel(max_force=maxForce, mp=0.3, l=0.5, mc=1.0, dist=1) + guess2 = guess(model) + guess2['tF'] = duration # Our guess should match the constraints. + return model, guess2 + +def compute_solutions(): + model, guess = make_cartpole_kelly17() + options = [get_opts(N=10, ftol=1e-3, guess=guess), + get_opts(N=40, ftol=1e-6)] + solutions = direct_solver(model, options) + return model, solutions + +def direct_cartpole(): + model, solutions = compute_solutions() + from irlc.ex05.direct_plot import plot_solutions + print("Did we succeed?", solutions[-1]['solver']['success']) + plot_solutions(model, solutions, animate=True, pdf="direct_cartpole_force") + model.close() + +if __name__ == "__main__": + direct_cartpole() diff --git a/irlc/ex05/direct_cartpole_time.py b/irlc/ex05/direct_cartpole_time.py new file mode 100644 index 0000000000000000000000000000000000000000..ccf63363f8cd7ba4ec00b8e7fe075a25673b7c11 --- /dev/null +++ b/irlc/ex05/direct_cartpole_time.py @@ -0,0 +1,28 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex05.model_cartpole import CartpoleModel +from irlc.ex05.direct import direct_solver, get_opts +from irlc.ex05.direct_plot import plot_solutions +from irlc.ex05.direct import guess + +def compute_solutions(): + """ + See: https://github.com/MatthewPeterKelly/OptimTraj/blob/master/demo/cartPole/MAIN_minTime.m + """ + model = CartpoleModel(max_force=50, mp=0.5, mc=2.0, l=0.5) + guess2 = guess(model) + guess2['tF'] = 2 + guess2['u'] = [[0], [0]] + + options = [get_opts(N=8, ftol=1e-3, guess=guess2), # important. + get_opts(N=16, ftol=1e-6), # This is a hard problem and we need gradual grid-refinement. + get_opts(N=32, ftol=1e-6), + get_opts(N=70, ftol=1e-6) + ] + solutions = direct_solver(model, options) + return model, solutions + +if __name__ == "__main__": + model, solutions = compute_solutions() + x_sim, u_sim, t_sim = plot_solutions(model, solutions[:], animate=True, pdf="direct_cartpole_mintime") + model.close() + print("Did we succeed?", solutions[-1]['solver']['success']) diff --git a/irlc/ex05/direct_pendulum.py b/irlc/ex05/direct_pendulum.py new file mode 100644 index 0000000000000000000000000000000000000000..80ae5a76deca15aaddaa444d09542e9800a0f90e --- /dev/null +++ b/irlc/ex05/direct_pendulum.py @@ -0,0 +1,27 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex05.direct import direct_solver, get_opts +from irlc.ex04.model_pendulum import SinCosPendulumModel +from irlc.ex05.direct_plot import plot_solutions +import numpy as np + +def compute_pendulum_solutions(): + model = SinCosPendulumModel() + """ + Test out implementation on a fairly small grid. Note this will work fairly terribly. + """ + guess = {'t0': 0, + 'tF': 4, + 'x': [np.asarray([0, 0]), np.asarray([np.pi, 0])], + 'u': [np.asarray([0]), np.asarray([0])]} + + options = [get_opts(N=10, ftol=1e-3, guess=guess), + get_opts(N=20, ftol=1e-3), + get_opts(N=80, ftol=1e-6) + ] + + solutions = direct_solver(model, options) + return model, solutions + +if __name__ == "__main__": + model, solutions = compute_pendulum_solutions() + plot_solutions(model, solutions, animate=True, pdf="direct_pendulum_real") diff --git a/irlc/ex05/direct_plot.py b/irlc/ex05/direct_plot.py new file mode 100644 index 0000000000000000000000000000000000000000..67a324ae8a79b9091e86f4ed7f194305fa8efd95 --- /dev/null +++ b/irlc/ex05/direct_plot.py @@ -0,0 +1,82 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import matplotlib.pyplot as plt +import numpy as np +from irlc.ex03.control_model import plot_trajectory +from irlc import savepdf + +""" +Helper function for plotting. +""" +def plot_solutions(model, solutions, animate=True, pdf=None, plot_defects=True, Ix=None, animate_repeats=1, animate_all=False, plot=True): + + for k, sol in enumerate(solutions): + grd = sol['grid'] + x_res = sol['grid']['x'] + u_res = sol['grid']['u'] + ts = sol['grid']['ts'] + u_fun = lambda x, t: sol['fun']['u'](t) + N = len(ts) + if pdf is not None: + pdf_out = f"{pdf}_sol{N}" + + x_sim, u_sim, t_sim = model.simulate(x0=grd['x'][0, :], u_fun=u_fun, t0=grd['ts'][0], tF=grd['ts'][-1], N_steps=1000) + if animate and (k == len(solutions)-1 or animate_all): + for _ in range(animate_repeats): + animate_rollout(model, x0=grd['x'][0, :], u_fun=u_fun, t0=grd['ts'][0], tF=grd['ts'][-1], N_steps=1000, fps=30) + + eqC_val = sol['eqC_val'] + labels = model.state_labels + + if Ix is not None: + labels = [l for k, l in enumerate(labels) if k in Ix] + x_res = x_res[:,np.asarray(Ix)] + x_sim = x_sim[:,np.asarray(Ix)] + + print("Initial State: " + ",".join(labels)) + print(x_res[0]) + print("Final State:") + print(x_res[-1]) + if plot: + ax = plot_trajectory(x_res, ts, lt='ko-', labels=labels, legend="Direct state prediction $x(t)$") + plot_trajectory(x_sim, t_sim, lt='-', ax=ax, labels=labels, legend="RK4 exact simulation") + # plt.suptitle("State", fontsize=14, y=0.98) + # make_space_above(ax, topmargin=0.5) + + if pdf is not None: + savepdf(pdf_out +"_x") + plt.show(block=False) + # print("plotting...") + plot_trajectory(u_res, ts, lt='ko-', labels=model.action_labels, legend="Direct action prediction $u(t)$") + # print("plotting... B") + # plt.suptitle("Action", fontsize=14, y=0.98) + # print("plotting... C") + # make_space_above(ax, topmargin=0.5) + # print("plotting... D") + if pdf is not None: + savepdf(pdf_out +"_u") + plt.show(block=False) + if plot_defects: + plot_trajectory(eqC_val, ts[:-1], lt='-', labels=labels) + plt.suptitle("Defects (equality constraint violations)") + if pdf is not None: + savepdf(pdf_out +"_defects") + plt.show(block=False) + return x_sim, u_sim, t_sim + + +def animate_rollout(model, x0, u_fun, t0, tF, N_steps = 1000, fps=10): + """ Helper function to animate a policy. """ + + import time + # if sys.gettrace() is not None: + # print("Not animating stuff in debugger as it crashes.") + # return + y, _, tt = model.simulate(x0, u_fun, t0, tF, N_steps=N_steps) + secs = tF-t0 + frames = int( np.ceil( secs * fps ) ) + I = np.round( np.linspace(0, N_steps-1, frames)).astype(int) + y = y[I,:] + + for i in range(frames): + model.render(x=y[i], render_mode="human") + time.sleep(1/fps) diff --git a/irlc/ex05/model_brachistochrone.py b/irlc/ex05/model_brachistochrone.py new file mode 100644 index 0000000000000000000000000000000000000000..14c0ae74370488ec0147c9427826da025f74beb2 --- /dev/null +++ b/irlc/ex05/model_brachistochrone.py @@ -0,0 +1,55 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" +The Brachistochrone problem. See +https://apmonitor.com/wiki/index.php/Apps/BrachistochroneProblem +and (Bet10) + +References: + [Bet10] John T Betts. Practical methods for optimal control and estimation using nonlinear programming. Volume 19. Siam, 2010. +""" +import sympy as sym +import numpy as np +from irlc.ex03.control_model import ControlModel +from irlc.ex03.control_cost import SymbolicQRCost +from gymnasium.spaces import Box + +class ContiniouBrachistochrone(ControlModel): + state_labels= ["$x$", "$y$", "bead speed"] + action_labels = ['Tangent angle'] + + def __init__(self, g=9.82, h=None, x_dist=1): + self.g = g + self.h = h + self.x_dist = x_dist # or x_B + super().__init__() + + def get_cost(self) -> SymbolicQRCost: + # TODO: 1 lines missing. + raise NotImplementedError("Instantiate cost=SymbolicQRCost(...) here corresponding to minimum time.") + return cost + + def x0_bound(self) -> Box: + return Box(0, 0, shape=(self.state_size,)) + + def xF_bound(self) -> Box: + return Box(np.array([self.x_dist, -np.inf, -np.inf]), np.array([self.x_dist, np.inf, np.inf])) + + def sym_f(self, x, u, t=None): + # TODO: 3 lines missing. + raise NotImplementedError("Implement function body") + return xp + + def sym_h(self, x, u, t): + r""" + Add a dynamical constraint of the form + + .. math:: + + h(x, u, t) \leq 0 + """ + if self.h is None: + return [] + else: + # compute a single dynamical constraint as in (Bet10, Example (4.10)) (Note y-axis is reversed in the example) + # TODO: 1 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") diff --git a/irlc/ex05/model_cartpole.py b/irlc/ex05/model_cartpole.py new file mode 100644 index 0000000000000000000000000000000000000000..aea63db9a39f72c6dfde43b4da2d687f72194ff7 --- /dev/null +++ b/irlc/ex05/model_cartpole.py @@ -0,0 +1,173 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex04.discrete_control_cost import DiscreteQRCost +import sympy as sym +import numpy as np +import gymnasium as gym +from gymnasium.spaces import Box +from irlc.ex03.control_model import ControlModel +from irlc.ex03.control_cost import SymbolicQRCost +from irlc.ex04.discrete_control_model import DiscreteControlModel +from irlc.ex04.control_environment import ControlEnvironment + +class CartpoleModel(ControlModel): + state_labels = ["$x$", r"$\frac{dx}{dt}$", r"$\theta$", r"$\frac{d \theta}{dt}$"] + action_labels = ["Cart force $u$"] + + def __init__(self, mc=2, + mp=0.5, + l=0.5, + max_force=50, dist=1.0): + self.mc = mc + self.mp = mp + self.l = l + self.max_force = max_force + self.dist = dist + self.cp_render = {} + super().__init__() + + + def tF_bound(self) -> Box: + return Box(0.01, np.inf, shape=(1,)) + + def x_bound(self) -> Box: + return Box(np.asarray([-2 * self.dist, -np.inf, -2 * np.pi, -np.inf]), np.asarray([2 * self.dist, np.inf, 2 * np.pi, np.inf])) + + def x0_bound(self) -> Box: + return Box(np.asarray([0, 0, np.pi, 0]), np.asarray([0, 0, np.pi, 0])) + + def xF_bound(self) -> Box: + return Box(np.asarray([self.dist, 0, 0, 0]), np.asarray([self.dist, 0, 0, 0])) + + def u_bound(self) -> Box: + return Box(np.asarray([-self.max_force]), np.asarray([self.max_force])) + + def get_cost(self) -> SymbolicQRCost: + return SymbolicQRCost(R=np.eye(1) * 0, Q=np.eye(4) * 0, qc=1) # just minimum time + + def sym_f(self, x, u, t=None): + mp = self.mp + l = self.l + mc = self.mc + g = 9.81 # Gravity on earth. + + x_dot = x[1] + theta = x[2] + sin_theta = sym.sin(theta) + cos_theta = sym.cos(theta) + theta_dot = x[3] + F = u[0] + # Define dynamics model as per Razvan V. Florian's + # "Correct equations for the dynamics of the cart-pole system". + # Friction is neglected. + + # Eq. (23) + temp = (F + mp * l * theta_dot ** 2 * sin_theta) / (mc + mp) + numerator = g * sin_theta - cos_theta * temp + denominator = l * (4.0 / 3.0 - mp * cos_theta ** 2 / (mc + mp)) + theta_dot_dot = numerator / denominator + + # Eq. (24) + x_dot_dot = temp - mp * l * theta_dot_dot * cos_theta / (mc + mp) + xp = [x_dot, + x_dot_dot, + theta_dot, + theta_dot_dot] + return xp + + def close(self): + for r in self.cp_render.values(): + r.close() + + def render(self, x, render_mode="human"): + if render_mode not in self.cp_render: + self.cp_render[render_mode] = gym.make("CartPole-v1", render_mode=render_mode) # environment only used for rendering. Change to v1 in gym 0.26. + self.cp_render[render_mode].max_time_limit = 10000 + self.cp_render[render_mode].reset() + self.cp_render[render_mode].unwrapped.state = np.asarray(x) # environment is wrapped + return self.cp_render[render_mode].render() + +class SinCosCartpoleModel(CartpoleModel): + def phi_x(self, x): + x, dx, theta, theta_dot = x[0], x[1], x[2], x[3] + return [x, dx, sym.sin(theta), sym.cos(theta), theta_dot] + + def phi_x_inv(self, x): + x, dx, sin_theta, cos_theta, theta_dot = x[0], x[1], x[2], x[3], x[4] + theta = sym.atan2(sin_theta, cos_theta) # Obtain angle theta from sin(theta),cos(theta) + return [x, dx, theta, theta_dot] + + def phi_u(self, u): + return [sym.atanh(u[0] / self.max_force)] + + def phi_u_inv(self, u): + return [sym.tanh(u[0]) * self.max_force] + +def _cartpole_discrete_cost(model): + pole_length = model.continuous_model.l + + state_size = model.state_size + Q = np.eye(state_size) + Q[0, 0] = 1.0 + Q[1, 1] = Q[4, 4] = 0. + Q[0, 2] = Q[2, 0] = pole_length + Q[2, 2] = Q[3, 3] = pole_length ** 2 + + print("Warning: I altered the cost-matrix to prevent underflow. This is not great.") + R = np.array([[0.1]]) + Q_terminal = 1 * Q + + q = np.asarray([0,0,0,-1,0]) + # Instantaneous control cost. + c3 = DiscreteQRCost(Q=Q*0, R=R * 0.1, q=1 * q, qN=q * 1) + c3 += c3.goal_seeking_cost(Q=Q, x_target=model.x_upright) + c3 += c3.goal_seeking_terminal_cost(QN=Q_terminal, xN_target=model.x_upright) + cost = c3 + return cost + +class GymSinCosCartpoleModel(DiscreteControlModel): + state_labels = ['x', 'd_x', '$\sin(\\theta)$', '$\cos(\\theta)$', '$d\\theta/dt$'] + action_labels = ['Torque $u$'] + + def __init__(self, dt=0.02, cost=None, transform_actions=True, **kwargs): + model = SinCosCartpoleModel(**kwargs) + self.transform_actions = transform_actions + super().__init__(model=model, dt=dt, cost=cost) + self.x_upright = np.asarray(self.phi_x(model.xF_bound().low )) + if cost is None: + cost = _cartpole_discrete_cost(self) + self.cost = cost + + @property + def max_force(self): + return self.continuous_model.maxForce + + +class GymSinCosCartpoleEnvironment(ControlEnvironment): + def __init__(self, Tmax=5, transform_actions=True, supersample_trajectory=False, render_mode='human', **kwargs): + discrete_model = GymSinCosCartpoleModel(transform_actions=transform_actions, **kwargs) + self.observation_space = Box(low=-np.inf, high=np.inf, shape=(5,), dtype=float) + if transform_actions: + self.action_space = Box(low=-np.inf, high=np.inf, shape=(1,), dtype=float) + super().__init__(discrete_model, Tmax=Tmax,render_mode=render_mode, supersample_trajectory=supersample_trajectory) + + +class DiscreteCartpoleModel(DiscreteControlModel): + def __init__(self, dt=0.02, cost=None, **kwargs): + model = CartpoleModel(**kwargs) + super().__init__(model=model, dt=dt, cost=cost) + + +class CartpoleEnvironment(ControlEnvironment): + def __init__(self, Tmax=5, supersample_trajectory=False, render_mode='human', **kwargs): + discrete_model = DiscreteCartpoleModel(**kwargs) + super().__init__(discrete_model, Tmax=Tmax, supersample_trajectory=supersample_trajectory, render_mode=render_mode) + + +if __name__ == "__main__": + from irlc import train, VideoMonitor + from irlc import Agent + env = GymSinCosCartpoleEnvironment() + agent = Agent(env) + env = VideoMonitor(env) + stats, traj = train(env, agent, num_episodes=1, max_steps=100) + env.close() diff --git a/irlc/ex06/__init__.py b/irlc/ex06/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6e26755e5ec4fe79d8350778babe173741127191 --- /dev/null +++ b/irlc/ex06/__init__.py @@ -0,0 +1,2 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""This directory contains the exercises for week 6.""" diff --git a/irlc/ex06/__pycache__/__init__.cpython-311.pyc b/irlc/ex06/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2005a99d71b80389f53d84cdab39fae60aa41248 Binary files /dev/null and b/irlc/ex06/__pycache__/__init__.cpython-311.pyc differ diff --git a/irlc/ex06/__pycache__/dlqr.cpython-311.pyc b/irlc/ex06/__pycache__/dlqr.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e3b9ef8646190a661fbefabe8b43de0c6663fce1 Binary files /dev/null and b/irlc/ex06/__pycache__/dlqr.cpython-311.pyc differ diff --git a/irlc/ex06/boeing_lqr.py b/irlc/ex06/boeing_lqr.py new file mode 100644 index 0000000000000000000000000000000000000000..e06cf3f4efdc2ef72a4f38b20e4df647e85e145d --- /dev/null +++ b/irlc/ex06/boeing_lqr.py @@ -0,0 +1,85 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [Her24] Tue Herlau. Sequential decision making. (Freely available online), 2024. +""" +import numpy as np +import matplotlib.pyplot as plt +from irlc import savepdf +from irlc import train +from irlc.ex06.model_boeing import BoeingEnvironment +from irlc.ex06.lqr_agent import LQRAgent +from irlc.ex03.control_model import ControlModel +import scipy + + +def boeing_simulation(): + env = BoeingEnvironment(Tmax=10) + model = env.discrete_model.continuous_model # get the model from the Boeing environment + dt = env.dt # Get the discretization time. + A, B, d = compute_A_B_d(model, dt) + # Use compute_Q_R_q to get the Q, R, and q matrices in the discretized system + # TODO: 1 lines missing. + raise NotImplementedError("Compute Q, R and q here") + ## TODO: Half of each line of code in the following 1 lines have been replaced by garbage. Make it work and remove the error. + #---------------------------------------------------------------------------------------------------------------------------- + # agent = LQRAgent(env, A=A?????????????????????????? + raise NotImplementedError("Use your LQRAgent to plan using the system matrices.") + stats, trajectories = train(env, agent, return_trajectory=True) + return stats, trajectories, env + +def compute_Q_R_q(model : ControlModel, dt : float): + cost = model.get_cost() # Get the continuous-time cost-function + # use print(cost) to see what it contains. + # Then get the discretized matrices using the techniques described in (Her24, Subsection 13.1.6). + # TODO: 3 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + return Q, R, q + +def compute_A_B_d(model : ControlModel, dt : float): + if model.d is None: + d = np.zeros((model.state_size,)) # Ensure d is set to a zero vector if it is not defined. + else: + d = model.d + + A_discrete = scipy.linalg.expm(model.A * dt) # This is the discrete A-matrix computed using the matrix exponential + # Now it is your job to define B_discrete and d_discrete. + # TODO: 2 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + return A_discrete, B_discrete, d_discrete.flatten() + +def boeing_experiment(): + _, trajectories, env = boeing_simulation() + model = env.discrete_model.continuous_model + + dt = env.dt + Q, R, q = compute_Q_R_q(model, dt) + print("Discretization time is", dt) + print("Original q-vector was:", model.get_cost().q) + print("Discretized version is:", q) + + t = trajectories[-1] + out = t.state @ model.P.T + + plt.plot(t.time, out[:, 0], '-', label=env.observation_labels[0]) + plt.plot(t.time, out[:, 1], '-', label=env.observation_labels[1]) + plt.grid() + plt.legend() + plt.xlabel("Time/seconds") + plt.ylabel("Output") + savepdf("boing_lqr_output") + plt.show(block=False) + plt.close() + + plt.plot(t.time[:-1], t.action[:, 0], '-', label=env.action_labels[0]) + plt.plot(t.time[:-1], t.action[:, 1], '-', label=env.action_labels[1]) + plt.xlabel("Time/seconds") + plt.ylabel("Control action") + plt.grid() + plt.legend() + savepdf("boing_lqr_action") + plt.show() + +if __name__ == "__main__": + boeing_experiment() diff --git a/irlc/ex06/dlqr.py b/irlc/ex06/dlqr.py new file mode 100644 index 0000000000000000000000000000000000000000..205aa9fc433157df852e97470903e44720e7f44b --- /dev/null +++ b/irlc/ex06/dlqr.py @@ -0,0 +1,207 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [Her24] Tue Herlau. Sequential decision making. (Freely available online), 2024. +""" +import numpy as np +import matplotlib.pyplot as plt +from irlc import bmatrix +from irlc import savepdf + + + +def LQR(A : list, # Dynamic + B : list, # Dynamics + d : list =None, # Dynamics (optional) + Q : list=None, + R: list=None, + H : list=None, + q : list=None, + r : list=None, + qc : list=None, + QN : np.ndarray =None, # Terminal cost term + qN : np.ndarray=None, # Terminal cost term + qcN : np.ndarray =None, # Terminal cost term. + mu : float =0 # regularization parameter which will only be relevant next week. + ): + r""" + Implement the LQR as defined in (Her24, Algorithm 22). I recommend viewing this documentation online (documentation for week 6). + + When you solve this exercise, look at the algorithm in the book. Since the LQR problem is on the form: + + .. math:: + + x_{k+1} = A_k x_k + B_k u_k + d_k + + For :math:`k=0,\dots,N-1` this means there are :math:`N` matrices :math:`A_k`. This is implemented by assuming that + :python:`A` (i.e., the input argument) is a :python:`list` of length :math:`N` so that :python:`A[k]` corresponds + to :math:`A_k`. + + Similar conventions are used for the cost term (please see the lecture notes or the online documentation for their meaning). Recall it has the form: + + .. math:: + + c(x_k, u_k) = \frac{1}{2} \mathbf x_k^\top Q_k \mathbf x_k + \frac{1}{2} \mathbf q_k^\top \mathbf x_k + q_k + \cdots + + When the function is called, the vector :math:`\textbf{q}_k` corresponds to :python:`q` and the constant :math:`q_k` correspond to :python:`qc` (q-constant). + + .. note:: + + Only the terms :python:`A` and :python:`B` are required. The rest of the terms will default to 0-matrices. + + The LQR algorithm will ultimately compute a control law of the form: + + .. math:: + + \mathbf u_k = L_k \mathbf x_k + \mathbf l_k + + And a cost-to-go function as: + + .. math:: + + J_k(x_k) = \frac{1}{2} \mathbf x_k^\top V_k \mathbf x_k + v_k^\top \mathbf x_k + v_k + + Again there are :math:`N-1` terms. The function then return :python:`return (L, l), (V, v, vc)` so that :python:`L[k]` corresponds to :math:`L_k`. + + :param A: A list of :python:`np.ndarray` containing all terms :math:`A_k` + :param B: A list of :python:`np.ndarray` containing all terms :math:`B_k` + :param d: A list of :python:`np.ndarray` containing all terms :math:`\mathbf d_k` (optional) + :param Q: A list of :python:`np.ndarray` containing all terms :math:`Q_k` (optional) + :param R: A list of :python:`np.ndarray` containing all terms :math:`R_k` (optional) + :param H: A list of :python:`np.ndarray` containing all terms :math:`H_k` (optional) + :param q: A list of :python:`np.ndarray` containing all terms :math:`\mathbf q_k` (optional) + :param r: A list of :python:`np.ndarray` containing all terms :math:`\mathbf r_k` (optional) + :param qc: A list of :python:`float` containing all terms :math:`q_k` (i.e., constant terms) (optional) + :param QN: A :python:`np.ndarray` containing the terminal cost term :math:`Q_N` (optional) + :param qN: A :python:`np.ndarray` containing the terminal cost term :math:`\mathbf q_N` (optional) + :param qcN: A :python:`np.ndarray` containing the terminal cost term :math:`q_N` + :param mu: A regularization term which is useful for iterative-LQR (next week). Default to 0. + :return: A tuple of the form :python:`(L, l), (V, v, vc)` corresponding to the control and cost-matrices. + """ + N = len(A) + n,m = B[0].shape + # Initialize empty lists for control matrices and cost terms + L, l = [None]*N, [None]*N + V, v, vc = [None]*(N+1), [None]*(N+1), [None]*(N+1) + # Initialize constant cost-function terms to zero if not specified. + # They will be initialized to zero, meaning they have no effect on the update rules. + QN = np.zeros((n,n)) if QN is None else QN + qN = np.zeros((n,)) if qN is None else qN + qcN = 0 if qcN is None else qcN + H, q, qc, r = init_mat(H,m,n,N=N), init_mat(q,n,N=N), init_mat(qc,1,N=N), init_mat(r,m,N=N) + d = init_mat(d,n, N=N) + """ In the next line, you should initialize the last cost-term. This is similar to how we in DP had the initialization step + > J_N(x_N) = g_N(x_N) + Except that since x_N is no longer discrete, we store it as matrices/vectors representing a second-order polynomial, i.e. + > J_N(X_N) = 1/2 * x_N' V[N] x_N + v[N]' x_N + vc[N] + """ + # TODO: 1 lines missing. + raise NotImplementedError("Initialize V[N], v[N], vc[N] here") + + In = np.eye(n) + for k in range(N-1,-1,-1): + # When you update S_uu and S_ux remember to add regularization as the terms ... (V[k+1] + mu * In) ... + # Note that that to find x such that + # >>> x = A^{-1} y this + # in a numerically stable manner this should be done as + # >>> x = np.linalg.solve(A, y) + # The terms you need to update will be, in turn: + # Suu = ... + # Sux = ... + # Su = ... + # L[k] = ... + # l[k] = ... + # V[k] = ... + # V[k] = ... + # v[k] = ... + # vc[k] = ... + ## TODO: Half of each line of code in the following 4 lines have been replaced by garbage. Make it work and remove the error. + #---------------------------------------------------------------------------------------------------------------------------- + # Suu = R[k] + B[k].T @ (???????????????????????? + # Sux = H[k] + B[k].T @ (???????????????????????? + # Su = r[k] + B[k].T @ v[k + 1????????????????????????????? + # L[k] = -np.linal????????????????? + raise NotImplementedError("Insert your solution and remove this error.") + l[k] = -np.linalg.solve(Suu, Su) # You get this for free. Notice how we use np.lingalg.solve(A,x) to compute A^{-1} x + V[k] = Q[k] + A[k].T @ V[k+1] @ A[k] - L[k].T @ Suu @ L[k] + V[k] = 0.5 * (V[k] + V[k].T) # I recommend putting this here to keep V positive semidefinite + # You get these for free: Compare to the code in the algorithm. + v[k] = q[k] + A[k].T @ (v[k+1] + V[k+1] @ d[k]) + Sux.T @ l[k] + vc[k] = vc[k+1] + qc[k] + d[k].T @ v[k+1] + 1/2*( d[k].T @ V[k+1] @ d[k] ) + 1/2*l[k].T @ Su + + return (L,l), (V,v,vc) + + +def init_mat(X, a, b=None, N=None): + """ + Helper function. Check if X is None, and if so return a list + [A, A,....] + which is N long and where each A is a (a x b) zero-matrix, else returns X repeated N times: + [X, X, ...] + """ + M0 = np.zeros((a,) if b is None else (a, b)) + if X is not None: + return [m if m is not None else M0 for m in X] + else: + return [M0] * N + +def lqr_rollout(x0,A,B,d,L,l): + """ + Compute a rollout (states and actions) given solution from LQR controller function. + + x0 is a vector (starting state), and A, B, d and L, l are lists of system/control matrices. + """ + x, states,actions = x0, [x0], [] + n,m = B[0].shape + N = len(L) + d = init_mat(d,n,1,N) # Initialize as a list of zero matrices [ np.zeros((n,1)), np.zeros((n,1)), ...] + l = init_mat(l,m,1,N) # Initialize as a list of zero matrices [ np.zeros((m,1)), np.zeros((m,1)), ...] + + for k in range(N): + u = L[k] @ x + l[k] + x = A[k] @ x + B[k] @ u + d[k] + actions.append(u) + states.append(x) + return states, actions + +if __name__ == "__main__": + """ + Solve this problem (see also lecture notes for the same example) + http://cse.lab.imtlucca.it/~bemporad/teaching/ac/pdf/AC2-04-LQR-Kalman.pdf + """ + N = 20 + A = np.ones((2,2)) + A[1,0] = 0 + B = np.asarray([[0], [1]]) + Q = np.zeros((2,2)) + R = np.ones((1,1)) + + print("System matrices A, B, Q, R") + print(bmatrix(A)) + print(bmatrix(B)) + print(bmatrix(Q)) + print(bmatrix(R)) + + for rho in [0.1, 10, 100]: + Q[0,0] = 1/rho + (L,l), (V,v,vc) = LQR(A=[A]*N, B=[B]*N, d=None, Q=[Q]*N, R=[R]*N, QN=Q) + + x0 = np.asarray( [[1],[0]]) + trajectory, actions = lqr_rollout(x0,A=[A]*N, B=[B]*N, d=None,L=L,l=l) + + xs = np.concatenate(trajectory, axis=1)[0,:] + + plt.plot(xs, 'o-', label=f'rho={rho}') + + k = 10 + print(f"Control matrix in u_k = L_k x_k + l_k at k={k}:", L[k]) + for k in [N-1,N-2,0]: + print(f"L[{k}] is:", L[k].round(4)) + plt.title("Double integrator") + plt.xlabel('Steps $k$') + plt.ylabel('$x_1 = $ x[0]') + plt.legend() + plt.grid() + savepdf("dlqr_double_integrator") + plt.show() diff --git a/irlc/ex06/dlqr_check.py b/irlc/ex06/dlqr_check.py new file mode 100644 index 0000000000000000000000000000000000000000..3d86db35853cffea347fbb5a5666a7ba9ec47681 --- /dev/null +++ b/irlc/ex06/dlqr_check.py @@ -0,0 +1,40 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +from irlc.ex06.dlqr import LQR + +def urnd(sz): + return np.random.uniform(-1, 1, sz) + +def check_LQR(): + np.random.seed(42) + n,m,N = 3,2,4 + """ + Create a randomized, nonsense control problem and solve it. Since seed is fixed we can expect same solution. + """ + # system tersm + A = [urnd((n, n)) for _ in range(N)] + B = [urnd((n, m)) for _ in range(N)] + d = [urnd((n,)) for _ in range(N)] + # cost terms + Q = [urnd((n, n)) for _ in range(N)] + R = [urnd((m, m)) for _ in range(N)] + H = [urnd((m, n)) for _ in range(N)] + q = [urnd((n,)) for _ in range(N)] + qc = [urnd(()) for _ in range(N)] + r = [urnd((m,)) for _ in range(N)] + # terminal costs + QN = urnd((n, n)) + qN = urnd((n,)) + qcN = urnd(()) + return LQR(A=A, B=B, d=d, Q=Q, R=R, H=H, q=q, r=r, qc=qc, QN=QN, qN=qN, qcN=qcN, mu=0) + + +if __name__ == "__main__": + (L, l), (V, v, vc) = check_LQR() + N = len(V)-1 + print(", ".join([f"l[{k}]={l[k].round(4)}" for k in [N - 1, N - 2, 0]])) + print("\n".join([f"L[{k}]={L[k].round(4)}" for k in [N - 1, N - 2, 0]])) + + print("\n".join([f"V[{k}]={V[k].round(4)}" for k in [0]])) + print(", ".join([f"v[{k}]={v[k].round(4)}" for k in [N, N - 1, 0]])) + print(", ".join([f"vc[{k}]={vc[k].round(4)}" for k in [N, N - 1, 0]])) diff --git a/irlc/ex06/lqr_agent.py b/irlc/ex06/lqr_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..f62ec55971e3757bb84a7ac113a24fee99462c4e --- /dev/null +++ b/irlc/ex06/lqr_agent.py @@ -0,0 +1,54 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex04.locomotive import LocomotiveEnvironment +from irlc import train, plot_trajectory, savepdf, Agent +from irlc.ex06.dlqr import LQR +from irlc.ex04.control_environment import ControlEnvironment +import numpy as np +import matplotlib.pyplot as plt + +class LQRAgent(Agent): + def __init__(self, env : ControlEnvironment, A, B, Q, R, d=None, q=None): + N = int((env.Tmax / env.dt)) # Obtain the planning horizon + """ Define A, B as the list of A/B matrices here. I.e. x[t+1] = A x[t] + B x[t] + d. + You should use the function model.f to do this, which has build-in functionality to compute Jacobians which will be equal to A, B """ + """ Define self.L, self.l here as the (lists of) control matrices. """ + ## TODO: Half of each line of code in the following 1 lines have been replaced by garbage. Make it work and remove the error. + #---------------------------------------------------------------------------------------------------------------------------- + # (self.L, self.l), _ = LQR(A=[A]*N, B=[B]*N, d=[d]*N if d is not No??????????????????????????????????????????????????????????????????? + raise NotImplementedError("Insert your solution and remove this error.") + self.dt = env.dt + super().__init__(env) + + def pi(self,x, k, info=None): + """ + Compute the action here using u = L_k x + l_k. + You should use self.L, self.l to get the control matrices (i.e. L_k = self.L[k] ), + """ + # TODO: 1 lines missing. + raise NotImplementedError("Compute current action here") + return u + + +if __name__ == "__main__": + # Make a guess at the system matrices for planning. We will return on how to compute these exactly in a later exercise. + A = np.ones((2, 2)) + A[1, 0] = 0 + B = np.asarray([[0], [1]]) + Q = np.eye(2)*3 + R = np.ones((1, 1))*2 + q = np.asarray([-1.1, 0 ]) + + # Create and test our LQRAgent. + env = LocomotiveEnvironment(render_mode='human', Tmax=10, slope=1) + agent = LQRAgent(env, A=A, B=B, Q=Q, R=R, q=q) + stats, traj = train(env, agent, num_episodes=1) + + env.reset() + savepdf("locomotive_snapshot.pdf", env=env) # Make a plot for the exercise file. + env.state_labels = ["x(t)", "v(t)"] + env.action_labels = ["u(t)"] + plot_trajectory(traj[0], env) + plt.show(block=True) + savepdf("lqr_agent") + plt.show() + env.close() diff --git a/irlc/ex06/lqr_pid.py b/irlc/ex06/lqr_pid.py new file mode 100644 index 0000000000000000000000000000000000000000..136cae2ba5a2bebcf45880daec13e853a7bee9b4 --- /dev/null +++ b/irlc/ex06/lqr_pid.py @@ -0,0 +1,79 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import matplotlib.pyplot as plt +import numpy as np +from irlc import savepdf, train +from irlc.ex04.pid_locomotive_agent import PIDLocomotiveAgent +from irlc.ex06.lqr_agent import LQRAgent +from irlc.ex04.model_harmonic import HarmonicOscilatorEnvironment +from irlc.ex06.boeing_lqr import compute_A_B_d, compute_Q_R_q + +class ConstantLQRAgent(LQRAgent): + # TODO: 3 lines missing. + raise NotImplementedError("Complete this agent here. You need to update the policy-function: def pi(self, ..).") + +def get_Kp_Kd(L0): + # TODO: 1 lines missing. + raise NotImplementedError("Use lqr_agent.L to define Kp and Kd.") + return Kp, Kd + + +if __name__ == "__main__": + Delta = 0.06 # Time discretization constant + # Define a harmonic osscilator environment. Use .., render_mode='human' to see a visualization. + env = HarmonicOscilatorEnvironment(Tmax=8, dt=Delta, m=0.5, R=np.eye(1) * 8, render_mode=None) # set render_mode='human' to see the oscillator. + model = env.discrete_model.continuous_model # Get the ControlModel corresponding to this environment. + + + # Compute the discretized A, B and d matrices using the helper functions we defined in the Boeing problem. + # Note that these are for the discrete environment: x_{k+1} = A x_k + B u_k + d + A, B, d = compute_A_B_d(model, Delta) + Q, R, q = compute_Q_R_q(model, Delta) + + # Run the LQR agent + lqr_agent = LQRAgent(env, A=A, B=B, d=d, Q=Q, R=R, q=q) + _, traj1 = train(env, lqr_agent, return_trajectory=True) + + # Part 1. Build an agent that always takes actions u_k = L_0 x_k + l_0 + constant_agent = ConstantLQRAgent(env, A=A, B=B, d=d, Q=Q, R=R, q=q) + # Check that its policy is independent of $k$: + x0, _ = env.reset() + print(f"Initial state is {x0=}") + print(f"Action at time step k=0 {constant_agent.pi(x0, k=0)=}") + print(f"Action at time step k=5 (should be the same) {constant_agent.pi(x0, k=0)=}") + + _, traj2 = train(env, constant_agent, return_trajectory=True) + + # Part 2. Use the L and l matrices (see lqr_agent.L and lqr_agent.l) + # to select Kp and Kd in a PID agent. Then let's use the Locomotive agent to see the effect of the controller. + # Use render_mode='human' to see its effect. + # We only need to use L. + # Hint: compare the form of the LQR and PID controller and use that to select Kp and Kd. + Kp, Kd = get_Kp_Kd(lqr_agent.L[0]) # Use lqr_agent.L to define Kp and Kd. + + # Define and run the PID agent. + pid_agent = PIDLocomotiveAgent(env, env.dt, Kp=Kp, Kd=Kd) + _, traj3 = train(env, pid_agent, return_trajectory=True) + + # Plot all actions and state sequences. + plt.figure(figsize=(10,5)) + plt.grid() + plt.plot(traj1[0].time[:-1], traj1[0].action, label="Optimal LQR action sequence") + plt.plot(traj2[0].time[:-1], traj2[0].action, '.-', label="Constant LQR action sequence") + plt.plot(traj3[0].time[:-1], traj3[0].action, label="PID agent action sequence") + plt.xlabel("Time / Seconds") + plt.ylabel("Action / Newtons") + plt.ylim([-.2, .2]) + plt.legend() + savepdf("pid_lqr_actions") + plt.show(block=True) + + plt.figure(figsize=(10, 5)) + plt.grid() + plt.plot(traj1[0].time, traj1[0].state[:, 0], label="Optimal LQR states x(t)") + plt.plot(traj2[0].time, traj2[0].state[:, 0], label="Constant LQR states x(t)") + plt.plot(traj3[0].time, traj3[0].state[:, 0], label="PID agent states x(t)") + plt.xlabel("Time / Seconds") + plt.ylabel("Position x(t) / Meters") + plt.ylim([-1, 1]) + plt.legend() + savepdf("pid_lqr_states") diff --git a/irlc/ex06/model_boeing.py b/irlc/ex06/model_boeing.py new file mode 100644 index 0000000000000000000000000000000000000000..57e0a0c7a3664a45005437b4576038021c60f4ef --- /dev/null +++ b/irlc/ex06/model_boeing.py @@ -0,0 +1,62 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +from irlc.ex04.discrete_control_model import DiscreteControlModel +from irlc.ex04.control_environment import ControlEnvironment +from irlc.ex04.model_linear_quadratic import LinearQuadraticModel + +class BoeingModel(LinearQuadraticModel): + """ + Boeing 747 level flight example. + + See: https://books.google.dk/books?id=tXZDAAAAQBAJ&pg=PA147&lpg=PA147&dq=boeing+747+flight+0.322+model+longitudinal+flight&source=bl&ots=L2RpjCAWiZ&sig=ACfU3U2m0JsiHmUorwyq5REcOj2nlxZkuA&hl=en&sa=X&ved=2ahUKEwir7L3i6o3qAhWpl4sKHQV6CdcQ6AEwAHoECAoQAQ#v=onepage&q=boeing%20747%20flight%200.322%20model%20longitudinal%20flight&f=false + Also: https://web.stanford.edu/~boyd/vmls/vmls-slides.pdf + """ + state_labels = ["Longitudinal velocity (x) ft/sec", "Velocity in y-axis ft/sec", "Angular velocity", + "angle wrt. horizontal"] + action_labels = ['Elevator', "Throttle"] + observation_labels = ["Airspeed", "Climb rate"] + + def __init__(self, output=None): + if output is None: + output = [10, 0] + # output = [10, 0] + A = [[-0.003, 0.039, 0, -0.322], + [-0.065, -.319, 7.74, 0], + [.02, -.101, -0.429, 0], + [0, 0, 1, 0]] + B = [[.01, 1], + [-.18, -.04], + [-1.16, .598], + [0, 0]] + + A, B = np.asarray(A), np.asarray(B) + self.u0 = 7.74 # speed in hundred feet/seconds + self.P = np.asarray([[1, 0, 0, 0], [0, -1, 0, 7.74]]) # Projection of state into airspeed + + dt = 0.1 # Scale the cost by this factor. + + # Set up the cost: + self.Q_obs = np.eye(2) + Q = self.P.T @ self.Q_obs @ self.P / dt + R = np.eye(2) / dt + q = -np.asarray(output) @ self.Q_obs @ self.P / dt + super().__init__(A=A, B=B, Q=Q, R=R, q=q) + + def state2outputs(self, x): + return self.P @ x + +class DiscreteBoeingModel(DiscreteControlModel): + def __init__(self, output=None): + model = BoeingModel(output=output) + dt = 0.1 + super().__init__(model=model, dt=dt) + + +class BoeingEnvironment(ControlEnvironment): + @property + def observation_labels(self): + return self.discrete_model.continuous_model.observation_labels + + def __init__(self, Tmax=10): + model = DiscreteBoeingModel() + super().__init__(discrete_model=model, Tmax=Tmax) diff --git a/irlc/ex06/model_rendevouz.py b/irlc/ex06/model_rendevouz.py new file mode 100644 index 0000000000000000000000000000000000000000..c6a98291f8a6a8e282e7a7ae4c2cf99b4c779d27 --- /dev/null +++ b/irlc/ex06/model_rendevouz.py @@ -0,0 +1,95 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +from irlc.utils.graphics_util_pygame import UpgradedGraphicsUtil +from irlc.ex04.discrete_control_model import DiscreteControlModel +from irlc.ex04.control_environment import ControlEnvironment +from irlc.ex04.model_linear_quadratic import LinearQuadraticModel +from gymnasium.spaces import Box + +""" +SEE: https://github.com/anassinator/ilqr/blob/master/examples/rendezvous.ipynb +""" +class ContiniousRendevouzModel(LinearQuadraticModel): + state_labels= ["x0", "y0", "x1", "y1", 'Vx0', "Vy0", "Vx1", "Vy1"] + action_labels = ['Fx0', 'Fy0', "Fx1", "Fy1"] + x0 = np.array([0, 0, 10, 10, 0, -5, 5, 0]) # Initial state. + + def __init__(self, m=10.0, alpha=0.1, simple_bounds=None, cost=None): + m00 = np.zeros((4,4)) + mI = np.eye(4) + A = np.block( [ [m00, mI], [m00, -alpha/m*mI] ] ) + B = np.block( [ [m00], [mI/m]] ) + state_size = len(self.x0) + action_size = 4 + self.m = m + self.alpha = alpha + Q = np.eye(state_size) + Q[0, 2] = Q[2, 0] = -1 + Q[1, 3] = Q[3, 1] = -1 + R = 0.1 * np.eye(action_size) + self.viewer = None + super().__init__(A=A, B=B, Q=Q*20, R=R*20) + + def x0_bound(self) -> Box: + return Box(self.x0, self.x0) # self.bounds['x0_low'] = self.bounds['x0_high'] = list(self.x0) + + def render(self, x, render_mode="human"): + """ Render the environment. You don't have to understand this code. """ + if self.viewer is None: + self.viewer = HarmonicViewer(xstar=0, x0=self.x0) # target: x=0. + self.viewer.update(x) + import time + time.sleep(0.05) + return self.viewer.blit(render_mode=render_mode) + + def close(self): + pass + + +class DiscreteRendevouzModel(DiscreteControlModel): + def __init__(self, dt=0.1, cost=None, transform_actions=True, **kwargs): + model = ContiniousRendevouzModel(**kwargs) + super().__init__(model=model, dt=dt, cost=cost) + +class RendevouzEnvironment(ControlEnvironment): + def __init__(self, Tmax=20, render_mode=None, **kwargs): + discrete_model = DiscreteRendevouzModel(**kwargs) + super().__init__(discrete_model, Tmax=Tmax, render_mode=render_mode) + +class HarmonicViewer(UpgradedGraphicsUtil): + def __init__(self, xstar = 0, x0=None): + self.xstar = xstar + width = 800 + self.x0 = x0 + sz = 20 + self.scale = width/(2*sz) + self.p1h = [] + self.p2h = [] + super().__init__(screen_width=width, xmin=-sz, xmax=sz, ymin=-sz, ymax=sz, title='Rendevouz environment') + + def render(self): + self.draw_background(background_color=(255, 255, 255)) + # dw = self.dw + p1 = self.x[:2] + p2 = self.x[2:4] + self.p1h.append(p1) + self.p2h.append(p2) + self.circle("asdf", pos=p1, r=.5 * self.scale, fillColor=(200, 0, 0)) + self.circle("asdf", pos=p2, r=.5 * self.scale, fillColor=(0, 0, 200) ) + if len(self.p1h) > 2: + self.polyline('...', np.stack(self.p1h)[:,0], np.stack(self.p1h)[:,1], width=1, color=(200, 0, 0)) + self.polyline('...', np.stack(self.p2h)[:,0], np.stack(self.p2h)[:,1], width=1, color=(0, 0, 200)) + + if tuple(self.x) == tuple(self.x0): + self.p1h = [] + self.p2h = [] + + + def update(self, x): + self.x = x + + +if __name__ == "__main__": + from irlc import Agent, train + env = RendevouzEnvironment(render_mode='human') + train(env, Agent(env), num_episodes=4) diff --git a/irlc/ex07/__init__.py b/irlc/ex07/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1e8044b811c2884a4486857534492a8d7a83575b --- /dev/null +++ b/irlc/ex07/__init__.py @@ -0,0 +1,2 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""This directory contains the exercises for week 7.""" diff --git a/irlc/ex07/__pycache__/__init__.cpython-311.pyc b/irlc/ex07/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..31f12480f2dcfe61ecbfb39cae46bc4ec87cffd0 Binary files /dev/null and b/irlc/ex07/__pycache__/__init__.cpython-311.pyc differ diff --git a/irlc/ex07/__pycache__/ilqr.cpython-311.pyc b/irlc/ex07/__pycache__/ilqr.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a751e7129fe8878e0d83ad2613ed7b7c560ad85 Binary files /dev/null and b/irlc/ex07/__pycache__/ilqr.cpython-311.pyc differ diff --git a/irlc/ex07/ilqr.py b/irlc/ex07/ilqr.py new file mode 100644 index 0000000000000000000000000000000000000000..8e33a8f7a0ba13fcbe07df05a76ed6f096743b86 --- /dev/null +++ b/irlc/ex07/ilqr.py @@ -0,0 +1,273 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" +This implements two methods: The basic ILQR method, described in (Her24, Algorithm 24), and the linesearch-based method +described in (Her24, Algorithm 25). + +If you are interested, you can consult (TET12) (which contains generalization to DDP) and (Har20, Alg 1). + +References: + [Her24] Tue Herlau. Sequential decision making. (Freely available online), 2024. + [TET12] Yuval Tassa, Tom Erez, and Emanuel Todorov. Synthesis and stabilization of complex behaviors through online trajectory optimization. In 2012 IEEE/RSJ International Conference on Intelligent Robots and Systems, 4906–4913. IEEE, 2012. (See tassa2012.pdf). + [Har20] James Harrison. Optimal and learning-based control combined course notes. (See AA203combined.pdf), 2020. +""" +import warnings +import numpy as np +from irlc.ex06.dlqr import LQR +from irlc.ex04.discrete_control_model import DiscreteControlModel + +def ilqr_basic(model : DiscreteControlModel, N, x0, us_init : list = None, n_iterations=500, verbose=True): + """ + Implements the basic ilqr algorithm, i.e. (Her24, Algorithm 24). Our notation (x_bar, etc.) will be consistent with the lecture slides + """ + mu, alpha = 1, 1 # Hyperparameters. For now, just let them have defaults and don't change them + # Create a random initial state-sequence + n, m = model.state_size, model.action_size + u_bar = [np.random.uniform(-1, 1,(model.action_size,)) for _ in range(N)] if us_init is None else us_init + x_bar = [x0] + [np.zeros(n, )] * N + """ + Initialize nominal trajectory xs, us using us and x0 (i.e. simulate system from x0 using action sequence us). + The simplest way to do this is to call forward_pass with all-zero sequence of control vector/matrix l, L. + """ + # TODO: 2 lines missing. + raise NotImplementedError("Initialize x_bar, u_bar here") + J_hist = [] + for i in range(n_iterations): + """ + Compute derivatives around trajectory and cost estimate J of trajectory. To do so, use the get_derivatives + function. Remember the functions will return lists of derivatives. + """ + # TODO: 2 lines missing. + raise NotImplementedError("Compute J and derivatives A_k = f_x, B_k = f_u, ....") + """ Backward pass: Obtain feedback law matrices l, L using the backward_pass function. + """ + # TODO: 1 lines missing. + raise NotImplementedError("Compute L, l = .... here") + """ Forward pass: Given L, l matrices computed above, simulate new (optimal) action sequence. + In the lecture slides, this is similar to how we compute u^*_k and x_k + Once they are computed, iterate the iLQR algorithm by setting x_bar, u_bar equal to these values + """ + # TODO: 1 lines missing. + raise NotImplementedError("Compute x_bar, u_bar = ...") + if verbose: + print(f"{i}> J={J:4g}, change in cost since last iteration {0 if i == 0 else J-J_hist[-1]:4g}") + J_hist.append(J) + return x_bar, u_bar, J_hist, L, l + +def ilqr_linesearch(model : DiscreteControlModel, N, x0, n_iterations, us_init=None, tol=1e-6, verbose=True): + """ + For linesearch implement method described in (Her24, Algorithm 25) (we will use regular iLQR, not DDP!) + """ + # The range of alpha-values to try out in the linesearch + # plus parameters relevant for regularization scheduling. + alphas = 1.1 ** (-np.arange(10) ** 2) # alphas = [1, 1.1^{-2}, ...] + mu_min = 1e-6 + mu_max = 1e10 + Delta_0 = 2 + mu = 1.0 + Delta = Delta_0 + + n, m = model.state_size, model.action_size + u_bar = [np.random.uniform(-1, 1, (model.action_size,)) for _ in range(N)] if us_init is None else us_init + x_bar = [x0] + [np.zeros(n, )] * (N) + # Initialize nominal trajectory xs, us (same as in basic linesearch) + # TODO: 2 lines missing. + raise NotImplementedError("Copy-paste code from previous solution") + J_hist = [] + + converged = False + for i in range(n_iterations): + alpha_was_accepted = False + """ Step 1: Compute derivatives around trajectory and cost estimate of trajectory. + (copy-paste from basic implementation). In our implementation, J_bar = J_{u^star}(x_0) """ + # TODO: 2 lines missing. + raise NotImplementedError("Obtain derivatives f_x, f_u, ... as well as cost of trajectory J_bar = ...") + try: + """ + Step 2: Backward pass to obtain control law (l, L). Same as before so more copy-paste + """ + # TODO: 1 lines missing. + raise NotImplementedError("Obtain l, L = ... in backward pass") + """ + Step 3: Forward pass and alpha scheduling. + Decrease alpha and check condition |J^new < J'|. Apply the regularization scheduling as needed. """ + for alpha in alphas: + x_hat, u_hat = forward_pass(model, x_bar, u_bar, L=L, l=l, alpha=alpha) # Simulate trajectory using this alpha + # TODO: 1 lines missing. + raise NotImplementedError("Compute J_new = ... as the cost of trajectory x_hat, u_hat") + + if J_new < J_prime: + """ Linesearch proposed trajectory accepted! Set current trajectory equal to x_hat, u_hat. """ + if np.abs((J_prime - J_new) / J_prime) < tol: + converged = True # Method does not seem to decrease J; converged. Break and return. + + J_prime = J_new + x_bar, u_bar = x_hat, u_hat + ''' + The update was accepted and you should change the regularization term mu, + and the related scheduling term Delta. + ''' + # TODO: 1 lines missing. + raise NotImplementedError("Delta, mu = ...") + alpha_was_accepted = True # accept this alpha + break + except np.linalg.LinAlgError as e: + # Matrix in dlqr was not positive-definite and this diverged + warnings.warn(str(e)) + + if not alpha_was_accepted: + ''' No alphas were accepted, which is not too hot. Regularization should change + ''' + # TODO: 1 lines missing. + raise NotImplementedError("Delta, mu = ...") + + if mu_max and mu >= mu_max: + raise Exception("Exceeded max regularization term; we are stuffed.") + + dJ = 0 if i == 0 else J_prime-J_hist[-1] + info = "converged" if converged else ("accepted" if alpha_was_accepted else "failed") + if verbose: + print(f"{i}> J={J_prime:4g}, decrease in cost {dJ:4g} ({info}).\nx[N]={x_bar[-1].round(2)}") + J_hist.append(J_prime) + if converged: + break + return x_bar, u_bar, J_hist, L, l + +def backward_pass(A : list, B : list, c_x : list, c_u : list, c_xx : list, c_ux : list, c_uu : list, mu=1): + r"""Given all derivatives, apply the LQR algorithm to get the control law. + + The input arguments are described in the online documentation and the lecture notes. You should use them to call your + implementation of the :func:`~irlc.ex06.dlqr.LQR` method. Note that you should give a value of all inputs except for the ``d``-term. + + :param A: linearization of the dynamics matrices :math:`A_k`. + :param B: linearization of the dynamics matrices :math:`B_k`. + :param c_x: Cost terms corresponding to :math:`\mathbf{q}_k` + :param c_u: Cost terms corresponding to :math:`\mathbf{r}_k` + :param c_xx: Cost terms corresponding to :math:`Q_k` + :param c_ux: Cost terms corresponding to :math:`H_k` + :param c_uu: Cost terms corresponding to :math:`R_k` + :param mu: Regularization parameter for the LQR method + :return: The control law :math:`L_k, \mathbf{l}_k` as two lists. + """ + Q, QN = c_xx[:-1], c_xx[-1] # An example. + # TODO: 4 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + # Define the inputs using the linearization inputs. + (L, l), (V, v, vc) = LQR(A=A, B=B, R=R, Q=Q, QN=QN, H=H, q=q, qN=qN, r=r, mu=mu) + return L, l + +def cost_of_trajectory(model : DiscreteControlModel, xs : list, us : list) -> float: + r"""Helper function which computes the cost of the trajectory. + + The cost is defined as: + + .. math:: + + c_N( \bar {\mathbf x}_N, \bar {\mathbf u}_) + \sum_{k=0}^{N-1} c_k(\bar {\mathbf x}_k, \bar {\mathbf u}_k) + + and to compute it, you should use the two helper methods ``model.cost.c`` and ``model.cost.cN`` + (see :func:`~irlc.ex04.discrete_control_cost.DiscreteQRCost.c` and :func:`~irlc.ex04.discrete_control_cost.DiscreteQRCost.cN`). + + :param model: The control model used to compute the cost. + :param xs: A list of length :math:`N+1` of the form :math:`\begin{bmatrix}\bar {\mathbf x}_0 & \dots & \bar {\mathbf x}_N \end{bmatrix}` + :param us: A list of length :math:`N` of the form :math:`\begin{bmatrix}\bar {\mathbf x}_0 & \dots & \bar {\mathbf x}_{N-1} \end{bmatrix}` + :return: The cost as a number. + """ + N = len(us) + JN = model.cost.cN(xs[-1]) + return sum(map(lambda args: model.cost.c(*args), zip(xs[:-1], us, range(N)))) + JN + +def get_derivatives(model : DiscreteControlModel, x_bar : list, u_bar : list): + """Compute all the derivatives used in the model. + + The return type should match the meaning in (Her24, Subequation 17.8) and in the online documentation. + + - ``c`` should be a list of length :math:`N+1` + - ``c_x`` should be a list of length :math:`N+1` + - ``c_xx`` should be a list of length :math:`N+1` + - ``c_u`` should be a list of length :math:`N` + - ``c_uu`` should be a list of length :math:`N` + - ``c_ux`` should be a list of length :math:`N` + - ``A`` should be a list of length :math:`N` + - ``B`` should be a list of length :math:`N` + + Use the model to compute these terms. For instance, this will compute the first terms ``A[0]`` and ``B[0]``:: + + A0, B0 = model.f_jacobian(x_bar[0], u_bar[0], 0) + + Meanwhile, to compute the first terms of the cost-functions you should use:: + + c[0], c_x[0], c_u[0], c_xx[0], c_ux[0], c_uu[0] = model.cost.c(x_bar[0], u_bar[0], k=0, compute_gradients=True) + + :param model: The model to use when computing the derivatives of the cost + :param x_bar: The nominal state-trajectory + :param u_bar: The nominal action-trajectory + :return: Lists of all derivatives computed around the nominal trajectory (see the lecture notes). + """ + N = len(u_bar) + """ Compute A_k, B_k (lists of matrices of length N) as the jacobians of the dynamics. To do so, + recall from the online documentation that: + x, f_x, f_u = model.f(x, u, k, compute_jacobian=True) + """ + A = [None]*N + B = [None]*N + c = [None] * (N+1) + c_x = [None] * (N + 1) + c_xx = [None] * (N + 1) + + c_u = [None] * (N+1) + c_ux = [None] * (N + 1) + c_uu = [None] * (N + 1) + # Now update each entry correctly (i.e., ensure there are no None elements left). + # TODO: 4 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + """ Compute derivatives of the cost function. For terms not including u these should be of length N+1 + (because of gN!), for the other lists of length N + recall model.cost.c has output: + c[i], c_x[i], c_u[i], c_xx[i], c_ux[i], c_uu[i] = model.cost.c(x, u, i, compute_gradients=True) + """ + # TODO: 2 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + # Concatenate the derivatives associated with the last time point N. + cN, c_xN, c_xxN = model.cost.cN(x_bar[N], compute_gradients=True) + # TODO: 3 lines missing. + raise NotImplementedError("Update c, c_x and c_xx with the terminal terms.") + return A, B, c, c_x, c_u, c_xx, c_ux, c_uu + +def forward_pass(model : DiscreteControlModel, x_bar : list, u_bar : list, L : list, l : list, alpha=1.0): + r"""Simulates the effect of the controller on the model + + We assume the system starts in :math:`\mathbf{x}_0 = \bar {\mathbf x}_0`, and then simulate the effect of + generating actions using the closed-loop policy + + .. math:: + + \mathbf{u}_k = \bar {\mathbf u}_k + \alpha \mathbf{l}_k + L_k (\mathbf{x}_k - \bar { \mathbf x}_k) + + (see (Her24, eq. (17.16))). + + :param model: The model used to compute the dynamics. + :param x_bar: A nominal list of states + :param u_bar: A nominal list of actions (not used by the method) + :param L: A list of control matrices :math:`L_k` + :param l: A list of control vectors :math:`\mathbf{l}_k` + :param alpha: The linesearch parameter. + :return: A list of length :math:`N+1` of simulated states and a list of length :math:`N` of simulated actions. + """ + N = len(u_bar) + x = [None] * (N+1) + u_star = [None] * N + x[0] = x_bar[0].copy() + + for i in range(N): + r""" Compute using (Her24, eq. (17.16)) + u_{i} = ... + """ + # TODO: 1 lines missing. + raise NotImplementedError("u_star[i] = ....") + """ Remember to compute + x_{i+1} = f_k(x_i, u_i^*) + here: + """ + # TODO: 1 lines missing. + raise NotImplementedError("x[i+1] = ...") + return x, u_star diff --git a/irlc/ex07/ilqr_agent.py b/irlc/ex07/ilqr_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..9280fc7d9f14e4ffa9b8763762bbe747506ad9f1 --- /dev/null +++ b/irlc/ex07/ilqr_agent.py @@ -0,0 +1,56 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [Her24] Tue Herlau. Sequential decision making. (Freely available online), 2024. +""" +from irlc.ex06.model_rendevouz import RendevouzEnvironment +from irlc.ex07.ilqr_rendovouz_basic import ilqr +from irlc import train +from irlc import Agent +import numpy as np + +class ILQRAgent(Agent): + def __init__(self, env, discrete_model, N=250, ilqr_iterations=10, use_ubar=False, use_linesearch=True): + super().__init__(env) + self.dt = discrete_model.dt + # x0 = discrete_model.reset() + x0,_ = env.reset() + x0 = np.asarray(x0) # Get the initial state. We will take this from the environment. + xs, us, self.J_hist, L, l = ilqr(discrete_model, N, x0, n_iter=ilqr_iterations, use_linesearch=use_linesearch) + self.ubar = us + self.xbar = xs + self.L = L + self.l = l + self.use_ubar = use_ubar # Should policy use open-loop u-bar (suboptimal) or closed-loop L_k, l_k? + + def pi(self, x, k, info=None): + if self.use_ubar: + u = self.ubar[k] + else: + if k >= len(self.ubar): + print(k, len(self.ubar)) + k = len(self.ubar)-1 + # See (Her24, eq. (17.16)) + # TODO: 1 lines missing. + raise NotImplementedError("Generate action using the control matrices.") + return u + +def solve_rendevouz(): + env = RendevouzEnvironment() + N = int(env.Tmax / env.dt) + agent = ILQRAgent(env, env.discrete_model, N=N) + stats, trajectories = train(env, agent, num_episodes=1, return_trajectory=True) + env.close() + return stats, trajectories, agent + +if __name__ == "__main__": + from irlc.ex07.ilqr_rendovouz_basic import plot_vehicles + import matplotlib.pyplot as plt + stats, trajectories, agent = solve_rendevouz() + t =trajectories[0].state + xb = agent.xbar + plot_vehicles(t[:,0], t[:,1], t[:,2], t[:,3], linespec=':', legend=("RK4 policy simulation", "RK4 policy simulation")) + plot_vehicles(xb[:,0], xb[:,1], xb[:,2], xb[:,3], linespec='-') + plt.legend() + plt.show() diff --git a/irlc/ex07/ilqr_cartpole.py b/irlc/ex07/ilqr_cartpole.py new file mode 100644 index 0000000000000000000000000000000000000000..d2463a56ff438c600d4e4047d1e349757716dfc7 --- /dev/null +++ b/irlc/ex07/ilqr_cartpole.py @@ -0,0 +1,83 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import matplotlib.pyplot as plt +import numpy as np +from irlc.ex05.model_cartpole import GymSinCosCartpoleEnvironment +import time +from irlc.ex07.ilqr_rendovouz_basic import ilqr +from irlc import savepdf + +# Number of steps. +N = 100 +def cartpole(use_linesearch): + env = GymSinCosCartpoleEnvironment(render_mode='human') + x0, info = env.reset() + xs, us, J_hist, L, l = ilqr(env.discrete_model, N, x0, n_iter=300, use_linesearch=use_linesearch) + plot_cartpole(env, xs, us, use_linesearch=use_linesearch) + +def plot_cartpole(env, xs, us, J_hist=None, use_linesearch=True): + animate(xs, env) + env.close() + # Transform actions/states using build-in functions. + def gapply(f, xm): + usplit = np.split(xm, len(xm)) + u2 = [f(u.flat) for u in usplit] + us = np.stack(u2) + return us + + us = gapply(env.discrete_model.phi_u_inv, us) + xs = gapply(env.discrete_model.phi_x_inv, xs) + + t = np.arange(N + 1) * env.dt + x = xs[:, 0] + theta = np.unwrap(xs[:, 2]) # Makes for smoother plots. + theta_dot = xs[:, 3] + pdf_ex = '_linesearch' if use_linesearch else '' + ev = 'cartpole_' + + plt.plot(theta, theta_dot) + plt.xlabel("theta (rad)") + plt.ylabel("theta_dot (rad/s)") + plt.title("Orientation Phase Plot") + plt.grid() + savepdf(f"{ev}theta{pdf_ex}") + plt.show() + + _ = plt.plot(t[:-1], us) + _ = plt.xlabel("time (s)") + _ = plt.ylabel("Force (N)") + _ = plt.title("Action path") + plt.grid() + savepdf(f"{ev}action{pdf_ex}") + plt.show() + + _ = plt.plot(t, x) + _ = plt.xlabel("time (s)") + _ = plt.ylabel("Position (m)") + _ = plt.title("Cart position") + plt.grid() + savepdf(f"{ev}position{pdf_ex}") + plt.show() + if J_hist is not None: + _ = plt.plot(J_hist) + _ = plt.xlabel("Iteration") + _ = plt.ylabel("Total cost") + _ = plt.title("Total cost-to-go") + plt.grid() + savepdf(f"{ev}J{pdf_ex}") + plt.show() + +def animate(xs0, env): + render = True + if render: + for i in range(2): + render_(xs0, env.discrete_model) + time.sleep(1) + # env.viewer.close() + +def render_(xs, env): + for i in range(xs.shape[0]): + x = xs[i] + env.render(x=x) + +if __name__ == "__main__": + cartpole(use_linesearch=True) diff --git a/irlc/ex07/ilqr_cartpole_agent.py b/irlc/ex07/ilqr_cartpole_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..cd82bd29a258b4eaf04b3f4f2ee3e64b74bf8af2 --- /dev/null +++ b/irlc/ex07/ilqr_cartpole_agent.py @@ -0,0 +1,43 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +from irlc.ex07.ilqr_agent import ILQRAgent +from irlc import train +from irlc import savepdf +import matplotlib.pyplot as plt +from irlc.ex05.model_cartpole import GymSinCosCartpoleEnvironment + +def cartpole_experiment(N=12, use_linesearch=True, figex="", animate=True): + np.random.seed(2) + Tmax = .9 + dt = Tmax/N + env = GymSinCosCartpoleEnvironment(dt=dt, Tmax=Tmax, supersample_trajectory=True, render_mode='human' if animate else None) + agent = ILQRAgent(env, env.discrete_model, N=N, ilqr_iterations=200, use_linesearch=use_linesearch) + stats, trajectories = train(env, agent, num_episodes=1, return_trajectory=True) + + agent.use_ubar = True + stats2, trajectories2 = train(env, agent, num_episodes=1, return_trajectory=True) + env.close() + + xb = agent.xbar + tb = np.arange(N+1)*dt + plt.figure(figsize=(8,6)) + F = 3 + plt.plot(trajectories[0].time, trajectories[0].state[:,F], 'k-', label='Closed-loop $\\pi$') + plt.plot(trajectories2[0].time, trajectories2[0].state[:,F], '-', label='Open-loop $\\bar{u}_k$') + + plt.plot(tb, xb[:,F], '.-', label="iLQR rediction $\\bar{x}_k$") + plt.xlabel("Time/seconds") + plt.ylabel("$\cos(\\theta)$") + plt.title(f"Cartpole environment $T={N}$") + + plt.grid() + plt.legend() + ev = "pendulum" + savepdf(f"irlc_cartpole_theta_N{N}_{use_linesearch}{figex}") + plt.show() + +def plt_cartpole(): + cartpole_experiment(N=50, use_linesearch=True, animate=True) + +if __name__ == '__main__': + plt_cartpole() diff --git a/irlc/ex07/ilqr_pendulum.py b/irlc/ex07/ilqr_pendulum.py new file mode 100644 index 0000000000000000000000000000000000000000..5bcc82e7d94d072bf5ce529e08a99bb6f0647895 --- /dev/null +++ b/irlc/ex07/ilqr_pendulum.py @@ -0,0 +1,68 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +from irlc.ex04.model_pendulum import DiscreteSinCosPendulumModel +import matplotlib.pyplot as plt +import time +from irlc.ex07.ilqr_rendovouz_basic import ilqr +from irlc import savepdf + +def pendulum(use_linesearch): + print("> Using iLQR to solve Pendulum swingup task. Using linesearch?", use_linesearch) + dt = 0.02 + model = DiscreteSinCosPendulumModel(dt, cost=None) + N = 250 + # This rather clunky line gets us the initial state; we transform the bound by the variable transformation. + x0 = np.asarray(model.phi_x(model.continuous_model.x0_bound().low)) + + n_iter = 200 # Use 200 iLQR iterations. + # xs, us, J_hist, L, l = ilqr(model, ...) Write a function call like this, but with the correct parametesr + # TODO: 1 lines missing. + raise NotImplementedError("Call iLQR here (see hint above).") + + render = True + if render: + for i in range(2): + render_(xs, model) + time.sleep(2) # Sleep for two seconds between simulations. + model.close() + xs = np.asarray([model.phi_x_inv(x) for x, u in zip(xs, us)]) # Convert to Radians. We use the build-in functions to change coordinates. + xs, us = np.asarray(xs), np.asarray(us) + + t = np.arange(N) * dt + theta = np.unwrap(xs[:, 0]) # Makes for smoother plots. + theta_dot = xs[:, 1] + + pdf_ex = '_linesearch' if use_linesearch else '' + stitle = "(using linesearch)" if use_linesearch else "(not using linesearch) " + ev = 'pendulum_' + _ = plt.plot(theta, theta_dot) + _ = plt.xlabel("$\\theta$ (rad)") + _ = plt.ylabel("$d\\theta/dt$ (rad/s)") + _ = plt.title(f"Phase Plot {stitle}") + plt.grid() + savepdf(f"{ev}theta{pdf_ex}") + plt.show() + + _ = plt.plot(t, us) + _ = plt.xlabel("time (s)") + _ = plt.ylabel("Force (N)") + _ = plt.title(f"Action path {stitle}") + plt.grid() + savepdf(f"{ev}action{pdf_ex}") + plt.show() + + _ = plt.plot(J_hist) + _ = plt.xlabel("Iteration") + _ = plt.ylabel("Total cost") + _ = plt.title(f"Total cost-to-go {stitle}") + plt.grid() + savepdf(f"{ev}J{pdf_ex}") + plt.show() + +def render_(xs, env): + for i in range(xs.shape[0]): + env.render(xs[i]) + +if __name__ == "__main__": + pendulum(use_linesearch=False) + pendulum(use_linesearch=True) diff --git a/irlc/ex07/ilqr_pendulum_agent.py b/irlc/ex07/ilqr_pendulum_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..a52b0237cd6669973b0dabe95c4b31d6556eb1c3 --- /dev/null +++ b/irlc/ex07/ilqr_pendulum_agent.py @@ -0,0 +1,63 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +from irlc.ex04.model_pendulum import GymSinCosPendulumEnvironment +from irlc.ex07.ilqr_agent import ILQRAgent +from irlc import train +from irlc import savepdf +import matplotlib.pyplot as plt + +Tmax = 3 +def pen_experiment(N=12, use_linesearch=True,figex="", animate=True): + dt = Tmax / N + env = GymSinCosPendulumEnvironment(dt, Tmax=Tmax, supersample_trajectory=True, render_mode="human" if animate else None) + agent = ILQRAgent(env, env.discrete_model, N=N, ilqr_iterations=200, use_linesearch=use_linesearch) + + stats, trajectories = train(env, agent, num_episodes=1, return_trajectory=True) + + agent.use_ubar = True + stats2, trajectories2 = train(env, agent, num_episodes=1, return_trajectory=True) + env.close() + + plot_pendulum_trajectory(env, trajectories[0], label='Closed-loop $\\pi$') + xb = agent.xbar + tb = np.arange(N+1)*dt + plt.figure(figsize=(12, 6)) + plt.plot(trajectories[0].time, trajectories[0].state[:,1], '-', label='Closed-loop $\\pi(x_k)$') + + plt.plot(trajectories2[0].time, trajectories2[0].state[:,1], '-', label='Open-loop $\\bar{u}_k$') + plt.plot(tb, xb[:,1], 'o-', label="iLQR prediction $\\bar{x}_k$") + plt.grid() + plt.legend() + ev = "pendulum" + savepdf(f"irlc_pendulum_theta_N{N}_{use_linesearch}{figex}") + plt.show() + + ## Plot J + plt.figure(figsize=(6, 6)) + plt.semilogy(agent.J_hist, 'k.-') + plt.xlabel("iLQR Iterations") + plt.ylabel("Cost function estimate $J$") + # plt.title("Last value: {") + plt.grid() + savepdf(f"irlc_pendulum_J_N{N}_{use_linesearch}{figex}") + plt.show() + +def plot_pendulum_trajectory(env, traj, style='k.-', label=None, action=False, **kwargs): + if action: + y = traj.action[:, 0] + y = np.clip(y, env.action_space.low[0], env.action_space.high[0]) + else: + y = traj.state[:, 1] + + plt.plot(traj.time[:-1] if action else traj.time, y, style, label=label, **kwargs) + plt.xlabel("Time/seconds") + if action: + plt.ylabel("Torque $u$") + else: + plt.ylabel("$\cos(\\theta)$") + plt.grid() + pass + +N = 50 +if __name__ == "__main__": + pen_experiment(N=N, use_linesearch=True) diff --git a/irlc/ex07/ilqr_rendevoyz.py b/irlc/ex07/ilqr_rendevoyz.py new file mode 100644 index 0000000000000000000000000000000000000000..8cd6cdc30de69f057cb7d024bbdb46f6530e146e --- /dev/null +++ b/irlc/ex07/ilqr_rendevoyz.py @@ -0,0 +1,5 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex07.ilqr_rendovouz_basic import plot_rendevouz + +if __name__ == "__main__": + plot_rendevouz(use_linesearch=True) diff --git a/irlc/ex07/ilqr_rendovouz_basic.py b/irlc/ex07/ilqr_rendovouz_basic.py new file mode 100644 index 0000000000000000000000000000000000000000..255103bd8d5c77baa8d9dfc850885aed8c8259d0 --- /dev/null +++ b/irlc/ex07/ilqr_rendovouz_basic.py @@ -0,0 +1,97 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +import matplotlib.pyplot as plt +from irlc import savepdf +from irlc.ex07.ilqr import ilqr_basic, ilqr_linesearch +from irlc.ex06.model_rendevouz import DiscreteRendevouzModel +from irlc.ex04.control_environment import ControlEnvironment +from irlc.ex04.discrete_control_model import DiscreteControlModel + + +def ilqr(model : DiscreteControlModel, N, x0, n_iter, use_linesearch, verbose=True): + if not use_linesearch: + xs, us, J_hist, L, l = ilqr_basic(model, N, x0, n_iterations=n_iter,verbose=verbose) + else: + xs, us, J_hist, L, l = ilqr_linesearch(model, N, x0, n_iterations=n_iter, tol=1e-6,verbose=verbose) + xs, us = np.stack(xs), np.stack(us) + return xs, us, J_hist, L, l + +def plot_vehicles(x_0, y_0, x_1, y_1, linespec='-', legend=("Vehicle 1", "Vehicle 2")): + _ = plt.title("Trajectory of the two omnidirectional vehicles") + _ = plt.plot(x_0, y_0, "r"+linespec, label=legend[0]) + _ = plt.plot(x_1, y_1, "b"+linespec, label=legend[1]) + +Tmax = 20 +def solve_rendovouz(use_linesearch=False): + model = DiscreteRendevouzModel() + x0 = np.asarray(model.continuous_model.x0_bound().low) # Starting position + N = int(Tmax/model.dt) + return ilqr(model, N, x0, n_iter=10, use_linesearch=use_linesearch), model + +def plot_rendevouz(use_linesearch=False): + (xs, us, J_hist, _, _), env = solve_rendovouz(use_linesearch=use_linesearch) + N = int(Tmax / env.dt) + dt = env.dt + x_0 = xs[:, 0] + y_0 = xs[:, 1] + x_1 = xs[:, 2] + y_1 = xs[:, 3] + x_0_dot = xs[:, 4] + y_0_dot = xs[:, 5] + x_1_dot = xs[:, 6] + y_1_dot = xs[:, 7] + + pdf_ex = '_linesearch' if use_linesearch else '' + ev = 'rendevouz_' + plot_vehicles(x_0, y_0, x_1, y_1, linespec='-', legend=("Vehicle 1", "Vehicle 2")) + plt.legend() + savepdf(f'{ev}trajectory{pdf_ex}') + plt.show() + + t = np.arange(N + 1) * dt + _ = plt.plot(t, x_0, "r") + _ = plt.plot(t, x_1, "b") + _ = plt.xlabel("Time (s)") + _ = plt.ylabel("x (m)") + _ = plt.title("X positional paths") + _ = plt.legend(["Vehicle 1", "Vehicle 2"]) + savepdf(f'{ev}vehicles_x_pos{pdf_ex}') + plt.show() + + _ = plt.plot(t, y_0, "r") + _ = plt.plot(t, y_1, "b") + _ = plt.xlabel("Time (s)") + _ = plt.ylabel("y (m)") + _ = plt.title("Y positional paths") + _ = plt.legend(["Vehicle 1", "Vehicle 2"]) + savepdf(f'{ev}vehicles_y_pos{pdf_ex}') + plt.show() + + _ = plt.plot(t, x_0_dot, "r") + _ = plt.plot(t, x_1_dot, "b") + _ = plt.xlabel("Time (s)") + _ = plt.ylabel("x_dot (m)") + _ = plt.title("X velocity paths") + _ = plt.legend(["Vehicle 1", "Vehicle 2"]) + savepdf(f'{ev}vehicles_vx{pdf_ex}') + plt.show() + + _ = plt.plot(t, y_0_dot, "r") + _ = plt.plot(t, y_1_dot, "b") + _ = plt.xlabel("Time (s)") + _ = plt.ylabel("y_dot (m)") + _ = plt.title("Y velocity paths") + _ = plt.legend(["Vehicle 1", "Vehicle 2"]) + savepdf(f'{ev}vehicles_vy{pdf_ex}') + plt.show() + + _ = plt.plot(J_hist) + _ = plt.xlabel("Iteration") + _ = plt.ylabel("Total cost") + _ = plt.title("Total cost-to-go") + savepdf(f'{ev}cost_to_go{pdf_ex}') + plt.show() + + +if __name__ == "__main__": + plot_rendevouz(use_linesearch=False) diff --git a/irlc/ex07/linearization_agent.py b/irlc/ex07/linearization_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..e06916139a0acaa5ceb36cb5f60aaf62ce55bbb3 --- /dev/null +++ b/irlc/ex07/linearization_agent.py @@ -0,0 +1,67 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [Her24] Tue Herlau. Sequential decision making. (Freely available online), 2024. +""" +from irlc.ex06.dlqr import LQR +from irlc import Agent +from irlc.ex05.model_cartpole import GymSinCosCartpoleEnvironment +from irlc import train, savepdf +import matplotlib.pyplot as plt +import numpy as np +from irlc.ex04.control_environment import ControlEnvironment +from irlc.ex04.discrete_control_model import DiscreteControlModel + +class LinearizationAgent(Agent): + """ Implement the simple linearization procedure described in (Her24, Algorithm 23) which expands around a single fixed point. """ + def __init__(self, env: ControlEnvironment, model : DiscreteControlModel, xbar=None, ubar=None): + self.model = model + N = 50 # Plan on this horizon. The control matrices will converge fairly quickly. + """ Define A, B, d as the list of A/B matrices here. I.e. x[t+1] = A x[t] + B u[t] + d. + You should use the function model.f to do this, which has build-in functionality to compute Jacobians which will be equal to A, B. + It is important that you linearize around xbar, ubar. See (Her24, Section 17.1) for further details. """ + # TODO: 4 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + Q, q, R = self.model.cost.Q, self.model.cost.q, self.model.cost.R + """ Define self.L, self.l here as the (lists of) control matrices. """ + # TODO: 1 lines missing. + raise NotImplementedError("Compute control matrices L, l here using LQR(...)") + super().__init__(env) + + def pi(self, x, k, info=None): + """ + Compute the action here using u_k = L_0 x_k + l_0. The control matrix/vector L_0 can be found as the output from LQR, i.e. + L_0 = L[0] and l_0 = l[0]. + + The reason we use L_0, l_0 (and not L_k, l_k) is because the LQR problem itself is an approximation of the true dynamics + and this controller will be able to balance the pendulum for an infinite amount of time. + """ + # TODO: 1 lines missing. + raise NotImplementedError("Compute current action here") + return u + + +def get_offbalance_cart(waiting_steps=30, sleep_time=0.1): + env = GymSinCosCartpoleEnvironment(Tmax=3, render_mode='human') + env.reset() + import time + time.sleep(sleep_time) + env.state = env.discrete_model.x_upright + env.state[-1] = 0.01 # a bit of angular speed. + for _ in range(waiting_steps): # Simulate the environment for 30 steps to get things out of balance. + env.step(1) + time.sleep(sleep_time) + return env + + +if __name__ == "__main__": + np.random.seed(42) # I don't think these results are seed-dependent but let's make sure. + from irlc import plot_trajectory + env = get_offbalance_cart(4) # Simulate for 4 seconds to get the cart off-balance. Same idea as PID control. + agent = LinearizationAgent(env, model=env.discrete_model, xbar=env.discrete_model.x_upright, ubar=env.action_space.sample()*0) + _, trajectories = train(env, agent, num_episodes=1, return_trajectory=True, reset=False) # Note reset=False to maintain initial conditions. + plot_trajectory(trajectories[0], env, xkeys=[0,2, 3], ukeys=[0]) + env.close() + savepdf("linearization_cartpole") + plt.show() diff --git a/irlc/ex08/__init__.py b/irlc/ex08/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..28514114cf38978975fea28d6e6670715223cfb8 --- /dev/null +++ b/irlc/ex08/__init__.py @@ -0,0 +1,2 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""This directory contains the exercises for week 8.""" diff --git a/irlc/ex08/__pycache__/__init__.cpython-311.pyc b/irlc/ex08/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0cf56d19154df1c345a7e784ebd4018c06211464 Binary files /dev/null and b/irlc/ex08/__pycache__/__init__.cpython-311.pyc differ diff --git a/irlc/ex08/__pycache__/bandits.cpython-311.pyc b/irlc/ex08/__pycache__/bandits.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c02bb467c268e6303ac30d6dbf56ff684aeef2ec Binary files /dev/null and b/irlc/ex08/__pycache__/bandits.cpython-311.pyc differ diff --git a/irlc/ex08/__pycache__/simple_agents.cpython-311.pyc b/irlc/ex08/__pycache__/simple_agents.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07b42a3efde578976cf2c50196bdb81b6934b371 Binary files /dev/null and b/irlc/ex08/__pycache__/simple_agents.cpython-311.pyc differ diff --git a/irlc/ex08/bandit_example.py b/irlc/ex08/bandit_example.py new file mode 100644 index 0000000000000000000000000000000000000000..fa5412b7920ab3f503ab0a5b3f9136ae8e5db32c --- /dev/null +++ b/irlc/ex08/bandit_example.py @@ -0,0 +1,27 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +import matplotlib.pyplot as plt + +if __name__ == "__main__": + from irlc import Agent, train, savepdf + from irlc.ex08.bandits import StationaryBandit + bandit = StationaryBandit(k=10) # A 10-armed bandit + agent = Agent(bandit) # Recall the agent takes random actions + _, trajectories = train(bandit, agent, return_trajectory=True, num_episodes=1, max_steps=500) + plt.plot(trajectories[0].reward) + plt.xlabel("Time step") + plt.ylabel("Reward per time step") + savepdf("dumbitA") + plt.show() + + agent = Agent(bandit) # Recall the agent takes random actions + for i in range(10): + _, trajectories = train(bandit, agent, return_trajectory=True, num_episodes=1, max_steps=500) + regret = np.asarray([r['average_regret'] for r in trajectories[0].env_info[1:]]) + cum_regret = np.cumsum(regret) + plt.plot(cum_regret, label=f"Episode {i}") + plt.legend() + plt.xlabel("Time step") + plt.ylabel("Accumulated Regret") + savepdf("dumbitB") + plt.show() diff --git a/irlc/ex08/bandits.py b/irlc/ex08/bandits.py new file mode 100644 index 0000000000000000000000000000000000000000..7b3b9577c41fe40f4ee1735e8055f46120a32212 --- /dev/null +++ b/irlc/ex08/bandits.py @@ -0,0 +1,216 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +import numpy as np +import matplotlib.pyplot as plt +from gymnasium import Env +from gymnasium.spaces import Discrete +from irlc import train +from tqdm import tqdm +import sys +from irlc import cache_read, cache_write, cache_exists + +class BanditEnvironment(Env): + r""" + A helper class for defining bandit problems similar to e.g. the 10-armed testbed discsused in (SB18). + We are going to implement the bandit problems as greatly simplfied gym environments, as this will allow us to + implement the bandit agents as the familiar ``Agent``. I hope this way of doing it will make it clearer that bandits + are in fact a sort of reinforcement learning method. + + The following code shows an example of how to use a bandit environment: + + .. runblock:: pycon + + >>> from irlc.ex08.bandits import StationaryBandit + >>> env = StationaryBandit(k=10) # 10-armed testbed. + >>> env.reset() # Reset env.q_star + >>> s, r, _, _, info = env.step(3) + >>> print(f"The reward we got from taking arm a=3 was {r=}") + + """ + def __init__(self, k : int): + r""" + Initialize a bandit problem. The observation space is given a dummy value since bandit problems of the sort + (SB18) discuss don't have observations. + + :param k: The number of arms. + """ + super().__init__() + self.observation_space = Discrete(1) # Dummy observation space with a single observation. + self.action_space = Discrete(k) # The arms labelled 0,1,...,k-1. + self.k = k # Number of arms + + def reset(self): + """ + Use this function to reset the all internal parameters of the environment and get ready for a new episode. + In the (SB18) 10-armed bandit testbed, this would involve resetting the expected return + + .. math:: + q^*_a + + The function must return a dummy state and info dictionary to agree with the gym ``Env`` class, but their values are + irrelevant + + :return: + - s - a state, for instance 0 + - info - the info dictionary, for instance {} + """ + raise NotImplementedError("Implement the reset method") + + def bandit_step(self, a): + """ + This helper function simplify the definition of the environments ``step``-function. + + Given an action :math:`r`, this function computes the reward obtained by taking that action :math:`r_t` + and the average regret. This is defined as the expected reward we miss out on by taking the potentially suboptimal action :math:`a` + and is defined as: + + .. math:: + \max_{a'} q^*_{a'} - q_a + + Once implemented, the reward and regret enters into the ``step`` function as follows: + + .. runblock:: pycon + + >>> from irlc.ex08.bandits import StationaryBandit + >>> env = StationaryBandit(k=4) # 4-armed testbed. + >>> env.reset() # Reset all parameters. + >>> _, r, _, _, info = env.step(2) # Take action a=2 + >>> print(f"Reward from a=2 was {r=}, the regret was {info['average_regret']=}") + + :param a: The current action we take + :return: + - r - The reward we thereby incur + - regret - The average regret incurred by taking this action (0 for an optimal action) + """ + reward = 0 # Compute the reward associated with arm a + regret = 0 # Compute the regret, by comparing to the optimal arms reward. + return reward, regret + + def step(self, action): + """ + This function is automatically defind and you do not have to edit it. + In a bandit environment, the step function is simplified greatly since there are no + states to keep track on. It should simply return the reward incurred by the action ``a`` + and (for convenience) also returns the average regret in the ``info``-dictionary. + + :param action: The current action we take :math:`a_t` + :return: + - next_state - This is always ``None`` + - reward - The reward obtained by taking the given action. In (SB18) this is defined as :math:`r_t` + - terminated - Always ``False``. Bandit problems don't terminate. + - truncated - Always ``False`` + - info - For convenience, this includes the average regret (used by the plotting methods) + + """ + reward, average_regret = self.bandit_step(action) + info = {'average_regret': average_regret} + return None, reward, False, False, info + +class StationaryBandit(BanditEnvironment): + """ + Implement the 'stationary bandit environment' which is described in (SB18, Section 2.3) + and used as a running example throughout the chapter. + + We will implement a version with a constant mean offset (q_star_mean), so that + + q* = x + q_star_mean, x ~ Normal(0,1) + + q_star_mean can just be considered to be zero at first. + """ + def __init__(self, k, q_star_mean=0): + super().__init__(k) + self.q_star_mean = q_star_mean + + def reset(self): + """ Set q^*_k = N(0,1) + mean_value. The mean_value is 0 in most examples. I.e., implement the 10-armed testbed environment. """ + self.q_star = np.random.randn(self.k) + self.q_star_mean + self.optimal_action = np.argmax(self.q_star) # Optimal action is the one with the largest q^*-value. + return 0, {} # The reset method in a gym Env must return a (dummy) state and a dictionary. + + def bandit_step(self, a): + """ Return the reward/regret for action a for the simple bandit. Use self.q_star (see reset-function above). + To implement it, implement the reward (see the description of the 10-armed testbed for more information. + How is it computed from from q^*_k?) and also compute the regret. + + As a small hint, since we are computing the average regret, it will in fact be the difference between the + value of q^* corresponding to the current arm, and the q^* value for the optimal arm. + Remember it is 0 if the optimal action is selected. + """ + # TODO: 2 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + # Actual logic goes here. Use self.q_star[a] to get mean reward and np.random.randn() to generate random numbers. + return reward, regret + + def __str__(self): + return f"{type(self).__name__}_{self.q_star_mean}" + +""" +Helper function for running a bunch of bandit experiments and plotting the results. + +The function will run the agents in 'agents' (a list of bandit agents) +on the bandit environment 'bandit' and plot the result. + +Each agent will be evaluated for num_episodes episodes, and one episode consist of 'steps' steps. +However, to speed things up you can use cache, and the bandit will not be evaluated for more than +'max_episodes' over all cache runs. + +""" +def eval_and_plot(bandit, agents, num_episodes=2000, max_episodes=2000, steps=1000, labels=None, use_cache=True): + if labels is None: + labels = [str(agent) for agent in agents] + + f, axs = plt.subplots(nrows=3, ncols=1) + f.set_size_inches(10,7) + (ax1, ax2, ax3) = axs + for i,agent in enumerate(agents): + rw, oa, regret, num_episodes = run_agent(bandit, agent, episodes=num_episodes, max_episodes=max_episodes, steps=steps, use_cache=use_cache) + ax1.plot(rw, label=labels[i]) + ax2.plot(oa, label=labels[i]) + ax3.plot(regret, label=labels[i]) + + for ax in axs: + ax.grid() + ax.set_xlabel("Steps") + + ax1.set_ylabel("Average Reward") + ax2.set_ylabel("% optimal action") + ax3.set_ylabel("Regret $L_t$") + ax3.legend() + f.suptitle(f"Evaluated on {str(bandit)} for {num_episodes} episodes") + +def run_agent(env, agent, episodes=2000, max_episodes=2000, steps=1000, use_cache=False, verbose=True): + """ + Helper function. most of the work involves the cache; the actual training is done by 'train'. + """ + C_regrets_cum_sum, C_oas_sum, C_rewards_sum, C_n_episodes = 0, 0, 0, 0 + if use_cache: + cache = f"cache/{str(env)}_{str(agent)}_{steps}.pkl" + if cache_exists(cache): + print("> Reading from cache", cache) + C_regrets_cum_sum, C_oas_sum, C_rewards_sum, C_n_episodes = cache_read(cache) + + regrets = [] + rewards = [] + cruns = max(0, min(episodes, max_episodes - C_n_episodes)) # Missing runs. + for _ in tqdm(range(cruns), file=sys.stdout, desc=str(agent),disable=not verbose): + stats, traj = train(env, agent, max_steps=steps, verbose=False, return_trajectory=True) + regret = np.asarray([r['average_regret'] for r in traj[0].env_info[1:]]) + regrets.append(regret) + rewards.append(traj[0].reward) + + regrets_cum_sum = C_regrets_cum_sum + oas_sum = C_oas_sum + rewards_sum = C_rewards_sum + episodes = C_n_episodes + if len(regrets) > 0: + regrets_cum_sum += np.cumsum(np.sum(np.stack(regrets), axis=0)) + oas_sum += np.sum(np.stack(regrets) == 0, axis=0) + rewards_sum += np.sum(np.stack(rewards), axis=0) + episodes += cruns + if use_cache and cruns > 0: + cache_write((regrets_cum_sum, oas_sum, rewards_sum, episodes), cache, protocol=4) + return rewards_sum/episodes, oas_sum/episodes, regrets_cum_sum/episodes, episodes diff --git a/irlc/ex08/gradient_agent.py b/irlc/ex08/gradient_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..34b296b0cd1dbcea66b194b63d16422b423df98e --- /dev/null +++ b/irlc/ex08/gradient_agent.py @@ -0,0 +1,48 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc import savepdf +import numpy as np +import matplotlib.pyplot as plt +from irlc.ex08.bandits import eval_and_plot, StationaryBandit +from irlc import Agent + +class GradientAgent(Agent): + def __init__(self, env, alpha=None, use_baseline=True): + self.k = env.action_space.n + self.alpha = alpha + self.baseline=use_baseline + self.H = np.zeros((self.k,)) + super().__init__(env) + + def Pa(self): + """ This helper method returns the probability distribution P(A=a) of chosing the + arm a as a vector + """ + pi_a = np.exp(self.H) + return pi_a / np.sum(pi_a) + + def pi(self, s, t, info_s=None): + if t == 0: + self.R_bar = 0 # average reward baseline + self.H *= 0 # Reset H to all-zeros. + self.t = t # Sore the current time step. + return np.random.choice( self.k, p=self.Pa() ) + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + # TODO: 9 lines missing. + raise NotImplementedError("Implement function body") + + def __str__(self): + return f"{type(self).__name__}_{self.alpha}_{'baseline' if self.baseline else 'no_baseline'}" + +if __name__ == "__main__": + baseline_bandit = StationaryBandit(k=10, q_star_mean=4) + alphas = [0.1, 0.4] + agents = [GradientAgent(baseline_bandit, alpha=alpha, use_baseline=False) for alpha in alphas] + agents += [GradientAgent(baseline_bandit, alpha=alpha, use_baseline=True) for alpha in alphas] + + labels = [f'Gradient Bandit alpha={alpha}' for alpha in alphas ] + labels += [f'With baseline: Gradient Bandit alpha={alpha}' for alpha in alphas ] + use_cache = False + eval_and_plot(baseline_bandit, agents, max_episodes=2000, num_episodes=100, labels=labels, use_cache=use_cache) + savepdf("gradient_baseline") + plt.show() diff --git a/irlc/ex08/grand_bandit_race.py b/irlc/ex08/grand_bandit_race.py new file mode 100644 index 0000000000000000000000000000000000000000..ad466aaaffc88b0b4aa43375b55640aa17dc096a --- /dev/null +++ b/irlc/ex08/grand_bandit_race.py @@ -0,0 +1,78 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import matplotlib.pyplot as plt +from irlc.ex08.simple_agents import BasicAgent +from irlc.ex08.bandits import StationaryBandit, eval_and_plot +from irlc.ex08.nonstationary import MovingAverageAgent, NonstationaryBandit +from irlc.ex08.gradient_agent import GradientAgent +from irlc.ex08.ucb_agent import UCBAgent +from irlc import savepdf +import time + +if __name__ == "__main__": + print("Ladies and gentlemen. It is time for the graaand bandit race") + def intro(bandit, agents): + print("We are live from the beautiful surroundings where they will compete in:") + print(bandit) + print("Who will win? who will have the most regret? we are about to find out") + print("in a minute after a brief word from our sponsors") + time.sleep(1) + print("And we are back. Let us introduce todays contestants:") + for a in agents: + print(a) + print("And they are off!") + epsilon = 0.1 + alpha = 0.1 + c = 2 + # TODO: 1 lines missing. + raise NotImplementedError("Define the bandit here: bandit1 = ...") + # TODO: 5 lines missing. + raise NotImplementedError("define agents list here") + labels = ["Basic", "Moving avg.", "gradient", "Gradient+baseline", "UCB"] + ''' + Stationary, no offset. Vanilla setting. + ''' + intro(bandit1, agents) + # TODO: 1 lines missing. + raise NotImplementedError("Call eval_and_plot here") + plt.suptitle("Stationary bandit (no offset)") + savepdf("grand_race_1") + plt.show() + ''' + Stationary, but with offset + ''' + print("Whew what a race. Let's get ready to next round:") + # TODO: 1 lines missing. + raise NotImplementedError("Define bandit2 = ... here") + intro(bandit2, agents) + # TODO: 1 lines missing. + raise NotImplementedError("Call eval_and_plot here") + plt.suptitle("Stationary bandit (with offset)") + savepdf("grand_race_2") + plt.show() + ''' + Long (nonstationary) simulations + ''' + print("Whew what a race. Let's get ready to next round which will be a long one.") + # TODO: 1 lines missing. + raise NotImplementedError("define bandit3 here") + intro(bandit3, agents) + # TODO: 1 lines missing. + raise NotImplementedError("call eval_and_plot here") + plt.suptitle("Non-stationary bandit (no offset)") + savepdf("grand_race_3") + plt.show() + + ''' + Stationary, no offset, long run. Exclude stupid bandits. + ''' + agents2 = [] + agents2 += [GradientAgent(bandit1, alpha=alpha, use_baseline=False)] + agents2 += [GradientAgent(bandit1, alpha=alpha, use_baseline=True)] + agents2 += [UCBAgent(bandit1, c=2)] + labels = ["Gradient", "Gradient+baseline", "UCB"] + intro(bandit1, agents2) + # TODO: 1 lines missing. + raise NotImplementedError("Call eval_and_plot here") + plt.suptitle("Stationary bandit (no offset)") + savepdf("grand_race_4") + plt.show() diff --git a/irlc/ex08/nonstationary.py b/irlc/ex08/nonstationary.py new file mode 100644 index 0000000000000000000000000000000000000000..1128f0a0bd24b8a8d487c2f8c79ac4f38a94d58f --- /dev/null +++ b/irlc/ex08/nonstationary.py @@ -0,0 +1,62 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +import numpy as np +import matplotlib.pyplot as plt +from irlc.ex08.simple_agents import BasicAgent +from irlc.ex08.bandits import StationaryBandit, eval_and_plot +from irlc import savepdf + +class NonstationaryBandit(StationaryBandit): + def __init__(self, k, q_star_mean=0, reward_change_std=0.01): + self.reward_change_std = reward_change_std + super().__init__(k, q_star_mean) + + def bandit_step(self, a): + """ Implement the non-stationary bandit environment (as described in (SB18)). + Hint: use reward_change_std * np.random.randn() to generate a single random number with the given std. + then add one to each coordinate. Remember you have to compute the regret as well, see StationaryBandit for ideas. + (remember the optimal arm will change when you add noise to q_star) """ + # TODO: 2 lines missing. + raise NotImplementedError("Implement function body") + return super().bandit_step(a) + + def __str__(self): + return f"{type(self).__name__}_{self.q_star_mean}_{self.reward_change_std}" + + +class MovingAverageAgent(BasicAgent): + """ + The simple bandit from (SB18, Section 2.4), but with moving average alpha + as described in (SB18, Eqn. (2.3)) + """ + def __init__(self, env, epsilon, alpha): + # TODO: 2 lines missing. + raise NotImplementedError("Implement function body") + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + + def __str__(self): + return f"{type(self).__name__}_{self.epsilon}_{self.alpha}" + + +if __name__ == "__main__": + plt.figure(figsize=(10, 10)) + epsilon = 0.1 + alphas = [0.15, 0.1, 0.05] + + # TODO: 4 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + + labels = [f"Basic agent, epsilon={epsilon}"] + # TODO: 1 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + use_cache = False # Set this to True to use cache (after code works!) + eval_and_plot(bandit, agents, steps=10000, num_episodes=200, labels=labels, use_cache=use_cache) + savepdf("nonstationary_bandits") + plt.show() diff --git a/irlc/ex08/simple_agents.py b/irlc/ex08/simple_agents.py new file mode 100644 index 0000000000000000000000000000000000000000..8c51d02312a2150d5d58c2166210befc2c366fca --- /dev/null +++ b/irlc/ex08/simple_agents.py @@ -0,0 +1,57 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +import numpy as np +import matplotlib.pyplot as plt +from irlc.ex08.bandits import StationaryBandit, eval_and_plot +from irlc import Agent +from irlc import savepdf + +class BasicAgent(Agent): + """ + Simple bandit as described on (SB18, Section 2.4). + """ + def __init__(self, env, epsilon): + super().__init__(env) + self.k = env.action_space.n + self.epsilon = epsilon + + def pi(self, s, t, info=None): + """ Since this is a bandit, s=None and can be ignored, while t refers to the time step in the current episode """ + if t == 0: + # At step 0 of episode. Re-initialize data structure. + # TODO: 2 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + # compute action here + # TODO: 1 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + """ Since this is a bandit, done, s, sp, info_s, info_sp can all be ignored. + From the input arguments you should only use a + """ + # TODO: 2 lines missing. + raise NotImplementedError("Implement function body") + + def __str__(self): + return f"BasicAgent_{self.epsilon}" + +if __name__ == "__main__": + N = 100000 + S = [np.max( np.random.randn(10) ) for _ in range(100000) ] + print( np.mean(S), np.std(S)/np.sqrt(N) ) + + use_cache = False # Set this to True to use cache (after code works!) + from irlc.utils.timer import Timer + timer = Timer(start=True) + R = 100 + steps = 1000 + env = StationaryBandit(k=10) + agents = [BasicAgent(env, epsilon=.1), BasicAgent(env, epsilon=.01), BasicAgent(env, epsilon=0) ] + eval_and_plot(env, agents, num_episodes=100, steps=1000, max_episodes=150, use_cache=use_cache) + savepdf("bandit_epsilon") + plt.show() + print(timer.display()) diff --git a/irlc/ex08/ucb_agent.py b/irlc/ex08/ucb_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..cd706ab879562cd16c31fac2941eaa78ef6caa7c --- /dev/null +++ b/irlc/ex08/ucb_agent.py @@ -0,0 +1,45 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +import numpy as np +import matplotlib.pyplot as plt +from irlc.ex08.simple_agents import BasicAgent +from irlc import savepdf +from irlc import Agent + +class UCBAgent(Agent): + def __init__(self, env, c=2): + self.c = c + super().__init__(env) + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + # TODO: 2 lines missing. + raise NotImplementedError("Train agent here") + + def pi(self, s, k, info=None): + if k == 0: + """ Initialize the agent""" + # TODO: 3 lines missing. + raise NotImplementedError("Reset agent (i.e., make it ready to learn in a new episode with a new optimal action)") + # TODO: 1 lines missing. + raise NotImplementedError("Compute (and return) optimal action") + + def __str__(self): + return f"{type(self).__name__}_{self.c}" + +from irlc.ex08.bandits import StationaryBandit, eval_and_plot +if __name__ == "__main__": + """ Reproduce (SB18, Fig. 2.4) comparing UCB agent to epsilon greedy """ + runs, use_cache = 100, False + c = 2 + eps = 0.1 + + steps = 1000 + env = StationaryBandit(k=10) + agents = [UCBAgent(env,c=c), BasicAgent(env, epsilon=eps)] + eval_and_plot(bandit=env, agents=agents, num_episodes=runs, steps=steps, max_episodes=2000, use_cache=use_cache) + savepdf("UCB_agent") + plt.show() diff --git a/irlc/ex09/__init__.py b/irlc/ex09/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e753a4f8165d8230bf25dad15c13bb55af050a60 --- /dev/null +++ b/irlc/ex09/__init__.py @@ -0,0 +1,2 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""This directory contains the exercises for week 9.""" diff --git a/irlc/ex09/__pycache__/__init__.cpython-311.pyc b/irlc/ex09/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..770ea7a94070c53deff8bab18a19af41044593a6 Binary files /dev/null and b/irlc/ex09/__pycache__/__init__.cpython-311.pyc differ diff --git a/irlc/ex09/__pycache__/mdp.cpython-311.pyc b/irlc/ex09/__pycache__/mdp.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e6c15cd660ced299b47841c52203bdccca264409 Binary files /dev/null and b/irlc/ex09/__pycache__/mdp.cpython-311.pyc differ diff --git a/irlc/ex09/__pycache__/mdp_warmup.cpython-311.pyc b/irlc/ex09/__pycache__/mdp_warmup.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..48b1298531f6c58a39f743d79eeb976a262d212c Binary files /dev/null and b/irlc/ex09/__pycache__/mdp_warmup.cpython-311.pyc differ diff --git a/irlc/ex09/__pycache__/rl_agent.cpython-311.pyc b/irlc/ex09/__pycache__/rl_agent.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5540d8734650d6d4c0b057b272c9ec92e443e436 Binary files /dev/null and b/irlc/ex09/__pycache__/rl_agent.cpython-311.pyc differ diff --git a/irlc/ex09/__pycache__/value_iteration.cpython-311.pyc b/irlc/ex09/__pycache__/value_iteration.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..824c674ad5ff89eefd1e4abc43e24567ffb038f5 Binary files /dev/null and b/irlc/ex09/__pycache__/value_iteration.cpython-311.pyc differ diff --git a/irlc/ex09/__pycache__/value_iteration_agent.cpython-311.pyc b/irlc/ex09/__pycache__/value_iteration_agent.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f0351567c0782f22dfae996b4cd3f6b35a9c91f Binary files /dev/null and b/irlc/ex09/__pycache__/value_iteration_agent.cpython-311.pyc differ diff --git a/irlc/ex09/gambler.py b/irlc/ex09/gambler.py new file mode 100644 index 0000000000000000000000000000000000000000..c45a7e5cb6726b808b857c951752923b56c99cbb --- /dev/null +++ b/irlc/ex09/gambler.py @@ -0,0 +1,81 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +from irlc import savepdf +import matplotlib.pyplot as plt +from irlc.ex09.value_iteration import value_iteration +from irlc.ex09.mdp import MDP + +class GamblerEnv(MDP): + """ + The gamler problem (see description given in (SB18, Example 4.3)) + + See the MDP class for more information about the methods. In summary: + > the state is the amount of money you have. if state = goal or state = 0 the game ends (use this for is_terminal) + > A are the available actions (a list). Note that these depends on the state; see below or example for details. + > Psr are the transitions (see MDP class for documentation) + """ + def __init__(self, goal=100, p_heads=0.4): + super().__init__(initial_state=goal//2) + self.goal = goal + self.p_heads = p_heads + + def is_terminal(self, state): + """ Implement if the state is terminal (0 or self.goal) """ + # TODO: 1 lines missing. + raise NotImplementedError("Return true only if state is terminal.") + + def A(self, s): + """ Action is the amount you choose to gamle. + You can gamble from 0 and up to the amount of money you have (state), + but not so much you will exceed the goal amount (see (SB18) for details). + In other words, return this as a list, and the number of elements should depend on the state s. """ + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + + def Psr(self, s, a): + """ Implement transition probabilities here. + the reward is 1 if you win (obtain goal amount) and otherwise 0. Remember the format should + return a dictionary with entries: + > { (sp, r) : probability } + + You can see the small-gridworld example (see exercise description) for an example of how to use this function, + but now you should keep in mind that since you can win (or not) the dictionary you return should have two entries: + one with a probability of self.p_heads (winning) and one with a probability of 1-self.p_heads (loosing). + """ + # TODO: 4 lines missing. + raise NotImplementedError("Implement function body") + return outcome_dict + +def gambler(): + """ + Gambler's problem from (SB18, Example 4.3) + """ + mdp = GamblerEnv(p_heads=0.4) + pi, V = value_iteration(mdp, gamma=1., theta=1e-11) + + V = [V[s] for s in mdp.states] + plt.bar(mdp.states, V) + plt.xlabel('Capital') + plt.ylabel('Value Estimates') + plt.title('Final value function (expected return) vs State (Capital)') + plt.grid() + savepdf("gambler_valuefunction") + plt.show() + + y = [pi[s] for s in mdp.nonterminal_states] + plt.bar(mdp.nonterminal_states, y, align='center', alpha=0.5) + plt.xlabel('Capital') + plt.ylabel('Final policy (stake)') + plt.title('Capital vs Final Policy') + plt.grid() + savepdf("gambler_policy") + plt.show() + + +if __name__ == "__main__": + + gambler() diff --git a/irlc/ex09/mdp.py b/irlc/ex09/mdp.py new file mode 100644 index 0000000000000000000000000000000000000000..367ebdf9bb3e6f532f84f1b53da2a9f26cafdf87 --- /dev/null +++ b/irlc/ex09/mdp.py @@ -0,0 +1,303 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +import numpy as np +import gymnasium as gym +from gymnasium import Env +from collections import defaultdict +from tqdm import tqdm +import sys + +class MDP: + r""" + This class represents a Markov Decision Process. It defines three main components: + + - The actions available in a given state :math:`A(s)` + - The transition probabilities :math:`p(s', r | s, a)` + - A terminal check to determine if a state :math:`s` is terminal + - A way to specify the initial state: + + - As a single state the MDP always begins in (most common) + - As a general distribution :math:`p(s_0)`. + + In addition to this it allows you to access either + - The set of all states (including terminal states) as ``mdp.states`` + - The set of all non-terminal states as ``mdp.non_terminal_states`` + + .. note:: + The ``states`` and ``non_termianl_states`` are computed lazily. This means that if you don't access them, they won't use memory. + This allows you to specify MDPs with an infinite number of states without running out of memory. + """ + def __init__(self, initial_state=None, verbose=False): + """ + Initialize the MDP. In the case where ``initial_state`` is set to a value :math:`s_0`, the initial state distribution will be + + .. math:: + p(s_0) = 1 + + :param initial_state: An optional initial state. + :param verbose: If ``True``, the class will print out debug information (useful for very large MDPs) + """ + self.verbose=verbose + self.initial_state = initial_state # Starting state s_0 of the MDP. + # The following variables that begin with _ are used to cache computations. The reason why we don't compute them + # up-front is because their computation may be time-consuming and they might not be needed. + self._states = None + self._nonterminal_states = None + self._terminal_states = None + + def is_terminal(self, state) -> bool: + r""" + Determines if a state is terminal (i.e., the environment/model is complete). In (SB18), the terminal + state is written as :math:`s_T`. + + .. runblock:: pycon + + >>> from irlc.gridworld.gridworld_environments import FrozenLake + >>> mdp = FrozenLake().mdp + >>> mdp.is_terminal(mdp.initial_state) # False, obviously. + + + :param state: The state :math:`s` to check + :return: ``True`` if the state is terminal and otherwise ``False``. + """ + return False # Return true if the given state is terminal. + + def Psr(self, state, action) -> dict: + r""" + Represents the transition probabilities: + + .. math:: + P(s', r | s, a) + + When called with state ``state`` and action ``action``, the function returns a dictionary of the form + ``{(s1, r1): p1, (s2, r2): p2, ...}``, so that ``p2`` is the probability of transitioning to ``s2`` (and obtaining + reward ``r2``) given we are in state ``state`` and take action ``action``: + + .. math:: + P(s_2, r_2 | s,a) = p_2 + + An example: + + .. runblock:: pycon + + >>> from irlc.gridworld.gridworld_environments import FrozenLake + >>> mdp = FrozenLake().mdp + >>> transitions = mdp.Psr(mdp.initial_state, 0) # P( ... | s0, a=0) + >>> for (sp, r), p in transitions.items(): + ... print(f"P(s'={sp}, r={r} | s={mdp.initial_state}, a=0) = {p}") + + :param state: The state to compute the transition probabilities in + :param action: The action to compute the transition probabilities in + :return: A dictionary where the keys are state, reward pairs we will transition to, :math:`p(s', r | ...)`, and the values are their probability. + """ + raise NotImplementedError("Return state distribution as a dictionary (see class documentation)") + + def A(self, state) -> list: + """ + Returns a list of actions available in the given state: + + .. math:: + A(s) + + An example to get the actions in the initial state: + + .. runblock:: pycon + + >>> from irlc.gridworld.gridworld_environments import FrozenLake + >>> mdp = FrozenLake().mdp + >>> mdp.A(mdp.initial_state) + + :param state: State to compute the actions in :math:`s` + :return: The list of available actions :math:`\mathcal A(s) = \{0, 1, ..., n-1\}` + """ + raise NotImplementedError("Return set/list of actions in given state A(s) = {a1, a2, ...}") + + def initial_state_distribution(self): + """ + (**Optional**) specify the initial state distribution. Should return a dictionary of the form: + ``{s0: p0, s1: p1, ..., sn: pn}``, in which case :math:`p(S_0 = s_k) = p_k`. + + You will typically not overwrite this function but just set the initial state. In that case the initial state distribution + is deterministic: + + + .. runblock:: pycon + + >>> from irlc.gridworld.gridworld_environments import FrozenLake + >>> mdp = FrozenLake().mdp + >>> mdp.initial_state_distribution() + + + + :return: An initial state distribution as a dictionary, where the keys are states, and the valuse are their probability. + """ + if self.initial_state is not None: + return {self.initial_state: 1} + else: + raise Exception("Either specify the initial state, or implement this method.") + + @property + def nonterminal_states(self): + r""" + The list of non-terminal states, i.e. :math:`\mathcal{S}` in (SB18) + + + .. runblock:: pycon + + >>> from irlc.gridworld.gridworld_environments import FrozenLake + >>> mdp = FrozenLake().mdp + >>> mdp.nonterminal_states + + :return: The list of non-terminal states :math:`\mathcal{S}` + """ + if self._nonterminal_states is None: + self._nonterminal_states = [s for s in self.states if not self.is_terminal(s)] + return self._nonterminal_states + + @property + def states(self): + r""" + The list of all states including terminal ones, i.e. :math:`\mathcal{S}^+` in (SB18). + The terminal states are those where ``is_terminal(state)`` is true. + + .. runblock:: pycon + + >>> from irlc.gridworld.gridworld_environments import FrozenLake + >>> mdp = FrozenLake().mdp + >>> mdp.states + + :return: The list all states :math:`\mathcal{S}^+` + """ + if self._states is None: + next_chunk = set(self.initial_state_distribution().keys()) + all_states = list(next_chunk) + while True: + new_states = set() + for s in tqdm(next_chunk, file=sys.stdout) if self.verbose else next_chunk: + if self.is_terminal(s): + continue + for a in self.A(s): + new_states = new_states | {sp for sp, r in self.Psr(s, a)} + + new_states = [s for s in new_states if s not in all_states] + if len(new_states) == 0: + break + all_states += new_states + next_chunk = new_states + self._states = list(set(all_states)) + + return self._states + + +def rng_from_dict(d): + """ Helper function. If d is a dictionary {x1: p1, x2: p2, ...} then this will sample an x_i with probability p_i """ + w, pw = zip(*d.items()) # seperate w and p(w) + i = np.random.choice(len(w), p=pw) # Required because numpy cast w to array (and w may contain tuples) + return w[i] + +class MDP2GymEnv(Env): + + def A(self, state): + raise Exception("Don't use this function; it is here for legacy reasons") + + def __init__(self, mdp, render_mode=None): + # We ignore this variable in this class, however, the Gridworld environment will check if + # render_mode == "human" and use it to render the environment. See: + # https://younis.dev/blog/render-api/ + self.render_mode = render_mode + self.mdp = mdp + self.state = None + # actions = set + all_actions = set.union(*[set(self.mdp.A(s)) for s in self.mdp.nonterminal_states ]) + n = max(all_actions) - min(all_actions) + 1 + assert isinstance(n, int) + self.action_space = gym.spaces.Discrete(n=n, start=min(all_actions)) + # Make observation space: + states = self.mdp.nonterminal_states + if not hasattr(self, 'observation_space'): + if isinstance(states[0], tuple): + self.observation_space = gym.spaces.Tuple([gym.spaces.Discrete(n+1) for n in np.asarray(states).max(axis=0)]) + else: + print("Could not guess observation space. Set it manually.") + + + def reset(self, seed=None, options=None): + info = {} + if seed is not None: + np.random.seed(seed) + self.action_space.seed(seed) + self.observation_space.seed(seed) + info['seed'] = seed + + ps = self.mdp.initial_state_distribution() + self.state = rng_from_dict(ps) + if self.render_mode == "human": + self.render() + info['mask'] = self._mk_mask(self.state) + return self.state, info + + def step(self, action): + ps = self.mdp.Psr(self.state, action) + self.state, reward = rng_from_dict(ps) + terminated = self.mdp.is_terminal(self.state) + if self.render_mode == "human": + self.render() + info = {'mask': self._mk_mask(self.state)} if not terminated else None + return self.state, reward, terminated, False, info + + def _mk_mask(self, state): + # self.A(state) + mask = np.zeros((self.action_space.n,), dtype=np.int8) + for a in self.mdp.A(state): + mask[a - self.action_space.start] = 1 + return mask + + +class GymEnv2MDP(MDP): + def __init__(self, env): + super().__init__() + self._states = list(range(env.observation_space.n)) + if hasattr(env, 'env'): + env = env.env + self._terminal_states = [] + for s in env.P: + for a in env.P[s]: + for (pr, sp, reward, done) in env.P[s][a]: + if done: + self._terminal_states.append(sp) + + self._terminal_states = set(self._terminal_states) + self.env = env + + def is_terminal(self, state): + return state in self._terminal_states + + def A(self, state): + return list(self.env.P[state].keys()) + + def Psr(self, state, action): + d = defaultdict(float) + for (pr, sp, reward, done) in self.env.P[state][action]: + d[ (sp, reward)] += pr + return d + +if __name__ == '__main__': + """A handful of examples of using the MDP-class in conjunction with a gym environment:""" + env = gym.make("FrozenLake-v1") + mdp = GymEnv2MDP(env) + from irlc.ex09.value_iteration import value_iteration + value_iteration(mdp) + mdp = GymEnv2MDP(gym.make("FrozenLake-v1")) + print("N = ", mdp.nonterminal_states) + print("S = ", mdp.states) + print("Is state 3 terminal?", mdp.is_terminal(3), "is state 11 terminal?", mdp.is_terminal(11)) + state = 0 + print("A(S=0) =", mdp.A(state)) + action = 2 + mdp.Psr(state, action) # Get transition probabilities + for (next_state, reward), Pr in mdp.Psr(state, action).items(): + print(f"P(S'={next_state},R={reward} | S={state}, A={action} ) = {Pr:.2f}") diff --git a/irlc/ex09/mdp_warmup.py b/irlc/ex09/mdp_warmup.py new file mode 100644 index 0000000000000000000000000000000000000000..aab1ac665700dfc6392773d9501056a43bb735c4 --- /dev/null +++ b/irlc/ex09/mdp_warmup.py @@ -0,0 +1,86 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +from irlc.ex09.mdp import MDP + + +def value_function2q_function(mdp : MDP, s, gamma, v : dict) -> dict: + r"""This helper function converts a value function to an action-value function. + + Given a value-function ``v`` and a state ``s``, this function implements the update: + + .. math:: + + Q(s,a) = \mathbb{E}[r + \gamma * v(s') | s, a] = \sum_{r, s'} (r + \gamma v(s') ) p(s', r| s,a) + + as described in (SB18, ). It should return a dictionary of the form:: + + {a1: Q(s,a1), a2: Q(s,a2), ..., an: Q(s,an)} + + where the actions are keys. You can compute these using ``mdp.A(s)``. When done the following should work:: + + Qs = value_function2q_function(mdp, s, gamma, v) + Qs[a] # This is the Q-value Q(s,a) + + Hints: + + * Remember that ``v[s'] = 0`` if ``s'`` is a terminal state (this is explained in (SB18)). + + :param mdp: An MDP instance. Use this to compute :math:`p(s', r| s,a)` + :param s: A state + :param gamma: The discount factor :math:`\gamma` + :param v: The value function represented as a dictionary. + :return: A dictionary representing :math:`Q` of the form ``{a1: Q(s,a1), a2: Q(s,a2), ..., an: Q(s,an)}`` + """ + # TODO: 1 lines missing. + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + return q_dict + +def expected_reward(mdp : MDP, s, a) -> float: + # TODO: 1 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + return expected_reward + +def q_function2value_function(policy : dict, Q : dict, s) -> float: + # TODO: 1 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + return V_s + +if __name__ == "__main__": + from irlc.gridworld.gridworld_environments import FrozenLake + mdp = FrozenLake(living_reward=0.2).mdp # Get the MDP of this environment. + + ## Part 1: Expected reward + s0 = mdp.initial_state + s0 = (0, 3) # initial state + a = 3 # Go east. + print("Expected reward E[r | s0, a] =", expected_reward(mdp, s=s0, a=0), "should be 0.2") + print("Expected reward E[r | s0, a] =", expected_reward(mdp, s=(1, 2), a=0), "should be 0") + + + ## Part 2 + # First let's create a non-trivial value function + V = {} + for s in mdp.nonterminal_states: + V[s] = s[0] + 2*s[1] + print("Value function is", V) + # Compute the corresponding Q(s,a)-values in state s0: + q_ = value_function2q_function(mdp, s=s0, gamma=0.9, v=V) + print(f"Q-values in {s0=} is", q_) + + ## Part 3 + # Create a non-trivial Q-function for this problem. + Q = {} + for s in mdp.nonterminal_states: + for a in mdp.A(s): + Q[s,a] = s[0] + 2*s[1] - 10*a # The particular values are not important in this example + # Create a policy. In this case pi(a=3) = 0.4. + pi = {0: 0.2, + 1: 0.2, + 2: 0.2, + 3: 0.4} + print(f"Value-function in {s0=} is", q_function2value_function(pi, Q, s=s0)) diff --git a/irlc/ex09/policy_evaluation.py b/irlc/ex09/policy_evaluation.py new file mode 100644 index 0000000000000000000000000000000000000000..8abbf5e7a742d29cd18b77ce805346e3d6b12f9f --- /dev/null +++ b/irlc/ex09/policy_evaluation.py @@ -0,0 +1,68 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +from collections import defaultdict +import numpy as np +import matplotlib.pyplot as plt +from irlc.ex09.mdp_warmup import value_function2q_function +from irlc.ex09.small_gridworld import SmallGridworldMDP, plot_value_function +from irlc import savepdf + + +def policy_evaluation(pi, mdp, gamma=.99, theta=0.00001): + """ Implements the iterative policy-evaluation algorithm ((SB18, Section 4.1)). + The algorithm is given a policy pi which is represented as a dictionary so that + + > pi[s][a] = p + + is the probability p of taking action a in state s. The 'mdp' is a MDP-instance and the other terms have the same meaning as in the algorithm. + It should return a dictionary v so that + > v[s] + is the value-function evaluated in state s. I recommend using the qs_-function defined above. + """ + v = defaultdict(float) + Delta = theta #Initialize the 'Delta'-variable to a large value to make sure the first iteration of the method runs. + while Delta >= theta: # Outer loop in (SB18) + Delta = 0 # Remember to update Delta (same meaning as in (SB18)) + # Remember that 'S' in (SB18) is actually just the set of non-terminal states (NOT including terminal states!) + for s in mdp.nonterminal_states: # See the MDP class if you are curious about how this variable is defined. + """ Implement the main body of the policy evaluation algorithm here. You can do this directly, + or implement (and use) the value_function2q_function-function (consider what it does and compare to the algorithm). + If you do so, note that value_function2q_function(mdp, s, gamma, v) computes the equivalent of Q(s,a) (as a dictionary), + and in the algorithm, you then need to compute the expectation over pi: + > sum_a pi(a|s) Q(s,a) + In code it would be more akin to + q = value_function2q_function(...) + sum_a pi[s][a] * q[a] + + Don't be afraid to use a few more lines than I do. + """ + # TODO: 2 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + """ stop condition. v_ is the yafcport value of the value function (see algorithm listing in (SB18)) which you need to update. """ + Delta = max(Delta, np.abs(v_ - v[s])) + return v + + +if __name__ == "__main__": + mdp = SmallGridworldMDP() + """ + Create the random policy pi0 below. The policy is defined as a nested dict, i.e. + + > pi0[s][a] = (probability to take action a in state s) + + """ + pi0 = {s: {a: 1/len(mdp.A(s)) for a in mdp.A(s) } for s in mdp.nonterminal_states } + V = policy_evaluation(pi0, mdp, gamma=1) + plot_value_function(mdp, V) + plt.title("Value function using random policy") + savepdf("policy_eval") + plt.show() + + expected_v = np.array([0, -14, -20, -22, + -14, -18, -20, -20, + -20, -20, -18, -14, + -22, -20, -14, 0]) diff --git a/irlc/ex09/policy_iteration.py b/irlc/ex09/policy_iteration.py new file mode 100644 index 0000000000000000000000000000000000000000..a2ab623a5d40097bee4d0b47d541d8e547a4f954 --- /dev/null +++ b/irlc/ex09/policy_iteration.py @@ -0,0 +1,63 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +import numpy as np +from irlc.ex09.small_gridworld import SmallGridworldMDP +import matplotlib.pyplot as plt +from irlc.ex09.policy_evaluation import policy_evaluation +from irlc.ex09.mdp_warmup import value_function2q_function + +def policy_iteration(mdp, gamma=1.0): + r""" + Implement policy iteration (see (SB18, Section 4.3)). + + Note that policy iteration only considers deterministic policies. we will therefore use the shortcut by representing the policy pi + as a dictionary (similar to the DP-problem in week 2!) so that + > a = pi[s] + is the action in state s. + + """ + pi = {s: np.random.choice(mdp.A(s)) for s in mdp.nonterminal_states} + policy_stable = False + V = None # Sutton has an initialization-step, but it can actually be skipped if we intialize the policy randomly. + while not policy_stable: + # Evaluate the current policy using your code from the previous exercise. + # The main complication is that we need to transform our deterministic policy, pi[s], into a stochastic one pi[s][a]. + # It will be defined as: + # >>> pi_prob[s][a] = 1 if a = pi[s] and otherwise 0. + pi_prob = {s: {a: 1 if pi[s] == a else 0 for a in mdp.A(s)} for s in mdp.nonterminal_states} + V = policy_evaluation(pi_prob, mdp, gamma) + V = policy_evaluation( {s: {pi[s]: 1} for s in mdp.nonterminal_states}, mdp, gamma) + """ Implement the method. This is step (3) in (SB18). """ + policy_stable = True # Will be set to False if the policy pi changes + r""" Implement the steps for policy improvement here. Start by writing a for-loop over all non-terminal states + you can see the policy_evaluation function for how to do this, but + I recommend looking at the property mdp.nonterminal_states (see MDP class for more information). + Hints: + * In the algorithm in (SB18), you need to perform an argmax_a over what is actually Q-values. The function + value_function2q_function(mdp, s, gamma, V) can compute these. + * The argmax itself, assuming you follow the above procedure, involves a dictionary. It can be computed + using methods similar to those we saw in week2 of the DP problem. + It is not a coincidence these algorithms are very similar -- if you think about it, the maximization step closely resembles the DP algorithm! + """ + # TODO: 6 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + return pi, V + +if __name__ == "__main__": + mdp = SmallGridworldMDP() + pi, v = policy_iteration(mdp, gamma=0.99) + expected_v = np.array([ 0, -1, -2, -3, + -1, -2, -3, -2, + -2, -3, -2, -1, + -3, -2, -1, 0]) + + from irlc.ex09.small_gridworld import plot_value_function + plot_value_function(mdp, v) + plt.title("Value function using policy iteration to find optimal policy") + from irlc import savepdf + savepdf("policy_iteration") + plt.show() diff --git a/irlc/ex09/rl_agent.py b/irlc/ex09/rl_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..94e3c1ca80c3b7d41770296ae6eb224aa40f5bab --- /dev/null +++ b/irlc/ex09/rl_agent.py @@ -0,0 +1,212 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +from irlc.utils.common import defaultdict2 +from irlc import Agent + +class TabularAgent(Agent): + """ + This helper class will simplify the implementation of most basic reinforcement learning. Specifically it provides: + + - A :math:`Q(s,a)`-table data structure + - An epsilon-greedy exploration method + + The code for the class is very simple, and I think it is a good idea to at least skim it. + + The Q-data structure can be used a follows: + + .. runblock:: pycon + + >>> from irlc.ex09.rl_agent import TabularAgent + >>> from irlc.gridworld.gridworld_environments import BookGridEnvironment + >>> env = BookGridEnvironment() + >>> agent = TabularAgent(env) + >>> state, info = env.reset() # Get the info-dictionary corresponding to s + >>> agent.Q[state, 1] = 2.5 # Update a Q-value; action a=1 is now optimal. + >>> agent.Q[state, 1] # Check it has indeed been updated. + >>> agent.Q[state, 0] # Q-values are 0 by default. + >>> agent.Q.get_optimal_action(state, info) # Note we pass along the info-dictionary corresopnding to this state + + .. note:: + The ``get_optimal_action``-function requires an ``info`` dictionary. This is required since the info dictionary + contains information about which actions are available. To read more about the Q-values, see :class:`~irlc.ex09.rl_agent.TabularQ`. + """ + def __init__(self, env, gamma=0.99, epsilon=0): + """ + Initialize a tabular environment. For convenience it stores the discount factor :math:`\gamma` and + exploration parameter :math:`\\varepsilon` for epsilon-greedy exploration. Access them as e.g. ``self.gamma`` + + When you implement an agent and overwrite the ``__init__``-method, you should include a call such as ``super( + ).__init__(gamma, epsilon)``. + + :param env: The gym environment + :param gamma: The discount factor :math:`\gamma` + :param epsilon: Exploration parameter :math:`\\varepsilon` for epsilon-greedy exploration + """ + super().__init__(env) + self.gamma, self.epsilon = gamma, epsilon + self.Q = TabularQ(env) + + def pi_eps(self, s, info): + """ + Performs :math:`\\varepsilon`-greedy exploration with :math:`\\varepsilon =` ``self.epsilon`` and returns the + action. Recall this means that with probability :math:`\\varepsilon` it returns a random action, and otherwise + it returns an action associated with a maximal Q-value (:math:`\\arg\\max_a Q(s,a)`). An example: + + .. runblock:: pycon + + >>> from irlc.ex09.rl_agent import TabularAgent + >>> from irlc.gridworld.gridworld_environments import BookGridEnvironment + >>> env = BookGridEnvironment() + >>> agent = TabularAgent(env) + >>> state, info = env.reset() + >>> agent.pi_eps(state, info) # Note we pass along the info-dictionary corresopnding to this state + + .. note:: + The ``info`` dictionary is used to mask (exclude) actions that are not possible in the state. + It is similar to the info dictionary in ``agent.pi(s,info)``. + + :param s: A state :math:`s_t` + :param info: The corresponding ``info``-dictionary returned by the gym environment + :return: An action computed using :math:`\\varepsilon`-greedy action selection based the Q-values stored in the ``self.Q`` class. + """ + if info is not None and 'seed' in info: # In case info contains a seed, reset the random number generator. + np.random.seed(info['seed']) + return Agent.pi(self, s, k=0, info=info) if np.random.rand() < self.epsilon else self.Q.get_optimal_action(s, info) + + +class ValueAgent(TabularAgent): + """ + This is a simple wrapper class around the Agent class above. It fixes the policy and is therefore useful for doing + value estimation. + """ + def __init__(self, env, gamma=0.95, policy=None, v_init_fun=None): + self.env = env + self.policy = policy # policy to evaluate + """ self.v holds the value estimates. + Initially v[s] = 0 unless v_init_fun is given in which case v[s] = v_init_fun(s). """ + self.v = defaultdict2(float if v_init_fun is None else v_init_fun) + super().__init__(env, gamma=gamma) + self.Q = None # Blank out the Q-values which will not be used. + + def pi(self, s, k, info=None): + return TabularAgent.pi(self, s, k, info) if self.policy is None else self.policy(s) + + def value(self, s): + return self.v[s] + +def _masked_actions(action_space, mask): + """Helper function which applies a mask to the action space.""" + from irlc.utils.common import DiscreteTextActionSpace + if isinstance(action_space, DiscreteTextActionSpace): + return [a for a in range(action_space.n) if mask[a] == 1] + else: + return [a for a in range(action_space.n) if mask[a - action_space.start] == 1] + + +class TabularQ: + """ + This is a helper class for storing Q-values. It is used by the :class:`~ircl.ex09.rl_agent.TabularAgent` to store + Q-values where it can be be accessed as ``self.Q[s,a]``. + """ + def __init__(self, env): + """ + Initialize the table. It requires a gym environment to know how many actions there are for each state. + :param env: A gym environment. + """ + self._known_masks = {} # Cache the known action masks. + + def q_default(s): + if s in self._known_masks: + return {a: 0 for a in range(self.env.action_space.n) if self._known_masks[s][a- self.env.action_space.start] == 1} + else: + return {a: 0 for a in range(self.env.action_space.n)} + + # qfun = lambda s: OrderedDict({a: 0 for a in (env.P[s] if hasattr(env, 'P') else range(env.action_space.n))}) + self.q_ = defaultdict2(lambda s: q_default(s)) + self.env = env + + def get_Qs(self, state, info_s=None): + """ + Get a list of all known Q-values for this particular state. That is, in a given state, it will return the two + lists: + + .. math:: + \\begin{bmatrix} a_1 \\\\ a_2 \\\\ \\vdots \\\\ a_k \\end{bmatrix}, \\quad + \\begin{bmatrix} Q(s,a_1) \\\\ Q(s,a_1) \\\\ \\vdots \\\\ Q(s,a_k) \\end{bmatrix} \\\\ + + the ``info_s`` parameter will ensure actions are correctly masked. An example of how to use this function from + a policy: + + .. runblock:: pycon + + >>> from irlc.ex09.rl_agent import TabularAgent + >>> class MyAgent(TabularAgent): + ... def pi(self, s, k, info=None): + ... actions, q_values = self.Q.get_Qs(s, info) + + :param state: The state to query + :param info_s: The info-dictionary returned by the environment for this state. Used for action-masking. + :return: + - actions - A tuple containing all actions available in this state ``(a_1, a_2, ..., a_k)`` + - Qs - A tuple containing all Q-values available in this state ``(Q[s,a1], Q[s, a2], ..., Q[s,ak])`` + """ + if info_s is not None and 'mask' in info_s: + if state not in self._known_masks: + self._known_masks[state] = info_s['mask'] + # Probably a good idea to check the Q-values are okay... + avail_actions = _masked_actions(self.env.action_space, info_s['mask']) + self.q_[state] = {a: self.q_[state][a] for a in avail_actions} + + (actions, Qa) = zip(*self.q_[state].items()) + return tuple(actions), tuple(Qa) + + def get_optimal_action(self, state, info_s): + """ + For a given state ``state``, this function returns the optimal action for that state. + + .. math:: + a^* = \\arg\\max_a Q(s,a) + + An example: + .. runblock:: pycon + + >>> from irlc.ex09.rl_agent import TabularAgent + >>> class MyAgent(TabularAgent): + ... def pi(self, s, k, info=None): + ... a_star = self.Q.get_optimal_action(s, info) + + + :param state: State to find the optimal action in :math:`s` + :param info_s: The ``info``-dictionary corresponding to this state + :return: The optimal action according to the Q-table :math:`a^*` + """ + actions, Qa = self.get_Qs(state, info_s) + a_ = np.argmax(np.asarray(Qa) + np.random.rand(len(Qa)) * 1e-8) + return actions[a_] + + def _chk_mask(self, s, a): + if s in self._known_masks: + mask = self._known_masks[s] + if mask[a - self.env.action_space.start] == 0: + raise Exception(f" Invalid action. You tried to access Q[{s}, {a}], however the action {a} has been previously masked and therefore cannot exist in this state. The mask for {s} is mask={mask}.") + + def __getitem__(self, state_comma_action): + s, a = state_comma_action + self._chk_mask(s, a) + return self.q_[s][a] + + def __setitem__(self, state_comma_action, q_value): + s, a = state_comma_action + self._chk_mask(s, a) + self.q_[s][a] = q_value + + def to_dict(self): + """ + This helper function converts the known Q-values to a dictionary. This function is only used for + visualization purposes in some of the examples. + + :return: A dictionary ``q`` of all known Q-values of the form ``q[s][a]`` + """ + # Convert to a regular dictionary + d = {s: {a: Q for a, Q in Qs.items() } for s,Qs in self.q_.items()} + return d diff --git a/irlc/ex09/small_gridworld.py b/irlc/ex09/small_gridworld.py new file mode 100644 index 0000000000000000000000000000000000000000..32711713130f81d7a4ce6bd4a46f2458698d8f05 --- /dev/null +++ b/irlc/ex09/small_gridworld.py @@ -0,0 +1,39 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +from irlc.ex09.mdp import MDP +import seaborn as sns + +# action space available to the agent +UP,RIGHT, DOWN, LEFT = 0, 1, 2, 3 +class SmallGridworldMDP(MDP): + def __init__(self, rows=4, cols=4): + self.rows, self.cols = rows, cols # Number of rows, columns. + super().__init__(initial_state=(rows//2, cols//2) ) # Initial state is in the middle of the board. + + def A(self, state): + return [UP, DOWN, RIGHT, LEFT] # All four directions available. + + def Psr(self, state, action): + row, col = state # state is in the format state = (row, col) + if action == UP: row -= 1 + if action == DOWN: row += 1 + if action == LEFT: col += 1 + if action == RIGHT: col -= 1 + + col = min(self.cols-1, max(col, 0)) # Check boundary conditions. + row = min(self.rows-1, max(row, 0)) + reward = -1 # Always get a reward of -1 + next_state = (row, col) + # Note that P(next_state, reward | state, action) = 1 because environment is deterministic + return {(next_state, reward): 1} + + def is_terminal(self, state): + row, col = state + return (row == 0 and col == 0) or (row == self.rows-1 and col == self.cols-1) + + +def plot_value_function(env, v): + A = np.zeros((env.rows, env.cols)) + for (row, col) in env.nonterminal_states: + A[row, col] = v[(row,col)] + sns.heatmap(A, cmap="YlGnBu", annot=True, cbar=False, square=True, fmt='g') diff --git a/irlc/ex09/value_iteration.py b/irlc/ex09/value_iteration.py new file mode 100644 index 0000000000000000000000000000000000000000..9c651b667a2ac70dd08a3f41e6332b88d55f0ebb --- /dev/null +++ b/irlc/ex09/value_iteration.py @@ -0,0 +1,73 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +import matplotlib.pyplot as plt +from collections import defaultdict +import numpy as np +from irlc.ex09.mdp_warmup import value_function2q_function +from irlc import savepdf + +def value_iteration(mdp, gamma=.99, theta=0.0001, max_iters=10 ** 6, verbose=False): + """ Implement the value-iteration algorithm defined in (SB18, Section 4.4). + The inputs should be self-explanatory given the pseudo-code. + + I have also included a max_iters variable which represents an upper bound on the total number of iterations. This is useful + if you want to check what the algorithm does after a certain (e.g. 1 or 2) steps. + + The verbose-variable makes the algorithm print out the biggest change in the value-function in a single step. + This is useful if you run it on a large problem and want to know how much time remains, or simply get an idea of + how quickly it converges. + """ + V = defaultdict(lambda: 0) # value function + for i in range(max_iters): + Delta = 0 + for s in mdp.nonterminal_states: + """ Perform the update the value-function V[s] here for the given state. + Note that this has a lot of similarity to the policy-evaluation algorithm, and you can re-use + a lot of that solution, including value_function2q_function(...) (assuming you used that function). """ + # TODO: 2 lines missing. + raise NotImplementedError("Complete the algorithm here.") + if verbose: + print(i, Delta) + if Delta < theta: + break + # Turn the value-function into a policy. It implements the last line of the algorithm. + pi = values2policy(mdp, V, gamma) + return pi, V + +def values2policy(mdp, V, gamma): + r""" Turn the value-function V into a policy. The value function V is implemented as a dictionary so that + > value = V[s] + is the value-function in state s. + The procedure you implement is the very last line of the value-iteration algorithm (SB18, Section 4.4), and it should return + a policy pi as a dictionary so that + > a = pi[s] + is the action in state s. + + Note once again you can re-use the qs_-function. and the argmax -- in fact, the solution is very similar to your solution to the + policy-iteration problem in policy_iteration.py. + As you have properly noticed, even though we implement different algorithms, they are all build using the same + building-block. + """ + pi = {} + for s in mdp.nonterminal_states: + # Create the policy here. pi[s] = a is the action to be taken in state s. + # You can use the qs_ helper function to simplify things and perhaps + # re-use ideas from the dp.py problem from week 2. + # TODO: 2 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + return pi + +if __name__ == "__main__": + import seaborn as sns + from irlc.ex09.small_gridworld import SmallGridworldMDP, plot_value_function + env = SmallGridworldMDP() + policy, v = value_iteration(env, gamma=0.99, theta=1e-6) + plot_value_function(env, v) + + plt.title("Value function obtained using value iteration to find optimal policy") + savepdf("value_iteration") + plt.show() diff --git a/irlc/ex09/value_iteration_agent.py b/irlc/ex09/value_iteration_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..063fcbefeb50bd2a10579964abbbb0ec17f9fb15 --- /dev/null +++ b/irlc/ex09/value_iteration_agent.py @@ -0,0 +1,42 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex09.value_iteration import value_iteration +from irlc import TabularAgent +import numpy as np + + +class ValueIterationAgent(TabularAgent): + def __init__(self, env, mdp=None, gamma=1, epsilon=0, **kwargs): + super().__init__(env) + self.epsilon = epsilon + # TODO: 1 lines missing. + raise NotImplementedError("Call the value_iteration function and store the policy for later.") + + def pi(self, s, k, info=None): + """ With probability (1-epsilon), the take optimal action as computed using value iteration + With probability epsilon, take a random action. You can do this using return self.random_pi(s) + """ + if np.random.rand() < self.epsilon: + return super().pi(s, k, info) # Recall that by default the policy takes random actions. + else: + """ Return the optimal action here. This should be computed using value-iteration. + To speed things up, I recommend calling value-iteration from the __init__-method and store the policy. """ + # TODO: 1 lines missing. + raise NotImplementedError("Compute and return optimal action according to value-iteration.") + return action + + def __str__(self): + return f"ValueIteration(epsilon={self.epsilon})" + + +if __name__ == "__main__": + from irlc.gridworld.gridworld_environments import SuttonCornerGridEnvironment + env = SuttonCornerGridEnvironment(living_reward=-1, render_mode='human') + from irlc import train, interactive + # Note you can access the MDP for a gridworld using env.mdp. The mdp will be an instance of the MDP class we have used for planning so far. + agent = ValueIterationAgent(env, mdp=env.mdp) # Make a ValueIteartion-based agent + # Visualize & interactivity. Press P or space to follow the policy. + agent.Q = None # This ensure the value function is visualized. + env, agent = interactive(env, agent) + train(env, agent, num_episodes=20) # Train for 100 episodes + env.savepdf("smallgrid.pdf") # Take a snapshot of the final configuration + env.close() # Whenever you use a VideoMonitor, call this to avoid a dumb openglwhatever error message on exit diff --git a/irlc/ex10/__init__.py b/irlc/ex10/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..066dc00a7ae293f26c784ccb6aa91d017ee0adea --- /dev/null +++ b/irlc/ex10/__init__.py @@ -0,0 +1,2 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""This directory contains the exercises for week 10.""" diff --git a/irlc/ex10/blackjack/__init__.py b/irlc/ex10/blackjack/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/ex10/blackjack/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/ex10/blackjack/mc_agent_blackjack.py b/irlc/ex10/blackjack/mc_agent_blackjack.py new file mode 100644 index 0000000000000000000000000000000000000000..f04c457b45db88af90bab205031dde0cab39353c --- /dev/null +++ b/irlc/ex10/blackjack/mc_agent_blackjack.py @@ -0,0 +1,48 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import gym +import numpy as np +from collections import defaultdict +import matplotlib.pyplot as plt +from irlc import main_plot +from irlc import savepdf +from irlc.ex01.agent import train +from irlc.ex10.mc_evaluate_blackjack import plot_blackjack_value, plot_blackjack_policy +from irlc.ex10.mc_agent import MCAgent + +def run_experiment(episodes, first_visit=True, **kwargs): + env_name = 'Blackjack-v1' + env = gym.make(env_name) + agent = MCAgent(env, **kwargs) + lbl = "_".join(map(str, kwargs.values())) + fvl = "First" if first_visit else "Every" + title = f"MC agent ({fvl} visit)" + + expn = f"experiments/{env_name}_MCagent_{episodes}_{first_visit}_{lbl}" # Name the experiment. Pass the label to the train function to store intermediate results. See the online documentation for more information. + # TODO: 1 lines missing. + raise NotImplementedError("call the train(...) function here.") + + # Matplotlib with seaborn is for some reason very slow. + # This code re-samples the curve to just 400 points: + main_plot(expn, smoothing_window=episodes//100, resample_ticks=400) + plt.title("Estimated returns in blackjack using " + title) + plt.ylim([-0.3, 0]) + savepdf(f"blackjack_MC_agent_{episodes}_{first_visit}") + plt.show() + + V = defaultdict(lambda: 0) + A = defaultdict(lambda: 0) + for s, av in agent.Q.to_dict().items(): + A[s] = agent.pi(s, 0) + V[s] = max(av.values() ) + + plot_blackjack_value(V, title=title, pdf_out=f"blackjack_mcagent_policy{fvl}_valfun_{episodes}") + plt.show() + plot_blackjack_policy(A, title=title) + savepdf(f"blackjack_mcagent_policy{fvl}_{episodes}") + plt.show() + +if __name__ == "__main__": + episodes = 1000000 + # episodes = 1000 # Uncomment to run far fewer episodes during debugging. + run_experiment(episodes, epsilon=0.05, first_visit=True) + run_experiment(episodes, epsilon=0.05, first_visit=False) diff --git a/irlc/ex10/blackjack/mc_evaluate_blackjack.py b/irlc/ex10/blackjack/mc_evaluate_blackjack.py new file mode 100644 index 0000000000000000000000000000000000000000..1e0cd7b9773947a6f6dd6904f9655a20a19490a8 --- /dev/null +++ b/irlc/ex10/blackjack/mc_evaluate_blackjack.py @@ -0,0 +1,93 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import matplotlib.pyplot as plt +import numpy as np + +def get_by_ace(V,ace=False): + dd = V.copy() + dd.clear() + for (p,d,ac),val in V.items(): + if ac == ace: + dd[ (p,d)] = val + return dd + +def plot_surface_2(X,Y,Z,fig=None, ax=None, **kwargs): + if fig is None and ax is None: + fig = plt.figure(figsize=(20, 10)) + if ax is None: + ax = fig.add_subplot(projection='3d') + surf = ax.plot_surface(X, Y, Z, cmap=plt.cm.coolwarm, linewidth=1, edgecolors='k', **kwargs) + ax.view_init(ax.elev, -120) + if fig is not None: + fig.colorbar(surf, shrink=0.5, aspect=5) + return ax + +def to_matrix(V): + min_x = min(k[0] for k in V.keys()) + max_x = max(k[0] for k in V.keys()) + min_y = min(k[1] for k in V.keys()) + max_y = max(k[1] for k in V.keys()) + + x_range = np.arange(min_x, max_x + 1) + y_range = np.arange(min_y, max_y + 1) + X, Y = np.meshgrid(x_range, y_range) + + Z_ace = np.zeros_like(X, dtype=float) + for j,(x, y) in enumerate( zip( X.flat, Y.flat)): + Z_ace.flat[j] = float(V[(x,y)]) + return X, Y, Z_ace + +def plot_blackjack_value(V, title="Value Function", pdf_out=None): + """ + Plots the value function as a surface plot. + """ + for lbl, ac in zip(["Usable ace", "No usable ace"], [True, False]): + w = get_by_ace(V,ace=ac) + X,Y,Z = to_matrix(w) + ax = plot_surface_2(X, Y, Z) + ax.set_zlabel("Value") + ax.set_title(title) + if pdf_out is not None: + savepdf(pdf_out+"_"+lbl.replace(" ", "_")) + +def plot_blackjack_policy(V, title): + plt.figure(figsize=(18, 12)) + for lbl, ac in zip(["Usable ace", "No usable ace"], [True, False]): + w = get_by_ace(V,ace=ac) + X, Y, Z = to_matrix(w) + plt.subplot(1,2,1+ac) + plt.imshow(Z.T) + plt.title(f"{title} ({lbl})") + plt.gca().invert_yaxis() + plt.ylabel('Player Sum') + plt.xlabel('Dealer Showing') + plt.colorbar() + +def policy20(s): + # TODO: 1 lines missing. + raise NotImplementedError("Implement the rule where we stick if we have a score of 20 or more.") + +if __name__ == "__main__": + from irlc.ex10.mc_evaluate import MCEvaluationAgent + from irlc.ex01.agent import train + import gym + from irlc import main_plot, savepdf + + nenv = "Blackjack-v1" + env = gym.make(nenv) + episodes = 50000 + gamma = 1 + experiment = f"experiments/{nenv}_first_{episodes}" + """ Instantiate the agent and call the training method here. Make sure to pass the policy=policy20 function to the MCEvaluationAgent + and set gamma=1. """ + # TODO: 2 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + main_plot(experiment, smoothing_window=episodes//100, resample_ticks=200) + plt.ylim([-0.5, 0]) + plt.title("Blackjack using first-visit MC") + savepdf("blackjack_stick20_first") + plt.show() + + pdf = "blackjack_stick20_valuefun" + plot_blackjack_value(agent.v, title="MC first-visit value function", pdf_out=pdf) + savepdf("blackjack_stick20_valuefun") + plt.show() diff --git a/irlc/ex10/blackjack/random_walk_example.py b/irlc/ex10/blackjack/random_walk_example.py new file mode 100644 index 0000000000000000000000000000000000000000..0e64027c0279c1fe0bcd0a009045ffec7750b698 --- /dev/null +++ b/irlc/ex10/blackjack/random_walk_example.py @@ -0,0 +1,112 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +import numpy as np +import matplotlib.pyplot as plt +from tqdm import tqdm +from irlc import savepdf +from irlc.ex10.td0_evaluate import TD0ValueAgent +from irlc.ex10.mc_evaluate import MCEvaluationAgent +import seaborn as sns +import pandas as pd +from irlc.ex01.agent import train +from irlc.ex09.mdp import MDP2GymEnv, MDP + +class ChainMRP(MDP): + def __init__(self, length=6): + """ + Build the "Chain MRP" yafcport from (SB18). Terminal states are [0,6], + all states are [0,1,2,3,4,5,6] and initial state is 3. (default settings). + """ + self.max_states = length + super().__init__(initial_state=length // 2) + + def is_terminal(self, state): + return state == 0 or state == self.max_states + + def A(self, s): # 0: left, 1: right. + return [0,1] + + def Psr(self, s, a): + # TODO: 1 lines missing. + raise NotImplementedError("Return the P(s', r | s,a) values here. See e.g. the gampler problem from previous week for help.") + return {(sp, 1 if sp == self.max_states else 0): 1.0} + +class ChainEnvironment(MDP2GymEnv): + def __init__(self, *args, **kwargs): + super().__init__(mdp=ChainMRP(*args, **kwargs)) + +if __name__ == "__main__": + """ plot results as in (SB18, Example 6.2) """ + env = ChainEnvironment() + V_init = np.array([0.5, 0.5, 0.5, 0.5, 0.5]) + V_true = np.array([1 / 6, 2 / 6, 3 / 6, 4 / 6, 5 / 6]) + states = range(1,6) + """ + This is a bit janky. The value-function is initialized at + 0.5 in the example, however (see (SB18)) the value function must be initialized at + 0 in terminal states. We make a function to initialize the value function + and pass it along to the ValueAgent; the ValueAgent then uses a subclassed + defaultdict which can handle a parameterized default value. """ + v_init_fun = lambda x: 0.5 + + fig, ax = plt.subplots(figsize=(15, 6), ncols=2) + """ Make TD plot """ + td_episodes = [0, 1, 10, 100] + V_current = np.copy(V_init) + xticks = ['A', 'B', 'C', 'D', 'E'] + + for i, episodes in enumerate(td_episodes): + agent = TD0ValueAgent(env, v_init_fun=v_init_fun) + train(env, agent, num_episodes=episodes,verbose=False, return_trajectory=False) + vs = [agent.value(s) for s in states] + ax[0].plot(vs, label=f"{episodes} episodes", marker='o') + + ax[0].plot(V_true, label='true values', marker='o') + ax[0].set(xlabel='State', ylabel='Estimated Value', title='Estimated Values TD(0)', + xticks=np.arange(5), xticklabels=['A','B','C','D','E']) + ax[0].legend() + + """ Make TD vs. MC plot """ + td_alphas = [0.05, 0.15, 0.1] + mc_alphas = [0.01, 0.03] + episodes = 100 + runs = 200 + + def eval_mse(agent): + errors = [] + for i in range(episodes): + V_ = [agent.value(s) for s in states] + train(env, agent, num_episodes=1, verbose=False, return_trajectory=False) + z = np.sqrt(np.sum(np.power(V_ - V_true, 2)) / 5.0) + errors.append(z) + return errors + + methods = [(TD0ValueAgent, 'TD', alpha) for alpha in td_alphas] + methods += [(MCEvaluationAgent, 'MC', alpha) for alpha in mc_alphas] + + dfs = [] + for AC,method,alpha in tqdm(methods): + TD_mse = [] + for r in range(runs): + agent = AC(env, alpha=alpha, gamma=1, v_init_fun=v_init_fun) + err_ = eval_mse(agent) + TD_mse.append( np.asarray(err_)) + + # Happy times with pandas. Let's up the production value by also plotting 1 std. + for u,mse in enumerate(TD_mse): + df = pd.DataFrame(mse, columns=['rmse']) + df.insert(len(df.columns), 'Unit', u) + df.insert(len(df.columns), 'Episodes', range(episodes)) + df.insert(len(df.columns), 'Condition', f"{method} $\\alpha$={alpha}") + dfs.append(df) + + data = pd.concat(dfs, ignore_index=True) + sns.lineplot(data=data, x='Episodes', y='rmse', hue="Condition", errorbar=('ci', 95), estimator='mean') + plt.ylabel("RMS error (averaged over states)") + plt.title("Empirical RMS error, averaged over states") + savepdf("random_walk_example") + plt.show() diff --git a/irlc/ex10/envs.py b/irlc/ex10/envs.py new file mode 100644 index 0000000000000000000000000000000000000000..bd341256a496d21dd28bcff38cc2a172e06ce9b1 --- /dev/null +++ b/irlc/ex10/envs.py @@ -0,0 +1,50 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import gymnasium as gym + +gym.envs.register( + id='Gambler-v0', + entry_point='irlc.ex09.gambler:GamblerEnv', +) + +gym.envs.register( + id='Tenv-v0', + entry_point='irlc.ex09.gambler:TEnv', + max_episode_steps=100, +) + +gym.envs.register( + id='JackRental4-v0', + entry_point='irlc.ex09.jacks_car_rental:RentalEnv', + max_episode_steps=1000, + kwargs={"max_cars": 4, + "poisson_truncation": 4, + "cache_str": "jack_rental_environment_4"}, +) + +gym.envs.register( + id='JackRental-v0', + entry_point='irlc.ex09.jacks_car_rental:RentalEnv', + max_episode_steps=1000, + kwargs={"cache_str": "jack_rental_environment"}, +) # "compress_tol": 0.01 + +gym.envs.register( + id='SmallGridworld-v0', + entry_point='irlc.gridworld.gridworld_environments:SuttonCornerGridEnvironment', + # max_episode_steps=100, # Stop trying to make it happen +) + +gym.envs.register( # Like MountainCar-v0, but time limit increased from 200 to 500. + id='MountainCar500-v0', + entry_point='gymnasium.envs.classic_control:MountainCarEnv', + max_episode_steps=500, + reward_threshold=-110.0, +) + + +if __name__ == "__main__": + print("Testing...") + mc = gym.make('MountainCar500-v0') + # j4 = gym.make("JackRental4-v0") + # jack = gym.make("JackRental-v0") + sg = gym.make("SmallGridworld-v0") diff --git a/irlc/ex10/mc_agent.py b/irlc/ex10/mc_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..0719f15ff7518194491fcf26095f27442deacd4c --- /dev/null +++ b/irlc/ex10/mc_agent.py @@ -0,0 +1,86 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from collections import defaultdict +import matplotlib.pyplot as plt +from irlc.ex09.rl_agent import TabularAgent +from irlc import main_plot, savepdf, train +from irlc import interactive +def get_MC_return_SA(episode, gamma, first_visit=True): + """ Helper method for computing the MC returns. + Given an episodes in the form [ (s0,a0,r1), (s1,a1,r2), ...] + this function computes (if first_visit=True) a new list + + > [((s,a), G) , ... ] + + consisting of the unique $(s_t,a_t)$ pairs in episode along with their return G_t (computed from their first occurance). + Alternatively, if first_visit=False, the method return a list of same length of episode + with all (s,a) pairs and their return. + """ + sa = [(s, a) for s, a, r in episode] # Get all state/action pairs. Useful for checking if we have visited a state/action before. + G = 0 + returns = [] + for t in reversed(range(len(episode))): + # TODO: 2 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + if sa_t not in sa[:t] or not first_visit: + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + return returns + +class MCAgent(TabularAgent): + def __init__(self, env, gamma=1.0, epsilon=0.05, alpha=None, first_visit=True): + if alpha is None: + self.returns_sum = defaultdict(float) + self.returns_count = defaultdict(float) + self.alpha = alpha + self.first_visit = first_visit + self.episode = [] + super().__init__(env, gamma, epsilon) + + def pi(self, s,k, info=None): + """ + Compute the policy of the MC agent. Remember the agent is epsilon-greedy. You can use the pi_eps(s,info)-function defined + in the TabularAgent class. + """ + # TODO: 1 lines missing. + raise NotImplementedError("Compute action here using the Q-values. (remember to be epsilon-greedy)") + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + """ + Consult your implementation of value estimation agent for ideas. Note you can index the Q-values as + + >> self.Q[s, a] = new_q_value + + see comments in the Agent class for more details, however for now you can consider them as simply a nested + structure where ``self.Q[s, a]`` defaults to 0 unless the Q-value has been updated. + """ + # TODO: 12 lines missing. + raise NotImplementedError("Train the agent here.") + + def __str__(self): + return f"MC_{self.gamma}_{self.epsilon}_{self.alpha}_{self.first_visit}" + +if __name__ == "__main__": + """ Load environment but make sure it is time-limited. Can you tell why? """ + envn = "SmallGridworld-v0" + + from irlc.gridworld.gridworld_environments import SuttonCornerGridEnvironment, BookGridEnvironment + env = SuttonCornerGridEnvironment(uniform_initial_state=True) + # env = BookGridEnvironment(living_reward=-0.05) # Uncomment to test an alternative environment with a negative living reward. + + gamma = 1 + episodes = 20000 + experiment="experiments/mcagent_smallgrid" + agent = MCAgent(env, gamma=gamma, first_visit=True) + train(env, agent, experiment_name=experiment, num_episodes=episodes, return_trajectory=False) + main_plot(experiments=[experiment], resample_ticks=200) + plt.title("Smallgrid MC agent value function") + plt.ylim([-10, 0]) + savepdf("mcagent_smallgrid") + plt.show() + + env, agent = interactive(env, agent) + env.reset() + env.plot() + plt.title(f"MC on-policy control of {envn} using first-visit") + savepdf("MC_agent_value_smallgrid") + plt.show(block=False) diff --git a/irlc/ex10/mc_evaluate.py b/irlc/ex10/mc_evaluate.py new file mode 100644 index 0000000000000000000000000000000000000000..973d4b1040e499dc85f1cf72add0617e768ae2d7 --- /dev/null +++ b/irlc/ex10/mc_evaluate.py @@ -0,0 +1,120 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc import savepdf +import matplotlib.pyplot as plt +from irlc.ex09.rl_agent import ValueAgent +from collections import defaultdict +from irlc.ex01.agent import train +import numpy as np +import matplotlib +#matplotlib.use('qtagg') # Fix crash on linux with default backend. + +def get_MC_return_S(episode, gamma, first_visit=True): + """ Helper method for computing the MC returns. + Given an episodes in the form ``[ (s0,a0,r1), (s1,a1,r2), ...]`` + this function computes (if first_visit=True) a new list:: + + [(s0, G0), (s1, G1), ...] + + consisting of the unique s_t values in the episode along with their return G_t (computed from their first occurance). + + Alternatively, if first_visit=False, the method return a list of same length of episode + with all s values and their return. + """ + ss = [s for s, a, r in episode] + G = 0 + returns = [] + for t in reversed(range(len(episode))): + # TODO: 2 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + if s_t not in ss[:t] or not first_visit: + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + return returns +class MCEvaluationAgent(ValueAgent): + def __init__(self, env, policy=None, gamma=1, alpha=None, first_visit=True, v_init_fun=None): + self.episode = [] + self.first_visit = first_visit + self.alpha = alpha + if self.alpha is None: + self.returns_sum_S = defaultdict(float) + self.returns_count_N = defaultdict(float) + super().__init__(env, gamma, policy, v_init_fun=v_init_fun) + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + self.episode.append((s, a, r)) # Gather the episode + if done: # Only train when the episode has stopped + returns = get_MC_return_S(self.episode, self.gamma, self.first_visit) + for s, G in returns: + if self.alpha: + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + else: + # TODO: 3 lines missing. + raise NotImplementedError("Implement function body") + + self.episode = [] + + def __str__(self): + return f"MCeval_{self.gamma}_{self.alpha}_{self.first_visit}" + + +if __name__ == "__main__": + envn = "SmallGridworld-v0" + from irlc import interactive + from irlc.gridworld.gridworld_environments import SuttonCornerGridEnvironment + env = SuttonCornerGridEnvironment(render_mode=None) + gamma = 1 + episodes = 200 + agent = MCEvaluationAgent(env, gamma=gamma) + train(env, agent, num_episodes=episodes) + env.render_mode = 'human' + env, agent = interactive(env, agent, autoplay=True) + env.plot() + plt.title(f"MC evaluation of {envn} using first-visit") + savepdf("MC_value_random_smallgrid") + plt.show(block=False) + env.close() + + env = SuttonCornerGridEnvironment(render_mode=None) + agent_every = MCEvaluationAgent(env, gamma=gamma, first_visit=False) + train(env, agent_every, num_episodes=episodes) + env.render_mode = 'human' + env, agent = interactive(env, agent, autoplay=True) + env.plot() + plt.title(f"MC evaluation of {envn} using every-visit") + savepdf("MC_value_random_smallgrid_every") + plt.show(block=False) + env.close() + s0 = (1, 1) + print(f"Estimated value functions v_pi(s0) for first visit {agent.v[(1,1)]:3}") + print(f"Estimated value functions v_pi(s0) for every visit {agent_every.v[(1,1)]:3}") + + ## Second part: + repeats = 5000 # increase to e.g. 20'000. + episodes = 1 + ev, fv = [], [] + env = SuttonCornerGridEnvironment() + print(f"Repeating experiment {repeats} times, this may take a while.") + for _ in range(repeats): + """ + Instantiate two agents with first_visit=True and first_visit=False. + Train the agents using the train function for episodes episodes. You might want to pass verbose=False to the + 'train'-method to suppress output. + When done, compute the mean of agent.values() and add it to the lists ev / fv; the mean of these lists + are the desired result. + """ + agent = MCEvaluationAgent(env, gamma=gamma) + # TODO: 1 lines missing. + raise NotImplementedError("Create and train an every-visit agent.") + + train(env, agent, num_episodes=episodes, verbose=False) + # TODO: 1 lines missing. + raise NotImplementedError("Create and train an every-visit agent.") + + ev.append(agent.v[(1,1)]) + fv.append(agent_every.v[(1,1)]) + + print(f"First visit: Mean of value functions E[v_pi(s0)] after {repeats} repeats {np.mean(fv):3}") + print(f"Every visit: Mean of value functions E[v_pi(s0)] after {repeats} repeats {np.mean(ev):3}") + env.close() + plt.close() diff --git a/irlc/ex10/question_td0.py b/irlc/ex10/question_td0.py new file mode 100644 index 0000000000000000000000000000000000000000..3f31e5b4ab770e85db1c8752af0c1eff3e2293d3 --- /dev/null +++ b/irlc/ex10/question_td0.py @@ -0,0 +1,36 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +def a_compute_deltas(v: dict, states: list, rewards: list, gamma: float) -> list: + # TODO: Code has been removed from here. + raise NotImplementedError("Insert your solution and remove this error.") + return deltas + + +def b_perform_td0(v: dict, states: list, rewards: list, gamma: float, alpha: float) -> dict: + # TODO: Code has been removed from here. + raise NotImplementedError("Insert your solution and remove this error.") + return v + + +def c_perform_td0_batched(v: dict, states: list, rewards: list, gamma: float, alpha: float) -> dict: + # TODO: Code has been removed from here. + raise NotImplementedError("Insert your solution and remove this error.") + return v + + +if __name__ == "__main__": + states = [1, 0, 2, -1, 2, 4, 5, 4, 3, 2, 1, -1] + rewards = [1, 0.5, -1, 0, 1, 2, 2, 0, 0, -1, 0.5] + # In the notation of the problem: T = len(rewards). + v = {s: 0 for s in states} # Initialize the value function v. + gamma = 0.9 + alpha = 0.2 + + deltas = a_compute_deltas(v, states, rewards, gamma) + print(f"The first value of delta should be 1, your value is {deltas[0]=}") + + v = b_perform_td0(v, states, rewards, gamma, alpha) + print(f"The value function v(s=1) should be 0.25352, your value is {v[1]=}") + + v_batched = {s: 0 for s in states} # Initialize the value function anew + v_batched = c_perform_td0_batched(v_batched, states, rewards, gamma, alpha) + print(f"The batched value function in v(s=1) should be 0.3, your value is {v_batched[1]=}") diff --git a/irlc/ex10/td0_evaluate.py b/irlc/ex10/td0_evaluate.py new file mode 100644 index 0000000000000000000000000000000000000000..98aa5fc2547852c39135c62e642d917baa5c8a3d --- /dev/null +++ b/irlc/ex10/td0_evaluate.py @@ -0,0 +1,43 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +import matplotlib.pyplot as plt +from irlc.ex09.rl_agent import ValueAgent +from irlc import savepdf +from irlc.ex01.agent import train + +class TD0ValueAgent(ValueAgent): + def __init__(self, env, policy=None, gamma=0.99, alpha=0.05, v_init_fun=None): + self.alpha = alpha + super().__init__(env, gamma=gamma, policy=policy, v_init_fun=v_init_fun) + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + # TODO: 3 lines missing. + raise NotImplementedError("Implement function body") + + def __str__(self): + return f"TD0Value_{self.gamma}_{self.alpha}" + +def value_function_test(env, agent, v_true, episodes=200): + err = [] + for t in range(episodes): + train(env, agent, num_episodes=1, verbose=False) + err.append( np.mean( [(v_true - v0) ** 2 for k, v0 in agent.v.items()] ) ) + return np.asarray(err) + +if __name__ == "__main__": + envn = "SmallGridworld-v0" + + from irlc.gridworld.gridworld_environments import SuttonCornerGridEnvironment + from irlc import interactive + env = SuttonCornerGridEnvironment() # Make the gridworld environment itself + + gamma = 1 + agent = TD0ValueAgent(env, gamma=gamma, alpha=0.05) # Make a TD(0) agent + train(env, agent, num_episodes=2000, return_trajectory=False) # Train for 2000 episodes + env = SuttonCornerGridEnvironment(render_mode='human') # Re-make the gridworld to get rendering. + env, agent = interactive(env, agent) # Add a video monitor, the environment will now show an animation + train(env,agent,num_episodes=1) # Train for a (single) new episode + env.plot() # Plot the current state of the environment/agent + plt.title(f"TD0 evaluation of {envn}") + savepdf("TD_value_random_smallgrid") + plt.show(block=False) diff --git a/irlc/ex11/__init__.py b/irlc/ex11/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6fc08338fb059441ae58e5efe88d2db3a4052153 --- /dev/null +++ b/irlc/ex11/__init__.py @@ -0,0 +1,2 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""This directory contains the exercises for week 11.""" diff --git a/irlc/ex11/__pycache__/__init__.cpython-311.pyc b/irlc/ex11/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..af11ce3f70498da4527edc9a5bc56e55d7a5bcca Binary files /dev/null and b/irlc/ex11/__pycache__/__init__.cpython-311.pyc differ diff --git a/irlc/ex11/__pycache__/feature_encoder.cpython-311.pyc b/irlc/ex11/__pycache__/feature_encoder.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1d8b22f7b9271cf3563787f709c9e3f17b64afa7 Binary files /dev/null and b/irlc/ex11/__pycache__/feature_encoder.cpython-311.pyc differ diff --git a/irlc/ex11/__pycache__/q_agent.cpython-311.pyc b/irlc/ex11/__pycache__/q_agent.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4e5fe50d5559fca176eeb75f5ace797e16509486 Binary files /dev/null and b/irlc/ex11/__pycache__/q_agent.cpython-311.pyc differ diff --git a/irlc/ex11/feature_encoder.py b/irlc/ex11/feature_encoder.py new file mode 100644 index 0000000000000000000000000000000000000000..79f6bb01e4505115910ad6b4069f5d233f0dacdb --- /dev/null +++ b/irlc/ex11/feature_encoder.py @@ -0,0 +1,402 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +from math import floor +from gymnasium.spaces.box import Box +import numpy as np +from irlc.ex09.rl_agent import _masked_actions +from irlc.utils.common import defaultdict2 + +class FeatureEncoder: + """ + The idea behind linear function approximation of :math:`Q`-values is that + + - We initialize (and eventually learn) a :math:`d`-dimensional weight vector :math:`w \in \mathbb{R}^d` + - We assume there exists a function to compute a :math:`d`-dimensional feature vector :math:`x(s,a) \in \mathbb{R}^d` + - The :math:`Q`-values are then represented as + + .. math:: + Q(s,a) = x(s,a)^\\top w + + Learning is therefore entirely about updating :math:`w`. + + The following example shows how you initialize the linear :math:`Q`-values and compute them in a given state: + + .. runblock:: pycon + + >>> import gymnasium as gym + >>> from irlc.ex11.feature_encoder import LinearQEncoder + >>> env = gym.make('MountainCar-v0') + >>> Q = LinearQEncoder(env, tilings=8) + >>> s, _ = env.reset() + >>> a = env.action_space.sample() + >>> Q(s,a) # Compute a Q-value. + >>> Q.d # Get the number of dimensions + >>> Q.x(s,a)[:4] # Get the first four coordinates of the x-vector + >>> Q.w[:4] # Get the first four coordinates of the w-vector + + """ + def __init__(self, env): + """ + Initialize the feature encoder. It requires an environment to know the number of actions and dimension of the state space. + + :param env: An openai Gym ``Env``. + """ + self.env = env + self.w = np.zeros((self.d, )) + self._known_masks = {} + + def q_default(s): + from irlc.utils.common import DiscreteTextActionSpace + if s in self._known_masks: + return {a: 0 for a in range(self.env.action_space.n) if + self._known_masks[s][(a - self.env.action_space.start) if not isinstance(self.env.action_space, DiscreteTextActionSpace) else a] == 1} + else: + return {a: 0 for a in range(self.env.action_space.n)} + + # qfun = lambda s: OrderedDict({a: 0 for a in (env.P[s] if hasattr(env, 'P') else range(env.action_space.n))}) + + self.q_ = defaultdict2(lambda s: q_default(s)) + + @property + def d(self): + """ Get the number of dimensions of :math:`w` + + .. runblock:: pycon + + >>> import gymnasium as gym + >>> from irlc.ex11.feature_encoder import LinearQEncoder + >>> env = gym.make('MountainCar-v0') + >>> Q = LinearQEncoder(env, tilings=8) # as in (SB18) + >>> Q.d + """ + raise NotImplementedError() + + def x(self, s, a): + """ + Computes the :math:`d`-dimensional feature vector :math:`x(s,a)` + + .. runblock:: pycon + + >>> import gymnasium as gym + >>> from irlc.ex11.feature_encoder import LinearQEncoder + >>> env = gym.make('MountainCar-v0') + >>> Q = LinearQEncoder(env, tilings=8) # as in (SB18) + >>> s, info = env.reset() + >>> x = Q.x(s, env.action_space.sample()) + + :param s: A state :math:`s` + :param a: An action :math:`a` + :return: Feature vector :math:`x(s,a)` + """ + raise NotImplementedError() + + def get_Qs(self, state, info_s=None): + """ + This is a helper function, it is only for internal use. + + :param state: + :param info_s: + :return: + """ + if info_s is not None and 'mask' in info_s and not isinstance(state, np.ndarray): + if state not in self._known_masks: + self._known_masks[state] = info_s['mask'] + # Probably a good idea to check the Q-values are okay... + avail_actions = _masked_actions(self.env.action_space, info_s['mask']) + self.q_[state] = {a: self.q_[state][a] for a in avail_actions} + # raise Exception() + # from irlc.utils.common import ExplicitActionSpace + # + # zip(*self.q_[state].items()) + from irlc.pacman.pacman_environment import PacmanEnvironment + from irlc.pacman.pacman_utils import Actions + if isinstance(state, np.ndarray): + actions = tuple(range(self.env.action_space.n)) + elif isinstance(self.env, PacmanEnvironment): + # actions = Actions + # actions = tuple(Actions._directions.keys()) + actions = _masked_actions(self.env.action_space, info_s['mask']) + actions = tuple([self.env.action_space.actions[n] for n in actions]) + else: + actions = tuple(self.q_[state].keys()) + + # if isinstance(self.env, PacmanEnvironment): + # # TODO: Make smarter masking. + # actions = [a for a in actions if a in self.env.A(state)] + # actions = + Qs = tuple([self(state,a) for a in actions]) + # TODO: Implement masking and masking-cache. + return actions, Qs + # + # actions = list( self.env.P[state].keys() if hasattr(self.env, 'P') else range(self.env.action_space.n) ) + # Qs = [self(state, a) for a in actions] + # return tuple(actions), tuple(Qs) + + def get_optimal_action(self, state, info=None): + """ + For a given state ``state``, this function returns the optimal action for that state. + + .. math:: + a^* = \\arg\\max_a Q(s,a) + + An example: + + .. runblock:: pycon + + >>> from irlc.ex09.rl_agent import TabularAgent + >>> class MyAgent(TabularAgent): + ... def pi(self, s, k, info=None): + ... a_star = self.Q.get_optimal_action(s, info) + + :param state: State to find the optimal action in :math:`s` + :param info: The ``info``-dictionary corresponding to this state + :return: The optimal action according to the Q-values :math:`a^*` + """ + actions, Qa = self.get_Qs(state, info) + if len(actions) == 0: + print("Bad actions list") + a_ = np.argmax(np.asarray(Qa) + np.random.rand(len(Qa)) * 1e-8) + return actions[a_] + + def __call__(self, s, a): + """ + Evaluate the Q-values for the given state and action. An example: + + .. runblock:: pycon + + >>> import gymnasium as gym + >>> from irlc.ex11.feature_encoder import LinearQEncoder + >>> env = gym.make('MountainCar-v0') + >>> Q = LinearQEncoder(env, tilings=8) # as in (SB18) + >>> s, info = env.reset() + >>> Q(s, env.action_space.sample()). # Compute Q(s,a) + + :param s: A state :math:`s` + :param a: An action :math:`a` + :return: Feature vector :math:`x(s,a)` + """ + return self.x(s, a) @ self.w + + def __getitem__(self, item): + raise Exception("Hi! You tried to access linear Q-values as Q[s,a]. You need to use Q(s,a). This choice signifies they are not represented as a table, but as a linear combination x(s,a)^T w") + # s,a = item + # return self.__call__(s, a) + + def __setitem__(self, key, value): + raise Exception("Oy! You tried to set a linearly encoded Q-value as in Q[s, a] = new_q_value.\n This is not possible since they are represented as x(s,a)^T w. Rewrite the expression to update Q.w.") + +class DirectEncoder(FeatureEncoder): + def __init__(self, env): + self.d_ = np.prod( env.observation_space.shape ) * env.action_space.n + # self.d_ = len(self.x(env.reset(), env.action_space.n)) + super().__init__(env) + + def x(self, s, a): + xx = np.zeros( (self.d,)) + n = s.size + xx[n * a:n*(a+1) ] = s + return xx + + ospace = self.env.observation_space.shape + simple = False + if not isinstance(ospace, tuple): + ospace = (ospace,) + simple = True + + sz = [] + for j, disc in enumerate(ospace): + sz.append(disc.n) + + total_size = sum(sz) + csum = np.cumsum(sz, ) - sz[0] + self.max_size = total_size * self.env.action_space.n + + + def fixed_sparse_representation(s, action): + if simple: + s = (s,) + s_encoded = [cs + ds + total_size * action for ds, cs in zip(s, csum)] + return s_encoded + + self.get_active_tiles = fixed_sparse_representation + + # super().__init__(env) + + @property + def d(self): + return self.d_ + return 10000*8 + x = np.zeros(self.d) + at = self.get_active_tiles(s, a) + x[at] = 1.0 + return x + + +class GridworldXYEncoder(FeatureEncoder): + def __init__(self, env): + self.env = env + self.na = self.env.action_space.n + self.ns = 2 + super().__init__(env) + + @property + def d(self): + return self.na*self.ns + + def x(self, s, a): + x,y = s + xx = [np.zeros(self.ns) for _ in range(self.na)] + xx[a][0] = x + xx[a][1] = y + # return xx[a] + xx = np.concatenate(xx) + return xx + +class SimplePacmanExtractor(FeatureEncoder): + def __init__(self, env): + self.env = env + from irlc.pacman.feature_extractor import SimpleExtractor + # from reinforcement.featureExtractors import SimpleExtractor + self._extractor = SimpleExtractor() + self.fields = ["bias", "#-of-ghosts-1-step-away", "#-of-ghosts-1-step-away", "eats-food", "closest-food"] + super().__init__(env) + + def x(self, s, a): + xx = np.zeros_like(self.w) + # ap = self.env._actions_gym2pac[a] + ap = a + for k, v in self._extractor.getFeatures(s, ap).items(): + xx[self.fields.index(k)] = v + return xx + + @property + def d(self): + return len(self.fields) + +class LinearQEncoder(FeatureEncoder): + def __init__(self, env, tilings=8, max_size=2048): + """ + Implements the tile-encoder described by (SB18) + + :param env: The openai Gym environment we wish to solve. + :param tilings: Number of tilings (translations). Typically 8. + :param max_size: Maximum number of dimensions. + """ + if isinstance(env.observation_space, Box): + os = env.observation_space + low = os.low + high = os.high + scale = tilings / (high - low) + hash_table = IHT(max_size) + self.max_size = max_size + def tile_representation(s, action): + s_ = list( (s*scale).flat ) + active_tiles = tiles(hash_table, tilings, s_, [action]) # (s * scale).tolist() + # if 0 not in active_tiles: + # active_tiles.append(0) + return active_tiles + self.get_active_tiles = tile_representation + else: + # raise Exception("Implement in new class") + # + # Use Fixed Sparse Representation. See: + # https://castlelab.princeton.edu/html/ORF544/Readings/Geramifard%20-%20Tutorial%20on%20linear%20function%20approximations%20for%20dynamic%20programming%20and%20RL.pdf + + ospace = env.observation_space + simple = False + if not isinstance(ospace, tuple): + ospace = (ospace,) + simple = True + + sz = [] + for j,disc in enumerate(ospace): + sz.append( disc.n ) + + total_size = sum(sz) + csum = np.cumsum(sz,) - sz[0] + self.max_size = total_size * env.action_space.n + + def fixed_sparse_representation(s, action): + if simple: + s = (s,) + s_encoded = [cs + ds + total_size * action for ds,cs in zip(s, csum)] + return s_encoded + self.get_active_tiles = fixed_sparse_representation + super().__init__(env) + + def x(self, s, a): + x = np.zeros(self.d) + at = self.get_active_tiles(s, a) + x[at] = 1.0 + return x + + @property + def d(self): + return self.max_size + + +""" +Following code contains the tile-coding utilities copied from: +http://incompleteideas.net/tiles/tiles3.py-remove +""" +class IHT: + """Structure to handle collisions""" + + def __init__(self, size_val): + self.size = size_val + self.overfull_count = 0 + self.dictionary = {} + + + def count(self): + return len(self.dictionary) + + def full(self): + return len(self.dictionary) >= self.size + + def get_index(self, obj, read_only=False): + d = self.dictionary + if obj in d: + return d[obj] + elif read_only: + return None + size = self.size + count = self.count() + if count >= size: + if self.overfull_count == 0: + print('IHT full, starting to allow collisions') + self.overfull_count += 1 + return hash(obj) % self.size + else: + d[obj] = count + return count + + + + +def hash_coords(coordinates, m, read_only=False): + if isinstance(m, IHT): return m.get_index(tuple(coordinates), read_only) + if isinstance(m, int): return hash(tuple(coordinates)) % m + if m is None: return coordinates + + +def tiles(iht_or_size, num_tilings, floats, ints=None, read_only=False): + """returns num-tilings tile indices corresponding to the floats and ints""" + if ints is None: + ints = [] + qfloats = [floor(f * num_tilings) for f in floats] + tiles = [] + for tiling in range(num_tilings): + tilingX2 = tiling * 2 + coords = [tiling] + b = tiling + for q in qfloats: + coords.append((q + b) // num_tilings) + b += tilingX2 + coords.extend(ints) + tiles.append(hash_coords(coords, iht_or_size, read_only)) + return tiles diff --git a/irlc/ex11/nstep_sarsa_agent.py b/irlc/ex11/nstep_sarsa_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..b1648dc302576a4cd6d45e68e03486dd0bb12f00 --- /dev/null +++ b/irlc/ex11/nstep_sarsa_agent.py @@ -0,0 +1,84 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +from irlc.ex01.agent import train +import gymnasium as gym +from irlc import main_plot +import matplotlib.pyplot as plt +from irlc.ex11.q_agent import QAgent + +class SarsaNAgent(QAgent): + """ Implement the N-step semi-gradient sarsa agent from (SB18, Section 7.2)""" + def __init__(self, env, gamma=1, alpha=0.2, epsilon=0.1, n=1): + # Variables for TD-n + self.n = n # as in n-step sarse + # Buffer lists for previous (S_t, R_{t}, A_t) triplets + self.R, self.S, self.A = [None] * (self.n + 1), [None] * (self.n + 1), [None] * (self.n + 1) + super().__init__(env, gamma=gamma, alpha=alpha, epsilon=epsilon) + + def pi(self, s, k, info=None): + self.t = k # Save current step in episode for use in train. + if self.t == 0: # First action is epsilon-greedy. + self.A[self.t] = self.pi_eps(s, info) + return self.A[self.t % (self.n+1)] + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + # Recall we are given S_t, A_t, R_{t+1}, S_{t+1} and done is whether t=T+1. + n = self.n # n as in n-step sarsa. + t = self.t # Current time step t as in s_t. + if t == 0: # We are in the initial state. Reset buffer. + self.S[0], self.A[0] = s, a + # Store current observations in buffer. + self.S[(t+1)%(n+1)] = sp + self.R[(t+1)%(n+1)] = r + self.A[(t+1)%(n+1)] = self.pi_eps(sp, info_sp) if not done else -1 + + if done: + T = t+1 + tau_steps_to_train = range(t - n + 1, T) + else: + T = 1e10 + tau_steps_to_train = [t - n + 1] + # Tau represent the current tau-steps which are to be updated. The notation is compatible with that in Sutton. + for tau in tau_steps_to_train: + if tau >= 0: + """ + Compute the return for this tau-step and perform the relevant Q-update. + The first step is to compute the expected return G in the below section. + """ + # TODO: 4 lines missing. + raise NotImplementedError("Compute G= (expected return) here.") + + S_tau, A_tau = self.S[tau%(n+1)], self.A[tau%(n+1)] + delta = (G - self._q(S_tau, A_tau)) + if n == 1: # Check your implementation is correct when n=1 by comparing it with regular Sarsa learning. + delta_Sarsa = (r + (0 if done else self.gamma * self._q(sp,A_tau_n)) - self._q(S_tau,A_tau)) + if abs(delta-delta_Sarsa) > 1e-10: + raise Exception("n=1 agreement with Sarsa learning failed. You have at least one bug!") + self._upd_q(S_tau, A_tau, delta) + + def _q(self, s, a): return self.Q[s,a] # Using these helper methods will come in handy when we work with function approximators, but it is optional. + def _upd_q(self, s, a, delta): self.Q[s,a] += self.alpha * delta + + def __str__(self): + return f"SarsaN_{self.gamma}_{self.epsilon}_{self.alpha}_{self.n}" + + +if __name__ == "__main__": + envn = 'CliffWalking-v0' + env = gym.make(envn) + from irlc.ex11.sarsa_agent import sarsa_exp + from irlc.ex11.q_agent import q_exp + + agent = SarsaNAgent(env, n=5, epsilon=0.1,alpha=0.5) + exp = f"experiments/{envn}_{agent}" + for _ in range(10): # Train 10 times to get an idea about the average performance. + train(env, agent, exp, num_episodes=200, max_runs=10) + main_plot([q_exp, sarsa_exp, exp], smoothing_window=10) # plot with results from Q/Sarsa simulations. + plt.ylim([-100,0]) + from irlc import savepdf + savepdf("n_step_sarsa_cliff") + plt.show() diff --git a/irlc/ex11/q_agent.py b/irlc/ex11/q_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..906e873472810f97593e76f5e0c9343abc6389c7 --- /dev/null +++ b/irlc/ex11/q_agent.py @@ -0,0 +1,85 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +from irlc.ex09.mdp import GymEnv2MDP +from irlc.ex09.rl_agent import TabularAgent +from irlc import train +import gymnasium as gym +from irlc import main_plot +import matplotlib.pyplot as plt +from irlc import savepdf +from irlc.ex09.value_iteration_agent import ValueIterationAgent + +class QAgent(TabularAgent): + r""" + Implement the Q-learning agent (SB18, Section 6.5) + Note that the Q-datastructure already exist, as do helper functions useful to compute an epsilon-greedy policy. + You can access these as + + > self.Q[s,a] = 31 # Set a Q-value. + + See the TabularAgent class for more information. + """ + def __init__(self, env, gamma=1.0, alpha=0.5, epsilon=0.1): + self.alpha = alpha + super().__init__(env, gamma, epsilon) + + def pi(self, s, k, info=None): + """ + Return current action using epsilon-greedy exploration. You should look at the TabularAgent class for ideas. + """ + # TODO: 1 lines missing. + raise NotImplementedError("Implement the epsilon-greedy policy here.") + return action + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + """ + Implement the Q-learning update rule, i.e. compute a* from the Q-values. + As a hint, note that self.Q[sp,a] corresponds to q(s_{t+1}, a) and + that what you need to update is self.Q[s, a] = ... + + You may want to look at self.Q.get_optimal_action(state) to compute a = argmax_a Q[s,a]. + """ + # TODO: 3 lines missing. + raise NotImplementedError("Update the Q[s,a]-values here.") + + def __str__(self): + return f"QLearner_{self.gamma}_{self.epsilon}_{self.alpha}" + +q_exp = f"experiments/cliffwalk_Q" +epsilon = 0.1 +max_runs = 10 +alpha = 0.5 +def cliffwalk(): + env = gym.make('CliffWalking-v0') + agent = QAgent(env, epsilon=epsilon, alpha=alpha) + train(env, agent, q_exp, num_episodes=200, max_runs=max_runs) + + # As a baseline, we set up/evaluate a value-iteration agent to get an idea about the optimal performance. + # To do so, we need an MDP object. We create an MDP object out of the gym environment below. + # You can look at the code if you like, but it is simply a helper function to convert from one datastructure to another, + # and all it does is to give a MDP object which is needed for our value-iteration implementation from the previous + # week. + mdp = GymEnv2MDP(env) + vi_exp = "experiments/cliffwalk_VI" + Vagent = ValueIterationAgent(env, mdp=mdp, epsilon=epsilon) + train(env, Vagent, vi_exp, num_episodes=200, max_runs=max_runs) + + vi_exp_opt = "experiments/cliffwalk_VI_optimal" + Vagent_opt = ValueIterationAgent(env, mdp=mdp, epsilon=0) # Same, but with epsilon=0 + train(env, Vagent_opt, vi_exp_opt, num_episodes=200, max_runs=max_runs) + + exp_names = [q_exp, vi_exp, vi_exp_opt] + return env, exp_names + +if __name__ == "__main__": + for _ in range(10): + env, exp_names = cliffwalk() + main_plot(exp_names, smoothing_window=10) + plt.ylim([-100, 0]) + plt.title("Q-learning on " + env.spec.name) + savepdf("Q_learning_cliff") + plt.show() diff --git a/irlc/ex11/sarsa_agent.py b/irlc/ex11/sarsa_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..29aa196dfc4ca45ec80ac26bbbf4e6718272142a --- /dev/null +++ b/irlc/ex11/sarsa_agent.py @@ -0,0 +1,52 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +import matplotlib.pyplot as plt +from irlc.ex11.q_agent import QAgent +from irlc import main_plot, savepdf +from irlc.ex01.agent import train +from irlc.ex11.q_agent import cliffwalk, alpha, epsilon + +class SarsaAgent(QAgent): + r""" Implement the Sarsa control method from (SB18, Section 6.4). It is recommended you complete + the Q-agent first because the two methods are very similar and the Q-agent is easier to implement. """ + def __init__(self, env, gamma=1, alpha=0.5, epsilon=0.1): + super().__init__(env, gamma=gamma, alpha=alpha, epsilon=epsilon) + + def pi(self, s, k, info=None): + if k == 0: + """ we are at the beginning of the episode. Generate a by being epsilon-greedy""" + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + else: + """ Return the action self.a you generated during the train where you know s_{t+1} """ + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + """ + generate A' as self.a by being epsilon-greedy. Re-use code from the Agent class. + """ + # TODO: 1 lines missing. + raise NotImplementedError("self.a = ....") + """ now that you know A' = self.a, perform the update to self.Q[s,a] here """ + # TODO: 2 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + + def __str__(self): + return f"Sarsa{self.gamma}_{self.epsilon}_{self.alpha}" + +sarsa_exp = f"experiments/cliffwalk_Sarsa" +if __name__ == "__main__": + env, q_experiments = cliffwalk() # get results from Q-learning + agent = SarsaAgent(env, epsilon=epsilon, alpha=alpha) + for _ in range(10): + train(env, agent, sarsa_exp, num_episodes=200, max_runs=10) + main_plot(q_experiments + [sarsa_exp], smoothing_window=10) + plt.ylim([-100, 0]) + plt.title("Q and Sarsa learning on " + env.spec.name) + savepdf("QSarsa_learning_cliff") + plt.show() diff --git a/irlc/ex11/semi_grad_q.py b/irlc/ex11/semi_grad_q.py new file mode 100644 index 0000000000000000000000000000000000000000..0910717159ff25dd4e6b0a5c5b22423c2840ef3d --- /dev/null +++ b/irlc/ex11/semi_grad_q.py @@ -0,0 +1,45 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import gymnasium as gym +from irlc.ex01.agent import train +from irlc import main_plot +import matplotlib.pyplot as plt +from irlc.ex11.q_agent import QAgent +from irlc.ex11.feature_encoder import LinearQEncoder +from irlc import savepdf + +class LinearSemiGradQAgent(QAgent): + def __init__(self, env, gamma=1.0, alpha=0.5, epsilon=0.1, q_encoder=None): + """ The Q-values, as implemented using a function approximator, can now be accessed as follows: + + >> self.Q(s,a) # Compute q-value + >> self.Q.x(s,a) # Compute gradient of the above expression wrt. w + >> self.Q.w # get weight-vector. + + I would recommend inserting a breakpoint and investigating the above expressions yourself; + you can of course al check the class LinearQEncoder if you want to see how it is done in practice. + """ + super().__init__(env, gamma, epsilon=epsilon, alpha=alpha) + self.Q = LinearQEncoder(env, tilings=8) if q_encoder is None else q_encoder + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + # TODO: 4 lines missing. + raise NotImplementedError("Implement function body") + + def __str__(self): + return f"LinearSemiGradQ{self.gamma}_{self.epsilon}_{self.alpha}" + +num_of_tilings = 8 +alpha = 1 / num_of_tilings +episodes = 300 +x = "Episode" +experiment_q = "experiments/mountaincar_semigrad_q" + +if __name__ == "__main__": + from irlc.ex10 import envs + env = gym.make("MountainCar500-v0") + for _ in range(10): + agent = LinearSemiGradQAgent(env, gamma=1, alpha=alpha, epsilon=0) + train(env, agent, experiment_q, num_episodes=episodes, max_runs=10) + main_plot(experiments=[experiment_q], x_key=x, y_key='Length', smoothing_window=30, resample_ticks=100) + savepdf("semigrad_q") + plt.show() diff --git a/irlc/ex11/semi_grad_sarsa.py b/irlc/ex11/semi_grad_sarsa.py new file mode 100644 index 0000000000000000000000000000000000000000..4c0e8147df943aa6e06a50f1e1a9f27d225f1ee3 --- /dev/null +++ b/irlc/ex11/semi_grad_sarsa.py @@ -0,0 +1,52 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +import matplotlib.pyplot as plt +from irlc import main_plot, savepdf +from irlc.ex01.agent import train +import numpy as np +import gymnasium as gym +from irlc.ex11.semi_grad_q import LinearSemiGradQAgent +np.seterr(all='raise') + +class LinearSemiGradSarsa(LinearSemiGradQAgent): + def __init__(self, env, gamma=0.99, epsilon=0.1, alpha=0.5, q_encoder=None): + """ Implement the Linear semi-gradient Sarsa method from (SB18, Section 10.1)""" + super().__init__(env, gamma, epsilon=epsilon, alpha=alpha, q_encoder=q_encoder) + + def pi(self, s, k, info=None): + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + return action + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + # TODO: 4 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + + if sum(np.abs(self.Q.w)) > 1e5: raise Exception("Weights diverged. Decrease alpha") + + def __str__(self): + return f"LinSemiGradSarsa{self.gamma}_{self.epsilon}_{self.alpha}" + +experiment_sarsa = "experiments/mountaincar_Sarsa" + +if __name__ == "__main__": + from irlc.ex11.semi_grad_q import experiment_q, alpha, x + from irlc.ex10 import envs + + env = gym.make("MountainCar500-v0") + for _ in range(10): + agent = LinearSemiGradSarsa(env, gamma=1, alpha=alpha, epsilon=0) + train(env, agent, experiment_sarsa, num_episodes=300, max_runs=10) + + main_plot(experiments=[experiment_q, experiment_sarsa], x_key=x, y_key='Length', smoothing_window=30) + savepdf("semigrad_q_sarsa") + plt.show() + + # Turn off averaging + main_plot(experiments=[experiment_q, experiment_sarsa], x_key=x, y_key='Length', smoothing_window=30, units="Unit", estimator=None) + savepdf("semigrad_q_sarsa_individual") + plt.show() diff --git a/irlc/ex12/__init__.py b/irlc/ex12/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6bf40e6e933a260431d40475e289b2c184c861ac --- /dev/null +++ b/irlc/ex12/__init__.py @@ -0,0 +1,2 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""This directory contains the exercises for week 12.""" diff --git a/irlc/ex12/mountain_car.py b/irlc/ex12/mountain_car.py new file mode 100644 index 0000000000000000000000000000000000000000..7483bdfceded62b90c718923a18f43cd251cb0d4 --- /dev/null +++ b/irlc/ex12/mountain_car.py @@ -0,0 +1,155 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.utils.common import log_time_series +from irlc.ex10 import envs +import numpy as np +import matplotlib.pyplot as plt +from tqdm import tqdm +from irlc import savepdf +from irlc.ex01.agent import train +from irlc.ex12.semi_grad_nstep_sarsa import LinearSemiGradSarsaN +import gymnasium as gym +from irlc import main_plot +from irlc.ex12.semi_grad_sarsa_lambda import LinearSemiGradSarsa + +# Helper function for plotting the value functions. +def plot_surface_2(X,Y,Z,fig=None, ax=None, **kwargs): + if fig is None and ax is None: + fig = plt.figure(figsize=(20, 10)) + if ax is None: + ax = fig.add_subplot(projection='3d') + surf = ax.plot_surface(X, Y, Z, cmap=plt.cm.coolwarm, linewidth=1, edgecolors='k', **kwargs) + ax.view_init(ax.elev, -120) + if fig is not None: + fig.colorbar(surf, shrink=0.5, aspect=5) + return ax + + +def plot_mountaincar_value_function(env, value_function, ax): + """ + 3d plot + """ + grid_size = 40 + low = env.unwrapped.observation_space.low + high = env.unwrapped.observation_space.high + X,Y = np.meshgrid( np.linspace(low[0], high[0], grid_size), np.linspace(low[1], high[1], grid_size) ) + Z = X*0 + for i, (x,y) in enumerate(zip(X.flat, Y.flat)): + Z.flat[i] = value_function( (x,y) ) + + plot_surface_2(X,Y,Z,ax=ax) + ax.set_xlabel('Position') + ax.set_ylabel('Velocity') + ax.set_zlabel('Cost to go') + +def figure_10_1(): + episodes = 9000 + plot_episodes = [1, 99, episodes - 1] + scale = 8 + fig = plt.figure(figsize=(4*scale, scale)) + axes = [fig.add_subplot(1, len(plot_episodes), i+1, projection='3d') for i in range(len(plot_episodes))] + num_of_tilings = 8 + alpha = 0.3 + + env = gym.make("MountainCar-v0") + agent = LinearSemiGradSarsa(env, gamma=1, alpha=alpha/num_of_tilings, epsilon=0) + for ep in tqdm(range(episodes)): + train(env, agent, num_episodes=1, max_steps=np.inf, verbose=False) + if ep in plot_episodes: + v = lambda s: -max(agent.Q.get_Qs(s)[1]) + ax = axes[plot_episodes.index(ep)] + plot_mountaincar_value_function(env, v, ax=ax) + ax.set_title(f'Episode {ep+1}') + + from irlc import savepdf + savepdf("semigrad_sarsa_10-1") + plt.show() + +def figure_10_2(): + episodes = 500 + num_of_tilings = 8 + alphas = [0.1, 0.2, 0.5] + env = gym.make("MountainCar500-v0") + + experiments = [] + for alpha in alphas: + agent = LinearSemiGradSarsa(env, gamma=1, alpha=alpha / num_of_tilings, epsilon=0) + experiment = f"experiments/mountaincar_10-2_{agent}_{episodes}" + train(env, agent, experiment_name=experiment, num_episodes=episodes,max_runs=10) + experiments.append(experiment) + + main_plot(experiments=experiments, y_key="Length") + plt.xlabel('Episode') + plt.ylabel('Steps per episode') + plt.title(env.spec.name + " - Semigrad Sarsa - Figure 10.2") + savepdf("mountaincar_10-2") + plt.show() + +def figure_10_3(): + from irlc.ex12.semi_grad_sarsa_lambda import LinearSemiGradSarsaLambda + from irlc.ex11.semi_grad_q import LinearSemiGradQAgent + + max_runs = 10 + episodes = 500 + num_of_tilings = 8 + alphas = [0.5, 0.3] + n_steps = [1, 8] + + env = gym.make("MountainCar500-v0") + experiments = [] + + """ Plot results of experiments here. """ + # TODO: 16 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + + main_plot(experiments=experiments, y_key="Length") + plt.xlabel('Episode') + plt.ylabel('Steps per episode') + plt.title(env.spec.name + " - Semigrad N-step Sarsa - Figure 10.3") + savepdf("mountaincar_10-3") + plt.show() + +def figure_10_4(): + alphas = np.arange(0.25, 1.75, 0.25) + n_steps = np.power(2, np.arange(0, 5)) + episodes = 50 + env = gym.make("MountainCar500-v0") + experiments = [] + num_of_tilings = 8 + max_asteps = 500 + run = True + for n_step_index, n_step in enumerate(n_steps): + aexp = [] + did_run = False + for alpha_index, alpha in enumerate(alphas): + if not run: + continue + if (n_step == 8 and alpha > 1) or (n_step == 16 and alpha > 0.75): + # In these cases it won't converge, so ignore them + asteps = max_asteps #max_steps * episodes + else: + n = n_step + agent = LinearSemiGradSarsaN(env, gamma=1, alpha=alpha / num_of_tilings, epsilon=0, n=n) + _, stats, _ = train(env, agent, num_episodes=episodes) + asteps = np.mean( [s['Length'] for s in stats] ) + did_run = did_run or stats is not None + + aexp.append({'alpha': alpha, 'average_steps': asteps}) + + experiment = f"experiments/mc_10-4_lsgn_{n_step}" + experiments.append(experiment) + if did_run: + log_time_series(experiment, aexp) + + main_plot(experiments, x_key="alpha", y_key="average_steps", ci=None) + plt.xlabel('alpha') + plt.ylabel('Steps per episode') + plt.title("Figure 10.4: Semigrad n-step Sarsa on mountain car") + plt.ylim([150, 300]) + savepdf("mountaincar_10-4") + plt.show() + +if __name__ == '__main__': + figure_10_1() + figure_10_2() + figure_10_3() + figure_10_4() diff --git a/irlc/ex12/sarsa_lambda_agent.py b/irlc/ex12/sarsa_lambda_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..9cd7baf7955fbfa16c6032acdaaf0dc213eb1c56 --- /dev/null +++ b/irlc/ex12/sarsa_lambda_agent.py @@ -0,0 +1,68 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from collections import defaultdict +import gymnasium as gym +from irlc.ex01.agent import train +from irlc import main_plot, savepdf +import matplotlib.pyplot as plt +from irlc.ex11.sarsa_agent import SarsaAgent + + +class SarsaLambdaAgent(SarsaAgent): + def __init__(self, env, gamma=0.99, epsilon=0.1, alpha=0.5, lamb=0.9): + """ + Implementation of Sarsa(Lambda) in the tabular version, see + http://incompleteideas.net/book/first/ebook/node77.html + for details. Remember to reset the + eligibility trace E after each episode, i.e. set E(s,a) = 0. + + Note 'lamb' is an abbreveation of lambda, because lambda is a reserved keyword in python. + + The constructor initializes e, the eligibility trace. Since we want to easily be able to find the non-zero + elements it will be convenient to use a dictionary. I.e. + + self.e[(s,a)] is the eligibility trace e(s,a) (or E(s,a) if you prefer). + + Note that Sarsa(Lambda) generalize Sarsa. This means that we again must generate the next action A' from S' in the train method and + store it for when we take actions in the policy method pi. I.e. we can re-use the Sarsa Agents code for the policy (self.pi). + """ + super().__init__(env, gamma=gamma, alpha=alpha, epsilon=epsilon) + self.lamb = lamb + # We use a dictionary to store the eligibility trace. It can be indexed as self.e[s,a]. + self.e = defaultdict(float) + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + # TODO: 1 lines missing. + raise NotImplementedError("a_prime = ... (get action for S'=sp using self.pi_eps; see Sarsa)") + # TODO: 1 lines missing. + raise NotImplementedError("delta = ... (The ordinary Sarsa learning signal)") + # TODO: 1 lines missing. + raise NotImplementedError("Update the eligibility trace e(s,a) += 1") + for (s,a), ee in self.e.items(): + # TODO: 2 lines missing. + raise NotImplementedError("Update Q values and eligibility trace") + if done: # Clear eligibility trace after each episode and update variables for Sarsa + self.e.clear() + else: + self.a = a_prime + + def __str__(self): + return f"SarsaLambda_{self.gamma}_{self.epsilon}_{self.alpha}_{self.lamb}" + +if __name__ == "__main__": + envn = 'CliffWalking-v0' + env = gym.make(envn) + + alpha =0.05 + sarsaLagent = SarsaLambdaAgent(env,gamma=0.99, epsilon=0.1, alpha=alpha, lamb=0.9) + sarsa = SarsaAgent(env,gamma=0.99,alpha=alpha,epsilon=0.1) + methods = [("SarsaL", sarsaLagent), ("Sarsa", sarsa)] + + experiments = [] + for k, (name,agent) in enumerate(methods): + expn = f"experiments/{envn}_{name}" + train(env, agent, expn, num_episodes=500, max_runs=10) + experiments.append(expn) + main_plot(experiments, smoothing_window=10, resample_ticks=200) + plt.ylim([-100, 0]) + savepdf("cliff_sarsa_lambda") + plt.show() diff --git a/irlc/ex12/sarsa_lambda_open.py b/irlc/ex12/sarsa_lambda_open.py new file mode 100644 index 0000000000000000000000000000000000000000..0fe4e1c621d79dfce739fda10974961ea5c9971c --- /dev/null +++ b/irlc/ex12/sarsa_lambda_open.py @@ -0,0 +1,35 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex12.sarsa_lambda_agent import SarsaLambdaAgent +from irlc.gridworld.gridworld_environments import OpenGridEnvironment +from irlc import train, interactive + +def keyboard_play(Agent, method_label='MC', num_episodes=1000, alpha=0.5, autoplay=False, **args): + print("Evaluating", Agent, "on the open gridworld environment.") + print("Press p to follow the agents policy or use the keyboard to input actions") + print("(Please be aware that Sarsa, N-step Sarsa, and Sarsa(Lambda) do not always make the right updates when you input actions with the keyboard)") + + env = OpenGridEnvironment(render_mode='human', frames_per_second=10) + try: + agent = Agent(env, gamma=0.99, epsilon=0.1, alpha=alpha, **args) + except Exception as e: # If it is a value agent without the epsilon. + agent = Agent(env, gamma=0.99, alpha=alpha, **args) + env, agent = interactive(env, agent, autoplay=autoplay) + train(env, agent, num_episodes=num_episodes) + env.close() + +if __name__ == "__main__": + """ + Example: Play a three episodes and save a snapshot of the Q-values as a .pdf + """ + env = OpenGridEnvironment(render_mode='human') + agent = SarsaLambdaAgent(env, gamma=0.99, epsilon=0.1, alpha=.5) + env, agent = interactive(env, agent, autoplay=True) + train(env, agent, num_episodes=3) + from irlc import savepdf + savepdf("sarsa_lambda_opengrid", env=env) + env.close() + + """ Example: Keyboard play + You can input actions manually with the keyboard, but the Q-values are not necessarily updates correctly in this mode. Can you tell why? + You can let the agent play by pressing `p`, in which case the Q-values will be updated correctly. """ + keyboard_play(SarsaLambdaAgent, method_label="Sarsa(Lambda)", lamb=0.8) diff --git a/irlc/ex12/semi_grad_nstep_sarsa.py b/irlc/ex12/semi_grad_nstep_sarsa.py new file mode 100644 index 0000000000000000000000000000000000000000..c7f6ac23a708b4a7529af7097185afe11fb60c7b --- /dev/null +++ b/irlc/ex12/semi_grad_nstep_sarsa.py @@ -0,0 +1,53 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex01.agent import train +import gymnasium as gym +from irlc.ex11.semi_grad_sarsa import LinearSemiGradSarsa +from irlc.ex11.nstep_sarsa_agent import SarsaNAgent + +class LinearSemiGradSarsaN(SarsaNAgent, LinearSemiGradSarsa): + def __init__(self, env, gamma=0.99, alpha=0.5, epsilon=0.1, q_encoder=None, n=1): + """ + Note you can access the super-classes as: + >> SarsaNAgent.pi(self, s) # Call the pi(s) as implemented in SarsaNAgent + Alternatively, just inherit from Agent and set up data structure as required. + """ + SarsaNAgent.__init__(self, env, gamma, alpha=alpha, epsilon=epsilon, n=n) + LinearSemiGradSarsa.__init__(self, env, gamma, alpha=alpha, epsilon=epsilon, q_encoder=q_encoder) + + def pi(self, s, k, info=None): + return SarsaNAgent.pi(self, s, k, info) + + def _q(self, s, a): + """ + Return Q(s,a) using the linear function approximator with weights self.w; i.e. use self.q + """ + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + + def _upd_q(self, s, a, delta): + """ + Update the weight-vector w using the appropriate rule (see exercise description). I.e. the update + should be of the form + + self.w += self.alpha * delta * (gradient of Q(s,a;w) + + where + delta = (G^n - Q(s,a;w) + """ + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + + def __str__(self): + return f"LinSemiGradSarsaN{self.gamma}_{self.epsilon}_{self.alpha}_{self.n}" + + +experiment_nsarsa = "experiments/mountaincar_SarsaN" +if __name__ == "__main__": + from irlc.ex12.semi_grad_sarsa_lambda import alpha, plot_including_week10, experiment_sarsaL, episodes + import irlc.ex10.envs + env = gym.make("MountainCar500-v0") + for _ in range(10): + agent = LinearSemiGradSarsaN(env, gamma=1, alpha=alpha, epsilon=0, n=4) + train(env, agent, experiment_nsarsa, num_episodes=episodes, max_runs=10) + # plot while including the results from last week for Sarsa and Q-learning + plot_including_week10([experiment_sarsaL, experiment_nsarsa],output="semigrad_sarsan") diff --git a/irlc/ex12/semi_grad_sarsa_lambda.py b/irlc/ex12/semi_grad_sarsa_lambda.py new file mode 100644 index 0000000000000000000000000000000000000000..04644d9253e0b98e7de5e68a08d8583276629233 --- /dev/null +++ b/irlc/ex12/semi_grad_sarsa_lambda.py @@ -0,0 +1,74 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +import gymnasium as gym +import numpy as np +from irlc.ex01.agent import train +from irlc import main_plot, savepdf +import matplotlib.pyplot as plt +from irlc.ex11.semi_grad_sarsa import LinearSemiGradSarsa + +class LinearSemiGradSarsaLambda(LinearSemiGradSarsa): + def __init__(self, env, gamma=0.99, epsilon=0.1, alpha=0.5, lamb=0.9, q_encoder=None): + """ + Sarsa(Lambda) with linear feature approximators (see (SB18, Section 12.7)). + """ + super().__init__(env, gamma, alpha=alpha, epsilon=epsilon, q_encoder=q_encoder) + self.z = np.zeros(self.Q.d) # Vector to store eligibility trace (same dimension as self.w) + self.lamb = lamb # lambda in Sarsa(lambda). We cannot use the reserved keyword 'lambda'. + + def pi(self, s, k, info=None): + if k == 0: # If beginning of episode. + self.a = self.pi_eps(s, info) + self.x = self.Q.x(s,self.a) + self.Q_old = 0 + return self.a + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + a_prime = self.pi_eps(sp, info_sp) if not done else -1 + x_prime = self.Q.x(sp, a_prime) if not done else None + """ + Update the eligibility trace self.z and the weights self.w here. + Note Q-values are approximated as Q = w @ x. + We use Q_prime = w * x(s', a') to denote the new q-values for (stored for next iteration as in the pseudo code) + """ + # TODO: 5 lines missing. + raise NotImplementedError("Update z, w") + if done: # Reset eligibility trace and time step t as in Sarsa. + self.z = self.z * 0 + else: + self.Q_old, self.x, self.a = Q_prime, x_prime, a_prime + + def __str__(self): + return f"LinearSarsaLambda_{self.gamma}_{self.epsilon}_{self.alpha}_{self.lamb}" + + +from irlc.ex11.semi_grad_q import experiment_q, x, episodes +from irlc.ex11.semi_grad_sarsa import experiment_sarsa +from irlc.ex10 import envs +experiment_sarsaL = "experiments/mountaincar_sarsaL" +num_of_tilings = 8 +alpha = 1 / num_of_tilings / 2 # learning rate + +def plot_including_week10(experiments, output): + exps = ["../ex11/" + e for e in [experiment_q, experiment_sarsa]] + experiments + + main_plot(exps, x_key=x, y_key='Length', smoothing_window=30, resample_ticks=100) + savepdf(output) + plt.show() + + # Turn off averaging + main_plot(exps, x_key=x, y_key='Length', smoothing_window=30, units="Unit", estimator=None, resample_ticks=100) + savepdf(output+"_individual") + plt.show() + +if __name__ == "__main__": + env = gym.make("MountainCar500-v0") + for _ in range(5): # run experiment 10 times + agent = LinearSemiGradSarsaLambda(env, gamma=1, alpha=alpha, epsilon=0) + train(env, agent, experiment_sarsaL, num_episodes=episodes, max_runs=10) + # Make plots (we use an external function so we can re-use it for the semi-gradient n-step controller) + plot_including_week10([experiment_sarsaL], output="semigrad_sarsaL") diff --git a/irlc/ex13/__init__.py b/irlc/ex13/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..d082cf61275cab1778627c63f97cafa89e399c09 --- /dev/null +++ b/irlc/ex13/__init__.py @@ -0,0 +1,2 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""This directory contains the exercises for week 13.""" diff --git a/irlc/ex13/__pycache__/__init__.cpython-311.pyc b/irlc/ex13/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7b3d045b0c7c2ba7e2f61edc2467536eae8273e1 Binary files /dev/null and b/irlc/ex13/__pycache__/__init__.cpython-311.pyc differ diff --git a/irlc/ex13/__pycache__/buffer.cpython-311.pyc b/irlc/ex13/__pycache__/buffer.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eefadd98930e8787a3439fd4bd00601ed4c07aa0 Binary files /dev/null and b/irlc/ex13/__pycache__/buffer.cpython-311.pyc differ diff --git a/irlc/ex13/__pycache__/dqn_network.cpython-311.pyc b/irlc/ex13/__pycache__/dqn_network.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4ae0dbe1149808f291ed5478bf244e3b5a6d8334 Binary files /dev/null and b/irlc/ex13/__pycache__/dqn_network.cpython-311.pyc differ diff --git a/irlc/ex13/__pycache__/torch_networks.cpython-311.pyc b/irlc/ex13/__pycache__/torch_networks.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..feab49d6f92ed16db97b01804a73a8c62dd2096c Binary files /dev/null and b/irlc/ex13/__pycache__/torch_networks.cpython-311.pyc differ diff --git a/irlc/ex13/buffer.py b/irlc/ex13/buffer.py new file mode 100644 index 0000000000000000000000000000000000000000..05ef6b56bde9132e8eac0d3122ebcca2bec459be --- /dev/null +++ b/irlc/ex13/buffer.py @@ -0,0 +1,109 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +import random +from collections import deque +from irlc import cache_read, cache_write + +class BasicBuffer: + """ + The buffer class is used to keep track of past experience and sample it for learning. + """ + def __init__(self, max_size=2000): + """ + Creates a new (empty) buffer. + + :param max_size: Maximum number of elements in the buffer. This should be a large number like 100'000. + """ + self.buffer = deque(maxlen=max_size) + + def push(self, state, action, reward, next_state, done): + """ + Add information from a single step, :math:`(s_t, a_t, r_{t+1}, s_{t+1}, \\text{done})` to the buffer. + + .. runblock:: pycon + + >>> import gymnasium as gym + >>> from irlc.ex13.buffer import BasicBuffer + >>> env = gym.make("CartPole-v1") + >>> b = BasicBuffer() + >>> s, info = env.reset() + >>> a = env.action_space.sample() + >>> sp, r, done, _, info = env.step(a) + >>> b.push(s, a, r, sp, done) + >>> len(b) # Get number of elements in buffer + + :param state: A state :math:`s_t` + :param action: Action taken :math:`a_t` + :param reward: Reward obtained :math:`r_{t+1}` + :param next_state: Next state transitioned to :math:`s_{t+1}` + :param done: ``True`` if the environment terminated else ``False`` + :return: ``None`` + """ + experience = (state, action, np.array([reward]), next_state, done) + self.buffer.append(experience) + + def sample(self, batch_size): + """ + Sample ``batch_size`` elements from the buffer for use in training a deep Q-learning method. + The elements returned all be numpy ``ndarray`` where the first dimension is the batch dimension, i.e. of size + ``batch_size``. + + .. runblock:: pycon + + >>> import gymnasium as gym + >>> from irlc.ex13.buffer import BasicBuffer + >>> env = gym.make("CartPole-v1") + >>> b = BasicBuffer() + >>> s, info = env.reset() + >>> a = env.action_space.sample() + >>> sp, r, done, _, _ = env.step(a) + >>> b.push(s, a, r, sp, done) + >>> S, A, R, SP, DONE = b.sample(batch_size=32) + >>> S.shape # Dimension batch_size x n + >>> R.shape # Dimension batch_size x 1 + + :param batch_size: Number of elements to sample + :return: + - S - Matrix of size ``batch_size x n`` of sampled states + - A - Matrix of size ``batch_size x n`` of sampled actions + - R - Matrix of size ``batch_size x n`` of sampled rewards + - SP - Matrix of size ``batch_size x n`` of sampled states transitioned to + - DONE - Matrix of size ``batch_size x 1`` of bools indicating if the environment terminated + + """ + state_batch = [] + action_batch = [] + reward_batch = [] + next_state_batch = [] + done_batch = [] + assert len(self.buffer) > 0, "The replay buffer must be non-empty in order to sample a batch: Use push()" + batch = random.choices(self.buffer, k=batch_size) + for state, action, reward, next_state, done in batch: + state_batch.append(state) + action_batch.append(action) + reward_batch.append(reward) + next_state_batch.append(next_state) + done_batch.append(done) + + return map(lambda x: np.asarray(x), (state_batch, action_batch, reward_batch, next_state_batch, done_batch)) + + def __len__(self): + return len(self.buffer) + + def save(self, path): + """ + Use this to save the content of the buffer to a file + + :param path: Path where to save (use same argument with ``load``) + :return: ``None`` + """ + cache_write(self.buffer, path) + + def load(self, path): + """ + Use this to load buffer content from a file + + :param path: Path to load from (use same argument with ``save``) + :return: ``None`` + """ + self.buffer = cache_read(path) diff --git a/irlc/ex13/deepq_agent.py b/irlc/ex13/deepq_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..43facb24f1cabda107e726b71354b318476de225 --- /dev/null +++ b/irlc/ex13/deepq_agent.py @@ -0,0 +1,130 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +USE_KERAS = False # Toggle to use Keras/Pytorch +import gymnasium as gym +import numpy as np +import os +from matplotlib import pyplot as plt +from irlc.ex01.agent import train +from irlc.ex13.buffer import BasicBuffer +from irlc import cache_write, cache_read, cache_exists +from irlc.ex09.rl_agent import TabularAgent +from irlc.ex13.torch_networks import TorchNetwork as QNetwork # Torch network architechture + +class DeepQAgent(TabularAgent): + def __init__(self, env, network=None, buffer=None, gamma=0.99, epsilon=None, alpha=0.001, batch_size=32, + replay_buffer_size=2000, replay_buffer_minreplay=500): + # Ensure 'epsilon' is a function to allow gradually decreasing exploration rate + epsilon = epsilon if callable(epsilon) else lambda steps, episodes: epsilon + super().__init__(env, gamma=gamma, epsilon=epsilon) + self.memory = BasicBuffer(replay_buffer_size) if buffer is None else buffer + """ + All the 'deep' stuff is handled by a seperate class. For instance + self.Q(s) + will return a [batch_size x actions] matrix of Q-values + """ + self.Q = network(env, trainable=True) if network else QNetwork(env, trainable=True, learning_rate=alpha) + self.batch_size = batch_size + self.replay_buffer_minreplay = replay_buffer_minreplay + self.steps, self.episodes = 0, 0 + + def pi(self, s, k, info_s=None): + eps_ = self.epsilon(self.steps, self.episodes) # get the learning rate + # return action by regular epsilon-greedy exploration + return self.env.action_space.sample() if np.random.rand() < eps_ else np.argmax(self.Q(s[np.newaxis,...])) + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + self.memory.push(s, a, r, sp, done) # save current observation + if len(self.memory) > self.replay_buffer_minreplay: + self.experience_replay() # do the actual training step + self.steps, self.episodes = self.steps + 1, self.episodes + done + + def experience_replay(self): + """ + Perform the actual deep-Q learning step. + + The actual learning is handled by calling self.Q.fit(s,target) + where s is defined as below (i.e. all states from the replay buffer) + and target is the desired value of self.Q(s). + + Note that target must therefore be of size Batch x Actions. In other words fit minimize + + |Q(s) - target|^2 + + which must implement the proper cost. This can be done by setting most entries of target equal to self.Q(s) + and the other equal to y, which is Q-learning target for Q(s,a). """ + """ First we sample from replay buffer. Returns numpy Arrays of dimension + > [self.batch_size] x [...]] + for instance 'a' will be of dimension [self.batch_size x 1]. + """ + s,a,r,sp,done = self.memory.sample(self.batch_size) + # TODO: 3 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + self.Q.fit(s, target) + + def save(self, path): # allows us to save/load model + if not os.path.isdir(path): + os.makedirs(path) + self.Q.save(os.path.join(path, "Q")) + cache_write(dict(steps=self.steps, episodes=self.episodes), os.path.join(path, "agent.pkl")) + mpath = os.path.join(path, "memory.pkl") + import shutil + if os.path.isfile(mpath): + shutil.move(mpath, mpath +".backup") # shuffle file + self.memory.save(mpath) + + def load(self, path): # allows us to save/load model + if not cache_exists(os.path.join(path, "agent.pkl")): + return False + for k, v in cache_read(os.path.join(path, "agent.pkl")).items(): + self.__dict__[k] = v + self.Q.load(os.path.join(path, "Q")) + self.memory.load(os.path.join(path, "memory.pkl")) + return True + + def __str__(self): + return f"basic_DQN{self.gamma}" + +def linear_interp(maxval, minval, delay, miniter): + """ + Will return a function f(i) with the following signature: + + f(i) = maxval for i < delay + f(i) = linear interpolate between max/minval until delay+miniter + f(i) = miniter for i > delay+miniter + """ + return lambda steps, episodes: min(max([maxval- ((steps-delay)/miniter)*(maxval-minval), minval]), maxval) + +cartpole_dqn_options = dict(gamma=0.95, epsilon=linear_interp(maxval=1,minval=0.01,delay=300,miniter=5000), + replay_buffer_minreplay=300, replay_buffer_size=500000) + +def mk_cartpole(): + env = gym.make("CartPole-v0") + agent = DeepQAgent(env, **cartpole_dqn_options) + return env, agent + +if __name__ == "__main__": + env_id = "CartPole-v0" + ex = f"experiments/cartpole_dqn" + num_episodes = 200 # We train for 200 episodes + env, agent = mk_cartpole() + train(env, agent, experiment_name=ex, num_episodes=num_episodes) + from irlc import main_plot, savepdf + main_plot([ex], units="Unit", estimator=None, smoothing_window=None) + savepdf("cartpole_dqn") + plt.show() + + """ Part 2: The following code showcase how to use the save/load method to store intermediate results + and resume training. Note you have to manually remove 'bad' runs otherwise it will resume where + it left off """ + ex = f"experiments/cartpole_dqn_cache" + num_episodes = 20 # we train 20 just episodes at a time + for j in range(10): # train for a total of 200 episodes + env, agent = mk_cartpole() + """ + saveload_model=True means it will store and load intermediate results + i.e. we can resume training later. It will not be very useful for cartpole, but necesary for e.g. + the atari environment which can run for days + """ + agent.load(ex) + train(env, agent, experiment_name=ex, num_episodes=num_episodes, resume_stats=True) # Resume stat collection from last checkpoint. + agent.save(ex) diff --git a/irlc/ex13/double_deepq_agent.py b/irlc/ex13/double_deepq_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..99b624b1a44d27afcfda1bc4f0341144b06a4e1a --- /dev/null +++ b/irlc/ex13/double_deepq_agent.py @@ -0,0 +1,73 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import gymnasium as gym +import numpy as np +import os +from irlc.ex13.deepq_agent import DeepQAgent +from matplotlib import pyplot as plt +from irlc.ex13.torch_networks import TorchNetwork as QNetwork # Torch network architechture + +class DoubleQAgent(DeepQAgent): + def __init__(self, env, network=None, buffer=None, gamma=0.99, epsilon=0.2, alpha=0.001, tau=0.1, batch_size=32, + replay_buffer_size=2000, replay_buffer_minreplay=500): + super().__init__(env, network=network, buffer=buffer, gamma=gamma,epsilon=epsilon, alpha=alpha, batch_size=batch_size, + replay_buffer_size=replay_buffer_size, replay_buffer_minreplay=replay_buffer_minreplay) + # The target network play the role of q_{phi'} in the slides. + self.target = QNetwork(env, learning_rate=alpha, trainable=False) if network is None else network(env, learning_rate=alpha, trainable=False) + self.tau = tau # Rate at which the weights in the target network is updated (see slides) + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + self.memory.push(s, a, r, sp, done) + if len(self.memory) > self.replay_buffer_minreplay: + self.experience_replay() + # TODO: 1 lines missing. + raise NotImplementedError("update Phi here in the self.target network") + self.steps, self.episodes = self.steps + 1, self.episodes + done + + def experience_replay(self): + """ Update the double-Q method, i.e. make sure to select actions a' using self.Q + but evaluate the Q-values using the target network (see slides). + In other words, + > self.target(s) + is a Q-function network which evaluates + > q-hat_{\phi'}(s,:). + Asides this, the code will be nearly identical to the basic DQN agent """ + s,a,r,sp,done = self.memory.sample(self.batch_size) + # TODO: 5 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + self.Q.fit(s, target=target) + + def save(self, path): + super().save(path) + self.target.save(os.path.join(path, "Q_target")) # also save target network + + def load(self, path): + loaded = super().load(path) + if loaded: + self.Q.load(os.path.join(path, "Q_target")) # also load target network + return loaded + + + def __str__(self): + return f"doubleDQN_{self.gamma}" + +from irlc.ex13.deepq_agent import cartpole_dqn_options +cartpole_doubleq_options = {**cartpole_dqn_options, 'tau': 0.08} + +def mk_cartpole(): + env = gym.make("CartPole-v0") + agent = DoubleQAgent(env, **cartpole_doubleq_options) + return env, agent + +if __name__ == "__main__": + from irlc import main_plot, savepdf + + env_id = "CartPole-v0" + MAX_EPISODES = 200 + for j in range(1): + env, agent = mk_cartpole() + from irlc.ex01.agent import train + ex = f"experiments/cartpole_double_dqn" + train(env, agent, experiment_name=ex, num_episodes=MAX_EPISODES) + main_plot([f"experiments/cartpole_dqn", ex], estimator=None, smoothing_window=None) + savepdf("cartpole_double_dqn") + plt.show() diff --git a/irlc/ex13/dqn_network.py b/irlc/ex13/dqn_network.py new file mode 100644 index 0000000000000000000000000000000000000000..d1920992457e06a6c4d390dc654f601d4e51a73a --- /dev/null +++ b/irlc/ex13/dqn_network.py @@ -0,0 +1,63 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +class DQNNetwork: + """ + A class representing a deep Q network. + Note that this function is batched. I.e. ``s`` is assumed to be a numpy array of dimension ``batch_size x n`` + + The following example shows how you can evaluate the Q-values in a given state. An example: + + .. runblock:: pycon + + >>> from irlc.ex13.torch_networks import TorchNetwork + >>> import gymnasium as gym + >>> import numpy as np + >>> env = gym.make("CartPole-v1") + >>> Q = TorchNetwork(env, trainable=True, learning_rate=0.001) # DQN network requires an env to set network dimensions + >>> batch_size = 32 # As an example + >>> states = np.random.rand(batch_size, env.observation_space.shape[0]) # Creates some dummy input + >>> states.shape # batch_size x n + >>> qvals = Q(states) # Evaluate Q(s,a) + >>> qvals.shape # This is a tensor of dimension batch_size x actions + >>> print(qvals[0,1]) # Get Q(s_0, 1) + >>> Y = np.random.rand(batch_size, env.action_space.n) # Generate target Q-values (training data) + >>> Q.fit(states, Y) # Train the Q-network for 1 gradient descent step + """ + def update_Phi(self, source, tau=0.01): + """ + Update (adapts) the weights in this network towards those in source by a small amount. + + For each weight :math:`w_i` in (this) network, and each corresponding weight :math:`w'_i` in the ``source`` network, + the following Polyak update is performed: + + .. math:: + w_i \\leftarrow w_i + \\tau (w'_i - w_i) + + :param source: Target network to update towards + :param tau: Update rate (rate of change :math:`\\tau` + :return: ``None`` + """ + + raise NotImplementedError + + def __call__(self, s): + """ + Evaluate the Q-values in the given (batched) state. + + :param s: A matrix of size ``batch_size x n`` where :math:`n` is the state dimension. + :return: The Q-values as a ``batch_size x d`` dimensional matrix where :math:`d` is the number of actions. + """ + raise NotImplementedError + + def fit(self, s, target): + """ + Fit the network weights by minimizing + + .. math:: + \\frac{1}{B}\sum_{i=1}^B \sum_{a=1}^K \| q_\phi(s_i)_a - y_{i,a} \|^2 + + where ``target`` corresponds to :math:`y` and is a ``[batch_size x actions]`` matrix of target Q-values. + :param s: + :param target: + :return: + """ + raise NotImplementedError diff --git a/irlc/ex13/duel_deepq_agent.py b/irlc/ex13/duel_deepq_agent.py new file mode 100644 index 0000000000000000000000000000000000000000..65491d368a25462fa597e0c4017b995e40b9f9fc --- /dev/null +++ b/irlc/ex13/duel_deepq_agent.py @@ -0,0 +1,35 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import gymnasium as gym +import matplotlib.pyplot as plt +from irlc import main_plot, savepdf +from irlc.ex01.agent import train +from irlc.ex13.double_deepq_agent import DoubleQAgent +from irlc.ex13.torch_networks import TorchDuelNetwork as DuelNetwork +from irlc.ex13.buffer import BasicBuffer +from irlc.ex13.double_deepq_agent import cartpole_doubleq_options + +class DuelQAgent(DoubleQAgent): + def __init__(self, env, network=None, buffer=None, gamma=0.99, epsilon=None, alpha=0.001, tau=0.1, batch_size=32, + replay_buffer_size=2000, replay_buffer_minreplay=500): + network = DuelNetwork if network is None else network # Only relevant change + buffer = buffer if buffer is not None else BasicBuffer(max_size=500000) + super().__init__(env, network=network, buffer=buffer, gamma=gamma,epsilon=epsilon, alpha=alpha, tau=tau,batch_size=batch_size, + replay_buffer_size=replay_buffer_size, replay_buffer_minreplay=replay_buffer_minreplay) + self.target.update_Phi(self.Q) + + def __str__(self): + return f"DuelQ_{self.gamma}" + +def mk_cartpole(): + env = gym.make("CartPole-v0") + agent = DuelQAgent(env, **cartpole_doubleq_options) + return env, agent + +if __name__ == "__main__": + env,agent = mk_cartpole() + ex = f"experiments/cartpole_duel_dqn" + train(env, agent, experiment_name=ex, num_episodes=200) + plt.close() + main_plot([f"experiments/cartpole_dqn", f"experiments/cartpole_double_dqn", ex], smoothing_window=None) + savepdf("cartpole_duel_dqn") + plt.show() diff --git a/irlc/ex13/dyna_q.py b/irlc/ex13/dyna_q.py new file mode 100644 index 0000000000000000000000000000000000000000..a764bef575e35b50f993f7f4e7502520df0caac6 --- /dev/null +++ b/irlc/ex13/dyna_q.py @@ -0,0 +1,89 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +import numpy as np +from irlc.ex01.agent import train +import gymnasium as gym +from irlc import main_plot +import matplotlib.pyplot as plt +from irlc import savepdf +from irlc.ex11.sarsa_agent import SarsaAgent +from irlc.ex11.q_agent import QAgent +from irlc.ex12.sarsa_lambda_agent import SarsaLambdaAgent +from irlc.ex13.maze_dyna_environment import MazeEnvironment + +class DynaQ(QAgent): + """ + Implement the tabular dyna-Q agent (SB18, Section 8.7). + """ + def __init__(self, env, gamma=1.0, alpha=0.5, epsilon=0.1, n=5): + super().__init__(env, gamma, alpha=alpha, epsilon=epsilon) + """ + Model is a list of experience, i.e. of the form + Model = [ (s_t, a_t, r_{t+1}, s_{t+1}, done_t), ...] + """ + self.Model = [] + self.n = n # number of planning steps + + def q_update(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + """ + Update the Q-function self.Q[s,a] as in regular Q-learning + """ + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + self.q_update(s,a,r,sp,done, info_s, info_sp) + self.Model.append( (s,a, r,sp, done)) + for _ in range(self.n): + """ Obtain a random transition from the replay buffer. You can use np.random.randint + then call self.q_update on the random sample. """ + # TODO: 2 lines missing. + raise NotImplementedError("Implement function body") + + def __str__(self): + return f"DynaQ_{self.gamma}_{self.epsilon}_{self.alpha}_{self.n}" + + +def dyna_experiment(env, env_name='maze',num_episodes=50,epsilon=0.1, alpha=0.1, gamma=.95, runs=2): + for _ in range(runs): # Increase runs for nicer error bars + agents = [QAgent(env, epsilon=epsilon, alpha=alpha,gamma=gamma), + SarsaAgent(env, epsilon=epsilon, alpha=alpha, gamma=gamma), + SarsaLambdaAgent(env, epsilon=epsilon, alpha=alpha, gamma=gamma,lamb=0.9), + DynaQ(env, epsilon=epsilon, alpha=alpha,gamma=gamma,n=5), + DynaQ(env, epsilon=epsilon, alpha=alpha,gamma=gamma, n=50), + ] + + experiments = [] + for agent in agents: + expn = f"experiments/b{env_name}_{str(agent)}" + train(env, agent, expn, num_episodes=num_episodes, max_runs=100) + experiments.append(expn) + return experiments + +if __name__ == "__main__": + from irlc.ex09.mdp import MDP2GymEnv + """ The maze-environment is created as an MDP, and we then convert it to a Gym environment. + Alternatively, use the irlc.gridworld.gridworld_environments.py - method to specify the layout as in the other gridworld examples. """ + env = MDP2GymEnv(MazeEnvironment()) + experiments = dyna_experiment(env, env_name='maze',num_episodes=50,epsilon=0.1, alpha=0.1, gamma=.95, runs=4) + main_plot(experiments, smoothing_window=None, y_key="Length") + plt.ylim([0, 500]) + plt.title("Dyna Q on simple Maze (Figure 8.2)") + savepdf("dynaq_maze_8_2") + plt.show() + + # Part 2: Cliffwalking as reference. + env = gym.make('CliffWalking-v0') + gamma, alpha, epsilon = 1, 0.5, 0.1 + # Call the dyna_experiment(...) function here similar to the previous call but using new parameters. + # TODO: 1 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + main_plot(experiments, smoothing_window=5) + plt.ylim([-150, 0]) + plt.title("Dyna-Q learning on " + env.spec.name) + savepdf("dyna_cliff") + plt.show() diff --git a/irlc/ex13/maximization_bias_environment.py b/irlc/ex13/maximization_bias_environment.py new file mode 100644 index 0000000000000000000000000000000000000000..9e40bc32f912d5549e5aa067f3d3b83017ca0437 --- /dev/null +++ b/irlc/ex13/maximization_bias_environment.py @@ -0,0 +1,93 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +import numpy as np +from irlc.ex01.agent import train +from irlc import main_plot +import matplotlib.pyplot as plt +from irlc.ex09.mdp import MDP, MDP2GymEnv +from irlc import savepdf +from irlc.ex11.sarsa_agent import SarsaAgent +from irlc.ex11.q_agent import QAgent +from irlc.ex13.tabular_double_q import TabularDoubleQ + +class MaximizationBiasEnvironment(MDP): + """ + The Maximization Bias yafcport from (SB18, Example 6.7). + For easy implementation, we fix the number of transitions from state B to terminal state to + normal_transitions. The code ensure they still have average reward 0.1, i.e. no action will be preferred. + there are B_actions possible actions from state B in this yafcport (the number is not given in the yafcport). + """ + def __init__(self, B_actions=10, normal_transitions=100, **kwargs): + self.state_A = 0 + self.state_B = 1 + self.LEFT = 0 + self.RIGHT = 1 + self.B_actions = B_actions + self.n_transitions = normal_transitions + super().__init__(initial_state=self.state_A, **kwargs) + + def is_terminal(self, state): + return state == 2 + + def A(self, s): + # define the actions pace + if s == self.state_A: + return [self.LEFT, self.RIGHT] + elif s == self.state_B: # in state B + return [n for n in range(self.B_actions)] + else: + return [0] # terminal; return a dummy action 0 which does nothing (some code is sensitive to empty action spaces) + + def Psr(self, s, a): + t = 2 # terminal state + if s == self.state_A: + if a == self.RIGHT: + # TODO: 1 lines missing. + raise NotImplementedError("Implement what the environment does in state A with a RIGHT action") + else: + # TODO: 1 lines missing. + raise NotImplementedError("Implement what the environment does in state A with a LEFT action") + else: # s is in state B + p = 1/self.n_transitions # transition probability + rewards = [np.random.randn() for _ in range(self.n_transitions)] + rewards = [r - np.mean(rewards)-0.1 for r in rewards] + return { (t, r): p for r in rewards} + +if __name__ == "__main__": + """ + The Maximization Bias from (SB18, Example 6.7). + I have fixed the number of "junk" actions in state B to 10, but it can easily be changed + in the environment. + + I don't have an easy way to get the number of 'left'-actions, so instead i plot + the trajectory length: it is 1 for a right action, and 2 for a left. + """ + env = MDP2GymEnv(MaximizationBiasEnvironment()) + + for _ in range(100): + epsilon = 0.1 + alpha = 0.1 + gamma = 1 + agents = [QAgent(env, epsilon=epsilon, alpha=alpha), + SarsaAgent(env, epsilon=epsilon, alpha=alpha), + TabularDoubleQ(env, epsilon=epsilon, alpha=alpha)] + + experiments = [] + for agent in agents: + expn = f"experiments/bias_{str(agent)}" + train(env, agent, expn, num_episodes=300, max_runs=100) + experiments.append(expn) + + main_plot(experiments, smoothing_window=10, y_key="Length") + plt.ylim([1, 2]) + plt.title("Double-Q learning on Maximization-Bias ex. (Figure 6.5)") + savepdf("maximization_bias_6_5") + plt.show() + + main_plot(experiments, smoothing_window=10) + savepdf("maximization_bias_6_5_reward") + plt.show() diff --git a/irlc/ex13/maze_dyna_environment.py b/irlc/ex13/maze_dyna_environment.py new file mode 100644 index 0000000000000000000000000000000000000000..771af4903e4ee3bb81e097e540c3b4146ee5c1cc --- /dev/null +++ b/irlc/ex13/maze_dyna_environment.py @@ -0,0 +1,118 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" +The DynaQ Maze environment. + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" + +from irlc.ex09.mdp import MDP + +class MazeEnvironment(MDP): + """ + The Maze environment from (SB18, Example 8.1) + """ + def __init__(self, **kwargs): + self.maze_ = HiddenMaze() + super().__init__(initial_state=tuple(self.maze_.START_STATE), **kwargs) + + def is_terminal(self, state): + return state == tuple(self.maze_.GOAL_STATES[0]) + + def A(self, s): + return self.maze_.actions + + def Psr(self, s, a): + xy, r = self.maze_.step(list(s), a) + return { (tuple(xy), r): 1 } + +# A wrapper class for a maze, containing all the information about the maze. +# Basically it's initialized to DynaMaze by default, however it can be easily adapted +# to other maze +class HiddenMaze: + def __init__(self): + # maze width + self.WORLD_WIDTH = 9 + + # maze height + self.WORLD_HEIGHT = 6 + + # all possible actions + self.ACTION_UP = 0 + self.ACTION_DOWN = 1 + self.ACTION_LEFT = 2 + self.ACTION_RIGHT = 3 + self.actions = [self.ACTION_UP, self.ACTION_DOWN, self.ACTION_LEFT, self.ACTION_RIGHT] + + # start state + self.START_STATE = [2, 0] + + # goal state + self.GOAL_STATES = [[0, 8]] + + # all obstacles + self.obstacles = [[1, 2], [2, 2], [3, 2], [0, 7], [1, 7], [2, 7], [4, 5]] + self.old_obstacles = None + self.new_obstacles = None + + # time to change obstacles + self.obstacle_switch_time = None + + # initial state action pair values + # self.stateActionValues = np.zeros((self.WORLD_HEIGHT, self.WORLD_WIDTH, len(self.actions))) + + # the size of q value + self.q_size = (self.WORLD_HEIGHT, self.WORLD_WIDTH, len(self.actions)) + + # max steps + self.max_steps = float('inf') + + # track the resolution for this maze + self.resolution = 1 + + # extend a state to a higher resolution maze + # @state: state in lower resoultion maze + # @factor: extension factor, one state will become factor^2 states after extension + def extend_state(self, state, factor): + new_state = [state[0] * factor, state[1] * factor] + new_states = [] + for i in range(0, factor): + for j in range(0, factor): + new_states.append([new_state[0] + i, new_state[1] + j]) + return new_states + + # extend a state into higher resolution + # one state in original maze will become @factor^2 states in @return new maze + def extend_maze(self, factor): + new_maze = HiddenMaze() + new_maze.WORLD_WIDTH = self.WORLD_WIDTH * factor + new_maze.WORLD_HEIGHT = self.WORLD_HEIGHT * factor + new_maze.START_STATE = [self.START_STATE[0] * factor, self.START_STATE[1] * factor] + new_maze.GOAL_STATES = self.extend_state(self.GOAL_STATES[0], factor) + new_maze.obstacles = [] + for state in self.obstacles: + new_maze.obstacles.extend(self.extend_state(state, factor)) + new_maze.q_size = (new_maze.WORLD_HEIGHT, new_maze.WORLD_WIDTH, len(new_maze.actions)) + # new_maze.stateActionValues = np.zeros((new_maze.WORLD_HEIGHT, new_maze.WORLD_WIDTH, len(new_maze.actions))) + new_maze.resolution = factor + return new_maze + + # take @action in @state + # @return: [new state, reward] + def step(self, state, action): + x, y = state + if action == self.ACTION_UP: + x = max(x - 1, 0) + elif action == self.ACTION_DOWN: + x = min(x + 1, self.WORLD_HEIGHT - 1) + elif action == self.ACTION_LEFT: + y = max(y - 1, 0) + elif action == self.ACTION_RIGHT: + y = min(y + 1, self.WORLD_WIDTH - 1) + if [x, y] in self.obstacles: + x, y = state + if [x, y] in self.GOAL_STATES: + reward = 1.0 + else: + reward = 0.0 + return [x, y], reward diff --git a/irlc/ex13/tabular_double_q.py b/irlc/ex13/tabular_double_q.py new file mode 100644 index 0000000000000000000000000000000000000000..a2280d893bbdfdae2672ff417f4a8ca37fee5aa3 --- /dev/null +++ b/irlc/ex13/tabular_double_q.py @@ -0,0 +1,78 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +import numpy as np +from irlc.ex01.agent import train +import gymnasium as gym +from irlc import main_plot +import matplotlib.pyplot as plt +from irlc import savepdf +from irlc.ex11.sarsa_agent import SarsaAgent +from irlc.ex11.q_agent import QAgent +from irlc import Agent + +class TabularDoubleQ(QAgent): + """ + Implement the tabular version of the double-Q learning agent from + (SB18, Section 6.7). + + Note we will copy the Q-datastructure from the Agent class. + """ + def __init__(self, env, gamma=1.0, alpha=0.5, epsilon=0.1): + super().__init__(env, gamma, epsilon) + self.alpha = alpha + # The two Q-value functions. These are of the same type as the regular self.Q function + from irlc.ex09.rl_agent import TabularQ + self.Q1 = TabularQ(env) + self.Q2 = TabularQ(env) + self.Q = None # remove self.Q (we will not use it in double Q) + + def pi(self, s, k, info=None): + """ + Implement the epsilon-greedy action. The implementation is nearly identical to pi_eps in the Agent class + which can be used for inspiration, however we should use Q1+Q2 as the Q-value. + """ + a1, Q1 = self.Q1.get_Qs(s, info) + a2, Q2 = self.Q2.get_Qs(s, info) + Q = np.asarray(Q1) + np.asarray(Q2) + + # TODO: 1 lines missing. + raise NotImplementedError("Return epsilon-greedy action using Q") + + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + """ + Implement the double-Q learning rule, i.e. with probability np.random.rand() < 0.5 switch + the role of the two Q networks Q1 and Q2. Use the code for the regular Q-agent as inspiration. + """ + # TODO: 4 lines missing. + raise NotImplementedError("Implement function body") + + def __str__(self): + return f"TabularDoubleQ_{self.gamma}_{self.epsilon}_{self.alpha}" + +if __name__ == "__main__": + """ Part 1: Cliffwalking """ + env = gym.make('CliffWalking-v0') + epsilon = 0.1 + alpha = 0.25 + gamma = 1.0 + for _ in range(20): + agents = [QAgent(env, gamma=1, epsilon=epsilon, alpha=alpha), + SarsaAgent(env, gamma=1, epsilon=epsilon, alpha=alpha), + TabularDoubleQ(env, gamma=1, epsilon=epsilon, alpha=alpha)] + + experiments = [] + for agent in agents: + expn = f"experiments/doubleq_cliffwalk_{str(agent)}" + train(env, agent, expn, num_episodes=500, max_runs=20) + experiments.append(expn) + + main_plot(experiments, smoothing_window=10) + plt.ylim([-100, 0]) + plt.title("Double-Q learning on " + env.spec.name) + savepdf("double_Q_learning_cliff") + plt.show() diff --git a/irlc/ex13/torch_networks.py b/irlc/ex13/torch_networks.py new file mode 100644 index 0000000000000000000000000000000000000000..9ea56b5b32b2d92f7a67fcae747107eb4b14d656 --- /dev/null +++ b/irlc/ex13/torch_networks.py @@ -0,0 +1,131 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +import os +from irlc.ex13.dqn_network import DQNNetwork +import torch +import torch.nn as nn +import torch.optim as optim +import torch.autograd as autograd + +# Use GPU; If the drivers give you grief you can turn GPU off without a too big hit on performance in the cartpole task +USE_CUDA = torch.cuda.is_available() + +Variable = lambda *args, **kwargs: autograd.Variable(*args, **kwargs).cuda() if USE_CUDA else autograd.Variable(*args, **kwargs) + +class TorchNetwork(nn.Module,DQNNetwork): + def __init__(self, env, trainable=True, learning_rate=0.001, hidden=30): + nn.Module.__init__(self) + DQNNetwork.__init__(self) + self.env = env + self.hidden = hidden + self.actions = env.action_space.n + self.build_model_() + if trainable: + self.optimizer = optim.Adam(self.parameters(), lr=learning_rate) + if USE_CUDA: + self.cuda() + + def build_feature_network(self): + num_observations = np.prod(self.env.observation_space.shape) + return (nn.Linear(num_observations, self.hidden), + nn.ReLU(), + nn.Linear(self.hidden, self.hidden), + nn.ReLU()) + + def build_model_(self): + num_actions = self.env.action_space.n + self.model = nn.Sequential(*self.build_feature_network(), nn.Linear(self.hidden,num_actions)) + + def forward(self, s): + s = Variable(torch.FloatTensor(s)) + s = self.model(s) + return s + + def __call__(self, s): + return self.forward(s).detach().numpy() + + def fit(self, s, target): + q_value = self.forward(s) + loss = (q_value - torch.FloatTensor(target).detach()).pow(2).sum(axis=1).mean() + self.optimizer.zero_grad() + loss.backward() + self.optimizer.step() + + def update_Phi(self, source, tau=1): + """ + Polyak adapt weights of this class given source: + I.e. tau=1 means adopt weights in one step, + tau = 0.001 means adopt very slowly, tau=1 means instant overwriting + """ + state = self.state_dict() + for k, wa in state.items(): + wb = source.state_dict()[k] + state[k] = wa*(1 - tau) + wb * tau + self.load_state_dict(state) + + def save(self, path): + if not os.path.exists(os.path.dirname(path)): + os.mkdir(os.path.dirname(path)) + torch.save(self.state_dict(), path+".torchsave") + + def load(self, path): + self.load_state_dict(torch.load(path+".torchsave")) + self.eval() # set batch norm layers, dropout, other stuff we don't use + +class TorchDuelNetwork(TorchNetwork): + def build_model_(self): + self.feature = nn.Sequential(*self.build_feature_network()) + self.advantage = nn.Sequential(nn.Linear(self.hidden, self.hidden), + nn.ReLU(), + nn.Linear(self.hidden, self.actions)) + self.value = nn.Sequential(nn.Linear(self.hidden, self.hidden), + nn.ReLU(), + nn.Linear(self.hidden, 1)) + + def forward(self, s): + """ + Return tensor corresponding to Q-values when using dueling Q-networks (see exercise description) + """ + # TODO: 4 lines missing. + raise NotImplementedError("Implement function body") + return value + advantage - advantage.mean() + +class TorchDuelNetworkAtari(TorchNetwork): + def build_feature_network(self): + hidden_size = 256 + in_channels = self.env.observation_space.shape[-1] + num_actions = self.env.action_space.n + return (nn.Conv2d(in_channels, 32, kernel_size=8, stride=4), + nn.BatchNorm2d(32), + nn.Conv2d(32, 64, kernel_size=4, stride=2), + nn.BatchNorm2d(64), + nn.Conv2d(64, 64, kernel_size=3, stride=1), + nn.BatchNorm2d(64), + nn.Linear(7 * 7 * 64, hidden_size), # has to be adjusted for other resolutionz + nn.Linear(hidden_size, num_actions) ) + +if __name__ == "__main__": + a = 234 + import gymnasium as gym + + env = gym.make("CartPole-v0") + Q = DQNNetwork(env, trainable=True, learning_rate=0.001) + + # self.Q = Network(env, trainable=True) # initialize the network + """ Assuming s has dimension [batch_dim x d] this returns a float numpy Array + array of Q-values of [batch_dim x actions], such that qvals[i,a] = Q(s_i,a) """ + batch_size = 32 # As an example + # Creates some dummy input + states = [env.reset()[0] for _ in range(batch_size)] + states.shape # batch_size x n + + qvals = Q(states) + qvals.shape # This is a tensor of dimension batch_size x actions + print(qvals[0,1]) # Get Q(s_0, 1) + + Y = np.random.rand( (batch_size, 1)) # Generate target Q-values (training data) + Q.fit(states, Y) # Train the Q-network. + + + + # Q = TorchNetwork() diff --git a/irlc/exam/__init__.py b/irlc/exam/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..4615460b3db7947c972d696206eb8266ca543c94 --- /dev/null +++ b/irlc/exam/__init__.py @@ -0,0 +1,2 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# This file is required for the test-system to find the tests in the exam. diff --git a/irlc/exam/exam2023spring/__init__.py b/irlc/exam/exam2023spring/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/exam/exam2023spring/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/exam/exam2023spring/readme.md b/irlc/exam/exam2023spring/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..c041c5216f5a12754b844c5d316e1d0e835ec09a --- /dev/null +++ b/irlc/exam/exam2023spring/readme.md @@ -0,0 +1,2 @@ +This directory is purposefully left empty. During the exam, you will be given a `.zip` file with the content of this directory. +Replace this directory with the corresponding directory from the `.zip` file to begin working on the exam. diff --git a/irlc/exam/exam2023spring/solution/readme.md b/irlc/exam/exam2023spring/solution/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..8d673296bd5e370246c88be8ff6eff946d4ce4f1 --- /dev/null +++ b/irlc/exam/exam2023spring/solution/readme.md @@ -0,0 +1 @@ +I will make the solution to the exam available in this directory. diff --git a/irlc/exam/exam2024spring/__init__.py b/irlc/exam/exam2024spring/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/exam/exam2024spring/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/exam/exam2024spring/readme.md b/irlc/exam/exam2024spring/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..c041c5216f5a12754b844c5d316e1d0e835ec09a --- /dev/null +++ b/irlc/exam/exam2024spring/readme.md @@ -0,0 +1,2 @@ +This directory is purposefully left empty. During the exam, you will be given a `.zip` file with the content of this directory. +Replace this directory with the corresponding directory from the `.zip` file to begin working on the exam. diff --git a/irlc/exam/midterm2023a/__init__.py b/irlc/exam/midterm2023a/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/exam/midterm2023a/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/exam/midterm2023a/readme.md b/irlc/exam/midterm2023a/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..c041c5216f5a12754b844c5d316e1d0e835ec09a --- /dev/null +++ b/irlc/exam/midterm2023a/readme.md @@ -0,0 +1,2 @@ +This directory is purposefully left empty. During the exam, you will be given a `.zip` file with the content of this directory. +Replace this directory with the corresponding directory from the `.zip` file to begin working on the exam. diff --git a/irlc/exam/midterm2023a/solution/readme.md b/irlc/exam/midterm2023a/solution/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..8d673296bd5e370246c88be8ff6eff946d4ce4f1 --- /dev/null +++ b/irlc/exam/midterm2023a/solution/readme.md @@ -0,0 +1 @@ +I will make the solution to the exam available in this directory. diff --git a/irlc/exam/midterm2023b/__init__.py b/irlc/exam/midterm2023b/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/exam/midterm2023b/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/exam/midterm2023b/readme.md b/irlc/exam/midterm2023b/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..c041c5216f5a12754b844c5d316e1d0e835ec09a --- /dev/null +++ b/irlc/exam/midterm2023b/readme.md @@ -0,0 +1,2 @@ +This directory is purposefully left empty. During the exam, you will be given a `.zip` file with the content of this directory. +Replace this directory with the corresponding directory from the `.zip` file to begin working on the exam. diff --git a/irlc/exam/midterm2023b/solution/readme.md b/irlc/exam/midterm2023b/solution/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..8d673296bd5e370246c88be8ff6eff946d4ce4f1 --- /dev/null +++ b/irlc/exam/midterm2023b/solution/readme.md @@ -0,0 +1 @@ +I will make the solution to the exam available in this directory. diff --git a/irlc/exam/readme.md b/irlc/exam/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..c189b31a4b85f0ec947ad1761de0e72448513e77 --- /dev/null +++ b/irlc/exam/readme.md @@ -0,0 +1,15 @@ +# Folder for the exam and midterms + +Before the exam: + - Ensure that the `irlc`-code generally works (you can run exercises, the packages we use such as `gymnasium` or `numpy` are installed, etc.) + - You have no problem running the various `unitgrade`-test scripts and generating `.token`-files + +During the exam: + - Download a `.zip` file with the code from the digital exam + - For the midterm, you can find the file on DTU Learn + - The `zip` file will contain the toolbox code including solutions. It will also contain a directory: + ```bash + irlc/exam/exam2024spring + ``` + - This directory contains the code you need to work on for the exam. Replace the directory on your local computer with this directory and you should be all set up + - The `.zip` file will also contain solutions to nearly all exercises. Use these if benefits you. diff --git a/irlc/exam_tabular_examples/__init__.py b/irlc/exam_tabular_examples/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/exam_tabular_examples/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/exam_tabular_examples/helper.py b/irlc/exam_tabular_examples/helper.py new file mode 100644 index 0000000000000000000000000000000000000000..4fd09f2f9245aaa0724a2e5720223f8809600edf --- /dev/null +++ b/irlc/exam_tabular_examples/helper.py @@ -0,0 +1,11 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc import interactive, train + + +def keyboard_play_value(env, agent, method_label='MC', q=False): + env, agent = interactive(env, agent) + agent.label = method_label + agent.label = 'MC (first visit)' + env.view_mode = 1 # Set value-function view-mode. + train(env, agent, num_episodes=100) + env.close() diff --git a/irlc/exam_tabular_examples/lecture_10_mc_value_every.py b/irlc/exam_tabular_examples/lecture_10_mc_value_every.py new file mode 100644 index 0000000000000000000000000000000000000000..59fdeb19c65633d776e9afec57093347de720555 --- /dev/null +++ b/irlc/exam_tabular_examples/lecture_10_mc_value_every.py @@ -0,0 +1,9 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.exam_tabular_examples.helper import keyboard_play_value +from irlc.gridworld.gridworld_environments import BookGridEnvironment +from irlc.ex10.mc_evaluate import MCEvaluationAgent + +if __name__ == "__main__": + env = BookGridEnvironment(view_mode=1, render_mode='human') + agent = MCEvaluationAgent(env, gamma=.9, alpha=0.4, first_visit=False) + keyboard_play_value(env,agent,method_label='MC every') diff --git a/irlc/exam_tabular_examples/lecture_10_mc_value_first.py b/irlc/exam_tabular_examples/lecture_10_mc_value_first.py new file mode 100644 index 0000000000000000000000000000000000000000..0c444523c2f8b6d3345e6c6085fbba1ae87a6290 --- /dev/null +++ b/irlc/exam_tabular_examples/lecture_10_mc_value_first.py @@ -0,0 +1,13 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.gridworld.gridworld_environments import BookGridEnvironment +from irlc.ex10.mc_evaluate import MCEvaluationAgent +from irlc import interactive, train + +if __name__ == "__main__": + env = BookGridEnvironment(view_mode=1, render_mode='human') + agent = MCEvaluationAgent(env, gamma=.9, alpha=0.4) + agent.label = 'MC (first visit)' + env, agent = interactive(env, agent) + env.view_mode = 1 # Automatically set value-function view-mode. + train(env, agent, num_episodes=100) + env.close() diff --git a/irlc/exam_tabular_examples/sarsa_lambda_delay.py b/irlc/exam_tabular_examples/sarsa_lambda_delay.py new file mode 100644 index 0000000000000000000000000000000000000000..de3107f39d2ab6e18281ca091b9c1f45307677e3 --- /dev/null +++ b/irlc/exam_tabular_examples/sarsa_lambda_delay.py @@ -0,0 +1,45 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from collections import defaultdict +from irlc.ex11.q_agent import QAgent + +class SarsaLambdaDelayAgent(QAgent): + def __init__(self, env, gamma=0.99, epsilon=0.1, alpha=0.5, lamb=0.9): + super().__init__(env, gamma=gamma, alpha=alpha, epsilon=epsilon) + self.lamb = lamb + self.method = 'Sarsa(Lambda)' + self.e = defaultdict(float) + + def pi(self, s, k, info=None): + self.t = k + action = self.pi_eps(s,info=info) + return action + + def lmb_update(self, s, a, r, sp, ap, done): + delta = r + self.gamma * (self.Q[sp,ap] if not done else 0) - self.Q[s,a] + for (s,a), ee in self.e.items(): + self.Q[s,a] += self.alpha * delta * ee + self.e[(s,a)] = self.gamma * self.lamb * ee + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + # if self.t == 0: + # self.e.clear() + + if self.t > 0: + # We have an update in the buffer and can update the states. + self.lmb_update(self.s_prev, self.a_prev, self.r_prev, s, a, done=False) + self.e[(s, a)] += 1 + + if done: + self.lmb_update(s, a, r, sp, ap=None, done=True) + self.e.clear() + + self.s_prev = s + self.a_prev = a + self.r_prev = r + + def __str__(self): + return f"SarsaLambdaDelay_{self.gamma}_{self.epsilon}_{self.alpha}_{self.lamb}" + +if __name__ == "__main__": + from irlc.ex12.sarsa_lambda_open import keyboard_play + keyboard_play(SarsaLambdaDelayAgent, method_label="Sarsa(Lambda) (delayed)") diff --git a/irlc/exam_tabular_examples/sarsa_nstep_delay.py b/irlc/exam_tabular_examples/sarsa_nstep_delay.py new file mode 100644 index 0000000000000000000000000000000000000000..32f2aad7a9e4656e81f6c4bab6be8107b4cba005 --- /dev/null +++ b/irlc/exam_tabular_examples/sarsa_nstep_delay.py @@ -0,0 +1,77 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +import gymnasium as gym +from irlc import main_plot +import matplotlib.pyplot as plt +from irlc.gridworld.gridworld_environments import OpenGridEnvironment +from irlc import train +from irlc.ex11.q_agent import QAgent + +class SarsaDelayNAgent(QAgent): + """ Implement the N-step semi-gradient sarsa agent from (SB18, Section 7.2)""" + def __init__(self, env, gamma=1, alpha=0.2, epsilon=0.1, n=1): + # Variables for TD-n + self.method = 'Sarsa' if n == 1 else f'Sarsa({n=})' + + self.n = n # as in n-step sarse + # Buffer lists for previous (S_t, R_{t}, A_t) triplets + self.R, self.S, self.A = [None] * (self.n + 1), [None] * (self.n + 1), [None] * (self.n + 1) + super().__init__(env, gamma=gamma, alpha=alpha, epsilon=epsilon) + + def pi(self, s, k, info=None): + self.t = k # Save current step in episode for use in train. + return self.pi_eps(s, info) + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + # Recall we are given S_t, A_t, R_{t+1}, S_{t+1} and done is whether t=T+1. + n, t = self.n, self.t + # Store current observations in buffer. + self.S[t%(n+1)] = s + self.A[t%(n+1)] = a # self.pi_eps(sp) if not done else -1 + self.R[(t+1)%(n+1)] = r + if done: + T = t+1 + tau_steps_to_train = range(t - n, T) + else: + T = 1e10 + tau_steps_to_train = [t - n ] if t > 0 else [] + + # Tau represent the current tau-steps which are to be updated. The notation is compatible with that in Sutton. + for tau in tau_steps_to_train: + if tau >= 0: + """ + Compute the return for this tau-step and perform the relevant Q-update. + The first step is to compute the expected return G in the below section. + """ + G = sum([self.gamma**(i-tau-1)*self.R[i%(n+1)] for i in range(tau+1, min(tau+n, T)+1)]) + S_tau_n, A_tau_n = self.S[(tau+n)%(n+1)], self.A[(tau+n)%(n+1)] + if tau+n < T: + G += self.gamma**n * self._q(S_tau_n, A_tau_n) + S_tau, A_tau = self.S[tau%(n+1)], self.A[tau%(n+1)] + delta = G - self._q(S_tau, A_tau) + + if n == 1: # Check your implementation is correct when n=1 by comparing it with regular Sarsa learning. + delta_Sarsa = (self.R[ (tau+1)%(n+1) ] + (0 if tau+n==T else self.gamma * self._q(S_tau_n,A_tau_n)) - self._q(S_tau,A_tau)) + if abs(delta-delta_Sarsa) > 1e-10: + raise Exception("n=1 agreement with Sarsa learning failed. You have at least one bug!") + self._upd_q(S_tau, A_tau, delta) + + def _q(self, s, a): return self.Q[s,a] # Using these helper methods will come in handy when we work with function approximators, but it is optional. + def _upd_q(self, s, a, delta): self.Q[s,a] += self.alpha * delta + + def __str__(self): + return f"SarsaN_{self.gamma}_{self.epsilon}_{self.alpha}_{self.n}" + +from irlc.ex11.nstep_sarsa_agent import SarsaNAgent +from irlc.lectures.lec11.lecture_10_sarsa_open import open_play +if __name__ == "__main__": + n = 8 + env = OpenGridEnvironment() + agent = SarsaDelayNAgent(env, n=n) + train(env, agent, num_episodes=100) + + open_play(SarsaDelayNAgent, method_label=f"Sarsa n={n}", n=n) diff --git a/irlc/exam_tabular_examples/tabular_examples.py b/irlc/exam_tabular_examples/tabular_examples.py new file mode 100644 index 0000000000000000000000000000000000000000..f9932a592ec8a8552a17c2dc7f4c2e899e4f8d3a --- /dev/null +++ b/irlc/exam_tabular_examples/tabular_examples.py @@ -0,0 +1,78 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex12.sarsa_lambda_agent import SarsaLambdaAgent +from irlc.gridworld.gridworld_environments import OpenGridEnvironment, BookGridEnvironment, SuttonCornerGridEnvironment, SuttonMazeEnvironment +from irlc import train, savepdf +from irlc.ex12.sarsa_lambda_open import keyboard_play +import matplotlib.pyplot as plt +from irlc.ex11.q_agent import QAgent +from irlc.ex11.sarsa_agent import SarsaAgent +from irlc.ex11.nstep_sarsa_agent import SarsaNAgent +from irlc.ex10.mc_agent import MCAgent +from irlc.ex10.mc_evaluate import MCEvaluationAgent +from irlc.exam_tabular_examples.sarsa_nstep_delay import SarsaDelayNAgent +from irlc.exam_tabular_examples.sarsa_lambda_delay import SarsaLambdaDelayAgent +from irlc import interactive + +def open_snapshop(Agent, method_label="Unknown method", file_name=None, alpha=0.5, autoplay=False, **kwargs): + env = OpenGridEnvironment(render_mode='human') + agent = Agent(env, gamma=0.99, epsilon=0.1, alpha=alpha, **kwargs) + agent.label =method_label + print("Running", agent) + env, agent = interactive(env, agent, autoplay=autoplay) + train(env, agent, num_episodes=3) + if file_name is not None: + env.plot() + plt.title(method_label) + savepdf("exam_tabular_"+file_name) + env.close() + +if __name__ == "__main__": + """ All simulations run using gamma=0.99, epsilon=0.1 and alpha=0.5 (when applicable). """ + + """ The following lines will show all the agents using automatic play. It is used to generate screenshots. + Uncomment to go to interactive play. """ + # import numpy as np + # np.random.seed(42) + # env = SuttonMazeEnvironment(living_reward=-2, render_mode='human') + # agent = MCAgent(env, alpha=0.8, epsilon=0, gamma=0.4) + # env, agent = interactive(env, agent) + # train(env, agent, num_episodes=2) + + open_snapshop(MCAgent, "Monte-Carlo control (first visit)", file_name="mc_first", alpha=None) + open_snapshop(MCAgent, "Monte-Carlo control (every visit)", file_name="mc_every", alpha=None, first_visit=False) + open_snapshop(SarsaAgent, "Sarsa", file_name="sarsa") + open_snapshop(SarsaNAgent, "n-step Sarsa (n=8)", file_name="sarsa_n8", n=8) + open_snapshop(QAgent, "Q-learning", file_name="q_learning") + open_snapshop(SarsaLambdaAgent, "Sarsa(Lambda)", file_name="sarsa_lambda") + open_snapshop(MCEvaluationAgent, "Monte-Carlo value-estimation (first visit)", file_name="mc_evaluation_first", alpha=None) + open_snapshop(MCEvaluationAgent, "Monte-Carlo value-estimation (every visit)", file_name="mc_evaluation_every", first_visit=False) + + """ MC-methods for value estimation. This is the upgraded demo which also shows the number of times + a state has been visited in the value-estimation algorithm. """ + keyboard_play(MCEvaluationAgent, "Monte-Carlo value-estimation (first visit)", alpha=None) + keyboard_play(MCEvaluationAgent, "Monte-Carlo value-estimation (every visit)", alpha=None, first_visit=False) + + """ Control methods: + Play with the agents (using keyboard input) """ + keyboard_play(MCAgent, "Monte-Carlo control (first visit)", alpha=None) + keyboard_play(MCAgent, "Monte-Carlo control (every visit)", alpha=None, first_visit=False) + keyboard_play(QAgent, "Q-learning") + + """ These agents also accept keyboard input, but they are not guaranteed to update the Q-values correctly because the next state A' (in Suttons notation) + is generated in the train() method; i.e. we cannot easily overwrite it using the keyboard. I have included them for completeness, but + be a little careful with them. """ + keyboard_play(SarsaAgent, "Sarsa") + keyboard_play(SarsaNAgent, "n-step Sarsa (n=8)", n=8) + keyboard_play(SarsaLambdaAgent, "Sarsa(Lambda)") + + """ Bonus keyboard input agents: These agents implement the same methods as their counterparts above, however they 'wait' with updating + Q(S_t, A_t) until time t+1 when the (actual) next action A_{t+1} is available. This means that when they are used in conjunction with keyboard inputs, + the Q-values will be updated correctly since we can actually set A_{t+1} equal to the keyboard input. + This also mean the updates to the Q-values appear to lag one step behind the methods above. + I have included them in the case some find it useful to test the Q-values using the keyboard, + however, the implementations/delay-idea is not part of the exam pensum: only use them if you find them useful for studying, and otherwise just rely on the + description of the methods in the lecture material. + """ + keyboard_play(SarsaDelayNAgent, "Sarsa (delayed)", n=1) # We use that Sarsa is equal to n-step sarsa with n=1. + keyboard_play(SarsaDelayNAgent, "n-step Sarsa (n=8, delayed)", n=8) + keyboard_play(SarsaLambdaDelayAgent, "Sarsa(Lambda) (delayed)") diff --git a/irlc/gridworld/__init__.py b/irlc/gridworld/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/gridworld/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/gridworld/__pycache__/__init__.cpython-311.pyc b/irlc/gridworld/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..51fdb1aad155ed02d6e2e424253ad9f79f46fc88 Binary files /dev/null and b/irlc/gridworld/__pycache__/__init__.cpython-311.pyc differ diff --git a/irlc/gridworld/__pycache__/gridworld_environments.cpython-311.pyc b/irlc/gridworld/__pycache__/gridworld_environments.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..df934112e5c0902a93095143b4af40c837a6e60b Binary files /dev/null and b/irlc/gridworld/__pycache__/gridworld_environments.cpython-311.pyc differ diff --git a/irlc/gridworld/__pycache__/gridworld_graphics_display.cpython-311.pyc b/irlc/gridworld/__pycache__/gridworld_graphics_display.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e17825632aa1704e500dad60aa5ca905581874d Binary files /dev/null and b/irlc/gridworld/__pycache__/gridworld_graphics_display.cpython-311.pyc differ diff --git a/irlc/gridworld/__pycache__/gridworld_mdp.cpython-311.pyc b/irlc/gridworld/__pycache__/gridworld_mdp.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..402d55ea6f792f614d4f20b2f76dd75b30781609 Binary files /dev/null and b/irlc/gridworld/__pycache__/gridworld_mdp.cpython-311.pyc differ diff --git a/irlc/gridworld/demo_agents/__init__.py b/irlc/gridworld/demo_agents/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/gridworld/demo_agents/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/gridworld/demo_agents/hidden_agents.py b/irlc/gridworld/demo_agents/hidden_agents.py new file mode 100644 index 0000000000000000000000000000000000000000..d831b118fac4c28ab77e3f44457c63ca727fd671 --- /dev/null +++ b/irlc/gridworld/demo_agents/hidden_agents.py @@ -0,0 +1,235 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from collections import defaultdict +import numpy as np +from irlc import TabularAgent # , PlayWrapper, VideoMonitor, train +from irlc.ex09.mdp_warmup import value_function2q_function + + +class ValueIterationAgent2(TabularAgent): + def __init__(self, env, gamma=.99, epsilon=0, theta=1e-5, only_current_state=False): + self.v = defaultdict(lambda: 0) + self.steps = 0 + self.mdp = env.mdp + self.only_current_state = only_current_state + super().__init__(env, gamma, epsilon=epsilon) + + def pi(self, s, k, info=None): + # TODO: 2 lines missing. + raise NotImplementedError("Implement function body") + return self.random_pi(s) if np.random.rand() < self.epsilon else a + + @property + def label(self): + label = f"Value iteration after {self.steps} steps" + return label + + def v2Q(self, s): # used for rendering right now + return value_function2q_function(self.mdp, s, self.gamma, self.v) + + def train(self, s, a, r, sp, done=False, info_sp=None): + delta = 0 + v2 = {} + for s in self.env.P.keys(): + v, v2[s] = self.v[s], max(value_function2q_function(self.mdp, s, self.gamma, self.v).values()) if len(self.mdp.A(s)) > 0 else 0 + delta = max(delta, np.abs(v - self.v[s])) + + self.v = v2 + + for s in self.mdp.nonterminal_states: + for a in self.mdp.A(s): + self.Q[s,a] = self.v2Q(s)[a] + + self.delta = delta + self.steps += 1 + + def __str__(self): + return f"VIAgent_{self.gamma}" + + +class PolicyEvaluationAgent2(TabularAgent): + def __init__(self, env, mdp=None, gamma=0.99, steps_between_policy_improvement=10, only_update_current=False): + if mdp is None: + mdp = env.mdp + self.mdp = mdp + self.v = defaultdict(lambda: 0) + self.imp_steps = 0 + self.steps_between_policy_improvement = steps_between_policy_improvement + self.steps = 0 + self.policy = {} + self.only_update_current = only_update_current + for s in mdp.nonterminal_states: + self.policy[s] = {} + for a in mdp.A(s): + self.policy[s][a] = 1/len(mdp.A(s)) + super().__init__(env, gamma) + + + def pi(self, s,k, info=None): + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + return np.random.choice(a, p=pa) + + def v2Q(self, s): # used for rendering right now + return value_function2q_function(self.mdp, s, self.gamma, self.v) + + @property + def label(self): + if self.steps_between_policy_improvement is None: + label = f"Policy evaluation after {self.steps} steps" + else: + dd = self.steps % self.steps_between_policy_improvement == 0 + # print(dd) + label = f"PI after {self.steps} steps/{self.imp_steps-dd} policy improvements" + return label + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + if not self.only_update_current: + v2 = {} + for s in self.mdp.nonterminal_states: + q = value_function2q_function(self.mdp, s, self.gamma, self.v) + if len(q) == 0: + v2[s] = 0 + else: + v2[s] = sum( [qv * self.policy[s][a] for a, qv in q.items()] ) + + + for s in self.mdp.nonterminal_states: + for a,q in self.v2Q(s).items(): + self.Q[s,a] = q + + for k, v in v2.items(): + self.v[k] = v2[k] + + else: + # Only update Q-value in current state: + Q_ = 0 + # print(a) + + for (sp, r), p in self.mdp.Psr(s, a).items(): + Q_ += p*(r + (0 if self.mdp.is_terminal(sp) else sum([self.Q[sp, ap]*pa for ap, pa in self.policy[sp].items()]) )) + + # Q_ += p * (r + (0 if self.mdp.is_terminal(sp) else sum( + # [self.Q[sp, ap] * pa for ap, pa in self.policy[sp].items()]))) + + + self.Q[s, a] = Q_ + + v_ = 0 + for a in self.mdp.A(s): + for (sp, r), p in self.mdp.Psr(s, a).items(): + v_ += self.policy[s][a] * (self.v[sp] * self.gamma + r)*p + self.v[s] = v_ + + + if self.steps_between_policy_improvement is not None and (self.steps+1) % self.steps_between_policy_improvement == 0: + self.policy = {} + for s in self.mdp.nonterminal_states: + q = value_function2q_function(self.mdp, s, self.gamma, self.v) + if len(q) == 0: + continue + a_ = max(q, key=q.get) # optimal action + self.policy[s] = {} + for a in self.mdp.A(s): + self.policy[s][a] = 1 if q[a] == max(q.values()) else 0 #if a == a_ else 0 + + n = sum(self.policy[s].values()) + for a in self.policy[s]: + self.policy[s][a] *= 1/n + + self.imp_steps += 1 + self.steps += 1 + + def __str__(self): + return f"PIAgent_{self.gamma}" + + + +class ValueIterationAgent3(TabularAgent): + def __init__(self, env, mdp=None, epsilon=0, gamma=0.99, steps_between_policy_improvement=10, only_update_current=False): + if mdp is None: + mdp = env.mdp + self.mdp = mdp + self.v = defaultdict(lambda: 0) + self.imp_steps = 0 + self.steps_between_policy_improvement = steps_between_policy_improvement + self.steps = 0 + self.policy = {} + self.only_update_current = only_update_current + self.v = defaultdict(float) + for s in mdp.nonterminal_states: + self.policy[s] = {} + for a in mdp.A(s): + self.policy[s][a] = 1/len(mdp.A(s)) + super().__init__(env, gamma, epsilon=epsilon) + + + def pi(self, s,k, info=None): + from irlc import Agent + if np.random.rand() <self.epsilon: + return Agent.pi(self, s, k=k, info=info) + + a, pa = zip(*self.policy[s].items()) + return np.random.choice(a, p=pa) + + + def v2Q(self, s): # used for rendering right now + if not self.only_update_current: + a,q = self.Q.get_Qs(s) + return {a_: q_ for a_, q_ in zip(a,q)} + else: + return value_function2q_function(self.mdp, s, self.gamma, self.v) + + + def vi_q(self, s, a): + Q_ = 0 + for (sp, r), p in self.mdp.Psr(s, a).items(): + if self.mdp.is_terminal(sp): + QT = 0 + else: + qvals = [self.Q[sp, a_] for a_ in self.mdp.A(sp)] + QT = max(qvals) * (1-self.epsilon) + self.epsilon*np.mean(qvals) + Q_ += p * (r + self.gamma * QT) + return Q_ + + @property + def label(self): + label = f"Value Iteration after {self.steps} steps" + return label + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + s_ = s + if not self.only_update_current: + q_ = dict() + for s in self.mdp.nonterminal_states: + for a in self.mdp.A(s): + q_[s,a] = self.vi_q(s, a) + for (s,a), q in q_.items(): + self.Q[s,a] = q + else: + # Only update Q-value in current state: + # s = s_ + qq = value_function2q_function(self.mdp, s, self.gamma, self.v) + self.v[s] = max(qq.values()) + self.Q[s, a] = self.vi_q(s,a) + + + for s in self.mdp.nonterminal_states: + # q = qs_(self.mdp, s, self.gamma, self.v) + # if len(q) == 0: + # continue + # a_ = max(q, key=q.get) # optimal action + self.policy[s] = {} + qs = [self.Q[s,a] for a in self.mdp.A(s)] + + for a in self.mdp.A(s): + self.policy[s][a] = 1 if self.Q[s,a] >= max(qs)-1e-6 else 0 #if a == a_ else 0 + S = sum(self.policy[s].values()) + for a in self.mdp.A(s): + self.policy[s][a] = self.policy[s][a] / S + if not self.only_update_current: + self.v[s] = max([self.Q[s, a_] for a_ in self.mdp.A(s)]) + + self.steps += 1 + + def __str__(self): + return f"PIAgent_{self.gamma}" diff --git a/irlc/gridworld/gridworld_environments.py b/irlc/gridworld/gridworld_environments.py new file mode 100644 index 0000000000000000000000000000000000000000..d58b21b5c7546ff1281aa1905b00b3e65567fb9c --- /dev/null +++ b/irlc/gridworld/gridworld_environments.py @@ -0,0 +1,362 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +import numpy as np +from collections import defaultdict +from gymnasium.envs.toy_text.frozen_lake import FrozenLakeEnv +from gymnasium.spaces.discrete import Discrete +from irlc.ex09.mdp import MDP2GymEnv +from irlc.gridworld.gridworld_mdp import GridworldMDP, FrozenGridMDP +from irlc import Timer +from gymnasium.spaces.multi_discrete import MultiDiscrete +import pygame + +grid_cliff_grid = [[' ',' ',' ',' ',' ', ' ',' ',' ',' ',' ', ' '], + [' ',' ',' ',' ',' ', ' ',' ',' ',' ',' ', ' '], + ['S',-100, -100, -100, -100,-100, -100, -100, -100, -100, 0]] + +grid_cliff_grid2 = [[' ',' ',' ',' ',' '], + ['S',' ',' ',' ',' '], + [-100,-100, -100, -100, 0]] + +grid_discount_grid = [[' ',' ',' ',' ',' '], + [' ','#',' ',' ',' '], + [' ','#', 1,'#', 10], + ['S',' ',' ',' ',' '], + [-10,-10, -10, -10, -10]] + +grid_bridge_grid = [[ '#',-100, -100, -100, -100, -100, '#'], + [ 1, 'S', ' ', ' ', ' ', ' ', 10], + [ '#',-100, -100, -100, -100, -100, '#']] + +grid_book_grid = [[' ',' ',' ',+1], + [' ','#',' ',-1], + ['S',' ',' ',' ']] + +grid_maze_grid = [[' ',' ',' ', +1], + ['#','#',' ','#'], + [' ','#',' ',' '], + [' ','#','#',' '], + ['S',' ',' ',' ']] + +sutton_corner_maze = [[ 1, ' ', ' ', ' '], + [' ', ' ', ' ', ' '], + [' ', 'S', ' ', ' '], + [' ', ' ', ' ', 1]] + +# A big yafcport open maze. +grid_open_grid = [[' ']*8 for _ in range(5)] +grid_open_grid[0][0] = 'S' +grid_open_grid[-1][-1] = 1 + + +class GridworldEnvironment(MDP2GymEnv): + metadata = { + 'render.modes': ['human', 'rgb_array'], + 'video.frames_per_second': 1000, + } + def get_keys_to_action(self): + return {(pygame.K_LEFT,): GridworldMDP.WEST, (pygame.K_RIGHT,): GridworldMDP.EAST, + (pygame.K_UP,): GridworldMDP.NORTH, (pygame.K_DOWN,): GridworldMDP.SOUTH} + + # return {(key.LEFT,): GridworldMDP.WEST, (key.RIGHT,): GridworldMDP.EAST, (key.UP,): GridworldMDP.NORTH, (key.DOWN,): GridworldMDP.SOUTH} + + def _get_mdp(self, grid, uniform_initial_state=False): + return GridworldMDP(grid, living_reward=self.living_reward) + + def __init__(self, grid=None, uniform_initial_state=True, living_reward=0,zoom=1, view_mode=0, render_mode=None, print_states=False, + frames_per_second=None, + **kwargs): + self.print_states = print_states + self.living_reward = living_reward + mdp = self._get_mdp(grid) + self.render_mode = render_mode + super().__init__(mdp, render_mode=render_mode) + self.action_space = Discrete(4) + # self.observation_space = MultiDiscrete([mdp.height, mdp.width]) # N.b. the state space does not contain the terminal state. + self.render_episodes = 0 + self.render_steps = 0 + self.timer = Timer() + self.view_mode = view_mode + self.agent = None # If this is set, the environment will try to render the internal state of the agent. + # It is a little hacky, it allows us to make the visualizations etc. + # Set up rendering if required. + self.display_pygame = None + # self.screen = None + self.zoom = zoom # Save zoom level. + self.total_reward = 0 + self.frames_per_second = frames_per_second + def _step(*args, **kwargs): + s = self.state + o = type(self).step(self, *args, **kwargs) + done = o[2] + a = args[0] + self.total_reward += o[1] + self.render_steps += 1 + self.render_episodes += done + if self.print_states: + if isinstance(self, FrozenLake): + pr = f" This occurred with probability: P(s', r | s, a) = {self.mdp.Psr(s, a)[(o[0], o[1])]:.2f}." + else: + pr = "" + if done: + pt = f" Total reward for this episode was {self.total_reward}." + else: + pt = "" + print(f"s={s}, a={a} --> s'={o[0]}, r={o[1]}. {pr}{pt}") + return o + self.step = _step + + def reset(self, *args, **kwargs): + o = super().reset(*args, **kwargs) + self.total_reward = 0 + if self.print_states: + print(f"Starting in state s={o[0]}") + return o + + def keypress(self, key): + if key.unicode == 'm': + # changing mode... + self.view_mode += 1 + self.render() + return + + if key == 116: # This may easily not be used. + self.view_mode += 1 + self.render() + + + def render(self): + if self.display_pygame is None: + from irlc.gridworld.gridworld_graphics_display import GraphicsGridworldDisplay + self.display_pygame = GraphicsGridworldDisplay(self.mdp, size=int(150 * self.zoom), frames_per_second=self.frames_per_second) # last item is grid size + + agent = self.agent + label = None + method_label = agent.method if hasattr(agent, 'method') else '' + if label is None and len(method_label) > 0: + label = f"{method_label} AFTER {self.render_steps} STEPS" + + state = self.state + avail_modes = [] + if agent != None: + label = (agent.label if hasattr(agent, 'label') else label if label is not None else '') #if label is None else label + v = agent.v if hasattr(agent, 'v') else None + Q = agent.Q if hasattr(agent, 'Q') else None + # policy = agent.policy if hasattr(agent, 'policy') else None + v2Q = agent.v2Q if hasattr(agent, 'v2Q') else None + avail_modes = [] + if Q is not None: + avail_modes.append("Q") + avail_modes.append("v") + elif v is not None: + avail_modes.append("v") + + if len(avail_modes) > 0: + self.view_mode = self.view_mode % len(avail_modes) + if avail_modes[self.view_mode] == 'v': + preferred_actions = None + + if v == None: + preferred_actions = {} + v = {s: max(Q.get_Qs(s)[1]) for s in self.mdp.nonterminal_states} + + for s in self.mdp.nonterminal_states: + acts, values = Q.get_Qs(s) + preferred_actions[s] = [a for (a,w) in zip(acts, values) if np.round(w, 2) == np.round(v[s], 2)] + + if v2Q is not None: + preferred_actions = {} + for s in self.mdp.nonterminal_states: + q = v2Q(s) + mv = np.round( max( q.values() ), 2) + preferred_actions[s] = [k for k, v in q.items() if np.round(v, 2) == mv] + + if agent != None and hasattr(agent, 'policy') and agent.policy is not None and state in agent.policy and isinstance(agent.policy[state], dict): + for s in self.mdp.nonterminal_states: + preferred_actions[s] = [a for a, v in agent.policy[s].items() if v == max(agent.policy[s].values()) ] + + if hasattr(agent, 'returns_count_N'): + returns_count = agent.returns_count_N + else: + returns_count = None + if hasattr(agent, 'returns_sum_S'): + returns_sum = agent.returns_sum_S + else: + returns_sum = None + + self.display_pygame.displayValues(mdp=self.mdp, v=v, preferred_actions=preferred_actions, currentState=state, message=label, returns_count=returns_count, returns_sum=returns_sum) + + elif avail_modes[self.view_mode] == 'Q': + + if hasattr(agent, 'e') and isinstance(agent.e, defaultdict): + eligibility_trace = defaultdict(float) + for k, v in agent.e.items(): + eligibility_trace[k] = v + + else: + eligibility_trace = None + + if hasattr(agent, 'returns_count_N'): + returns_count = agent.returns_count_N + elif hasattr(agent, 'returns_count'): + returns_count = agent.returns_count + else: + returns_count = None + if hasattr(agent, 'returns_sum_S'): + returns_sum = agent.returns_sum_S + elif hasattr(agent, 'returns_sum'): + returns_sum = agent.returns_sum + else: + returns_sum = None + + self.display_pygame.displayQValues(self.mdp, Q, currentState=state, message=label, eligibility_trace=eligibility_trace, returns_count=returns_count, returns_sum=returns_sum) + else: + raise Exception("No view mode selected") + else: + # self.pygame_display = Gridworl + self.display_pygame.displayNullValues(self.mdp, currentState=state, message=label) + # self.display.displayNullValues(self.mdp, currentState=state) + + render_out2 = self.display_pygame.blit(render_mode=self.render_mode) + return render_out2 + + def close(self): + # print("Closing time...") + if self.display_pygame is not None: + self.display_pygame.close() + + +class BookGridEnvironment(GridworldEnvironment): + def __init__(self, *args, **kwargs): + super().__init__(grid_book_grid, *args, **kwargs) + +class BridgeGridEnvironment(GridworldEnvironment): + def __init__(self, *args, **kwargs): + super().__init__(grid_bridge_grid, *args, **kwargs) + +class CliffGridEnvironment(GridworldEnvironment): + def __init__(self, *args, **kwargs): + super().__init__(grid_cliff_grid, living_reward=-1, *args, **kwargs) + +class CliffGridEnvironment2(GridworldEnvironment): + def __init__(self, *args, **kwargs): + super().__init__(grid_cliff_grid2, living_reward=-1, *args, **kwargs) + + +class OpenGridEnvironment(GridworldEnvironment): + def __init__(self, *args, **kwargs): + super().__init__(grid_open_grid, *args, **kwargs) + +""" +Implement Suttons little corner-maze environment (see (SB18, Example 4.1)). +You can make an instance using: +> from irlc.gridworld.gridworld_environments import SuttonCornerGridEnvironment +> env = SuttonCornerGridEnvironment() +To get access the the mdp (as a MDP-class instance, for instance to see the states env.mdp.nonterminal_states) use +> env.mdp +""" +class SuttonCornerGridEnvironment(GridworldEnvironment): + def __init__(self, *args, living_reward=-1, **kwargs): # living_reward=-1 means the agent gets a reward of -1 per step. + super().__init__(sutton_corner_maze, *args, living_reward=living_reward, **kwargs) + +class SuttonMazeEnvironment(GridworldEnvironment): + def __init__(self, *args, render_mode=None, living_reward=0, **kwargs): + sutton_maze_grid = [[' ', ' ', ' ', ' ', ' ', ' ', ' ', '#', +1], + [' ', ' ', '#', ' ', ' ', ' ', ' ', '#', ' '], + ['S', ' ', '#', ' ', ' ', ' ', ' ', '#', ' '], + [' ', ' ', '#', ' ', ' ', ' ', ' ', ' ', ' '], + [' ', ' ', ' ', ' ', ' ', '#', ' ', ' ', ' '], + [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']] + + super().__init__(sutton_maze_grid, *args, render_mode=render_mode, living_reward=living_reward, **kwargs) + + + + +# "4x4":[ +# "SFFF", +# "FHFH", +# "FFFH", +# "HFFG" +# ] + +# "8x8": [ +# "SFFFFFFF", +# "FFFFFFFF", +# "FFFHFFFF", +# "FFFFFHFF", +# "FFFHFFFF", +# "FHHFFFHF", +# "FHFFHFHF", +# "FFFHFFFG", +# ] +# frozen_lake_4 = [['S', ' ', ' ', ' '], +# [' ', 0, ' ', 0], +# [' ', ' ', ' ', 0], +# [ 0, ' ', ' ', +1]] + +grid_book_grid_ = [[' ',' ',' ',+1], + [' ','#',' ',-1], + ['S',' ',' ',' ']] + +frozen_lake_4 = [['S',' ',' ',' '], + [' ','#',' ',-1], + [ 0 , ' ', ' ', +1]] + +class FrozenLake(GridworldEnvironment): + def _get_mdp(self, grid, uniform_initial_state=False): + return FrozenGridMDP(grid, is_slippery=self.is_slippery, living_reward=self.living_reward) + + def __init__(self, is_slippery=True, living_reward=0, *args, **kwargs): + self.is_slippery = is_slippery + menv = FrozenLakeEnv(is_slippery=is_slippery) # Load frozen-lake game layout and convert to our format 'grid' + gym2grid = dict(F=' ', G=1, H=0) + grid = [[gym2grid.get(s.decode("ascii"), s.decode("ascii")) for s in l] for l in menv.desc.tolist()] + menv.close() + super().__init__(grid=grid, *args, living_reward=living_reward, **kwargs) + +if __name__ == "__main__": + import gym + # env = gym.make('CartPole-v1', render_mode="human") + # env.reset() + # + # a = 234 gym + # env = gym.make('CartPole-v1', render_mode="human") + # env.reset() + from irlc import interactive, Agent, train + from irlc.ex11.q_agent import QAgent + from irlc.ex11.sarsa_agent import SarsaAgent + # env = SuttonMazeEnvironment(render_mode="human", zoom=0.75) + # env = OpenGridEnvironment(render_mode='human', zoom=0.75) + # env = OpenGridEnvironment() + env = CliffGridEnvironment() + agent = QAgent(env) + # env, agent = interactive(env, QAgent(env)) + # stats, trajectories = train(env, agent, num_episodes=100, experiment_name='q_learning') + stats, trajectories = train(env, SarsaAgent(env), num_episodes=100, experiment_name='sarsa') + + from irlc import main_plot + main_plot(experiments=['q_learning', 'sarsa']) + from matplotlib import pyplot as plt + plt.show() + # from irlc import VideoMonitor, train, Agent, PlayWrapper + # agent = Agent(env) + env.reset() + env.close() + + # agent = PlayWrapper(agent, env) + # env = VideoMonitor(env) + # env = Video + + # a = 234 + # for r in range(100): + # import time + # env.reset() + # time.sleep(1) + # train(env, agent, 2000) + a = 234 + # env.step(0) diff --git a/irlc/gridworld/gridworld_graphics_display.py b/irlc/gridworld/gridworld_graphics_display.py new file mode 100644 index 0000000000000000000000000000000000000000..f8fda14053d55a2cc09aab97e650bc5edcb9d27a --- /dev/null +++ b/irlc/gridworld/gridworld_graphics_display.py @@ -0,0 +1,543 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# gridworld_graphics_display.py +# --------------------------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + +from irlc.utils.graphics_util_pygame import GraphicsUtilGym, formatColor +from irlc.pacman.pacman_graphics_display import PACMAN_OUTLINE_WIDTH, PACMAN_SCALE +from irlc.gridworld.gridworld_mdp import GridworldMDP +from collections import defaultdict +import math +import numpy as np + +# import sphinx_autorun + +BACKGROUND_COLOR = formatColor(0, 0, 0) +EDGE_COLOR = formatColor(1, 1, 1) +OBSTACLE_COLOR = formatColor(0.5, 0.5, 0.5) +TEXT_COLOR = formatColor(1, 1, 1) +MUTED_TEXT_COLOR = formatColor(0.7, 0.7, 0.7) +LOCATION_COLOR = formatColor(0, 0, 1) +RED_TEXT_COLOR = formatColor(.68, .93, 0.93) +from irlc.pacman.pacman_graphics_display import PACMAN_COLOR + +def getEndpoints(direction, position=(0, 0)): + x, y = position + pos = x - int(x) + y - int(y) + width = 30 + 80 * math.sin(math.pi * pos) + delta = width / 2 + if direction == 'West': + endpoints = (180 + delta, 180 - delta) + elif direction == 'North': + endpoints = (90 + delta, 90 - delta) + elif direction == 'South': + endpoints = (270 + delta, 270 - delta) + else: + endpoints = (0 + delta, 0 - delta) + return endpoints + + +class GraphicsGridworldDisplay: + time_since_last_update = 0 + key_queue = [] + def __init__(self, mdp, size=120, frames_per_second=None): + self.mdp = mdp + self.ga = GraphicsUtilGym() + self.Q_old = None + self.v_old = None + self.Null_old = None + title = "Gridworld Display" + self.GRID_SIZE = size + self.MARGIN = self.GRID_SIZE * 0.75 + screen_width = (mdp.width - 1) * self.GRID_SIZE + self.MARGIN * 2 + screen_height = (mdp.height - 0.5) * self.GRID_SIZE + self.MARGIN * 2 + self.ga.begin_graphics(screen_width, screen_height, BACKGROUND_COLOR, title=title, frames_per_second=frames_per_second) + self.annotations = [] + # function to refresh the window + + + def draw_annotation(self): + for a in self.annotations: + if a['type'] == 'text': + self.ga.text(f"adf", (a['x'], a['y']), a['color'], a['message'], "Courier", anchor='c', fontsize=a['fontsize'], bold=a['bold']) + + + def annotate_text(self, state, symbol='o', color=(200,50, 50), dx=0, dy=0, action=None, fontsize=30, bold=False): + x,y = self.to_screen(state) + x += int(dx * self.GRID_SIZE) + y += int(dy * self.GRID_SIZE) + if action is not None: + from irlc.gridworld.gridworld_mdp import GridworldMDP + dd = 0.2 + if action == GridworldMDP.NORTH: y -= int( dd * self.GRID_SIZE) + elif action == GridworldMDP.SOUTH: y += int( dd * self.GRID_SIZE) + elif action == GridworldMDP.EAST: x += int(dd * self.GRID_SIZE) + elif action == GridworldMDP.WEST: x -= int( dd * self.GRID_SIZE) + + self.annotations.append({'type': 'text', 'x': x, 'y': y, 'message': symbol, 'color': color, 'fontsize': fontsize, 'bold': bold}) + + + def close(self): + # Stop pygame and refresh thread + self.ga.close() + + def blit(self, render_mode=None): + return self.ga.blit(render_mode=render_mode) + # if render_mode == 'rgb_array': + # return np.transpose( + # np.array(pygame.surfarray.pixels3d(self.screen)), axes=(1, 0, 2) + # ) + # + # pass + + + + # def autorefresh(self, env, interval=0.1): + # raise Exception("What is this?") + # def task(env, interval): + # while True: + # env.render() + # time.sleep(interval) + # + # from threading import Thread + # thread = Thread(target=task, args=(env, interval)) + # thread.start() + + # def end_frame(self): + # self.ga.end_frame() + + def displayValues(self, mdp, v, preferred_actions=None, currentState=None, message='Agent Values', returns_count=None, returns_sum=None): + # if self.v_old == None: + # self.ga.gc.clear() + # self.v_old = {} + # else: + # pass + self.ga.draw_background() + m = [v[s] for s in mdp.nonterminal_states] + self.Q_old = None + grid = mdp.grid + minValue = min(m) + maxValue = max(m) + + for x in range(mdp.width): + for y in range(mdp.height): + name = f"V_{x}_{y}_" + state = (x, y) + gridType = grid[x, y] + isExit = str(gridType) != gridType + isCurrent = currentState == state + if gridType == '#': + self.drawSquare(name, x, y, 0, 0, 0, None, None, True, False, isCurrent) + else: + value = v[state] + value = np.round(value, 2) + valString = '%.2f' % value + if mdp.is_terminal(state): + all_actions = [] + else: + all_actions = mdp.A(state) + if preferred_actions != None: + all_actions = preferred_actions[state] + + returns_sum_ = returns_sum[state] if returns_sum is not None else None + returns_count_ = returns_count[state] if returns_count is not None else None + self.drawSquare(name, x, y, value, minValue, maxValue, valString, all_actions, False, isExit, isCurrent, + returns_sum=returns_sum_, returns_count=returns_count_) + + # print("Drawing...") + if isinstance(currentState, tuple): + # print("found pacman") + screen_x, screen_y = self.to_screen(currentState) + self.draw_player((screen_x, screen_y), 0.12 * self.GRID_SIZE) + # else: + # print("no instance found??") + + pos = self.to_screen(((mdp.width - 1.0) / 2.0, - 0.8)) + self.ga.text(f"v_text_", pos, TEXT_COLOR, message, "Courier", -32, "bold", "c") + self.draw_annotation() + + def displayNullValues(self, mdp, currentState=None, message=''): + self.ga.draw_background() + grid = mdp.grid + # self.blank() + for x in range(mdp.width): + for y in range(mdp.height): + state = (x, y) + gridType = grid[x,y] + isExit = str(gridType) != gridType + isCurrent = currentState == state + name = f"sq_{x}_{y}" + if gridType == '#': + self.drawSquare(name, x, y, 0, 0, 0, None, None, True, False, isCurrent) + else: + self.drawNullSquare(name, mdp.grid, x, y, False, isExit, isCurrent) + pos = self.to_screen(((mdp.width - 1.0) / 2.0, - 0.8)) + + if isinstance(currentState, tuple): + screen_x, screen_y = self.to_screen(currentState) + self.draw_player((screen_x, screen_y), 0.12 * self.GRID_SIZE) + else: + pass + # print("No player!") + # pos = self.to_screen(((mdp.width - 1.0) / 2.0, - 0.8)) + # self.ga.text("Q_values_text", pos, TEXT_COLOR, message, "Courier", -32, "bold", "c") + + self.ga.text("bottom_text", pos, TEXT_COLOR, message, "Courier", -32, "bold", "c") + self.draw_annotation() + + + def displayQValues(self, mdp, Q, currentState=None, message="Agent Q-Values", eligibility_trace=None, returns_count=None, returns_sum=None): + """ Eligibility trace is an optional dictionary-like object. """ + self.ga.draw_background() + if self.Q_old == None: + # self.ga.gc.clear() + + self.Q_old = {} + self.e_old = {} + else: + pass + # self.ga.gc.copy_all() + + self.v_old = None + self.Null_old = None + + m = [max(Q.get_Qs(s)[1]) for s in mdp.nonterminal_states] + mv = [min(Q.get_Qs(s)[1]) for s in mdp.nonterminal_states] + + minValue = min(mv) + maxValue = max(m) + for x in range(mdp.width): + for y in range(mdp.height): + state = (x, y) + if state not in mdp.nonterminal_states: + actions = [] + Qs = [] + else: + actions, Qs = Q.get_Qs((x, y)) + Qs = list(np.round(Qs, decimals=2)) + + # Q_same = False + if self.Q_old != None and Qs == self.Q_old.get((x, y), 0): + Q_same = True + else: + Q_same = False + Q_same = False + E_same = True + if eligibility_trace is not None: + es = [eligibility_trace[state, a] for a in actions] + if state in self.e_old and self.e_old[state] == es: + E_same = True + else: + E_same = False + + if E_same and Q_same: + continue + else: + self.Q_old[state] = Qs + if eligibility_trace is not None: + self.e_old[state] = es + + name = f"Qsqr_{x}_{y}" + gridType = mdp.grid[x, y] + isExit = (str(gridType) != gridType) + isCurrent = (currentState == state) + # actions = mdp.A(state) + if actions == None or len(actions) == 0: + actions = [None] + q = defaultdict(lambda: 0) + valStrings = {} + + if gridType == '#': + self.drawSquare(name, x, y, 0, 0, 0, None, None, True, False, isCurrent) + elif isExit: + action = actions[0] # next(iter(q.keys())) + value = Qs[0] # q[action] # q[action] + valString = '%.2f' % value + self.drawSquare(name, x, y, value, minValue, maxValue, valString, [action], False, isExit, + isCurrent) + else: + actions, Qs = Q.get_Qs(state) + de = None + rs = None # return-sum + rN = None # return-count + + for k, action in enumerate(actions): + v = Qs[k] # Get the Q-value. + # v = Q[state, action] + q[action] += v + valStrings[action] = '%.2f' % v + # etrace = None if eligibility_trace is None else eligibility_trace[] + # print(state, action, eligibility_trace[state, action]) + de = None if eligibility_trace is None else {a: eligibility_trace[state, a] for a in actions} + if returns_sum is not None: + rs = {a: returns_sum[state, a] for a in actions} + if returns_count is not None: + rN = {a: returns_count[state, a] for a in actions} + + + self.drawSquareQ(name, x, y, q, minValue, maxValue, valStrings, actions, isCurrent, eligibility_trace=de, returns_sum=rs, returns_count=rN) + pos = self.to_screen(((mdp.width - 1.0) / 2.0, - 0.8)) + self.ga.text("Q_values_text", pos, TEXT_COLOR, message, "Courier", -32, "bold", "c") + + if isinstance(currentState, tuple): + + screen_x, screen_y = self.to_screen(currentState) + self.draw_player((screen_x, screen_y), 0.12 * self.GRID_SIZE) + self.draw_annotation() + + + def drawNullSquare(self, name, grid, x, y, isObstacle, isTerminal, isCurrent): + square_color = getColor(0, -1, 1) + if isObstacle: + square_color = OBSTACLE_COLOR + (screen_x, screen_y) = self.to_screen((x, y)) + self.square(name + "_s1", (screen_x, screen_y), + 0.5 * self.GRID_SIZE, + color=square_color, + filled=1, + width=1) + self.square(name + "_s2", (screen_x, screen_y), + 0.5 * self.GRID_SIZE, + color=EDGE_COLOR, + filled=0, + width=3) + if isTerminal and not isObstacle: + self.square(name + "_s3", (screen_x, screen_y), + 0.4 * self.GRID_SIZE, + color=EDGE_COLOR, + filled=0, + width=2) + self.ga.text(name + "_text", (screen_x, screen_y), + TEXT_COLOR, + str(grid[x,y]), + "Courier", -24, "bold", "c") + self.draw_annotation() + + + def drawSquare(self, name, x, y, val, min, max, valStr, all_action, isObstacle, isTerminal, isCurrent, + returns_count=None, returns_sum=None): + square_color = getColor(val, min, max) + (screen_x, screen_y) = self.to_screen((x, y)) + if isObstacle: + square_color = OBSTACLE_COLOR + + self.square(name + "_o1", (screen_x, screen_y), 0.5 * self.GRID_SIZE, color=square_color, filled=1, width=1) + + self.square(name + "_o2", (screen_x, screen_y), 0.5 * self.GRID_SIZE, color=EDGE_COLOR, filled=0, width=3) + if isTerminal and not isObstacle: + self.square(name + "_o3", (screen_x, screen_y), 0.4 * self.GRID_SIZE, color=EDGE_COLOR, filled=0, width=2) + + if all_action is None: + all_action = [] + GRID_SIZE = self.GRID_SIZE + for action in all_action: + if action == GridworldMDP.NORTH: + self.ga.polygon(name + "_p1", [(screen_x, screen_y - 0.45 * GRID_SIZE), + (screen_x + 0.05 * GRID_SIZE, screen_y - 0.40 * GRID_SIZE), + (screen_x - 0.05 * GRID_SIZE, screen_y - 0.40 * GRID_SIZE)], EDGE_COLOR, + filled=1, smoothed=False) + if action == GridworldMDP.SOUTH: + self.ga.polygon(name + "_p2", [(screen_x, screen_y + 0.45 * GRID_SIZE), + (screen_x + 0.05 * GRID_SIZE, screen_y + 0.40 * GRID_SIZE), + (screen_x - 0.05 * GRID_SIZE, screen_y + 0.40 * GRID_SIZE)], EDGE_COLOR, + filled=1, smoothed=False) + if action == GridworldMDP.WEST: + self.ga.polygon(name + "_p3", [(screen_x - 0.45 * GRID_SIZE, screen_y), + (screen_x - 0.4 * GRID_SIZE, screen_y + 0.05 * GRID_SIZE), + (screen_x - 0.4 * GRID_SIZE, screen_y - 0.05 * GRID_SIZE)], EDGE_COLOR, + filled=1, smoothed=False) + if action == GridworldMDP.EAST: + self.ga.polygon(name + "_p4", [(screen_x + 0.45 * GRID_SIZE, screen_y), + (screen_x + 0.4 * GRID_SIZE, screen_y + 0.05 * GRID_SIZE), + (screen_x + 0.4 * GRID_SIZE, screen_y - 0.05 * GRID_SIZE)], EDGE_COLOR, + filled=1, smoothed=False) + + text_color = TEXT_COLOR + if not isObstacle: + self.ga.text(name + "_txt", (screen_x, screen_y - (GRID_SIZE/6 if isCurrent else 0) ), text_color, valStr, "Courier", -30, "bold", "c") + + if returns_count is not None: + self.ga.text(name + "_rc", (screen_x-GRID_SIZE/3, screen_y+GRID_SIZE/7), RED_TEXT_COLOR, f"N(s)={int(returns_count)}", "Courier", -20, "bold", "w") + if returns_sum is not None: + self.ga.text(name + "_rs", (screen_x-GRID_SIZE/3, screen_y+2*GRID_SIZE/7), RED_TEXT_COLOR, f"S(s)={returns_sum:.2f}", "Courier", -20, "bold", "w") + + # if returns_count is not None: + # self.ga.text(name + "_rs", (screen_x, screen_y), text_color, valStr, "Courier", -30, "bold", "c") + + + def drawSquareQ(self, name, x, y, qVals, minVal, maxVal, valStrs, bestActions, isCurrent, eligibility_trace=None, returns_sum=None, returns_count=None): + + GRID_SIZE = self.GRID_SIZE + (screen_x, screen_y) = self.to_screen((x, y)) + center = (screen_x, screen_y) + nw = (screen_x - 0.5 * GRID_SIZE, screen_y - 0.5 * GRID_SIZE) + ne = (screen_x + 0.5 * GRID_SIZE, screen_y - 0.5 * GRID_SIZE) + se = (screen_x + 0.5 * GRID_SIZE, screen_y + 0.5 * GRID_SIZE) + sw = (screen_x - 0.5 * GRID_SIZE, screen_y + 0.5 * GRID_SIZE) + + n = (screen_x, screen_y - 0.5 * GRID_SIZE + 5) + s = (screen_x, screen_y + 0.5 * GRID_SIZE - 5) + w = (screen_x - 0.5 * GRID_SIZE + 5, screen_y) + e = (screen_x + 0.5 * GRID_SIZE - 5, screen_y) + + actions = qVals.keys() + for action in actions: + wedge_color = getColor(qVals[action], minVal, maxVal) + if action == GridworldMDP.NORTH: + self.ga.polygon(name + "_s1", (center, nw, ne), wedge_color, filled=1, smoothed=False) + if action == GridworldMDP.SOUTH: + self.ga.polygon(name + "_s2", (center, sw, se), wedge_color, filled=1, smoothed=False) + if action == GridworldMDP.EAST: + self.ga.polygon(name + "_s3", (center, ne, se), wedge_color, filled=1, smoothed=False) + if action == GridworldMDP.WEST: + self.ga.polygon(name + "_s4", (center, nw, sw), wedge_color, filled=1, smoothed=False) + + self.square(name + "_base_square", (screen_x, screen_y), + 0.5 * GRID_SIZE, + color=EDGE_COLOR, + filled=0, + width=3) + + self.ga.line(name + "_l1", ne, sw, color=EDGE_COLOR) + self.ga.line(name + "_l2", nw, se, color=EDGE_COLOR) + + for action in actions: + text_color = TEXT_COLOR + if qVals[action] < max(qVals.values()): text_color = MUTED_TEXT_COLOR + valStr = "" + if action in valStrs: + valStr = valStrs[action] + h = -20 # Font size (for reasons). + if eligibility_trace is not None: + estr = f'{eligibility_trace[action]:.2f}' + dh = 0.105 * GRID_SIZE + ECOL = RED_TEXT_COLOR if eligibility_trace[action] != 0 else getColor(qVals[action], minVal, maxVal) + esize = -16 + + NCOL = RED_TEXT_COLOR + NSIZE = int(GRID_SIZE/170 * 20) + S_str = '' + N_str = '' + + rca = None + if returns_sum is not None and returns_sum[action] is not None: + rca = returns_sum[action] + + rcc = None + if returns_count is not None and returns_count[action] is not None: + rcc = returns_count[action] + + if rca is not None: + S_str = f"S(s)={returns_sum[action]:.2f}" + if rcc is not None: + N_str = f"N(s)={int(returns_count[action])}" + dh = 0.105 * GRID_SIZE + + # self.ga.text(name + "_rc", (screen_x - GRID_SIZE / 3, screen_y + GRID_SIZE / 7), RED_TEXT_COLOR, + # f"N(s)={int(returns_count)}", "Courier", -20, "bold", "w") + # if returns_sum is not None: + # self.ga.text(name + "_rs", (screen_x - GRID_SIZE / 3, screen_y + 2 * GRID_SIZE / 7), RED_TEXT_COLOR, + # f"S(s)={returns_sum:.2f}", "Courier", -20, "bold", "w") + # dw = 0.095 * GRID_SIZE + + if action == GridworldMDP.NORTH: + self.ga.text(name + "_txt1", n, text_color, valStr, "Courier", h, "bold", "n") + if eligibility_trace is not None: + self.ga.text(name + "_txt1e", (n[0], n[1]+dh), ECOL, estr, "Courier", esize, "bold", "n") + if rca is not None: + self.ga.text(f"{name}_txt_s{action}", (n[0], n[1] + dh), NCOL, S_str, "Courier", 10, "bold", "n",fontsize=NSIZE) + if rcc is not None: + self.ga.text(f"{name}_txt_n{action}", (n[0], n[1] + 2*dh), NCOL, N_str, "Courier", 2, "bold", "n",fontsize=NSIZE) + + + if action == GridworldMDP.SOUTH: + self.ga.text(name + "_txt2", s, text_color, valStr, "Courier", h, "bold", "s") + if eligibility_trace is not None: + self.ga.text(name + "_txt2e", (s[0], s[1]-dh), ECOL, estr, "Courier", esize, "bold", "s") + if rca is not None: + self.ga.text(f"{name}_txt_s{action}", (s[0], s[1] - 1.5*dh), NCOL, S_str, "Courier", 10, "bold", "n",fontsize=NSIZE) + if rcc is not None: + self.ga.text(f"{name}_txt_n{action}", (s[0], s[1] - 1.2*2*dh), NCOL, N_str, "Courier", 2, "bold", "n",fontsize=NSIZE) + + if action == GridworldMDP.EAST: + self.ga.text(name + "_txt3", e, text_color, valStr, "Courier", h, "bold", "e") + if eligibility_trace is not None: + self.ga.text(name + "_txt3e", (e[0], e[1]+dh), ECOL, estr, "Courier", esize, "bold", "e") + if rca is not None: + self.ga.text(f"{name}_txt_s{action}", (e[0]-1.4*dh, e[1] - 0.4*dh+dh), NCOL, S_str, "Courier", 10, "bold", "n",fontsize=NSIZE) + if rcc is not None: + self.ga.text(f"{name}_txt_n{action}", (e[0]-1.4*dh, e[1] + 0.4*dh+dh), NCOL, N_str, "Courier", 2, "bold", "n",fontsize=NSIZE) + + if action == GridworldMDP.WEST: + self.ga.text(name + "_txt4", w, text_color, valStr, "Courier", h, "bold", "w") + if eligibility_trace is not None: + self.ga.text(name + "_txt4e", (w[0], w[1]+dh), ECOL, estr, "Courier", esize, "bold", "w") + if rca is not None: + self.ga.text(f"{name}_txt_s{action}", (w[0]+1.6*dh, w[1] - 0.4*dh+dh), NCOL, S_str, "Courier", 10, "bold", "n",fontsize=NSIZE) + if rcc is not None: + self.ga.text(f"{name}_txt_n{action}", (w[0]+1.6*dh, w[1] + 0.4*dh+dh), NCOL, N_str, "Courier", 2, "bold", "n",fontsize=NSIZE) + + + + def square(self, name, pos, size, color, filled, width): + x, y = pos + dx, dy = size, size + return self.ga.polygon(name, [(x - dx, y - dy), (x - dx, y + dy), (x + dx, y + dy), (x + dx, y - dy)], + outlineColor=color, + fillColor=color, filled=filled, width=width, smoothed=False, closed=True) + + def draw_player(self, position, grid_size): + # PACMAN_COLOR + + self.ga.circle("pacman", position, PACMAN_SCALE * grid_size * 2, + fillColor=PACMAN_COLOR, outlineColor=PACMAN_COLOR, + endpoints=getEndpoints(0), + width=PACMAN_OUTLINE_WIDTH) + + def to_screen(self, point): + (gamex, gamey) = point + x = gamex * self.GRID_SIZE + self.MARGIN + y = (self.mdp.height - gamey - 1) * self.GRID_SIZE + self.MARGIN + return (x, y) + + +def getColor(val, min_value, max_value): + r = val * 0.65 / min_value if val < 0 and min_value < 0 else 0 + g = val * 0.65 / max_value if val > 0 and max_value > 0 else 0 + return formatColor(r, g, 0) + + +if __name__ == "__main__": + from irlc.gridworld.gridworld_environments import OpenGridEnvironment + env = OpenGridEnvironment(render_mode='human') + # env = BookGridEnvironment() + + from irlc.ex11.q_agent import QAgent + from irlc import train + + + agent = QAgent(env) + # env = VideoMonitor(env, agent=agent, fps=2000) + import time + + t = time.time() + n = 200 + train(env, agent, max_steps=n, num_episodes=10000, verbose=False) + env.close() + + print("time per step", (time.time() - t) / n) + # 0.458 + # 0.63 + # 0.61 + # Benchmark over 100 steps: everything else: 0.04 (11 %), setup: 0.25 (72 %), viewer.render: 0.06 (16 %) + +# 423, 390, 342 (cur) diff --git a/irlc/gridworld/gridworld_mdp.py b/irlc/gridworld/gridworld_mdp.py new file mode 100644 index 0000000000000000000000000000000000000000..80c2bb61a365babb6a1812269400500fe1d90550 --- /dev/null +++ b/irlc/gridworld/gridworld_mdp.py @@ -0,0 +1,71 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from collections import defaultdict +from irlc.ex09.mdp import MDP + + +class GridworldMDP(MDP): + TERMINAL = "Terminal state" + NORTH = 0 # These are the four available actions. + EAST = 1 + SOUTH = 2 + WEST = 3 + actions2labels = {NORTH: 'North', + SOUTH: 'South', + EAST: 'East', + WEST: 'West'} # This dictionary is useful for labelling purposes but otherwise serve no purpose. + + def __init__(self, grid, living_reward=0.0, noise=0.0): + self.grid = {} + self.height = len(grid) + self.width = len(grid[0]) + initial_state = None + for dy, line in enumerate(grid): + y = self.height - dy - 1 + for x, el in enumerate(line): + self.grid[x, y] = el + if el == 'S': + initial_state = (x, y) + self.noise = noise + self.living_reward = living_reward + super().__init__(initial_state=initial_state) + + def A(self, state): + """ + Returns list of valid actions available in 'state'. + + You can try to go into walls (but will state in your location) + and when you are on the exit-squares (i.e., the ones with numbers), you have a single action available + 'North' which will take you to the terminal square. + """ + return (self.NORTH,) if type(self.grid[state]) in [int, float] else (self.NORTH, self.EAST, self.SOUTH, self.WEST) + + def is_terminal(self, state): + return state == self.TERMINAL + + def Psr(self, state, action): + if type(self.grid[state]) in [float, int]: + return {(self.TERMINAL, self.grid[state]): 1.} + + probabilities = defaultdict(float) + for a, pr in [(action, 1-self.noise), ((action - 1) % 4, self.noise/2), ((action + 1) % 4, self.noise/2)]: + sp = self.f(state, a) + r = self.grid[state] if type(self.grid[state]) in [int, float] else self.living_reward + probabilities[(sp, r)] += pr + return probabilities + + def f(self, state, action): + x, y = state + nxt = {self.NORTH: (x, y+1), + self.WEST: (x-1, y), + self.EAST: (x+1, y), + self.SOUTH: (x, y-1)} + return nxt[action] if self._legal(nxt[action]) else state + + def _legal(self, state): + return state in self.grid and self.grid[state] != "#" + + +class FrozenGridMDP(GridworldMDP): + def __init__(self, grid, is_slippery=True, living_reward=0): + self.is_slippery = is_slippery + super().__init__(grid, noise=2/3 if is_slippery else 0, living_reward=living_reward) diff --git a/irlc/lectures/__init__.py b/irlc/lectures/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/lectures/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/lectures/lec01/__init__.py b/irlc/lectures/lec01/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/lectures/lec01/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/lectures/lec01/lecture_01_car_random.py b/irlc/lectures/lec01/lecture_01_car_random.py new file mode 100644 index 0000000000000000000000000000000000000000..e1ffe55a94a4e5d38bc558b2d94a205400feea2b --- /dev/null +++ b/irlc/lectures/lec01/lecture_01_car_random.py @@ -0,0 +1,10 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.car.car_model import CarEnvironment +from irlc.ex01.agent import train, Agent + +if __name__ == "__main__": + env = CarEnvironment(render_mode='human') + env.action_space.low[1] = 0 # To ensure we do not drive backwards. + agent = Agent(env) + stats, _ = train(env, agent, num_episodes=1, verbose=False) + env.close() diff --git a/irlc/lectures/lec01/lecture_01_pacman.py b/irlc/lectures/lec01/lecture_01_pacman.py new file mode 100644 index 0000000000000000000000000000000000000000..cba2e1b58bed71d53c46a1d15dd178e344da7563 --- /dev/null +++ b/irlc/lectures/lec01/lecture_01_pacman.py @@ -0,0 +1,15 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.pacman.pacman_environment import PacmanEnvironment +from irlc.ex01.agent import train, Agent +from irlc import interactive + +def ppacman(): + # smallGrid + env = PacmanEnvironment(layout='mediumClassic', render_mode='human') + env, agent = interactive(env, Agent(env)) + stats, _ = train(env, agent, num_episodes=100, verbose=False) + print("Accumulated reward", stats[-1]['Accumulated Reward']) + env.close() + +if __name__ == "__main__": + ppacman() diff --git a/irlc/lectures/lec01/lecture_01_pendulum_random.py b/irlc/lectures/lec01/lecture_01_pendulum_random.py new file mode 100644 index 0000000000000000000000000000000000000000..a5e7fc4b0ee98dba4cdf0a637d7c834b9ce58528 --- /dev/null +++ b/irlc/lectures/lec01/lecture_01_pendulum_random.py @@ -0,0 +1,9 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex01.agent import train, Agent +from irlc.ex04.model_pendulum import GymSinCosPendulumEnvironment + +if __name__ == "__main__": + env = GymSinCosPendulumEnvironment(Tmax=100, render_mode='human') + agent = Agent(env) + stats, _ = train(env, agent, num_episodes=1, verbose=False) + env.close() diff --git a/irlc/lectures/lec02/__init__.py b/irlc/lectures/lec02/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/lectures/lec02/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/lectures/lec02/lecture_02_dp_gridworld_short.py b/irlc/lectures/lec02/lecture_02_dp_gridworld_short.py new file mode 100644 index 0000000000000000000000000000000000000000..d2831e64a18df333041cb383950e84b0d4ebc289 --- /dev/null +++ b/irlc/lectures/lec02/lecture_02_dp_gridworld_short.py @@ -0,0 +1,8 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.chapter1.dp_planning_agent import dp_visualization +from irlc.gridworld.gridworld_environments import FrozenLake + +if __name__ == "__main__": + env = FrozenLake(render_mode='human') + dp_visualization(env, N=4, num_episodes=10) + env.close() diff --git a/irlc/lectures/lec02/lecture_02_frozen_lake.py b/irlc/lectures/lec02/lecture_02_frozen_lake.py new file mode 100644 index 0000000000000000000000000000000000000000..3a91f818af94df9f8b5dd3d7cbb7ce4b4b211012 --- /dev/null +++ b/irlc/lectures/lec02/lecture_02_frozen_lake.py @@ -0,0 +1,13 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.gridworld.gridworld_environments import FrozenLake +from gymnasium.wrappers import TimeLimit +from irlc import Agent, interactive, train + +if __name__ == "__main__": + env = FrozenLake(is_slippery=True, living_reward=-1e-4, render_mode="human") + N = 40 + env, agent = interactive(env, Agent(env)) + env = TimeLimit(env, max_episode_steps=N) + num_episodes = 100 + train(env, agent, num_episodes=num_episodes) + env.close() diff --git a/irlc/lectures/lec02/lecture_02_frozen_long_slippery.py b/irlc/lectures/lec02/lecture_02_frozen_long_slippery.py new file mode 100644 index 0000000000000000000000000000000000000000..217929b2a325c160cebed3363f23c0f5733f0e84 --- /dev/null +++ b/irlc/lectures/lec02/lecture_02_frozen_long_slippery.py @@ -0,0 +1,8 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.chapter1.dp_planning_agent import dp_visualization +from irlc.gridworld.gridworld_environments import FrozenLake + +if __name__ == "__main__": + env = FrozenLake(is_slippery=True, living_reward=-1e-4, render_mode='human') + dp_visualization(env, N=40, num_episodes=100) + env.close() diff --git a/irlc/lectures/lec02/lecture_02_keyboard_pacman_g1.py b/irlc/lectures/lec02/lecture_02_keyboard_pacman_g1.py new file mode 100644 index 0000000000000000000000000000000000000000..06aa7e90647228a87d84bab423163a462b7d10f6 --- /dev/null +++ b/irlc/lectures/lec02/lecture_02_keyboard_pacman_g1.py @@ -0,0 +1,23 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.pacman.pacman_environment import PacmanEnvironment +from irlc.ex01.agent import train +from irlc.ex01.agent import Agent +from irlc import interactive +from irlc.lectures.chapter3dp.dp_pacman import SS1tiny +# from irlc.pacman.layouts import S + +# from irlc import PlayWrapper +# from irlc import VideoMonitor + +def ppac(layout_str, name="pac"): + env = PacmanEnvironment(layout=None, layout_str=layout_str, animate_movement=True) + agent = Agent(env) + env, agent = interactive(env, agent) + # agent = PlayWrapper(agent, env) + # env = VideoMonitor(env) + stats, _ = train(env, agent, num_episodes=5, max_steps=8) + print("Accumulated reward for all episodes:", [s['Accumulated Reward'] for s in stats]) + env.close() + +if __name__ == "__main__": + ppac(SS1tiny) diff --git a/irlc/lectures/lec02/lecture_02_keyboard_pacman_g2.py b/irlc/lectures/lec02/lecture_02_keyboard_pacman_g2.py new file mode 100644 index 0000000000000000000000000000000000000000..cd1f8dff48a59a699d084fbc72b8489f5976b844 --- /dev/null +++ b/irlc/lectures/lec02/lecture_02_keyboard_pacman_g2.py @@ -0,0 +1,6 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex02.old.dp_pacman import SS2tiny +from irlc.lectures.lec02.lecture_02_keyboard_pacman_g1 import ppac + +if __name__ == "__main__": + ppac(SS2tiny) diff --git a/irlc/lectures/lec02/lecture_02_optimal_dp_g0.py b/irlc/lectures/lec02/lecture_02_optimal_dp_g0.py new file mode 100644 index 0000000000000000000000000000000000000000..8c914974699e423122d2d1bf7429fd91048afe20 --- /dev/null +++ b/irlc/lectures/lec02/lecture_02_optimal_dp_g0.py @@ -0,0 +1,38 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.pacman.pacman_environment import PacmanEnvironment +from irlc.ex02.dp_agent import DynamicalProgrammingAgent +from gymnasium.wrappers import TimeLimit +from irlc.pacman.pacman_environment import PacmanWinWrapper +from irlc.ex01.agent import train +# from irlc import VideoMonitor +# from irlc.ex02.old.dp_pacman import DPPacmanModel +from irlc.lectures.chapter3dp.dp_pacman import DPPacmanModel +# from irlc import PlayWrapper +from irlc import interactive + +def simulate_1_game(layout_str): + N = 30 + env = PacmanEnvironment(layout=None, layout_str=layout_str, render_mode='human') + + # env = VideoMonitor(env, fps=3) + model = DPPacmanModel(env, N=N, verbose=True) + agent = DynamicalProgrammingAgent(env, model=model) + # agent = PlayWrapper(agent, env) + env, agent = interactive(env, agent) + env = TimeLimit(env, max_episode_steps=N) + env = PacmanWinWrapper(env) + stats, trajectories = train(env, agent, num_episodes=100, verbose=False, return_trajectory=True) + env.close() + + +SS0 = """ +%%%%%%%%%% +% P . % +% %%%%%. % +% % +% %%% %%%% +%. .% +%%%%%%%%%% +""" +if __name__ == "__main__": + simulate_1_game(layout_str=SS0) diff --git a/irlc/lectures/lec02/lecture_02_optimal_dp_g1.py b/irlc/lectures/lec02/lecture_02_optimal_dp_g1.py new file mode 100644 index 0000000000000000000000000000000000000000..1cd3b98141171872084db070c64bb1c7aff5ac8c --- /dev/null +++ b/irlc/lectures/lec02/lecture_02_optimal_dp_g1.py @@ -0,0 +1,25 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# from irlc.pacman.pacman_environment import GymPacmanEnvironment +# from irlc.ex02.dp_agent import DynamicalProgrammingAgent +# from gym.wrappers import TimeLimit +# from irlc.pacman.pacman_environment import PacmanWinWrapper +# from irlc.ex01.agent import train +# # from irlc import VideoMonitor +from irlc.lectures.chapter3dp.dp_pacman import DPPacmanModel, SS1tiny +from irlc import interactive +from irlc.lectures.lec02.lecture_02_optimal_dp_g0 import simulate_1_game + +# def simulate_1_game(layout_str): +# N = 8 +# env = GymPacmanEnvironment(layout=None, layout_str=layout_str, animate_movement=True) +# env = VideoMonitor(env, fps=3) +# model = DPPacmanModel(env, N=N, verbose=True) +# agent = DynamicalProgrammingAgent(env, model=model) +# agent = PlayWrapper(agent, env) +# env = TimeLimit(env, max_episode_steps=N) +# env = PacmanWinWrapper(env) +# stats, trajectories = train(env, agent, num_episodes=100, verbose=False, return_trajectory=True) +# env.close() + +if __name__ == "__main__": + simulate_1_game(layout_str=SS1tiny) diff --git a/irlc/lectures/lec02/lecture_02_optimal_dp_g2.py b/irlc/lectures/lec02/lecture_02_optimal_dp_g2.py new file mode 100644 index 0000000000000000000000000000000000000000..32c4b590116bcfd1c2eb52e55efb8fd1832dd371 --- /dev/null +++ b/irlc/lectures/lec02/lecture_02_optimal_dp_g2.py @@ -0,0 +1,6 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.chapter3dp.dp_pacman import SS2tiny +from irlc.lectures.lec02.lecture_02_optimal_dp_g1 import simulate_1_game + +if __name__ == "__main__": + simulate_1_game(layout_str=SS2tiny) diff --git a/irlc/lectures/lec03/__init__.py b/irlc/lectures/lec03/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/lectures/lec03/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/lectures/lec03/ex_03_search.py b/irlc/lectures/lec03/ex_03_search.py new file mode 100644 index 0000000000000000000000000000000000000000..7d5ce2ca57e2fb179f264be0f519d6d334287b12 --- /dev/null +++ b/irlc/lectures/lec03/ex_03_search.py @@ -0,0 +1,18 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc import Agent, train, savepdf +from irlc.pacman.pacman_environment import PacmanEnvironment +from irlc.ex03.dp_forward import dp_forward +from irlc.ex03.search_problem import SearchProblem +from irlc.ex03.search_problem import EnsureTerminalSelfTransitionsWrapper +from irlc.ex03.pacman_search import layout2, layout1 + +if __name__ == "__main__": + env = PacmanEnvironment(layout_str=layout1, render_mode='human') + env.reset() + savepdf("ex03_layout1", env=env) + env.close() + + env = PacmanEnvironment(layout_str=layout1, render_mode='human') + env.reset() + savepdf("ex03_layout2", env=env) + env.close() diff --git a/irlc/lectures/lec03/lecture_03_alphab.py b/irlc/lectures/lec03/lecture_03_alphab.py new file mode 100644 index 0000000000000000000000000000000000000000..fa81c07f7264c87577af4431c2be9339a152c139 --- /dev/null +++ b/irlc/lectures/lec03/lecture_03_alphab.py @@ -0,0 +1,7 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex03multisearch.multisearch_alphabeta import GymAlphaBetaAgent +from irlc.lectures.lec03.lecture_03_minimax import gminmax + +if __name__ == "__main__": + d = 3 + gminmax(Agent=GymAlphaBetaAgent,depth=d) diff --git a/irlc/lectures/lec03/lecture_03_dotsearch_astar_manhattan.py b/irlc/lectures/lec03/lecture_03_dotsearch_astar_manhattan.py new file mode 100644 index 0000000000000000000000000000000000000000..ebea74a0e4980b47a004271f184f81f38154fa9e --- /dev/null +++ b/irlc/lectures/lec03/lecture_03_dotsearch_astar_manhattan.py @@ -0,0 +1,8 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec03.lecture_03_dotsearch_dp import singledot +from irlc.lectures.chapter4search.yield_version.pacman_yield import AStarAgentYield +from irlc.ex03multisearch.pacman_problem_positionsearch_astar import manhattanHeuristic + +if __name__ == "__main__": + agent_args = dict(heuristic=manhattanHeuristic) + singledot(SAgent=AStarAgentYield, agent_args=agent_args) diff --git a/irlc/lectures/lec03/lecture_03_dotsearch_bfs.py b/irlc/lectures/lec03/lecture_03_dotsearch_bfs.py new file mode 100644 index 0000000000000000000000000000000000000000..2fafd77ced41a6c50ad917927cc70801ea29061a --- /dev/null +++ b/irlc/lectures/lec03/lecture_03_dotsearch_bfs.py @@ -0,0 +1,9 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec03.lecture_03_dotsearch_dp import singledot +from irlc.lectures.chapter4search.yield_version.pacman_yield import BFSAgentYield + +if __name__ == "__main__": + # agent_args = dict(heuristic=manhattanHeuristic,N=30) + singledot(SAgent=BFSAgentYield) + + # singledot(SAgent=BFSAgentYield) diff --git a/irlc/lectures/lec03/lecture_03_dotsearch_dfs.py b/irlc/lectures/lec03/lecture_03_dotsearch_dfs.py new file mode 100644 index 0000000000000000000000000000000000000000..276aa6bee3f60db8a9172d3dab1ba3ee463918f4 --- /dev/null +++ b/irlc/lectures/lec03/lecture_03_dotsearch_dfs.py @@ -0,0 +1,9 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec03.lecture_03_dotsearch_dp import singledot +from irlc.lectures.chapter4search.yield_version.pacman_yield import DFSAgentYield + +if __name__ == "__main__": + # agent_args = dict(heuristic=manhattanHeuristic,N=30) + singledot(SAgent=DFSAgentYield) + + # singledot(SAgent=BFSAgentYield) diff --git a/irlc/lectures/lec03/lecture_03_dotsearch_dp.py b/irlc/lectures/lec03/lecture_03_dotsearch_dp.py new file mode 100644 index 0000000000000000000000000000000000000000..baff1ee775c117f2d1cfb55948667899eba0db5e --- /dev/null +++ b/irlc/lectures/lec03/lecture_03_dotsearch_dp.py @@ -0,0 +1,12 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.chapter4search.yield_version.pacman_yield import stest, ForwardDPSearchAgent, dargs +# from irlc.ex03.pacsearch_agents import GymPositionSearchProblem, manhattanHeuristic, GymCornersProblem, cornersHeuristic, foodHeuristic, GymFoodSearchProblem, GymAnyFoodSearchProblem +from irlc.ex03multisearch.pacman_problem_positionsearch import GymPositionSearchProblem#, manhattanHeuristic + + +def singledot(layout='smallMaze', SAgent=None, agent_args=None, layout_str=None): + stest(layout=layout, layout_str=layout_str, SAgent=SAgent, prob=GymPositionSearchProblem(), agent_args=agent_args, zoom=2, **dargs, fps=30) # part 3 + +if __name__ == "__main__": + agent_args = dict(N=30) + singledot(SAgent=ForwardDPSearchAgent, agent_args=agent_args) diff --git a/irlc/lectures/lec03/lecture_03_expectimax.py b/irlc/lectures/lec03/lecture_03_expectimax.py new file mode 100644 index 0000000000000000000000000000000000000000..826975f29ec88a7aeaedf08eff8bf356980791f7 --- /dev/null +++ b/irlc/lectures/lec03/lecture_03_expectimax.py @@ -0,0 +1,7 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex03multisearch.multisearch_agents import GymExpectimaxAgent +from irlc.lectures.lec03.lecture_03_minimax import gminmax + +if __name__ == "__main__": + d = 3 + gminmax(Agent=GymExpectimaxAgent,depth=d) diff --git a/irlc/lectures/lec03/lecture_03_minimax.py b/irlc/lectures/lec03/lecture_03_minimax.py new file mode 100644 index 0000000000000000000000000000000000000000..eb8ee7362072498ac35df5df4367f822898fd4bb --- /dev/null +++ b/irlc/lectures/lec03/lecture_03_minimax.py @@ -0,0 +1,35 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex01.agent import train +from irlc.pacman.pacman_environment import GymPacmanEnvironment +from irlc.utils.video_monitor import VideoMonitor +from irlc.ex03multisearch.multisearch_agents import GymMinimaxAgent + + +layout_str = """ +%%%%%%%%% +% % +% %%%% % +% % +% P % +%%%% % +%%%% .G % +%%%% % +%%%%%%%%% +""".strip() + +def gminmax(layout='smallClassic', layout_str=layout_str, Agent=None, depth=3, **kwargs): + zoom = 2 + env = GymPacmanEnvironment(layout=layout, layout_str=layout_str, zoom=zoom, **kwargs) + agent = Agent(env, depth=depth) + from irlc import PlayWrapper + agent = PlayWrapper(agent, env) + + env = VideoMonitor(env, agent=agent, agent_monitor_keys=tuple(), fps=10) + train(env, agent, num_episodes=30) + env.close() + +if __name__ == "__main__": + d = 3 + gminmax(layout='minimaxClassic', layout_str=layout_str, Agent=GymMinimaxAgent,depth=d) + # gminmax(layout='minimaxClassic', layout_str=layout_str, Agent=GymAlphaBetaAgent, depth=d) + # gminmax(layout='minimaxClassic', layout_str=layout_str, Agent=GymExpectimaxAgent,depth=d) diff --git a/irlc/lectures/lec03/lecture_03_squaresearch_bfs.py b/irlc/lectures/lec03/lecture_03_squaresearch_bfs.py new file mode 100644 index 0000000000000000000000000000000000000000..ac1e0953cd661d8ae9d0c859b6470ff7b28af798 --- /dev/null +++ b/irlc/lectures/lec03/lecture_03_squaresearch_bfs.py @@ -0,0 +1,12 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.chapter4search.yield_version.pacman_yield import BFSAgentYield +from irlc.lectures.chapter4search.search_tables import s_large + +# def tricksearchdot(layout='trickySearch', SAgent=None, agent_args=None, layout_str=None): +# stest(layout_str=layout_str, SAgent=SAgent, prob=GymFoodSearchProblem(), agent_args=agent_args, zoom=2, **dargs, fps=1000) # part 3 + +from irlc.lectures.lec03.lecture_03_tricksearch_bfs import tricksearchdot + +if __name__ == "__main__": + # agent_args = dict(heuristic=manhattanHeuristic,N=30) + tricksearchdot(SAgent=BFSAgentYield, agent_args=None, layout_str=s_large) diff --git a/irlc/lectures/lec03/lecture_03_tricksearch_astar.py b/irlc/lectures/lec03/lecture_03_tricksearch_astar.py new file mode 100644 index 0000000000000000000000000000000000000000..6c658491e286be118f4d82e429add412cd680b40 --- /dev/null +++ b/irlc/lectures/lec03/lecture_03_tricksearch_astar.py @@ -0,0 +1,10 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# from irlc.ex03.pacsearch_agents import GymPositionSearchProblem, manhattanHeuristic, GymCornersProblem, cornersHeuristic, foodHeuristic, GymFoodSearchProblem, GymAnyFoodSearchProblem +from irlc.lectures.chapter4search.yield_version.pacman_yield import AStarAgentYield + +from irlc.lectures.lec03.lecture_03_tricksearch_bfs import tricksearchdot +from irlc.ex03multisearch.pacman_problem_foodsearch_astar import foodHeuristic + +if __name__ == "__main__": + agent_args = dict(heuristic=foodHeuristic) + tricksearchdot(SAgent=AStarAgentYield, agent_args=agent_args) diff --git a/irlc/lectures/lec03/lecture_03_tricksearch_bfs.py b/irlc/lectures/lec03/lecture_03_tricksearch_bfs.py new file mode 100644 index 0000000000000000000000000000000000000000..89b776456aa03640d6e75721ff7804ca3dbf8b6a --- /dev/null +++ b/irlc/lectures/lec03/lecture_03_tricksearch_bfs.py @@ -0,0 +1,21 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.chapter4search.yield_version.pacman_yield import stest, dargs +from irlc.ex03multisearch.pacman_problem_foodsearch import GymFoodSearchProblem +from irlc.lectures.chapter4search.yield_version.pacman_yield import BFSAgentYield + +layout_str = """ +%%%%%%%%%%%% +% % % +%.%.%.%% % % +% P % % +%%%%%%%%%% % +%. % +%%%%%%%%%%%% +""".strip() + +def tricksearchdot(layout_str=layout_str, SAgent=None, agent_args=None): + stest(layout_str=layout_str, SAgent=SAgent, prob=GymFoodSearchProblem(), agent_args=agent_args, zoom=2, **dargs, fps=1000) # part 3 + +if __name__ == "__main__": + # agent_args = dict(heuristic=manhattanHeuristic,N=30) + tricksearchdot(SAgent=BFSAgentYield, agent_args=None) diff --git a/irlc/lectures/lec03/lecture_03_tricksearch_dfs.py b/irlc/lectures/lec03/lecture_03_tricksearch_dfs.py new file mode 100644 index 0000000000000000000000000000000000000000..f3b2ac4ad2eeed59217fe591c95385576e69c7ec --- /dev/null +++ b/irlc/lectures/lec03/lecture_03_tricksearch_dfs.py @@ -0,0 +1,10 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# from irlc.ex03.pacsearch_agents import GymPositionSearchProblem, manhattanHeuristic, GymCornersProblem, cornersHeuristic, foodHeuristic, GymFoodSearchProblem, GymAnyFoodSearchProblem + +from irlc.lectures.chapter4search.yield_version.pacman_yield import DFSAgentYield +from irlc.lectures.lec03.lecture_03_tricksearch_bfs import tricksearchdot + + +if __name__ == "__main__": + # agent_args = dict(heuristic=manhattanHeuristic,N=30) + tricksearchdot(SAgent=DFSAgentYield, agent_args=None) diff --git a/irlc/lectures/lec03/snapshot_base/openaigym.video.0.8068.video000000.meta.json b/irlc/lectures/lec03/snapshot_base/openaigym.video.0.8068.video000000.meta.json new file mode 100644 index 0000000000000000000000000000000000000000..5dc734d01281b1a52d401032ec7e9c6da2d4ea39 --- /dev/null +++ b/irlc/lectures/lec03/snapshot_base/openaigym.video.0.8068.video000000.meta.json @@ -0,0 +1 @@ +{"episode_id": 0, "content_type": "video/mp4"} \ No newline at end of file diff --git a/irlc/lectures/lec03/snapshot_base/openaigym.video.0.8068.video000000.mp4 b/irlc/lectures/lec03/snapshot_base/openaigym.video.0.8068.video000000.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..17e5e5fbd204f4f1c8bf240b166ab0a318db4744 Binary files /dev/null and b/irlc/lectures/lec03/snapshot_base/openaigym.video.0.8068.video000000.mp4 differ diff --git a/irlc/lectures/lec04/__init__.py b/irlc/lectures/lec04/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/lectures/lec04/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/lectures/lec04/lecture_04_car_basic_pid.py b/irlc/lectures/lec04/lecture_04_car_basic_pid.py new file mode 100644 index 0000000000000000000000000000000000000000..8ed6d96ae433dfaefa0b74955808c02a544d5930 --- /dev/null +++ b/irlc/lectures/lec04/lecture_04_car_basic_pid.py @@ -0,0 +1,20 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# from irlc.ex04.pid_lunar import lunar_single_mission, get_lunar_lander +# import gym +from irlc import train +from irlc.car.car_model import CarEnvironment +from irlc.ex04.pid_car import PIDCarAgent +from irlc import savepdf +from irlc import interactive, Agent + +if __name__ == "__main__": + env = CarEnvironment(noise_scale=0, Tmax=30, max_laps=1, render_mode='human') + agent = PIDCarAgent(env, v_target=.2, use_both_x5_x3=False) + stats, trajectories = train(env, agent, num_episodes=1, return_trajectory=True) + env.close() + + + + + # env = CarEnvironment(noise_scale=0,Tmax=30, max_laps=1, render_mode='human') + # agent = PIDCarAgent(env, v_target=1, use_both_x5_x3=True) # I recommend lowering v_target to make the problem simpler. diff --git a/irlc/lectures/lec04/lecture_04_cartpole_A.py b/irlc/lectures/lec04/lecture_04_cartpole_A.py new file mode 100644 index 0000000000000000000000000000000000000000..3f4a2899db5fb22821b12f645f4381f547e85eb9 --- /dev/null +++ b/irlc/lectures/lec04/lecture_04_cartpole_A.py @@ -0,0 +1,10 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc import train +from irlc.ex04.pid_cartpole import PIDCartpoleAgent, get_offbalance_cart + +if __name__ == "__main__": + env = get_offbalance_cart(30) + agent = PIDCartpoleAgent(env, dt=env.dt, Kp=120, Ki=0, Kd=10, balance_to_x0=False) + # agent = PlayWrapper(agent, env) + _, trajectories = train(env, agent, num_episodes=1, reset=False) + env.close() diff --git a/irlc/lectures/lec04/lecture_04_cartpole_B.py b/irlc/lectures/lec04/lecture_04_cartpole_B.py new file mode 100644 index 0000000000000000000000000000000000000000..a57e0950c43dd941464534354d377206b719097a --- /dev/null +++ b/irlc/lectures/lec04/lecture_04_cartpole_B.py @@ -0,0 +1,14 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc import train +from irlc.ex04.pid_cartpole import PIDCartpoleAgent, get_offbalance_cart + +if __name__ == "__main__": + """ + Second task: We will now also try to bring the cart towards x=0. + """ + env = get_offbalance_cart(30) + agent = PIDCartpoleAgent(env, env.dt, ...) + # TODO: 1 lines missing. + raise NotImplementedError("Define your agent here (including parameters)") + _, trajectories = train(env, agent, num_episodes=1, reset=False) # Note reset=False to maintain initial conditions. + env.close() diff --git a/irlc/lectures/lec04/lecture_04_harmonic.py b/irlc/lectures/lec04/lecture_04_harmonic.py new file mode 100644 index 0000000000000000000000000000000000000000..7d7409954d82b313805391ec1d35bdfc6ab5a054 --- /dev/null +++ b/irlc/lectures/lec04/lecture_04_harmonic.py @@ -0,0 +1,14 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc import train +from irlc.ex04.model_harmonic import HarmonicOscilatorEnvironment +from irlc import Agent +import numpy as np + +class NullAgent(Agent): + def pi(self, x, k, info=None): + return np.asarray([0]) + +if __name__ == "__main__": + env = HarmonicOscilatorEnvironment(render_mode='human') + train(env, NullAgent(env), num_episodes=1, max_steps=200) + env.close() diff --git a/irlc/lectures/lec04/lecture_04_lunar.py b/irlc/lectures/lec04/lecture_04_lunar.py new file mode 100644 index 0000000000000000000000000000000000000000..c68fee76535414d7069884fd6aeb5708a6d975a3 --- /dev/null +++ b/irlc/lectures/lec04/lecture_04_lunar.py @@ -0,0 +1,15 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex04.pid_lunar import lunar_single_mission, get_lunar_lander +import gymnasium +from irlc import train + +if __name__ == "__main__": + env = gymnasium.make('LunarLanderContinuous-v2', render_mode='human') + env._max_episode_steps = 1000 # We don't want it to time out. + + agent = get_lunar_lander(env) + # agent = PlayWrapper(agent, env) + # env = VideoMonitor(env) + + stats, traj = train(env, agent, return_trajectory=True, num_episodes=10) + env.close() diff --git a/irlc/lectures/lec04/lecture_04_pendulum_random.py b/irlc/lectures/lec04/lecture_04_pendulum_random.py new file mode 100644 index 0000000000000000000000000000000000000000..58d084308b202ff91b5d7b4a332904b1f88979f6 --- /dev/null +++ b/irlc/lectures/lec04/lecture_04_pendulum_random.py @@ -0,0 +1,8 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc import Agent, train +from irlc.ex04.model_pendulum import GymSinCosPendulumEnvironment + +if __name__ == "__main__": + env = GymSinCosPendulumEnvironment(Tmax=20, render_mode='human') + train(env, Agent(env), num_episodes=1) + env.close() diff --git a/irlc/lectures/lec04/lecture_04_pid_d.py b/irlc/lectures/lec04/lecture_04_pid_d.py new file mode 100644 index 0000000000000000000000000000000000000000..8b05ff10e27da1bb65508aaf74b573559f5c15fd --- /dev/null +++ b/irlc/lectures/lec04/lecture_04_pid_d.py @@ -0,0 +1,5 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec04.lecture_04_pid_p import pidplot + +if __name__ == "__main__": + pidplot(Kp=40, Kd=100, Ki=0) diff --git a/irlc/lectures/lec04/lecture_04_pid_iA.py b/irlc/lectures/lec04/lecture_04_pid_iA.py new file mode 100644 index 0000000000000000000000000000000000000000..fa350611daa8148a817edf063eae1d525b1f55ef --- /dev/null +++ b/irlc/lectures/lec04/lecture_04_pid_iA.py @@ -0,0 +1,6 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec04.lecture_04_pid_p import pidplot + + +if __name__ == "__main__": + pidplot(Kp=40, Kd=50, Ki=0, slope=2, target=0) diff --git a/irlc/lectures/lec04/lecture_04_pid_iB.py b/irlc/lectures/lec04/lecture_04_pid_iB.py new file mode 100644 index 0000000000000000000000000000000000000000..9fda178a6a643fc20b606d10f1705d042dd1146d --- /dev/null +++ b/irlc/lectures/lec04/lecture_04_pid_iB.py @@ -0,0 +1,6 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec04.lecture_04_pid_p import pidplot + + +if __name__ == "__main__": + pidplot(Kp=40, Kd=50, Ki=10, slope=2, target=0) diff --git a/irlc/lectures/lec04/lecture_04_pid_p.py b/irlc/lectures/lec04/lecture_04_pid_p.py new file mode 100644 index 0000000000000000000000000000000000000000..ed3eb6b95ec5d348594a7f2d03d28b38428cbab7 --- /dev/null +++ b/irlc/lectures/lec04/lecture_04_pid_p.py @@ -0,0 +1,19 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex04.locomotive import LocomotiveEnvironment +from irlc.ex04.pid_locomotive_agent import PIDLocomotiveAgent +from irlc.ex01.agent import train + +def pidplot(Kp=40, Kd=0, Ki=0, slope=0, target=0): + dt = .04 + m = 70 + Tmax=20 + env = LocomotiveEnvironment(m=m, slope=slope, dt=dt, Tmax=Tmax, render_mode='human') + # env = VideoMonitor(env) + # Kp = 40 + agent = PIDLocomotiveAgent(env, dt=dt, Kp=Kp, Ki=Ki, Kd=Kd, target=0) + # env = PlayWrapper(agent, env) + train(env, agent, num_episodes=1) + env.close() + +if __name__ == "__main__": + pidplot(Kp=40, Kd=0, Ki=0) diff --git a/irlc/lectures/lec05/__init__.py b/irlc/lectures/lec05/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/lectures/lec05/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/lectures/lec05/lecture_05_carpole_random.py b/irlc/lectures/lec05/lecture_05_carpole_random.py new file mode 100644 index 0000000000000000000000000000000000000000..e82a89bbe407251ed4d9b02d2881c33ccfefa20b --- /dev/null +++ b/irlc/lectures/lec05/lecture_05_carpole_random.py @@ -0,0 +1,9 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc import Agent, train +from irlc.ex05.model_cartpole import GymSinCosCartpoleEnvironment + +if __name__ == "__main__": + + env = GymSinCosCartpoleEnvironment(Tmax=20, render_mode='human') + train(env, Agent(env), num_episodes=1) + env.close() diff --git a/irlc/lectures/lec05/lecture_05_cartpole_kelly.py b/irlc/lectures/lec05/lecture_05_cartpole_kelly.py new file mode 100644 index 0000000000000000000000000000000000000000..1bc3bcce247df45f93d74fdbb4cc731c8f7b539d --- /dev/null +++ b/irlc/lectures/lec05/lecture_05_cartpole_kelly.py @@ -0,0 +1,10 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex05.direct_cartpole_kelly import compute_solutions +from irlc.ex05.direct_plot import plot_solutions +import matplotlib.pyplot as plt + +if __name__ == "__main__": + env, solutions = compute_solutions() + print("Did we succeed?", solutions[-1]['solver']['success']) + plot_solutions(env, solutions, animate=True, pdf=None, animate_all=True, animate_repeats=3) + env.close() diff --git a/irlc/lectures/lec05/lecture_05_cartpole_time.py b/irlc/lectures/lec05/lecture_05_cartpole_time.py new file mode 100644 index 0000000000000000000000000000000000000000..ebd6e873e6e2036dcd6f6e13c6ee24c124694813 --- /dev/null +++ b/irlc/lectures/lec05/lecture_05_cartpole_time.py @@ -0,0 +1,11 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex05.direct_cartpole_time import compute_solutions +from irlc.ex05.direct_plot import plot_solutions +import matplotlib.pyplot as plt + +if __name__ == "__main__": + env, solutions = compute_solutions() + print("Did we succeed?", solutions[-1]['solver']['success']) + plot_solutions(env, solutions, animate=True, pdf=None, animate_all=True, animate_repeats=3) + env.close() + pass diff --git a/irlc/lectures/lec06/__init__.py b/irlc/lectures/lec06/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/lectures/lec06/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/lectures/lec06/lecture6_lqr_locomotive.py b/irlc/lectures/lec06/lecture6_lqr_locomotive.py new file mode 100644 index 0000000000000000000000000000000000000000..2c9ddf3c2dec5a84476a66a11861d89a9ff08f7b --- /dev/null +++ b/irlc/lectures/lec06/lecture6_lqr_locomotive.py @@ -0,0 +1,37 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import matplotlib.pyplot as plt +import numpy as np +from irlc import savepdf, train +from irlc.ex04.pid_locomotive_agent import PIDLocomotiveAgent +from irlc.ex06.lqr_agent import LQRAgent +from irlc.ex04.model_harmonic import HarmonicOscilatorEnvironment +from irlc.ex06.boeing_lqr import compute_A_B_d, compute_Q_R_q +from irlc.ex07.linearization_agent import LinearizationAgent +from irlc.ex06.lqr_pid import ConstantLQRAgent +from irlc.ex04.locomotive import LocomotiveEnvironment +from irlc.ex04.pid_locomotive_agent import PIDLocomotiveAgent +from irlc.ex01.agent import train +from irlc.ex03.control_cost import SymbolicQRCost +import matplotlib +#matplotlib.use('qtagg') +dt = .04 +m = 70 +Tmax=10 +slope = 0 + +env = LocomotiveEnvironment(m=m, slope=slope, dt=dt, Tmax=Tmax, render_mode='human') + +model = env.discrete_model +model.cost = SymbolicQRCost(Q=np.eye(2)*100, R=np.eye(1)).discretize(dt=dt) +agent = LinearizationAgent(env, model=model, xbar=env.observation_space.sample(), ubar=env.action_space.sample()) +_, traj = train(env, agent, num_episodes=1) +env.close() +if False: + from irlc import plot_trajectory, savepdf + import matplotlib.pyplot as plt + plt.figure() + plot_trajectory(trajectory=traj[0], env=env, xkeys=[0, 1], ukeys=[]) + savepdf('lqr_pid_locomotive_state.pdf') + plot_trajectory(trajectory=traj[0], env=env, ukeys=[0], xkeys=[]) + savepdf('lqr_pid_locomotive_action.pdf') + env.close() diff --git a/irlc/lectures/lec06/lecture_06_cartpole_ilqr.py b/irlc/lectures/lec06/lecture_06_cartpole_ilqr.py new file mode 100644 index 0000000000000000000000000000000000000000..dc5633578e01195c85c72b5e6d29080d5029c038 --- /dev/null +++ b/irlc/lectures/lec06/lecture_06_cartpole_ilqr.py @@ -0,0 +1,47 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +from irlc.ex07.ilqr_agent import ILQRAgent +from irlc import train +from irlc.ex05.model_cartpole import GymSinCosCartpoleEnvironment +# from irlc import VideoMonitor + +def cartpole_experiment(N=12, use_linesearch=True, figex="", animate=True): + np.random.seed(2) + Tmax = .9 + dt = Tmax/N + + env = GymSinCosCartpoleEnvironment(dt=dt, Tmax=Tmax, supersample_trajectory=True, render_mode='human') + agent = ILQRAgent(env, env.discrete_model, N=N, ilqr_iterations=200, use_linesearch=use_linesearch) + # if animate: + # env =VideoMonitor(env) + stats, trajectories = train(env, agent, num_episodes=3, return_trajectory=True) + + # agent.use_ubar = True + # stats2, trajectories2 = train(env, agent, num_episodes=1, return_trajectory=True) + # env.close() + env.close() + +def plt_cartpole(): + cartpole_experiment(N=50, use_linesearch=True, animate=True) + +if __name__ == '__main__': + np.random.seed(42) + plt_cartpole() + + # xb = agent.xbar + # tb = np.arange(N+1)*dt + # plt.figure(figsize=(8,6)) + # F = 3 + # # plt.plot(trajectories[0].time, trajectories[0].state[:,F], 'k-', label='Closed-loop $\\pi$') + # # plt.plot(trajectories2[0].time, trajectories2[0].state[:,F], '-', label='Open-loop $\\bar{u}_k$') + # + # plt.plot(tb, xb[:,F], '.-', label="iLQR rediction $\\bar{x}_k$") + # plt.xlabel("Time/seconds") + # plt.ylabel("$\cos(\\theta)$") + # plt.title(f"Pendulum environment $T={N}$") + # + # plt.grid() + # plt.legend() + # ev = "pendulum" + # savepdf(f"irlc_cartpole_theta_N{N}_{use_linesearch}{figex}") + # plt.show() diff --git a/irlc/lectures/lec06/lecture_06_linearize.py b/irlc/lectures/lec06/lecture_06_linearize.py new file mode 100644 index 0000000000000000000000000000000000000000..311dd27eb55cfca8fc3ef90fb25941155b7cadbd --- /dev/null +++ b/irlc/lectures/lec06/lecture_06_linearize.py @@ -0,0 +1,6 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex07.linearization_agent import get_offbalance_cart + +if __name__ == "__main__": + env = get_offbalance_cart(waiting_steps=20, sleep_time=0.1) + env.close() diff --git a/irlc/lectures/lec06/lecture_06_linearize_b.py b/irlc/lectures/lec06/lecture_06_linearize_b.py new file mode 100644 index 0000000000000000000000000000000000000000..b582957f75442e2162edaccc09ff4af51cdfeb13 --- /dev/null +++ b/irlc/lectures/lec06/lecture_06_linearize_b.py @@ -0,0 +1,18 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc import plot_trajectory, train +from irlc.ex07.linearization_agent import get_offbalance_cart, LinearizationAgent +import numpy as np +import matplotlib +matplotlib.use("tkagg") +import matplotlib.pyplot as plt + + +if __name__ == "__main__": + np.random.seed(42) # I don't think these results are seed-dependent but let's make sure. + env = get_offbalance_cart(4, sleep_time=0.08) # Simulate for a little time to get an off-balance cart. Increase 4-->10 to get failure. + agent = LinearizationAgent(env, model=env.discrete_model, xbar=env.discrete_model.x_upright, ubar=env.action_space.sample()*0) + _, trajectories = train(env, agent, num_episodes=1, return_trajectory=True, reset=False) # Note reset=False to maintain initial conditions. + plt.figure() + plot_trajectory(trajectories[0], env, xkeys=[0, 2, 3], ukeys=[0]) + plt.show() + env.close() diff --git a/irlc/lectures/lec06/lecture_06_pendulum_bilqr_L.py b/irlc/lectures/lec06/lecture_06_pendulum_bilqr_L.py new file mode 100644 index 0000000000000000000000000000000000000000..e0cb2ca23b92b51c2b23db190b67ca172404f5de --- /dev/null +++ b/irlc/lectures/lec06/lecture_06_pendulum_bilqr_L.py @@ -0,0 +1,7 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +from irlc.lectures.lec06.lecture_06_pendulum_bilqr_ubar import pen_experiment + +if __name__ == "__main__": + np.random.seed(2) # (!) + pen_experiment(N=50, use_linesearch=False, use_ubar=False) diff --git a/irlc/lectures/lec06/lecture_06_pendulum_bilqr_ubar.py b/irlc/lectures/lec06/lecture_06_pendulum_bilqr_ubar.py new file mode 100644 index 0000000000000000000000000000000000000000..d61a8ddc17ba23ba3c440108f5eaa4dba70a8530 --- /dev/null +++ b/irlc/lectures/lec06/lecture_06_pendulum_bilqr_ubar.py @@ -0,0 +1,66 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +from irlc.ex04.model_pendulum import GymSinCosPendulumEnvironment +from irlc.ex07.ilqr_agent import ILQRAgent +from irlc import train +from irlc import savepdf +import matplotlib.pyplot as plt + +Tmax = 3 +def pen_experiment(N=12, use_linesearch=True,figex="", animate=True, use_ubar=False): + dt = Tmax / N + env = GymSinCosPendulumEnvironment(dt, Tmax=Tmax, supersample_trajectory=True, render_mode='human' if animate else None) + agent = ILQRAgent(env, env.discrete_model, N=N, ilqr_iterations=200, use_linesearch=use_linesearch) + # if animate: + # env = VideoMonitor(env) + + if use_ubar: + agent.use_ubar = True + stats2, trajectories = train(env, agent, num_episodes=1, return_trajectory=True) + env.close() + + plot_pendulum_trajectory(trajectories[0], label=f'Use linesearch? {use_linesearch}. Use u-bar? {use_ubar}') + plt.legend() + plt.show() + + plt.figure(figsize=(6, 6)) + plt.semilogy(agent.J_hist, 'k.-') + plt.xlabel("iLQR Iterations") + plt.ylabel("Cost function estimate $J$") + # plt.title("Last value: {") + plt.grid() + # savepdf(f"irlc_pendulum_J_N{N}_{use_linesearch}{figex}") + plt.show() + # + # plt.show() + # xb = agent.xbar + # tb = np.arange(N+1)*dt + # plt.figure(figsize=(12, 6)) + # plt.plot(trajectories2[0].time, trajectories2[0].state[:,1], '-', label='Open-loop $\\bar{u}_k$') + # plt.plot(tb, xb[:,1], 'o-', label="iLQR prediction $\\bar{x}_k$") + # plt.grid() + # plt.legend() + # ev = "pendulum" + # savepdf(f"irlc_pendulum_theta_N{N}_{use_linesearch}{figex}") + # plt.show() + + ## Plot J + +# +def plot_pendulum_trajectory(traj, style='k-', label=None, action=False, **kwargs): + y = traj.state[:, 1] if not action else traj.action[:,0] + plt.plot(traj.time[:-1] if action else traj.time, y, style, label=label, **kwargs) + + plt.xlabel("Time/seconds") + if action: + plt.ylabel("Torque $u$") + else: + plt.ylabel("$\cos(\\theta)$") + plt.grid() + pass + +N = 50 + +if __name__ == "__main__": + np.random.seed(2) # (!) + pen_experiment(N=N, use_linesearch=False, use_ubar=True) diff --git a/irlc/lectures/lec06/lecture_06_pendulum_ilqr_L.py b/irlc/lectures/lec06/lecture_06_pendulum_ilqr_L.py new file mode 100644 index 0000000000000000000000000000000000000000..6e475bf6335497c98536f9850ce9934c53d2d4fd --- /dev/null +++ b/irlc/lectures/lec06/lecture_06_pendulum_ilqr_L.py @@ -0,0 +1,5 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +if __name__ == "__main__": + from irlc.lectures.lec06.lecture_06_pendulum_bilqr_ubar import pen_experiment + N = 50 + pen_experiment(N=N, use_linesearch=True, use_ubar=False) diff --git a/irlc/lectures/lec06/lecture_06_pendulum_ilqr_ubar.py b/irlc/lectures/lec06/lecture_06_pendulum_ilqr_ubar.py new file mode 100644 index 0000000000000000000000000000000000000000..b44a35cc127904eee0bb318cab2b383388cd4110 --- /dev/null +++ b/irlc/lectures/lec06/lecture_06_pendulum_ilqr_ubar.py @@ -0,0 +1,5 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +if __name__ == "__main__": + from irlc.lectures.lec06.lecture_06_pendulum_bilqr_ubar import pen_experiment + N = 50 + pen_experiment(N=N, use_linesearch=True, use_ubar=True) diff --git a/irlc/lectures/lec07/__init__.py b/irlc/lectures/lec07/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/lectures/lec07/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/lectures/lec07/lecture_07_boing_lqr.py b/irlc/lectures/lec07/lecture_07_boing_lqr.py new file mode 100644 index 0000000000000000000000000000000000000000..7a140752a73aa016366a0c2cd371f66504c2c08e --- /dev/null +++ b/irlc/lectures/lec07/lecture_07_boing_lqr.py @@ -0,0 +1,19 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex04.model_boeing import BoeingEnvironment +from irlc.ex07.lqr_learning_agents import learning_lqr, learning_lqr_mpc, learning_lqr_mpc_local +from irlc.ex07.learning_agent_mpc_optimize import learning_optimization_mpc_local + +if __name__ == "__main__": + env = BoeingEnvironment(output=[10, 0]) + + # Part A: LQR and global regression + learning_lqr(env) + + # Part B: LQR+MPC + # learning_lqr_mpc(env) + # + # # Part C: LQR+MPC and local regression + # learning_lqr_mpc_local(env) + # + # # Part D: Optimization+MPC and local regression + # learning_optimization_mpc_local(env) diff --git a/irlc/lectures/lec07/lecture_07_boing_lqr_mpc.py b/irlc/lectures/lec07/lecture_07_boing_lqr_mpc.py new file mode 100644 index 0000000000000000000000000000000000000000..2c4a72274d3f0fd68c713147bcc4910a9e1b879c --- /dev/null +++ b/irlc/lectures/lec07/lecture_07_boing_lqr_mpc.py @@ -0,0 +1,14 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex04.model_boeing import BoeingEnvironment +from irlc.ex07.lqr_learning_agents import learning_lqr, learning_lqr_mpc, learning_lqr_mpc_local +from irlc.ex07.learning_agent_mpc_optimize import learning_optimization_mpc_local + +if __name__ == "__main__": + env = BoeingEnvironment(output=[10, 0]) + learning_lqr_mpc(env) + + # # Part C: LQR+MPC and local regression + # learning_lqr_mpc_local(env) + # + # # Part D: Optimization+MPC and local regression + # learning_optimization_mpc_local(env) diff --git a/irlc/lectures/lec07/lecture_07_boing_lqr_mpc_local.py b/irlc/lectures/lec07/lecture_07_boing_lqr_mpc_local.py new file mode 100644 index 0000000000000000000000000000000000000000..22376d13457883a8f9b9f89becbffa9e46aedbb9 --- /dev/null +++ b/irlc/lectures/lec07/lecture_07_boing_lqr_mpc_local.py @@ -0,0 +1,9 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex04.model_boeing import BoeingEnvironment +from irlc.ex07.lqr_learning_agents import learning_lqr, learning_lqr_mpc, learning_lqr_mpc_local +from irlc.ex07.learning_agent_mpc_optimize import learning_optimization_mpc_local + +if __name__ == "__main__": + env = BoeingEnvironment(output=[10, 0]) + learning_lqr_mpc_local(env) + # learning_optimization_mpc_local(env) diff --git a/irlc/lectures/lec07/lecture_07_boing_lqr_mpc_optim.py b/irlc/lectures/lec07/lecture_07_boing_lqr_mpc_optim.py new file mode 100644 index 0000000000000000000000000000000000000000..4ed3f3e080238b559fb656e1f5640a1374109bfc --- /dev/null +++ b/irlc/lectures/lec07/lecture_07_boing_lqr_mpc_optim.py @@ -0,0 +1,8 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex04.model_boeing import BoeingEnvironment +from irlc.ex07.lqr_learning_agents import learning_lqr, learning_lqr_mpc, learning_lqr_mpc_local +from irlc.ex07.learning_agent_mpc_optimize import learning_optimization_mpc_local + +if __name__ == "__main__": + env = BoeingEnvironment(output=[10, 0]) + learning_optimization_mpc_local(env) diff --git a/irlc/lectures/lec07/lecture_07_lmpc.py b/irlc/lectures/lec07/lecture_07_lmpc.py new file mode 100644 index 0000000000000000000000000000000000000000..5fff87cb5ed8a3581de2f2c8c6ec6ae94957aeb4 --- /dev/null +++ b/irlc/lectures/lec07/lecture_07_lmpc.py @@ -0,0 +1,5 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex07.lmpc_run import main + +if __name__ == "__main__": + main(show_episode=True) diff --git a/irlc/lectures/lec07/lecture_07_pendulum_mpc_lqr.py b/irlc/lectures/lec07/lecture_07_pendulum_mpc_lqr.py new file mode 100644 index 0000000000000000000000000000000000000000..8867c0afeac9588af42a796e93ec70def74d2a07 --- /dev/null +++ b/irlc/lectures/lec07/lecture_07_pendulum_mpc_lqr.py @@ -0,0 +1,4 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +if __name__ == "__main__": + from irlc.ex07.mpc_pendulum_experiment_lqr import main_pendulum_lqr + main_pendulum_lqr() diff --git a/irlc/lectures/lec07/lecture_07_pendulum_mpc_optm.py b/irlc/lectures/lec07/lecture_07_pendulum_mpc_optm.py new file mode 100644 index 0000000000000000000000000000000000000000..9eff242ac8034a7e3f4ce8aa79e95d15f86d822b --- /dev/null +++ b/irlc/lectures/lec07/lecture_07_pendulum_mpc_optm.py @@ -0,0 +1,4 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +if __name__ == "__main__": + from irlc.ex07.mpc_pendulum_experiment_optim import main_pendulum + main_pendulum() diff --git a/irlc/lectures/lec07/lecture_07_pendulum_simple.py b/irlc/lectures/lec07/lecture_07_pendulum_simple.py new file mode 100644 index 0000000000000000000000000000000000000000..337b16585b8cd3c0a709a49b5eb7b378a08c2757 --- /dev/null +++ b/irlc/lectures/lec07/lecture_07_pendulum_simple.py @@ -0,0 +1,41 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex04.model_pendulum import GymSinCosPendulumEnvironment +from irlc.utils.video_monitor import VideoMonitor +from irlc.ex04.discrete_control_cost import goal_seeking_qr_cost, DiscreteQRCost +from irlc.ex01.agent import train +from irlc.ex07.lqr_learning_agents import MPCLocalLearningLQRAgent, MPCLearningAgent +from irlc import plot_trajectory, main_plot +import matplotlib.pyplot as plt +import numpy as np +from irlc.ex07.mpc_pendulum_experiment_lqr import mk_mpc_pendulum_env + +L = 12 +def main_pendulum_lqr_simple(Tmax=10): + + """ Run Local LQR/MPC agent using the parameters + L = 12 + neighboorhood_size = 50 + min_buffer_size = 50 + """ + env_pendulum = mk_mpc_pendulum_env() + + # agent = .... (instantiate agent here) + # TODO: 1 lines missing. + raise NotImplementedError("Instantiate your agent here") + env_pendulum = VideoMonitor(env_pendulum) + + experiment_name = f"pendulum{L}_lqr" + stats, trajectories = train(env_pendulum, agent, experiment_name=experiment_name, num_episodes=16,return_trajectory=True) + plt.show() + for k in range(len(trajectories)): + plot_trajectory(trajectories[k], env_pendulum) + plt.title(f"Trajectory {k}") + plt.show() + + env_pendulum.close() + main_plot(experiment_name) + plt.show() + +if __name__ == "__main__": + np.random.seed(1) + main_pendulum_lqr_simple() diff --git a/irlc/lectures/lec07/pendulum12/2021-03-19_08-21-20.207/log.txt b/irlc/lectures/lec07/pendulum12/2021-03-19_08-21-20.207/log.txt new file mode 100644 index 0000000000000000000000000000000000000000..dc592cfe3c7ae897391e9ce88d0c1454848920e8 --- /dev/null +++ b/irlc/lectures/lec07/pendulum12/2021-03-19_08-21-20.207/log.txt @@ -0,0 +1,17 @@ +Episode Accumulated Reward Average Reward Length Steps +0 -4895.647575826604 -39.165180606612836 125 125 +1 -4401.3119492497035 -35.21049559399765 125 250 +2 -2909.118791375824 -23.272950331006577 125 375 +3 -1355.8374521458716 -10.846699617166973 125 500 +4 -1392.1376132555315 -11.13710090604426 125 625 +5 -2377.229711438541 -19.017837691508326 125 750 +6 -1605.205601135333 -12.841644809082668 125 875 +7 -1142.6024308594363 -9.140819446875495 125 1000 +8 -1110.17238007953 -8.881379040636237 125 1125 +9 -1415.0153227915457 -11.320122582332363 125 1250 +10 -1084.806186007314 -8.678449488058519 125 1375 +11 -1204.58673322474 -9.636693865797913 125 1500 +12 -1582.8902992149344 -12.66312239371947 125 1625 +13 -1234.3968104401945 -9.875174483521556 125 1750 +14 -2290.3685781570866 -18.322948625256696 125 1875 +15 -1317.928485283048 -10.543427882264387 125 2000 diff --git a/irlc/lectures/lec07/pendulum12/2021-03-19_08-21-20.207/trajectories.pkl b/irlc/lectures/lec07/pendulum12/2021-03-19_08-21-20.207/trajectories.pkl new file mode 100644 index 0000000000000000000000000000000000000000..34930a77fba816ca18036b42921823ab18cefc24 Binary files /dev/null and b/irlc/lectures/lec07/pendulum12/2021-03-19_08-21-20.207/trajectories.pkl differ diff --git a/irlc/lectures/lec07/pendulum12/2022-03-17_14-16-10.758/log.txt b/irlc/lectures/lec07/pendulum12/2022-03-17_14-16-10.758/log.txt new file mode 100644 index 0000000000000000000000000000000000000000..3005052ea2c0aa4f28a74ca66b23b716fa4a925d --- /dev/null +++ b/irlc/lectures/lec07/pendulum12/2022-03-17_14-16-10.758/log.txt @@ -0,0 +1,17 @@ +Episode Accumulated Reward Average Reward Length Steps +0 -5062.646915554269 -40.50117532443415 125 125 +1 -4545.443228168109 -36.36354582534487 125 250 +2 -3992.451522701582 -31.939612181612656 125 375 +3 -2660.945115772302 -21.287560926178415 125 500 +4 -1089.641113544413 -8.71712890835529 125 625 +5 -1794.3862709577143 -14.35509016766171 125 750 +6 -1599.3332228782826 -12.79466578302626 125 875 +7 -1999.3347944176303 -15.994678355341035 125 1000 +8 -1240.1770407677993 -9.921416326142396 125 1125 +9 -1128.717151496786 -9.029737211974288 125 1250 +10 -1148.8528175884883 -9.19082254070789 125 1375 +11 -1199.5840778420286 -9.596672622736232 125 1500 +12 -1147.0703774473068 -9.176563019578447 125 1625 +13 -1245.4139074019245 -9.963311259215399 125 1750 +14 -1257.9333517907346 -10.063466814325885 125 1875 +15 -1309.9607605947551 -10.479686084758042 125 2000 diff --git a/irlc/lectures/lec07/pendulum12/2022-03-17_14-16-10.758/trajectories.pkl b/irlc/lectures/lec07/pendulum12/2022-03-17_14-16-10.758/trajectories.pkl new file mode 100644 index 0000000000000000000000000000000000000000..c2b34f7154ed75b8dfa9e69bcd3de982d0cd0eb1 Binary files /dev/null and b/irlc/lectures/lec07/pendulum12/2022-03-17_14-16-10.758/trajectories.pkl differ diff --git a/irlc/lectures/lec07/pendulum12_lqr/2023-03-17_08-13-45.172/log.txt b/irlc/lectures/lec07/pendulum12_lqr/2023-03-17_08-13-45.172/log.txt new file mode 100644 index 0000000000000000000000000000000000000000..643cee39646b4d0a0ee0189cf724ae538a4cd508 --- /dev/null +++ b/irlc/lectures/lec07/pendulum12_lqr/2023-03-17_08-13-45.172/log.txt @@ -0,0 +1,17 @@ +Episode Accumulated Reward Length Steps +0 -5042.020051692956 125 0 +1 -4627.801003133058 125 1 +2 -3635.9503089227105 125 2 +3 -2459.2456085370436 125 3 +4 -1142.0454750510762 125 4 +5 -1563.5408392433658 125 5 +6 -1718.6689962603696 125 6 +7 -1215.2631997008277 125 7 +8 -1172.9345344478274 125 8 +9 -1108.3729746371948 125 9 +10 -1012.6787060036193 125 10 +11 -1715.9593847985013 125 11 +12 -1009.5943996400636 125 12 +13 -1082.3121757069966 125 13 +14 -1248.0530347172762 125 14 +15 -1496.6826680867007 125 15 diff --git a/irlc/lectures/lec07/pendulum12_lqr/2023-03-17_08-13-45.172/trajectories.pkl b/irlc/lectures/lec07/pendulum12_lqr/2023-03-17_08-13-45.172/trajectories.pkl new file mode 100644 index 0000000000000000000000000000000000000000..3e9f2d19af2a1f4bdcdbc7a8922852a440795c46 Binary files /dev/null and b/irlc/lectures/lec07/pendulum12_lqr/2023-03-17_08-13-45.172/trajectories.pkl differ diff --git a/irlc/lectures/lec07/tmp-pdfcrop-10536.tex b/irlc/lectures/lec07/tmp-pdfcrop-10536.tex new file mode 100644 index 0000000000000000000000000000000000000000..ea5c21c82776ad1a4c43bc64861caa907b877528 --- /dev/null +++ b/irlc/lectures/lec07/tmp-pdfcrop-10536.tex @@ -0,0 +1,131 @@ +\catcode37 14 % percent +\catcode33 12 % exclam +\catcode34 12 % quote +\catcode35 6 % hash +\catcode39 12 % apostrophe +\catcode40 12 % left parenthesis +\catcode41 12 % right parenthesis +\catcode45 12 % minus +\catcode46 12 % period +\catcode60 12 % less +\catcode61 12 % equals +\catcode62 12 % greater +\catcode64 12 % at +\catcode91 12 % left square +\catcode93 12 % right square +\catcode96 12 % back tick +\catcode123 1 % left curly brace +\catcode125 2 % right curly brace +\catcode126 12 % tilde +\catcode`\#=6 % +\escapechar=92 % +\def\IfUndefined#1#2#3{% + \begingroup\expandafter\expandafter\expandafter\endgroup + \expandafter\ifx\csname#1\endcsname\relax + #2% + \else + #3% + \fi +} +\def\pdffilehex{746D702D70646663726F702D31303533362D696D672E706466} +\IfUndefined{pdfunescapehex}{% + \begingroup + \gdef\pdffile{}% + \def\do#1#2{% + \ifx\relax#2\relax + \ifx\relax#1\relax + \else + \errmessage{Invalid hex string, should not happen!}% + \fi + \else + \lccode`0="#1#2\relax + \lowercase{% + \xdef\pdffile{\pdffile0}% + }% + \expandafter\do + \fi + }% + \expandafter\do\pdffilehex\relax\relax + \endgroup +}{% + \edef\pdffile{\pdfunescapehex{\pdffilehex}}% +} +\immediate\write-1{Input file: \pdffile} +\pdfcompresslevel=9 \pdfoutput=1 % +\csname pdfmapfile\endcsname{} +\def\setpdfversion#1#2{% + \IfUndefined{pdfobjcompresslevel}{% + }{% + \ifnum#1=1 % + \ifnum#2<5 + \pdfobjcompresslevel=0 % + \else + \pdfobjcompresslevel=2 % + \fi + \fi + }% + \IfUndefined{pdfminorversion}{% + \IfUndefined{pdfoptionpdfminorversion}{% + }{% + \pdfoptionpdfminorversion=#2\relax + }% + }{% + \pdfminorversion=#2\relax + \IfUndefined{pdfmajorversion}{% + \ifnum#2=0 \pdfminorversion=5\fi} + {\pdfmajorversion=#1\relax}% + }% +} +\def\page #1 [#2 #3 #4 #5]{% + \count0=#1\relax + \setbox0=\hbox{% + \pdfximage page #1 mediabox{\pdffile}% + \pdfrefximage\pdflastximage + }% + \pdfhorigin=-#2bp\relax + \pdfvorigin=#3bp\relax + \pdfpagewidth=#4bp\relax + \advance\pdfpagewidth by -#2bp\relax + \pdfpageheight=#5bp\relax + \advance\pdfpageheight by -#3bp\relax + \ht0=\pdfpageheight + \shipout\box0\relax +} +\def\pageclip #1 [#2 #3 #4 #5][#6 #7 #8 #9]{% + \count0=#1\relax + \dimen0=#4bp\relax \advance\dimen0 by -#2bp\relax + \edef\imagewidth{\the\dimen0}% + \dimen0=#5bp\relax \advance\dimen0 by -#3bp\relax + \edef\imageheight{\the\dimen0}% + \pdfximage page #1 mediabox{\pdffile}% + \setbox0=\hbox{% + \kern -#2bp\relax + \lower #3bp\hbox{\pdfrefximage\pdflastximage}% + }% + \wd0=\imagewidth\relax + \ht0=\imageheight\relax + \dp0=0pt\relax + \pdfhorigin=#6pt\relax + \pdfvorigin=#7bp\relax + \pdfpagewidth=\imagewidth + \advance\pdfpagewidth by #6bp\relax + \advance\pdfpagewidth by #8bp\relax + \pdfpageheight=\imageheight\relax + \advance\pdfpageheight by #7bp\relax + \advance\pdfpageheight by #9bp\relax + \pdfxform0\relax + \shipout\hbox{\pdfrefxform\pdflastxform}% +}% +\def\pageinclude#1{% + \pdfhorigin=0pt\relax + \pdfvorigin=0pt\relax + \pdfximage page #1 mediabox{\pdffile}% + \setbox0=\hbox{\pdfrefximage\pdflastximage}% + \pdfpagewidth=\wd0\relax + \pdfpageheight=\ht0\relax + \advance\pdfpageheight by \dp0\relax + \shipout\hbox{% + \raise\dp0\box0\relax + }% +} +\setpdfversion{1}{4} diff --git a/irlc/lectures/lec07/tmp-pdfcrop-12592.tex b/irlc/lectures/lec07/tmp-pdfcrop-12592.tex new file mode 100644 index 0000000000000000000000000000000000000000..479d43b09cb3943939732adbb8bf9fc8cecbc151 --- /dev/null +++ b/irlc/lectures/lec07/tmp-pdfcrop-12592.tex @@ -0,0 +1,131 @@ +\catcode37 14 % percent +\catcode33 12 % exclam +\catcode34 12 % quote +\catcode35 6 % hash +\catcode39 12 % apostrophe +\catcode40 12 % left parenthesis +\catcode41 12 % right parenthesis +\catcode45 12 % minus +\catcode46 12 % period +\catcode60 12 % less +\catcode61 12 % equals +\catcode62 12 % greater +\catcode64 12 % at +\catcode91 12 % left square +\catcode93 12 % right square +\catcode96 12 % back tick +\catcode123 1 % left curly brace +\catcode125 2 % right curly brace +\catcode126 12 % tilde +\catcode`\#=6 % +\escapechar=92 % +\def\IfUndefined#1#2#3{% + \begingroup\expandafter\expandafter\expandafter\endgroup + \expandafter\ifx\csname#1\endcsname\relax + #2% + \else + #3% + \fi +} +\def\pdffilehex{746D702D70646663726F702D31323539322D696D672E706466} +\IfUndefined{pdfunescapehex}{% + \begingroup + \gdef\pdffile{}% + \def\do#1#2{% + \ifx\relax#2\relax + \ifx\relax#1\relax + \else + \errmessage{Invalid hex string, should not happen!}% + \fi + \else + \lccode`0="#1#2\relax + \lowercase{% + \xdef\pdffile{\pdffile0}% + }% + \expandafter\do + \fi + }% + \expandafter\do\pdffilehex\relax\relax + \endgroup +}{% + \edef\pdffile{\pdfunescapehex{\pdffilehex}}% +} +\immediate\write-1{Input file: \pdffile} +\pdfcompresslevel=9 \pdfoutput=1 % +\csname pdfmapfile\endcsname{} +\def\setpdfversion#1#2{% + \IfUndefined{pdfobjcompresslevel}{% + }{% + \ifnum#1=1 % + \ifnum#2<5 + \pdfobjcompresslevel=0 % + \else + \pdfobjcompresslevel=2 % + \fi + \fi + }% + \IfUndefined{pdfminorversion}{% + \IfUndefined{pdfoptionpdfminorversion}{% + }{% + \pdfoptionpdfminorversion=#2\relax + }% + }{% + \pdfminorversion=#2\relax + \IfUndefined{pdfmajorversion}{% + \ifnum#2=0 \pdfminorversion=5\fi} + {\pdfmajorversion=#1\relax}% + }% +} +\def\page #1 [#2 #3 #4 #5]{% + \count0=#1\relax + \setbox0=\hbox{% + \pdfximage page #1 mediabox{\pdffile}% + \pdfrefximage\pdflastximage + }% + \pdfhorigin=-#2bp\relax + \pdfvorigin=#3bp\relax + \pdfpagewidth=#4bp\relax + \advance\pdfpagewidth by -#2bp\relax + \pdfpageheight=#5bp\relax + \advance\pdfpageheight by -#3bp\relax + \ht0=\pdfpageheight + \shipout\box0\relax +} +\def\pageclip #1 [#2 #3 #4 #5][#6 #7 #8 #9]{% + \count0=#1\relax + \dimen0=#4bp\relax \advance\dimen0 by -#2bp\relax + \edef\imagewidth{\the\dimen0}% + \dimen0=#5bp\relax \advance\dimen0 by -#3bp\relax + \edef\imageheight{\the\dimen0}% + \pdfximage page #1 mediabox{\pdffile}% + \setbox0=\hbox{% + \kern -#2bp\relax + \lower #3bp\hbox{\pdfrefximage\pdflastximage}% + }% + \wd0=\imagewidth\relax + \ht0=\imageheight\relax + \dp0=0pt\relax + \pdfhorigin=#6pt\relax + \pdfvorigin=#7bp\relax + \pdfpagewidth=\imagewidth + \advance\pdfpagewidth by #6bp\relax + \advance\pdfpagewidth by #8bp\relax + \pdfpageheight=\imageheight\relax + \advance\pdfpageheight by #7bp\relax + \advance\pdfpageheight by #9bp\relax + \pdfxform0\relax + \shipout\hbox{\pdfrefxform\pdflastxform}% +}% +\def\pageinclude#1{% + \pdfhorigin=0pt\relax + \pdfvorigin=0pt\relax + \pdfximage page #1 mediabox{\pdffile}% + \setbox0=\hbox{\pdfrefximage\pdflastximage}% + \pdfpagewidth=\wd0\relax + \pdfpageheight=\ht0\relax + \advance\pdfpageheight by \dp0\relax + \shipout\hbox{% + \raise\dp0\box0\relax + }% +} +\setpdfversion{1}{4} diff --git a/irlc/lectures/lec08/__init__.py b/irlc/lectures/lec08/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/lectures/lec08/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/lectures/lec08/demo_bandit.py b/irlc/lectures/lec08/demo_bandit.py new file mode 100644 index 0000000000000000000000000000000000000000..c1f61c38c14d03d20b91d95a773e31e0e5c9bfe1 --- /dev/null +++ b/irlc/lectures/lec08/demo_bandit.py @@ -0,0 +1,25 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex08.devel.bandit_graphics_environment import GraphicalBandit +import time +from irlc import train +from irlc.ex08.simple_agents import BasicAgent +from irlc import interactive + +def bandit_eps(autoplay=False): + env = GraphicalBandit(10, render_mode='human',frames_per_second=30) + env.reset() + #env.viewer.show_q_star = True + # env.show_q_ucb = True + agent = BasicAgent(env, epsilon=0.1) + agent.method = 'Epsilon-greedy' + env, agent = interactive(env, agent, autoplay=autoplay) + + t0 = time.time() + n = 3000 + stats, _ = train(env, agent, max_steps=n, num_episodes=10, return_trajectory=False, verbose=False) + tpf = (time.time()-t0)/ n + print("tpf", tpf, 'fps', 1/tpf) + env.close() + +if __name__ == "__main__": + bandit_eps() diff --git a/irlc/lectures/lec08/demo_bandit_ucb.py b/irlc/lectures/lec08/demo_bandit_ucb.py new file mode 100644 index 0000000000000000000000000000000000000000..8e596321207df550e83ae0c6176b19af0704249b --- /dev/null +++ b/irlc/lectures/lec08/demo_bandit_ucb.py @@ -0,0 +1,26 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex08.devel.bandit_graphics_environment import GraphicalBandit +from irlc import interactive, train +# import numpy as np +import time + +def bandit_ucb(autoplay=False): + env = GraphicalBandit(10, render_mode='human', frames_per_second=30) + env.reset() + #env.viewer.show_q_star = True + #env.viewer.show_q_ucb = True + from irlc.ex08.ucb_agent import UCBAgent + agent = UCBAgent(env, c=1) + agent.method = 'UCB' + + env, agent = interactive(env, agent, autoplay=autoplay) + t0 = time.time() + n = 500 + stats, _ = train(env, agent, max_steps=n, num_episodes=10, return_trajectory=False, verbose=False) + tpf = (time.time() - t0) / n + print("tpf", tpf, 'fps', 1 / tpf) + env.close() + + +if __name__ == "__main__": + bandit_ucb() diff --git a/irlc/lectures/lec09/__init__.py b/irlc/lectures/lec09/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/lectures/lec09/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/lectures/lec09/unf_frozenlake.py b/irlc/lectures/lec09/unf_frozenlake.py new file mode 100644 index 0000000000000000000000000000000000000000..421bf1b9a76fe0325ec334122297d155f8fe3ab1 --- /dev/null +++ b/irlc/lectures/lec09/unf_frozenlake.py @@ -0,0 +1,11 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex01.agent import Agent +from irlc.gridworld.gridworld_environments import BookGridEnvironment, FrozenLake, FrozenLakeEnv +from irlc import interactive, train + +if __name__ == "__main__": + env = FrozenLake(render_mode='human', print_states=True) + env, agent = interactive(env, Agent(env)) + agent.label = "Random agent" + train(env, agent, num_episodes=100, verbose=False) + env.close() diff --git a/irlc/lectures/lec09/unf_gridworld.py b/irlc/lectures/lec09/unf_gridworld.py new file mode 100644 index 0000000000000000000000000000000000000000..e5458d9db1ad9b31db7f7d9665173824c3408594 --- /dev/null +++ b/irlc/lectures/lec09/unf_gridworld.py @@ -0,0 +1,12 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex01.agent import Agent +from irlc.gridworld.gridworld_environments import BookGridEnvironment, FrozenLakeEnv +from irlc import interactive, train + + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human', print_states=True, living_reward=-0.05) + env, agent = interactive(env, Agent(env)) + agent.label = "Random agent" + train(env, agent, num_episodes=100, verbose=False) + env.close() diff --git a/irlc/lectures/lec09/unf_policy_evaluation_frozen.py b/irlc/lectures/lec09/unf_policy_evaluation_frozen.py new file mode 100644 index 0000000000000000000000000000000000000000..9adda9fd454d02ed5809cb122d70365c9130c148 --- /dev/null +++ b/irlc/lectures/lec09/unf_policy_evaluation_frozen.py @@ -0,0 +1,20 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.gridworld.gridworld_environments import FrozenLake +from irlc import interactive, train +from irlc.gridworld.demo_agents.hidden_agents import PolicyEvaluationAgent2 + +def policy_evaluation(env=None): + agent = PolicyEvaluationAgent2(env, gamma=1., steps_between_policy_improvement=None) + env, agent = interactive(env, agent) + train(env, agent, num_episodes=100) + env.close() + +def policy_improvement(env=None, q_mode=True): + agent = PolicyEvaluationAgent2(env, gamma=1.,steps_between_policy_improvement=20) + env, agent = interactive(env, agent) + train(env, agent, num_episodes=1000, verbose=False) + env.close() + +if __name__ == "__main__": + env = FrozenLake(render_mode='human', living_reward=-0.0) + policy_evaluation(env) diff --git a/irlc/lectures/lec09/unf_policy_evaluation_gridworld.py b/irlc/lectures/lec09/unf_policy_evaluation_gridworld.py new file mode 100644 index 0000000000000000000000000000000000000000..efec41198536780a74ac8db2c12bd325a703fd09 --- /dev/null +++ b/irlc/lectures/lec09/unf_policy_evaluation_gridworld.py @@ -0,0 +1,20 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.gridworld.gridworld_environments import BookGridEnvironment +from irlc import interactive, train +from irlc.gridworld.demo_agents.hidden_agents import PolicyEvaluationAgent2 + +def policy_evaluation(env=None): + agent = PolicyEvaluationAgent2(env, gamma=1., steps_between_policy_improvement=None) + env, agent = interactive(env, agent) + train(env, agent, num_episodes=100) + env.close() + +def policy_improvement(env=None, q_mode=True): + agent = PolicyEvaluationAgent2(env, gamma=1.,steps_between_policy_improvement=20) + env, agent = interactive(env, agent) + train(env, agent, num_episodes=1000) + env.close() + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human', living_reward=-0.05) + policy_evaluation(env) diff --git a/irlc/lectures/lec09/unf_policy_evaluation_stepwise_gridworld.py b/irlc/lectures/lec09/unf_policy_evaluation_stepwise_gridworld.py new file mode 100644 index 0000000000000000000000000000000000000000..a438af8f6d67f7d80b09c1efffdda5e69af84fb4 --- /dev/null +++ b/irlc/lectures/lec09/unf_policy_evaluation_stepwise_gridworld.py @@ -0,0 +1,20 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.gridworld.gridworld_environments import BookGridEnvironment +from irlc import interactive, train +from irlc.gridworld.demo_agents.hidden_agents import PolicyEvaluationAgent2 + +def policy_evaluation_stepwise(env=None): + agent = PolicyEvaluationAgent2(env, gamma=1., steps_between_policy_improvement=None, only_update_current=True) + env, agent = interactive(env, agent) + train(env, agent, num_episodes=100) + env.close() + +def policy_improvement(env=None, q_mode=True): + agent = PolicyEvaluationAgent2(env, gamma=1.,steps_between_policy_improvement=20) + env, agent = interactive(env, agent) + train(env, agent, num_episodes=1000) + env.close() + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human', living_reward=-0.05) + policy_evaluation_stepwise(env) diff --git a/irlc/lectures/lec09/unf_policy_improvement_frozenlake.py b/irlc/lectures/lec09/unf_policy_improvement_frozenlake.py new file mode 100644 index 0000000000000000000000000000000000000000..7242b00a8195c5c368bfb4d9abebc79498e35b1c --- /dev/null +++ b/irlc/lectures/lec09/unf_policy_improvement_frozenlake.py @@ -0,0 +1,7 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.gridworld.gridworld_environments import BookGridEnvironment, FrozenLake +from irlc.lectures.unf.unf_policy_evaluation_gridworld import policy_improvement + +if __name__ == "__main__": + env = FrozenLake(render_mode='human', living_reward=-0) + policy_improvement(env) diff --git a/irlc/lectures/lec09/unf_policy_improvement_gridworld.py b/irlc/lectures/lec09/unf_policy_improvement_gridworld.py new file mode 100644 index 0000000000000000000000000000000000000000..eb6d7623bf8b566aed9e589d4586256d2b5b3fd5 --- /dev/null +++ b/irlc/lectures/lec09/unf_policy_improvement_gridworld.py @@ -0,0 +1,7 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.gridworld.gridworld_environments import BookGridEnvironment +from irlc.lectures.unf.unf_policy_evaluation_gridworld import policy_improvement + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human', living_reward=-0.05) + policy_improvement(env) diff --git a/irlc/lectures/lec09/unf_vi_frozenlake.py b/irlc/lectures/lec09/unf_vi_frozenlake.py new file mode 100644 index 0000000000000000000000000000000000000000..4ece4f2e944c076be048a9fdae6f453ce6ff01ad --- /dev/null +++ b/irlc/lectures/lec09/unf_vi_frozenlake.py @@ -0,0 +1,17 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.gridworld.gridworld_environments import FrozenLake +from irlc.ex01.agent import train +from irlc.gridworld.demo_agents.hidden_agents import ValueIterationAgent3 +from irlc import interactive + +def q1_vi(env): + agent = ValueIterationAgent3(env, epsilon=0, gamma=1, only_update_current=False) + env, agent = interactive(env, agent) + env.reset() + train(env, agent, num_episodes=100) + env.close() + + +if __name__ == "__main__": + env = FrozenLake(render_mode='human', living_reward=-0) + q1_vi(env) diff --git a/irlc/lectures/lec09/unf_vi_gridworld.py b/irlc/lectures/lec09/unf_vi_gridworld.py new file mode 100644 index 0000000000000000000000000000000000000000..56319aff50475f896188825feba5b86012791582 --- /dev/null +++ b/irlc/lectures/lec09/unf_vi_gridworld.py @@ -0,0 +1,19 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.gridworld.gridworld_environments import BookGridEnvironment +# from irlc.utils.video_monitor import VideoMonitor +from irlc.ex01.agent import train +from irlc.gridworld.demo_agents.hidden_agents import ValueIterationAgent3 +from irlc import interactive + +def q1_vi(env): + agent = ValueIterationAgent3(env, epsilon=0, gamma=1, only_update_current=False) + env, agent = interactive(env, agent) + # experiment = "experiments/q1_value_iteration" + # env = VideoMonitor(env, agent=agent, fps=100, continious_recording=True, agent_monitor_keys=('v', 'v2Q'), render_kwargs={'method_label': 'VI'}) + env.reset() + train(env, agent, num_episodes=100) + env.close() + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human', living_reward=-0.05) + q1_vi(env) diff --git a/irlc/lectures/lec09/unf_vi_gridworld_stepwise.py b/irlc/lectures/lec09/unf_vi_gridworld_stepwise.py new file mode 100644 index 0000000000000000000000000000000000000000..152a91b25114104a470fc43d4611a36601e9d9c0 --- /dev/null +++ b/irlc/lectures/lec09/unf_vi_gridworld_stepwise.py @@ -0,0 +1,16 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.gridworld.gridworld_environments import BookGridEnvironment +from irlc.ex01.agent import train +from irlc.gridworld.demo_agents.hidden_agents import ValueIterationAgent3 +from irlc import interactive + +def q1_vi(env): + agent = ValueIterationAgent3(env, epsilon=0, gamma=1, only_update_current=True) + env, agent = interactive(env, agent) + env.reset() + train(env, agent, num_episodes=100) + env.close() + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human', living_reward=-0.05, print_states=False) + q1_vi(env) diff --git a/irlc/lectures/lec10/__init__.py b/irlc/lectures/lec10/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/lectures/lec10/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/lectures/lec10/lecture_10_mc_action_value_first_one_state.py b/irlc/lectures/lec10/lecture_10_mc_action_value_first_one_state.py new file mode 100644 index 0000000000000000000000000000000000000000..a55be35a5aad6a1457b56227681f306174dc6108 --- /dev/null +++ b/irlc/lectures/lec10/lecture_10_mc_action_value_first_one_state.py @@ -0,0 +1,60 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec10.lecture_10_mc_q_estimation import keyboard_play +from irlc.gridworld.gridworld_environments import SuttonCornerGridEnvironment, BookGridEnvironment +from irlc.ex10.mc_agent import MCAgent + +from irlc.ex10.mc_evaluate import MCEvaluationAgent +import numpy as np +from irlc import interactive, train + +class MCControlAgentOneState(MCAgent): + def __init__(self, *args, state_action=None, **kwargs): + a = 34 + super().__init__(*args, **kwargs) + if state_action is None: + state_action = (self.env.mdp.initial_state, self.env.mdp.A(self.env.mdp.initial_state)[0]) + + self.state_action = state_action + self._clear_states() + + def _clear_states(self, val=None): + for s in self.env.mdp.nonterminal_states: + for a in self.env.mdp.A(s): + # self.Q[s,a] = 0 + if (s,a) != self.state_action: + self.returns_sum[s,a] = val + self.returns_count[s,a] = val + + # if s in self.Q.q_: + k = next(self.env.mdp.Psr(s, self.env.mdp.A(s)[0]).keys().__iter__() )[0] + if not self.env.mdp.is_terminal(k): + self.Q[s,a] = 0 + + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + # self.episode = [e for e in self.episode if e[0] == self.state] + self._clear_states(0) + super().train(s, a, r, sp, done) + # Clear out many of the state, actions: + self._clear_states(None) + # for s in self.env.mdp.nonterminal_states: + # if s != self.state: + # self.v[s] = None + + pass + + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human', living_reward=-0.05, print_states=True, zoom=2) + agent = MCControlAgentOneState(env, gamma=1, alpha=None, first_visit=True) + method_label = 'MC (gamma=1)' + agent.label = method_label + autoplay = False + env, agent = interactive(env, agent, autoplay=autoplay) + # agent = PlayWrapper(agent, env,autoplay=autoplay) + # env = VideoMonitor(env, agent=agent, fps=100, agent_monitor_keys=('pi', 'Q'), render_kwargs={'method_label': method_label}) + num_episodes = 1000 + train(env, agent, num_episodes=num_episodes) + env.close() + + # keyboard_play(env,agent,method_label='MC (alpha=0.5)') diff --git a/irlc/lectures/lec10/lecture_10_mc_action_value_first_one_state_b.py b/irlc/lectures/lec10/lecture_10_mc_action_value_first_one_state_b.py new file mode 100644 index 0000000000000000000000000000000000000000..f0f705bb72f4592cfe19888e2e8ad5406e2a3ca9 --- /dev/null +++ b/irlc/lectures/lec10/lecture_10_mc_action_value_first_one_state_b.py @@ -0,0 +1,21 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec10.lecture_10_mc_q_estimation import keyboard_play +from irlc.gridworld.gridworld_environments import SuttonCornerGridEnvironment, BookGridEnvironment +from irlc.ex10.mc_agent import MCAgent +from irlc.lectures.lec10.lecture_10_mc_action_value_first_one_state import MCControlAgentOneState +from irlc.ex10.mc_evaluate import MCEvaluationAgent +import numpy as np +from irlc import interactive, train + + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human', living_reward=-0.05, print_states=True, zoom=2) + agent = MCControlAgentOneState(env, gamma=1, alpha=None, first_visit=True, state_action=( (0,2), 2)) + method_label = 'MC control (gamma=1)' + agent.label = method_label + autoplay = False + env, agent = interactive(env, agent, autoplay=autoplay) + num_episodes = 1000 + train(env, agent, num_episodes=num_episodes) + env.close() + # keyboard_play(env,agent,method_label='MC (alpha=0.5)') diff --git a/irlc/lectures/lec10/lecture_10_mc_control.py b/irlc/lectures/lec10/lecture_10_mc_control.py new file mode 100644 index 0000000000000000000000000000000000000000..e286478a8cabf26f4878528a1fbc0f402e5c25ef --- /dev/null +++ b/irlc/lectures/lec10/lecture_10_mc_control.py @@ -0,0 +1,13 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec10.lecture_10_mc_q_estimation import keyboard_play +from irlc.gridworld.gridworld_environments import BookGridEnvironment +from irlc.ex10.mc_agent import MCAgent +import numpy as np + +if __name__ == "__main__": + np.random.seed(433) + env = BookGridEnvironment(render_mode='human',zoom=2) + # agent = MCAgent(env, gamma=0.9, epsilon=0.15, alpha=0.1, first_visit=True) + agent = MCAgent(env, gamma=1.0, epsilon=0.15, alpha=None, first_visit=True) + # env, agent = interactive(env, agent) + keyboard_play(env,agent,method_label='MC control') diff --git a/irlc/lectures/lec10/lecture_10_mc_corner.py b/irlc/lectures/lec10/lecture_10_mc_corner.py new file mode 100644 index 0000000000000000000000000000000000000000..a1ec3e1490270533cb4259ad9f83b2f9bc22bac2 --- /dev/null +++ b/irlc/lectures/lec10/lecture_10_mc_corner.py @@ -0,0 +1,10 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec10.lecture_10_mc_q_estimation import keyboard_play +from irlc.gridworld.gridworld_environments import SuttonCornerGridEnvironment +from irlc.ex10.mc_agent import MCAgent +import numpy as np + +if __name__ == "__main__": + env = SuttonCornerGridEnvironment(render_mode='human') + agent = MCAgent(env, gamma=1, epsilon=1, alpha=.5, first_visit=False) + keyboard_play(env,agent,method_label='MC (alpha=0.5)') diff --git a/irlc/lectures/lec10/lecture_10_mc_onestate_every.py b/irlc/lectures/lec10/lecture_10_mc_onestate_every.py new file mode 100644 index 0000000000000000000000000000000000000000..710532e519d8971fd7a819c4bb532e4bf253e15e --- /dev/null +++ b/irlc/lectures/lec10/lecture_10_mc_onestate_every.py @@ -0,0 +1,12 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.exam_tabular_examples.helper import keyboard_play_value +# from irlc.gridworld_pyglet.gridworld_environments import BookGridEnvironment +from irlc.ex10.mc_evaluate import MCEvaluationAgent +# from irlc.gridworld_pyglet.gridworld_environments import GridworldEnvironment +from irlc.lectures.lec10.lecture_10_mc_onestate_first import CaughtGrid + + +if __name__ == "__main__": + env = CaughtGrid(view_mode=1, render_mode='humanp') + agent = MCEvaluationAgent(env, gamma=1, alpha=None, first_visit=False) + keyboard_play_value(env,agent,method_label='MC (every visit)') diff --git a/irlc/lectures/lec10/lecture_10_mc_onestate_first.py b/irlc/lectures/lec10/lecture_10_mc_onestate_first.py new file mode 100644 index 0000000000000000000000000000000000000000..c111aa624334fe8611d496bff8bd41ca0dd01ee4 --- /dev/null +++ b/irlc/lectures/lec10/lecture_10_mc_onestate_first.py @@ -0,0 +1,18 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.exam_tabular_examples.helper import keyboard_play_value +# from irlc.gridworld_pyglet.gridworld_environments import BookGridEnvironment +from irlc.ex10.mc_evaluate import MCEvaluationAgent +from irlc.gridworld.gridworld_environments import GridworldEnvironment + +map = [['#', '#', '#', '#'], + ['#','S',0,'#'], + ['#','#','#','#']] + +class CaughtGrid(GridworldEnvironment): + def __init__(self, **kwargs): + super().__init__(map, living_reward=1, zoom=1.5, **kwargs) + +if __name__ == "__main__": + env = CaughtGrid(view_mode=1, render_mode='human') + agent = MCEvaluationAgent(env, gamma=1, alpha=None) + keyboard_play_value(env,agent,method_label='MC (first visit)') diff --git a/irlc/lectures/lec10/lecture_10_mc_q_estimation.py b/irlc/lectures/lec10/lecture_10_mc_q_estimation.py new file mode 100644 index 0000000000000000000000000000000000000000..4b6ef32a716a3ab261a6adfb07c6fd0bc9250f0e --- /dev/null +++ b/irlc/lectures/lec10/lecture_10_mc_q_estimation.py @@ -0,0 +1,40 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# from irlc.berkley.rl.feature_encoder import SimplePacmanExtractor +# from irlc.utils.player_wrapper_pyglet import PlayWrapper +# from irlc.gridworld.gridworld import BerkleyBookGridEnvironment +from irlc.gridworld.gridworld_environments import BookGridEnvironment +# from irlc.utils.video_monitor import VideoMonitor +from irlc import train, interactive +# from irlc import interactive + +def keyboard_play(env, agent, method_label='MC',autoplay=False, num_episodes=1000): + agent.label = method_label + env, agent = interactive(env, agent, autoplay=autoplay) + # agent = PlayWrapper(agent, env,autoplay=autoplay) + # env = VideoMonitor(env, agent=agent, fps=100, agent_monitor_keys=('pi', 'Q'), render_kwargs={'method_label': method_label}) + train(env, agent, num_episodes=num_episodes) + env.close() + + +def automatic_play(env, agent, method_label='MC'): + # agent = PlayWrapper(agent, env) + env = VideoMonitor(env, agent=agent, fps=40, continious_recording=True, agent_monitor_keys=('pi', 'Q'), render_kwargs={'method_label': method_label}) + train(env, agent, num_episodes=1000) + env.close() + +def automatic_play_value(env, agent, method_label='MC'): + agent.label = method_label + env, agent = interactive(env, agent) + + # env = VideoMonitor(env, agent=agent, fps=40, continious_recording=True, agent_monitor_keys=('v'), render_kwargs={'method_label': method_label}) + # agent = PlayWrapper(agent, env) + train(env, agent, num_episodes=1000) + env.close() + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human', zoom=2, living_reward=-0.05) + from irlc.ex10.mc_agent import MCAgent + agent = MCAgent(env, gamma=0.9, epsilon=1., first_visit=True, alpha=None) + # agent.label = + # env, agent = interactive(env, agent) + keyboard_play(env, agent, method_label='MC Q-estimation (First visit)') diff --git a/irlc/lectures/lec10/lecture_10_mc_value_every.py b/irlc/lectures/lec10/lecture_10_mc_value_every.py new file mode 100644 index 0000000000000000000000000000000000000000..8598fa5e78834d5337f33217a21eeb7694af587e --- /dev/null +++ b/irlc/lectures/lec10/lecture_10_mc_value_every.py @@ -0,0 +1,11 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.exam_tabular_examples.helper import keyboard_play_value +# from irlc.berkley.rl.feature_encoder import SimplePacmanExtractor +from irlc.gridworld.gridworld_environments import BookGridEnvironment +from irlc.ex10.mc_evaluate import MCEvaluationAgent + +if __name__ == "__main__": + env = BookGridEnvironment(view_mode=1, render_mode='human', living_reward=-0.05) + agent = MCEvaluationAgent(env, gamma=.9, alpha=None, first_visit=False) + + keyboard_play_value(env,agent,method_label='MC every') diff --git a/irlc/lectures/lec10/lecture_10_mc_value_every_one_state.py b/irlc/lectures/lec10/lecture_10_mc_value_every_one_state.py new file mode 100644 index 0000000000000000000000000000000000000000..bd86fafbf767e54a0313aece2d7b38102fc8f6a7 --- /dev/null +++ b/irlc/lectures/lec10/lecture_10_mc_value_every_one_state.py @@ -0,0 +1,58 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec10.lecture_10_mc_q_estimation import keyboard_play +from irlc.gridworld.gridworld_environments import SuttonCornerGridEnvironment, BookGridEnvironment +from irlc.lectures.lec10.lecture_10_mc_value_first_one_state import MCAgentOneState +from irlc.ex10.mc_agent import MCAgent +from irlc.ex10.mc_evaluate import MCEvaluationAgent +import numpy as np +from irlc import interactive, train + +# class MCAgentOneState(MCEvaluationAgent): +# def __init__(self, *args, state=None, **kwargs): +# a = 34 +# super().__init__(*args, **kwargs) +# if state is None: +# state = self.env.mdp.initial_state +# self.state = state +# self._clear_states() +# +# def _clear_states(self, val=None): +# for s in self.env.mdp.nonterminal_states: +# # for a in self.env.mdp.A(s): +# # self.Q[s,a] = 0 +# if s != self.state: +# self.returns_sum_S[s] = val +# self.returns_count_N[s] = val +# +# if s in self.v: +# k = next(self.env.mdp.Psr(s, self.env.mdp.A(s)[0]).keys().__iter__() )[0] +# if not self.env.mdp.is_terminal(k): +# +# del self.v[s] +# +# +# def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): +# # self.episode = [e for e in self.episode if e[0] == self.state] +# self._clear_states(0) +# super().train(s, a, r, sp, done) +# # Clear out many of the state, actions: +# self._clear_states(None) +# # for s in self.env.mdp.nonterminal_states: +# # if s != self.state: +# # self.v[s] = None +# pass + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human', living_reward=-0.05, print_states=True) + agent = MCAgentOneState(env, gamma=1, alpha=None, first_visit=False) + method_label = 'MC (gamma=1)' + agent.label = method_label + autoplay = False + env, agent = interactive(env, agent, autoplay=autoplay) + # agent = PlayWrapper(agent, env,autoplay=autoplay) + # env = VideoMonitor(env, agent=agent, fps=100, agent_monitor_keys=('pi', 'Q'), render_kwargs={'method_label': method_label}) + num_episodes = 1000 + train(env, agent, num_episodes=num_episodes) + env.close() + + # keyboard_play(env,agent,method_label='MC (alpha=0.5)') diff --git a/irlc/lectures/lec10/lecture_10_mc_value_first.py b/irlc/lectures/lec10/lecture_10_mc_value_first.py new file mode 100644 index 0000000000000000000000000000000000000000..549b79754bf4508691f5182032566a7562e56b6e --- /dev/null +++ b/irlc/lectures/lec10/lecture_10_mc_value_first.py @@ -0,0 +1,32 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.gridworld.gridworld_environments import BookGridEnvironment, BridgeGridEnvironment, GridworldEnvironment +from irlc.ex10.mc_evaluate import MCEvaluationAgent +from irlc import interactive, train + +class BridgeGridEnvironment2(GridworldEnvironment): + def __init__(self, *args, **kwargs): + super().__init__(grid_bridge_grid, *args, **kwargs) + + +grid_bridge_grid = [[ '#',-100, -100, -100, -100, -100, '#'], + [ 1, ' ', 'S', ' ', ' ', ' ', 2], + [ '#',-100, -100, -100, -100, -100, '#']] + + +if __name__ == "__main__": + + # env = BridgeGridEnvironment2(view_mode=1, render_mode='human', living_reward=0) + # agent = MCEvaluationAgent(env, gamma=.8, alpha=None, first_visit=False) + # env, agent = interactive(env, agent) + # train(env, agent, num_episodes=1000) + # env.close() + + env = BookGridEnvironment(view_mode=1, render_mode='human', living_reward=-0.05) + agent = MCEvaluationAgent(env, gamma=1, alpha=None) + # agent = PlayWrapper(agent, env) + agent.label = 'MC First (gamma=1)' + env, agent = interactive(env, agent) + env.view_mode = 1 # Automatically set value-function view-mode. + # env = VideoMonitor(env, agent=agent, fps=200, render_kwargs={'method_label': 'MC first'}) + train(env, agent, num_episodes=1000) + env.close() diff --git a/irlc/lectures/lec10/lecture_10_mc_value_first_one_state.py b/irlc/lectures/lec10/lecture_10_mc_value_first_one_state.py new file mode 100644 index 0000000000000000000000000000000000000000..c998543f234744811dbbf68613dce641776f1934 --- /dev/null +++ b/irlc/lectures/lec10/lecture_10_mc_value_first_one_state.py @@ -0,0 +1,64 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec10.lecture_10_mc_q_estimation import keyboard_play +from irlc.gridworld.gridworld_environments import SuttonCornerGridEnvironment, BookGridEnvironment +from irlc.ex10.mc_agent import MCAgent +from irlc.ex10.mc_evaluate import MCEvaluationAgent +import numpy as np +from irlc import interactive, train + +class MCAgentOneState(MCEvaluationAgent): + def __init__(self, *args, state=None, **kwargs): + a = 34 + super().__init__(*args, **kwargs) + if state is None: + state = self.env.mdp.initial_state + self.state = state + self._clear_states() + + def _clear_states(self, val=None): + for s in self.env.mdp.nonterminal_states: + # for a in self.env.mdp.A(s): + # self.Q[s,a] = 0 + if s != self.state: + self.returns_sum_S[s] = val + self.returns_count_N[s] = val + + if s in self.v: + k = next(self.env.mdp.Psr(s, self.env.mdp.A(s)[0]).keys().__iter__() )[0] + if not self.env.mdp.is_terminal(k): + + del self.v[s] + + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + # self.episode = [e for e in self.episode if e[0] == self.state] + self._clear_states(0) + super().train(s, a, r, sp, done) + self._clear_states(None) + + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human', living_reward=-0.05, print_states=True, zoom=2) + agent = MCAgentOneState(env, gamma=1, alpha=None, first_visit=True) + method_label = 'MC (gamma=1)' + agent.label = method_label + autoplay = False + env, agent = interactive(env, agent, autoplay=autoplay) + # agent = PlayWrapper(agent, env,autoplay=autoplay) + # env = VideoMonitor(env, agent=agent, fps=100, agent_monitor_keys=('pi', 'Q'), render_kwargs={'method_label': method_label}) + num_episodes = 1000 + train(env, agent, num_episodes=num_episodes) + env.close() + + import matplotlib.pyplot as plt + import numpy as np + + import matplotlib.pyplot as plt + import numpy as np + + lt = np.linspace(np.log(1000), np.log(2000) + 0*5000) + plt.plot(lt, 5 + 2 * np.sqrt(lt / 500), 'k-') + plt.plot(lt, 10 + 2 * np.sqrt(lt / (np.exp(lt) - 500)), 'r-') + plt.xlabel('log(t)') + plt.show() + # keyboard_play(env,agent,method_label='MC (alpha=0.5)') diff --git a/irlc/lectures/lec10/lecture_10_mc_value_first_one_state_b.py b/irlc/lectures/lec10/lecture_10_mc_value_first_one_state_b.py new file mode 100644 index 0000000000000000000000000000000000000000..6567221b84c2df45f4c73f7921df5173c7e66608 --- /dev/null +++ b/irlc/lectures/lec10/lecture_10_mc_value_first_one_state_b.py @@ -0,0 +1,58 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec10.lecture_10_mc_q_estimation import keyboard_play +from irlc.gridworld.gridworld_environments import SuttonCornerGridEnvironment, BookGridEnvironment +from irlc.lectures.lec10.lecture_10_mc_value_first_one_state import MCAgentOneState +from irlc.ex10.mc_agent import MCAgent +from irlc.ex10.mc_evaluate import MCEvaluationAgent +import numpy as np +from irlc import interactive, train + +# class MCAgentOneState(MCEvaluationAgent): +# def __init__(self, *args, state=None, **kwargs): +# a = 34 +# super().__init__(*args, **kwargs) +# if state is None: +# state = self.env.mdp.initial_state +# self.state = state +# self._clear_states() +# +# def _clear_states(self, val=None): +# for s in self.env.mdp.nonterminal_states: +# # for a in self.env.mdp.A(s): +# # self.Q[s,a] = 0 +# if s != self.state: +# self.returns_sum_S[s] = val +# self.returns_count_N[s] = val +# if s in self.v: +# k = next(self.env.mdp.Psr(s, self.env.mdp.A(s)[0]).keys().__iter__() )[0] +# if not self.env.mdp.is_terminal(k): +# +# del self.v[s] +# +# def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): +# # self.episode = [e for e in self.episode if e[0] == self.state] +# self._clear_states(0) +# super().train(s, a, r, sp, done) +# # Clear out many of the state, actions: +# self._clear_states(None) +# # for s in self.env.mdp.nonterminal_states: +# # if s != self.state: +# # self.v[s] = None +# +# pass + + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human', living_reward=-0.05) + agent = MCAgentOneState(env, gamma=1, alpha=None, first_visit=True, state=(0,2)) + method_label = 'MC (gamma=1)' + agent.label = method_label + autoplay = False + env, agent = interactive(env, agent, autoplay=autoplay) + # agent = PlayWrapper(agent, env,autoplay=autoplay) + # env = VideoMonitor(env, agent=agent, fps=100, agent_monitor_keys=('pi', 'Q'), render_kwargs={'method_label': method_label}) + num_episodes = 1000 + train(env, agent, num_episodes=num_episodes) + env.close() + + # keyboard_play(env,agent,method_label='MC (alpha=0.5)') diff --git a/irlc/lectures/lec10/lecture_10_td_corner.py b/irlc/lectures/lec10/lecture_10_td_corner.py new file mode 100644 index 0000000000000000000000000000000000000000..e2aa0cde7955dac16fd6a97edb13339c3ed68ad7 --- /dev/null +++ b/irlc/lectures/lec10/lecture_10_td_corner.py @@ -0,0 +1,9 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec10.lecture_10_mc_q_estimation import keyboard_play +from irlc.gridworld.gridworld_environments import SuttonCornerGridEnvironment +from irlc.ex10.td0_evaluate import TD0ValueAgent + +if __name__ == "__main__": + env = SuttonCornerGridEnvironment() + agent = TD0ValueAgent(env, gamma=1, alpha=0.5) + keyboard_play(env,agent,method_label='TD(0) (alpha=0.5)') diff --git a/irlc/lectures/lec10/lecture_10_td_keyboard.py b/irlc/lectures/lec10/lecture_10_td_keyboard.py new file mode 100644 index 0000000000000000000000000000000000000000..8787900face05cca2791b80d72fc51323dec2392 --- /dev/null +++ b/irlc/lectures/lec10/lecture_10_td_keyboard.py @@ -0,0 +1,9 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec10.lecture_10_mc_q_estimation import automatic_play_value +from irlc.gridworld.gridworld_environments import BookGridEnvironment +from irlc.ex10.td0_evaluate import TD0ValueAgent + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human', living_reward=-0.05) + agent = TD0ValueAgent(env, gamma=1.0, alpha=0.2) + automatic_play_value(env,agent,method_label='TD(0)') diff --git a/irlc/lectures/lec10/unf_gridworld_action_value.py b/irlc/lectures/lec10/unf_gridworld_action_value.py new file mode 100644 index 0000000000000000000000000000000000000000..67d3ad982874e98221729b5c138a7f1373a6c706 --- /dev/null +++ b/irlc/lectures/lec10/unf_gridworld_action_value.py @@ -0,0 +1,42 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex01.agent import Agent +from irlc.gridworld.gridworld_environments import BookGridEnvironment, FrozenLakeEnv +# from irlc.utils.video_monitor import VideoMonitor +from irlc import interactive, train +# from irlc.ex01.agent import train +# from irlc import PlayWrapper + +from irlc.ex10.mc_agent import MCAgent + +class SingleActionValueAgent(MCAgent): + def __init__(self, env, gamma=1.0, epsilon=0.05, alpha=None, first_visit=True): + super().__init__(env, gamma=1., epsilon=1, alpha=None, first_visit=True) + + def pi(self, s, k, info=None): + if k == 0: + return 1 + else: + return super().pi_eps(s, info=None) + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + super().train(s, a, r, sp, done, info_s, info_sp) + for s in self.env.mdp.nonterminal_states: + for a in self.env.mdp.A(s): + if s == (0,0) and a == 1: + pass + elif len(self.env.mdp.A(s)) == 1: + pass + else: + self.Q[s,a] = 0 + a = 234 + + + + + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human', print_states=True, living_reward=-0.05) + env, agent = interactive(env, SingleActionValueAgent(env)) + agent.label = "Random agent" + train(env, agent, num_episodes=100, verbose=False) + env.close() diff --git a/irlc/lectures/lec10/unf_gridworld_value.py b/irlc/lectures/lec10/unf_gridworld_value.py new file mode 100644 index 0000000000000000000000000000000000000000..7286e1d56fc41698bb90e488ef242ba6236a8f59 --- /dev/null +++ b/irlc/lectures/lec10/unf_gridworld_value.py @@ -0,0 +1,42 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex01.agent import Agent +from irlc.gridworld.gridworld_environments import BookGridEnvironment, FrozenLakeEnv +# from irlc.utils.video_monitor import VideoMonitor +from irlc import interactive, train +# from irlc.ex01.agent import train +# from irlc import PlayWrapper +from irlc.ex10.mc_agent import MCAgent +from irlc.ex10.mc_evaluate import MCEvaluationAgent + +class SingleActionValueAgent(MCEvaluationAgent): + def __init__(self, env, gamma=1.0, epsilon=0.05, alpha=None, first_visit=True): + super().__init__(env, gamma=1., alpha=None, first_visit=True) + + # def pi(self, s, k, info=None): + # if k == 0: + # return 1 + # else: + # return super().pi_eps(s, info=None) + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + super().train(s, a, r, sp, done, info_s, info_sp) + for s in self.env.mdp.nonterminal_states: + # for a in self.env.mdp.A(s): + if s == (0,0):# and a == 1: + pass + elif len(self.env.mdp.A(s)) == 1: + pass + else: + self.v[s] = 0 + a = 234 + + + + + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human', print_states=True, living_reward=-0.05) + env, agent = interactive(env, SingleActionValueAgent(env)) + agent.label = "Random agent" + train(env, agent, num_episodes=100, verbose=False) + env.close() diff --git a/irlc/lectures/lec11/__init__.py b/irlc/lectures/lec11/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/lectures/lec11/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/lectures/lec11/exam_sol.py b/irlc/lectures/lec11/exam_sol.py new file mode 100644 index 0000000000000000000000000000000000000000..7687d1736244fd5531c35cd54ebdac7c25fc0a61 --- /dev/null +++ b/irlc/lectures/lec11/exam_sol.py @@ -0,0 +1,11 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.gridworld.gridworld_environments import BookGridEnvironment +from irlc.exam_tabular_examples.sarsa_nstep_delay import SarsaDelayNAgent +from irlc import interactive, train + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human') + agent = SarsaDelayNAgent(env, gamma=1, epsilon=0.1, alpha=0.9, n=1) # Exam problem. + # agent = SarsaDelayNAgent(env, gamma=0.95, epsilon=0.1, alpha=.2, n=1) + env, agent = interactive(env, agent) + train(env, agent, num_episodes=10) diff --git a/irlc/lectures/lec11/lecture_10_grid_lin_q.py b/irlc/lectures/lec11/lecture_10_grid_lin_q.py new file mode 100644 index 0000000000000000000000000000000000000000..659201d8487242b35aaa56cde863327a2d341595 --- /dev/null +++ b/irlc/lectures/lec11/lecture_10_grid_lin_q.py @@ -0,0 +1,10 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.berkley.rl.semi_grad_q import LinearSemiGradQAgent +from irlc.ex11.feature_encoder import GridworldXYEncoder +from irlc.gridworld.gridworld_environments import BookGridEnvironment +from irlc.lectures.lec10.lecture_10_mc_q_estimation import keyboard_play + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human') + agent = LinearSemiGradQAgent(env, gamma=0.95, epsilon=0.1, alpha=.01, q_encoder=GridworldXYEncoder(env)) + keyboard_play(env, agent, method_label="Q-lin-xy") diff --git a/irlc/lectures/lec11/lecture_10_sarsa_open.py b/irlc/lectures/lec11/lecture_10_sarsa_open.py new file mode 100644 index 0000000000000000000000000000000000000000..4e1ca8c2c18c33a9eb2c0dcf38d8d354c192460a --- /dev/null +++ b/irlc/lectures/lec11/lecture_10_sarsa_open.py @@ -0,0 +1,12 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.gridworld.gridworld_environments import OpenGridEnvironment +from irlc.lectures.lec10.lecture_10_mc_q_estimation import keyboard_play +from irlc.ex11.sarsa_agent import SarsaAgent + +def open_play(Agent, method_label, frames_per_second=30, **args): + env = OpenGridEnvironment(render_mode='human', frames_per_second=frames_per_second) + agent = Agent(env, gamma=0.99, epsilon=0.1, alpha=.5, **args) + keyboard_play(env, agent, method_label=method_label) + +if __name__ == "__main__": + open_play(SarsaAgent, method_label="Sarsa") diff --git a/irlc/lectures/lec11/lecture_11_nstep_open.py b/irlc/lectures/lec11/lecture_11_nstep_open.py new file mode 100644 index 0000000000000000000000000000000000000000..fc5e285575ca230982ed20ac722a483e99e52026 --- /dev/null +++ b/irlc/lectures/lec11/lecture_11_nstep_open.py @@ -0,0 +1,11 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# from irlc.berkley.rl.feature_encoder import SimplePacmanExtractor + +from irlc.ex11.nstep_sarsa_agent import SarsaNAgent +from irlc.exam_tabular_examples.sarsa_nstep_delay import SarsaDelayNAgent + +from irlc.lectures.lec11.lecture_10_sarsa_open import open_play +if __name__ == "__main__": + # env = OpenGridEnvironment() + # agent = (env, gamma=0.95, epsilon=0.1, alpha=.5) + open_play(SarsaDelayNAgent, method_label="Sarsa n=8", n=8) diff --git a/irlc/lectures/lec11/lecture_11_pacman_lin_q.py b/irlc/lectures/lec11/lecture_11_pacman_lin_q.py new file mode 100644 index 0000000000000000000000000000000000000000..3b7e121efe6485e2529359a5979091cfc207cd1a --- /dev/null +++ b/irlc/lectures/lec11/lecture_11_pacman_lin_q.py @@ -0,0 +1,32 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex11.semi_grad_q import LinearSemiGradQAgent +from irlc.pacman.pacman_environment import PacmanEnvironment, PacmanWinWrapper +from irlc.ex11.feature_encoder import SimplePacmanExtractor +import matplotlib.pyplot as plt +# from irlc.utils.video_monitor import VideoMonitor +from irlc.ex01.agent import train +# from irlc import PlayWrapper +from irlc import interactive + +def play_pacman(env, agent, layout = 'smallGrid'): + train(env, agent, num_episodes=100) + + env2 = PacmanWinWrapper(env) + + # env2 = Monitor(env2, directory="experiments/randomdir", force=True) + # env2 = VideoMonitor(env2) + env2, agent = interactive(env, agent) + agent.epsilon = 0 + agent.alpha = 0 + # agent = PlayWrapper(agent, env2) + train(env2, agent, num_episodes=100) + plt.show() + env.close() + +if __name__ == "__main__": + layout = 'smallGrid' + env = PacmanEnvironment(animate_movement=True, layout=layout, render_mode='human', frames_per_second=100) + qex = SimplePacmanExtractor(env) + agent = LinearSemiGradQAgent(env, epsilon=0.05, alpha=0.1, gamma=0.8, q_encoder=qex) + play_pacman(env, agent, layout = 'smallGrid') + # main_plot('experiments/q_lin') diff --git a/irlc/lectures/lec11/lecture_11_pacman_q.py b/irlc/lectures/lec11/lecture_11_pacman_q.py new file mode 100644 index 0000000000000000000000000000000000000000..7a51a0679ae8ee815a34df28dedb721b5632ebee --- /dev/null +++ b/irlc/lectures/lec11/lecture_11_pacman_q.py @@ -0,0 +1,35 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.pacman.pacman_environment import PacmanEnvironment, PacmanWinWrapper +# from irlc.berkley.rl.feature_encoder import SimplePacmanExtractor +# from irlc.utils.player_wrapper_pyglet import PlayWrapper +from irlc import main_plot +import matplotlib.pyplot as plt +# from irlc.utils.video_monitor import VideoMonitor +from irlc.ex01.agent import train +# from irlc.lectures.lecture_09_mc import keyboard_play +from irlc.ex11.q_agent import QAgent +from irlc import interactive + + +def play_pacman(env, agent, layout = 'smallGrid'): + + train(env, agent, num_episodes=100) + env2 = PacmanWinWrapper(env) + # env2 = Monitor(env2, directory="experiments/randomdir", force=True) + # env2 = VideoMonitor(env2) + env2, agent = interactive(env2, agent) + agent.epsilon = 0 + agent.alpha = 0 + # agent = PlayWrapper(agent, env2) + train(env2, agent, num_episodes=100) + plt.show() + env.close() + +if __name__ == "__main__": + layout = 'smallGrid' + env = PacmanEnvironment(animate_movement=False, layout=layout, render_mode='human') + agent = QAgent(env, epsilon=0.05, alpha=0.1, gamma=0.8) + # from irlc import PlayWrapper + # agent = PlayWrapper(agent, env) + play_pacman(env, agent, layout = 'smallGrid') + # main_plot('experiments/q_lin') diff --git a/irlc/lectures/lec11/lecture_11_q.py b/irlc/lectures/lec11/lecture_11_q.py new file mode 100644 index 0000000000000000000000000000000000000000..d3df9dbb8f1836bfbe0c622be1212acbb57b6367 --- /dev/null +++ b/irlc/lectures/lec11/lecture_11_q.py @@ -0,0 +1,10 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# from irlc.berkley.rl.feature_encoder import SimplePacmanExtractor +from irlc.gridworld.gridworld_environments import BookGridEnvironment +from irlc.lectures.lec10.lecture_10_mc_q_estimation import keyboard_play +from irlc.ex11.q_agent import QAgent + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human') + agent = QAgent(env, gamma=0.95, epsilon=0.1, alpha=.2) + keyboard_play(env, agent, method_label="Q-learning") diff --git a/irlc/lectures/lec11/lecture_11_q_cliff.py b/irlc/lectures/lec11/lecture_11_q_cliff.py new file mode 100644 index 0000000000000000000000000000000000000000..421db1fa16764a3b432bd03d4a072f2108dabe77 --- /dev/null +++ b/irlc/lectures/lec11/lecture_11_q_cliff.py @@ -0,0 +1,18 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.gridworld.gridworld_environments import CliffGridEnvironment, CliffGridEnvironment2 +from irlc.ex11.q_agent import QAgent + + +# def cliffwalk(env, agent, method_label="method"): +# agent = PlayWrapper(agent, env) + # env = VideoMonitor(env, agent=agent, fps=100, continious_recording=True, agent_monitor_keys=('pi', 'Q'), render_kwargs={'method_label': method_label}) + # train(env, agent, num_episodes=200) + # env.close() + +from irlc.lectures.lec11.lecture_11_sarsa_cliff import cliffwalk, gamma, alpha, epsi +if __name__ == "__main__": + import numpy as np + np.random.seed(1) + env = CliffGridEnvironment2(zoom=.8, render_mode='human') + agent = QAgent(env, gamma=gamma, epsilon=epsi, alpha=alpha) + cliffwalk(env, agent, method_label="Q-learning") diff --git a/irlc/lectures/lec11/lecture_11_q_open.py b/irlc/lectures/lec11/lecture_11_q_open.py new file mode 100644 index 0000000000000000000000000000000000000000..f0a35a5ba17fde85fb2b10da97413aba4879c5c6 --- /dev/null +++ b/irlc/lectures/lec11/lecture_11_q_open.py @@ -0,0 +1,12 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.gridworld_pyglet.gridworld_environments import OpenGridEnvironment +from irlc.lectures.lec10.lecture_10_mc_q_estimation import keyboard_play +from irlc.ex11.q_agent import QAgent + +def open_play(Agent, method_label, **args): + env = OpenGridEnvironment() + agent = Agent(env, gamma=0.99, epsilon=0.1, alpha=.5, **args) + keyboard_play(env, agent, method_label=method_label) + +if __name__ == "__main__": + open_play(QAgent, method_label="Q-learning") diff --git a/irlc/lectures/lec11/lecture_11_sarsa.py b/irlc/lectures/lec11/lecture_11_sarsa.py new file mode 100644 index 0000000000000000000000000000000000000000..7dfb39d048975b86a31fbae151fef17944935155 --- /dev/null +++ b/irlc/lectures/lec11/lecture_11_sarsa.py @@ -0,0 +1,10 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.gridworld.gridworld_environments import BookGridEnvironment +from irlc.lectures.lec10.lecture_10_mc_q_estimation import keyboard_play +from irlc.exam_tabular_examples.sarsa_nstep_delay import SarsaDelayNAgent + +if __name__ == "__main__": + env = BookGridEnvironment(render_mode='human') + # agent = SarsaDelayNAgent(env, gamma=1, epsilon=0.1, alpha=0.9, n=1) # Exam problem. + agent = SarsaDelayNAgent(env, gamma=0.95, epsilon=0.1, alpha=.2, n=1) + keyboard_play(env, agent, method_label="Sarsa") diff --git a/irlc/lectures/lec11/lecture_11_sarsa_cliff.py b/irlc/lectures/lec11/lecture_11_sarsa_cliff.py new file mode 100644 index 0000000000000000000000000000000000000000..3d250fa581975dbbc9fbf1fd2afebd5814c6b6e3 --- /dev/null +++ b/irlc/lectures/lec11/lecture_11_sarsa_cliff.py @@ -0,0 +1,33 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# from irlc.utils.player_wrapper_pyglet import PlayWrapper +from irlc.gridworld.gridworld_environments import CliffGridEnvironment, CliffGridEnvironment2 +# from irlc.utils.video_monitor import VideoMonitor +from irlc.ex01.agent import train +from irlc import interactive +from irlc.ex11.sarsa_agent import SarsaAgent + + +def cliffwalk(env, agent, method_label="method"): + # agent = PlayWrapper(agent, env) + env.label = method_label + agent.method_label = method_label + agent.label = method_label + agent.method = method_label + + + env, agent = interactive(env, agent) + # env = VideoMonitor(env, agent=agent, fps=200, continious_recording=True, agent_monitor_keys=('pi', 'Q'), render_kwargs={'method_label': method_label}) + train(env, agent, num_episodes=1000) + env.close() + +epsi = 0.5 +gamma = 1.0 +alpha = .3 + +if __name__ == "__main__": + import numpy as np + np.random.seed(1) + env = CliffGridEnvironment2(zoom=.8, render_mode='human') + agent = SarsaAgent(env, gamma=gamma, epsilon=epsi, alpha=alpha) + # agent = QAgent(env, gamma=0.95, epsilon=0.5, alpha=.2) + cliffwalk(env, agent, method_label="Sarsa") diff --git a/irlc/lectures/lec12/__init__.py b/irlc/lectures/lec12/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/lectures/lec12/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/lectures/lec12/lecture_12_mc_open.py b/irlc/lectures/lec12/lecture_12_mc_open.py new file mode 100644 index 0000000000000000000000000000000000000000..e0adf318bfe2985e41e994e4afcdc5a0f26494f0 --- /dev/null +++ b/irlc/lectures/lec12/lecture_12_mc_open.py @@ -0,0 +1,19 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# from irlc.berkley.rl.feature_encoder import SimplePacmanExtractor +# from irlc.lectures.lecture_09_mc import keyboard_play + +# alpha = 0.5 +# gamma = + +# def open_play(Agent, method_label, **args): +# env = OpenGridEnvironment() +# agent = Agent(env, gamma=0.95, epsilon=0.1, alpha=.5, **args) +# keyboard_play(env, agent, method_label=method_label) + +from irlc.lectures.lec11.lecture_10_sarsa_open import open_play +from irlc.ex10.mc_agent import MCAgent +if __name__ == "__main__": + # env = OpenGridEnvironment() + # agent = (env, gamma=0.95, epsilon=0.1, alpha=.5) + open_play(MCAgent, method_label="MC agent") + # diff --git a/irlc/lectures/lec12/lecture_12_pacman.py b/irlc/lectures/lec12/lecture_12_pacman.py new file mode 100644 index 0000000000000000000000000000000000000000..3e3f9fbe04edc908086220f25715d09827db33c9 --- /dev/null +++ b/irlc/lectures/lec12/lecture_12_pacman.py @@ -0,0 +1,21 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex11.semi_grad_q import LinearSemiGradQAgent +from irlc.pacman.pacman_environment import PacmanEnvironment +from irlc.ex01.agent import train +from irlc import interactive +from irlc.lectures.chapter14lectures.lecture11pacman import layout, rns +# from irlc import VideoMonitor + +if __name__ == "__main__": + env = PacmanEnvironment(animate_movement=False, layout=layout) + + n, agent = rns[-1] + agent = agent(env) + # env, agent = interactive(env, agent) + + train(env, agent, num_episodes=100, max_runs=20) + env2 = PacmanEnvironment(animate_movement=True, layout=layout, render_mode='human') + # agent.env = env2 + env2, agent = interactive(env2, agent) + train(env2, agent, num_episodes=100, max_runs=20) + env2.close() diff --git a/irlc/lectures/lec12/lecture_12_sarsa_lamda_open.py b/irlc/lectures/lec12/lecture_12_sarsa_lamda_open.py new file mode 100644 index 0000000000000000000000000000000000000000..0e1a233749e000ec65dbdeacecd29cb443f122e4 --- /dev/null +++ b/irlc/lectures/lec12/lecture_12_sarsa_lamda_open.py @@ -0,0 +1,6 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec11.lecture_10_sarsa_open import open_play +from irlc.exam_tabular_examples.sarsa_lambda_delay import SarsaLambdaDelayAgent + +if __name__ == "__main__": + open_play(SarsaLambdaDelayAgent, method_label="Sarsa(Lambda)", lamb=0.8) diff --git a/irlc/lectures/lec12/lecture_12_sarsa_nstep.py b/irlc/lectures/lec12/lecture_12_sarsa_nstep.py new file mode 100644 index 0000000000000000000000000000000000000000..0f04c1abc96ad14ca75d458ecbc882eb01354d3c --- /dev/null +++ b/irlc/lectures/lec12/lecture_12_sarsa_nstep.py @@ -0,0 +1,13 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.gridworld.gridworld_environments import OpenGridEnvironment +from irlc import train +from irlc.lectures.lec11.lecture_10_sarsa_open import open_play +from irlc.exam_tabular_examples.sarsa_nstep_delay import SarsaDelayNAgent + +if __name__ == "__main__": + n = 8 + env = OpenGridEnvironment() + agent = SarsaDelayNAgent(env, n=n) + train(env, agent, num_episodes=100) + + open_play(SarsaDelayNAgent, method_label=f"Sarsa n={n}", n=n) diff --git a/irlc/lectures/lec12/lecture_12_sarsa_open.py b/irlc/lectures/lec12/lecture_12_sarsa_open.py new file mode 100644 index 0000000000000000000000000000000000000000..dfba5b0e37f8668ef3b847155adcb15d52734e1c --- /dev/null +++ b/irlc/lectures/lec12/lecture_12_sarsa_open.py @@ -0,0 +1,11 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc import train +from irlc.gridworld.gridworld_environments import OpenGridEnvironment +from irlc.lectures.lec11.lecture_10_sarsa_open import open_play +from irlc.exam_tabular_examples.sarsa_nstep_delay import SarsaDelayNAgent + +if __name__ == "__main__": + env = OpenGridEnvironment() + agent = SarsaDelayNAgent(env, n=1) + train(env, agent, num_episodes=100) + open_play(SarsaDelayNAgent, method_label=f"Sarsa") diff --git a/irlc/lectures/lec13/double_q_viz.py b/irlc/lectures/lec13/double_q_viz.py new file mode 100644 index 0000000000000000000000000000000000000000..cca339d46fbdde4ffc94433b3e146b9ae3145a69 --- /dev/null +++ b/irlc/lectures/lec13/double_q_viz.py @@ -0,0 +1,71 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +from irlc.ex01.agent import train +import gymnasium as gym +from irlc import main_plot +import matplotlib.pyplot as plt +from irlc import savepdf +from irlc.ex11.sarsa_agent import SarsaAgent +from irlc.ex11.q_agent import QAgent +from irlc.ex13.tabular_double_q import TabularDoubleQ +from irlc.ex09.rl_agent import TabularQ +from irlc.gridworld.gridworld_environments import CliffGridEnvironment + +class DoubleQVizAgent(TabularDoubleQ): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.Q = TabularQ(self.env) + + def train(self, s, a, r, sp, done=False, info_s=None, info_sp=None): + super().train(s, a, r, sp, done, info_s,info_sp) + self.Q[s,a] = (self.Q1[s,a] + self.Q2[s,a] )/2 + +def train_cliff(runs=4, extension="long", save_pdf=False, alpha=0.02, num_episodes=5000): + """ Part 1: Cliffwalking """ + # env = gym.make('CliffWalking-v0') + + env = CliffGridEnvironment(zoom=1) + epsilon = 0.1 + # alpha = 0.02 + for _ in range(runs): + agents = [QAgent(env, gamma=1, epsilon=epsilon, alpha=alpha), + SarsaAgent(env, gamma=1, epsilon=epsilon, alpha=alpha), + DoubleQVizAgent(env, gamma=1, epsilon=epsilon, alpha=alpha)] + + experiments = [] + for agent in agents: + expn = f"experiments/doubleq_cliffwalk_{extension}_{str(agent)}" + train(env, agent, expn, num_episodes=num_episodes, max_runs=1e6) + experiments.append(expn) + if save_pdf: + main_plot(experiments, smoothing_window=20, resample_ticks=500) + plt.ylim([-100, 50]) + plt.title(f"Double-Q learning on Cliffwalk ({extension})") + savepdf(f"double_Q_learning_cliff_{extension}") + plt.show() + return agents, env + + +def grid_experiment(runs=20, extension="long", alpha=0.02, num_episodes=5000): + from irlc.gridworld.gridworld_environments import CliffGridEnvironment + # from irlc import VideoMonitor, PlayWrapper + from irlc import interactive + + agents, env = train_cliff(runs=runs, extension=extension, save_pdf=True, alpha=alpha, num_episodes=num_episodes) + labels = ["Q-learning", "Sarsa", "Double Q-learning"] + for na in range(len(agents)): + env2 = CliffGridEnvironment(zoom=1, view_mode='human') + env2, agent = interactive(env2, agent=agents[na])# , agent_monitor_keys=('Q',), render_kwargs={'method_label': labels[na]}) + # agent = PlayWrapper(agents[na], env) + env2.savepdf(f"doubleq_cliff_{extension}_agent_{na}") + env2.close() + + env.close() + pass + +if __name__ == "__main__": + """ + Test cliffwalk in both the long and short version + """ + grid_experiment(runs=1, extension="long", alpha=0.02, num_episodes=5000) + grid_experiment(runs=1, extension="short", alpha=0.25, num_episodes=500) diff --git a/irlc/lectures/lec13/lecture_13_Q_maze.py b/irlc/lectures/lec13/lecture_13_Q_maze.py new file mode 100644 index 0000000000000000000000000000000000000000..c1b0582f47628a24f1c5d2de5c436c96b6b9cb10 --- /dev/null +++ b/irlc/lectures/lec13/lecture_13_Q_maze.py @@ -0,0 +1,14 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec11.lecture_10_sarsa_open import open_play +from irlc.ex11.q_agent import QAgent +from irlc.ex13.dyna_q import DynaQ +from irlc.lectures.lec10.lecture_10_mc_q_estimation import keyboard_play +from irlc.gridworld.gridworld_environments import SuttonMazeEnvironment + +def sutton_maze_play(Agent, method_label="Q-learning agent", **kwargs): + env = SuttonMazeEnvironment(render_mode='human') + agent = Agent(env, gamma=0.98, epsilon=0.1, alpha=.5, **kwargs) + keyboard_play(env, agent, method_label=method_label) + +if __name__ == "__main__": + sutton_maze_play(DynaQ, method_label="Q-learning agent", n=0) diff --git a/irlc/lectures/lec13/lecture_13_Q_open.py b/irlc/lectures/lec13/lecture_13_Q_open.py new file mode 100644 index 0000000000000000000000000000000000000000..b45e0697e34600e0dc22f5f0b0ba597f1e9beb41 --- /dev/null +++ b/irlc/lectures/lec13/lecture_13_Q_open.py @@ -0,0 +1,6 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec11.lecture_10_sarsa_open import open_play +from irlc.ex11.q_agent import QAgent + +if __name__ == "__main__": + open_play(QAgent, method_label="Q-learning agent") diff --git a/irlc/lectures/lec13/lecture_13_dyna_q_5_maze.py b/irlc/lectures/lec13/lecture_13_dyna_q_5_maze.py new file mode 100644 index 0000000000000000000000000000000000000000..293771d49426b2d27e92c6e719bbac8854973438 --- /dev/null +++ b/irlc/lectures/lec13/lecture_13_dyna_q_5_maze.py @@ -0,0 +1,10 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec11.lecture_10_sarsa_open import open_play +from irlc.ex11.q_agent import QAgent +from irlc.ex13.dyna_q import DynaQ +from irlc.lectures.lec10.lecture_10_mc_q_estimation import keyboard_play +from irlc.gridworld.gridworld_environments import SuttonMazeEnvironment +from irlc.lectures.lec13.lecture_13_Q_maze import sutton_maze_play + +if __name__ == "__main__": + sutton_maze_play(DynaQ, method_label="DynaQ (n=5)", n=5) diff --git a/irlc/lectures/lec13/lecture_13_sarsa_lambda_maze.py b/irlc/lectures/lec13/lecture_13_sarsa_lambda_maze.py new file mode 100644 index 0000000000000000000000000000000000000000..4336879fe4d49f99c6852847a968491829afc77f --- /dev/null +++ b/irlc/lectures/lec13/lecture_13_sarsa_lambda_maze.py @@ -0,0 +1,6 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.lectures.lec13.lecture_13_Q_maze import sutton_maze_play +from irlc.ex12.sarsa_lambda_agent import SarsaLambdaAgent + +if __name__ == "__main__": + sutton_maze_play(SarsaLambdaAgent, method_label="Sarsa(Lambda=0.9)", lamb=0.9) diff --git a/irlc/lectures/readme.md b/irlc/lectures/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..1d3ccdbfd57fe2976e3fb29c3256386f548da3ff --- /dev/null +++ b/irlc/lectures/readme.md @@ -0,0 +1,6 @@ +# In-class examples + +This folder contains various examples used throughout class. You should be able to run most of the examples +if you find it helpful (and many of the examples are simply running the exercise code), however, +in some instances I have made small changes to the exercises to provide additional visualizations etc. Also note that the code is sometimes not +well organized -- in other words, the folder is provided "as is" for those who find it helpful, and you are free to ignore it. diff --git a/irlc/pacman/__init__.py b/irlc/pacman/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..991666419eb8411137ce96f826e7d6883892af7b --- /dev/null +++ b/irlc/pacman/__init__.py @@ -0,0 +1,2 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.pacman.pacman_environment import PacmanEnvironment diff --git a/irlc/pacman/__pycache__/__init__.cpython-311.pyc b/irlc/pacman/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..59a1fd223427c1d692a1f9db502ea71573779c82 Binary files /dev/null and b/irlc/pacman/__pycache__/__init__.cpython-311.pyc differ diff --git a/irlc/pacman/__pycache__/gamestate.cpython-311.pyc b/irlc/pacman/__pycache__/gamestate.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9aa794223cbc54858e8d02ec80d33c6986577e08 Binary files /dev/null and b/irlc/pacman/__pycache__/gamestate.cpython-311.pyc differ diff --git a/irlc/pacman/__pycache__/layout.cpython-311.pyc b/irlc/pacman/__pycache__/layout.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..03091d715903d53e6e467e17b514095681882ca2 Binary files /dev/null and b/irlc/pacman/__pycache__/layout.cpython-311.pyc differ diff --git a/irlc/pacman/__pycache__/pacman_environment.cpython-311.pyc b/irlc/pacman/__pycache__/pacman_environment.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b601df57ba77d362482407b7d03491083d23a656 Binary files /dev/null and b/irlc/pacman/__pycache__/pacman_environment.cpython-311.pyc differ diff --git a/irlc/pacman/__pycache__/pacman_graphics_display.cpython-311.pyc b/irlc/pacman/__pycache__/pacman_graphics_display.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6b583f2ce171807c07a0c845e957334a277a79ec Binary files /dev/null and b/irlc/pacman/__pycache__/pacman_graphics_display.cpython-311.pyc differ diff --git a/irlc/pacman/__pycache__/pacman_text_display.cpython-311.pyc b/irlc/pacman/__pycache__/pacman_text_display.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3cf59db02dff7911114b3da4081c6965dc0c4f08 Binary files /dev/null and b/irlc/pacman/__pycache__/pacman_text_display.cpython-311.pyc differ diff --git a/irlc/pacman/__pycache__/pacman_utils.cpython-311.pyc b/irlc/pacman/__pycache__/pacman_utils.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8a7be403248e32e151ccbef8ab4ac2bfd4a8ff9b Binary files /dev/null and b/irlc/pacman/__pycache__/pacman_utils.cpython-311.pyc differ diff --git a/irlc/pacman/feature_extractor.py b/irlc/pacman/feature_extractor.py new file mode 100644 index 0000000000000000000000000000000000000000..7a409464496293476228ba0d699316eab62c143e --- /dev/null +++ b/irlc/pacman/feature_extractor.py @@ -0,0 +1,109 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# feature_extractor.py +# -------------------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). +from irlc.pacman.pacman_utils import Actions + +## Other classes +class FeatureExtractor: + def getFeatures(self, state, action): + """ + Returns a dict from features to counts + Usually, the count will just be 1.0 for + indicator functions. + """ + raise NotImplementedError() + # util.raiseNotDefined() + +class IdentityExtractor(FeatureExtractor): + def getFeatures(self, state, action): + from collections import defaultdict + feats = defaultdict(lambda: 0) + # feats = util.Counter() + feats[(state,action)] = 1.0 + return feats + +class CoordinateExtractor(FeatureExtractor): + def getFeatures(self, state, action): + from collections import defaultdict + feats = defaultdict(lambda: 0) + # feats = util.Counter() + feats[state] = 1.0 + feats['x=%d' % state[0]] = 1.0 + feats['y=%d' % state[0]] = 1.0 + feats['action=%s' % action] = 1.0 + return feats + +def closestFood(pos, food, walls): + """ + closestFood -- this is similar to the function that we have + worked on in the search project; here its all in one place + """ + fringe = [(pos[0], pos[1], 0)] + expanded = set() + while fringe: + pos_x, pos_y, dist = fringe.pop(0) + if (pos_x, pos_y) in expanded: + continue + expanded.add((pos_x, pos_y)) + # if we find a food at this location then exit + if food[pos_x][pos_y]: + return dist + # otherwise spread out from the location to its neighbours + nbrs = Actions.getLegalNeighbors((pos_x, pos_y), walls) + for nbr_x, nbr_y in nbrs: + fringe.append((nbr_x, nbr_y, dist+1)) + # no food found + return None + +class SimpleExtractor(FeatureExtractor): + """ + Returns simple features for a basic reflex Pacman: + - whether food will be eaten + - how far away the next food is + - whether a ghost collision is imminent + - whether a ghost is one step away + """ + + def getFeatures(self, state, action): + # extract the grid of food and wall locations and get the ghost locations + food = state.getFood() + walls = state.getWalls() + ghosts = state.getGhostPositions() + + from collections import defaultdict + features = defaultdict(lambda: 0) + + # features = util.Counter() + + features["bias"] = 1.0 + + # compute the location of pacman after he takes the action + x, y = state.getPacmanPosition() + dx, dy = Actions.directionToVector(action) + next_x, next_y = int(x + dx), int(y + dy) + + # count the number of ghosts 1-step away + features["#-of-ghosts-1-step-away"] = sum((next_x, next_y) in Actions.getLegalNeighbors(g, walls) for g in ghosts) + + # if there is no danger of ghosts then add the food feature + if not features["#-of-ghosts-1-step-away"] and food[next_x][next_y]: + features["eats-food"] = 1.0 + + dist = closestFood((next_x, next_y), food, walls) + if dist is not None: + # make the distance a number less than one otherwise the update + # will diverge wildly + features["closest-food"] = float(dist) / (walls.width * walls.height) + # features.divideAll(10.0) + features = {k: v/10.0 for k, v in features.items() } + return features diff --git a/irlc/pacman/gamestate.py b/irlc/pacman/gamestate.py new file mode 100644 index 0000000000000000000000000000000000000000..c75db5f6b1b38acd1a17ac217ceac057426163db --- /dev/null +++ b/irlc/pacman/gamestate.py @@ -0,0 +1,812 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# gamestate.py +# --------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +""" +Pacman.py holds the logic for the classic pacman game along with the main +code to run a game. This file is divided into three sections: + + (i) Your interface to the pacman world: + Pacman is a complex environment. You probably don't want to + read through all of the code we wrote to make the game runs + correctly. This section contains the parts of the code + that you will need to understand in order to complete the + project. There is also some code in pacman_utils.py that you should + understand. + + (ii) The hidden secrets of pacman: + This section contains all of the logic code that the pacman + environment uses to decide who can move where, who dies when + things collide, etc. You shouldn't need to read this section + of code, but you can if you want. + + (iii) Framework to start a game: + The final section contains the code for reading the command + you use to set up the game, then starting up a new game, along with + linking in all the external parts (agent functions, graphics). + Check this section out to see all the options available to you. + +To play your first game, type 'python gamestate.py' from the command line. +The keys are 'a', 's', 'd', and 'w' to move (or arrow keys). Have fun! +""" +import irlc.pacman.pacman_utils +from irlc.pacman.pacman_utils import GameStateData +from irlc.pacman.pacman_utils import Game +from irlc.pacman.pacman_utils import Directions +from irlc.pacman.pacman_utils import Actions + + +################################################### +# YOUR INTERFACE TO THE PACMAN WORLD: A GameState # +################################################### + +class GameState: + r""" + A `GameState` specifies the full game state, including the food, capsules, + agent configurations and score changes. + + `GameState`\ s are used by the Game object to capture the actual state of the game and + can be used by agents to reason about the game. + + Much of the information in a GameState is stored in a `GameStateData` object. We + strongly suggest that you access that data via the accessor methods below rather + than referring to the `GameStateData` object directly. + + Note that in classic Pacman, Pacman is always agent 0. + + To get you started, here are some examples. + + .. runblock:: pycon + + >>> from irlc.pacman.pacman_environment import PacmanEnvironment, very_small_haunted_maze + >>> env = PacmanEnvironment(layout_str=very_small_haunted_maze) + >>> state, _ = env.reset() # Get starting state + >>> print(state) + + In the above code, `state` is a `GameState` instance -- i.e. has all the methods found in + *this* class. So for instance to know if the game is won or lost you can do: + + .. runblock:: pycon + + >>> from irlc.pacman.pacman_environment import PacmanEnvironment, very_small_haunted_maze + >>> env = PacmanEnvironment(layout_str=very_small_haunted_maze) + >>> state, _ = env.reset() # Get starting state + >>> print("Did we win?", state.is_won(), "did we loose?", state.is_lost()) + + Or to get the available actions, and then the *next* state representing what occurs when you take an action `a`: + + .. runblock:: pycon + + >>> from irlc.pacman.pacman_environment import PacmanEnvironment, very_small_haunted_maze + >>> env = PacmanEnvironment(layout_str=very_small_haunted_maze) + >>> state, _ = env.reset() # Get starting state + >>> actions = state.A() + >>> print("Available actions are", actions) + >>> next_state = state.f(actions[0]) # Take the first action + >>> print(next_state) # Result of taking the first of the available actions. + + When a ghost move, it will select randomly between the available actions. Thus, the chance of a single move is :python:`1/len(state.A())`. + """ + ##################################################### + # 02465-relevant stuff: These methods allows you to # + # interact with the game-state. See comments above. # + ##################################################### + + def player(self) -> int: + """Return the current player. + + The players take turns. Initially ``player=0``, meaning it is Pacman (your) turn, and in case there are ghosts + player will then increment until all ghosts have moved at which point ``player = 0`` again and the game is ready + for the next step. + + :return: The id of the player who will make the next move. + """ + return self._player + + def players(self): + """Return the total number of players. + + :return: Return the number of ghosts + 1 (pacman). + """ + return self.getNumAgents() + + def A(self): + """Return the available actions for the current player in this state. + + If the state is won/lost, the actions will be just the stop-action: ``["Stop"]``. + + :return: Available actions as a list. + """ + if self.is_won() or self.is_lost(): + return [Directions.STOP] + else: + return self.getLegalActions(self.player()) + + def f(self, a : str) -> object: + """Let the current player take action ``a``. + + This will return a new GameState corresponding to the current player taking an action. + + :param a: The action to take. + :return: The next GameState. + """ + if self.is_won() or self.is_lost(): + return self + + suc = self.generateSuccessor(self.player(), a) + suc._player = (self.player() + 1) % self.getNumAgents() + return suc + + def is_lost(self): + """Determine if this is a lost game. + + :return: ``True`` if this GameState corresponds to a lost game (a ghost ate pacman) + """ + return self.data._lose + + def is_won(self): + """Determine if this is a won game. + + :return: ``True`` if this GameState corresponds to a won game (all pellets eaten) + """ + return self.data._win + + ################################################################################################## + # End of 02465-related stuff. These methods are internal to the game and should **not** be used. # + ################################################################################################## + + # static variable keeps track of which states have had getLegalActions called + explored = set() + def getAndResetExplored(): + tmp = GameState.explored.copy() + GameState.explored = set() + return tmp + getAndResetExplored = staticmethod(getAndResetExplored) + + def getLegalActions( self, agentIndex=0 ): + # """ + # Returns the legal actions for the agent specified. + # """ +# GameState.explored.add(self) + if self.is_won() or self.is_lost(): return [] + + if agentIndex == 0: # Pacman is moving + return PacmanRules.getLegalActions( self ) + else: + return GhostRules.getLegalActions( self, agentIndex ) + + def generateSuccessor( self, agentIndex, action): + # """ + # Returns the successor state after the specified agent takes the action. + # """ + # Check that successors exist + if self.is_won() or self.is_lost(): raise Exception('Can\'t generate a successor of a terminal state.') + + # Copy current state + state = GameState(self) + + # Let agent's logic deal with its action's effects on the board + if agentIndex == 0: # Pacman is moving + state.data._eaten = [False for i in range(state.getNumAgents())] + PacmanRules.applyAction( state, action ) + else: # A ghost is moving + GhostRules.applyAction( state, action, agentIndex ) + + # Time passes + if agentIndex == 0: + state.data.scoreChange += -TIME_PENALTY # Penalty for waiting around + else: + GhostRules.decrementTimer( state.data.agentStates[agentIndex] ) + + # Resolve multi-agent effects + GhostRules.checkDeath( state, agentIndex ) + + # Book keeping + state.data._agentMoved = agentIndex + state.data.score += state.data.scoreChange + GameState.explored.add(self) + GameState.explored.add(state) + return state + + + def getLegalPacmanActions( self ): + return self.getLegalActions( 0 ) + + def generatePacmanSuccessor( self, action ): + # """ + # Generates the successor state after the specified pacman move + # """ + return self.generateSuccessor( 0, action ) + + def getPacmanState( self ): + # """ + # Returns an AgentState object for pacman (in pacman_utils.py) + # + # state.pos gives the current position + # state.direction gives the travel vector + # """ + return self.data.agentStates[0].copy() + + def getPacmanPosition( self ): + return self.data.agentStates[0].getPosition() + + def getGhostStates( self ): + return self.data.agentStates[1:] + + def getGhostState( self, agentIndex ): + if agentIndex == 0 or agentIndex >= self.getNumAgents(): + raise Exception("Invalid index passed to getGhostState") + return self.data.agentStates[agentIndex] + + def getGhostPosition( self, agentIndex ): + if agentIndex == 0: + raise Exception("Pacman's index passed to getGhostPosition") + return self.data.agentStates[agentIndex].getPosition() + + def getGhostPositions(self): + return [s.getPosition() for s in self.getGhostStates()] + + def getNumAgents( self ): + return len( self.data.agentStates ) + + def getScore( self ): + return float(self.data.score) + + def getCapsules(self): + # """ + # Returns a list of positions (x,y) of the remaining capsules. + # """ + return self.data.capsules + + def getNumFood( self ): + return self.data.food.count() + + def getFood(self): + # """ + # Returns a Grid of boolean food indicator variables. + # + # Grids can be accessed via list notation, so to check + # if there is food at (x,y), just call + # + # currentFood = state.getFood() + # if currentFood[x][y] == True: ... + # """ + return self.data.food + + def getWalls(self): + # """ + # Returns a Grid of boolean wall indicator variables. + # + # Grids can be accessed via list notation, so to check + # if there is a wall at (x,y), just call + # + # walls = state.getWalls() + # if walls[x][y] == True: ... + # """ + return self.data.layout.walls + + def hasFood(self, x, y): + return self.data.food[x][y] + + def hasWall(self, x, y): + return self.data.layout.walls[x][y] + + + ############################################# + # Helper methods: # + # You shouldn't need to call these directly # + ############################################# + + def __init__( self, prevState = None): + # """ + # Generates a new state by copying information from its predecessor. + # """ + if prevState != None: # Initial state + self.data = GameStateData(prevState.data) + else: + self.data = GameStateData() + self._player = 0 + + def deepCopy( self ): + state = GameState( self ) + state.data = self.data.deepCopy() + return state + + def __eq__( self, other ): + # """ + # Allows two states to be compared. + # """ + return hasattr(other, 'data') and self.data == other.data + + def __hash__( self ): + # """ + # Allows states to be keys of dictionaries. + # """ + return hash( self.data ) + + def __str__( self ): + return str(self.data) + + def initialize( self, layout, numGhostAgents=1000 ): + # """ + # Creates an initial game state from a layout array (see layout.py). + # """ + self.data.initialize(layout, numGhostAgents) + +############################################################################ +# THE HIDDEN SECRETS OF PACMAN # +# # +# You shouldn't need to look through the code in this section of the file. # +############################################################################ + +SCARED_TIME = 40 # Moves ghosts are scared +COLLISION_TOLERANCE = 0.7 # How close ghosts must be to Pacman to kill +TIME_PENALTY = 1 # Number of points lost each round + +class ClassicGameRules: + """ + These game rules manage the control flow of a game, deciding when + and how the game starts and ends. + """ + def __init__(self, timeout=30): + self.timeout = timeout + + def newGame( self, layout, pacmanAgent, ghostAgents, quiet = False, catchExceptions=False, time_penalty=TIME_PENALTY): + agents = [pacmanAgent] + ghostAgents[:layout.getNumGhosts()] + initState = GameState() # Time penalty is my idea + initState.initialize( layout, len(ghostAgents) ) + game = Game(agents=agents, rules=self, catchExceptions=catchExceptions) + game.state = initState + self.initialState = initState.deepCopy() + self.quiet = quiet + return game + + def process(self, state, game): + """ + Checks to see whether it is time to end the game. + """ + if state.is_won(): self.win(state, game) + if state.is_lost(): self.lose(state, game) + + def win( self, state, game ): + if not self.quiet: print("Pacman emerges victorious! Score: %d" % state.data.score) + game.gameOver = True + + def lose( self, state, game ): + if not self.quiet: print("Pacman died! Score: %d" % state.data.score) + game.gameOver = True + + def getProgress(self, game): + return float(game.state.getNumFood()) / self.initialState.getNumFood() + + def agentCrash(self, game, agentIndex): + if agentIndex == 0: + print("Pacman crashed") + else: + print("A ghost crashed") + + def getMaxTotalTime(self, agentIndex): + return self.timeout + + def getMaxStartupTime(self, agentIndex): + return self.timeout + + def getMoveWarningTime(self, agentIndex): + return self.timeout + + def getMoveTimeout(self, agentIndex): + return self.timeout + + def getMaxTimeWarnings(self, agentIndex): + return 0 + +class PacmanRules: + """ + These functions govern how pacman interacts with his environment under + the classic game rules. + """ + PACMAN_SPEED=1 + + def getLegalActions( state ): + """ + Returns a list of possible actions. + """ + return Actions.getPossibleActions( state.getPacmanState().configuration, state.data.layout.walls ) + getLegalActions = staticmethod( getLegalActions ) + + def applyAction( state, action ): + """ + Edits the state to reflect the results of the action. + """ + legal = PacmanRules.getLegalActions( state ) + if action not in legal: + raise Exception("Illegal action " + str(action)) + + pacmanState = state.data.agentStates[0] + + # Update Configuration + vector = Actions.directionToVector( action, PacmanRules.PACMAN_SPEED ) + pacmanState.configuration = pacmanState.configuration.generateSuccessor( vector ) + + # Eat + next = pacmanState.configuration.getPosition() + nearest = nearestPoint( next ) + if manhattanDistance( nearest, next ) <= 0.5 : + # Remove food + PacmanRules.consume( nearest, state ) + applyAction = staticmethod( applyAction ) + + def consume( position, state ): + x,y = position + # Eat food + if state.data.food[x][y]: + state.data.scoreChange += 10 + state.data.food = state.data.food.copy() + state.data.food[x][y] = False + state.data._foodEaten = position + # TODO: cache numFood? + numFood = state.getNumFood() + if numFood == 0 and not state.data._lose: + state.data.scoreChange += 500 + state.data._win = True + # Eat capsule + if( position in state.getCapsules() ): + state.data.capsules.remove( position ) + state.data._capsuleEaten = position + # Reset all ghosts' scared timers + for index in range( 1, len( state.data.agentStates ) ): + state.data.agentStates[index].scaredTimer = SCARED_TIME + consume = staticmethod( consume ) + +class GhostRules: + """ + These functions dictate how ghosts interact with their environment. + """ + GHOST_SPEED=1.0 + def getLegalActions( state, ghostIndex ): + """ + Ghosts cannot stop, and cannot turn around unless they + reach a dead end, but can turn 90 degrees at intersections. + """ + conf = state.getGhostState( ghostIndex ).configuration + possibleActions = Actions.getPossibleActions( conf, state.data.layout.walls ) + reverse = Actions.reverseDirection( conf.direction ) + if Directions.STOP in possibleActions: + possibleActions.remove( Directions.STOP ) + if reverse in possibleActions and len( possibleActions ) > 1: + possibleActions.remove( reverse ) + return possibleActions + getLegalActions = staticmethod( getLegalActions ) + + def applyAction( state, action, ghostIndex): + + legal = GhostRules.getLegalActions( state, ghostIndex ) + if action not in legal: + raise Exception("Illegal ghost action " + str(action)) + + ghostState = state.data.agentStates[ghostIndex] + speed = GhostRules.GHOST_SPEED + if ghostState.scaredTimer > 0: speed /= 2.0 + vector = Actions.directionToVector( action, speed ) + ghostState.configuration = ghostState.configuration.generateSuccessor( vector ) + applyAction = staticmethod( applyAction ) + + def decrementTimer( ghostState): + timer = ghostState.scaredTimer + if timer == 1: + ghostState.configuration.pos = nearestPoint( ghostState.configuration.pos ) + ghostState.scaredTimer = max( 0, timer - 1 ) + decrementTimer = staticmethod( decrementTimer ) + + def checkDeath( state, agentIndex): + pacmanPosition = state.getPacmanPosition() + if agentIndex == 0: # Pacman just moved; Anyone can kill him + for index in range( 1, len( state.data.agentStates ) ): + ghostState = state.data.agentStates[index] + ghostPosition = ghostState.configuration.getPosition() + if GhostRules.canKill( pacmanPosition, ghostPosition ): + GhostRules.collide( state, ghostState, index ) + else: + ghostState = state.data.agentStates[agentIndex] + ghostPosition = ghostState.configuration.getPosition() + if GhostRules.canKill( pacmanPosition, ghostPosition ): + GhostRules.collide( state, ghostState, agentIndex ) + checkDeath = staticmethod( checkDeath ) + + def collide( state, ghostState, agentIndex): + if ghostState.scaredTimer > 0: + state.data.scoreChange += 200 + GhostRules.placeGhost(state, ghostState) + ghostState.scaredTimer = 0 + # Added for first-person + state.data._eaten[agentIndex] = True + else: + if not state.data._win: + state.data.scoreChange -= 500 + state.data._lose = True + collide = staticmethod( collide ) + + def canKill( pacmanPosition, ghostPosition ): + return manhattanDistance( ghostPosition, pacmanPosition ) <= COLLISION_TOLERANCE + canKill = staticmethod( canKill ) + + def placeGhost(state, ghostState): + ghostState.configuration = ghostState.start + placeGhost = staticmethod( placeGhost ) + +############################# +# FRAMEWORK TO START A GAME # +############################# + +def default(str): + return str + ' [Default: %default]' + +def parseAgentArgs(str): + if str == None: return {} + pieces = str.split(',') + opts = {} + for p in pieces: + if '=' in p: + key, val = p.split('=') + else: + key,val = p, 1 + opts[key] = val + return opts + +# def readCommand( argv ): +# """ +# Processes the command used to run pacman from the command line. +# """ +# from optparse import OptionParser +# usageStr = """ +# USAGE: python gamestate.py <options> +# EXAMPLES: (1) python gamestate.py +# - starts an interactive game +# (2) python gamestate.py --layout smallClassic --zoom 2 +# OR python gamestate.py -l smallClassic -z 2 +# - starts an interactive game on a smaller board, zoomed in +# """ +# parser = OptionParser(usageStr) +# +# parser.add_option('-n', '--numGames', dest='numGames', type='int', +# help=default('the number of GAMES to play'), metavar='GAMES', default=1) +# parser.add_option('-l', '--layout', dest='layout', +# help=default('the LAYOUT_FILE from which to load the map layout'), +# metavar='LAYOUT_FILE', default='mediumClassic') +# parser.add_option('-p', '--pacman', dest='pacman', +# help=default('the agent TYPE in the pacmanAgents module to use'), +# metavar='TYPE', default='KeyboardAgent') +# parser.add_option('-t', '--textGraphics', action='store_true', dest='textGraphics', +# help='Display output as text only', default=False) +# parser.add_option('-q', '--quietTextGraphics', action='store_true', dest='quietGraphics', +# help='Generate minimal output and no graphics', default=False) +# parser.add_option('-g', '--ghosts', dest='ghost', +# help=default('the ghost agent TYPE in the ghostAgents module to use'), +# metavar = 'TYPE', default='RandomGhost') +# parser.add_option('-k', '--numghosts', type='int', dest='numGhosts', +# help=default('The maximum number of ghosts to use'), default=4) +# parser.add_option('-z', '--zoom', type='float', dest='zoom', +# help=default('Zoom the size of the graphics window'), default=1.0) +# parser.add_option('-f', '--fixRandomSeed', action='store_true', dest='fixRandomSeed', +# help='Fixes the random seed to always play the same game', default=False) +# parser.add_option('-r', '--recordActions', action='store_true', dest='record', +# help='Writes game histories to a file (named by the time they were played)', default=False) +# parser.add_option('--replay', dest='gameToReplay', +# help='A recorded game file (pickle) to replay', default=None) +# parser.add_option('-a','--agentArgs',dest='agentArgs', +# help='Comma separated values sent to agent. e.g. "opt1=val1,opt2,opt3=val3"') +# parser.add_option('-x', '--numTraining', dest='numTraining', type='int', +# help=default('How many episodes are training (suppresses output)'), default=0) +# parser.add_option('--frameTime', dest='frameTime', type='float', +# help=default('Time to delay between frames; <0 means keyboard'), default=0.1) +# parser.add_option('-c', '--catchExceptions', action='store_true', dest='catchExceptions', +# help='Turns on exception handling and timeouts during games', default=False) +# parser.add_option('--timeout', dest='timeout', type='int', +# help=default('Maximum length of time an agent can spend computing in a single game'), default=30) +# +# options, otherjunk = parser.parse_args(argv) +# if len(otherjunk) != 0: +# raise Exception('Command line input not understood: ' + str(otherjunk)) +# args = dict() +# +# # Fix the random seed +# if options.fixRandomSeed: random.seed('cs188') +# +# # Choose a layout +# args['layout'] = layout.getLayout( options.layout ) +# if args['layout'] == None: raise Exception("The layout " + options.layout + " cannot be found") +# +# # Choose a Pacman agent +# noKeyboard = options.gameToReplay == None and (options.textGraphics or options.quietGraphics) +# pacmanType = loadAgent(options.pacman, noKeyboard) +# agentOpts = parseAgentArgs(options.agentArgs) +# if options.numTraining > 0: +# args['numTraining'] = options.numTraining +# if 'numTraining' not in agentOpts: agentOpts['numTraining'] = options.numTraining +# pacman = pacmanType(**agentOpts) # Instantiate Pacman with agentArgs +# args['pacman'] = pacman +# +# # Don't display training games +# if 'numTrain' in agentOpts: +# options.numQuiet = int(agentOpts['numTrain']) +# options.numIgnore = int(agentOpts['numTrain']) +# +# # Choose a ghost agent +# ghostType = loadAgent(options.ghost, noKeyboard) +# args['ghosts'] = [ghostType( i+1 ) for i in range( options.numGhosts )] +# +# # Choose a display format +# if options.quietGraphics: +# import text_display_pacman +# args['display'] = text_display_pacman.NullGraphics() +# elif options.textGraphics: +# import text_display_pacman +# text_display_pacman.SLEEP_TIME = options.frameTime +# args['display'] = text_display_pacman.PacmanGraphics() +# else: +# pass +# # from gympackman import ggraphicsDisplay +# # args['display'] = ggraphicsDisplay.PacmanGraphics(options.zoom, frameTime = options.frameTime) +# args['numGames'] = options.numGames +# args['record'] = options.record +# args['catchExceptions'] = options.catchExceptions +# args['timeout'] = options.timeout +# +# # Special case: recorded games don't use the runGames method or args structure +# if options.gameToReplay != None: +# print('Replaying recorded game %s.' % options.gameToReplay) +# import cPickle +# f = open(options.gameToReplay) +# try: recorded = cPickle.load(f) +# finally: f.close() +# recorded['display'] = args['display'] +# replayGame(**recorded) +# sys.exit(0) +# +# args['options'] = options +# return args + +# def loadAgent(pacman, nographics): +# # Looks through all pythonPath Directories for the right module, +# pythonPathStr = os.path.expandvars("$PYTHONPATH") +# if pythonPathStr.find(';') == -1: +# pythonPathDirs = pythonPathStr.split(':') +# else: +# pythonPathDirs = pythonPathStr.split(';') +# pythonPathDirs.append('.') +# from irlc.berkley import pacman as pcman +# pythonPathDirs.append(os.path.dirname(pcman.__file__)) +# if pacman == 'PacmanQAgent': +# from irlc.berkley.pacman.qlearningAgents import QLearningAgent +# return QLearningAgent +# if pacman == 'RandomGhost': +# from irlc.berkley.pacman.ghostAgents import RandomGhost +# return RandomGhost +# +# for moduleDir in pythonPathDirs: +# if not os.path.isdir(moduleDir): continue +# moduleNames = [f for f in os.listdir(moduleDir) if f.endswith('gents.py')] +# print(moduleNames) +# for modulename in moduleNames: +# try: +# module = __import__(modulename[:-3]) +# except ImportError: +# continue +# print(module) +# if pacman in dir(module): +# if nographics and modulename == 'keyboardAgents.py': +# raise Exception('Using the keyboard requires graphics (not text display)') +# return getattr(module, pacman) +# raise Exception('The agent ' + pacman + ' is not specified in any *Agents.py.') + +def replayGame( layout, actions, display ): + import ghostAgents + from irlc.berkley import pacmanAgents + rules = ClassicGameRules() + agents = [pacmanAgents.GreedyAgent()] + [irlc.pacman.pacman_utils.RandomGhost(i + 1) for i in range(layout.getNumGhosts())] + game = rules.newGame( layout, agents[0], agents[1:], display ) + state = game.state + display.initialize(state.data) + + for action in actions: + # Execute the action + state = state.generateSuccessor( *action ) + # Change the display + display.update( state.data ) + # Allow for game specific conditions (winning, losing, etc.) + rules.process(state, game) + + display.finish() + +def runGames( layout, pacman, ghosts, display, numGames, record, numTraining = 0, catchExceptions=False, timeout=30 ): + # import __main__ + # global __main__ + # __main__.__dict__['_display'] = display + + rules = ClassicGameRules(timeout) + games = [] + + for i in range( numGames ): + beQuiet = i < numTraining + if beQuiet: + # Suppress output and graphics + import text_display_pacman + gameDisplay = text_display_pacman.NullGraphics() + rules.quiet = True + else: + gameDisplay = display + rules.quiet = False + game = rules.newGame( layout, pacman, ghosts, gameDisplay, beQuiet, catchExceptions) + game.run() + if not beQuiet: games.append(game) + + if record: + import time, cPickle + fname = ('recorded-game-%d' % (i + 1)) + '-'.join([str(t) for t in time.localtime()[1:6]]) + with open(fname, "w") as f: + # f = file(fname, 'w') + components = {'layout': layout, 'actions': game.moveHistory} + cPickle.dump(components, f) + # f.close() + + if (numGames-numTraining) > 0: + scores = [game.state.getScore() for game in games] + wins = [game.state.is_won() for game in games] + winRate = wins.count(True)/ float(len(wins)) + print('Average Score:', sum(scores) / float(len(scores))) + print('Scores: ', ', '.join([str(score) for score in scores])) + print('Win Rate: %d/%d (%.2f)' % (wins.count(True), len(wins), winRate)) + print('Record: ', ', '.join([ ['Loss', 'Win'][int(w)] for w in wins])) + + return games + +# if __name__ == '__main__': +# """ +# The main function called when gamestate.py is run +# from the command line: +# +# > python gamestate.py +# +# See the usage string for more details. +# +# > python gamestate.py --help +# """ +# import sys +# +# sys.adaptor = 'tk' +# # sys.adaptor = 'gym' +# ss = "-p PacmanQAgent -n 1 -l mediumGrid -a numTraining=100" +# +# sys.argv.extend(ss.split()) +# args = readCommand( sys.argv[1:] ) # Get game components based on input +# runGames( **args ) +# +# # import cProfile +# # cProfile.run("runGames( **args )") +# pass + + +def nearestPoint( pos ): + """ + Finds the nearest grid point to a position (discretizes). + """ + ( current_row, current_col ) = pos + + grid_row = int( current_row + 0.5 ) + grid_col = int( current_col + 0.5 ) + return ( grid_row, grid_col ) + +def manhattanDistance( xy1, xy2 ): + "Returns the Manhattan distance between points xy1 and xy2" + return abs( xy1[0] - xy2[0] ) + abs( xy1[1] - xy2[1] ) diff --git a/irlc/pacman/layout.py b/irlc/pacman/layout.py new file mode 100644 index 0000000000000000000000000000000000000000..92413e01429a751e019e77b70c2027d8ab912f76 --- /dev/null +++ b/irlc/pacman/layout.py @@ -0,0 +1,157 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# layout.py +# --------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +# from irlc.berkley.util import manhattanDistance +from irlc.pacman.pacman_utils import Grid +import os +import random + +VISIBILITY_MATRIX_CACHE = {} + +def manhattanDistance( xy1, xy2 ): + "Returns the Manhattan distance between points xy1 and xy2" + return abs( xy1[0] - xy2[0] ) + abs( xy1[1] - xy2[1] ) + +class Layout: + """ + A Layout manages the static information about the game board. + """ + + def __init__(self, layoutText): + self.width = len(layoutText[0]) + self.height= len(layoutText) + self.walls = Grid(self.width, self.height, False) + self.food = Grid(self.width, self.height, False) + self.capsules = [] + self.agentPositions = [] + self.numGhosts = 0 + self.processLayoutText(layoutText) + self.layoutText = layoutText + self.totalFood = len(self.food.asList()) + # self.initializeVisibilityMatrix() + + def getNumGhosts(self): + return self.numGhosts + + # def initializeVisibilityMatrix(self): + # global VISIBILITY_MATRIX_CACHE + # if reduce(str.__add__, self.layoutText) not in VISIBILITY_MATRIX_CACHE: + # from game import Directions + # vecs = [(-0.5,0), (0.5,0),(0,-0.5),(0,0.5)] + # dirs = [Directions.NORTH, Directions.SOUTH, Directions.WEST, Directions.EAST] + # vis = Grid(self.width, self.height, {Directions.NORTH:set(), Directions.SOUTH:set(), Directions.EAST:set(), Directions.WEST:set(), Directions.STOP:set()}) + # for x in range(self.width): + # for y in range(self.height): + # if self.walls[x][y] == False: + # for vec, direction in zip(vecs, dirs): + # dx, dy = vec + # nextx, nexty = x + dx, y + dy + # while (nextx + nexty) != int(nextx) + int(nexty) or not self.walls[int(nextx)][int(nexty)] : + # vis[x][y][direction].add((nextx, nexty)) + # nextx, nexty = x + dx, y + dy + # self.visibility = vis + # VISIBILITY_MATRIX_CACHE[reduce(str.__add__, self.layoutText)] = vis + # else: + # self.visibility = VISIBILITY_MATRIX_CACHE[reduce(str.__add__, self.layoutText)] + + def isWall(self, pos): + x, col = pos + return self.walls[x][col] + + def getRandomLegalPosition(self): + x = random.choice(range(self.width)) + y = random.choice(range(self.height)) + while self.isWall( (x, y) ): + x = random.choice(range(self.width)) + y = random.choice(range(self.height)) + return (x,y) + + def getRandomCorner(self): + poses = [(1,1), (1, self.height - 2), (self.width - 2, 1), (self.width - 2, self.height - 2)] + return random.choice(poses) + + def getFurthestCorner(self, pacPos): + poses = [(1,1), (1, self.height - 2), (self.width - 2, 1), (self.width - 2, self.height - 2)] + dist, pos = max([(manhattanDistance(p, pacPos), p) for p in poses]) + return pos + + # def isVisibleFrom(self, ghostPos, pacPos, pacDirection): + # row, col = [int(x) for x in pacPos] + # return ghostPos in self.visibility[row][col][pacDirection] + + def __str__(self): + return "\n".join(self.layoutText) + + def deepCopy(self): + return Layout(self.layoutText[:]) + + def processLayoutText(self, layoutText): + """ + Coordinates are flipped from the input format to the (x,y) convention here + + The shape of the maze. Each character + represents a different type of object. + % - Wall + . - Food + o - Capsule + G - Ghost + P - Pacman + Other characters are ignored. + """ + maxY = self.height - 1 + for y in range(self.height): + for x in range(self.width): + layoutChar = layoutText[maxY - y][x] + self.processLayoutChar(x, y, layoutChar) + self.agentPositions.sort() + self.agentPositions = [ ( i == 0, pos) for i, pos in self.agentPositions] + + def processLayoutChar(self, x, y, layoutChar): + if layoutChar == '%': + self.walls[x][y] = True + elif layoutChar == '.': + self.food[x][y] = True + elif layoutChar == 'o': + self.capsules.append((x, y)) + elif layoutChar == 'P': + self.agentPositions.append( (0, (x, y) ) ) + elif layoutChar in ['G']: + self.agentPositions.append( (1, (x, y) ) ) + self.numGhosts += 1 + elif layoutChar in ['1', '2', '3', '4']: + self.agentPositions.append( (int(layoutChar), (x,y))) + self.numGhosts += 1 +def getLayout(name, back = 2): + if name.endswith('.lay'): + layout = tryToLoad('layouts/' + name) + if layout == None: layout = tryToLoad(name) + else: + layout = tryToLoad('layouts/' + name + '.lay') + if layout == None: layout = tryToLoad(name + '.lay') + if layout == None and back >= 0: + curdir = os.path.abspath('.') + os.chdir('..') + layout = getLayout(name, back -1) + os.chdir(curdir) + return layout + +def tryToLoad(fullname): + import pathlib + fullname = os.path.join(fullname, pathlib.Path(__file__).parent.absolute(), fullname) + if(not os.path.exists(fullname)): return None + # os.path.abspath(fullname) + f = open(fullname) + try: return Layout([line.strip() for line in f]) + finally: f.close() diff --git a/irlc/pacman/layouts/bigCorners.lay b/irlc/pacman/layouts/bigCorners.lay new file mode 100644 index 0000000000000000000000000000000000000000..4d89d7bc33868d51dd93d2e019a4dae1f62dd043 --- /dev/null +++ b/irlc/pacman/layouts/bigCorners.lay @@ -0,0 +1,37 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%. % %.% +% %%%%% % %%% %%% %%%%%%% % % +% % % % % % % % +%%%%% %%%%% %%% % % % %%% %%%%% % %%% +% % % % % % % % % % % % % +% %%% % % % %%% %%%%% %%% % %%% %%% % +% % % % % % % % % +%%% %%%%%%%%% %%%%%%% %%% %%% % % % % +% % % % % % % +% % %%%%% % %%% % % %%% % %%% %%% % % +% % % % % % % % % % % % % % +% % % %%%%%%% % %%%%%%%%% %%% % %%% % +% % % % % % % % % % +%%% %%% % %%%%% %%%%% %%% %%% %%%%% % +% % % % % % % % % +% % % % % % %%% %%% %%% % % % % % % +% % % % % %% % % % % % % % % % +% % %%%%% % %%% %%% % %%% %%% %%%%% +% % % % % % % % % % % +% %%% % % % %%% %%% %%%%%%%%% % %%% +% % % % % % % +% %%% %%%%%%%%%%%%%%%%%%%%% % % %%% % +% % % % +% % % %%%%% %%% % % % % %%%%%%%%%%%%% +% % % % % % % % % % % % +% % %%% %%% % % % %%%%%%%%% %%% % % % +% % % % % % %P % % % % % % +% %%% %%% %%% % %%% % % %%%%% % %%%%% +% % % % % % % % +%%% % %%%%% %%%%% %%% %%% % %%% % %%% +% % % % % % % % % % % % % % % +% % %%% % % % % %%%%%%%%% % % % % % % +% % % % +% % % %%% %%% %%%%%%% %%% %%% %%% % +%.% % % % % .% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/irlc/pacman/layouts/bigHunt.lay b/irlc/pacman/layouts/bigHunt.lay new file mode 100644 index 0000000000000000000000000000000000000000..48ccd0cc68a28856ad50d0b10b67900e4e24366f --- /dev/null +++ b/irlc/pacman/layouts/bigHunt.lay @@ -0,0 +1,20 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%% +%P % +% %%%%%%%%%%%% %%% % +% %% % +% %% % +% % % % +% %%%%%% %%% %% % %G% +% %%%%%% +% %%%%%% % % % +% % % % % +% % G % % %%%%%%%% % +% % % % % +% % % % %%%%%%%% % +% % G % +% %% % %% %% % +% %% % % +% G% % +%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % % % %%%%%%%%%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/irlc/pacman/layouts/bigMaze.lay b/irlc/pacman/layouts/bigMaze.lay new file mode 100644 index 0000000000000000000000000000000000000000..e11fade6e907ee916bf9bedb4c8de3cbddd17c97 --- /dev/null +++ b/irlc/pacman/layouts/bigMaze.lay @@ -0,0 +1,37 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% % % % % % % % +% %%%%%%% % %%% % %%% %%% %%%%%%% % % +% % % % % % % % +%%%%% %%%%% %%% % % % %%% %%%%% % %%% +% % % % % % % % % % % % % % +% %%% % % % %%% %%%%% %%% % %%% %%% % +% % % % % % % % % +%%% %%%%%%%%% %%%%%%% %%% %%% % % % % +% % % % % % % +% % %%%%% % %%% % % %%% % %%% %%% % % +% % % % % % % % % % % % % % +% % % %%%%%%% % %%%%%%%%% %%% % %%% % +% % % % % % % % % % +%%% %%% % %%%%% %%%%% %%% %%% %%%%% % +% % % % % % % % % % % % +% % % % % %%% %%% %%% %%% % % % % % % +% % % % % % % % % +%%% %%%%%%% % % %%%%% %%% % %%% %%%%% +% % % % % % % % % % +%%%%% % % %%%%%%%%% %%%%%%%%%%% % %%% +% % % % % % % % % +% %%% %%%%% %%%%%%%%% %%%%% % % %%% % +% % % % % % % +% % % %%%%% %%% % % % % %%%%%%%%%%%%% +% % % % % % % % % % % % +% % %%% %%% % % % %%%%%%%%% %%% % % % +% % % % % % % % % % % % % +% %%% %%% %%%%% %%% % % %%%%% % %%%%% +% % % % % % % % % +%%% % %%%%% %%%%% %%% %%% % %%% % %%% +% % % % % % % % % % % % % % % +% % %%% % % % % %%%%%%%%% % % % % % % +% % % % % % +% % % % %%% %%% %%%%%%% %%% %%% %%% % +%.% % % % % % % % P% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/irlc/pacman/layouts/bigSafeSearch.lay b/irlc/pacman/layouts/bigSafeSearch.lay new file mode 100644 index 0000000000000000000000000000000000000000..b5fd414060d7366e3ddf121ac7a0fa6cb325ed26 --- /dev/null +++ b/irlc/pacman/layouts/bigSafeSearch.lay @@ -0,0 +1,8 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%.%.........%% G % o%%%%.....% +%.%.%%%%%%%.%%%%%% %%%%%%%.%%.% +%............%...%............% +%%%%%...%%%.. ..%.%...%.%%% +%o%%%.%%%%%.%%%%%%%.%%%.%.%%%%% +% ..........Po...%...%. o% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/irlc/pacman/layouts/bigSearch.lay b/irlc/pacman/layouts/bigSearch.lay new file mode 100644 index 0000000000000000000000000000000000000000..bb59eb8db68c35789c625e8fe7c987f4293a84aa --- /dev/null +++ b/irlc/pacman/layouts/bigSearch.lay @@ -0,0 +1,15 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%.....%.................%.....% +%.%%%.%.%%%.%%%%%%%.%%%.%.....% +%.%...%.%......%......%.%.....% +%...%%%.%.%%%%.%.%%%%...%%%...% +%%%.%.%.%.%......%..%.%...%.%%% +%...%.%%%.%.%%% %%%.%.%%%.%...% +%.%%%.......% %.......%%%.% +%...%.%%%%%.%%%%%%%.%.%%%.%...% +%%%.%...%.%....%....%.%...%.%%% +%...%%%.%.%%%%.%.%%%%.%.%%%...% +%.......%......%......%.....%.% +%.....%.%%%.%%%%%%%.%%%.%.%%%.% +%.....%........P....%...%.....% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/irlc/pacman/layouts/boxSearch.lay b/irlc/pacman/layouts/boxSearch.lay new file mode 100644 index 0000000000000000000000000000000000000000..4a113fcd9ea16537f99997244336a005c88bc9d2 --- /dev/null +++ b/irlc/pacman/layouts/boxSearch.lay @@ -0,0 +1,14 @@ +%%%%%%%%%%%%%% +%. . . . . % % +% % % +%. . . . . %G% +% % % +%. . . . . % % +% % % +%. . . . . % % +% P %G% +%. . . . . % % +% % % +%. . . . . % % +% % % +%%%%%%%%%%%%%% diff --git a/irlc/pacman/layouts/capsuleClassic.lay b/irlc/pacman/layouts/capsuleClassic.lay new file mode 100644 index 0000000000000000000000000000000000000000..06a5c51ad27818a5871869f2851e3f6229728e3b --- /dev/null +++ b/irlc/pacman/layouts/capsuleClassic.lay @@ -0,0 +1,7 @@ +%%%%%%%%%%%%%%%%%%% +%G. G ....% +%.% % %%%%%% %.%%.% +%.%o% % o% %.o%.% +%.%%%.% %%% %..%.% +%..... P %..%G% +%%%%%%%%%%%%%%%%%%%% diff --git a/irlc/pacman/layouts/contestClassic.lay b/irlc/pacman/layouts/contestClassic.lay new file mode 100644 index 0000000000000000000000000000000000000000..84c8733a8446bff05f8c1f1f377325c7ba2ae35e --- /dev/null +++ b/irlc/pacman/layouts/contestClassic.lay @@ -0,0 +1,9 @@ +%%%%%%%%%%%%%%%%%%%% +%o...%........%...o% +%.%%.%.%%..%%.%.%%.% +%...... G GG%......% +%.%.%%.%% %%%.%%.%.% +%.%....% ooo%.%..%.% +%.%.%%.% %% %.%.%%.% +%o%......P....%....% +%%%%%%%%%%%%%%%%%%%% diff --git a/irlc/pacman/layouts/contoursMaze.lay b/irlc/pacman/layouts/contoursMaze.lay new file mode 100644 index 0000000000000000000000000000000000000000..a06895692e8db8d90c85296885232a8d3f1c5829 --- /dev/null +++ b/irlc/pacman/layouts/contoursMaze.lay @@ -0,0 +1,11 @@ +%%%%%%%%%%%%%%%%%%%%% +% % +% % +% % +% % +% P % +% % +% % +% % +%. % +%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/irlc/pacman/layouts/greedySearch.lay b/irlc/pacman/layouts/greedySearch.lay new file mode 100644 index 0000000000000000000000000000000000000000..4072363672fd50bf31b5cf5e10ef10f3f0e8617b --- /dev/null +++ b/irlc/pacman/layouts/greedySearch.lay @@ -0,0 +1,8 @@ +%%%%%% +%....% +% %%.% +% %%.% +%.P .% +%.%%%% +%....% +%%%%%% \ No newline at end of file diff --git a/irlc/pacman/layouts/mediumClassic.lay b/irlc/pacman/layouts/mediumClassic.lay new file mode 100644 index 0000000000000000000000000000000000000000..33c5db85a6b6b99130e1c357731bf152813626b5 --- /dev/null +++ b/irlc/pacman/layouts/mediumClassic.lay @@ -0,0 +1,11 @@ +%%%%%%%%%%%%%%%%%%%% +%o...%........%....% +%.%%.%.%%%%%%.%.%%.% +%.%..............%.% +%.%.%%.%% %%.%%.%.% +%......%G G%......% +%.%.%%.%%%%%%.%%.%.% +%.%..............%.% +%.%%.%.%%%%%%.%.%%.% +%....%...P....%...o% +%%%%%%%%%%%%%%%%%%%% diff --git a/irlc/pacman/layouts/mediumCorners.lay b/irlc/pacman/layouts/mediumCorners.lay new file mode 100644 index 0000000000000000000000000000000000000000..6a397568874a3e78a575b94fcd5aa307298d4eb1 --- /dev/null +++ b/irlc/pacman/layouts/mediumCorners.lay @@ -0,0 +1,14 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%. % % % %.% +% % % %%%%%% %%%%%%% % % +% % % % % % +%%%%% %%%%% %%% %% %%%%% % %%% +% % % % % % % % % +% %%% % % % %%%%%%%% %%% %%% % +% % %% % % % % +%%% % %%%%%%% %%%% %%% % % % % +% % %% % % % +% % %%%%% % %%%% % %%% %%% % % +% % % % % % %%% % +%. %P%%%%% % %%% % .% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/irlc/pacman/layouts/mediumDottedMaze.lay b/irlc/pacman/layouts/mediumDottedMaze.lay new file mode 100644 index 0000000000000000000000000000000000000000..103f818d75f1271e3aadf6e078d968676992823e --- /dev/null +++ b/irlc/pacman/layouts/mediumDottedMaze.lay @@ -0,0 +1,18 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% P% +% %%%%%%%%%%%%%%%%%%% %%% %%%%%%%% % +% %% % % %%% %%% %% ... % +% %% % % % % %%%% %%%%%%%%% %% %%%%% +% %% % % % % % %% %% %% ... % +% %% % % % % % %%%% %%% %%%%%% % +% % % % % % %% %%%%%%%% ... % +% %% % % %%%%%%%% %% %% %%%%% +% %% % %% %%%%%%%%% %% ... % +% %%%%%% %%%%%%% %% %%%%%% % +%%%%%% % %%%% %% % ... % +% %%%%%% %%%%% % %% %% %%%%% +% %%%%%% % %%%%% %% % +% %%%%%% %%%%%%%%%%% %% %% % +%%%%%%%%%% %%%%%% % +%. %%%%%%%%%%%%%%%% ...... % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/irlc/pacman/layouts/mediumGrid.lay b/irlc/pacman/layouts/mediumGrid.lay new file mode 100644 index 0000000000000000000000000000000000000000..52b270754875523aa9144bfa10498784cb6e3c61 --- /dev/null +++ b/irlc/pacman/layouts/mediumGrid.lay @@ -0,0 +1,7 @@ +%%%%%%%% +%P % +% .% . % +% % % +% .% . % +% G% +%%%%%%%% diff --git a/irlc/pacman/layouts/mediumMaze.lay b/irlc/pacman/layouts/mediumMaze.lay new file mode 100644 index 0000000000000000000000000000000000000000..55c1236e1fd462408a7c9ebc9091c2001def89e1 --- /dev/null +++ b/irlc/pacman/layouts/mediumMaze.lay @@ -0,0 +1,18 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% P% +% %%%%%%%%%%%%%%%%%%%%%%% %%%%%%%% % +% %% % % %%%%%%% %% % +% %% % % % % %%%% %%%%%%%%% %% %%%%% +% %% % % % % %% %% % +% %% % % % % % %%%% %%% %%%%%% % +% % % % % % %% %%%%%%%% % +% %% % % %%%%%%%% %% %% %%%%% +% %% % %% %%%%%%%%% %% % +% %%%%%% %%%%%%% %% %%%%%% % +%%%%%% % %%%% %% % % +% %%%%%% %%%%% % %% %% %%%%% +% %%%%%% % %%%%% %% % +% %%%%%% %%%%%%%%%%% %% %% % +%%%%%%%%%% %%%%%% % +%. %%%%%%%%%%%%%%%% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/irlc/pacman/layouts/mediumSafeSearch.lay b/irlc/pacman/layouts/mediumSafeSearch.lay new file mode 100644 index 0000000000000000000000000000000000000000..e7d6b1cc444b6b5b13bbedcc8b4ab1cf575ff0c6 --- /dev/null +++ b/irlc/pacman/layouts/mediumSafeSearch.lay @@ -0,0 +1,6 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%.% ....%% G %%%%%% o%%.% +%.%o%%%%%%%.%%%%%%% %%%%%.% +% %%%.%%%%%.%%%%%%%.%%%.%.%%%.% +% ..........Po...%.........% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/irlc/pacman/layouts/mediumScaryMaze.lay b/irlc/pacman/layouts/mediumScaryMaze.lay new file mode 100644 index 0000000000000000000000000000000000000000..65d4c33d1a422fa6013289415a5269b0d5c8ccf2 --- /dev/null +++ b/irlc/pacman/layouts/mediumScaryMaze.lay @@ -0,0 +1,18 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% P% +% %%%%%%%%%%%%%%%%%%% %%% %%%%%%%% % +% %% % % %%% %%% %%GG % +% %% % % % % %%%% %%%%%%%%% %% %%%%% +% %% % % % % % %%GG %% % +% %% % % % % % %%%%% %%% %%%%%% % +% %% % % % % %% %%%%%%%%% % +% %% % % %%%%%%%% %% %% %%%%% +% %% % %% %%%%%%%%% %% % +% %%% %% %%%%%%% %% %%%%%% % +%%%%%% % % %% %% % +% %%%%%% %% %% %% %% %%%%% +% %%%%%% % %%%%% %% % +% %%%% %%%%% %%%%%% % +%%%%%%%% % %%%%%% % +%. %%%%%%%%%%%%%%%% % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/irlc/pacman/layouts/mediumSearch.lay b/irlc/pacman/layouts/mediumSearch.lay new file mode 100644 index 0000000000000000000000000000000000000000..2f8af420ffda3e75db27f0129da5a145e91849f7 --- /dev/null +++ b/irlc/pacman/layouts/mediumSearch.lay @@ -0,0 +1,8 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%............%%%%%............% +%%%.%...%%%.........%.%...%.%%% +%...%%%.%.%%%%.%.%%%%%%.%%%...% +%.%.....%......%......%.....%.% +%.%%%.%%%%%.%%%%%%%.%%%.%.%%%%% +%.....%........P....%...%.....% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/irlc/pacman/layouts/minimaxClassic.lay b/irlc/pacman/layouts/minimaxClassic.lay new file mode 100644 index 0000000000000000000000000000000000000000..a547397b1195dfbba589af8628d90f086470199f --- /dev/null +++ b/irlc/pacman/layouts/minimaxClassic.lay @@ -0,0 +1,5 @@ +%%%%%%%%% +%.P G% +% %.%G%%% +%G %%% +%%%%%%%%% diff --git a/irlc/pacman/layouts/oddSearch.lay b/irlc/pacman/layouts/oddSearch.lay new file mode 100644 index 0000000000000000000000000000000000000000..2ddbc9a7e196feb033ff6635ba2c3f83c43d4c7e --- /dev/null +++ b/irlc/pacman/layouts/oddSearch.lay @@ -0,0 +1,7 @@ +%%%%%%%%%%%%%%%%%%%% +%...%.........%%...% +%.%.%.%%%%%%%%%%.%.% +%..................% +%%%%%%%%.%.%%%%%%%P% +%%%%%%%%....... % +%%%%%%%%%%%%%%%%%%%% diff --git a/irlc/pacman/layouts/oneHunt.lay b/irlc/pacman/layouts/oneHunt.lay new file mode 100644 index 0000000000000000000000000000000000000000..45291a9195d9a3a34c0a3de6249c4aa23f69cbf3 --- /dev/null +++ b/irlc/pacman/layouts/oneHunt.lay @@ -0,0 +1,16 @@ +%%%%%%%%%%%%%%%%%%%% +% % +% % +% G G % +% % +% P % +% % +% % +% % +% G G % +% % +% % +% % +%%%%%%%%%%%%%%%%%%%% +% % % % %%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/irlc/pacman/layouts/openClassic.lay b/irlc/pacman/layouts/openClassic.lay new file mode 100644 index 0000000000000000000000000000000000000000..6760b427eea3e203e645a1c3922d9218b852644d --- /dev/null +++ b/irlc/pacman/layouts/openClassic.lay @@ -0,0 +1,9 @@ +%%%%%%%%%%%%%%%%%%%%%%%%% +%.. P .... .... % +%.. ... ... ... ... % +%.. ... ... ... ... % +%.. .... .... G % +%.. ... ... ... ... % +%.. ... ... ... ... % +%.. .... .... o% +%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/irlc/pacman/layouts/openHunt.lay b/irlc/pacman/layouts/openHunt.lay new file mode 100644 index 0000000000000000000000000000000000000000..45d33887be999ebea9360b78caa3906df1a188f8 --- /dev/null +++ b/irlc/pacman/layouts/openHunt.lay @@ -0,0 +1,13 @@ +%%%%%%%%%%%%%%%%%%%% +%P G % +% %%% %%% %% %%% % +% G % +% % % +% % % +% %%%%%% %%%G%%% % +% G % +% % % +% % % +%%%%%%%%%%%%%%%%%%%% +% % % % %%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%% diff --git a/irlc/pacman/layouts/openMaze.lay b/irlc/pacman/layouts/openMaze.lay new file mode 100644 index 0000000000000000000000000000000000000000..5dee6891b79b07e3afb256b71c8cc1dab68ca975 --- /dev/null +++ b/irlc/pacman/layouts/openMaze.lay @@ -0,0 +1,23 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% P% +% % % +% % % +% % % +% % % +% % % +% % % % +% % % % +% % % % +% % % % +% % % % +% % % % +% % % % +%%%%%%%%%%%%%% %%%%%%%%%%%%%%%%% +% % % +% % % +% % % +% % +% % +% % +%. % +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/irlc/pacman/layouts/openSearch.lay b/irlc/pacman/layouts/openSearch.lay new file mode 100644 index 0000000000000000000000000000000000000000..f02d21d906d09e09f495c1fe37375fb7a89a6e07 --- /dev/null +++ b/irlc/pacman/layouts/openSearch.lay @@ -0,0 +1,7 @@ +%%%%%%%%%%%%%%%%%%%% +%..................% +%..................% +%........P.........% +%..................% +%..................% +%%%%%%%%%%%%%%%%%%%% diff --git a/irlc/pacman/layouts/originalClassic.lay b/irlc/pacman/layouts/originalClassic.lay new file mode 100644 index 0000000000000000000000000000000000000000..b2770c571cbaa3c2a556738028427a826fea3401 --- /dev/null +++ b/irlc/pacman/layouts/originalClassic.lay @@ -0,0 +1,27 @@ +%%%%%%%%%%%%%%%%%%%%%%%%%%%% +%............%%............% +%.%%%%.%%%%%.%%.%%%%%.%%%%.% +%o%%%%.%%%%%.%%.%%%%%.%%%%o% +%.%%%%.%%%%%.%%.%%%%%.%%%%.% +%..........................% +%.%%%%.%%.%%%%%%%%.%%.%%%%.% +%.%%%%.%%.%%%%%%%%.%%.%%%%.% +%......%%....%%....%%......% +%%%%%%.%%%%% %% %%%%%.%%%%%% +%%%%%%.%%%%% %% %%%%%.%%%%%% +%%%%%%.% %.%%%%%% +%%%%%%.% %%%% %%%% %.%%%%%% +% . %G GG G% . % +%%%%%%.% %%%%%%%%%% %.%%%%%% +%%%%%%.% %.%%%%%% +%%%%%%.% %%%%%%%%%% %.%%%%%% +%............%%............% +%.%%%%.%%%%%.%%.%%%%%.%%%%.% +%.%%%%.%%%%%.%%.%%%%%.%%%%.% +%o..%%....... .......%%..o% +%%%.%%.%%.%%%%%%%%.%%.%%.%%% +%%%.%%.%%.%%%%%%%%.%%.%%.%%% +%......%%....%%....%%......% +%.%%%%%%%%%%.%%.%%%%%%%%%%.% +%.............P............% +%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/irlc/pacman/layouts/powerClassic.lay b/irlc/pacman/layouts/powerClassic.lay new file mode 100644 index 0000000000000000000000000000000000000000..3f3d983a38b1acfbffd4ac77f0909ebc2d4fef42 --- /dev/null +++ b/irlc/pacman/layouts/powerClassic.lay @@ -0,0 +1,7 @@ +%%%%%%%%%%%%%%%%%%%% +%o....o%GGGG%o....o% +%..%...%% %%...%..% +%.%o.%........%.o%.% +%.o%.%.%%%%%%.%.%o.% +%........P.........% +%%%%%%%%%%%%%%%%%%%% diff --git a/irlc/pacman/layouts/smallClassic.lay b/irlc/pacman/layouts/smallClassic.lay new file mode 100644 index 0000000000000000000000000000000000000000..ce6c1d980029e70c7eb2bc5ce259067385007839 --- /dev/null +++ b/irlc/pacman/layouts/smallClassic.lay @@ -0,0 +1,7 @@ +%%%%%%%%%%%%%%%%%%%% +%......%G G%......% +%.%%...%% %%...%%.% +%.%o.%........%.o%.% +%.%%.%.%%%%%%.%.%%.% +%........P.........% +%%%%%%%%%%%%%%%%%%%% diff --git a/irlc/pacman/layouts/smallGrid.lay b/irlc/pacman/layouts/smallGrid.lay new file mode 100644 index 0000000000000000000000000000000000000000..4bbe2b6f630e2dabb95d0e223696e5759f4bd3ae --- /dev/null +++ b/irlc/pacman/layouts/smallGrid.lay @@ -0,0 +1,7 @@ +%%%%%%% +% P % +% %%% % +% %. % +% %%% % +%. G % +%%%%%%% diff --git a/irlc/pacman/layouts/smallHunt.lay b/irlc/pacman/layouts/smallHunt.lay new file mode 100644 index 0000000000000000000000000000000000000000..ef9059a6a5a958f91f9c8dc2a620d78f8e1911e1 --- /dev/null +++ b/irlc/pacman/layouts/smallHunt.lay @@ -0,0 +1,8 @@ +%%%%%%%%%%%%%%%%%%%% +%P G G % +% %%%%% %%%%%% % % % +% G % +% G % +%%%%%%%%%%%%%%%%%%%% +% % % % %%%%%%%%%%%% +%%%%%%%%%%%%%%%%%%%% diff --git a/irlc/pacman/layouts/smallMaze.lay b/irlc/pacman/layouts/smallMaze.lay new file mode 100644 index 0000000000000000000000000000000000000000..72d3ffc68bf290d1c83fc0c640b9e534666cbe54 --- /dev/null +++ b/irlc/pacman/layouts/smallMaze.lay @@ -0,0 +1,10 @@ +%%%%%%%%%%%%%%%%%%%%%% +% %% % % % +% %%%%%% % %%%%%% % +%%%%%% P % % +% % %%%%%% %% %%%%% +% %%%% % % % +% %%% %%% % % +%%%%%%%%%% %%%%%% % +%. %% % +%%%%%%%%%%%%%%%%%%%%%% \ No newline at end of file diff --git a/irlc/pacman/layouts/smallSafeSearch.lay b/irlc/pacman/layouts/smallSafeSearch.lay new file mode 100644 index 0000000000000000000000000000000000000000..b97feaa1419ed38709b989c7d086069c93a42b0b --- /dev/null +++ b/irlc/pacman/layouts/smallSafeSearch.lay @@ -0,0 +1,15 @@ +%%%%%%%%% +%.. % G % +%%% %%%%% +% % +%%%%%%% % +% % +% %%%%% % +% % % +%%%%% % % +% %o% +% %%%%%%% +% .% +%%%%%%%.% +%Po .% +%%%%%%%%% diff --git a/irlc/pacman/layouts/smallSearch.lay b/irlc/pacman/layouts/smallSearch.lay new file mode 100644 index 0000000000000000000000000000000000000000..c2321d4701bf5fa761407e8bc171c4d5fda66991 --- /dev/null +++ b/irlc/pacman/layouts/smallSearch.lay @@ -0,0 +1,5 @@ +%%%%%%%%%%%%%%%%%%%% +%. ...P .% +%.%%.%%.%%.%%.%% %.% +% %% %..... %.% +%%%%%%%%%%%%%%%%%%%% diff --git a/irlc/pacman/layouts/testClassic.lay b/irlc/pacman/layouts/testClassic.lay new file mode 100644 index 0000000000000000000000000000000000000000..4b3ffcabca09ecab04f7a6dfab205b459c3ac996 --- /dev/null +++ b/irlc/pacman/layouts/testClassic.lay @@ -0,0 +1,10 @@ +%%%%% +% . % +%.G.% +% . % +%. .% +% % +% .% +% % +%P .% +%%%%% diff --git a/irlc/pacman/layouts/testMaze.lay b/irlc/pacman/layouts/testMaze.lay new file mode 100644 index 0000000000000000000000000000000000000000..4d259a4fc7dcf78b904c13a369d40234935b6bfd --- /dev/null +++ b/irlc/pacman/layouts/testMaze.lay @@ -0,0 +1,3 @@ +%%%%%%%%%% +%. P% +%%%%%%%%%% diff --git a/irlc/pacman/layouts/testSearch.lay b/irlc/pacman/layouts/testSearch.lay new file mode 100644 index 0000000000000000000000000000000000000000..25bad237d8044018988ab6c0054d60317055207a --- /dev/null +++ b/irlc/pacman/layouts/testSearch.lay @@ -0,0 +1,5 @@ +%%%%% +%.P % +%%% % +%. % +%%%%% diff --git a/irlc/pacman/layouts/tinyCorners.lay b/irlc/pacman/layouts/tinyCorners.lay new file mode 100644 index 0000000000000000000000000000000000000000..526c88061163083aedb184e0ec6876e009b67bc0 --- /dev/null +++ b/irlc/pacman/layouts/tinyCorners.lay @@ -0,0 +1,8 @@ +%%%%%%%% +%. .% +% P % +% %%%% % +% % % +% % %%%% +%.% .% +%%%%%%%% diff --git a/irlc/pacman/layouts/tinyMaze.lay b/irlc/pacman/layouts/tinyMaze.lay new file mode 100644 index 0000000000000000000000000000000000000000..f7035a597d39ca648b0518077d608d1dceca98d7 --- /dev/null +++ b/irlc/pacman/layouts/tinyMaze.lay @@ -0,0 +1,7 @@ +%%%%%%% +% P% +% %%% % +% % % +%% %% +%. %%%% +%%%%%%% diff --git a/irlc/pacman/layouts/tinySafeSearch.lay b/irlc/pacman/layouts/tinySafeSearch.lay new file mode 100644 index 0000000000000000000000000000000000000000..fea686045d0012418eda37be684d513da8c15b28 --- /dev/null +++ b/irlc/pacman/layouts/tinySafeSearch.lay @@ -0,0 +1,7 @@ +%%%%%%%%% +% G %...% +%%%%%%% % +%Po % +%.%%.%%.% +%.%%....% +%%%%%%%%% diff --git a/irlc/pacman/layouts/tinySearch.lay b/irlc/pacman/layouts/tinySearch.lay new file mode 100644 index 0000000000000000000000000000000000000000..c51f4b0400295c0dfd21ad33a4c77a222046c481 --- /dev/null +++ b/irlc/pacman/layouts/tinySearch.lay @@ -0,0 +1,7 @@ +%%%%%%%%% +%.. ..% +%%%%.%% % +% P % +%.%% %%.% +%.%. .% +%%%%%%%%% diff --git a/irlc/pacman/layouts/trappedClassic.lay b/irlc/pacman/layouts/trappedClassic.lay new file mode 100644 index 0000000000000000000000000000000000000000..289557f7eb52d0355242220eac38fef8480a5ae2 --- /dev/null +++ b/irlc/pacman/layouts/trappedClassic.lay @@ -0,0 +1,5 @@ +%%%%%%%% +% P G% +%G%%%%%% +%.... % +%%%%%%%% diff --git a/irlc/pacman/layouts/trickyClassic.lay b/irlc/pacman/layouts/trickyClassic.lay new file mode 100644 index 0000000000000000000000000000000000000000..ffa156cca272d7e64af0180da7365e1b974cd7af --- /dev/null +++ b/irlc/pacman/layouts/trickyClassic.lay @@ -0,0 +1,13 @@ +%%%%%%%%%%%%%%%%%%%% +%o...%........%...o% +%.%%.%.%%..%%.%.%%.% +%.%.....%..%.....%.% +%.%.%%.%% %%.%%.%.% +%...... GGGG%.%....% +%.%....%%%%%%.%..%.% +%.%....% oo%.%..%.% +%.%....% %%%%.%..%.% +%.%...........%..%.% +%.%%.%.%%%%%%.%.%%.% +%o...%...P....%...o% +%%%%%%%%%%%%%%%%%%%% diff --git a/irlc/pacman/layouts/trickySearch.lay b/irlc/pacman/layouts/trickySearch.lay new file mode 100644 index 0000000000000000000000000000000000000000..4a607e648c8a4981875587e88355b1c2742375b6 --- /dev/null +++ b/irlc/pacman/layouts/trickySearch.lay @@ -0,0 +1,7 @@ +%%%%%%%%%%%%%%%%%%%% +%. ..% % +%.%%.%%.%%.%%.%% % % +% P % % +%%%%%%%%%%%%%%%%%% % +%..... % +%%%%%%%%%%%%%%%%%%%% diff --git a/irlc/pacman/pacman_environment.py b/irlc/pacman/pacman_environment.py new file mode 100644 index 0000000000000000000000000000000000000000..cc75de14e55314d70113815242c264a8f2b6bffe --- /dev/null +++ b/irlc/pacman/pacman_environment.py @@ -0,0 +1,243 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import pygame +from irlc.pacman.gamestate import Directions, ClassicGameRules +from irlc.pacman.layout import getLayout +from irlc.pacman.pacman_text_display import PacmanTextDisplay +from irlc.pacman.pacman_graphics_display import PacmanGraphics, FirstPersonPacmanGraphics +from irlc.pacman.pacman_utils import PacAgent, RandomGhost +from irlc.pacman.layout import Layout +import gymnasium as gym +from gymnasium import RewardWrapper +from irlc.utils.common import ExplicitActionSpace, DiscreteTextActionSpace + +datadiscs = """ +%%%%%%% +% .% +%.P%% % +%. .% +%%%%%%% +""" + +very_small_maze = """ +%%%%%% +%P. .% +% %%% +%%%%%% +""" + +very_small_haunted_maze = """ +%%%%%% +%P. .% +% %%%% +% G% +%%%%%% +""" + + +class PacmanEnvironment(gym.Env): + _unpack_search_state = True # A hacky fix to set the search state. + """ + A fairly messy pacman environment class. I do not recommend reading this code. + """ + metadata = { + 'render.modes': ['human', 'rgb_array'], + 'video.frames_per_second': 20 + } + + # def A(self, state): + # """ + # Return a list of actions available in the given state. This function should be considered deprecated. + # """ + # raise Exception("HArd deprecation.") + # return state.A() + + def __init__(self, layout_str=None, render_mode=None, animate_movement=None, layout='mediumGrid', zoom=2.0, num_ghosts=4, frames_per_second=30, ghost_agent=None, + method_str='', allow_all_actions=False, verbose=False): + self.metadata['video_frames_per_second'] = frames_per_second + self.ghosts = [ghost_agent(i+1) if ghost_agent is not None else RandomGhost(i+1) for i in range(num_ghosts)] + if animate_movement is None: + animate_movement = render_mode =='human' + if animate_movement: + render_mode = 'human' + + # from irlc.utils. + # self.action_space = ExplicitActionSpace(self) # Wrapper environments copy the action space. + + from irlc.pacman.gamestate import Directions + self.action_space = DiscreteTextActionSpace(seed=None, actions=[Directions.NORTH, Directions.EAST, Directions.SOUTH, Directions.WEST, Directions.STOP]) + + + # Load level layout + if layout_str is not None: + self.layout = Layout([line.strip() for line in layout_str.strip().splitlines()]) + else: + self.layout = getLayout(layout) + if self.layout is None: + raise Exception("Layout file not found", layout) + self.rules = ClassicGameRules(30) + self.options_frametime = 1/frames_per_second + self.game = None + + # Setup displays. + self.first_person_graphics = False + self.animate_movement = animate_movement + self.options_zoom = zoom + self.text_display = PacmanTextDisplay(1 / frames_per_second) + self.graphics_display = None + + # temporary variables for animation/visualization. Don't remove. + self.visitedlist = None + self.ghostbeliefs = None + self.path = None + self.render_mode = render_mode + self.method = method_str + + def reset(self, seed=None, options=None): + """ + Reset the environment. + + :param seed: + :param options: + :return: + """ + self.game = self.rules.newGame(self.layout, PacAgent(index=0), self.ghosts, quiet=True, catchExceptions=False) + self.game.numMoves = 0 + if self.render_mode == 'human': + self.render() + return self.state, {'mask': self.action_space._make_mask(self.state.A()) } + + + def close(self): + if self.graphics_display is not None: + self.graphics_display.close() + return + + @property + def state(self): + if self.game is None: + return None + return self.game.state.deepCopy() + + def get_keys_to_action(self): + return {(pygame.K_LEFT,): Directions.WEST, + (pygame.K_RIGHT,): Directions.EAST, + (pygame.K_UP,): Directions.NORTH, + (pygame.K_DOWN,): Directions.SOUTH, + (pygame.K_s,): Directions.STOP, + } + + def step(self, action): + r_ = self.game.state.getScore() + done = False + + if action not in self.state.A(): + # if action not in self.A(self.state): + raise Exception(f"Agent tried {action=} available actions {self.state.A()}") + + # Let player play `action`, then let the ghosts play their moves in sequence. + for agent_index in range(len(self.game.agents)): + a = self.game.agents[agent_index].getAction(self.game.state) if agent_index > 0 else action + self.game.state = self.game.state.f(a) + self.game.rules.process(self.game.state, self.game) + + if self.graphics_display is not None and self.animate_movement and agent_index == 0: + self.graphics_display.update(self.game.state, animate=self.animate_movement, ghostbeliefs=self.ghostbeliefs, path=self.path, visitedlist=self.visitedlist) + + done = self.game.gameOver or self.game.state.is_won() or self.game.state.is_lost() + if done: + break + reward = self.game.state.getScore() - r_ + return self.state, reward, done, False, {'mask': self.action_space._make_mask(self.state.A())} + + def render(self): + if hasattr(self, 'agent'): + path = self.agent.__dict__.get('path', None) + ghostbeliefs = self.agent.__dict__.get('ghostbeliefs', None) + visitedlist = self.agent.__dict__.get('visitedlist', None) + else: + path, ghostbeliefs, visitedlist = None, None, None + + # Initialize graphics adaptor. + if self.graphics_display is None and self.render_mode in ["human", 'rgb_array']: + if self.first_person_graphics: + self.graphics_display = FirstPersonPacmanGraphics(self.game.state, self.options_zoom, showGhosts=True, frameTime=self.options_frametime, ghostbeliefs=self.ghostbeliefs) + # self.graphics_display.ghostbeliefs = self.ghostbeliefs + else: + self.graphics_display = PacmanGraphics(self.game.state, self.options_zoom, frameTime=self.options_frametime, method=self.method) + + if self.render_mode in ["human", 'rgb_array']: + # if self.graphics_display is None: + # if self.first_person_graphics: + # self.graphics_display = FirstPersonPacmanGraphics(self.options_zoom, showGhosts=True, + # frameTime=self.options_frametime) + # self.graphics_display.ghostbeliefs = self.ghostbeliefs + # else: + # self.graphics_display = PacmanGraphics(self.options_zoom, frameTime=self.options_frametime) + + if not hasattr(self.graphics_display, 'viewer'): + self.graphics_display.initialize(self.game.state.data) + + # We save these because the animation code may need it in step() + self.visitedlist = visitedlist + self.path = path + self.ghostbeliefs = ghostbeliefs + self.graphics_display.master_render(self.game.state, ghostbeliefs=ghostbeliefs, path=path, visitedlist=visitedlist) + + return self.graphics_display.blit(render_mode=self.render_mode) + # return self.graphics_display.viewer.render(return_rgb_array=self.render_mode == "rgb_array") + + elif self.render_mode in ['ascii']: + return self.text_display.draw(self.game.state) + else: + raise Exception("Bad video mode", self.render_mode) + + @property + def viewer(self): + if self.graphics_display is not None and hasattr(self.graphics_display, 'viewer'): + return self.graphics_display.viewer + else: + return None + + +class PacmanWinWrapper(RewardWrapper): + def step(self, action): + observation, reward, done, truncated, info = self.env.step(action) + if self.env.game.state.is_won(): + reward = 1 + else: + reward = 0 + return observation, reward, done, truncated, info + + +if __name__ == "__main__": + # from irlc import VideoMonitor + import time + # from irlc.utils.player_wrapper_pygame import PlayWrapperPygame + # from irlc.utils.player_wrapper import PlayWrapper + from irlc.ex01.agent import Agent, train + from irlc import interactive + + # from irlc.pacman.pacman_environment import PacmanEnvironment + # from irlc import Agent + # env = PacmanEnvironment() + # s, info = env.reset() + # agent = Agent(env) + # agent.pi(s, k=0, info=info) # get a random action + # agent.pi(s, k=0) # If info is not specified, all actions are assumed permissible. + + + env = PacmanEnvironment(layout='mediumClassic', animate_movement=True, render_mode='human') + agent = Agent(env) + # agent = PlayWrapperPygame(agent, env) + env, agent = interactive(env, agent) + + # env = VideoMonitor(env) + # experiment = "experiments/pacman_q" + # if True: + # agent = Agent(env) + # agent = PlayWrapper(agent, env) + train(env, agent, num_episodes=1) + # env.unwrapped.close() + time.sleep(0.1) + env.close() +# 230 174, 159 diff --git a/irlc/pacman/pacman_graphics_display.py b/irlc/pacman/pacman_graphics_display.py new file mode 100644 index 0000000000000000000000000000000000000000..d01f7ec562992c06e59ccf60bdcff49c0c0ef563 --- /dev/null +++ b/irlc/pacman/pacman_graphics_display.py @@ -0,0 +1,700 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# graphicsDisplay.py +# ------------------ +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + +# Most code by Dan Klein and John Denero written or rewritten for cs188, UC Berkeley. +# Some code from a Pacman implementation by LiveWires, and used / modified with permission. + +# from irlc.utils.gym_graphics_utils import formatColor, GraphicsUtilGym, colorToVector +# from irlc.utils.gym_graphics_utils import formatColor, GraphicsUtilGym, colorToVector +from irlc.utils.graphics_util_pygame import formatColor, GraphicsUtilGym, colorToVector +from irlc.pacman.pacman_utils import Directions +import math +import time + +DEFAULT_GRID_SIZE = 30.0 +INFO_PANE_HEIGHT = 35 +BACKGROUND_COLOR = formatColor(0,0,0) +WALL_COLOR = formatColor(0.0/255.0, 51.0/255.0, 255.0/255.0) +INFO_PANE_COLOR = formatColor(.4,.4,0) +SCORE_COLOR = formatColor(.9, .9, .9) +PACMAN_OUTLINE_WIDTH = 2 +PACMAN_CAPTURE_OUTLINE_WIDTH = 4 + +GHOST_COLORS = [] +GHOST_COLORS.append(formatColor(.9,0,0)) # Red +GHOST_COLORS.append(formatColor(0,.3,.9)) # Blue +GHOST_COLORS.append(formatColor(.98,.41,.07)) # Orange +GHOST_COLORS.append(formatColor(.1,.75,.7)) # Green +GHOST_COLORS.append(formatColor(1.0,0.6,0.0)) # Yellow +GHOST_COLORS.append(formatColor(.4,0.13,0.91)) # Purple + +TEAM_COLORS = GHOST_COLORS[:2] + +GHOST_SHAPE = [ + ( 0, 0.3 ), + ( 0.25, 0.75 ), + ( 0.5, 0.3 ), + ( 0.75, 0.75 ), + ( 0.75, -0.5 ), + ( 0.5, -0.75 ), + (-0.5, -0.75 ), + (-0.75, -0.5 ), + (-0.75, 0.75 ), + (-0.5, 0.3 ), + (-0.25, 0.75 ) + ] +GHOST_SIZE = 0.65 +SCARED_COLOR = formatColor(1,1,1) + +GHOST_VEC_COLORS = [colorToVector(gc) for gc in GHOST_COLORS] + +PACMAN_COLOR = formatColor(255.0/255.0, 255.0/255.0, 61.0/255) +PACMAN_SCALE = 0.5 + +# Food +FOOD_COLOR = formatColor(1,1,1) +FOOD_SIZE = 0.1 + +# Laser +LASER_COLOR = formatColor(1,0,0) +LASER_SIZE = 0.02 + +# Capsule graphics +CAPSULE_COLOR = formatColor(1,1,1) +CAPSULE_SIZE = 0.25 +# Drawing walls +WALL_RADIUS = 0.15 + +class InfoPane: + def __init__(self, ga, layout, gridSize): + self.gridSize = gridSize + self.width = (layout.width) * gridSize + self.base = (layout.height + 1) * gridSize + self.height = INFO_PANE_HEIGHT + self.fontSize = 24 + self.textColor = PACMAN_COLOR + self.drawPane() + self.ga = ga + + + def toScreen(self, pos, y = None): + """ + Translates a point relative from the bottom left of the info pane. + """ + if y == None: + x,y = pos + else: + x = pos + x = self.gridSize + x # Margin + y = self.base + y + return x,y + + def drawPane(self): + self.scoreText = {'pos':self.toScreen(0, 0), + 'color':self.textColor, + 'contents': "SCORE: 0", + 'font': "Times", + 'size': self.fontSize, + 'style': "bold"} + + def initializeGhostDistances(self, distances): + self.ghostDistanceText = [] + size = 20 + if self.width < 240: + size = 12 + if self.width < 160: + size = 10 + + for i, d in enumerate(distances): + t = {'pos': self.toScreen(self.width/2 + self.width/8 * i, 0), + 'color': GHOST_COLORS[i+1], + 'contents': str(d), + 'font': "Times", + 'size':size, + 'style': "bold"} + self.ghostDistanceText.append(t) + + def updateScore(self, score, method=''): + self.scoreText['contents'] = "SCORE: % 4d %s" %(score, method) + + def setTeam(self, isBlue): + txt = "RED TEAM" + if isBlue: txt = "BLUE TEAM" + self.teamText = {'pos': self.toScreen(300, 0 ), + 'color': self.textColor, + 'contents': txt, + 'font': "Times", + 'size': self.fontSize, + 'style': "bold"} + + def updateGhostDistances(self, distances): + if len(distances) == 0: return + self.initializeGhostDistances(distances) + + def master_render(self): + self.ga.text("master_test", **self.scoreText) + if hasattr(self, 'teamText'): + self.ga.text("team_test", **self.teamText) + if hasattr(self, 'ghostDistanceText'): + for d in self.ghostDistanceText: + self.ga.text(f"ghost_distance_text_{d}_", **d) + + def drawGhost(self): + pass + + def drawPacman(self): + pass + + def drawWarning(self): + pass + + def clearIcon(self): + pass + + def updateMessage(self, message): + pass + + def clearMessage(self): + pass + + +class PacmanGraphics: + def __init__(self, state, zoom=1.0, frameTime=0.0, capture=False, isBlue=False, method=''): + self.have_window = 0 + self.currentGhostImages = {} + self.pacmanImage = None + self.zoom = zoom + self.gridSize = DEFAULT_GRID_SIZE * zoom + self.capture = capture + self.frameTime = frameTime + # self.visitedlist = None + # self.ghostbeliefs = None # for the ghost distributions + self.ga = GraphicsUtilGym() + # Used to be initialize. + self.isBlue = isBlue + self.startGraphics(state) + self.distributionImages = None # Initialized lazily + self.previousState = state + self.method = method + + # def initialize(self, state, isBlue = False): + + def master_render(self, state, ghostbeliefs=None, visitedlist=None, path=None): + # self.viewer.geoms = [] + # self.ga.gc. + # assert False + # state = state.data + # This is completely needless. Just update the things that need to be updated and let everything else be. + + # self.ga.gc.clear() + self.ga.draw_background() + if visitedlist is not None: + self.drawExpandedCells(cells=visitedlist) + + if path is not None: + # draw the given path. + path = [self.to_screen(p) for p in path] + x, y = zip(*path) + # name = f"render_path" + for k in range(len(x)-1): + self.ga.line('asdfasdf', here=(x[k], y[k]), there=(x[k+1], y[k+1]), width=4, color= formatColor(0.5, 0.95, 0.5) ) + + # if len(path) > 1: + # self.ga.plot(name, x, y, width=4, color=formatColor(0.5, 0.95, 0.5) ) + + if ghostbeliefs is not None: + self.drawDistributions(state.data, ghostbeliefs=ghostbeliefs) + + self.drawStaticObjects(state.data) + self.drawAgentObjects(state.data) + self.infoPane.updateScore(state.data.score, self.method) + + if 'ghostDistances' in dir(state.data): + self.infoPane.updateGhostDistances(state.data.ghostDistances) + self.infoPane.master_render() + # self.ga.gc.prune_frame() + # self.viewer.render() + + def blit(self, render_mode=None): + return self.ga.blit(render_mode=render_mode) + + + + def close(self): + self.ga.close() + + def startGraphics(self, state): + self.layout = state.data.layout + # layout = self.layout + self.width = self.layout.width + self.height = self.layout.height + self.make_window(self.width, self.height) + self.ga.draw_background() + self.infoPane = InfoPane(ga=self.ga, layout=self.layout, gridSize=self.gridSize) + self.currentState = self.layout # Unclear. + + def drawDistributions(self, state, ghostbeliefs=None): + ghostbeliefs = [gb.copy() for gb in ghostbeliefs] # uses a default dict. + if ghostbeliefs is None or len(ghostbeliefs) == 0: + return + walls = state.layout.walls + for x in range(walls.width): + for y in range(walls.height): + weights = [gb[(x,y)] for gb in ghostbeliefs] + color = [0.0, 0.0, 0.0] + colors = list(GHOST_VEC_COLORS)[1:] # With Pacman + if self.capture: colors = GHOST_VEC_COLORS + + for weight, gcolor in zip(weights, colors): + color = [min(1.0, c + 0.95 * g * weight ** .3) for c, g in zip(color, gcolor)] + color = formatColor(*color) + ( screen_x, screen_y ) = self.to_screen( (x, y) ) + self.ga.square(f"_belif_{x}_{y}_", (screen_x, screen_y), + 0.5 * self.gridSize, + color = color, # BACKGROUND_COLOR, + filled = 1, behind=2) + + def drawStaticObjects(self, state): + layout = self.layout + self.drawWalls(layout.walls) + self.food = self.drawFood(state.food) + self.capsules = self.drawCapsules(state.capsules) + + def drawAgentObjects(self, state): + self.agentImages = [] # (agentState, image) + for index, agent in enumerate(state.agentStates): + if agent.isPacman: + image = self.drawPacman(agent, index) + self.agentImages.append( (agent, image) ) + else: + image = self.drawGhost(agent, index) + self.agentImages.append( (agent, image) ) + + + def update(self, newState, animate=False, ghostbeliefs=None, path=None, visitedlist=None): + # newState = newState.data + agentIndex = newState.data._agentMoved + agentState = newState.data.agentStates[agentIndex] + # assert False + if self.agentImages[agentIndex][0].isPacman != agentState.isPacman: self.swapImages(agentIndex, agentState) + prevState, prevImage = self.agentImages[agentIndex] + if animate: + if agentState.isPacman: + self.animatePacman(agentState, prevState, prevImage, state=newState, ghostbeliefs=ghostbeliefs, path=path, visitedlist=visitedlist) + else: + self.moveGhost(agentState, agentIndex, prevState, prevImage) + + self.agentImages[agentIndex] = (agentState, prevImage) + + if newState.data._foodEaten != None: + self.removeFood(newState.data._foodEaten, self.food) + if newState.data._capsuleEaten != None: + self.removeCapsule(newState.data._capsuleEaten, self.capsules) + + if 'ghostDistances' in dir(newState): + self.infoPane.updateGhostDistances(newState.data.ghostDistances) + self.master_render(newState, ghostbeliefs=ghostbeliefs, path=path, visitedlist=visitedlist) + + def make_window(self, width, height): + grid_width = (width-1) * self.gridSize + grid_height = (height-1) * self.gridSize + screen_width = 2*self.gridSize + grid_width + screen_height = 2*self.gridSize + grid_height + INFO_PANE_HEIGHT + self.viewer = self.ga.begin_graphics(screen_width, screen_height, BACKGROUND_COLOR, "Pacman") + + def drawPacman(self, pacman, index): + position = self.getPosition(pacman) + d = pacman.draw_extra['delta_xy'] + position = (position[0] + d[0], position[1]+d[1]) + screen_point = self.to_screen(position) + + if 'endpoints' in pacman.draw_extra: + endpoints = pacman.draw_extra['endpoints'] + else: + endpoints = self.getEndpoints(self.getDirection(pacman)) + + width = PACMAN_OUTLINE_WIDTH + outlineColor = PACMAN_COLOR + fillColor = PACMAN_COLOR + + if self.capture: + outlineColor = TEAM_COLORS[index % 2] + fillColor = GHOST_COLORS[index] + width = PACMAN_CAPTURE_OUTLINE_WIDTH + + return [self.ga.circle("pacman", screen_point, PACMAN_SCALE * self.gridSize, + fillColor = fillColor, outlineColor = outlineColor, + endpoints = endpoints, + width = width)] + + def getEndpoints(self, direction, position=(0,0)): + x, y = position + pos = x - int(x) + y - int(y) + width = 30 + 80 * math.sin(math.pi* pos) + + delta = width / 2 + if (direction == 'West'): + endpoints = (180+delta, 180-delta) + elif (direction == 'North'): + endpoints = (90+delta, 90-delta) + elif (direction == 'South'): + endpoints = (270+delta, 270-delta) + else: + endpoints = (0+delta, 0-delta) + return endpoints + + def movePacman(self, position, direction, image,pacman): + # screenPosition = self.to_screen(position) + endpoints = self.getEndpoints( direction, position ) + # r = PACMAN_SCALE * self.gridSize + pacman.draw_extra['endpoints'] = endpoints + + def animatePacman(self, pacman, prevPacman, image, nframe=1, frames=4, state=None, ghostbeliefs=None, path=None, visitedlist=None): + if self.frameTime < 0: + print('Press any key to step forward, "q" to play') + if self.frameTime > 0.01 or self.frameTime < 0: + fx, fy = self.getPosition(prevPacman) + px, py = self.getPosition(pacman) + for nframe in range(1,int(frames) + 1): + pos = px*nframe/frames + fx*(frames-nframe)/frames, py*nframe/frames + fy*(frames-nframe)/frames + self.movePacman(pos, self.getDirection(pacman), image, pacman=pacman) + pacman.draw_extra['delta_xy'] = (pos[0]-px, pos[1]-py) + time.sleep(self.frameTime/frames) + self.master_render(state, ghostbeliefs=ghostbeliefs, path=path, visitedlist=visitedlist) + self.blit(render_mode='human') + else: + self.movePacman(self.getPosition(pacman), self.getDirection(pacman), image, pacman=pacman) + + + def getGhostColor(self, ghost, ghostIndex): + if ghost.scaredTimer > 0: + return SCARED_COLOR + else: + return GHOST_COLORS[ghostIndex] + + def drawGhost(self, ghost, agentIndex): + pos = self.getPosition(ghost) + dir = self.getDirection(ghost) + (screen_x, screen_y) = (self.to_screen(pos) ) + coords = [] + for (x, y) in GHOST_SHAPE: + coords.append((x*self.gridSize*GHOST_SIZE + screen_x, y*self.gridSize*GHOST_SIZE + screen_y)) + + colour = self.getGhostColor(ghost, agentIndex) + name = f"ghost_{agentIndex}_" + body = self.ga.polygon(name, coords, colour, filled = 1) + WHITE = formatColor(1.0, 1.0, 1.0) + BLACK = formatColor(0.0, 0.0, 0.0) + + dx = 0 + dy = 0 + if dir == 'North': + dy = -0.2 + if dir == 'South': + dy = 0.2 + if dir == 'East': + dx = 0.2 + if dir == 'West': + dx = -0.2 + leftEye = self.ga.circle(name +"_s1", (screen_x+self.gridSize*GHOST_SIZE*(-0.3+dx/1.5), screen_y-self.gridSize*GHOST_SIZE*(0.3-dy/1.5)), self.gridSize*GHOST_SIZE*0.2, WHITE, WHITE) + rightEye = self.ga.circle(name +"_s2",(screen_x+self.gridSize*GHOST_SIZE*(0.3+dx/1.5), screen_y-self.gridSize*GHOST_SIZE*(0.3-dy/1.5)), self.gridSize*GHOST_SIZE*0.2, WHITE, WHITE) + leftPupil = self.ga.circle(name +"_s3",(screen_x+self.gridSize*GHOST_SIZE*(-0.3+dx), screen_y-self.gridSize*GHOST_SIZE*(0.3-dy)), self.gridSize*GHOST_SIZE*0.08, BLACK, BLACK) + rightPupil = self.ga.circle(name +"_s4",(screen_x+self.gridSize*GHOST_SIZE*(0.3+dx), screen_y-self.gridSize*GHOST_SIZE*(0.3-dy)), self.gridSize*GHOST_SIZE*0.08, BLACK, BLACK) + ghostImageParts = [] + ghostImageParts.append(body) + ghostImageParts.append(leftEye) + ghostImageParts.append(rightEye) + ghostImageParts.append(leftPupil) + ghostImageParts.append(rightPupil) + return ghostImageParts + + def moveEyes(self, pos, dir, eyes): # does this do anything? + (screen_x, screen_y) = (self.to_screen(pos) ) + dx = 0 + dy = 0 + if dir == 'North': + dy = -0.2 + if dir == 'South': + dy = 0.2 + if dir == 'East': + dx = 0.2 + if dir == 'West': + dx = -0.2 + self.ga.moveCircle(eyes[0],(screen_x+self.gridSize*GHOST_SIZE*(-0.3+dx/1.5), screen_y-self.gridSize*GHOST_SIZE*(0.3-dy/1.5)), self.gridSize*GHOST_SIZE*0.2) + self.ga.moveCircle(eyes[1],(screen_x+self.gridSize*GHOST_SIZE*(0.3+dx/1.5), screen_y-self.gridSize*GHOST_SIZE*(0.3-dy/1.5)), self.gridSize*GHOST_SIZE*0.2) + self.ga.moveCircle(eyes[2],(screen_x+self.gridSize*GHOST_SIZE*(-0.3+dx), screen_y-self.gridSize*GHOST_SIZE*(0.3-dy)), self.gridSize*GHOST_SIZE*0.08) + self.ga.moveCircle(eyes[3],(screen_x+self.gridSize*GHOST_SIZE*(0.3+dx), screen_y-self.gridSize*GHOST_SIZE*(0.3-dy)), self.gridSize*GHOST_SIZE*0.08) + + def moveGhost(self, ghost, ghostIndex, prevGhost, ghostImageParts): + old_x, old_y = self.to_screen(self.getPosition(prevGhost)) + new_x, new_y = self.to_screen(self.getPosition(ghost)) + delta = new_x - old_x, new_y - old_y + + if ghost.scaredTimer > 0: + color = SCARED_COLOR + else: + color = GHOST_COLORS[ghostIndex] + self.ga.edit(ghostImageParts[0], ('fill', color), ('outline', color)) + self.moveEyes(self.getPosition(ghost), self.getDirection(ghost), ghostImageParts[-4:]) + + + def getPosition(self, agentState): + if agentState.configuration == None: return (-1000, -1000) + return agentState.getPosition() + + def getDirection(self, agentState): + if agentState.configuration == None: return Directions.STOP + return agentState.configuration.getDirection() + + def to_screen(self, point): + ( x, y ) = point + x = (x + 1)*self.gridSize + y = (self.height - y)*self.gridSize + return ( x, y ) + + # Fixes some TK issue with off-center circles + def to_screen2(self, point): + ( x, y ) = point + #y = self.height - y + x = (x + 1)*self.gridSize + y = (self.height - y)*self.gridSize + return ( x, y ) + + def drawWalls(self, wallMatrix): + wallColor = WALL_COLOR + + for xNum, x in enumerate(wallMatrix): + if self.capture and (xNum * 2) < wallMatrix.width: wallColor = TEAM_COLORS[0] + if self.capture and (xNum * 2) >= wallMatrix.width: wallColor = TEAM_COLORS[1] + + for yNum, cell in enumerate(x): + name = f"{xNum}_{yNum}_" + if cell: # There's a wall here + pos = (xNum, yNum) + screen = self.to_screen(pos) + screen2 = self.to_screen2(pos) + + # draw each quadrant of the square based on adjacent walls + wIsWall = self.isWall(xNum-1, yNum, wallMatrix) + eIsWall = self.isWall(xNum+1, yNum, wallMatrix) + nIsWall = self.isWall(xNum, yNum+1, wallMatrix) + sIsWall = self.isWall(xNum, yNum-1, wallMatrix) + nwIsWall = self.isWall(xNum-1, yNum+1, wallMatrix) + swIsWall = self.isWall(xNum-1, yNum-1, wallMatrix) + neIsWall = self.isWall(xNum+1, yNum+1, wallMatrix) + seIsWall = self.isWall(xNum+1, yNum-1, wallMatrix) + + # NE quadrant + if (not nIsWall) and (not eIsWall): + # inner circle + # self.ga.circle(name + "s1", screen2, WALL_RADIUS * self.gridSize, wallColor, wallColor, (0,91), 'arc') + self.ga.centered_arc(wallColor, screen2, WALL_RADIUS * self.gridSize, 0,90, width=2) + + if (nIsWall) and (not eIsWall): + # vertical line + self.ga.line(name + "s2", add(screen, (self.gridSize*WALL_RADIUS, 0)), add(screen, (self.gridSize*WALL_RADIUS, self.gridSize*(-0.5)-0)), wallColor) + if (not nIsWall) and (eIsWall): + # horizontal line + self.ga.line(name + "s3", add(screen, (0, self.gridSize*(-1)*WALL_RADIUS)), add(screen, (self.gridSize*0.5+0, self.gridSize*(-1)*WALL_RADIUS)), wallColor) + if (nIsWall) and (eIsWall) and (not neIsWall): + # outer circle + # self.ga.circle(name + "s4", add(screen2, (self.gridSize*2*WALL_RADIUS, self.gridSize*(-2)*WALL_RADIUS)), WALL_RADIUS * self.gridSize-1, wallColor, wallColor, (180,271), 'arc') + self.ga.centered_arc(wallColor, add(screen2, (self.gridSize * 2 * WALL_RADIUS, self.gridSize * (-2) * WALL_RADIUS)), WALL_RADIUS * self.gridSize- 0, 180, 270, width=2) + # centered_arc(self, color, pos, r, start_angle, stop_angle, width=1) + self.ga.line(name + "s5", add(screen, (self.gridSize*2*WALL_RADIUS-0, self.gridSize*(-1)*WALL_RADIUS)), add(screen, (self.gridSize*0.5+0, self.gridSize*(-1)*WALL_RADIUS)), wallColor) + self.ga.line(name + "s6", add(screen, (self.gridSize*WALL_RADIUS, self.gridSize*(-2)*WALL_RADIUS+0)), add(screen, (self.gridSize*WALL_RADIUS, self.gridSize*(-0.5))), wallColor) + + # NW quadrant + if (not nIsWall) and (not wIsWall): + # inner circle + # self.ga.circle(name + "s8", screen2, WALL_RADIUS * self.gridSize, wallColor, wallColor, (90,181), 'arc') + self.ga.centered_arc(wallColor, screen2, WALL_RADIUS * self.gridSize, 90,180, width=2) + + if (nIsWall) and (not wIsWall): + # vertical line + self.ga.line(name + "s10", add(screen, (self.gridSize*(-1)*WALL_RADIUS, 0)), add(screen, (self.gridSize*(-1)*WALL_RADIUS, self.gridSize*(-0.5)-0)), wallColor) + if (not nIsWall) and (wIsWall): + # horizontal line + self.ga.line(name + "s11", add(screen, (0, self.gridSize*(-1)*WALL_RADIUS)), add(screen, (self.gridSize*(-0.5)-0, self.gridSize*(-1)*WALL_RADIUS)), wallColor) + if (nIsWall) and (wIsWall) and (not nwIsWall): + # outer circle + # self.ga.circle(name + "s12", add(screen2, (self.gridSize*(-2)*WALL_RADIUS, self.gridSize*(-2)*WALL_RADIUS)), WALL_RADIUS * self.gridSize-1, wallColor, wallColor, (270,361), 'arc') + self.ga.centered_arc(wallColor, add(screen2, (self.gridSize*(-2)*WALL_RADIUS, self.gridSize*(-2)*WALL_RADIUS)), WALL_RADIUS * self.gridSize, 270,360, width=2) + + self.ga.line(name + "s13", add(screen, (self.gridSize*(-2)*WALL_RADIUS+0, self.gridSize*(-1)*WALL_RADIUS)), add(screen, (self.gridSize*(-0.5), self.gridSize*(-1)*WALL_RADIUS)), wallColor) + self.ga.line(name + "s14", add(screen, (self.gridSize*(-1)*WALL_RADIUS, self.gridSize*(-2)*WALL_RADIUS+1)), add(screen, (self.gridSize*(-1)*WALL_RADIUS, self.gridSize*(-0.5))), wallColor) + + # SE quadrant + if (not sIsWall) and (not eIsWall): + # inner circle + # self.ga.circle(name + "s18", screen2, WALL_RADIUS * self.gridSize, wallColor, wallColor, (270,361), 'arc') + self.ga.centered_arc(wallColor, screen2, WALL_RADIUS * self.gridSize, 270,360, width=2) + + if (sIsWall) and (not eIsWall): + # vertical line + self.ga.line(name + "s20", add(screen, (self.gridSize*WALL_RADIUS, 0)), add(screen, (self.gridSize*WALL_RADIUS, self.gridSize*(0.5)+0)), wallColor) + if (not sIsWall) and (eIsWall): + # horizontal line + self.ga.line(name + "s21", add(screen, (0, self.gridSize*(1)*WALL_RADIUS)), add(screen, (self.gridSize*0.5+1, self.gridSize*(1)*WALL_RADIUS)), wallColor) + if (sIsWall) and (eIsWall) and (not seIsWall): + # outer circle + # self.ga.circle(name + "s22", add(screen2, (self.gridSize*2*WALL_RADIUS, self.gridSize*(2)*WALL_RADIUS)), WALL_RADIUS * self.gridSize-1, wallColor, wallColor, (90,181), 'arc') + self.ga.centered_arc(wallColor, add(screen2, (self.gridSize*2*WALL_RADIUS, self.gridSize*(2)*WALL_RADIUS)), WALL_RADIUS * self.gridSize-0, 90,180, width=2) + self.ga.line(name + "s23", add(screen, (self.gridSize*2*WALL_RADIUS-0, self.gridSize*(1)*WALL_RADIUS)), add(screen, (self.gridSize*0.5, self.gridSize*(1)*WALL_RADIUS)), wallColor) + self.ga.line(name + "s24", add(screen, (self.gridSize*WALL_RADIUS, self.gridSize*(2)*WALL_RADIUS-0)), add(screen, (self.gridSize*WALL_RADIUS, self.gridSize*(0.5))), wallColor) + + # SW quadrant + if (not sIsWall) and (not wIsWall): + # inner circle + # self.ga.circle(name + "s30", screen2, WALL_RADIUS * self.gridSize, wallColor, wallColor, (180,271), 'arc') + self.ga.centered_arc(wallColor, screen2, WALL_RADIUS * self.gridSize, 180,270, width=2) + if (sIsWall) and (not wIsWall): + # vertical line + self.ga.line(name + "s31", add(screen, (self.gridSize*(-1)*WALL_RADIUS, 0)), add(screen, (self.gridSize*(-1)*WALL_RADIUS, self.gridSize*(0.5)+1)), wallColor) + if (not sIsWall) and (wIsWall): + # horizontal line + self.ga.line(name + "s32", add(screen, (0, self.gridSize*(1)*WALL_RADIUS)), add(screen, (self.gridSize*(-0.5)-0, self.gridSize*(1)*WALL_RADIUS)), wallColor) + if (sIsWall) and (wIsWall) and (not swIsWall): + # outer circle + # self.ga.circle(name + "s33", add(screen2, (self.gridSize*(-2)*WALL_RADIUS, self.gridSize*(2)*WALL_RADIUS)), WALL_RADIUS * self.gridSize-1, wallColor, wallColor, (0,91), 'arc') + self.ga.centered_arc(wallColor, add(screen2, (self.gridSize*(-2)*WALL_RADIUS, self.gridSize*(2)*WALL_RADIUS)), WALL_RADIUS * self.gridSize-0, 0, 90, width=2) + self.ga.line(name + "s34", add(screen, (self.gridSize*(-2)*WALL_RADIUS+0, self.gridSize*(1)*WALL_RADIUS)), add(screen, (self.gridSize*(-0.5), self.gridSize*(1)*WALL_RADIUS)), wallColor) + self.ga.line(name + "s35", add(screen, (self.gridSize*(-1)*WALL_RADIUS, self.gridSize*(2)*WALL_RADIUS-0)), add(screen, (self.gridSize*(-1)*WALL_RADIUS, self.gridSize*(0.5))), wallColor) + + def isWall(self, x, y, walls): + if x < 0 or y < 0: + return False + if x >= walls.width or y >= walls.height: + return False + return walls[x][y] + + def drawFood(self, foodMatrix ): + foodImages = [] + color = FOOD_COLOR + for xNum, x in enumerate(foodMatrix): + if self.capture and (xNum * 2) <= foodMatrix.width: color = TEAM_COLORS[0] + if self.capture and (xNum * 2) > foodMatrix.width: color = TEAM_COLORS[1] + imageRow = [] + foodImages.append(imageRow) + for yNum, cell in enumerate(x): + name = f"food_{xNum}_{yNum}_" + if cell: # There's food here + screen = self.to_screen((xNum, yNum )) + dot = self.ga.circle(name, screen, + FOOD_SIZE * self.gridSize, + outlineColor = color, fillColor = color, + width = 1) + imageRow.append(dot) + else: + imageRow.append(None) + return foodImages + + def drawCapsules(self, capsules ): + capsuleImages = {} + for capsule in capsules: + ( screen_x, screen_y ) = self.to_screen(capsule) + name = f"capsule_{screen_y}_{screen_x}_" + dot = self.ga.circle(name, (screen_x, screen_y), + CAPSULE_SIZE * self.gridSize, + outlineColor = CAPSULE_COLOR, + fillColor = CAPSULE_COLOR, + width = 1) + capsuleImages[capsule] = dot + return capsuleImages + + def removeFood(self, cell, foodImages ): + x, y = cell + + # remove_from_screen(foodImages[x][y]) + + def removeCapsule(self, cell, capsuleImages ): + x, y = cell + # remove_from_screen(capsuleImages[(x, y)]) + + def drawExpandedCells(self, cells): + """ + Draws an overlay of expanded grid positions for search agents + """ + n = float(len(cells)) + baseColor = [1.0, 0.0, 0.0] + self.clearExpandedCells() + self.expandedCells = [] + for k, cell in enumerate(cells): + screenPos = self.to_screen( cell) + cellColor = formatColor(*[(n-k) * c * .5 / n + .25 for c in baseColor]) + name = f"exp_cell_{screenPos}_" + block = self.ga.square(name, screenPos, + 0.5 * self.gridSize, + color = cellColor, + filled = 1, behind=2) + self.expandedCells.append(block) + # if self.frameTime < 0: + # refresh() + + def clearExpandedCells(self): + if 'expandedCells' in dir(self) and len(self.expandedCells) > 0: + for cell in self.expandedCells: + pass + +class FirstPersonPacmanGraphics(PacmanGraphics): + def __init__(self, state, zoom = 1.0, showGhosts = True, capture = False, frameTime=0, ghostbeliefs=None): + PacmanGraphics.__init__(self, state, zoom=zoom, frameTime=frameTime) + self.showGhosts = showGhosts + self.capture = capture + self.ghostbeliefs = ghostbeliefs + + + def initialize(self, state, isBlue = False): + self.isBlue = isBlue + PacmanGraphics.startGraphics(self, state) + self.layout = state.layout + self.previousState = state + + def lookAhead(self, config, state): + if config.getDirection() == 'Stop': + return + else: + pass + # Draw relevant ghosts + allGhosts = state.getGhostStates() + visibleGhosts = state.getVisibleGhosts() + for i, ghost in enumerate(allGhosts): + if ghost in visibleGhosts: + self.drawGhost(ghost, i) + else: + self.currentGhostImages[i] = None + + def getGhostColor(self, ghost, ghostIndex): + return GHOST_COLORS[ghostIndex] + + def getPosition(self, ghostState): + if not self.showGhosts and not ghostState.isPacman and ghostState.getPosition()[1] > 1: + return (-1000, -1000) + else: + return PacmanGraphics.getPosition(self, ghostState) + +def add(x, y): + return x[0] + y[0], x[1] + y[1] + +# 790 + +if __name__ == '__main__': + from irlc.pacman.pacman_environment import GymPacmanEnvironment + env = GymPacmanEnvironment(animate_movement=True, layout='mediumClassic', frame_time=0.0001) + # env = GymPacmanEnvironment(animate_movement=True, layout='smallClassic') + from irlc import VideoMonitor, train, Agent + env = VideoMonitor(env) + n = 100 + train(env, Agent(env), max_steps=n, num_episodes=1000) + # everything else: 0.20 (61 %), set up graphics: 0.03 (10 %), rendering: 0.09 (27 %) diff --git a/irlc/pacman/pacman_resources.py b/irlc/pacman/pacman_resources.py new file mode 100644 index 0000000000000000000000000000000000000000..6b5660e50a2701ea3d87761e69e78751a06cb578 --- /dev/null +++ b/irlc/pacman/pacman_resources.py @@ -0,0 +1,266 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import math +import numpy as np +import pygame +from PIL import ImageColor +# from pyglet.shapes import Circle, Rectangle, Polygon, Sector +# from irlc.utils.pyglet_rendering import GroupedElement +from irlc.pacman.pacman_graphics_display import GHOST_COLORS, GHOST_SHAPE + +WHITE = (255, 255, 255) +BLACK = (0, 0, 0) + + +# class Eye(GroupedElement): +# normal, cross = None, None +# +# def render(self): +# self.normal = [Circle(0, 0, .2, color=WHITE, batch=self.batch, group=self.group), +# Circle(0, 0, 0.1, color=BLACK, batch=self.batch, group=self.group)] # radius was 0.08 +# ew = 0.6 +# rw = ew/6 +# self.cross = [Rectangle(x=-ew/2, y=-rw/2, width=ew, height=rw, color=BLACK, group=self.group, batch=self.batch), +# Rectangle(x=-rw/2, y=-ew/2, width=rw, height=ew, color=BLACK, group=self.group, batch=self.batch)] +# self.set_eye_dir('stop') +# +# def set_eye_dir(self, direction='stop'): +# dead = direction.lower() == 'dead' +# for n in self.normal: +# n.visible = not dead +# pp = (0, 0) +# if direction.lower() == 'stop': +# pass +# dd = 0.1 +# if direction.lower() == 'east': +# pp = (dd, 0) +# # self.group.translate(dd, 0) +# +# if direction.lower() == 'west': +# pp = (-dd, 0) +# # self.group.translate(-dd, 0) +# if direction.lower() == 'south': +# pp = (0, -dd) +# # self.group.translate(0, -dd) +# if direction.lower() == 'north': +# # self.group.translate(0, dd) +# pp = (0, dd) +# self.normal[1].x = pp[0] +# self.normal[1].y = pp[1] +# +# for e in self.cross: +# e.visible = dead +# self.group.rotate(np.pi/4 if dead else 0) + +from irlc.utils.graphics_util_pygame import rotate_around + +class Ghost: + body_, eyes_ = None, None + def __init__(self, graphics_adaptor, agent_index=1, order=1, scale=10.): + self.agentIndex = agent_index + + # GS = [(x*scale, y*scale) for x,y in GHOST_SHAPE] + # self.GS = GS + # xx, yy = zip(*GS) + # xmin, xmax = min(xx), max(xx) + # ymin, ymax = min(yy), max(yy) + # this creates a surface + # self.GS = GS + # self.surf = pygame.Surface( (int(xmax-xmin), int(ymax-ymin)) ) + # Write ghost to this surface, then turn it to make it lie down. + self.ga = graphics_adaptor + # self.xmin = xmin + # self.ymin = ymin + # self.rect = self.surf.get_rect() + self.x = 0 + self.y = 0 + self.angle = 0 + self.scale = scale + + self.direction = 'stop' + # super().__init__(order=order) + + + def set_scared(self, scared): + return + from irlc.pacman.devel.pyglet_pacman_graphics import SCARED_COLOR, GHOST_COLORS + self.body_.color = SCARED_COLOR if scared else GHOST_COLORS[self.agentIndex] + + def eyes(self, direction): + return + for e in self.eyes_: + e.set_eye_dir(direction) + + def set_position(self, x, y): + # print("setting position", x,y) + # self.group.x = x + # self.group.y = y + # self.group.translate(x, y) + self.x = x + self.y = y + pass + + def rand_eyes(self): + return ['stop', 'east', 'west', 'north', 'south'][np.random.randint(0, 5)] + + + def set_direction(self, direction): + self.direction = direction + + return + self.eyes(direction) + + def kill(self): + self.set_direction('dead') + return + # return + # self.eyes('dead') + self.body_color = ImageColor.getcolor(GHOST_COLORS[3], "RGB") + # self.group.rotate(-np.pi/2) + + def resurrect(self): + self.set_direction(self.rand_eyes()) + # return + # self.eyes('straight') + return + self.body_.color = ImageColor.getcolor(GHOST_COLORS[self.agentIndex], "RGB") + self.group.rotate(0) + + def render(self): + # ghost_shape = tuple((x, -y) for (x, y) in GHOST_SHAPE) + dead = self.direction.lower() == 'dead' + angle = 0 + if dead: + angle = -90 + + ghost_shape = tuple((x*self.scale+self.x, -y*self.scale+self.y) for (x, y) in GHOST_SHAPE) + + # self.ga.polygon() + # print(ghost_shape) + xy0 = (self.x, self.y) + self.ga.polygon("asdfasf", [rotate_around(c, xy0, angle) for c in ghost_shape], GHOST_COLORS[self.agentIndex] if not dead else GHOST_COLORS[3], filled=1) + dx = 0.3 + dy = 0.3 + + # pdx = 0.2 + # pdy = 0.2 + + for k in range(2): + pos = (self.x + (-1 if k == 0 else 1)*dx*self.scale, self.y + dy*self.scale) + self.ga.circle("asdfsF", rotate_around(pos, xy0, angle), 0.15*self.scale, None, WHITE) + # Eyes: + # continue + + direction = self.direction + + + # for n in self.normal: + # n.visible = not dead + pp = (0, 0) + if direction.lower() == 'stop': + pass + dd = 0.1 + if direction.lower() == 'east': + pp = (dd, 0) + # self.group.translate(dd, 0) + if direction.lower() == 'west': + pp = (-dd, 0) + # self.group.translate(-dd, 0) + if direction.lower() == 'south': + pp = (0, -dd) + # self.group.translate(0, -dd) + if direction.lower() == 'north': + # self.group.translate(0, dd) + pp = (0, dd) + # self.normal[1].x = pp[0] + # self.normal[1].y = pp[1] + if not dead: + self.ga.circle("asdfsF", rotate_around( (pos[0] + pp[0]*self.scale, pos[1] + pp[1]*self.scale), xy0, self.angle), + 0.05 * self.scale, None, BLACK) + else: + ew = 0.6 + rw = ew / 6 + for k in range(2): + cross = [(-rw/2, ew/2), + (rw / 2, ew / 2), + (rw / 2, -ew / 2), + (-rw / 2, -ew / 2), + ] + cross = cross + [cross[0]] + cross = [rotate_around(c, (0,0), 45 + 90*k) for c in cross] + cc = [rotate_around( (pos[0]+x *self.scale+ pp[0], pos[1]+y *self.scale+ pp[1]), xy0, angle) for (x,y) in cross] + self.ga.polygon("asdfasf", cc, None, filled=True, fillColor=BLACK) + + + + + + + # self.cross = [ + # Rectangle(x=-ew / 2, y=-rw / 2, width=ew, height=rw, color=BLACK, group=self.group, batch=self.batch), + # Rectangle(x=-rw / 2, y=-ew / 2, width=rw, height=ew, color=BLACK, group=self.group, batch=self.batch)] + + + pass + # Circle(0, 0, .2, color=WHITE, batch=self.batch, group=self.group) + # + # self.normal = [Circle(0, 0, .2, color=WHITE, batch=self.batch, group=self.group), + # Circle(0, 0, 0.1, color=BLACK, batch=self.batch, group=self.group)] # radius was 0.08 + # ew = 0.6 + # rw = ew / 6 + # self.cross = [ + # Rectangle(x=-ew / 2, y=-rw / 2, width=ew, height=rw, color=BLACK, group=self.group, batch=self.batch), + # Rectangle(x=-rw / 2, y=-ew / 2, width=rw, height=ew, color=BLACK, group=self.group, batch=self.batch)] + # + # for e in self.cross: + # e.visible = dead + # return + # self.ga.polygon() + # colour = ImageColor.getcolor(GHOST_COLORS[self.agentIndex], "RGB") + # self.body_ = Polygon(*ghost_shape, color=colour, batch=self.batch, group=self.group) + # self.eyes_ = [Eye(order=self.group.order+1+k, pg=self.group, batch=self.batch) for k in range(2)] + # for k, e in enumerate(self.eyes_): + # e.group.translate(-.3 if k == 0 else .3, .3) + + +PACMAN_COLOR = (255, 255, 61) + + +# class Pacman(GroupedElement): +# body = None +# +# def __init__(self, grid_size, batch, pg=None, parent=None, order=0): +# self.delta = 0 +# self.GRID_SIZE = grid_size +# super().__init__(batch, pg=pg, parent=parent, order=order) +# self.set_animation(0, 4) +# +# def set_animation(self, frame, frames): +# pos = frame/frames +# width = 30 + 80 * math.sin(math.pi * pos) +# delta = width / 2 +# self.delta = delta * np.pi / 180 +# self.body._angle = 2*np.pi-2*self.delta +# self.body._start_angle = self.delta +# self.body._update_position() +# +# def set_direction(self, direction): +# if direction == 'Stop': +# pass +# else: +# angle = 0 +# if direction == 'East': +# angle = 0 +# elif direction == 'North': +# angle = np.pi/2 +# elif direction == 'West': +# angle = np.pi +# elif direction == 'South': +# angle = np.pi*1.5 +# self.group.rotate(angle) +# +# def render(self): +# width = 30 +# delta = width/2 +# delta = delta/180 * np.pi +# self.body = Sector(0, 0, self.GRID_SIZE/2, angle=2*np.pi-2*delta, start_angle=delta, +# color=PACMAN_COLOR, batch=self.batch, group=self.group) diff --git a/irlc/pacman/pacman_text_display.py b/irlc/pacman/pacman_text_display.py new file mode 100644 index 0000000000000000000000000000000000000000..72d4b107a57610227a7202e904808507bb9305c2 --- /dev/null +++ b/irlc/pacman/pacman_text_display.py @@ -0,0 +1,64 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# pacman_text_display.py +# -------------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). +import time + +DRAW_EVERY = 1 +SLEEP_TIME = 0 # This can be overwritten by __init__ +DISPLAY_MOVES = False +QUIET = False # Supresses output + +class PacmanTextDisplay: + def __init__(self, speed=None): + if speed != None: + global SLEEP_TIME + SLEEP_TIME = speed + + def initialize(self, state, isBlue = False): + self.draw(state) + self.pause() + self.turn = 0 + self.agentCounter = 0 + + def update(self, state): + numAgents = len(state.agentStates) + self.agentCounter = (self.agentCounter + 1) % numAgents + if self.agentCounter == 0: + self.turn += 1 + if DISPLAY_MOVES: + ghosts = [nearestPoint(state.getGhostPosition(i)) for i in range(1, numAgents)] + print("%4d) P: %-8s" % (self.turn, str(nearestPoint(state.getPacmanPosition()))),'| Score: %-5d' % state.score,'| Ghosts:', ghosts) + if self.turn % DRAW_EVERY == 0: + self.draw(state) + self.pause() + if state._win or state._lose: + self.draw(state) + + def pause(self): + time.sleep(SLEEP_TIME) + + def draw(self, state): + print(state) + + def finish(self): + pass + +def nearestPoint( pos ): + """ + Finds the nearest grid point to a position (discretizes). + """ + ( current_row, current_col ) = pos + + grid_row = int( current_row + 0.5 ) + grid_col = int( current_col + 0.5 ) + return ( grid_row, grid_col ) diff --git a/irlc/pacman/pacman_utils.py b/irlc/pacman/pacman_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..1c55dfb8791176d05e2d05f504a1ba112670e3ed --- /dev/null +++ b/irlc/pacman/pacman_utils.py @@ -0,0 +1,680 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# pacman_utils.py +# ------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). + + +# pacman_utils.py +# ------- +# Licensing Information: Please do not distribute or publish solutions to this +# project. You are free to use and extend these projects for educational +# purposes. The Pacman AI projects were developed at UC Berkeley, primarily by +# John DeNero (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# For more info, see http://inst.eecs.berkeley.edu/~cs188/sp09/pacman.html + +import traceback +import sys +from collections import defaultdict +import io +import numpy as np +# from irlc.berkley.util import manhattanDistance + + +class PacAgent: + """ + An agent must define a getAction method, but may also define the + following methods which will be called if they exist: + + def registerInitialState(self, state): # inspects the starting state + """ + def __init__(self, index=0): + self.index = index + + def getAction(self, state): + """ + The Agent will receive a GameState (from either {pacman, capture, sonar}.py) and + must return an action from Directions.{North, South, East, West, Stop} + """ + raise NotImplementedError() + +class Directions: + NORTH = 'North' + SOUTH = 'South' + EAST = 'East' + WEST = 'West' + STOP = 'Stop' + + LEFT = {NORTH: WEST, + SOUTH: EAST, + EAST: NORTH, + WEST: SOUTH, + STOP: STOP} + + RIGHT = dict([(y,x) for x, y in LEFT.items()]) + + REVERSE = {NORTH: SOUTH, + SOUTH: NORTH, + EAST: WEST, + WEST: EAST, + STOP: STOP} + +class Configuration: + """ + A Configuration holds the (x,y) coordinate of a character, along with its + traveling direction. + + The convention for positions, like a graph, is that (0,0) is the lower left corner, x increases + horizontally and y increases vertically. Therefore, north is the direction of increasing y, or (0,1). + """ + + def __init__(self, pos, direction): + self.pos = pos + self.direction = direction + + def getPosition(self): + return (self.pos) + + def getDirection(self): + return self.direction + + def isInteger(self): + x,y = self.pos + return x == int(x) and y == int(y) + + def __eq__(self, other): + if other == None: return False + return (self.pos == other.pos and self.direction == other.direction) + + def __hash__(self): + x = hash(self.pos) + y = hash(self.direction) + return hash(x + 13 * y) + + def __str__(self): + return "(x,y)="+str(self.pos)+", "+str(self.direction) + + def generateSuccessor(self, vector): + """ + Generates a new configuration reached by translating the current + configuration by the action vector. This is a low-level call and does + not attempt to respect the legality of the movement. + + Actions are movement vectors. + """ + x, y= self.pos + dx, dy = vector + direction = Actions.vectorToDirection(vector) + if direction == Directions.STOP: + direction = self.direction # There is no stop direction + return Configuration((x + dx, y+dy), direction) + +class AgentState: + """ + AgentStates hold the state of an agent (configuration, speed, scared, etc). + """ + + def __init__( self, startConfiguration, isPacman ): + self.start = startConfiguration + self.configuration = startConfiguration + self.isPacman = isPacman + self.scaredTimer = 0 + self.numCarrying = 0 + self.numReturned = 0 + # Tue + # self.draw_delta_xy = (0,0) + # for instance, pacman endpoints, mid-animation movement, etc. + self.draw_extra = {'delta_xy': (0,0)} + + + def __str__( self ): + if self.isPacman: + return "Pacman: " + str( self.configuration ) + else: + return "Ghost: " + str( self.configuration ) + + def __eq__( self, other ): + if other == None: + return False + return self.configuration == other.configuration and self.scaredTimer == other.scaredTimer + + def __hash__(self): + return hash(hash(self.configuration) + 13 * hash(self.scaredTimer)) + + def copy( self ): + state = AgentState( self.start, self.isPacman ) + state.configuration = self.configuration + state.scaredTimer = self.scaredTimer + state.numCarrying = self.numCarrying + state.numReturned = self.numReturned + return state + + def getPosition(self): + if self.configuration == None: return None + return self.configuration.getPosition() + + def getDirection(self): + return self.configuration.getDirection() + +class Grid: + """ + A 2-dimensional array of objects backed by a list of lists. Data is accessed + via grid[x][y] where (x,y) are positions on a Pacman map with x horizontal, + y vertical and the origin (0,0) in the bottom left corner. + + The __str__ method constructs an output that is oriented like a pacman board. + """ + def __init__(self, width, height, initialValue=False, bitRepresentation=None): + if initialValue not in [False, True]: + raise Exception('Grids can only contain booleans') + self.CELLS_PER_INT = 30 + + self.width = width + self.height = height + self.data = [[initialValue for y in range(height)] for x in range(width)] + if bitRepresentation: + self._unpackBits(bitRepresentation) + + def __getitem__(self, i): + return self.data[i] + + def __setitem__(self, key, item): + self.data[key] = item + + def __str__(self): + out = [[str(self.data[x][y])[0] for x in range(self.width)] for y in range(self.height)] + out.reverse() + return '\n'.join([''.join(x) for x in out]) + + def __eq__(self, other): + if other == None: return False + return self.data == other.data + + def __hash__(self): + # return hash(str(self)) + base = 1 + h = 0 + for l in self.data: + for i in l: + if i: + h += base + base *= 2 + return hash(h) + + def copy(self): + g = Grid(self.width, self.height) + g.data = [x[:] for x in self.data] + return g + + def deepCopy(self): + return self.copy() + + def shallowCopy(self): + g = Grid(self.width, self.height) + g.data = self.data + return g + + def count(self, item =True ): + return sum([x.count(item) for x in self.data]) + + def asList(self, key = True): + list = [] + for x in range(self.width): + for y in range(self.height): + if self[x][y] == key: list.append( (x,y) ) + return list + + def packBits(self): + """ + Returns an efficient int list representation + + (width, height, bitPackedInts...) + """ + bits = [self.width, self.height] + currentInt = 0 + for i in range(self.height * self.width): + bit = self.CELLS_PER_INT - (i % self.CELLS_PER_INT) - 1 + x, y = self._cellIndexToPosition(i) + if self[x][y]: + currentInt += 2 ** bit + if (i + 1) % self.CELLS_PER_INT == 0: + bits.append(currentInt) + currentInt = 0 + bits.append(currentInt) + return tuple(bits) + + def _cellIndexToPosition(self, index): + x = index / self.height + y = index % self.height + return x, y + + def _unpackBits(self, bits): + """ + Fills in data from a bit-level representation + """ + cell = 0 + for packed in bits: + for bit in self._unpackInt(packed, self.CELLS_PER_INT): + if cell == self.width * self.height: break + x, y = self._cellIndexToPosition(cell) + self[x][y] = bit + cell += 1 + + def _unpackInt(self, packed, size): + bools = [] + if packed < 0: raise ValueError("must be a positive integer") + for i in range(size): + n = 2 ** (self.CELLS_PER_INT - i - 1) + if packed >= n: + bools.append(True) + packed -= n + else: + bools.append(False) + return bools + +def reconstituteGrid(bitRep): + if type(bitRep) is not type((1,2)): + return bitRep + width, height = bitRep[:2] + return Grid(width, height, bitRepresentation= bitRep[2:]) + +#################################### +# Parts you shouldn't have to read # +#################################### + +class Actions: + """ + A collection of static methods for manipulating move actions. + """ + # Directions + _directions = {Directions.NORTH: (0, 1), + Directions.SOUTH: (0, -1), + Directions.EAST: (1, 0), + Directions.WEST: (-1, 0), + Directions.STOP: (0, 0)} + + _directionsAsList = _directions.items() + + TOLERANCE = .001 + + def reverseDirection(action): + if action == Directions.NORTH: + return Directions.SOUTH + if action == Directions.SOUTH: + return Directions.NORTH + if action == Directions.EAST: + return Directions.WEST + if action == Directions.WEST: + return Directions.EAST + return action + reverseDirection = staticmethod(reverseDirection) + + def vectorToDirection(vector): + dx, dy = vector + if dy > 0: + return Directions.NORTH + if dy < 0: + return Directions.SOUTH + if dx < 0: + return Directions.WEST + if dx > 0: + return Directions.EAST + return Directions.STOP + vectorToDirection = staticmethod(vectorToDirection) + + def directionToVector(direction, speed = 1.0): + # print(direction) + dx, dy = Actions._directions[direction] + # dx, dy = list(Actions._directions.values())[direction] + + return (dx * speed, dy * speed) + directionToVector = staticmethod(directionToVector) + + def getPossibleActions(config, walls): + possible = [] + x, y = config.pos + x_int, y_int = int(x + 0.5), int(y + 0.5) + + # In between grid points, all agents must continue straight + if (abs(x - x_int) + abs(y - y_int) > Actions.TOLERANCE): + return [config.getDirection()] + + for dir, vec in Actions._directionsAsList: + dx, dy = vec + next_y = y_int + dy + next_x = x_int + dx + if not walls[next_x][next_y]: possible.append(dir) + + return possible + + getPossibleActions = staticmethod(getPossibleActions) + + def getLegalNeighbors(position, walls): + x,y = position + x_int, y_int = int(x + 0.5), int(y + 0.5) + neighbors = [] + for dir, vec in Actions._directionsAsList: + dx, dy = vec + next_x = x_int + dx + if next_x < 0 or next_x == walls.width: continue + next_y = y_int + dy + if next_y < 0 or next_y == walls.height: continue + if not walls[next_x][next_y]: neighbors.append((next_x, next_y)) + return neighbors + getLegalNeighbors = staticmethod(getLegalNeighbors) + + def getSuccessor(position, action): + dx, dy = Actions.directionToVector(action) + x, y = position + return (x + dx, y + dy) + getSuccessor = staticmethod(getSuccessor) + +class GameStateData: + """ + + """ + def __init__( self, prevState = None ): + """ + Generates a new data packet by copying information from its predecessor. + """ + if prevState != None: + self.food = prevState.food.shallowCopy() + self.capsules = prevState.capsules[:] + self.agentStates = self.copyAgentStates( prevState.agentStates ) + self.layout = prevState.layout + self._eaten = prevState._eaten + self.score = prevState.score + + self._foodEaten = None + self._foodAdded = None + self._capsuleEaten = None + self._agentMoved = None + self._lose = False + self._win = False + self.scoreChange = 0 + + def deepCopy( self ): + state = GameStateData( self ) + state.food = self.food.deepCopy() + state.layout = self.layout.deepCopy() + state._agentMoved = self._agentMoved + state._foodEaten = self._foodEaten + state._foodAdded = self._foodAdded + state._capsuleEaten = self._capsuleEaten + + # Tue: I added these. I got no idea if this will screw things up. But why should they not be deep copied? + state._win = self._win + state._lose = self._lose + return state + + def copyAgentStates( self, agentStates ): + copiedStates = [] + for agentState in agentStates: + copiedStates.append( agentState.copy() ) + return copiedStates + + def __eq__( self, other ): + """ + Allows two states to be compared. + """ + if other == None: return False + # TODO Check for type of other + if not self.agentStates == other.agentStates: return False + if not self.food == other.food: return False + if not self.capsules == other.capsules: return False + # if not self.score == other.score: return False # This i am very unsure about. + return True + + def __hash__( self ): + """ + Allows states to be keys of dictionaries. + """ + for i, state in enumerate( self.agentStates ): + try: + int(hash(state)) + except TypeError as e: + print(e) + #hash(state) + return int((hash(tuple(self.agentStates)) + 13*hash(self.food) + 113* hash(tuple(self.capsules)) + 0 * hash(self.score)) % 1048575 ) + + def __str__( self ): + width, height = self.layout.width, self.layout.height + map = Grid(width, height) + if type(self.food) == type((1,2)): + self.food = reconstituteGrid(self.food) + for x in range(width): + for y in range(height): + food, walls = self.food, self.layout.walls + map[x][y] = self._foodWallStr(food[x][y], walls[x][y]) + + for agentState in self.agentStates: + if agentState == None: continue + if agentState.configuration == None: continue + x,y = [int( i ) for i in nearestPoint( agentState.configuration.pos )] + agent_dir = agentState.configuration.direction + if agentState.isPacman: + map[x][y] = self._pacStr( agent_dir ) + else: + map[x][y] = self._ghostStr( agent_dir ) + + for x, y in self.capsules: + map[x][y] = 'o' + + return str(map) + ("\nScore: %d\n" % self.score) + + # def str_no_score(self): # + # return "\n".join(str(self).splitlines()[:-1]) + + def _foodWallStr( self, hasFood, hasWall ): + if hasFood: + return '.' + elif hasWall: + return '%' + else: + return ' ' + + def _pacStr( self, dir ): + if dir == Directions.NORTH: + return 'v' + if dir == Directions.SOUTH: + return '^' + if dir == Directions.WEST: + return '>' + return '<' + + def _ghostStr( self, dir ): + return 'G' + if dir == Directions.NORTH: + return 'M' + if dir == Directions.SOUTH: + return 'W' + if dir == Directions.WEST: + return '3' + return 'E' + + def initialize( self, layout, numGhostAgents ): + """ + Creates an initial game state from a layout array (see layout.py). + """ + self.food = layout.food.copy() + #self.capsules = [] + self.capsules = layout.capsules[:] + self.layout = layout + self.score = 0 + self.scoreChange = 0 + + self.agentStates = [] + numGhosts = 0 + for isPacman, pos in layout.agentPositions: + if not isPacman: + if numGhosts == numGhostAgents: continue # Max ghosts reached already + else: numGhosts += 1 + self.agentStates.append( AgentState( Configuration( pos, Directions.STOP), isPacman) ) + self._eaten = [False for a in self.agentStates] + +try: + import boinc + _BOINC_ENABLED = True +except: + _BOINC_ENABLED = False + +class Game: + """ + The Game manages the control flow, soliciting actions from agents. + """ + + def __init__( self, agents, rules, display=None, startingIndex=0, muteAgents=False, catchExceptions=False ): + self.agentCrashed = False + self.agents = agents + # self.display = display + self.rules = rules + self.startingIndex = startingIndex + self.gameOver = False + self.muteAgents = muteAgents + self.catchExceptions = catchExceptions + self.moveHistory = [] + self.totalAgentTimes = [0 for agent in agents] + self.totalAgentTimeWarnings = [0 for agent in agents] + self.agentTimeout = False + # import cStringIO + + self.agentOutput = [io.StringIO() for agent in agents] + + def getProgress(self): + if self.gameOver: + return 1.0 + else: + return self.rules.getProgress(self) + + def _agentCrash( self, agentIndex, quiet=False): + "Helper method for handling agent crashes" + if not quiet: traceback.print_exc() + self.gameOver = True + self.agentCrashed = True + self.rules.agentCrash(self, agentIndex) + + OLD_STDOUT = None + OLD_STDERR = None + + def mute(self, agentIndex): + if not self.muteAgents: return + global OLD_STDOUT, OLD_STDERR + # import cStringIO + OLD_STDOUT = sys.stdout + OLD_STDERR = sys.stderr + sys.stdout = self.agentOutput[agentIndex] + sys.stderr = self.agentOutput[agentIndex] + + def unmute(self): + if not self.muteAgents: return + global OLD_STDOUT, OLD_STDERR + # Revert stdout/stderr to originals + sys.stdout = OLD_STDOUT + sys.stderr = OLD_STDERR + + +def nearestPoint( pos ): + """ + Finds the nearest grid point to a position (discretizes). + """ + ( current_row, current_col ) = pos + grid_row = int( current_row + 0.5 ) + grid_col = int( current_col + 0.5 ) + return ( grid_row, grid_col ) + + +def chooseFromDistribution( distribution ): + "Takes either a counter or a list of (prob, key) pairs and samples" + # k, v = zip( distribution.items() ) + k, v = zip(*distribution.items()) + sel = np.random.choice( list(k), 1, replace=True, p=list(v) ) + return sel[0] + + +class GhostAgent( PacAgent ): + # def __init__( self, index ): + # self.index = index + + def getAction( self, state ): + dist = self.getDistribution(state) + if len(dist) == 0: + return Directions.STOP + else: + return chooseFromDistribution(dist) + # return util.chooseFromDistribution( dist ) + + def getDistribution(self, state): + "Returns a Counter encoding a distribution over actions from the provided state." + raise NotImplementedError() + # util.raiseNotDefined() + + +class RandomGhost( GhostAgent ): + "A ghost that chooses a legal action uniformly at random." + def getDistribution( self, state ): + # dist = util.Counter() + dist = {} + for a in state.getLegalActions( self.index ): + dist[a] = 1.0 + sm = sum(dist.values()) + for a in dist: + dist[a] = dist[a]/sm + + # dist.normalize() + return dist + + +class DirectionalGhost( GhostAgent ): + "A ghost that prefers to rush Pacman, or flee when scared." + def __init__( self, index, prob_attack=0.8, prob_scaredFlee=0.8 ): + self.index = index + self.prob_attack = prob_attack + self.prob_scaredFlee = prob_scaredFlee + + def getDistribution( self, state ): + # Read variables from state + ghostState = state.getGhostState( self.index ) + legalActions = state.getLegalActions( self.index ) + pos = state.getGhostPosition( self.index ) + isScared = ghostState.scaredTimer > 0 + + speed = 1 + if isScared: speed = 0.5 + + actionVectors = [Actions.directionToVector( a, speed ) for a in legalActions] + newPositions = [( pos[0]+a[0], pos[1]+a[1] ) for a in actionVectors] + pacmanPosition = state.getPacmanPosition() + + # Select best actions given the state + distancesToPacman = [manhattanDistance( pos, pacmanPosition ) for pos in newPositions] + if isScared: + bestScore = max( distancesToPacman ) + bestProb = self.prob_scaredFlee + else: + bestScore = min( distancesToPacman ) + bestProb = self.prob_attack + bestActions = [action for action, distance in zip( legalActions, distancesToPacman ) if distance == bestScore] + + # Construct distribution + # dist = util.Counter() + + + dist = defaultdict(lambda: 0) + + for a in bestActions: dist[a] = bestProb / len(bestActions) + for a in legalActions: dist[a] += ( 1-bestProb ) / len(legalActions) + + sm = sum(dist.values()) + for k, v in dist.items(): + dist[k] = v /sm + # dist = {k: v/sm for k, v in dist.items() } + # dist.normalize() + return dist diff --git a/irlc/project0/__init__.py b/irlc/project0/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/project0/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/project0/__pycache__/__init__.cpython-311.pyc b/irlc/project0/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cc28450a2987c5790bd8ac7792fb3e66f490d636 Binary files /dev/null and b/irlc/project0/__pycache__/__init__.cpython-311.pyc differ diff --git a/irlc/project0/fruit_project_grade.py b/irlc/project0/fruit_project_grade.py new file mode 100644 index 0000000000000000000000000000000000000000..1207c3a31a045469dacb5ee27b1a91188c6ffc79 --- /dev/null +++ b/irlc/project0/fruit_project_grade.py @@ -0,0 +1,4 @@ +# irlc/project0/fruit_project_tests.py +''' WARNING: Modifying, decompiling or otherwise tampering with this script, it's data or the resulting .token file will be investigated as a cheating attempt. ''' +import bz2, base64 +exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file diff --git a/irlc/project0/fruit_project_tests.py b/irlc/project0/fruit_project_tests.py new file mode 100644 index 0000000000000000000000000000000000000000..331949cd0b9b4a335152864137d7ade2295a5d9e --- /dev/null +++ b/irlc/project0/fruit_project_tests.py @@ -0,0 +1,121 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from unitgrade import UTestCase, Report +from irlc.ex00.fruit_homework import add, misterfy, mean_value, fruits_ordered, BasicFruitShop, OnlineFruitShop, shop_smart +from unitgrade import hide + +class AdditionQuestion(UTestCase): + """ Problem 1: Adding two numbers """ + def test_add(self): + """ Adding two numbers together """ + self.assertEqual(add(2, 3), 5) # Test the add-function. + self.assertEqual(add(2, -917), -915) # Test the add-function. + + + +class MisterfyQuestion(UTestCase): + """ Problem 2: Misterfy a list """ + def test_misterfy(self): + """ Add 'mr' in front of each item in a string """ + self.assertEqualC(misterfy(['dog', 'cat', 'lion'])) + self.assertEqualC(misterfy(['giraffe'])) + self.assertEqualC(misterfy([])) + + + +class MeanOfDie(UTestCase): + """ Problem 3: Mean of die """ + def test_mean_value(self): + """ Compute mean of two dice """ + p_die = {1: 0.20, + 2: 0.10, + 3: 0.15, + 4: 0.05, + 5: 0.10, + 6: 0.40} + self.assertL2(mean_value(p_die), tol=0.0001) + self.assertL2(mean_value({-1: 0.5, 1: 0.5}), tol=0.0001) + + + +class FruitsOrdered(UTestCase): + """ Problem 4: The fruits_ordered function """ + def test_fruits_ordered(self): + """ fruits_ordered """ + order = {'apples': 1.0, + 'oranges': 3.0} + self.assertEqualC(list(sorted(fruits_ordered(order)))) + order2 = {'banana': 4, + 'apples': 1.0, + 'oranges': 3.0, + 'pears': 4} + self.assertEqualC(list(sorted(fruits_ordered(order2)))) + + +class BasicClass(UTestCase): + """ Problem 5: The BasicFruitShop """ + def test_cost(self): + """ Testing cost function """ + price1 = {"apple": 4, "pear": 8, 'orange': 10} + shop1 = BasicFruitShop("Alis Funky Fruits", price1) + self.assertEqualC(shop1.cost("apple")) + self.assertEqualC(shop1.cost("pear")) + + price2 = {'banana': 9, "apple": 5, "pear": 7, 'orange': 11} + shop2 = BasicFruitShop("Hansen Fruit Emporium", price2) + self.assertEqualC(shop2.cost("orange")) + self.assertEqualC(shop2.cost("banana")) + + +class Inheritance(UTestCase): + title = "Problem 6: Inheritance" + + def test_price_of_order(self): + """ Testing the price_of_order function """ + price_of_fruits = {'apples': 2, 'oranges': 1, 'pears': 1.5, 'mellon': 10, 'banana': 1.5} + shopA = OnlineFruitShop('shopA', price_of_fruits) + + order1 = {'apples': 1.0, + 'oranges': 3.0} + self.assertL2(shopA.price_of_order(order1), tol=1e-8) + order2 = {'banana': 4, + 'apples': 1.0, + 'oranges': 3.0, + 'pears': 4} + self.assertL2(shopA.price_of_order(order2), tol=1e-8) + + +class ClassUse(UTestCase): + title = "Problem 7: Using classes" + + def test_shop_smarter(self): + """ Testing the shop_smarter function """ + price_of_fruits = {'apples': 2, 'oranges': 1, 'pears': 1.5, 'mellon': 10} + shopA = OnlineFruitShop('shopA', price_of_fruits) + shopB = OnlineFruitShop('shopB', {'apples': 1.0, 'oranges': 5.0}) + + shops = [shopA, shopB] + order = {'apples': 1.0, + 'oranges': 3.0} + self.assertEqualC(shop_smart(order, shops).name) + order = {'apples': 3.0} # test with a new order. + self.assertEqualC(shop_smart(order, shops).name) + + +class FruitReport(Report): + title = "Fruit example report" + abbreviate_questions = True + questions = [(AdditionQuestion, 10), + (MisterfyQuestion, 10), + (MeanOfDie, 10), + (FruitsOrdered, 10), + (BasicClass, 10), + (Inheritance, 10), + (ClassUse, 10)] + + import irlc + pack_imports = [irlc] + + +if __name__ == "__main__": + from unitgrade import evaluate_report_student + evaluate_report_student(FruitReport()) diff --git a/irlc/project0/fruit_project_tests_complete_grade.py b/irlc/project0/fruit_project_tests_complete_grade.py new file mode 100644 index 0000000000000000000000000000000000000000..b76bbb32817ed635196d11010acc26ce20832851 --- /dev/null +++ b/irlc/project0/fruit_project_tests_complete_grade.py @@ -0,0 +1,4 @@ +# irlc/project0/fruit_project_tests_complete.py +''' WARNING: Modifying, decompiling or otherwise tampering with this script, it's data or the resulting .token file will be investigated as a cheating attempt. ''' +import bz2, base64 +exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file diff --git a/irlc/project0/unitgrade_data/AdditionQuestion.pkl b/irlc/project0/unitgrade_data/AdditionQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..11c0af9d431d3f61b3f2af0fba319cf8b8bb1958 Binary files /dev/null and b/irlc/project0/unitgrade_data/AdditionQuestion.pkl differ diff --git a/irlc/project0/unitgrade_data/BasicClass.pkl b/irlc/project0/unitgrade_data/BasicClass.pkl new file mode 100644 index 0000000000000000000000000000000000000000..110aa845e0142c58c171373055b1d656633a26d1 Binary files /dev/null and b/irlc/project0/unitgrade_data/BasicClass.pkl differ diff --git a/irlc/project0/unitgrade_data/ClassUse.pkl b/irlc/project0/unitgrade_data/ClassUse.pkl new file mode 100644 index 0000000000000000000000000000000000000000..25c6e361e111f36205adfa0ef92620470f6a0198 Binary files /dev/null and b/irlc/project0/unitgrade_data/ClassUse.pkl differ diff --git a/irlc/project0/unitgrade_data/FruitsOrdered.pkl b/irlc/project0/unitgrade_data/FruitsOrdered.pkl new file mode 100644 index 0000000000000000000000000000000000000000..b55dba6dd8f3b84b5a4bdd0a9f85ba60a0ce7f29 Binary files /dev/null and b/irlc/project0/unitgrade_data/FruitsOrdered.pkl differ diff --git a/irlc/project0/unitgrade_data/Inheritance.pkl b/irlc/project0/unitgrade_data/Inheritance.pkl new file mode 100644 index 0000000000000000000000000000000000000000..32072c814e584f70b67d9d0895c4d07be7286c27 Binary files /dev/null and b/irlc/project0/unitgrade_data/Inheritance.pkl differ diff --git a/irlc/project0/unitgrade_data/MeanOfDie.pkl b/irlc/project0/unitgrade_data/MeanOfDie.pkl new file mode 100644 index 0000000000000000000000000000000000000000..27877f6a8e70ffb0d0ad3cac120b703d81d980fd Binary files /dev/null and b/irlc/project0/unitgrade_data/MeanOfDie.pkl differ diff --git a/irlc/project0/unitgrade_data/MisterfyQuestion.pkl b/irlc/project0/unitgrade_data/MisterfyQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..2359530f1174d72bd344f9add58dd05e577704fb Binary files /dev/null and b/irlc/project0/unitgrade_data/MisterfyQuestion.pkl differ diff --git a/irlc/project1/Latex/02465project1_handin.tex b/irlc/project1/Latex/02465project1_handin.tex new file mode 100644 index 0000000000000000000000000000000000000000..f59e1d27e2cf427513a83618a9f3df9d071bd70b --- /dev/null +++ b/irlc/project1/Latex/02465project1_handin.tex @@ -0,0 +1,107 @@ +\documentclass[12pt,twoside]{article} +%\usepackage[table]{xcolor} % important to avoid options clash. +%\input{02465shared_preamble} +%\usepackage{cleveref} +\usepackage{url} +\usepackage{graphics} +\usepackage{multicol} +\usepackage{rotate} +\usepackage{rotating} +\usepackage{booktabs} +\usepackage{hyperref} +\usepackage{pifont} +\usepackage{latexsym} +\usepackage[english]{babel} +\usepackage{epstopdf} +\usepackage{etoolbox} +\usepackage{amsmath} +\usepackage{amssymb} +\usepackage{multirow,epstopdf} +\usepackage{fancyhdr} +\usepackage{booktabs} +\usepackage{xcolor} +\newcommand\redt[1]{ {\textcolor[rgb]{0.60, 0.00, 0.00}{\textbf{ #1} } } } + + +\newcommand{\m}[1]{\boldsymbol{ #1}} +\newcommand{\yoursolution}{ \redt{(your solution here) } } + + + +\title{ Report 1 hand-in } +\date{ \today } +\author{Alice (\texttt{s000001})\and Bob (\texttt{s000002})\and Clara (\texttt{s000003}) } + +\begin{document} +\maketitle + +\begin{table}[ht!] +\caption{Attribution table. Feel free to add/remove rows and columns} +\begin{tabular}{llll} +\toprule + & Alice & Bob & Clara \\ +\midrule + 1: A basic blaster-business & 0-100\% & 0-100\% & 0-100\% \\ + 2: Warmup & 0-100\% & 0-100\% & 0-100\% \\ + 3: Manually computing $J_{N-1}$ & 0-100\% & 0-100\% & 0-100\% \\ + 4: Compute optimal policy and value function & 0-100\% & 0-100\% & 0-100\% \\ + 5: Kiosk2 & 0-100\% & 0-100\% & 0-100\% \\ + 6: Explaining the policy & 0-100\% & 0-100\% & 0-100\% \\ + 7: Policy explanation continued & 0-100\% & 0-100\% & 0-100\% \\ + 8: Go east & 0-100\% & 0-100\% & 0-100\% \\ + 9: Describe the go-east problem & 0-100\% & 0-100\% & 0-100\% \\ + 10: Predict consequence of actions & 0-100\% & 0-100\% & 0-100\% \\ + 11: Possible future states & 0-100\% & 0-100\% & 0-100\% \\ + 12: Shortest path & 0-100\% & 0-100\% & 0-100\% \\ + 13: Predict consequence of actions with one ghost & 0-100\% & 0-100\% & 0-100\% \\ + 14: Possible future states with one ghost & 0-100\% & 0-100\% & 0-100\% \\ + 15: Optimal one-ghost planning & 0-100\% & 0-100\% & 0-100\% \\ + 16: Predict consequence of actions with several ghosts & 0-100\% & 0-100\% & 0-100\% \\ + 17: Future states & 0-100\% & 0-100\% & 0-100\% \\ + 18: Optimal planning & 0-100\% & 0-100\% & 0-100\% \\ +\bottomrule +\end{tabular} +\end{table} + +%\paragraph{Statement about collaboration:} +%Please edit this section to reflect how you have used external resources. The following statement will in most cases suffice: +%\emph{The code in the irls/project1 directory is entirely} + +%\paragraph{Main report:} +Headings have been inserted in the document for readability. You only have to edit the part which says \yoursolution. + +\section{The kiosk (\texttt{kiosk.py})} +\subsubsection*{{\color{red}Problem 1: A basic blaster-business}} + +\yoursolution +\redt{To get you started: \begin{align} + N & = 14 \\ + \mbox{for $k=0,\dots,N$: }\quad \mathcal{S}_k & = \dots \\ + \mbox{for $k=0,\dots,N-1$: }\quad \mathcal{A}_k(x_k) & = \dots \\ + & \vdots +\end{align} } + +\subsubsection*{{\color{red}Problem 3: Manually computing $J_{N-1}$}} + + \yoursolution + $$ + J_{N-1}(20) = ... + $$ + +\subsubsection*{{\color{red}Problem 6: Explaining the policy}} + + The first policy... this can be explained by noting ... \yoursolution + +\subsubsection*{{\color{red}Problem 7: Policy explanation continued}} + + $$\mu_{N-1}(0) = ...$$ +\yoursolution + +\section{Avoid the droid (\texttt{pacman.py)}} +\subsubsection*{{\color{red}Problem 9: Describe the go-east problem}} + + The environment is an example of a .... \\ + The controller is an example of a ... + \yoursolution + +\end{document} \ No newline at end of file diff --git a/irlc/project1/Latex/figures/kiosk1.pdf b/irlc/project1/Latex/figures/kiosk1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..54c179fa1703c83e77398a3f6382d3e685fc8fd9 Binary files /dev/null and b/irlc/project1/Latex/figures/kiosk1.pdf differ diff --git a/irlc/project1/Latex/figures/kiosk2.pdf b/irlc/project1/Latex/figures/kiosk2.pdf new file mode 100644 index 0000000000000000000000000000000000000000..07dd964485a357336d64c2393ce3fc97c8af1e14 Binary files /dev/null and b/irlc/project1/Latex/figures/kiosk2.pdf differ diff --git a/irlc/project1/Latex/figures/your_answer.pdf b/irlc/project1/Latex/figures/your_answer.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d8c092974e20aaaf1165958a53bdce3a2ebdbf8f Binary files /dev/null and b/irlc/project1/Latex/figures/your_answer.pdf differ diff --git a/irlc/project1/__init__.py b/irlc/project1/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/project1/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/project1/kiosk.py b/irlc/project1/kiosk.py new file mode 100644 index 0000000000000000000000000000000000000000..70f33719ab70e782558588bd3d336e4d112177fc --- /dev/null +++ b/irlc/project1/kiosk.py @@ -0,0 +1,70 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" +This project resembles the Inventory-control problem discussed in (Her24, Subsection 5.1.2) but with more complicated rules. +If you are stuck, the inventory-control problem will be a good place to start. + +I recommend to use the DP_stochastic function (as we did with the inventory-control example). This means +your main problem is to build appropriate DPModel-classes to represent the different problems. + +References: + [Her24] Tue Herlau. Sequential decision making. (Freely available online), 2024. +""" +from irlc.ex02.dp_model import DPModel +from irlc.ex02.dp import DP_stochastic +import matplotlib.pyplot as plt +from scipy.stats import binom +from irlc import savepdf +import numpy as np + +def plot_policy(pi, title, pdf): + """ Helper function to plot the policy functions pi, as generated by the DP_stochastic function. This function + can be used to visualize which actions are taken in which state (y-axis) at which time step (x-axis). """ + N = len(pi) + W = max(pi[0].keys()) + A = np.zeros((W, N)) + for i in range(W): + for j in range(N): + A[i, j] = pi[j][i] + plt.imshow(A) + plt.title(title) + savepdf(pdf) + plt.show() + +# TODO: 51 lines missing. +raise NotImplementedError("Insert your solution and remove this error.") + +def warmup_states(): + # TODO: 1 lines missing. + raise NotImplementedError("return state set") + +def warmup_actions(): + # TODO: 1 lines missing. + raise NotImplementedError("return action set") + +def solve_kiosk_1(): + # TODO: 1 lines missing. + raise NotImplementedError("Return cost and policy here (same format as DP_stochastic)") + +def solve_kiosk_2(): + # TODO: 1 lines missing. + raise NotImplementedError("Return cost and policy here (same format as DP_stochastic)") + + +def main(): + # Problem 14 + print("Available states S_0:", warmup_states()) + print("Available actions A_0(x_0):", warmup_actions()) + + J, pi = solve_kiosk_1() # Problem 16 + print("Kiosk1: Expected profits: ", -J[0][0], " imperial credits") + plot_policy(pi, "Kiosk1", "Latex/figures/kiosk1") + plt.show() + + J, pi = solve_kiosk_2() # Problem 17 + print("Kiosk 2: Expected profits: ", -J[0][0], " imperial credits") + plot_policy(pi, "Kiosk2", "Latex/figures/kiosk2") + plt.show() + + +if __name__ == "__main__": + main() diff --git a/irlc/project1/pacman.py b/irlc/project1/pacman.py new file mode 100644 index 0000000000000000000000000000000000000000..6ad08ecd50c9ce887f4e572d59e72cb21693d9b7 --- /dev/null +++ b/irlc/project1/pacman.py @@ -0,0 +1,169 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from collections import defaultdict +from irlc import train +from irlc.ex02.dp_model import DPModel +from irlc.ex02.dp import DP_stochastic +from irlc.ex02.dp_agent import DynamicalProgrammingAgent +from irlc.pacman.pacman_environment import PacmanEnvironment +from irlc.pacman.gamestate import GameState + +east = """ +%%%%%%%% +% P .% +%%%%%%%% """ + +east2 = """ +%%%%%%%% +% P.% +%%%%%%%% """ + +SS2tiny = """ +%%%%%% +%.P % +% GG.% +%%%%%% +""" + +SS0tiny = """ +%%%%%% +%.P % +% .% +%%%%%% +""" + +SS1tiny = """ +%%%%%% +%.P % +% G.% +%%%%%% +""" + +datadiscs = """ +%%%%%%% +% .% +%.P%% % +%. .% +%%%%%%% +""" + +# TODO: 30 lines missing. +raise NotImplementedError("Put your own code here") + +def p_next(x : GameState, u: str): + """ Given the agent is in GameState x and takes action u, the game will transition to a new state xp. + The state xp will be random when there are ghosts. This function should return a dictionary of the form + + {..., xp: p, ...} + + of all possible next states xp and their probability -- you need to compute this probability. + + Hints: + * In the above, xp should be a GameState, and p will be a float. These are generated using the functions in the GameState x. + * Start simple (zero ghosts). Then make it work with one ghosts, and then finally with any number of ghosts. + * Remember the ghosts move at random. I.e. if a ghost has 3 available actions, it will choose one with probability 1/3 + * The slightly tricky part is that when there are multiple ghosts, different actions by the individual ghosts may lead to the same final state + * Check the probabilities sum to 1. This will be your main way of debugging your code and catching issues relating to the previous point. + """ + # TODO: 8 lines missing. + raise NotImplementedError("Return a dictionary {.., xp: p, ..} where xp is a possible next state and p the probability") + return states + + +def go_east(map): + """ Given a map-string map (see examples in the top of this file) that can be solved by only going east, this will return + a list of states Pacman will traverse. The list it returns should therefore be of the form: + + [s0, s1, s2, ..., sn] + + where each sk is a GameState object, the first element s0 is the start-configuration (corresponding to that in the Map), + and the last configuration sn is a won GameState obtained by going east. + + Note this function should work independently of the number of required east-actions. + + Hints: + * Use the GymPacmanEnvironment class. The report description will contain information about how to set it up, as will pacman_demo.py + * Use this environment to get the first GameState, then use the recommended functions to go east + """ + # TODO: 5 lines missing. + raise NotImplementedError("Return the list of states pacman will traverse if he goes east until he wins the map") + return states + +def get_future_states(x, N): + # TODO: 4 lines missing. + raise NotImplementedError("return a list-of-list of future states [S_0,\dots,S_N]. Each S_k is a state space, i.e. a list of GameState objects.") + return state_spaces + +def win_probability(map, N=10): + """ Assuming you get a reward of -1 on wining (and otherwise zero), the win probability is -J_pi(x_0). """ + # TODO: 5 lines missing. + raise NotImplementedError("Return the chance of winning the given map within N steps or less.") + return win_probability + +def shortest_path(map, N=10): + """ If each move has a cost of 1, the shortest path is the path with the lowest cost. + The actions should be the list of actions taken. + The states should be a list of states the agent visit. The first should be the initial state and the last + should be the won state. """ + # TODO: 4 lines missing. + raise NotImplementedError("Return the cost of the shortest path, the list of actions taken, and the list of states.") + return actions, states + + +def no_ghosts(): + # Check the pacman_demo.py file for help on the GameState class and how to get started. + # This function contains examples of calling your functions. However, you should use unitgrade to verify correctness. + + ## Problem 1: Lets try to go East. Run this code to see if the states you return looks sensible. + states = go_east(east) + for s in states: + print(str(s)) + + ## Problem 3: try the p_next function for a few empty environments. Does the result look sensible? + x, _ = PacmanEnvironment(layout_str=east).reset() + action = x.A()[0] + print(f"Transitions when taking action {action} in map: 'east'") + print(x) + print(p_next(x, action)) # use str(state) to get a nicer representation. + + print(f"Transitions when taking action {action} in map: 'east2'") + x, _ = PacmanEnvironment(layout_str=east2).reset() + print(x) + print(p_next(x, action)) + + ## Problem 4 + print(f"Checking states space S_1 for k=1 in SS0tiny:") + x, _ = PacmanEnvironment(layout_str=SS0tiny).reset() + states = get_future_states(x, N=10) + for s in states[1]: # Print all elements in S_1. + print(s) + print("States at time k=10, |S_10| =", len(states[10])) + + ## Problem 6 + N = 20 # Planning horizon + action, states = shortest_path(east, N) + print("east: Optimal action sequence:", action) + + action, states = shortest_path(datadiscs, N) + print("datadiscs: Optimal action sequence:", action) + + action, states = shortest_path(SS0tiny, N) + print("SS0tiny: Optimal action sequence:", action) + + +def one_ghost(): + # Win probability when planning using a single ghost. Notice this tends to increase with planning depth + wp = [] + for n in range(10): + wp.append(win_probability(SS1tiny, N=n)) + print(wp) + print("One ghost:", win_probability(SS1tiny, N=12)) + + +def two_ghosts(): + # Win probability when planning using two ghosts + print("Two ghosts:", win_probability(SS2tiny, N=12)) + +if __name__ == "__main__": + no_ghosts() + one_ghost() + two_ghosts() diff --git a/irlc/project1/pacman_demo1.py b/irlc/project1/pacman_demo1.py new file mode 100644 index 0000000000000000000000000000000000000000..bf74e07b095507c77acd59521cd892a40b11d1ac --- /dev/null +++ b/irlc/project1/pacman_demo1.py @@ -0,0 +1,53 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.pacman.pacman_environment import PacmanEnvironment +from irlc.project1.pacman import east, datadiscs, SS1tiny, SS2tiny +from irlc import interactive, savepdf, Agent, train +import matplotlib +matplotlib.use('qtagg') + +count = """ +%%%% +%P % +%..% +%%%% +""" + + +if __name__ == "__main__": + # Example interaction with an environment: + # Instantiate the map 'east' and get a GameState instance: + env = PacmanEnvironment(layout_str=east, render_mode='human') + x, info = env.reset() # x is a irlc.pacman.gamestate.GameState object. See the online documentation for more examples. + print("Start configuration of board:") + print(x) + env.close() # If you use render_mode = 'human', I recommend you use env.close() at the end of the code to free up graphics resources. + # The GameState object `x` has a handful of useful functions. The important ones are: + # x.A() # Action space + # x.f(action) # State resulting in taking action 'action' in state 'x' + # x.players() # Number of agents on board (at least 1) + # x.player() # Whose turn it is (player = 0 is us) + # x.is_won() # True if we have won + # x.is_lost() # True if we have lost + # You can check if two GameState objects x1 and x2 are the same by simply doing x1 == x2. + # There are other functions in the GameState class, but I advise against using them. + from irlc.pacman.pacman_environment import PacmanEnvironment, datadiscs + env = PacmanEnvironment(layout_str=datadiscs, render_mode='human') + s, _ = env.reset() + + savepdf('pacman_east', env=env) + env.close() + + env = PacmanEnvironment(layout_str=datadiscs, render_mode='human') + env.reset() + savepdf('pacman_datadiscs', env=env) + env.close() + + env = PacmanEnvironment(layout_str=SS1tiny, render_mode='human') + env.reset() + savepdf('pacman_SS1tiny', env=env) + env.close() + + env = PacmanEnvironment(layout_str=SS2tiny, render_mode='human') + env.reset() + savepdf('pacman_SS2tiny', env=env) + env.close() diff --git a/irlc/project1/pacman_demo2.py b/irlc/project1/pacman_demo2.py new file mode 100644 index 0000000000000000000000000000000000000000..a3bf61d9756b70f810ff3f962ee59cb1f478bc11 --- /dev/null +++ b/irlc/project1/pacman_demo2.py @@ -0,0 +1,11 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.pacman.pacman_environment import PacmanEnvironment +from irlc.project1.pacman import east, datadiscs, SS1tiny, SS2tiny +from irlc import interactive, savepdf, Agent, train + +if __name__ == "__main__": + env = PacmanEnvironment(layout_str=datadiscs, render_mode='human') + env, agent = interactive(env, Agent(env)) + stats, trajectory = train(env, agent, num_episodes=1) + print("First state was\n", trajectory[0].state[0]) + env.close() diff --git a/irlc/project1/project1_grade.py b/irlc/project1/project1_grade.py new file mode 100644 index 0000000000000000000000000000000000000000..1321a9d1f6c241b906a94210aeae8fb43aa0d013 --- /dev/null +++ b/irlc/project1/project1_grade.py @@ -0,0 +1,4 @@ +# irlc/project1/project1_tests.py +''' WARNING: Modifying, decompiling or otherwise tampering with this script, it's data or the resulting .token file will be investigated as a cheating attempt. ''' +import bz2, base64 +exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file diff --git a/irlc/project1/project1_tests.py b/irlc/project1/project1_tests.py new file mode 100644 index 0000000000000000000000000000000000000000..dd846223725ae942bc57f2ead0768d4e3bb12de1 --- /dev/null +++ b/irlc/project1/project1_tests.py @@ -0,0 +1,377 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from unitgrade import UTestCase, Report +from irlc.pacman.gamestate import GameState +from irlc.pacman.pacman_environment import PacmanEnvironment +import numpy as np +from unitgrade import hide + +def get_starting_state(name): + s0, _ = PacmanEnvironment(layout_str=get_map(name)).reset() + return s0 + +def get_map(name): + from irlc.project1.pacman import east, east2, SS0tiny, datadiscs, SS1tiny, SS2tiny + names2maps = {'east': east, + 'east2': east2, + 'datadiscs': datadiscs, + 'SS0tiny': SS0tiny, + 'SS1tiny': SS1tiny, + 'SS2tiny': SS2tiny, + } + return names2maps[name] + +class Pacman1(UTestCase): + """ Problem 1: The go_east function """ + + def test_states_length(self): + from irlc.project1.pacman import go_east, east + self.title = "Checking number of states" + self.assertEqualC(len(go_east(east))) + # assert False + + + def test_first_state(self): + from irlc.project1.pacman import go_east, east + self.title = "Checking first state" + self.assertEqualC(str(go_east(east))[0]) # string representation of the first state. + + def test_all_states(self): + self.title = "Checking complete output" + from irlc.project1.pacman import go_east, east + self.assertEqualC(tuple(str(s) for s in go_east(east))) + + +class Pacman3(UTestCase): + """ Problem 3: the p_next function without droids """ + map = 'east' + action = 'East' + + def get_transitions(self): + from irlc.project1.pacman import p_next + + state = get_starting_state(self.map) + state_transitions = p_next(state, self.action) + self.assertIsInstance(state_transitions, dict) + for x in state_transitions: # Test if each new state is actually a GameState. + self.assertIsInstance(x, GameState) + dd = {s: np.round(p, 4) for s, p in state_transitions.items()} + return dd + + def test_dictionary_size(self): + """ Is the number of keys/values in the dictionary correct? """ + # print(self.get_expected_test_value()) + self.assertEqualC(len(self.get_transitions())) + # self.get_expected_value() + + + def test_probabilities(self): + """ Does the probabilities have the right value? """ + self.assertEqualC(set(self.get_transitions().values())) + + def test_states(self): + """ Does the dictionary contains the right states """ + self.assertEqualC(set(self.get_transitions().keys())) + + def test_everything(self): + """ Test both states and probabilities """ + self.assertEqualC(self.get_transitions()) + + +class Pacman4(UTestCase): + """ Problem 4: Compute the state spaces as a list [S_0, ..., S_N] on the map 'east' using N = 7 """ + map = 'east' + N = 7 + + @property + def states(self): + return self.__class__.states_ + + @property + def sizes(self): + return self.__class__.sizes_ + + @classmethod + def setUpClass(cls): + from irlc.project1.pacman import get_future_states + states = get_future_states(get_starting_state(cls.map), cls.N) + assert isinstance(states, list) + for S in states: + assert isinstance(S, list) + for s in S: + assert isinstance(s, GameState) + cls.sizes_ = [len(S) for S in states] + cls.states_ = [set(S) for S in states] + + def test_state_space_size_S0(self): + self.assertEqualC(self.sizes[0]) + + def test_state_space_size_S1(self): + self.assertEqualC(self.sizes[1]) + + def test_state_space_size_all(self): + self.assertEqualC(self.sizes) + + def test_number_of_spaces(self): + """ Check the list of state spaces has the right length. It should be N+1 long (S_0, ..., S_N) """ + self.assertEqualC(len(self.states)) + + def test_state_space_0(self): + """ Check the first element, the state space S0. + + Hints: + * It should be a list containning a single GameState object (the starting state) """ + self.assertEqualC(self.states[0]) + + def test_state_space_1(self): + """ Check the second element, the state space S1. + + Hints: + * It should be a list containing the GameState objects you can go to in one step. + * You should be able to figure out what they are from the description of the game rules. Note pacman will not move if he walks into the walls. """ + self.assertEqualC(self.states[1]) + + def test_state_spaces(self): + """ Test all state spaces S_0, ..., S_N + + Hints: + * If this method breaks, find the first state space which is wrongly computed, and work out which states are missing or should not be there + * I anticipate the won/lost game configurations may become a source of problems. Note you don't have to specify these manually; they should follow by using the s.f(action)-function. """ + + self.assertEqualC(tuple(self.states)) + + +class Pacman6a(UTestCase): + """ Problem 6a: No ghost optimal path (get_shortest_path) in map 'east' using N=20 """ + map = 'east' + N = 20 + + def get_shortest_path(self): + from irlc.project1.pacman import shortest_path + layout = get_map(self.map) + actions, states = shortest_path(layout, self.N) + return actions, states + + def test_sequence_lengths(self): + """ Test the length of the state/action lists. """ + actions, states = self.get_shortest_path() + print("self.map", self.map, 'actions', actions) + self.assertEqualC(len(actions)) + self.assertEqualC(len(states)) + + def test_trajectory(self): + """ Test the state/action trajectory """ + actions, states = self.get_shortest_path() + self.assertTrue(states[-1].is_won()) + + x0 = states[0] + for k, u in enumerate(actions): + x0 = x0.f(u) + self.assertTrue(x0 == states[k + 1]) + self.assertEqualC(states[1]) + # self.assertEqualC(J) + +class Pacman6b(Pacman6a): + """ Problem 6b: No ghost optimal path (get_shortest_path) in map 'SS1tiny' using N=20 """ + map = 'SS0tiny' + +class Pacman6c(Pacman6a): + """ Problem 6b: No ghost optimal path (get_shortest_path) in map 'datadiscs' using N=20 """ + map = 'datadiscs' + +## ONE GHOST +class Pacman7a(Pacman3): + """ Problem 7a: the p_next function with one droid """ + map = 'SS1tiny' + action = 'East' + +class Pacman7b(Pacman3): + """ Problem 7b: the p_next function with one droid """ + map = 'SS1tiny' + action = 'West' + +class Pacman8a(Pacman4): + """ Problem 5: Test the state spaces as a list [S_0, ..., S_N]. on the map 'SS1tiny' using N = 4 """ + map = 'SS1tiny' + N = 4 + +class Pacman8b(Pacman4): + """ Problem 6: Test the state spaces as a list [S_0, ..., S_N]. on the map 'SS1tiny' using N = 6 """ + map = 'SS1tiny' + N = 6 + pass + +class Pacman9(UTestCase): + """ Problem 9: Testing winrate on the map SS1tiny (win_probability) """ + map = 'SS1tiny' + + def _win_rate(self, N): + self.title = f"Testing winrate in {N} steps" + from irlc.project1.pacman import win_probability + p = np.round(win_probability(get_map(self.map), N), 4) + print("win rate in N ", N, "steps was", p) + # print("Testing win rate", self.get_expected_test_value()) + self.assertEqualC(p) + + def test_win_rate_N4(self): + self._win_rate(N=4) + + def test_win_rate_N5(self): + self._win_rate(N=5) + + def test_win_rate_N6(self): + self._win_rate(N=6) + + +# ## TWO GHOSTS +class Pacman10(Pacman3): # p_next for two ghosts + """ Problem 10: Testing the p_next function using SS2tiny """ + map = 'SS2tiny' + N = 4 + +class Pacman11(Pacman4): # State-space lists + """ Problem 11: Test the state spaces as a list [S_0, ..., S_N]. on the map 'SS2tiny' using N = 3 """ + map = 'SS2tiny' + N = 3 + +class Pacman12(Pacman9): # Optimal planning for two ghost-droids. + """ Problem 12: Testing winrate on the map SS2tiny (win_probability) """ + map = 'SS2tiny' + N = 2 + +class Kiosk1(UTestCase): + """ Problem 14: Warmup check of S_0 and A_0(x_0) """ + def test_warmup_states_length(self): + from irlc.project1.kiosk import warmup_states, warmup_actions + n = len(warmup_states()) + self.title = f"Checking length of state space is {n}" + self.assertEqualC(n) + + def test_warmup_actions_length(self): + from irlc.project1.kiosk import warmup_states, warmup_actions + n = len(warmup_actions()) + self.title = f"Checking length of action space is {n}" + self.assertEqualC(n) + + + def test_warmup_states(self): + self.title = "Checking state space" + from irlc.project1.kiosk import warmup_states, warmup_actions + self.assertEqualC(set(warmup_states())) + + def test_warmup_actions(self): + self.title = "Checking action space" + from irlc.project1.kiosk import warmup_states, warmup_actions + self.assertEqualC(set(warmup_actions())) + + +class Kiosk2(UTestCase): + """ Problem 16: solve_kiosk_1 """ + + @classmethod + def setUpClass(cls) -> None: + from irlc.project1.kiosk import solve_kiosk_1 + cls.J, cls.pi = solve_kiosk_1() + + def mk_title(self, k, x): + self.k = k + self.x = x + + if self.k is not None: + if self.k != -1: + sk = f"N-{-self.k - 1}" if self.k < 0 else str(self.k) + else: + sk = "N" + jp = "J_{" + sk + "}" if len(sk) > 1 else "J_"+sk + else: + jp = "J_k" + if self.x is not None: + xp = f"(x={self.x})" + else: + xp = "(x) for all x" + return "Checking cost-to-go " + jp + xp + + def check_J(self, k, x): + J = [{k: v for k, v in J_.items()} for J_ in self.__class__.J] + t = self.mk_title(k, x) + if k is not None and x is not None: + t += f" = {J[k][x]}" + self.title = t + + if k is not None: + J_ = J[k] + if x is not None: + self.assertAlmostEqualC(J_[x], msg=f"Failed test of J[{k}][{x}]", delta=1e-4) + # self.assertL2(J_[x], msg=f"Failed test of J[{k}][{x}]", tol=1e-5) + else: + for state in sorted(J_.keys()): + self.assertAlmostEqualC(J_[state], msg=f"Failed test of J[{k}][{state}]", delta=1e-4) + else: + for k, J_ in enumerate(J): + for state in sorted(J_.keys()): + self.assertAlmostEqualC(J_[state], msg=f"Failed test of J[{k}][{state}]", delta=1e-4) + + def test_case_1(self): + self.check_J(k=-1, x=10) + + def test_case_2(self): + self.check_J(k=-2, x=20) + + def test_case_3(self): + self.check_J(k=-2, x=0) + + def test_case_4(self): + self.check_J(k=0, x=0) + + def test_case_5(self): + self.check_J(k=1, x=4) + + def test_case_6(self): + self.check_J(k=None, x=None) + + +class Kiosk3(Kiosk2): + """ Problem 17: solve_kiosk_2 """ + @classmethod + def setUpClass(cls) -> None: + from irlc.project1.kiosk import solve_kiosk_2 + cls.J, cls.pi = solve_kiosk_2() + + +class Project1(Report): #240 total. + title = "02465 project part 1: Dynamical Programming" + remote_url = "https://02465material.pages.compute.dtu.dk/02465public/_static/evaluation/" + import irlc + pack_imports = [irlc] + abbreviate_questions = True + + pacman_questions = [ + (Pacman1, 10), # east + (Pacman3, 10), # p_next (g=0) + (Pacman4, 10), # future_states (g=0) + (Pacman6a, 4), # shortest_path (g=0) + (Pacman6b, 3), # shortest_path (g=0) + (Pacman6c, 3), # shortest_path (g=0) + (Pacman7a, 5), # p_next (g=1) + (Pacman7b, 5), # p_next (g=1) + (Pacman8a, 5), # future_states (g=1) + (Pacman8b, 5), # future_states (g=1) + (Pacman9, 10), # optimal planning (g=1) + (Pacman10, 10), # p_next (g=2) + (Pacman11, 10), # future_states (g=2) + (Pacman12, 10), # optimal planning (g=2) + ] + + kiosk_questions = [ + (Kiosk1, 10), + (Kiosk2, 25), + (Kiosk3, 25), + ] + + questions = [] + questions += pacman_questions + questions += kiosk_questions + +if __name__ == '__main__': + from unitgrade import evaluate_report_student + evaluate_report_student(Project1()) +# 448, 409 # 303 diff --git a/irlc/project1/project1_tests_complete_grade.py b/irlc/project1/project1_tests_complete_grade.py new file mode 100644 index 0000000000000000000000000000000000000000..aac3b1bc52eb188ee8f3c57f872797baa428605c --- /dev/null +++ b/irlc/project1/project1_tests_complete_grade.py @@ -0,0 +1,4 @@ +# irlc/project1/project1_tests_complete.py +''' WARNING: Modifying, decompiling or otherwise tampering with this script, it's data or the resulting .token file will be investigated as a cheating attempt. ''' +import bz2, base64 +exec(bz2.decompress(base64.b64decode('QlpoOTFBWSZTWSy+DbgGgRD/gH//xVZ7/////////v////5g/758veo974t9zH1QHfYAFJXMpycIO2KUASKUUFGtUCSSV7aUFOIaKQCEgDzlgG+mQBc+6WbuPjICSKoKNAWxQBR6ADVAaGRJx3QOkL7ddu9m6SPgHAEAEB5k7YD2AAAC8zxAAA41A9aAET75F27OtHA7gAAAPnoAe873QaTYO56640QPWdNRJQ0eAAAR6O+8QTtoS3Pbfd2kuQG7uXdbg0ADVDTV7HwBcQmtjbAiAABXAAAd3QCdtdwu3rnY0PN9h9AUNHR0JKFNPgCgAAAAAAABAHctrZny8AAAR0K1ryG+5x020iprlfK7Vu+uPFTU+saM+t9tJPe3fbaDPY+80S+VtrVhbK30OhoBUPu2u73epJ7U60+Ap7UkgsjJms29rh0DoKV7fd9demiG2m+zR1UXZilALbvDR93zPY7az3L73Xlr19oegHQOgCuvejthvPOiD1Prnt9AfW73u5zw7Du9Cglb4m49voHBw9953APu6+u92OMoC7fEpT0OfTBL6HFk+bMmlXvd03EW99u+8NuPXpoPb2+8fL0A96AaToUTytoNbbu+77Y+9tNtovu27VLpnVhqeXk3vq4+mvXj6e6333m5nXVG5rsq+aZvem+06BQd7zntu88T1gR227H3vdlpmYvPgAG8YPd3dO7nr65rK++9zI7wfXvvtU4j072ZKOuR0iOte+9nPT3nwlNECAEAQCNAJoaBMg1E20phJ5qm9RqNmpMTNJ6jNpTynqCU0BEEJNEaE0aT0BPUamp6n6iPKbyTKNPao9QAeoaAGgaAAlPRJERCNI9JkNE9Jg0mp4po09Q0aGjTQNANAyZNAaAAJPVKSRNGmpk0Taj01TIaPUA00BoNAAAAAA0AAGgiREQBAExCYmEATIyCYEbUmEmxJ4U8TU8k0aZGh6IFRJCaAhJoNRplT2pHo0aRHkxok2o0aekGT1DQB6mgAAB3oh/vPdSKfBARQT/ERQIogAMEUCKKfiFG0EFp+sAgK0iH0RN4iSSwYlQ3TIh/FQGyE3USYkhDZDQyKg/gsGAUoAwUBIoCoQNnyVrP+tC3GSEiv8Miiop9U//T8VJsQTWL1/ytwcFP+khpxnX+Z3K/9u7Nf3PZh/o/8HP+sUjicXbn+hdrOcjBHz/LTVdfckFN9blcUURUllff/JfPdf51Iv3OOutZ1FMd2peECQi0RDtCqH90ZLqbUC8vOc7keHm6iE0Z+7/Noqu9wwuXdG3d0V8YyefTjSoUotYtQ/SJTOQJyvpNl4Rgsu6G6/5TTtPG02n/FL/DJh+Tt/9xXblDvvf+9OtU83viLkbPw29nyhCDuQQPl8pV/Pc/zbECtXq1V+L9TgA21v7La1rstqNFo2Kk2NtoLRW0WjFtfzNrpEbKVE8W81NNtrRbISNf+djJA/0WAjIqSItiGGrSrszmpYhMzDJ9e18UtE+iJguHnwpOdoiCu09t+U5ziJ260c10oqkI0EkiRSSTqleankIIMRf01u00hqEqiaNMzSBix53RbJ9B/5/8dT/HjkqBG2Af7dRLVanx3/wlusWTYOXLm5n0/p+n3Ej5vbQ/b33f7lPJy007zy5+ibVbZAibDrZQpct4T73JSdxZdCIS7C4cenWUrj/p5Z1lklUexDs0Duw5FIQ4hPcQCpq7MjZJLk7ut6gSkP5Ahnb4SiEwyTV1si7hWM1ApD4kc32vlS3jxd/Kl8MGQ4mcN258rd0aIlfCISHeoYnWCGnLV3wnfvB//w0PWWskJ2mzeRnv/auGDC0hn67R25f6cT6/207Zb73OvSllQRamK+TUggSchtzo93n/nddc2tWMg30bd+6W5I/hNgl+FjfLD+2z999eRBj166UnCY8UN835naCvL4eqPt/zdvMRkDe4Qxf6F4muEH5remD88J2QjkmPO43Tv86fPf9fbpjSuTNMRisRTBNcIdN+HT55D50e10e3KCATPjho9r/uEqs7H3uYq/yIDz6CarGQSDKnuE1Rpy7Hpe0cUQgQ4ijxxe/6T+/dbtmAkWLmVgqr+P5I6fLqhPjNkC8OI3rWFxv/WmaINjjpSEDvKJltTJDREEBaUCSZQnF8CL5fV6Ds7OdO8458TrNo8T+/1b/POsMf86mkSarXvVvU8/jDPNIfxYEy0+uu/5qHsi7/L2fdy1Tk0xmmn7SUhzdVsicxHyRh1HmuG6GiSmR9PZK/B6Yu/P/neY40Q+PF4kLko1pOV9oNUclUCurscEzF13hGHV7Q93/e84bmB0GRVr6rEvLOX431aenx3FeCoaiP1aP9RnkXTrR+lPlO88rumW7cUu5j3bY8tDsXDMsQqolG7c/8XQjO7Sl1ETYjheiBS6dMnrvvzicXSnTwu1pz0ra173tc/k5XEfLjulzulJs8uiiu8pu0387Vz5rcSW7lRtcXagr0StG/seG0o/WmFN9C+u73ydE8fRfcK2lXrp5XKV++lSLmf6M4nWdMekE1wEHoJPpoYVkmcvdPTtuRThpynUnZrtpE7tSddlZVL+43ZdtWtUuCMYCGNcHRjlBBsjphKFekXvldEWQtzxMqXVKqSl1P+R4Kth/txs8/P6Zz+a/MiXHHy3E+ra0oTu9yrFPa7rsgvpZNV+BrTNBiq6IlXnEy9McFogUvcKsuYL4Nul8x8SX6fnOnuJBwK9sU6EgRFo3RwxKQHLYdfzzEj3m2eIINkZ5cNHAbHis3rsq+R0nCWA9mZojemDi3SaOLtV2v/T9nBJ+WrG6RKPr0wLYL95QgirRr/1lfS92HBBo5BrzC+7dbNuN6aF1XKBKjPvDg2nalG/9T01GuWDksTIQCMnnM6PHpvSCKnvO0rQlSSC5AgzIhMUSLma4uYvR58onU7LAaZk6u48TQbIamTAdtrW0MArwe6BJs7F5AVbMrTP5nfaHjde412BnIggQhEMZvxb+5zQSOGBPd48b9cT0+BYedhIMP2GlgKqooouJIuTgkqovXUe+dN/ASkw101qth0WZnngUuwHQuL2uzUTVEOkwpGWgxCV5ZCx9d5iCMhmInTF/VL6CoT5SPonmYkjma0Cd8DGKKKWjzRKiGIndDuivT+Xl/B11GacLDnAxBNH7OJe1EzHHhSH0WrDiGMtMoKpgqzuWkOFqOFIdv2UkQ00XAmc9QJyxAHX672lzJ4wvF5j8OH0/o5xJ8cJEcKgiONHFsfU+S2XOFyofQ9FQz6z8nnUMIzjfpgu9elibWmrZMnA9lGpglxyWIMs7QiTqKvHXhy6kdf+TTKaf04enJjfOGfh2GZQQiH9/8XZIUPnMBh+wn58FG2HqmSFgqCu/yCCN9ClGJmhBKXROiQaEC4PVVW/JwhkF9YYj/MOqHxHkNcdBMFTkOQQI7CA33GlHPds7U3TwB3tYASgYHmaM3WdaNH561q5hSx3bzBahdnRmyM90tqTuWTuq0MsIbMRY4F27PFJ9BYaIt3ylJ+PnhftEspSZsbsL3oLYoTmWelPim1J742cnOUHk7NhZhu3nO86HIMj4JiS3kUI5OG73znBXw9uI8cuzZ1QS+4GjuvLVMjeFo2leS278ZH0036ZUperzITWhMYDkLqa2R10ac9U3ZaUcMDTLCiLrO7s95Ig0IelrrmkFeIjTQXriciRYukXUdbZZydGJkh7rjq0OTgolpPofRxmwd57l6F8Taugz6zVkzXk/qfzxUCN9cRIUpFtXbBIkSxiClqBvFB1s8Nz7JQ1brozc6EGQg3Bqdqx42o2WUGuRShhN/jR6Xn5W+7n7OuT0WOcbwlq39lZ4+cGpmD5fKvN85sPjYd1ftrzRRjEdNTlOcE3OZductnTJtlWEFjs1iJHI9l45HgUvnLlTZGL6pomDuLDYlz33ItOuzmpcSvVVfO05ckRlQ7F7N0Gsj8l4mf/fXPZqQNpr2yNeRGAI7uFnfDCNJF+ZLgzBBgssu2qucNCRQRCvn2tP9WX9yghZP7yG9Jn+ceBIliXs3Ak3r46jb6LdumHczUNsGKkGiOqOIg30zuGhHsxoxMvJrpJtoz0R56SE11wgkdR0tkzynpoS3Y5XFriK2CDzmP9SaVMjEWLJmgXcmcSBM2RgzMZRuoilSlxfrrn4Fx1qXezfHCxuMWbIWPu5Rgw5UuCtsnapOlCSPTF21w5WnFPsyBMgmX0EKNmhywPfKPuJahKRKyPJPYvB1jdzZDi7NP1JSb6aZj19982J601e+80t+xENat7/PuOz8FzDvzfeZ40JNb9UGZJfaRtv8IvPSdi7IoGLzKRhZRlKB/lq5FzPrUtGUqI49PHpBqdcupBg1zJY1IJyJe4gg02HHuHrugmxLq9rInG4gF9mRoFUefA6EhT9MxzJ6bHama3vE5F001oQOJCXkUu2sx4ZU1bWuJCWISo+fT0Gr5ml5f5RG/UwIOlHtTcXGpnrlgXCYUFjZyReHSQAhIQWmrR17k8yg6IOdlhSc9WWGxuRMzHCEPKV0mL79cjsYSC2l1bvbg9O/pyrbChAcjjd656nH1ulTiqrf82GJW6oSkqst59UmNKOZb5cfLf2Dms3iHHLqKHf7hPuo/nSrVB3abk3rKE0vUDw5BFah0KCYvjoOfuPet7db1HASjb75TFs7qnzxiukn40liXzJZbkxHdSaaS7HrdsYUlc4nd5BS1NFupNE2PW+R0JGXF1n6oLr9Th6bi2fVrnFy9ksv0zKFttLUyxLr92V95ic5bq+qtBzcU31xxa0uHW8/S2hgfx9d95hiQOGwtZ+niQVrpdu5zNSjYJlu4hlMXn6v8Gn0/jMbKMcJRHb8/1XNbdUNxD6Xj0dwddqI96in+Dn4pN/c5qjFzm8ueb4kioBH3mlIXSPG4IKklRNEEi/G2BhSDa5nY9UMA94dxpbHGl6eUniUlMXr7H4WoYGF33hKsMncSH7nFEBDs47mGIqk9niHTZ/6oiIilLeEQ+V3fqqqb4XO2T1EoPo+3PMNaEYfMy+4tzxA93Cdk+bFxCXscJYCOlBylIHC6/3eiCTb7PUvnI3xKDGDph1n1Iz+P9T64c0JjQtefeeNTZnNjb6NUTxuWKnb76VvActcrTlEIH+r6LXJ3fBvppcK10XJtkw6iv8tuiWbeXOUhzf0flT2tz7QSHoUnf5D39zng3riNgiU0EmwvlSCU/l3Zbb74oceuOJajt73L4PW5Bp8o43Z5dH3x4l5vbgykbnvOm7HhOK23SpzgO9bScEklk0fpuQi8k0wy9qc2MqPFDidmJGhaLlZDy33m26njKnF8fd4c+WV7uV1lhsDmMjE4EbibeCCkaTvLnbw6uAWvX1FX/RhphYLcCMuYi+jUrIvlK7pFVVu26n4dm4vurfUs6Rhw3u4WzzzbJrYmBOyu7J2AY+jw8MTK8Dwpaix2qRH+EmxF2USw7lt+u6KF71N0UTA7CImBCb1DjCKVNFQICmBTJGr3HYWc5pt68VdDsr6DYk8Q58HRvuyA3dXrTdtd3rDd4vtHMzLFczTXQbv0H6oLPvuHdqc1dqcFg+h71E/rWQFuYjZBUYrmXp3CfdB8eMINncXeDrpx7kVH6W/k3OIfnPPgplXH3ckomi6giJMMQ1SdW6lTqFg0xFl4fO0m7Tds9/zmZVJp9r4l09DrTqbQ8IF3jnBCTTUlR+DqaHZAhPKZMCIG01KG4nw0/Pkr7DLxs7tz4dHPHyJMRJ4YwPHFiDoJn26ogTsEtzwT0ckrsbnWcjVXih+V21ZZiJBD0xLUMdsJHSo9qE400a+pD4hdFSf506adRss8HAe9BecnxCGh3E0H39boIkSZGc6WIvYRJEAmBJkhJhx3BAoK123X902bet+UzdFQ3x3uAk/o53n0V9M1rrb+K91/IGIa96kOQoZ8LaC0HPLlKjNxfE0HbTgw97TkXymXBqgujU3mJYIQq3n0XWxbBQQK3OWBoY0thdmN7iC5hEd5k/Ekc+HTdo2SHwM40O3Bxh35m33DXDvHxyx1EP2oB/OltHvOqHkfHla4dAkqA4zlIGxQVjtcdsjO4gJ8VBQkuxG8kNUvK4S3qd2V/GGytakMVLHekmadTOY+djjUfIabYBBJamOxG2DPkTlUmnPDa453FD80gdCzLQJbRs6w1VrZ4WBxKzg6ffhTHZGKWGRk9HfhKWK5FuBs4JJ2c66OiK9FBHun3MZ4+R0dhXs7eiFswdPWKdw6kTfDGzOaaFMmxobjG/GR3bx22rmF5JkY34EtFIkwb0faZ/McK7mWDncYXGDCd95lCTidOh+0m1PiGmWdWyy3U3Pz7uWA/LM9jnyCGuJ37s4w00annfohRv3rVbyHS5LaMCQdMLX0zTL75s0oaOaThRilPf7/XtSdNHZaOSuUMRG6b00phBYgyaWG/Z2epW6GMYO+mUyWegYLsWYjIUkCVwjKt+N6ctXq0y/tV8dhjWXDGHT0MO2s3d+dH+k4GeI5e0PkK7m52nB6skkwVwwsXavWHHvmcJlEbO7GCcw2OTkrXdJBMyoMbkyYb4GFdkuODmHvmTUir8M+jjj4W3dG7GeHLrZ3zGjhFDpiUOb1N2WcqRfrxz3wtsDBMi+blW04FmHUmQM7sy5ZKS51kO6lE6GpTuaA+aj7aaSvXXhJg1QW24dJkySbAHdqILi4QifHlPo0jE2PItkuqGI9CcZj4RHCzsEIN9bPZyhJ+OZJrMeRPIPm9vCwb7A4YcDeGJkC705iWuEW4PzWKaVNLZPWnDBsy0XRIilbS7sK35F5hhI4XFRPVPy0Rs+Fb3ncP6c3xK4jxHFcQONj+JlxZzTe2E/Y1QddGvO277HbqOd4gS7Ck3Jxdmjc8YPyKm4Vm8PIhboTc6KjiG38Oh+voaoPYzvTnnq8d/nknKno6B4RT9eSCX8/hmd7B20O/Sdb1j3TiPk99ezYToFQvInKdhwtR2ObaZDGtl7fOwgw7IuZjxGsJQNAcQZCNWjpqzArG2fJeWXOs7IMPeWm5pspwqj4Upz6PhZttcedUe3DnjVD8TM994qaVNYi3r8ltaIFzR6Y4mZeb43h/J3kiHRXuRIHeuA6QYF8sSRuRcxRibuqlicZH6IKwVc5G+Pk2cRcccNcitOWL4z2kO0s8GNeDxzNL61OfJr4p2EBKvKZlXGuOEu5Nc0ybmxU3qU2vELk5uwe4uKxkIsy3Kg45UW+Q5Yij7PiZJmbO1aIxkcW+zC+D5eZmj4bO0drh2M/lZqmmx28nvhs/QP6XXDsGoaI6dTskb7S4md5Q+mAz7HkgyO2ISXDHHsRdbMyJH51yW8xHDELt8iLglBHB25/BOtXYudV9EnbtR3nDgjEosN308skZdcZLYRojYwlVr8SU31799xKjpUUUdw8tYjBV307TlK5JqfmhsNsJ4T6awTg8TYrgIuIpTob+TF9rYY0ybq3O8tdiRUPCZdpTPHK7O9r+9nGfl7sshhbpG0jOxRNoIzMtw9CRs5WDUum49CAvKOuvYXYBIQhkIqG4k1ZXraQt+OAoKoMmR7x+gvnNiZ8EYHoLiC//XY+RoxajAdv14nXrX+JW6u/gAP+n8zmtvTXn2+qvLTlEUQ38XyPPUcYdsAghRDvrCH6Z+NIH49Sj/xRTpuDaRQNpv+uzyth82znt2dRh3YO2HoN+Yokav3H/mP9MJ3RAqRJCT/S+qcOz5DqHydehuKjg4YvlhIKqqYvj02rlfHqPksHFZljciU/DJmhCQEqIEIiSI45XRzdTJY401o253nwkno9jwb3mq/rU4Hlk+UcYeInzkSjvta8+BR1l9XllKdiOB+kv/2yzxt76Ui1M29H6nxjaPrsZWFPhwls8uKbl5c4zEejRyV743KsDmE/y4XzJ066vc7v81r4kYixv7L36mu7OV12H1YwPKkpk63RG6OUZZ9ld8zPBzdfldlv+OudMueTxLEsXOJUh5qQslpOd8ivyPsBA5703wmPkm9SJwHILzBg0oePjL1nV93eO3daPs7KUfT5N6is81fGmwb+ZQgXT9k/dtf+Pj6PhcX6efHjLdpD7s4UF1NV5r9HZxnla7uv9PXXbes+S6FnYJu+7GM1h4mYhJCzdIcOrxD4fBEMgURE0FQPT1231d69/WdfkUmBEVYlMVmYw6rmjEWyZMf5dMs77EBpDhYDswxkDNkD/L+/MOkw3rsoSwDSdlCOBbMouCqQmX6zh6++BHPQuCAIW7/g4VTHHfQ6PJvfbyhsbn41egmLk9I9SlOp+fGcXfyb+zfxX1PiFk1ln7n7/9kj88jx4Gvlw21bZM7o+2WOJ8nyQriWZRaEEhPoObQVNyRzjSwWaQyoibIjpgJARxrkZ7OAaJv8X1sFP0z7/49BBTLstcwlCTkczjVeJq/wTDVd6hnTySWcZz9HXOdN5MTkw4gr11czreWCcT+j4ldJ20uM/jhAR8eS8xjNe82A7x5+I4ZT+l3zKGnV8y2SSBOzrmwTeoM6n4+ub55qaODHrn8krWX1u7WUhsZnO7bts2cVLznOuanQ9CKxgr0+j4XTP4XLeyrkxFlYRveVs8xGwYlAhOCD+9SGSZC6AeQ59h4/0tQgmOxmP7iqYRfX7fr9X+AJ8U4Fn018dfLjKj1QCsnOxdSANX+vd9H6amDRV47Sbtc/Kbzs+737pJIQkAhK31trfO30fD4Lnx93sJfZNdMRQUzBVVQA53MAeoZ2mfJfHWoX5ezzyvUV735hFoLfus23OVfOK5eKud3XJ3bk7syuJUbcIPVjXIg45h4cFwriOFf68gtLbgyBnO4/JG9xTi6E20lPEjEyGTGAshXz/vHTSQyrnFCadFAWEC4uYDIjCSbFpNUa3R+B2T0Zev8fb3/VOvZToV7s1Kj3iBpoHM8GhiCbund9vwXPMnSkP/mp67jdue/9vmmuF9KlnDxt2xhLI44OCSZEQQcpbXwfY545A4KOYuPq+mMT1M75lKHTnkqpEGn7WkUP6frN0jGddkNuC3epmPseL5eo6gz7XldXqDBP8dZL2nN1Y8XUvH25UM+tZP7+HIl23Dj6TPGO9P91zGKMn4Z1c49oSjlz+ED8vwJJFS4648NTH9IW6f5wKi9KPrhpGGVv3ZiaVj4fyooyD+rlzjlEQubs1yqU45wsJUWH78Pnta2hzwHE3WYxMQUQwhA5v/OeabLTfKz65/Mo2W+5+RNdinrMOD+KpTStBeO9eKLZCQrTx2RvqjniihH4o3bT9z8YD1JZ9uXXSYEIKnUf1CGJKdtoq4IHgb/Y/in0aenWnOzUrakLF7nTkfEsQObUVQcQ7u1S6cZsTaayaao2P54S9Zz+c+JuC0UdBbQOjk6CLOPhJRemgcIBC/vG43TD0zH6k3msihJel+VpTSxahDdVtuQoR8AcusqWRDwpPDO8thefztOhuH3JK1WvGu9xb6oqTuehX61S6ddIgUbOScnAZ6WY4D+02LrYateZku+4TuU7RFtAyBH7PjDL6TZW3A+C7W+AsjaBxMQ1CsTlf3WESJwc9aS9imNqh2cclx6ninz5uR/fUZYncwY8m3NLASKiNx3luLY4Wd/e/sRAtFaB1TfijGTzTjpp9qhqSM4HrL1/7v36aaHu99a1uMGWHpN69aIgfVN6DTnlOYj1WwiSjdiRIknMIoUlFJNMbhIpDKhxlN3+nrGv6/uTfod2TamuKHj07c9FjxRTKTlJBLrIH+lH0aCcpCr6ZQXOi2/wHjHffBVpa80VLJIlp5QiyPVl17ED6LoWSOHs7kM4j0kT8hetXZiL+NZ9FMhNhgQj7iIJMmklrIU54VI8rQW+/5vR/Pahx10kY79qjqRVQrR6Jy0lOJQr4CUqqCU+bxuR1q5PasL57oIXevmyu+iRtY9s/dMlt6X4yloSzkn3x1vmppyXZBIs96oUFMf3r0XYTun9UYO98okmZfkSmkHP68o5LXlvP5qZ+isO+ix3Tikv0NLv5zv7dBwsHn8keeHejnaTSg+op/mrHrhjq2iZUP3OfQm1R0h7/W+nDanU366P2PDpFyHQCQVV0J/0Hg5qtv6SO2OXMvx+UGfsuct7F4LtvnuH9Ho+/QO9iDsy5FuLLwxPA/T6NObbi3cjvlE7iZgVH75Lg/KeBm5LyZX7A+0dfY+0es54tSpHF8OdvsmW5OTlgk1JrGfhOckOksX+pFkXK1HZfc46FzdjHKVe/baRJJZZ5XkhTvfY84NwKhcrUqTUhEz4CZC8vElyS9fz2JAYJTHBwveb/TxnqqVZzN/6pNJKDTRSkcPaQdbyeVuPnbw7q+m58GO0R2l/hDNn2uQdHEpF7tZMRRxr9e+8lwymKClnMlKQqI8r/jBZHan5Pwwe7DH5/GlPG6/Qk2yaovUVH2tWDaRcRspfr9lKEtYobTmpSe7f8e6ZNbgKIdM6LuqrPlR6VEfDGHD2vVI4Wwyw/MNBXKR7hzd+b05h9pzjoqAZcOx+eZKn2OprwlxPPW3frKk1C15+/jjjRSSs81GN1K/bN7XkNO9XXrKg/baBbk3Rdmj/xWz1Lscp6STQ6ZH9i0gV76Sql4vpPH93pieT9q3739OGfdGetIW/lXV3rR/740PC6mMiEsXtCVUkq74y4a2ly7INllrz3Kha5R7u/jb1k6qp9fF7JrpePJ+hhwu+/tgzsvh3h6LLzc9c31EOc++97r4HUPtxXjbh5PYqlX65Yyxo9HeXKGyPhpTS+lUdLOW7L49t7guFIPZjl0kdO27JN61x2jQkkWV+6zct0/olpdScuqdN6Lp7TPCnSuZPRPPDx+7Iik18y+Vi7OXdbhpx9EsNH8ceGB3eMopHOSoZU8vS8pvwar7oe998yms9pNvRdc+6j71LAW0OuOdf3LwU6bLvWinfn7buFKejCy3kzvPTXlQ0s8lvMHuOinepcb9pEh+2/jPsvnLK3y4cKy6Ksr59+6Psl37s53joF9HZBJIruUEk8RA+XuhoXcuSPFS5XxuydhIlJWd0QJ+v3RLN3cuyT4ynvQ9ES76TftiJPsoSF38AWBeqAm8/mr8vZv2eyHS51B8InvnqZl/t1LL8+2K8uTU1LqUq8JzFJvCC+KJ/ZKh5PpFOsv0Sti6aXF/Tac0pQ/0rpqpcJR6ZaxRMyifhpbHwsUMkWRo5KTukCQSr6PKCh93i3PnPtNdYMnb8g/AGVYZiRPmfkSBvw/CtT1F0Pbco7dV3RkJor0+zACbSDIhqRRJkIY9Plnwly+ysTS4khwinWfbt+jD6kBYA7JiP3ZAYiJCdMxg/EYhg7t0SlRM1LfKLb6POT1xUenB6ipMcgWcBZFqOMz41JJJj44cmssBNul2DOrvsj5l59lI07FpOkS+Pu+ySkl/x1uOVuHv+Xl9x3Zc7uijmQX7l3YDfZ/ChK6z3P3Hj5tciyfLG3T5nZuSm9uXD171jjP0K6++DBUq82yTLF4+dN4zpxswTlxuKcyfH08wpbYfjoD7XG4pBTvut+bi6wuvMODj2ls0hW+k13FbjXCOxSua+amd0b86FFKGMlKGWJA2+onvFbwLXeH22OVd6u7nHSZZp063IgwHjOrpiqkx3k77pLwJePGKT8OBaJg47G47fPlmURqjZQJrouPLflUxvfZdbvxvmeNOyDDOk9M74KfcqPqFxwknmt5LXdupHCdUfIjqDy5UedHmpY9OTeyws14h8UkQfI0HNKZUfnnjIl33Bm3WZnTarW+2eN4zKwWbrdAjEnGvs9LDcvzw5XqDzJZUw57NISKkLNYQS3Pj5yCaZDWExSzO2ZbCFROObu20jMzjwy9FDPHAi6j89/DncguMasiWsyCXpdjGF92+xcOsm69M3V1/TY3+JMO5szdY1zce7ebpTMb+nnqq8J8u668y5Z9p+s8EFdcPXGkOb3x7Tdm+kibN2I1KcInpODjz5a97B4W23XLDDIlYtuaRR2uoVh8TioYhA4759787mDPKrT6ZQEcedriYn7yrS59LhzXx3SoYIvRS5wwQ0Ik3bXW4nFqcdvGjYm7dwkdfj87CZAyEH/JzMPpeTFlT7qOJgIf4EKIDl/Bk/DDI9+0fbFmCUhshl0KMYX/zOfKrdU/UkB5dxyZbiR3e+4g7Lt/zlTUCXfaLBZGMDbGvqusH4dbgjEnsSJ2+745XbCujSPSUUiT78XFSRdfOU1cKBD/AT6v8dXsYlPN+MdMNlVaZmBmXRW4tfjF7iyEe33SgoXVjGeWUn+itNZlkJK3hdfwmMm4e3rinjf4NcJvpbMP1cP82Lh2HS4iNJl/Jdr6nO74euKH2h35GyUz81p+fwx9u8cuNRdl7jJlomMcHAuQzPZQhZPCHr1zPwNvEbVSjBo5t9KXUSEx+H5g1OzDMN4jcvaMmJHycHhzkhqDKgNZ/9yOpsi6lIc8JHRRXjxp0kUrh/tnPD39v4s4+6ueeqsmaRqSOVrOKNJzYnuTPNlXXnXGq4Vc/l2dJtad3K3p7fr9E8dp+M6aJ8fqilM8Me7t029V9bsnwLaPToTSQJL6k7CQkkkw0yCxkhcBvVTJhTUnO2igh/q7kzSBTkTk8q2ULyZ+Dz7M2Bw4pGIxMWfr1qMw2KO0zOGyAlQI3Tu5mcEsnDxDJj0xQ5QucTOpAA4novRWjiGiU+H1svZ2CY6R2PcESjfWPfspYInamF/MbwPQBtxodpmBCYb6/pcal4HxeNQFaY81RVBxZvRIcDaG3/Dze/oNd6rH9qQHcL3rzaPScDU4hroMDNGBiBZPPBoOgnWyTuNU2n5oYOWvDym4kIdkXxlupsP10hscMGEPN6CHaWakJsOLDnsM+DzssfDNHYJMhxXBz1/q854xDakIEPdW28PQ63W4d27ojzhq+wereZafopk29DJqXnFhwoqPTKYOkZ6ovMs76SkMh5zqCneNVp2dIEJB67y8PKlBk2rB3k9s6dG2DNgE2oR6TlpIEJg7Ovgr0nCECBsjIvdz02s35NaAwdvAo4SYgVGvO6Nhqxej/E73ZjvShjwHL7BwDflbEaJpouq5VmM9ImkvXbb4zvVGQgTJgV4Oyuq5M8ehExGrk47usXokiEeXLGOCMkLC0wWCYYa1JDe4MMe2+14l1wpWRkd8ZwsihhK/JAkIVSmEjEOXUhrjl0YvkyNDEwMPhvUKOME1N2SXieSOSHLx8caaJK295gwSTTJhcfF8cPr8zp0Kpdle6x0trqulNdz1EZgm+g1OTd7MTOB3ZBcfazHHEMvvO/pvC4QfzzgQFx8W5bskNG6D5yN6r3wN5nwzPtbo0T9mt/DZjHEwCv/EuobwzCgfxh4GmRpsNiDMwACfnfCfwlqIIHiE/b8nqPnZsP7IfX7sftlfmrd/NFIHHhi6m/638WRXksbrPfelSZPnGJQy69eq+/BEgkd7LZISCRT3Coer7fz5kkkkkkkkkkkhIskhv3+e/4P3fG78/wPHPs+fMnOr4/HW5icc17RNdGv0qpkjh+p4H7x95VbHJRuON0cLnFFKJLveSkqYY8MzDIYYTDDKXrkkwhZJKkLColkURT1mAgrULdcINgLBWKhGQBIoNSVEWaskYEsJSSUlSFEqDGJDJCm2YKSbaybTaBkiEVCILBAiiQEipxWgClUiEFIqQRpGlaVCKkFShoTBFQskpFSKkpIqFSGSYGBLBYRkZGIUSwiosiKVJMTEZAsWEwxDIRYtq7UuoUZFISMmM03q67SRhJMk0kxMqQmRopNMkMz1LdjRqJIiIlKlAyzGMyRIqWSyDA66rlkmBHtdxkMEoUqR6lwIyNKZkWlI9VOhGk3SuiphJpGJNgokj2cfnen3vPEQdeFvnkOKWJLl3c58RV2/jqJe08VcadJ6503gUhrwiTsfd94Sjk8Y9PxoeahtRWidGp08yO8bk2iJwlyTXUbiSNc1nTwuogqetReiDbpKE/ja7mqLjvXGXz1tSaUJ1HiuNvvldpZS1GjByL3xRkv11rrffO3rayesOzfLrLtD28J4rUeOdVBEdv4299up1sXSFx1WsvPHEzO+8XHTvho5NgUBp1OceJI51XCVJ3dDuhIdOujK0cZ344kxdTL8Kir7yn77vus8QcclLvx4N0odS/jlafHSSjrY+1GjLxCSzh4y9c7vmvhL3Gdtb1MpqWSkSlJJk22xmlpsRKQtVSRNZrZKbUQhGYWm2y2gNlsqGS1WwaMrS1psW0mSNmrLUCpUNs1tvlq2dQWi8q7tGCRIyhIkQaJQjSj6J2jJJTNiQSJREobKZMzGZZJSaQDMlSCGQho0SDG+F1uWIkTXkuEk1ItHrvxMTJx33rnmjO7w3JwaFAk78VvJ0/iOdcddPR3xdc9nR4jmtXHUHT9da5hznwn8cG+pT987zvvbng7mPF+LjccFcrN+LpyLO1ZxLJ4PFb68eK4Wsdik0XZ4ohOJ35GTcJFsO4ly8ZfO71zXiTmpfXXcz1HXVVF+OO743qN2/Z4XiFvDPCndXT7d/Hi+eV44O/Heb7ONafrvs1uL73XXG4rXPWozfXPG++zxKk44iWhw8diQGuqmh3ScT7l35znt9PwntZfPt50nfXV7tttm19N3eSWS2NJLJYyyW2WSVtsl83PagVcVVBU9qZULwBBMSQFWVzG4IMaMUSYosY3l9Hw7y9JRVFFMyVNuGouW7DTa1OObsZBNkUSSCgqqL5hMU5IKsc3MM4ct0gesttWwXriOLDmzayBo1ERfFRrhtWvZey5eNeOETjhHK4BExTOIpnLMzXA4hXMDUDUqFciYVwrhkcDXNQw1DNSpUImFS1oybbVVdDMKikSJXCsQU1MjldcjGuRKxXUqjdRayYyI6Pq8NcUCIcTFwI5a1jOS327CvMIhOTAkbTFqCrmROnK6rrmeXKWZmuWTPDhEBHtk0jkhiG6GY4iaxW80NQbibIZgbTYdV8an0fTrW9tW4aDEWJkZloiEsRQSJopIgiKJMQRJERSEghCBKUkQaMRpMQmkxtMpNMiMmKMyMWMBqIi0RoAwEGijVJZmowCRCaYmNjGLSWEo0YiCTGCwYiixBBgzNEmKQwYJozRkSsFCVJkoEJlNNiymTQaKoAk2KNigIkpCxjYZiyRimZgptqxk0JJpKIgiJmTJggorFjGDESTZSFFZNEylAQ0mTQYxZKopQooTM2MYyQhRpiEREJZYbGIyYpkUhqTYsYAyESaSgsRjBkmQFGJAksVjKSFEWMJRQaiqr33ixQZIurq7W6o0yUxpjGgi3iYq3DRmEmlnASzGdEnhJSNUxA91UjXDnfDa6cMFXNTKZNx34fii94JtvKbOeF2+uBcJ/DrnYMAN0uRgB8bTU2bNJGpqbTU1ptmzZWrNVZaVNNbMrLU2yySypIWyQIreEIOmZer262c9tvbTzGbBW077SGKJBILUiSIdSBLJHnsiSH+jx583p11ylg26kx6vlfVdeq+m8tb8EyRsESQGBIKUxkIMkzFEltJJSSRJmSUaSkmaMkYIZBk0kTIoDQYghiaQ1JKQiBBkhKMwxiNJAmQkkiCGYiwJiMZhiEhlRo1RoIhNEswyrGIDJEYpKIGWKakI2xRCNBEywZNCUwpANiKEBCjMCkIMkiYFCUkRQtjEJjQZIWCElAwSAIpklAlGSMSDSkJS1gMZmA2TaMMixUlFEUYmQlMkxKjGQ1gpKR5tvm92tyiIbjkXrpoIg7OcxmtVMj0VprhRBQ3qEB6IqohlOOvPdrMg76kwZNDaWWbzBqOp2+/KCC8pGKgwGACO9CJByFChKHbbCR1ISPltr8fURQdbDWIdvXx0ebmMApEAva0oWEgge5gX3+J8tiI5HgjJDMDI8WJNHzfGusTr8+zLMsx3V0FenQbNCvLlOR347JQb0K4SQjNu+og6esnUV46B5DhkcVOQ47y2KaFaHjos0PQ0N9/G90G3OrtESMZvTUCCXeTMwg+ApDtSLId4Hs69q9gU+JwOItLbK25WzELlMJLJkUNddET464QnMbsO+NRElVgbNio8Na30GiggpgA3iLEF6kU6uFtpSyy89tBJHx8aITokj1EsXBEmBSHkhZISd4Zg1001EJdpAErWiSR4IgkHGQI5+OYGiLRv3xJImgoLHFgzoGT1NEkjtSDQUliSPTw0J8KRKkqINlDpQ60gjjYwiTs4nfzNgbBJEOcI4gzbR2QNLgiTvKEb0JYShwbhSBOrJ0helxPQs0JJvuNaaTSLDtbbaqpXAPGyG8kI1DteoY3baJoiSTLoqkBPP4Q5dzwgn+2a9ZQHH0cv3O6OYn4RkQKOQAobFUWQRkZFJACSRJJCEU5dfVRVVjqvmIqEz1kyWSSPCISe3Mk+YfYDPsChoHYKGBQoNIf45a74TU8bGgHNpQ0MPIhVm5nw5XUszAXQraaKJwp65KT0lU9JDOMwHGC5nq91xvzu+tddAwDEC6GHHYAZnF6731rVlMIoYCe+OcGLDOju9+M1ErZTrZCQkhavMuTXiyBmPAPX3HgPv/L0dt0nXwaPDsapjxufD2+yWivu/ZmfKMtdPmhiXua61fRlrpXdcfbbCRIn7P5N/Bma7RZTsAr4h7pQ/YMzd5p2ukngPBzzD77b7sgdnaHwuWtoIm1RRRMslYUZmGBkgqIhqTOfi9OP98knx0gKHUoqHi+0B2xGMXu1zkZUoDMenger1XrDdbeUuCW62e1xRmad231ePH9vWZNdwPgoc7xEkS5Z3tPjrTy6dgC6CBz6M2CuOMrjAih1m/owLx3nDO/kO7Oo1vzaA8d/G90NwJWzlk0UQdFSZ1kTv2fgGE1HK6fxrW8DPEZqghhmGSYADbNhZShctSIrBiixRAQgC8/xdx6HDcHmwR1sgnt7euNpJJHvnTnrz381F2EWCo74A1zSkL0pQV0iLUHAqkRN4JoUqHP4S8znw4ybcFYvQwCbZqqohTtaTZs14ajrqYOQqqog7iJEhMrsV6F5Dvz86/oJLCVfbNn7DyL4SZCUk3ARAmTMgSJMwX3cOpw5QTOS6JKCko51U4Zg6DFOFlV6KXK5WGn2c7yd/StV4jTjLszoGbMMMMxXUe2ZdKg6WDMzegG5dTzDgANH+np7vDcLHXzicy26bwjaf2/ybe2USvPgi2Tmdnl78ykDy0ljXx21zu9BKcYMXe6+QbgZ0AhHh4eg8UzjyJwTICIooeqJaKganZyRH3fkc4PuLfehwBjAsSSTn8TYPuDYiNrdBaEwSTPq41GrCEypllksB+mKZQzMD89mVDsYGER4DmZOlSiZ0F7BuHoNfpDnk3m/4euo69Q6ScnkKHsGwYbBsGgposlNaDB1PA0G5Q3DAMOnAaCh7hoNGg6B2DAbhhgchseQ6hoMDYJhQ2DcjQSmBQwKeA8eu4bnodw6R1HoNw0NB5CnO4YHcKEwOAwOoUHIUOQ4FOQyIaDDQOQp0DYODqGxo6ncOQ6g2BsFDgKdfIaDoRyXA8hxBsHoO4eDgO4YHBDg5DkNw2DYMDDAoPJh2DyGBsDkKKdjA8B1Nw2seA7BgUOQ6FBhT6HAch1DwHkNzoeQpgUdTQaOB2B2BuHsGBoOxFDcOobBgaEwNwwPQOwYGxhyHQOoYG4UGwchgKHIdQ0HUPIfPprqjl41x71333mul29BNd4iMCcw3W0LUK446G+z26B7dHaAidwiRZZYpaKNJiyMLYrImZY1jaNFGxoLGxZMUQlikUkZK2883d55Aio9HLWSskGQHRQ5QSZCIkgiSW2QSZJASRAxJhJkTIiSJKKCxBRjbLUiRDISYhJKLIYkMRSGCQSJDGISIqSCZYKSCSSZESQEWDSSaCopmjYkAkMEQRIjAEoiDKFMRKIwomYCFIQlEQkyZJJktZqWRfhWDUu1YYoRlrnay/GumOGb66z59VHnx3zegY8AxfXPsb2+I8vHE8UzDMDSzDDDMa9HLit9Qd5ncrzfHK85OAaQklgHlF21EyhwGmimb2xTMEAtiEYsksTa5XTB0nCbDeFsLIJsZC8mc5g1mC5gcGTO+mBXOm1cS2GmF+TAwYpmGlILenEediGwRQWJZIiIochSMSFDLB19tokgjmRREIjUsikQqEJF2iTIQpxgia79I3khsDhznhdiOm/OoNVCTaRPG+eI9NnXgntmRDqXaushyGUxgBsAjo6znt568LTEWMYCwI8tzqnczCSJJEZIQKrMDAsiIiW941z1168eufbfrJD2smInZ2CvYNxuMlAlY0UEFIoIKbBRRStVABU5AKChSVzCbvi5+fJEkNvvLfCu6+J2eru9Yn5Nc1+YktSUcpaTiYMMSbksgN3ISgaLKnXr92+Yk445E6DgNg0UGB2FBsG7gtPrnPb7Po22kJ2+gcnhC29Rzj1c/Jj14215538tih3DyGwbh68hsbnAcBgewcB+bm2e1MhZ3CUe4UMDHjiJe0GxG2fsEDBxpJJJxheEltb0uQwA1qWgE2gMEDwOp2+5D9B4PtnV8fy+jOW2vPKz9gbgeXkCjUBQJBWEVUkFQCMUIrZm2raybaUtqpUtk1o50eYeDgM9rGy2XkC7cSZcGFAFICwFUJONY2brpo4NpqcNlLIQiiAkAjm/Cqrx7M4m3t2HdZgsbA02hv+swYZpJ6BjkYJjPqcA6VTf1q13euOOeL8PsOwDYIA4BizR0qSfxnV1Q++Dpa8DDGACYYZmqOcI45ivG5N833yAbBgBmYml0EtIcyRTs8fm421tvru9k5Dfro8U5AE5KyJjW377wAtdd+e/HDatOeeVNeLiFVIxZD+Na7bsZmBimZhmGYvzWuNnHEPo8W+cabrz2HeQ78vz3pgOhcIzxccvnajMOdgCbihzp1iyeA2GELFijrWJ7bc6Scg3BhDrEFiDuNxuhGkibySPBTWoMgjKR2vTt4537cyRx0N7qRNWWpakaqDRFpQ3CwKCgsCklqKyREuQDK7liBJFkkSRSHp6447+19toTgvAzZgw0gwbMJ10vsNFhqszZIZxQacUju74t/OdvXy597I8BdRsCh4DSOL77+HqJQJJvUxXjV+8+8THtu+ffvkbbP3PiLUwp3dHVrxIJu8HvxncmuPHDMzMYyQwN1ykkkx4AhIS8w2iwBUXr6+79/jZb21D2l9hu7QDx8exTA8gNPbEUW/kSTKCwIcx3tpGaqiGRFBltJTFGW1IsCgIL8/Er1Xyzma0whBtSBPlZEQ7Ow7Dadd231PL5/X5467EJCbvzWda85eeOfPi7pjlwY8gxuQ7uLvbt3456Y5aiOSKULEpQKRQoWEUIpQpEydlEQvYjHNuZ3m3sQkkiOApIiJykhMDMDMJnA75cAM1qDurm3jJ3q1+YZJmODgZjz47fAT6CA8gEh143QwMVXleuOYzD1329ezfr3ab99r60cm/YSb2EnuuSCXIYhHI1UkRIskcu3bR69u+/nVucdeXYDjx0cjMa8UWUu9RHZvoMK6sOumHANvPXz657dJyqI8umIiNVVDrszihgUQUKFQ2IsZLUingZJF1kb+PGtiEUSUsSE3KChRQoKFBQtChQpSMDIiKJQqKiMQikFRIyM4RLCULtURGLQ40iDYm0FCod6RHUcAx4699uOM79O++daDeobSWa74AxqiZ0EntBIf7Vf4co0tfsbtKToUpOmWzkyYwupAwYs1GzNQuuxubGrm3K4Q6ooiu6ddddjrtFzGS3MMtyi3LcgDQAMiubSlc24G7cFmgU0ySLkRUoRoy3ISSsmZZuk5JXLhmKV025mkGNCmi6pc2BtyCZRkJG00821tpcoCyNAkyCebpCUS27BF1LmIZE6u3AyMiDEBgdqbkgkx27NMTGZkJKpquWm6EcycriTRk50Z07p0ddLt2XLciRDdJKYa3LSCEkkHAZZSpxBpgE065Eo667KVd3O3SndlzKZjrTcpdRJv2G28l4u7qWZdmEzdXay07Butw2NTa56qtrXwkfVEd+8Xv5+rQajzBTpwB8auB6pvBNkSISHTfr02iI4Cz4QhIsIVCSSKkD6szxzfGjgOmDxO+9OgSZrIHvpSrATBB0xVhFu44Y3LS+5aIRHhSJ9aRBUIUyeNREkmn/ExJGJE+TzmuPHXgk+CBJHQgSRwQJI6KfJAdXyRCQ2kkQLCprEGB1QUyoA7lyNKgCGEymZnBzB9nxDgqI5iyettttttstttPopzznOnTjrm7F043dwmdO3E3dLudXHUG67nddM7tKHdHLicTMd10XbiXSvj3ltLUlsskrIxGKyQWMrbGJCKkXJAYqWNLkapFkW1YWQjbaqSSSFky0hSNuEyjbKtla5WtRkWWSMoowuQa4ySW222222SKDIyIrJCMREWOZAhBJFwOY4hORul0Lnd0Jm7uQTO7g4czp56ptu3mocpBO095vqIJqHeZEDsjaHEIdhFgwjIxSSMZkRZBkjMZJIjrp3duuYuzjkdMnXcSBEZCSCwRYyJCQZDIMyY5CLIMmJIeUaSzLFZWSRktyWI41kYRrkhCOVkmUlQpFhCsCVbbWYxlVEkYVtkkhLKtSSkxfF45066ddnXdc6ZFEJYlcydrumndBdpmcu7N0rgxYRZGBBkxkZFmDgcAww8YCSQ1zwQd5mbSEYFQjEhCO6AochUTPMyloi20GgbgRUTAgjjjYIFgjetIlZ1dpZJvURFNJaj5iBiESiDSeOMjbS7PK/NmaKtgAimeuFQ5kPbrzwaNjnz48SJJEVCEjohBSwiCBwy7A34ccFT68ej0X3B1leieij1nVkbwm5b4h3C+NQHiCGi05BbYEouYp7IahsJtOc6hRpw7BOicYG3NXXdu47UKhxBqp4hwbjXG31vUpud9vXsHtJJgWPLOvX2kznTj7Vikx35T93x9HHW+PWvN93GbYBACYQJSiSR6CxJIUiSJ56kU9bkQdfbkLvHXwkWb9g7BQc0Kwy7vPPfmY8x58A9uDusHl28hQNvgri2O3sAdgAdI7m2HAPHWSHkDuJ8Tru3ZsEsPY79SEu6IOvogSRtG3YZ36c8d9+5Zv3XtsQDCNpjAb8BWzQAXmgKLBUYABFBEii76TEQvhppuvhe2ag8hXeXIkiSIULHiomRJMsDvZ12MCkUkkoekYSRENoiSwe3mQ2lkIjw5bh0iFFtQ3URgWklUIoKkHHTqaLtxvrid+Jwaw56bA1JEqg/0wyDCNUhgUSe1dqR0MbQaWXKJahhTfMUO1bbZtBSyInawAiRxmDvnc93x53z6AKSxmYZHR1pjheaaRqDILwTOwAggwZm24zWHDbLbW7bAGPQtwRRAzy6r7BOqjNdW/PPRXgBtxjHKqYHJRSiREZD6kidJZYUiQKiogqSKBZIWREnaR2dpCEjiIsU0LyosLycMVmQBA2befHfkwBqdVMEQb5fXIR4XKw42a7AK5x5bwhtwB5RyvGiCYBv6dOnTip7GqSNhhmvNGjKcIxRIkOntgZohQW5YXRVUPYMwL3+PndZ8ps+LAI1ydRYIKaojy50oqG807d2/JsOG/TZjICOmc+vPqEPmHSIHcN8Dn289CSbvrkSfAYewUoUOodAz12I7eNwzkbhtIRzCOCI7A9qGBrdgS97gdoO0J55I2MO18TQbB4DpOQskDuHEyQdA7mzrsA2ESAdAFsR0tTcQPx56146YGGnBUmvEOtkTaEPJJMILJERHlIVBJJJPaEKIlD0HLc0R0Nw0Fj1ZEvrr048udZJFPW/GdQ4kjyGB2kk7hhsiKg0eMHt0kw5yNxNg4DpCUNaDAzXr0munD169TjCNhFChOA6hQ39PZrXtv44D0G3oM87G2iTMwN+OwTtrre1Cubc38eCBJHVHLtzzga54DuR5PIoMDQUOA2DYHoNh7BSJoKHkOgaDQdQpgaDDoHOENgpEwKJo5NByHIYBwFBsGg16VOATO3GogH5Xcr/chFQ4qorwNGOweQ/Tt7bB0kk4NiO50vgOAz161Q4IcXiqphA0VERICUgKrvQLD3G3sGg7jRuDsFB8Bsm4cGgopweZsiOngOQbB6279dHwHUw2PYih7jZI6h8nHbaSOvnPVdbbbbxmYmvUHeRE74HoJnHYLdPwuZ3xLedo8sBOsAaZ7ALBhuwBRHbv0kia4DfgPLmIb8pgY74Gxt30RCR5kihDfsHZBSKnmcUR0RRRSx5XrYhrETXbW5UCbr0OdgYG9DRGXyGgHmJOQ6cDZyo3Tfyvh0zz3V1VdNHqmHl2UIvQwEGEDDPdMgQmJ31vmSSSSTHo7VMXN34wwwKUQMkk+MCfAKE87BkE6dEtls0G8ApDoYTr7yWwuQdG/HSxbLZvJwGWRbjMMUxUyc2Le05bCYsbGMRvvXKp5drXr13pbG1c1zRjVFrxVFtblUai3jWuW2NRok1FqvG1y0V41y21zcqjUUVUVJtFUbbRWuXZYjUVSbavHSjVXOGo1SeK3KyW0lyuRFo1i0RsO625orWLJbFa9VNyotRWxqo216Vyi1RgC0RrWItQtSLGHEsKixCUoIKUiNKoAmqggpa4QigiJGILEOhAFEhihVtREU6UEUSIQQCKgcFBUTKqhI3kISKq6hYVEkKiSFZCTGKogoQWQkiUiSJVVVCgokhJ5sSEHmRIE3iwEsKSSLCpJDeohGhIChQCgoairuFVTWIIMiohIKowIgjIoqMIKiMiKgpqtaLY2o21sWjVFbChIqgFKoqVUUWQIRAIhBJZZKKiWSkUUWLFJ3sYbdxa622UtpSKo53ORhFJnLl3bqYyVLm6ZOW6u4Sq5s1bTa5RXa1TTbJAqC0jECICRaiSkWUiUo2RlmSJYihFQsRYc4IySy1ptmttK0xJJbJSZlo1rVKmyKltRURtbPF2rurtSWyUmbc3a2XN2qZaLWtU3NrrbVjdxzprptK0Sru5kZkiou7raABkkJNk3u2tvw7a1rTJokgkogskCShfRIIg5kkkJHSkHsQJI9oe6okhYSUiA+cjaHwsjZtCp3nLUjvBGjba1ktbVg1UW2Sqoxa1tdQWKyWtCBPj3LRBEYphIiTTWNblyKo0kRZd1rlBooMmxRsiXOS7u9rV1dDY0a0bFjSXjW7zV3SyWLRIbQaDUGu1KusovG25pMwzSuXKNY0mZFbypuY2IKNjVEFG3iq8svBk2QAybIRXjnddNcuTKSXd1055XbsplqLTc3CjARqS0UYijbNQxeK5aIKisaNiDRtGxBZkmgsVJtESEaCNk0aKjBoKKNYxkIu7sbGEKNo2gyESYxVBRajY2owTIIxBGu7tN3XZmUttlWRQ0YbSTDIifh/KFsBtFu1qt3yjz7Eb8K5RfQvh3q8nDCRLYQkTFWJMrllIwRxksW5VRHZBFJIkYIoR+O2gTFZJJCdkqKCIKc6hHFgkYuM5N6NN5ytlzml12mvV6sHrqLRhWXqcZIbztp10rBJDm3bsSLblqQVhJUIdbNbzeZpyZLbNGG7a9vL09D1yC1TkJY5NakYa7dakYOwqcQ4c0tZeWovJqR2713e9OtlQ5zq7YLgsYKLJIxk6tsY2Fy22XiSs15u2MdYlkLbS7DdhqFIbTSkNahFLIkdpMWyd05vOETZAx20lllSpKEqM2GuursyXGDF1ta4SW2YhMk2xeiHGsTiWRXsmgot1ChITBsIi2zM6nZK2RlhXFve2yG9s0RZJi4psbajCR5sK5qQSSKK1xbvObIXSanEgjSdVjZ3ZbtlhbKkqa1Y6xNbbui4s13VN1h0dsuZ4DMuZ4LxCYgqKCyitqNylCjTHCKiK4qgIwqjatcA60R1VIIigBSI7+DBWKxCJRSRbJajIkLGJYkqpAVWbarUsbalrVFUWLaXWWyqOZDlEgpCSQshshCR449zWwhEjOfYNgxE289A22hOgUMJ7h7buvcN4Q5EjpOwdYNykUOgMI+x3C7hwHQPG/J8wwN5v3xIHYNhgdzwGXNcEbowPAUKFg4wPJSG4TITxBtgTQYGj83i39w29eYjJEqSWWJZJRZCihZB9VtscsNd3KbdduXXVdbouus7rcIi5VxZOu6HJ2l0TFQHCZDvpUQQS+RPl69vj1khW+QQdR1HxHak3km0SiiWFsq2ZNZS0RqVHy+r6/rnH4nU7uF3iJwiSGDGiYbMpbFub5rVtDIioUUQJJzoKm4RUI0SMzGsqZkiD2ETMw3spoxIxidYGZwyXIYDxOYrmbHz7f9iz535muyS89Tx4SoqHoSYHqJXNB1zFVRGJwpBUqnqSmKuKKNcspppEVzXz58yek+Vit1VVl3bCjYpNxVG8u7xOUaI40VUsuGfV689TeK4PLVruxjRpM3xq0EivNt4SGK5MeAYBTcyzBFxwbduOOOohQnUJ09lJaAYBM6NDNsiDkDAHDQwMwsHXIrH1koYOWYEVEu6uGgIqNNMkALEqwRUbqQYBoCKjNqBdajrXWIguN2m/ldypU8JT6fd8+vh0SS7asu+duZI7ncxIOEURlvuzMl93OFhBBFTFHvx6+tr6MaxvMPI+E8RgCQmeULqbRSL6TIqKe46lF4Py3IuB2BhgJqhYmhpeJMFTNUknIUANsFVMFhg5lJZnUNFREpkhSSYwTwJM3MAm2YqkciuMhCOkg4pVNATKNHOGgaZqjsSOIHALLmFAdQ9fZ5k90+NjrsknM8I+YBGeMMMhB8ge7x4VBXz1LeB5s7K5F1Jp2rt5EiRu0stt4FxYcSXC23Q3mmrVHkbyZNtskIVWpkDDaZUqjIQqRGR4UOFNHBEJ0yIZmBhDzTom7pcjda4bguNrnTynfK/VzVtt90FDqkSROAfabSH1PDW3P233y+uTjr7eSDqFIoUjTPtDXUNdI7Bz09+3qSNyG4YFChmB1wYd2Z8+oYd4IdyKF4qGwCQjVaZgsDQBIwd56vnxRc1U+z77kra3p1zbWdaHjsFB7ZmZ5LnIOyI4MDZId0EoexR01ek0bhoObQ042DjOmdmtaXXZyGg5OQ0UMDAYFKDAYFOvQFKWxoGoTYGgwpBoKDAb9Q3Cg0huUsgb7JjUQpQ0F0SNBTBgdLd4YiRpagCIZJ4g8Plbmd2KABrAEDB4OA1CG7wFbjtCGUNiNrsDIOYIpsCuDc2QmhZ7HsHTYewbEYcx7dg7ag5gF57HYNiFCYXnfJ3DYLtuc8BwPjTupc0NePdqiGdCUh3aGiTNJaXp4IMba7bUrblqXWvjn369Xw2ByFDbA6mgeG4177htCT1JHadiNjKHxr2DAugvJgcd5gjihKRy6HQPe9QzPTzEQXpxrndw6OGc6aA3u52iKjxHbVicfHHf1m2pI8hQpQcxE7BQ4Dt208A2DEL2uA3CeJNjprgNwpDfgOuiexsHSSKRxA2iMxxIaoE4zsOACVJgbb4iNsxrMd41Pbdm7fuzpzQ64J4DYMGYFDDgFDQUFClKbmxoMNBsE2ChuUNztx22DpvXYOPTUbUO4U3OtOdtnLdnXhopCgEAQAcAFsRWVkinxPitpJQh0k64h0oT5mUSTNT7VsZACQHDCARbGDOBYdQsadB7G4bmbbSTLwBulIqxVuYGQSZt5kSdLCESOwFjAmIu+0HSZinXmwBmZm8zwvJ1cVNRXAkkielI8de4UU3jCO5oNBh7Gx1D2Nw2I20YbBgMDgTQdfV4iMpm3N7BWMNg8eOeB4DrxIkjZDehzQoUIbc256bk6xR5hhu5AG0HbGwYZghAGjjji+ekJtv2Dm9KeA4ChsVEM1l6B06wb0O/QiOm27A0G9DqaBeIBMoGgA40xNDO7NuS8NBrt4Iqg5hAkjIiIdXNzntqbamtgpEkTAtDQdJCEjJIjvVpbLYqtmW2SrFqjfOjmrlO6ubia3DmuauFWjajbbGi1GsW0WtG0Woo1UWNsVirRWxpLajaNaKA2NrRto1o1aKLaNtRRVouUbcKrRRqqLaNa3z619lqgTCaq0pIwq1krasFFJMxRBjbWtkBNDFogmTNhVAqxEoboSSR3kizVq5mLavd0J3dCd3ZO7s5zu26MzRGSucIiuchIyYDHN0pKSA3dxpMQUHOTd1umTESJuW6u9/nhPFyhlGTQwXdXMiEXduu67lcucic3TNzc5zM2yFhmwSaGGxju6OcCMmjJFzlpiEWZoxYwUYksSJMLGSik3LcsKGkxkiiSNgEpKNy6BBJiixCbEYmbnNFATMQUmndyoTEZMkUlBGJK7uimFc6TMUdOlGlKNy5IibQKV3dFJEWE0gm7upKXdcTUEJEGTaRmYwZrxcoyJqTBUDJKIiIxgNtRFilEUIQkxshoxKWNUYKJhQbRpkbFYsIUkFqNESWIqNG0RQUaZtFJpmxJoyZmowBaIZQpTMEVIVgxRJZLTKimRgSKYBsUamhoiIjGBklIWtGIpRCE1Jg2iMklRjYjElEhQRJpMkJsUZNkyYIKZBktJRESSQRRshZDMUIRIk1RRSaFMMwzBYSTEbFGyUiGCTRJRRqMRSYAplZTWNd12maYTDBpmINIMTJShSWIse/V77875vTYDRtFRWNIYtJiooNo2KCxbFRFBotijY0kSmCsaNRQUBYo2jGLGMmSxFhKSE2i1EaNaNoKMhk1iMwtFqNkMbaMmxG1GjWMUbFambFFFFRRqxqk2I1RiKSxY0baI0WLJaIogtGKiiITUbRpKjaMRFEWMVGsbYxgkxMsW2SRNBJRWC0WKiDYtdba8rt40RJJtiYmyWiyUGg2i8Y008vJNUQWNkxG0RjadrJto2xotiNgxaefN771a3xjkOQ7kMc/EGB3kT4BzHAPd2gwLoLMjAstnDPaRE2317IZ0DZAuBxtanS7EPNdw5NyMLB1C74FDsdOgdQUB2sScSEJHVA2hI0F6kUJuHQ4gIjwfD04Dq5dkQk7h4O++uvjijptuEtEjpQPjk455DYNwxKFLIR0QdHXZyDzOR2DAoYHgKHgG4dvHhd+l7G4DQF8HQXq2BrPf4mNXlkAKZJBhrAKrE5AETcZYiqqIdsUGtFABUoBJgGwChUPUabjexOHK+ohuw3dZl1ZBVTSKZmGQ/tfmqqZi89/Y32DADT/L5ghmfOxwGm3QehuMJnFqrZZMk8BhUchHsiqdfO84EDZt5bdzu0UN3MVVYhx32dvAMInLZDnv7+dvMN+U57J8qN6AZgZghtUAOHb4eccKx5XUBPd9w2+t23bDPTs5XnPOYzz38GBQMKBN5FFUSDoWG/W9yBeKVxkC1Ec4hEExUwVLCB0SJQRukkkgaILISQiaQhIwiWJEOsd6hKDpPsgCmHh9siyYnx2+5fZ0Gh33MkGLkcjmPWExcE773TFxNyZCY8y5KURRWjfd6HhbFWrRQWJLFtLFYUMmaNMRKTDNMWNaECWElGjINkimQohIkKJqYUhYGMU0EpTTSYGlFNCSZJUll+n+4jWS0zaDGNlNpJNSZjJkSWWWpLIk2NmqZGCjJiMkpQZFFiMKCsGxBKWRIlCYbQNQSUaMEieVastVV5rVrzbbVeW10kyKGZEYSGTIom1NCZDMGIkRopSGCwMaYoUkSzYxITNISSwsSWCIYszebyNBI0RlqUgUmAzaCRFEKREDKiUNjY0kpUgmwZNkYERZaljUVEamxEKZSYwhNoImZoxCWZiIwJIBZTNLx3POTavhtXyvZJbAveQ2JvCzUTxbnyaoL7oUORtoJnv0aklPd01CSPq236hHTcwKKHLJBlt3DdrWgvsQBUwKh0m9pDiKNCCCmBFA3qb88BHMOipIUkjpoiaSRRFggTmSXak7UzbMp0byRCR5dCbbYSTdCHClKiLJUiwskLEWFhYgIxCSSOwkkHaSRUTLsVVBTIIZNBoVIAltCiRgVFklkFhZJIohZVWRtJmtWaSy1VpalkrbVLEpbIFSIqJLCEVLCkgrZaKigHeZKQUBAmFaVUGNLJEEGrECSKqIII469u+tUnfcEQU51L57OMEMCGO09O/kmBoa/pQiZEi7vPnsqLgrnHJrT5S+MQ5ERZMDMJbt8GhNlI8FPaSRJHvRAkjCkgiCmZEIhJbIsnhFZI2qRlkfZmIQkWkISNknW2rVowkSIFBUISPPy/evzv9w7+99nQbeWQzKrFmLyXjNbxFSNRCECRZABICeq4aMB0VQJUYSSSZgvK8kb5V4JnfqL741JLMMMdHxMUtjRMswEd73oA4AF2AcGpzc98LPefC0gKoCADN9dedFBYmGieJbaWkUIiRFsIiJVdpCEjdN3i9xJ6cR5qR1seXOvWwbbXtXgOh7em3LuRnLa6mlh3Jq679fMziQocjUsHjrJdCoKm3QBQUKFQGRZIx4st6N1VVY1Zb6e3J0daht7NnjbeZ47USSSIb2O14CWkZIefAb/oTJNINms79a9XatSkfcDJAulsGnxFezc6A0CuTONJnldYUdlYsa1qqzM3QJNsRge5gmS+fc47debq7dO3cnfj2QhIup3Tw1xx368jO/hyA4a6PETr59XFReMii9WtCEtVUBxYClCCqmESKiqhc6Om8hI2LJE9wsEjiniyQNglClFpHrt20hNnZrQHJQ2K5pGikO6hNrIjahLNTiRBsAICIokCuHIVVUQ2G9XVVfEk5kSyxEkJSFRAPs37R0KUrgJDEMqCCkAzAKVUFI9MaURFIbAS0Pv5/SG3+efxSR0/OX/Tj8N2L/drXpxuR/3f2v+++1cuh3xbif3TwR/bGJjr7UVyS6Jx4b+h99q7Cw/j/LRicaPP2mCaYbi5/mP7fp+U9v4e2dx1np1fQm9dens84uOX67cd2nSW7yET58zrRmoj+WieYm3ex2nPjy91/41O6DBXDu4kPkodz2Skkf4B/iFv4xuqYKMk+Y2ek4Cnix4RPj93ljn/f5Xx2SCdsHWLrBC9FFRkkz+52PH2uY2uykUsOXOWIdB7nHYEgw1nrAiqIr9/2pfYaWwXWAPiBme5ML6tvvsrLPS8Sl7clI+l7sHFyJ5s0EfBGhR/gnrTJClGzl03SEOhM6RfypG9ohxv2md+jA57tp7V+scp0hB+RKre7UHF8X1/Yi2jt9TlLNepNLztzlaa3c4c/QmKU2J1bCP3ScU7QVprxlu+Ltzxrhf89IlN/lKkFYyZZT6eEkvdaMXH6dR/XY/vldfR4zEfJ39OspTL8uDw6qNy3ZNnW80I/a59f3pIQ47O9kc7GCv1iD0NAf0/7ISzMUM/jEz9xCyGLEBczJZH94hAK6YoRcAYKAJFVogoVBSoKYjJW3Kxq3Lmnjq1Ft+ZsbXpAXExEkW4hcRkFG4IVBVslW6eYQAQmGEmA8zDEPb5pEhNrOnZajO17l2hZl0lcMEXzwaQmahlTY8PS13nNYsfQYUybPXCrE66fQ17fz8KtY/nawm4jM2/v2NcipG5ZTHm47o0RH0Wdz+eNRFvqHotyKeWup9k81xDzeo/wL5F8bNX1ojc72ahVLO47rZkOTEj5895anLgl1KiFMN8fZzwuc5eOW6Ezc85vNSaYvXYVRX++Ho4x3PnQw6SKHo7PVvYsJsxqowUgzk2BtGtNqRL01ylvS+/lumCdwg4RdHt9eylonv+7R21j8YajmCVoXHfFEp57vvJXUEs0xlYxPY8mhqOZStMclqO+1xGJLyWg8wwk20OXSwUu9Q00S743ez1B/H393T+jyYCjbqdE/eltECOTaaEYxjRjE91BI9V9PqajMqFCU+a3P+L35f50+H6f2jPnzwsXZGrLkwJDfcWfyCfd/+H2/n/N+1/0f8f6zfv/X409aQ5wOgwEr09dHccEd//Jd7V52SEhNp0tHoRa8G4UUf0f5UB6dn7L2n9M05O134rfMP0O+PnELDYXP6LUfpLZDfmx7K0HHA3Nw0SJP+V/s/UPhsHAecj3O53wGQx+P2feLWHd0CSSXyp4Q+9mFcjwBByYcHAhvuH9O/x9D+S9kxycj8CjcSA9aS5XH+eam3tyvZYh45yZsi4/KrHA4MwFuw4nKdCYUDkqErgQ2WQWrn44bP5dzhkEyJ+l27amMZ1f2PMO1nNzT94QZbeuAPXQ1bQ/6hPsa1ubSagVHR6/Y3oCRJCQwJadmpHsSCA/y9JXh6wSY2XmzYX0SQCEyPAfiIsKWxrALwZuZqdk2H+ZyymNEO/kFuITEhJ5xPZE6OLKO7z/Q/SvL6A5Z1OB/dvPOHMpYYTaSeoK5nY/9/pPmjnYwNqhKRUfpFUVfdfGnqJe9y58M6M1CnRM+u2blzNUnj8FPQGiofAYSEBhFhEIDDGPg7Ht5P4xETNH8ezVmP2eJ3t7TwYnaciR6qfE77fNfd+G6v6q+q+2e/eflAKKMAAhD0BAxBkEfMEo+FRmZIndg3Hrr83j8RNXPLNN8JczlMpK47gd0at/Bw1R/vXdNcGZjcc5rYqvY0b4aFsSy36Po+AcX35wFVSJPQtPVv073tmpzIxCMSdodpsO7GyfUEHWO7+D4Wn5I5BIEbxvBnduoOhxx/lBGbJJC+CgPo4cXN6Cbb4ah65dy5Fze4i4tLaTBrp3L7/1t//G8TMkJNU0C69RtuMJgTD0kOx+8NgJqxh9fHSqo1Lu1nl2ZcDCJjyrS8FwgSfjgFXStBEqc5orTe26cPTG621/fPWv0G00yIYYX2TwP4f1AJi9Ce5siqqrlTsMfAYessteUpnyTwDNYkIvrqlnM9pV7NfcdBhzAHTPudxIKH4xqI34Md37h2cR2smhf0UOFlENt++25Q3yhe/5KY/C2tkELtWlcwV+ziZp/3aloBRrmYwWP6R035bh89bV1OuP7n/hwvt7QpohwYlis3AEwHETI1PvCHzsoovyJ87kJISP7hHujTR7y3Tbs0O2Gf7dTzRX+0fU3zZrlQW8bSkUHYOH/Y78PmfGF3Zkubdyyf9vj79b5/z4ZIXg7u4geYZqyMBBCp++++T+VIJGg7i3aDqQ9484XZKI6/n75i02r3HCPtg/OrZNU/6SHb+jJg70x4fjA2mh17g78zh0me2ueiK1Z++662YZGiHHTrzZP+X4n8IBIZIJAtewt8dN7CEctONt7+C+VbTdxw4vzdQ4epV6F5V6w8xNYYqvZ1InayQOonbG6o7osumMjJKlEYkA7omrZWHQ+W8Rlsf9f9SiqOp/jqcDfmsMxk7ctZ9Pz/rOFaYWQ/Tmj/ObiTszaoZ0kurs6yiPbLwXZg/xn4Qk9wjSURL21vlP/xr30+376XoTXOxsqJmpN/UzfomVl+bCVmfjc9XF+dHuxvS1Uv3Jvlhi+jVKmHy+a6dmVyqq5UvkTJoSVV0X5423uW1414+5umU3Mba+TFoI/W8rTfQ49qUe6/xR1Vn9qOqrAtP/fzmH7X0r1UaX2y9053Dzbv6WhVMUe/6ODGG4WXuT26qZfdrEV6p8vvf+yjzxVk6JL5TlyugjGhu33J9pVSEHpFpJpio4ulAvrWbTz3qipI6e6rqZt1St1xmRmY/CPW03zWwCEkrcbGSRDj28R9EObp6XMPTpqSQL+mZlPqC/8D+2+Id71w+7PC+KKXrU6PVVfZQy9jyUUdsE6Zc3PfvE7aJgpQKg7qWFxCm5C6+L/753We6cs7x5ISFuo9LLBS7/ttvkOGnduXeEdL3viBzrIbt5g1DsTDOjrPoicFkYz539ay+aezQ9BFB9J49OubD+h4js0r1JfD3sXPnk4Md+iZvySev86nZ2bTnqhW8lLOCXUPZuYtzmNfCVU+EE7SM6UyiVx1+iJ2u9e3xtSPT7yE9BNv2E8pqc+O5XFGyCp3DtCEn18pG21O1EhMlfad0y7R/ZYzKvrdQnKUWIOFdvWJrh34gtYhyh66ghpuOfVQ1ZTy77PDpc5GyKLcQ7LOuUn+tJ0Z69W/Y763a9/88Vd8YwnypgsTi1UyS/1WZHVpW9EdalXTWHVKxVQhFt/AvnMFQeIdWW6zqTrKzwlf4e7+adaMgSUHEGvyj0v3+75UU2V4j7IZ0kkp363MFs8bWTVRHrtC7HHxV8uUyc5oh+UvZHKhSJXyCsfTdet+ihVz3QgiPp5SfcdFPubKouJyHTR+MkXFz69hx676L6zplJ92THZ7vBNTH0jIKOIOKfk2VPEw6swVLNHCuZ2Qh9TrUUdfltueWn15ecKcfzwyFfuN8J6Jz3p4xirvLuecOSQ+3TdElHKJ4OTQuDsz7nZxdUrvmz9azbtmpOk3tRFzBPtDXM9xJPTuleWuAiov22Ao6zkzy+mkBHBRD7Py3PKBqLiqucf3mWAvrPiE89z1d6D1bn7WaPgl/HyVxXjNKJ8u3WnO4jZLCYyOpmk6ED74EMakw41Kp1kIq3z9mTdcRHwjCGmlo5JKHHMdIjwd0Ni4/K/2a36UCcPnYcYSRhMlVKSDyKKi9unA7yuPL7QkJEkEJ7F6wcSEhwQkigU7BzY8XIKGF5I8kx53P9KyEUwnEFkknuB6EyGd/HOT/2+iNVO7jd758l81f9a1JOjK+IJI/BYYO1Ec1XxckksHlHmnO+n9kjgsN+kWh23IzQ0JsdXLILvF2mmnTygL0Epu7uaX743FpcXnwoy3xFr6M1dUe6H3Xa/gjixPaME3uWc+kE4X1QPWWkSMU+j2TOjC9y5571JvVN6YJwuo/ydzZFkNUWT4xZJ7r8JHOx7PB2+kwlKIwqmEqJvtjrGEfFU2zp9G1py2TNbxf0c6QFyx5+6C9XvlPGQhNP2udPLfDT6NK8kS+DhHe4SQiF58HJBuiVGQ80Ou+XkdmR0j1eiv5Iv1wHMTMV4xmPDTsx8ETWzxq9EpzlLr4UlSfxftk8f+kdVIlXugssHfrN0votEqTllE+vC6irIYdlf9Pfn4cu5TrZyuz+dSlkUwa02kdZlea5wx6ffKO2HsmqdrlMJRPzx/tzcup7Z19wWmcCRtvjeTCkEKhy3ZSXWcem4uONJlTEVMMK0JzG2PcSainNtmhvXIdSOT5L3T5KE/GrShwy6Ok3Rf2/lkWbJEzKNiC/qxz+HYPwui/Nwu7/DhS6n6+3/tKmNL4e3j33d9OHb22P5p81S7Apjz0rZUXGMHlVZ//v3Ya+GGH450MPRQ2Cqb1Cdt69Xb69iTUJv8XiZ+p/qum6dDWaz1fTm/tN5S5IKcdjvsduIULif4f/X57TEUbSycNcnEJ0kBF32r+jhKQ0n5jiP4J9ncLfubqP2n86/b0vlNq6vHcxIYNz4vUFC8Trp0YSSQOUZ9Nb4Ven6MagHVPmzKDAFw0hSxCItsKCa3erIRuIZgfL9wicoBGdMDsKpv/YQoIhFAuMItSaz8k2flAnBeIn4tlSmxkRyY4zxmiN42N/70/zHKJ/DwBoNDe5X4cNaBeedCnpMWgKZM1jtcQmM2lLUWWFLQfoNRMLsNBrR0oPnNweZiPllQ67xbItXynU+P2Hh/Gnv6jt9rlPPV4muqeBpIyQJIc+dIVBDYFewCfLJFwhIY2INKBUY+IyDkwYByntxDExEq7CQy0DzLjgl7swQfoBVIyNn9p/DptDLA5DL8X5TT5+j7HYq+xGXLkkkIDcIzPG+KikNknzZOwkP05y81Kegf4Ht4Uwx3jTR0a0nPxsbjywv4YmcV9D6or6Cfm7bPaR9PfF4WHBhTTLNB6xe8fKxz5itQTqIekohW4iT3PifHrqt5MyWqsvDt9J1LBbosjDD/J2SY7Tt+rRPg25NdfVtq16pvzvE9FNhMB9GRPNtMWpUJEKeDuSjaGF64oZVcD5t5tU+LDA7APYbu3cHAN242p4yt4PAqU0KslL4Sn5vZ9hunRZI+Xav7V0fQ69DZofuqG6HeSvbi4wX8EUWGgaUZthjQYmFJSoMo2CA2GTd2btyDYLto569yHDe88uurIVkeXm4PnJ2pmpUGk6o/8yBnE2okhnCwnrDEKD9/PHzWWST2TTU+1yZkuRk68Ny9cy6j1hu0Ciknkay0tyJQe0KPexN0CRPBm/+g2J5ER8fnk1jVTAcvS6RyluV6KODiuCjDocBRE/AB90DYej0h06RIibgOwMFurGMUybZqlh8RAatq7eMEIxlYRXzu7AgLGYgoJhsnfBxnEIyTNFECBtOihLgIeuAYb7Z+H+n5bNPqbJbWM+vEgbpCQi58gP1tfmESAMVm2VTU2ypqpqytms1mVtlZTazbSrNrTWVltKyptqalVLNabKypayzWyltNmrSzSiyQlJM0WDSJpFm0GKU2DMpDCQ0k2ixGYlimLLSJBUFJmI1+bSugPqyZ/hLorGWJnNKWqixFYEYRkyNDDMk1IpEspmUymwtJNMk0DMqb2XXt5w4zjXBwnDW3G7cZUMkkiyQKiBZK+otdADCtfN9BSYIhqpBBU2gLFCxQWJYj1bgaDoEyNlAz1XYoERASIAuKV6PkjSqoYBSBFWDBAiAEFgixU/04W2UkM6+v1bA59YdPqxBt2+dup4BHaZXwFCkFCe2jYo82e8iIkc995QDvpNIDUTKggpw2tWqbdTZY9MDfHbFN029FBwVFFMlOkdANtG6IOLPiyR4p1+MmqInysc0dREKt6PWHYpEeEiRIpva4Iyasea1L0qOyEJFJBafKx0LG9UZcZUkhXfI3siSObPUikg4bY4UhI6e2aVLbS1us8TMkjAy5E9q9iAKkXkogTjEFUdILzibKoAAXnyevbtXpXzNbVtrpbFvlAylMAKr6W23jEW17tLWvo+yr7fyd4Q75CaOyCgJIgCvmRglREkGRAaEiXHU+IpTBB1OhdDB8R2U0vn8ayuMnnu4oYNXRd2GMRNxwVAWLIMhIpYk2p++OYuHhkVJrEZlrUPzfRH+S1Ui2EVYlsBki9+J/SxyUSKQgKWbz+bHz/2ugP+SoR8XV2UtHVQtgdz7eont7bQ/Yh73BfvZ6MJ928Bx9+v7L1ifk8N/Dt+Surq7vt7y8lLmq7GNExtlPy7banaijV2dfjvK171PrCRfmfkqQ2iSJFCdB0sKlWrcjbeZdh4t79KvKSX6N7a1Go9yqgpBFKIhJ80qIlIRICpAESKCQiKsgqnhQR96gWIDqPmSRiIniT8jvVpn8TyWUqtrMr4LAauVKma7rjK77/9Pu17t8LFRAcf1zJ/ECFUXCEwz0vySEEEnczFqHy2EaqT7No8kf5VfOOeHS3F4TEwu/kOm/i/lBpBySTozTbDS/k3JhvSBzLWEzpRMbKdCF4FchRi6xQOoUZ/I9fvk62iBs1pSfPTjz+36PjMZJ3wLjsYYNL2tNVdb+QYso4lFM5NQmrF1LPUeCcEiq4Qx1qCf6QCw7EnYSMnvI2kIMe9NQL9D8Od/Ple01q5FjUbb/PbuyZmEbKChxaFTbtV6SiEYpBh4oU1GH1OSU3wzkgKRO0E6cjgghCJqQQ/kIEnm/NdsnogPz5pP64js2eIk3hB4Mjg+MtLYWD4wj0HezaHS6iUG4kB3ekhe0JDMVIwCSA/nN6dGXBPUKTBbRpc4PccRD1rzUxAkAeZVGodCnmOh9siUcFflbUb2SqnHfR8nvu/11Ur5PmdX+G2iAd8ayppGSRiTrIaAknq6/wKP89iBE09iw0e0OgB+4Io9IxSCSIMrL8UTgvQVKSoyPVAdQ1SIc98SQA3Gp8UXkztSKFpESliFxKlBHSOJabgMqv48Pdsh0NVRJGJGJ5IlttVKlBEaGLFScSSPzT+qsQa4T2ffAsotRgpIAQSIpADaeM9f7uOPuz24gdAXBZASSR+5hUbDZsxuZiDSnfQVw6oFRFXSzFT71jWoj9JrR8QypMsi0HVR68k0acwyKMDZg8X58zyLwfHRJvhRFE1Dkqnx9F3qnNhWLKbgiB2IvFYgIInCdNGtSb7Y4bnJqMLE4kjobw0mlEPjVA/TltA7NQvxFu3RTmGqZMGkJABkWFnAX1wkRRkHU4/NBLDTam7fwktHNSxUg56Jr53VblGHEklkrh2xhMFEyOsV6i1Q2VdIakNh61WAfIDsHT7f6/zsWM/abT81T19x3VC2MwaepzT8QReYh+KI4Ie7Eg/YVYHWCZcmA8OPq80jtbvr0nCk1dPubyVl+r9X4ffDNhmB/KKU6ZY5FL8l5Yxv0QxkM2JFNCuWD85y/PCQ5Fn64wI0OP2lloWRG8OEscWWbvbhFUQU2LFIQTls/xNCQmD8RvpIiJPqkEFICgA1BVTVUEgjprQcv5f7uWx7Zofbw/isFEU/7d/WCm0SIio7PmnRymznXFQCzjp16BR1AgCpMSuq52XWmR6NKxCgkRHn14QGpAlkgQsiCWIFQkk7+NHPScVPneueflt4audl/Wvkh6IEkWe0G4Xps6O/R60+CBJF06h4dBISGu0VxYhzThzk6H3/Ixu1N4yEib9kvDvhL9d0wqGw0F6S8V+2rt/YTdyrHqEvzCwP0Qln1D72MwSmiBKChf9bCubz+n+bTeWqV+Au786/eJCQpqvwR+oKDCggoSJ8qZRTvmAwKtfyAThEHBHQRi0mQQBUnrFA9qgnUWfrKHMR+lx/8XmNkpbgsMHX7eAH6hrBZMJJha9rcUKUW+zO8bksyy1fNrb19xvzl+4fNuzNvoH3CGNVgafp67+qfUGi+4ehRPgfYdxrAnESdmxE1sdJ3RQ1Z6CQj8m76TeIu5XxE5FLWCbSsOfl89W+xSKIiVpFrtkWU8Asgf1FBsPZ9nwlB8IMeOPqM/YYW4k0NJiijFlmMZbsXqox9uPl8yoCwyUUyLJkDxW7VQtRUIjPtoDM0g5eVHZHjilGt16PE6EG5BQoUKIolCADhwgyYjpis8s8hcAFQNmwpQVTfpyjZH79ONoddXpECSO/r07cLuQFTOgBdhSiVhpUxAQwCAKkUTU8yMiSK8WSBI87NgHdjxv378RG8jCY5d2SG3YgSRgxQc5PE2IEkaQhIp42OnGeB7PH7a+r7ld92HYYDhtICAKnl+mPnJU8vywu2pAP+9bkecW4yaoXt2rxW8ap+bamFUWdu9tU9NNpZtGMknSxsulsOMqyWwuJ+Enu5cWcrVtMzJqdFqwjx4ggCpy3e711+aAaaKQioApNfzZrMht8PoENeCQPlYR7ymqxA54pIMZEcVEoIXCqt5zAEMQoSqcDBfPZWE3WXsf3BJ03jgVSrZbaRtzHZlZE8ZQ5qfEgC8kVfh3nvTVidJxHbO+EZMp5iBhVQin02HUN0fdEKPs0B+je4O7f1vX8z7z7dzW6naqtoqz41oXWWUslpS0jQwgQVIhgRjdj/VDd9O9R/ziWHHp6fnCdvQaUWinVZr7EKonantfZepHKkg+BS/nhZHNffY5LrmeaCMydYo/jDJ37/vs8CKU7cwjM1FxJMll5cIZIhZH7CCUlydHWawTCnF91aiRNJSw0ugifE/ouGRngZHAn6jVEogOg/klpCKxYmalHb6KMETBoKnlUR/3wMAPYH/6GQ+jB/0+FbhYcyUhyCqo2lruOEQSqaPaGGmgaf4fzzGMMslfnpimZYZZS2IWpBiU61NrKfn9mGWSWCFoFlgHuCUGA94XasWRR1Q6fgYjmkNR+5q3SMBN5o/Mmkpom2nVEpCVCZRiA0RJDZn8w8B7ujoOYSFaTcCwVzCVErtNKd2VxBiuG0K4UIxhec056bM4F2g44nlOZxhBHvlDUvcwhEgRDYRkRuTM5YgJiZthl2FsUolUwCii6piuYOzKFJAjHJmpdfb7jubhTMhdYx6sMxxF7lsXNk6Og4xLjoSBRlCIjNetyIbzXX0kDhK7uyC8POlqT+COX0pEjoUnik2zwlIJh9yOzUiQENKB2Yr3fYuOj/vMJBC/N+Hyf63/BMj8GG/CdKJwgo4TYmh4lMgpKYxNs3u7ffjakTa/zvufySHbrC1K/oDqHz8fzfydPHlOocwPIeddq9tjtI1L7aDoCJZg5nlQr08drUc+VxXlbzAd40pDiVT+euvOgKBLAOU8dRm78XgMASmAA8QMw3XlVQU2Iog6wUFBs0a5NAm7bQJfHXaz1vBzpJOEF9kISPXY52jmSROyCSBRIiKSQRFSIRKWFspEskhJFkBJFRC0gGJQYBu1fopOMnGE47B0gfo7riVYqSIUpMPlThNSBlCOyenzVNKNmv0hYGATB2RydajdKm1jUYYmoyL3eI0m0fbwd3qs+Efm6Lt02QPdLdhBpiGJUun3vqeqBCZkH3hiOH2YqZ7KE4l+ajiHDlkMeC8HtYEbrPGIXEGhcwUMHhsecBRMIvTgx26jzA/G1JCGUasICLcTPnm7uFQmZkSlKsmU0lmhciRJFRlmCw1ZaYg0UwxjXHlYp+i53pksfO8YHfn5godE+bDAf0wzJHuo5Pjc1WMuyk8vHkFBKLzpzuJ3W5ndAkqnvwH6o/ZqU+RDsieJy3R5EOqwhQZR+v3X8FMSWwVwfjEmo3PmfVjy8/R/uNjsfJrq76/dNZceSBOEeJ5TxVPcKph1j8XOuqLoCSJz9AayYoO7pIeH7kZwauOEUQw54A/4M2x0Nl1Igo45JEICGqzHqPxQ9EH0iZPTvanqiVH0KuAr1wN6RLX18FPXqgaH6ym2/+ef83zdo8fp+3T7lbH7hmLS0o0ssNzAafDxs2kDyUUj7SL2zYbR1tMdeTxE+EyD9YrlBd0pSZCAfDt8P3C4Uf6omzKbSkNuAioYQwWu8MG4huNTRNOPdO/9Xr8Z9sxU3H9HNPH/Ks0lbM5/PnBVoYsUw/AkjKah+2SFm6ImCd9kjnRnE+l7H2b7GTfJ0VhT26ZO6zGwdul1l3LmKIRLsYiEcadiFJpyELTsOI5Z9m1xeqWMnwjqOWZopzHP2OuhVVHlE8/e4uuHijNHBskrRyFi48LwQOIMOb+/e+jpIlYWO/VzOOj+yGH4MhHSMFhgOF5i5pg6jTKhYjKQV/qzdmMMx2LYWgwTbrwFQQOW+xBrxAShtFqk6NhJqKcTtaaEN2mZ9uIWIlMdIb+qrNeXrOZppCHsgpNfBUeOrheg9tGtTJO8nxcv7G3FjOmEVAnW9DmLv2I0Vgtp+iQxQpEkkHDun09FUZwYboo76WXnPCTcINprPV8yNLJo737ks1t36+E4Tod0JOA8MuTNpHTt3o3Zo0WbcyTo3vUjINccyBsO6aZE6syWBrnk5wo5TGPA+ZBxLPbcoclnE7kvdnPhnJsqXSzOSuAMJFN+ekzhs5PGwN0V6YO1NdXuCDlpnwUofy11ElucIgydThr1nMlCERMW0dmZS8CO66V8TWjqjgjgTOR7OEWcubKHxq7cfQW8sahNbjiGQ4KesCRXxxi9aIPaOPVcFm1BQUCZo2xtufcvPu8bIt1UePUXc17MHt8KN16W/LsUo0wPyKF5K5biuDeFqEcJ9vbQNsi3EEnU82OJhSmYcQ6IHdqcNE0pqO6OvpRkJ6+uzfFPGw8dS9bzYom40lNjba07UY8Q1AKOEw7Wbggy3WDkhGFSYXZZMQ0O+SXbIHaygsRDniTm31nXW4mm6h0W9JAJMo6fkt5Gvp8FMXcNl0uMq1K/JWt6jKWp1vcEEvBqSuYgk9I8Q7ArBPvl2X8ffjrV8anT4mIAiHNy6dVDpncxPjuk0TKFRSdW651WqN9yjvxUrisicOnvFXT8NvYpoe3bIfUPopCf6VOXxMay91VO5XG7em2rQac4RDDiZP5gaDJUKuJ8TtwewWk+WOQPSUQQtRqCWLT6q9hU3AKS84J16U2zcgS+Z0vGQZ5bXUt6pNh1ShMIVu87x4En/wjLyyb+221AJGefeNqHcdW8Gee58SztMK2DgbJi/FnJAhRUkqXIeJOR5g9eOriNKJ0MW2FMONTNPucnA2gVDYtJ0NKW1K7zwXfezcyQ52cO/DhNPZsDaH0fcT8h2P3reJCQ+yqD7CYJCFtYtyY03VGfuO4kSLhjGBGft/D9G4/mpOvjjynXuFI4kpBhChSRGe30Xv64kjjhx1OZPyGFTgTzQOkkUO5iuv4aUbbEHU9ogQabY3SaYFybWKVtKQXlwwoHMgo46SlUQCIcypFDAUju055zvnq+OntqSSEjiK5yEm1B9iiC0GBUkhbD26SIiR1JEIOog8OOC8N+uiS9/fhJIae6STJJMLIbh20RfZbeZvtKb6q1atyNMQ3ZHXg+3jzh0xNh1CFC7lAhJ4E4mflpdSXTrQomeR82YN9fO8nKrpykbUR8WDnJMzYXxNHxYV3ZzZ69z7m8PKb25uIvpCHgcehx3kc0dQxeP9xBx1pO783BDgiOgkTx0xRxLyJ0fajWnA03A6bGxamgE2+Y00r0K8tHc+88p27d+hpW3rG66mea402lt8TWCbIQrQnrTicn+JteEpXhVm54jiwlHCvLjSXp+NKnf13S1dO45dLbw8cbuJ5fKd1ZcxuNm5B4iJHflPzse6jjNHPOOiPxubhU+W2f7vDd1rZqSrM9W8JqctxvY5wXEMVKq2NbpuR3PidI0mHdNxzHBVpUsY8zom6TzN5gqUirQ7mFfCnVLU7SdY5O0kackmxN02OW0kYbJoaNzNGI1+MycyjTy6po7AyQgUu0IOBCK4IG3eF9qeSnXorjpaPyshdZak1bSolJOlDx4m/9l6XABkflT4w30EfvWJtyOhyE6Rdw8H/yCOh+UzQedD10HidAdQo7B2mteZU/hiGLkUhMS0kUy+a8r3/5N+Bei7O/QjhGBGKF0lRwQUjYXEJAnLgYh5ZMad+BnYShYfkCGhp/R/lfRh+wsnqX1JbkA9kn46HJ+hn1owFFBS+RB3rE1CZ2kkRRz0HCieTaRDzuG2YooiEzm1kqOIh3eibbc1nZgGhnIh6TdhucJQPWG2lqYkeIdg4mkanjK4MAdMLoJjtWxuZhGOZptqbdSd3ikgSzoOyFew3KCnlwF4qWB18RRBON7B4PEeRfGEinS9hQmidm8suO0gn34/jtU8DuOIa6HVQ2oIhp95n826noO85yuXqMD6LLFP7Qg70IbFP6YjIr+V3oHETifh+nHSPpDfE5Tm+CBI8imFEZSEkCAOfUPidZ4mjpqmjqjrCQUZEMdLrQ6za7MUQu5AKbEKp3xoC8FNPs0xFpJa4yxhVkKsssMU41MKs0xjBCoK13gTaer8j9fA2Am6Eh+u0/glSuujaE2sAjt/b7QAWQNozB5syLCN5x5YnxY2JcEc+DPPBUpS/qQMOLh1MZpcz2AmRobHNhvZ9AYPR9T4H309CBJglDD/ENs25bj1KnNmV0/Jx+2z6dbe8cJYh99tIFsgHFdebgGag1hsPUFQa4TF3QXooej2T60c8mzE0+c+imykolhs634+8K9Q2He/MXc5t3Q3hCHZlHMkX1D6MpjrwKU9AlPzeS6mk64RvSkDHXJAb96BjDNtAcGBMsEiK05ePmckLFEsHRyEG3DAR0AHzY/ExwaUgrt2FRyITQdpFcsTev9Fk2HLYJgdSKm+d1NPqKah0NROmyjLRBgmINVYdcE8fr1utF9FkqgJCmSmSMKqlDV2dh0vTxOswlTQ0kasTmj/WQROkkVQ4MK9oZpIH38/HBjssqZdJeS+mtynWpDX3Umiaicy4smCBPosIERyQEc9wYdO3JuYP3Mx1ohpDrCyJnl/u2OLrvpY0cEJpdxOz2mdJJNHRT9Pf+nLUG8SOs3FsjytY8mQ5ku6Dhfmg56QdR+TcpG3jgG2YOzGGDYw0mVLK4eLni80vUyUhnOrIz1sI6Gieyzy9lY57esdG+t+ubN0kyYpnb7YSRYEY7l3mxGyCOLIcV1qZUqrYFYlDPOx/HX25nRPNDoxUiXFpItYWwu4CSKBIYYlWA0sVkiBBP04tHCRRNm6l9fyUbFiaqCadK2LbhoahhD3LIRYRgkkgRUhFQ/HpwaGHIhGUVj4f+lhkPmKY4b40WJUVZ81y2TlZgu1kN9sDCCFkAeZfYKWfGEbQMd+dMne5F/qISMAIrInQLDGYPtr0W/3eLg6SHXBYAdkRQkEYzRMvnLA/F1HhE/iIFEPKP8QQZBxDmW18nb4/Jz4aOkoNIGiP9se1TCeYh9MXV2BxCUdBzH60/H/2/3Voms3r9hveH2Gai4PrWlxhMXFXGLMfPJWMTLGfB+ElirVVD9QKyxw2kyH4yPFK/5tKMgifUe3wjt1v5KPlUEEPoPqArz+Pj3Yr8+P4eMk5jn0ki4jJWMKxWLZVlsZkhJCSe5gVJghEAYYZFdkA+S8d6ar13W1xNMudK2uJtubll0V2xS7tuQpqs1ISEt26647udVXV/yeOUa6mpkiSTIg4UkMrlHCpMTIH3ufZHli0XEYDBcc/S2n7pZ+mMJnwD5/oPXLeuHu74ye1iOthliFuss1bbfbZG1RzQdcALnaRELgisggmYqEIKhyNTYtiAZgLx7cf19NX6tE/hD+T8IH4H8mP3hUrBMFS284MZMuc5MxXP+zHSAqL+BUPQBfesQOBZ/A5ZmZjj/S/sgm+OY54N44xqDnOuM7R1Zh0DFIjrktVbemZ/VeNpERI7yF0jlYHNGBFzuxeAQ88I0znTHHODG2ig0Zjvz5xCEjWDxeOudd99N9EiBtUhIvfXEjI82cT0G+shDNYhYrbMFaxEMm17AJDQWQiUiyQGqiLpq2BlSBgUguZKplDN+5GykbdfOJE589dIdJFBZFQk2653U5QhIzERN6bdTEhpe7VA1EEFKopVsRgggpWIh1gPj/H1ni4o6q9Za0+mfyw9hnlvOIggpHl7AHWKlgqUfU/vpjEPbIhpzS/ADx2KgnbNN4ggpSIlUIIKeVoq8VCWIIKQQGKqokihHr+od3mPA8prr56c4EvxLuzWOeCN6jb5OYVZN6Wsd9eM2PqeIJlFUOndXxEXrdxYQQK7jV6yqlPbvGaqqdJXEPUcKKqLINVkW+OLjFU8PuZ4XGb0ZfGKX2h1EaIfMubq4e6qXt5fKd0KNRKUPwq6TTfTWefPfnwb6WrCXdDx2KPiSYxjFOXdLl3Pv21rK+9tFUUWrEbIJ7B9hLIQhU/XSra9hJEm3kU7IQkbfVwMHSQhDREYD1fHEREe9VVVVVVVVVVVUzMzVVVVMzM1Vc5ziQSptslNV3Zu3UrEkYRhppJkYirBmZBcYqFE1jZCqWW7dF1hLG6W2Num00S2y23bukjJZKXZq7drZSa2zUmWbuhLkSTS6N1gpskkm2bJoy2uVt0YaOkjhJGQkgoLhpIluN2ag0ZFkhYC4usFEiySRkgqyQRjCSMkWLpWzI7LSri2LppLQzDTDBqn3pmhAe0D8Q6Dhy/JHulb1/Fdvv0Z5KTlhgZX41P20Vd1i075EP6P3v2Y1YsXP3fOzpkOOefvmfUlLke1I9k09CR0SNKi6lSOqPCmzbamixTmn69cXTP3TSRpwpBhGO+Ue/aZ3wyukUohcKIMdp09WA6D7fra7/9K5JZbbLFFS3ruzFdmHQiZtnwnMQLGmDEH70MaJpImc3Z0kkJDo/ncPzf4+F+SsX9e9U/f/mzrvy3K5dt8PhOm/5Pda6owNDZYXC4YunDMRWyYsp3TEOt7OknazSNUHcUHZGIz7hB9yA8emkb/0vpi0obgoGRyrBGZgONsmkjfocU0JsRrkwT4rlStXGVHBxUKDtces9TNiH1nMDzWXQuXe+zIGp2O34r2iHR+7P1v5PnI62lbYz1c3xtrAe6yGGmmg1UJlBkllJZatrGSfJYn8CnLmaX5G0NdMNjp7RK2NjGGUlAMhtNJeJdFF4KGhaperTib/KsMfTJFyjoESRIxRYUXOYUQ9V/3bO7BnFbjQH64dnkak96N5zZUpmfGaVWrMtjKcJmWjgqwhQSMIlwcxb9G7Qs6kOl3WkHo4VxbE6Rcp3cKlJWE0Hfd8cKnfgOJJEPHz226AzsJJ6u6rh8RJHtiV2Si6CKRUipBqwnWlCj1GTG05hAgQRkUYQIDikHmks821YHLutuE2S7GofMwZi0oLKr/NqxuKkjKlMIVGmIlBFZEnSFNBVUyRRSB36VLqFPaXNSyyhZGYV59YVeZN4x50a0SNPUQ+Fzyl9YxHlz6qRBo5n3WmIhiHlCQKP5eSfslUlBKp/YRZEbkkyo8R8vUqXytdqLctY2mXLFtdnNy3BqdJMamWjZKqKyc10tUm3N0ip668wlbftAirREQPPw36dHAErnDo/w2sgwhJCKkQj8pqYEyJqMN6/WnQUETnDpIlW7iSaZW6e70TrruUt6zfXtc1ZbIZ0PpPYuXAmzd5V/fOmqDgmhqDvJg9frndYbUnX6fZ8tTDzHRr9WmtjweDIQgySJCEmpEVMySSyWS0m03fi+RmswSILjEaFHnhE9SQf5ooSMxCouaEEsdgRWRCENImg9hp5+b0fTjL9vU26R4H9foicx6VMd8gydjPebmmd/9XRyGP/djImQldVc204PUVgIfIGD+j4eHICoWFsr6hT+iOb5qSCsOr8VfYaVoe+qg5H39pF/2CXg1O0RyH40dkCBJFT8Rj1nf5Y7/RqFXq5zIAVCopTgnmIFnp4c3sSDr3w79510h9R2AdQfnN/wLJPP1/xzh902n6n1fVbsEeT8VdtlZmr+kpSucI5CSd/rbObXgqSSLsud+HnTuUvPGVMpJFWfr1LFS7OPOYxwr2rvT0beoYdBG929Tzzu8HVhJJ48dXiK8noyxX7MzrrgHq63Yo2YQkyMYi5ikHqTj6WJiGYpJJWmN+JmsytDTQvCaE5Kp6QfQA9/SEKChrrA9QuAPD1afzQNB+1fQhveD8MpwDccgfyBqQF4YD8c1FJAf+EVagoHtiFkVUNEIcAiidIHr3QBk/kX+U2hszEkCb6V7/kPQHXoAj/TB5eH2aFAaTD+M+ZNMD9k4RIkPuJ7HOEhn7fCWTqlIdEz73I6wyEIDGBHJnEK/h0IHTtJyVC6ox7bsNTI3e1X6XBx2SjAp9pT1xBkBWk/eo3pD6LJqrS0kftskfhFnFYT3yfUXFSRC90kPdgKJcTWBQDtUlCH4vjH2ntPae1McSAecPPDdxYodRXDaDXbEyQo/gdOeTyvTTxiUfnOlTlETgJUR4QAtUeIfjNeG6vkg/MdXofj8sNsDcuZfw51U+XQqtRIhvEPJvZ8jHWrwlRxRGx92H2ufYt59rDvGPwTodUO4p7V1ZHK1/Mc7eTO38INvsx4cvlKpYKQFBR13pndx1duxAUEDWIZi4TiYCwywSkqtCLVadh39gvcf8Ux2D0ORMk+hY/duWGrHlJTlX3nryqUsUrSwI95ZlSRbOrMkzMgZmP77Ly4jH+sqhMQSBuSihxge9gtEgQeoO1TNIMO1k21zZUshIx8sF5QOiDJqorKahpSJrGmRkgDbY1iSNi2TVptNkUsC2qlq2SRY+xP4ZKPu9P5Ckf6B/8EGhy/EWO0vzpDICV3KD+I4EA4iHfhV4MHzqJQYX/jFQ7okEIQEiRUrSStZjLSo2mWQmzBNBMq/y3w78tfo/q3zyy6aB6FfiIEJS8oFGovADQ/suj0KUvtD6YyB72BItEk5fELr05BzCamCLCAwj+PpnThci8DSKss+ytm23NNdW2cKUwQTV7yoKUmtIboP9+o+qFIeD5H1/Nk7h/XKqgmxu9/EUPMqB85EPnIPVjsaejrew62w2j4gThH5en9t67Hck+y9J2Nmc6M1G08iION/BDIT1n1TdoR20VIrEP0fX5umjxkj+lk/ZKLBV/JmPzA7un6F0n3mTPr899Vvv8y/JOQdJCmdmlIcnMcFzBQ++mepnFrKEM6fDTjm2bGb90xjDpXaZiWbw+ae9JCSZ4LoOFWEU+MgKaF3oHRCRR5MRvJGrBHDZ/gfKKw9/MywttllCfGU03wA6j871L0z/2mv5p2TY7cseQc4m0wOB469YfcesNgjsMjp1UGHD31vOyigIf4qaUxZ5CvozX0t+fC9o/VDU9jkyYv6deZm2xyDB2PQhdzKwFRH88I58Mhr7B2dIuelPHFTykKmbiZXKAiSQaIdplEFEJ+AmFx6Z6TOJtlLZpp3yYEJjkKDsnFmnk2neIG+zHtxwt4nS1Oh6J9F6sEWjXaZ608Sr6MxSngs45Qhx1ydybNXuff6NKEZl7iI9oAiATIfZUHdUNPY5AzNQgbibM6pty7XjFo85J7j+w+4bD608gdC18RZ7oECA8/ZvNIHJD4xIC7BdPyRt478fvf0vyaqSymWUgNLKatAQzc+Zb/H+P6Qn6/H9Vf/KDwpOUm3tmDGpDbHBoYYyFmvM6D0KGr17ke+BJuiSIZMJhTu80v7tYdBs3M+zSBYmIyDBRQYkWflmLKr1Y4ax0VjiYFoznDo0Omrbs3kuYotexXXem8WqNF77utN3eZXh6uTu6ZIlnXaxcLs1indoucV7l0gVE8e/eUs8zXtkU3a9ebC6TqdzO2kSJIPjvqvTETCpiPQA5hDMRSlWyrE4kjczT6f2Q2WV8nlPU6ucdsCoHu9+mgLD/Kv7vZ2731RaIB88qK3D0BLIyHAiehhCg7knZBSEJF6gi1DqXvHocoRkncZLUEQ7z7DHdQFNAUdepv8djBmDcjRxeY4vsH+kkSEJwTDsC+fxnQKNoyLtO1dkmyh1XToKE74nuzTIHQr2MEmvxfpSr4+a5Z7S8eVmZi0a+1xgYmI1VWRT4Yq44gHaypqhgsxiLFdSAQj/mkcDF4m429D+6y7KIdzjyi4GAiAjmSCRQ51gCD2S4QEidzVb1nad5UOuLjRkJrJKXNp2B8lZfVDad72d4ekDP7aCu4yQKxNoW9Q5UTvKA7Pj7vtnTxJOLu2mCFoFSFynEUJCROFTzYC7KxpwUgREgCwFICeRxijmINMHAxMG7W0FtQa1RaQTVXpW9q4Ds4bq8G+shWDzS7Jy/CQr9JyBRJgnFLl8Eybn0zqqMSZHTyQZN5aGcMaeFwwPu48IXHAggtVoTh6gRF75V2/LoZGyHZy4a5KxK3VFzJFuRNngge9+Lxw7ZZu0sNZRrR6c+zz1fUsGe+G/PrmQ/gUHDN663BdNGtUw4gav3iQKm/HPZ6pNLeuGHIbx7RR+tcmJshokA7kYhiwif2GQ4rl9O56EwuIJsofi+o7izyONT20LhIdaeSdkkYQCETv7A55hAk5wKhBLu7ruu3S5ddRDTaS6UUXba5auW0bJltlnJ3JpIFSkigw1eum2zYekpx8WCQ3Aleuzs0mxpmfaWlFkcTaNROrTviZU6z091XKrJYON06rsTtO8unNaOpCD2eGXMIc9DS+HmEOQdGO53+q97MQeIdtTtFPzwTkI8uA6q7thQMhzOksMTDI+o6i3th66o8jQ6YmmPoIeD3UB8YFqgdEuMg+Y6SA9TqEa0VpShF/vighSZ0DJaoWj3GG4msrpOfH4Q4fLOuOnVaWWFyrfuuWr1yMTvmkkOk5IFOA3VXCLqQ2wMQZF5tJ4+ccek6deuv81UVWHF5Qu07OjTjhNSATNYgQ+ajpL1+Lr0+PnkqoKIILn2PIyvTmoOFmXgRFCeQhqYCqP2QNYKhpuUOF0/1NY9BIpqdORNkO3N0k5e8bnXMzn5hCHBm5TMXjkzTYfrOZXiTUoDiluA9no28OFpgKd7m4VNeOXM040uLgGshzNzKaQQTyTopuZHMcsqEMhDBK5RyqVIiioKo4YmdW64ajCRw6ttsyjtPQbg4nRvVrplOuHRvMqXGvB6JATrU6zcC7vNqpdEeDhx53M5zqg4j8eZSk2QSbMqlkwxd2CJVpZ5y7Fuq0lw2YtIpIFkQsihXBsyGCZlSCCNuubuKcGOLlzhQoIikVFJ7WF0337snsJn1g3d7zmOjZ1S8s4wdmysELI5i3w5QllxMMGi9LCoXBpEI4NNQtuQmcUiyHJSYo+Q9rFzlTw7V/bZhbPvradSyd5TGd88Sk92UFTZC4mIISSPGIlQZGES4OsVGyYgKt12FbPgZNUkUMcYA9A7F1AsMEXKzJhbCDYhSUBDPRgsjNxVSBdBcLMZ4nCc/OteaHsNzrFjFkSUqxpvPrlqxixl5YzM4cbNvjiGvnYGSQjJPLJQb6o+TtO7HQ9MRgSKaF++p1bIEmeUTtQaqWGO4AMCPkZG9uihrIBiJRBN0XWNsCiylSzFIOPPKEMt3bptFkVWLKqtomJW2FRpLhibVoZ0BY2LBpvpkxqGFY08m3VLxqZayZIRKNGo2iyY2NNNbZZRavfdteKkmMyT2JiCsCbvTeHsqeoVb5vdZWXxF86ys3itUa2xotkdgm6lbilKRxr6uyp+Sjkf9DcD6tchGbCE90+lk71iqVn7fkd9d1kmTFFFgHRAW+b0QAzQ2nV48vQhkP6GJH00QZ0byRVk62DEkI7oVEIl8Dt60qMD4V8vI9PqwYJZD0wsvkt0SBAU8CJCKKSDCPmlRTyh83do79Qsy/bI5HIWbJJRDAEKhJcqE9lSquJUOXmhS9QPaeJF7xPvpubuL8nkuu2vzrtdirmplbcuDoXoZMNpVZQlWqUQQxJAb+eDMyGjSxKLLFopCKixUVGi0apLRaS1GLMcyWoMkoIMSBCSqWqVItNIUlLVAUcfnNNDSGd7ajguOMY4Liji1RS2WpZG5h27T3+Lgvz+7NNLzI31hg4JrsBkMZCrmO2TIZjymHsFAdPMwhew5YR9cQx6quJVUbIVBH/cR/ycllmNtV8tZjmqiv9cTwKzo4yRuBIEISLcMTCTDS1uUU295DwZd5jZfCecjnN5FomAkWmc0SzJTG1OKFPKBYHJVf4SDFgs+4aDmJDgeQSHNqhmEXyr3z2E9OPiKL+BtLD68X9ExbDbo2WG7pfKBJGB0c1DRMh5mwcomgH5R+2aNqmvffqfc19zttMsFDMnXIlFWFK3kdtSQ+w6yOq8kHzoh3PE4qckOO8Ff3EP3iB5H3NonM6nyIrS+m6rW7Kvs1llCTMplps/k9KRydplOjV0AgZe2FTC5XcAtnY5scJTmQYhPthtcmAE/TFkAT1IYCC2HmpGJblxi8FSy4saoZcSrddXd1utzNpRqU14jb4QX44o57CgSghC6ETSIZHwNbHRQp91aKg6xH1VJ+4Ry6zEUtzrNREuAIXVKYB8OSYQ+yJTtR3QhAV3Ia84dhvouCkIDlOoy/bPR4Ad/MiR2Go7iyzr5AmZBT09AG4g6xYXSZTXGjfcjcjc28aZbkb/dppsqWLNpRb/sVF2ZXLAkTA1wPpcIiOHNgYiCJpZ5Nouandt7Lybz0VD3dctqVetGLT54bzadSRhyxy5ZzudtPtib+4hwpE4PhKhKFMDQNIJ7gj5KmQyAaBFoJouPPxTZCRCRRPMgnyUQe6kQwVIYuSLINjzVIc33e+iQqfv/F80/ywfJDWJ5vojUCNxaZCWtrEYurLS3KPu+kn3vajMMH4xF2iH7ei/PQdbAnmoCsB47oonxMFPjhCItwEyf+tKuIKYEMFetPD6iEWLuF6SrvbTIWWQyiXLax6X3+I25f/r/ATJksk2WM02u31fG0bfpNdWOD8f0xbKlJ3xzJOvZu0g0X9k/SgdiPuk6eX4v6v72i11pG/tjK+JkyYxj737C/7Ji9aKlxGfe0lRVlhX8Dm1MeB8PmtCobie2dQB5uUJD1xMGF+mJIIUWeJ5HWUHdtlKfosUIwMqGbcMwLmGzCZ18iT8RrUWKawhkdhj7/t/b30fnwjjrpCzYQ4R96HgnPuYGR/acuKeysOJCd2Pyq+KcRhzrPKN1tNV9XumJNnc3eEWo8SObJU2aM6uw59Wdau6Sx0Jjol8yOUNpqH9oSn3fgo/PHRZ8FMFQ1DLNszs/mGvd7TOf1dP31osqmz3f1oqP7rTSwccUr3Q9n7Qr78PlIdRsrJ+beV7ZIS89/By+Hzyy1XzrPpqVmrNlsdJEVZBmrqjhw1suHabrrsxdnKFBuQnKHgKdYvgRCPe9Aj5INBg8gWvGCkI9ck2IyG2hylximw1WL8S2iPs+3WG5ommIP1s5uR7zlOEJueQbjVG3Epq3LQizHWINUm0KQZC/1aKsNNNUExgc+07gpZAwEV80yg4EeQ7ND8ISZDq1fw+t6teXvghqiotlK2bNorSm2xrFSNVqQYqWaKyRCQZCESEST36GTiTr3Q6rPSdeJddcP4bLLqEqZYH7PO9nTQLRJ10UWQqydLQRxHZ1uYTZ6OExFzgzh5CabT7vykQ48/Kl6G6bcUX9n/QyvTEyr/6lVFulqnhiSZZO/2vFNSSazHhSty8pKdvE1WCDLTKI8p1skw1UthWJ6J6J6PhHPTru7F7Z2WPIgf8G4gs/5IikaVIdUhtUPKK06XLbMWZmsLLrive0UFQaYBCJFYQeZFitCNPmwrK/tDz9p2vb7VLGwTzA4ibx/hhhAiQsIrAhGiLAjIZQcvv7easVp1/YrWSsrM2MpcibSQ29QxYTaUBCLTBJoGPQMPVMRFyH19OfWBkKiZ+A/7aPpcYnlNRIMGQZCfXD5nxoxNyHX1rKQoIP6wcdrquiQ46f7hULg0+pLeqmTHTOxvJD4MnDZftheCnonkP8cCEgATvXu+BAxMa2u6GY2SjOBISYxEy22If1GQIUYZtuxkGbxcB3np86dFUubnyN182WqKqiP6HV+PvK8MTohjeYduBBFREE+ZhswZmlEGCQtpi4IpjWDkHIO8fSbzgJ2b3zgYcECOGJglxlyYC7Za3daNco2XXZlJaG87eCZUgkrbMxOSanxFDCdeCdGg/q3NU3HMhMzSgZthqZEjAY52UCtRk45Vk8if3lxGc5pVjtEEckzeMVEltCvN4dIgaJBSiGkDMGyzDaRkSWVFUhRTFJJU4VtwkKd2sEWwQcjPLbeGknGkYKHq5tIMPn/F35ziIcQQTx3MTksbSpp/Ar/VKj+dZVjofrg/1lAYKgB24t10c4pKWp9Ep/HhkXsZAYts3EIQF8QyniNOon7BSvmctRPOxJIwB3EIMIlleJISyQB3kohIQ+ROOnR7fQ+SHXi05Q7SSdjKyQVsilYZcmjRBJftG5o5rtipyY6NNTcNkLklIDSgoQojBuIfQevLNokTOBSbjRV3i0aCDBD842x6hedAFy6KJFoYH3HoHsAMj2/Onh14OZB6j1QhCC9HQUmzJ53C8sewWQkTFgXBfZjZMegXACfDuk7Ch88J8dlh7qaH2FPn3V3HBKPn1qHtoaikNZih9M1t16TSzadOhi2drHpue7D6LjUMGm74qsQ5QKHeDRFim6mtIXJiqwUwuYyYuK2wzxhmsMqlNsk8HUw/fMyfC/YuE2zoJv9Zvl7KrAMiQ3BAfRsPTXmMFWF0ccfjgVVBTA3wwyMZ4ptLdd9dMX9UKfNDl5sIedXcRUI1idEA5SMkZKhzh8tzkr3BqtRwgTOhhKa1eILIO++LrInZJP0zN1yZ8IQSqOQIw5b+ZCa/82tjGSLFAicEllszpzftG/nD4Z3h2JvqBPcFB3nxPxhQeoQH5AhiZ6rM6L74X4wT0Dm5h9Gs85CKjZDFRXBHDJiGQVDrJCGzEHUR+/EP65Vv6H+PoOxd0WG5fmihBijIPxqGCkCD5+VAOiBJE5IGUuIgyAEdFWrGD+M2o7QUnNGWRassQ/mKZQ/ZIj/Gw5h9W98Nrzknp8X0XiGJXdwHzKqfwdfvHzHUc52O1+v7JCfyVJ1h9ybK1spKtLRYiNtJa3nnYSZoJ63m8u+y6T0kluXc88K3Zty4s2pXXqqDQhAaoQIqPHKiZDEWMDBF3d1i1Fq5rXbN3XVdtLWybUmkd0W7qdorY1axE7MFkhkplsxYxLCmCymEWqUlT9FSRgqoWWFhqPvP0SUQijIDIobnl8Gn6or0wiQbj6ykuHZMcxx8gv0Ex5yzbMEpDuO61iA0DSIEKHByZuYEhZhTblP2N+kZ9XP8Cb4+dn3tO8D7JtIMhH2HjMnTW0Hz3u1ErsYaCB3HfRXTclCdFoisoL8b/+HnSy8/VkYHqxpp4Hr3XzdFEbJMhHu42HDBW+nE8xA/f5xWlDSJy8L0OiaZMVYRgSD1utFVZ+J4aMxwSJQfM+6fUjjuDoTa3s1D9jXtQuY88YzrbPssBFw7NZZZOhA4mdcH0wWzBlc/5DKZmN0M2S77gUN1o9FWJUX8iUeCcQeO30JSarDQOU4XrqD1w2rNJ0zVwO+zvdymzUR1zcczAoiY+0h5uTWBDlztlIS3CYRBmEhQJ0bdwO0BCG/jlu7khYJm6RsWLhe5o2Q09zB/uPzI7yOiadK1Pxe+XNDw1Sxyya7ppFpqJLdq7qWmbLEh3IZ618g8/mIhtyQ6VvrSUvK+VdU655yJb13B7MiPY1kjpsWdkqB3aEQlCl2yITOjKxotRLjUTbfyr2NOzI5OnNCZIIHMbOtNOiZPI+nYfk54WxIj3OpN7G5TfyFsR9k+GuWTQh652KydKbcMaYl6WD0mhPtqQSXCLuoqzJq4e3zLuMjDKgwgXpH7fnMFu5yYBOunHt1zGP1LP1862uSw71qL1AOdS06l6JJDhcw+QT1N7Nc4BzPAYl8n4d2j3W44aHSPSdRbmneVFmgXeeMpanNKR09MN+ch+PmfmdTWcKezgtFnS0NmEDWxsho7k255PPneVTzcdTRWkkMc35GxjffRrs2uBRgww2Epswvd2FiRcYA4IEW9FtPamzaCKdJuJNJDG5Cu2bLV/wrXfUd37+1mOcYUXZwlGDtN36cHl0KXUKDhFyfX9PemrdnzFw5SgERKKhieJ9/k8tlXhWxJXbhUM5+QiBvxnJs1lCfBgo8QVcmL834Ld83OY9+Oinhg4iJBPA4UtM6uX2lBvnvulGbUCp0naQ+NpHZ0xrIT0Zudarc3bu69TEdo3DlmiFsFUkpsOHMyiNkVEulXeEz5/D++/olXz15nJ1fmIMRxnyJgUj0rWvTarh+lj6Zn5P57O41DgdOE9/UUemXD7o7/sokkUpEhsThqWNhz6xAPZ7SGTczEj4Dhd0cGDnEk5Cl4iSUlHDSkpN46h9WfPw3DcJhcKkJnSQERAVBwQOyTSTAcTuTBUZeQK7WURwhrkzciJIDcscA6WBUblaMGBDIEWgaGg2vyvQUyIdnVRDQ4dW8bcxBcau2QnMaKBPZv0Go7UKA7GJCIK9dDQwPpuiS+y7uNRKhZGqpF/RsEebkMeGLiI5IEUiDiZqTJMxQlIRFjjMmT7ZzdNZmYNJy6WwzKFRv2sd1NwJV4QiiKzaGuy0mSE4/huoOF663pPnePUJ8tyFSTlw+ruQzTlPF42VKboFeqbN3MrlHNzUJ93DmXdpGEyRMOLMNBzUz7t5lFWyZMYsuY7QPOHgfbE56WX3j19koRz4L3RhAQNscpBcA7DB1UhUL5BG266/irhGE7bPPo6zM0urW1hPT9LE4T2or1i85LiUAOXSc6RLZFaLVNLS+79DET522/d67iLn2vt8K9ESCuKuPJ1phOuf6mqa9GumTSdepCE14PQJwisIjEspZpJBOY9htjMWSylNRpJ9sDViYnGQ+paJHiOwIC35b1ZwvyYXiXWG6pm+dARMkqXmOQMyWZD45CRImWHGIH7w5sWN7TBxYG1qQ9cbieeBR7SHidlZpo8YUjrWJcDvjWxKAh6GqJwFJ7Os5IPDBy2iYOdTWuPft29/ttV7bISxikjGwSe8VBtD4bh9Im5uEMRIJBiETMwSSlFCypIVe+GW2Ld0hkmhLKFpYfYX4yXGjASR8wSjS8YHOOdqTBDpmZOZVweurkc0TXMgpoJkxlsCFqiKSIiXCGMjkYDhk4aYxHERuAFQWMal2S7VDCQEMAq7ZhVoqS6mZJUqHCtqke7UDthystxzashJUK40zz084YhJHKHlszs0F9hNQm1j3PUbds21Rl1HGthYXQ0EhuwsCyySVyEz74+TPXM8ePM8EkPBneIQ7wlVVF5MI5ipOiYV4QxxMgEfKq8TZktW7YyqxV8v1vlr1cnwWR0Ok2fy+g3shG+y0Z0Qza97+97s/tDbJ25XzyQd3QZmmbowlQLo7IaJ+OP0Wm1sMSQuzC4wrQGoLk0IdQagckYyMgPmqnGxn6pU9EPIZ7AnMnAGNvR4YyzO4AX5bgX0OifLXNaLPVX2mMZH3XSyH5f3SpSiA0TVt3vH5ve5MmhYiIYiiIdnHUMbTIgITA7IbVdm70aLd14zlM3tR5kvcEEq8U8UfFziQfUfqxvXPXHXA4+0/H6DLZdrIbvqTQT0bT8TT3EsfGnBoNwSvbLLWqD8GATHiSo7GYENRwDsY0vZ7QEvNXTP7CMNEWFME4i6P22OIi0JIIsCJZ+GataqBEfnMM1q4NOEFPpDvwVMHOXJLDiCERH87hRolyjsxcHUsWc2vRGBw6s2LuGJg0PychI2WyMcgvvAg6m7OaY3UkJjSA50r3hS6eZXOtEZbCHZ9iamxl1dkR02BjeMYg1vjgwpt+JwXVMrBzgdmxbOAdjwO4P3kC2PUJnHZkNxTO4QdXI1FR3JfCksmDQK8cnpjcEMJhl21OJZtyHRDd5WUJ+WQ1oaOIhu6LOhKncUMJkQGiCXZNYSmHbp9zAr5coNJo24YSIQnZKnmOYjhUig4YxcUlNCW1oYvh6a7X7ayZIUu9YvHL1pmNMmGQmZmjFIyyHdZJjJ7csrR1bOcb5tyrMslzcTsRUhyymEIXAoeNgXqZhwPEzMJ3M04THchDOKJwoceOVOtmBzEdiRiPlPLmeK6eOsu05nMTg4HSEiZtK0xRrDeet5xVN8E0Un4EPoeVuamqhIT1I6iJiB8eF3L1cUTjbogcoeRFXHV6th0Nhcy0xjjXp0CZmw1GHMDmWURpypW6V0worXORyjD1kI6fum6eMu4d/MPgh2GTm+U5rLnSE5iLEwmBMmxAncTJjZsNaVxgabCswJCMBxBjRQXB7Axsqp5Za2aVhRu8tcud1Cu6WJuOXEyZ6aMZK9kfLTbJlzepjTzld3xLxRHM38rqTfMPqt5WIm455lhMzBjsITM8jBJFdiQwaBS4EcBbUIRWLeQhnExLkAxw1GqlU1KqZDPq5m61CksQlHp3SEbccpVmEEotKRzenunqpmdaVrL126fP6+2nrweCk7HbSdSxLid6asspcHPUnycQRBFxyB09cCUFRFNIlWaMMZGhlhgbChpcljlLRomBpdG6HiGA8/m+c5Vo6ibOoTY2YSKbzkg2iZmQJIDXb/LeNhl6Tu890ig72RIRmyY2ChWYVE5namhyTBSpb3qJFbDuUNEtrX9rfi6+H0dXolti2krSSWNIGR2lljsMmnVoGV3p1vUDh2GptNVQ0HMAgmYZ32PsniUpEZ8xctyEg4NTKNNkQk3ELmDgi64uQaSICtk83dXea7W6cSamNRUEMEpKI4OEhuYmFK5muEkuKuYhaNlBUQiQbG1obUNqbSBqb6wPa8tXaezfbCPjoZvEvooamK1LCmXKkoo4OZRNyGo4J7ZNNkoFuUej06M9DxA4QCQB130sTxNiZjqSn7nLUo1rhZg2BISWkkrdbsKlA4eyFY7GeegSR+d0y6uOXV56xvXpxV8RbC/wYjCxcZmMTmr4/C/gfHr4wbV9iUpkm6sddfpzuzTjJVtURgX4VRAwATDFPCSPcpEcMAhL8iuyRILBwcRY7+3BYzdJ22LbZmo6Te2vt5fD62OHySGEofylkTiyJpak1LEqkRi/jRkNcwJOtVA0ybKNLLKoTCmD99JhM4ncGFQNLQWxRzxyGyNenv6cOQzQvDds7OYVEs79Nl4Ik00Yqkem/Ap8409Bt7E4yzXAfSfH6Q1WDArAPb6oD0kIYo0ya4EjDbW0k8ePsbs+j4/f8m8EUOs7pcOZJQ/tf7l/kGDkYDhJ4PTrI58HFLj6OmbgEmiXCSFAZlhKkTC4ktyWYhXLmBXKQSpRmJZmHiSjiIkMDTlCCDm6IXBN1NwC6Bd03do5GaNEbu6Cet4cRk4NNIJLwMzbTsdhdHb+Xj5KEkijc4EgdKZthBRv7r9hODrHqOL1S4Ge3MPaCGfGSE+0qPq2Mvs/fBu5OLSp9K82RJ9atelr92+Or6evuAH6v47r1Ilonsu33NyFTSjJb8sMzWfkN+tmSbjvpqqQzdSWrbY7qoxiqnzVoekMj+7JkY+c7Qrzhs378TzVxZJiXDT5zio7QglqO+UaD7+HReuM+h+Ujqh9eztWzJSrJaVCDIkiR1g3E+c/UbsdcgxwkrZENADd3lCZYgnMSCLkQsfwh5jrHen2n4ZPrfc/Vn2luJByKpavs0aMI/SfT98u48xicIVB6+TnHnMIK4eAuWuUFUHpybOrlDUxQ5yG0IlSQtzaBUBhCLiMwhBw5lDmYKLl3SnZRXHf3bDYh9J5HAJgIkg0C0+NVZOalIBX1pzU4g+YMC44cCukQ3jFyoDmCkgbQInziaGDYDjcxBXPufaGYdA4Lr3FB3EIq5HprBLD19ooVETTcnWM+74KpSdiwhPCEbXxUHTwgbXpr4oAbYadQRR3CQ3WlGdtNODEMhYtS0tVZZVqHCuUlvZd3cyPbZteu1/9yTWi1nHt2EQ7yL0MbCGRPqhsA2eA4yicwA4ihvZz0DYieMUYEJFh95So9EhRSZEf189xPxtG0Tc7QyJ/Y4nbMVbUlVcYypjMRWZJ8MTyWJ/UphJ0PoMq+yR+mpQfLVj6vRTE90pJB+fSvl91lhuoyQvYSSqKxdff0dKRYQbmOdjO+vdJB9LL/Ul2Fhvg2eKnRY737x9wcxofqIQilm0VfIIIB5XoRxE1v2MAktYDkqyRDwXCwNvf3VN933XZbXKiFH0/YeBnMDquwvSygpuqml3GIR5GOCKv8cJ0YG3AxsK3i9RdB2cTeBkegaoJmRv3Psg1BMhl3HatobIPxBfA6Dw/ZPZR8fUOiAaQ/9yl4j1cduS5y7rd3V52rojY0ZNaxpUr3qy269t1XabaUtsLkGfGc3h3cOj9pBCENohs7R1L/X6LX4vd23NGvQeFH6J9wZH5wOQxD9oQAkQdTyPi3EZJw3AdsZHhpa2H2JwHhVUVVE9NFSRJVthpMplizD7E0TUec+m7Sg6fjH93EouG+dbDiQdh66MhFfPIVQSdKT9P5aPyYDKc+Zl3SBR0XaYtCgpsMGAo6zqTPPxjfs9+YdlhVRbPbq/dNJz16Y0/xLDiU5TI6SaRDE9OH2v4SdXjYuLhaeKj9F+47zufkd28ho7zDH7y1SsKjSUAnnmsDJFHdODOC6hQdjxoTsh/t9FPj/Fyns1zCX/Pm9s01xbiDrLlTpQOKUSL3jyUelcYwskjNkvWlz1mClq42U9BC0b+sRKHYTCDx+UMYBc78TkeIZtjf2+DXEKcJ5LT4FtUzSPT14yhglUV7W9UPbkgSERbBSyS2otGLCMrfhcRNlT50QjJJAJFQkRzEd/A+D7OPxaVZgbE/RpzPp3mZKeq4PJAIdv5bIEAhr4+95DmSI2d2/0Ro4yscR+Aj9hkQwTjtrfXKvT4JuPmHaB/wMqhegnZ64N9AgfVyWKdpEH5GTNKHufQZU9EPBCnvkFwokQX2eJ2HJqTRJglxm5uUJOxDokPW/mvVOjsuTZi4zopkaMGWMyFGNq3EJeAvZWXLmQDiWEnSOA3gRePcDQe6tHIJ7X/d+ijID64pJvPr9RP46LTz09nB5rA7NolWImDAObtOnk7BRwH4HsE2EQes5+rr+iz3YPst98JPAeB8uTE+TwtSEP+sfkun5oXIG6gnwP0hZSLP6U/7hGI5WxHCZrWgkoCBgqj7xH0fA0TDo93/LeRKPgKx/XU7vHBH81TS8xwXs86lr0fvcgP1/+peYFfeYGQtmHcOO8b+Fzsv9HomNCWSXrD6ff/8f/zFBWSZTWQt7ChwAtzTbgH/8EOV/4kAAv/feIGFgfHkvpwAuAHdtsCHdRwAAABHJigFBdAOJB0QAAAAAAAAAAAAAGxgAAAAAAAG0lsG5UAAAMrgAAAAAAAAAAAAAQxYAAAAAAAC7sVGzRIPfSlFVQkKlQooSoEIUgRRjMKSlEiVVKURRVbapRIkpUjO3CEpQgVQpJSUXZhUpUKipO+AAAD3nqRIVVBUUpUVLgADYqpprZM1Bo0sAAB6L3eqpSUqiqpJVSJCKhSklJJWbKhQipRIKJIpBKClQSKlbwAA7pEiWzNa1gqaxJCiqYAAbIDNomWhUpbZ31uqqKlVApKlCFBL53dVCQpSqlkMqpVVCESlFCiqiAiipVVNsApKggVElBA+wahVCVULgHon18AAAAAAAAAUAAABzu1wAAAAAAAC0dzQuAq2d3BcsfWtOr09WXAAAAAAAAAAAAABzYAAAAAAAA2OxuzJLz5UoSqhQiJVVKkFVIrWVSKbj7O8UJJSVIiSqlKo1kVFRElUu74AAAHh5EJtq2KwBazWghBWAAEaVM2qmtEjWWt84KAAHRudFQhRFFJUkIQVCRKinrDDh3QCpFUpIkVIlHbEVSumFXWq+8PfAOgPnygFmK1tqlTQyCVUMAAe7igCqSSfdVRA8b6jAJABlAbAHKy77V3aFKooSSLaqtmrYybW8OgAGdHbKigEhYMNjXOrcLbZ1y7d3HGJWataZXx9UrPeWkpn2p1ylTbBKlIp20BO9jpASFHQPcwHTbBqqK2waAMQF1rtCSCABQpSGkaDQANDTQankyClIgUiaNGgBkGmQw1PEaFKRSn4qADQAAABJ6pSIIEJEmgACYAhgmqSCBMQpTYpg1GCZAZAKiiABKJTSjQAAAAf8m1f9K/5yMUQpChN/yWr/v3eINKQgrapeVtalwYqsGSN1OYIzDEo1WtUwmJEP50TBTRYqI1RNKaAyrbD99VkasssZmYsYyAkQQlMwQW2RjBAJpooAistvNpZGMKtv2VDTnODMyOYqsW9UZomKSXFq3VsMxs0Y20jDViarF02tVQr88s/LPy2gVfs/T+b/H9/5f7bb39v8diiCfyp3lFcVlOmv7Z97/q+v6f24/H+u/5b9rnPFBv+uL0jJq8ctWl70ilt6EJJAXxf+vc1zjWq23F+xXr25zUc3qNQdjVaxbluM2qNuuYvulMW22dY1L3amGmM2rQ3eltM125G+zyuHrmXy96Ua+rPXscve/Ydqbbmu6w7YjtrF5it3rFb5Iiuqavujbe2YaHo5TmtUybvu+dTKfHctXOebs7b3iO7p22aUitY7c0sZL93jdcXx18cs13py2Sc6iHIzeOw82o0y/ZbF5mcOwkkkc3fFIznM7xBGJrvdqzBtr1amI1Xlb9tGsvvV93iNYtPM1pmj5tuk8rLNnE7tdqW1aHkrXLzDGWfku2+RKpW65t69jcSusqsr1AiFdhQlt0k6TIqyKUr0dhmE3L6mWBmZmwR3ZHjOc4yeMm+Ku+W+VrKTmzDSwzIkSE0xINCEZMwwkUwICMgRUzZMaTRqNjEY0mELGCo2jZNJYoCiTSUgaAIMKYjBYsQmJiUGhmyRsbE0hMSklFMaCWLAQChJoyykKpTGxqChhb+eVXq+IEm66Rdl1uuqS3W63U213flbzyYYr586xi1KmMWzEVbjGId3uttMd+d1m7PNOENFMxNN6luWvygkgKWeQBQhAxWzaybYDMpWswVWt19SSQeOun363j1vnOnbfb9GthvRGliAAA3lAkgGUFp1n/AFb0PkKLdigqglav5W/P9V7EUUmSkLJBiSxEkgzMmVBTMyDIZNGzNKTNhKNGGRGwYMTKIqYUajMxECYRoSNMmkTMWLTYmmYplLGTfv5/AkCK3KwxSXQD71T5r5z535rnu+973vUpSlKe973ve973qZMmTJkoZMmTJk8ePHjx48ePHjwCDglf3GN4h+X5fHL42VtrlLcxvO+x7ttUvzxyl6ABvXIBB3joElt5Nsaby22oaoSL1WVC+KYmZUViViJXsoSYohglRrSYJUolJgtKXTV80x7zjBrylOap0TLdtmCSFnzEDXRDCSCn8UNdblJaSurOq1JJef5jFolGZJP2GSX6NBPCUkzSc4TUlm9T9+bbSI4tq1LLRJYo20bUWi2LYqixosmo1EaC20aijYhmQt03vbrbdvJf87cxbFBqormumsVrEVRrz1dvJtkrGuf9C8YsVrGiLGjFb2rblt6/PWt41G1vhzVk/x3x1yjEbYjUYtiwYtQZK18U1c1i1FpNiNfPdsWTQW1vMa+KzrxjWiotbERbb33aK0G2Nk0aiirY1RZIqsYK2mW8ZtepXbXjWMaLRrO6rmixqLG2CPS3NXjpbGxaE1FFqxsavauVFUaKxv7U1qvXxear01Em0VGxsVt8K3NbFrepXNsVpKNkqNiwG123+e7vPTYxaNRWNRVRsWTM1iK2LRaqKii0aKvfdT413qr+27eklisaoqooTb2rmjEajLWm2KLBW/xVy0FjRWgNX27urltCW9q3CwaSLXt0k2NRWNFelvXnbGxqeu1zWDW8yuFGojGNY2jYooxWhV1dNm3prybVytysAaMVHptXLUYjYtvG1cs9da5smgybFpbFsSrIti20YHBPxMP7Rp9jaw2QimFk/l/nmJP6TnHCozT+JZoockslKfohcUYQEs4ZEzQCgoCl8/fsltdjvPvOfvfGp74FQUDALMCcEHeOCkBrvqfmp6TXevnywCMGgFsVdTxpCak5GCfyk1RSb/sQJp0Z8/mU5iGz3i6AJj38SeG/OfXKJHkNbFZ/ruRXKq/0t4xbJRqMWQwWCjUSaTaI1vVbuCgt8STK31r1bW8tXpUWtVYYQRoSgqFJg1sGi2yWNqMRsEWgqCiqiqTEZ/K66Bai2KxFXqtqblt/pXNvKpa5ojf9bbc2owVFGoxoo0WisWojG1FflW5eqpquUWotRaLFo11Wpr5Vt42KN+FuVsbFJqru3VFi3NzWKokxsazIMGxqK3qtWMBaJya6SaVZUt0IfYldTTKStFVioi1ixY0htUbFG2NvluVGxqe7dcosbYsYojaKir5pumiorelXNojbFYoTUWk9rdLYqmVi0W0kW0ajRJW2o1BNkBMT9jKSDFQ+numuW/78O3ji7/X4bfhl5/D61s4wQk7MUu1LekjFJgSkpKCipvZAxNMBK5xEhkmaMJJRKnCiaKQjXxMEk1JUpIYwhglEOdHDKENE54961SOJZIIySyCfc1+70E7iIhSTsldVHe8QLhIJf0wTuk3qST2a4QOEGkwnQpPXnPPv05nKSDM7u3rBIx3iJ9ashbHdOa5sj6yLKO/dzJNIK+to/WO53MCJZBhqyzlMOHU6cIkTpLLCBV06Cx5xmELpG6LWPZEIa6jogEHmTYpRIVCeOs0f1ASYpqLs0a6PIz0ZNVCFRAgXDISiZGdSYJoJ2SkwSklJulJoSQ/6CFJROiYIjS88DUmQWlZHpBBW14BZBWvrAT7r/O731rXDBkLRzaz3azDy/wWlTTVOYilsgBYlBZkzUZOCpCSi+FafkdpYajTF813hAg0Z9UMXaGHGq1ASiK8FD1oW3En35TLMrW1EaXz45s+IPme9NWYU6BxTiDNQHwhOzvm/9zJJJBjZ97sXOQ6ruZ0uijTrt5q1SZhIEaIeOdi4zx9ppea8OcDXNMrT62qX83Ol+C1FDdN3GvQVrvrekBXaXeWY+AsArsJC2wg0JhXNs0tdmZtD8ttpT8grv0xahvxZrer+rq2/H1dbfrEJGSEzBP87mmRqSYGRRQNMlrRNrRgZookUSRJAJDCBGxpYMgaWQkmO7kok0FJo0MmIhKBGKKSjNhIRLJCRiLMSkpRIEFECGmJERIEkYwjQkfPcSlksiVKGgRlkiaYwllDCQyDRf7LqQeu7d10CiMDGmk0KGIibIaCJlCNGUgmGIeLskggwiRZmkSCQmYSJQykwZMxJhEowmvO6zG5XEkqZRkUlQaUYU/S5kJoiZjT06JJUxMxZDEiRopQUoIWEkYGYAxiRZGBsmISaJJLMYgkGGiUAGDE0Sem6JTnEUxpAiGhIMimZpkAyTMUhedumMxsEY0Mra87cgSJCIS0YjApkJoZRIAiGRJg0hEhiXLsiZmTRMAkmgElgxEaTSGPHQkRCIJTQQoI3dda0SzRsGkhokkpNISCcupGIGCmKZIEGEzImKQUSJoUAsRogGSKAkaFnvru/y3l5MsYRqEnvukIoGxiJAiRJmMjIUpoINANrTNoZJBD33ChpLJkMSIypplCLeLokhEEhjEhKQxiaCUIxMxCTJExSSJMQRAYEwRSQomSGhRpZMpCMMMaIP1rvfVvPHfPbzENSCNBMiSGwwh8d0iEkRFkmzRGDRMmLEkykYaJ8ddl46RFKFJQo0mTBoXnXAJtaESiFMFbTAjRiQlJMzMzGmDeddNBJGUEJKSkSMizSoF46wYhlMoIEDN+nBlMmMMSYEwSYTDaU0aSJNEGWAkzed2EJMoJRkGUbOcRpNEMMYgREw1tJGSJkJkJkEUQwkDISJEFJPXXTUiGZFJMiQ5ukKBJEgiNIyzTGaUxMooIgLJv58+bweHclMJgEkgn/Du9uNJkQxTTQpBGaGBmQhoIiyWHLgwyRsJKZMUHd2SRjNEQlJ/u6NNAkGSZiT6/114mk0SkIRCgmaSg1IIBT7dShKIEkhItfDcoTEQMzIMbbQSaTISYGQksQEy5vG8RNAV3cir+N1IkIpCSQQY0JCSWUJRlMW2njmBQ0YRd3TFhmDDIg0xkAjO7sW2iUxDJkZEkr125kEI0IUjCokkmKY2KaI7upIir+6pue3YjDb31wQEHdwT326KCZQEJJROVzRjTCJGEgUYSJILzuEoUxMxMGEhgQmJBsgiM0YCmEMUph3V2kyYTSkwIggSmhhCAMwZigSKDa0Y0KSiFhSki5XRVlSsYjBbbKpbf4/n77u/JI0dG/9/v0AgcvfvjkTaQQfh0IIrmk0rnWt633+epg7OxAl2vptvz5tqlK82d2rsBVl676Q5IIN34qwiGFV3fuBViowPiYlkTgvL+NsM0mQmJGhpFCUpDBimiQhAEGBtf26gCmUsUmMBjl2xGYMajJgSkUYUESQjSREzRmpJEhGDIT/O4jDRSeOSDEyGkiJSJEw2RhJBMyNLGKCW1pJJH1LiZA0MTNIyBIMZYmaE2QWTMZEUxIBBiaK9d0JTZhhCxknLgzJYMQQYpg0koY5zGX+3dGpDE0IlMmJpkYQYkNIkCJSkIxNKE0ihkhRiIkyTRFa09/nzxQIgWIRoMYmGZiaDFCESRogiJoEEPbhimZjIpYNIqEMwDQ7q7FJMCCRkgQkyU0iQkV3cmAUlEiBpswwmUooaUjMAoIRiaRBmMsJJJRosEkCAwIoL0q2u1+7tboxEo8XYDJgpBAUIwMYxmkhenRoynncQmhMmSMMYozGIwzBMhCPO3QkSYQkR/t3CITIRE0Ikl7nEwwTJNFBenETTMyRMSaZkSiYiJQjNRGkMpMIoo+erer8+vP16/vz+vP2/Xr5VMGIgSkhJsaX666IYhaSSkGTKTMpMRjYISIZIwlkkgzSkGQEJmYJJJMjcuZIyIH5cZIQFIjIRJiimijZQwkaEspNAzBJZpljJKkkIgZf7OhP8XWJpmJgpFGxJZJKQyA0pBiZQl/a7+u6SMo0UmM0yNbQKGKYZiIQYRkGiZZRMpo0ShGKQmDRoSTPu7pCmATANMzIGUBFDIJGIlJAkmIBF9LpKJFto2tEMpS0q2xZdvszOp/MxJeZty7riuOMLv5wMfvmpXxkcYX8ZfmUIGABgSYdGAzIPusJJtUR+oh58yzEdUSTrnTOjOCeURNHBMIjCWSRDcn7OScAgw4Jg04JqeBY3FmqAGL1xu9aVCaXtO95U6VLl+V7vkMdvHECSa8MYp6MlATULbe7aOJJ+/czviQiCY9tW2xbVtltlmYxEg00/0uEKb8vf6/l/f4+4eqrqr2QqpBpODXP6FeKDFbuqCe9zMx5l8Cn3gLRscCrd+638UvULBDso5er4KcuZF1kzJoZ7V75XGRobtYrHbb7qatRxcK8vGL6fLWBM4ebMPYy/Ip2vRsCZJAC5kWZYFcozebkvvUorO3yCEJ/L2BMzjuzJhhmWF1d/t9+VZz9eCUmhEeEkpEFHmfdNqyZQ9pvTVNU1RxMhtVG1OKaqa1oJWqZSnihlTBMhgmSZJgCkt3TkmTIZQ807zk8Tek+KbN57pzTmO89EyQyT0n8JlHxPDyJScEpPSaE/mpPibEwTQmxPCcbCZiakpNRMzKbJkmEMETTik7QyTBPSVOSdpgmf2xPjUmqapURSfME8JqTCdkyTJMkNz0mhkmDJOCaEqUlQ7MEwZJuYJknBPxPCep6TZPScJwjJPCbCZJqmSdkqaE8YJ4HhTBMlJgqGxNSbkyTslE0JSckqftuiZJwSpRMGCcJU5StxPk9JgmpNCVKmpNGJglThN5hPCYJyJymhPaTYlJuTomtTUmScE00TJMTZKTYmiH4npNiZTRPDROj4wYJoaEonROSeJqTQTgnJMkwTtOE1Q5TwmybJUwnBMk1JirsTkqb+6kpNUwTKbEwW4xjHpNCck0KmpKTgnZqTUlE0T8TgnJOiek4JwnUKnScpk+J+JyTBOU6mswnCYE8EwTk0PCaE1JTgmAxNCdkwTBNzQS+YTo6TYnJNCbGTB8J6TBNENk1TtOyek1J6lTclE4J6TAlSZJunJOxOCoINAuswhDcwa7vsN3q+L+f7T/ca3Zvi/JYVBe+T6h2G98BW/Y+6oZBCE5HAUCkF+QOe7oRE8T4Q8EpKSyJSVCFaj3KscarVYvW9ayHMguc29N7NMkE5r83q+wMaMmxtNKWtJCEQEjQKSA0ybImGzZMkVtGIpAEQkIxSQYmCKQmRK/4dwxkKYZSiZlIlAc1xGWJNMhEjA0UYIhMxLBQGEsQkoQkoRYkggbIkoowT99zH+3diUZIQSQzKBkBYmBDTIhSkUmSgk1tMZKYZmGmwhIECMlIqbnBChElGEEraSkhCkMQE0xYCSnddGSKTCE5xYCDSSUxPS6TBM0SSaRJkMjMgiYZlKNCMoTM+63V0soRjENDFGaGxBFmSDRCGQGGUJCLJJjEUyREmRIy0jSYFMxsUEkDMxJmIyRZECITQAMBElNDJAJiglIkSNhSgpkQTJGaLKGQzP93KSMJiIwCYhKKNM0ExvXdEYSjTIojLAabMmgYBZAijZpIwxKJFEg2TBBmFEND31yZGxJhlCZQYUlMGKCQSZiRFJYiIkCUSiYhihjASJGUM3i/nz+/N7ARmwsxIQkiNCJiKSUswmRfDhJkyYM2DFChigSWJkIFCkiSvh0kiU0gKRo0GCZzcqLBSXqbsSJAbDx0iZiTGkERKKSGZARIAIMT03WHndhTGEJor893v77eJKSC+HFEoUkZMiFDJiYGaKmTTCYrurjCQBRVZNLuuzBF8N0ikp3cJFM0hChNFCJBkWZSE0pYyQTGJCmbBJTTGEDKmZMppUYDC1rzuyBMKMkgilEIm++4IKKERSYwiSMlLMFFIJKQSQ0o9duLCIxEgKZClIyJIpNlEFLuugyYkkozQxFBRQoSJoJFL+qzpAwLEw99d67qMzJLxcopYMgEgE0YUwRBmMpNMkIYiCMManqt9eefd9d5hIIUmSSkETJRPluZkgtaYEiL57pAshEkC863TIWZMitooIgoQzRMBSZIjGJKbCYiUURu7mRhGXnXZEpJCpCQZJkWEQEjCYSTAYZNKRAwjMiUkaQGPx7PPMKJRJASkpFKMiQWRJgEApYaGZAiSaEjKKSTINiXvtwhgzA3OmiSkhK7ulNB77oQSgsndyQEIaSwXduiZCChKCSSJNmYTMmYhmAGgxRJSSRIAkAxSvHIi+7355IG20GBGGbWiTRFEpSKR7cjJEpgwYJRUaYEkYGEmwMjCxCTIjJlIRjGhM0aU0Roa9roxhBIoiQkgIoUhRRRElDOV0hmYphkYggRmjSgoGZhmTWM9sbDByoSw0PEkurp7AgsfCPoIGQcPu/YPuqM12X1VrjRpmwLRu1i8nHru1841zOt976Ms1Y1FqPGkgFSokAKmC5G6PLQ2S2vZxur31vldUMDCsAg0CBgVgEYJghgl52J3ROKTSIUTdgIbxKEiZIUJSFEolCUJljVWttRO5qTSkNKTGdP2vfmvGbcMBhZ24zs7Y0vW1XpZp4ydcHgYZmYZmaCGMFGSaDJkkkP7dgjCIxlgxlGAhRBkoNJLu6kkktKZCEwQiWQ2QMX+/cEMMVEWQwKSUIyQQxMyStsB43SRMSxKREIiiRjGySiJkw38q17at55r7dDe+4gAYAAf6bqGospAmTTMlJrM0ZGCTGkTMgCzSNGYGAzA0kI0KDCSgSSMUlIYhgwYjGFKMUGZEghGTBkkTJEBhiZhJu66SYShYADKi9tyjP58+bySEiDBAYmUZRQMTGimQkDIzKDCRBIsAMojBZCEyKBjLMyJMAyw0Za+O5iZEoLAp8d2JYYlEpkEaYmMZCRKZljNNGpmQNNJlINJtWCJRMhQXxbugwzZKGkigyZkija0hpTGLMSJomZBTJDZSSZkJCMrxuZ++3IREikpIkYDPa5mUO66pGiihgmYxGZJgwQYQhKKYYGYwwBjMQiTBmSJjRUo/vuPX5+/VtvqtsrKbVexZEFMhAhTMZMCDIqsyfLpEoMAzSKkhIySSIoRmEohsEYSTDMMJCL67XJRDKTAyBmiNBFvVb+vLw9uwREkYgSUxYpGkEzTG9Vu3QKjJJTMGEymTGZAWEtttrq38rK3VZbbzsPpchoYaLJJgQ0po0qZQNgQxN7a6WlNrSWLEySSSlhJKTQwLu5Hy1tcgkPFxGUFmKGSIkRkCSxFIUxkCEQSGI2ZRTQgtSoyJglEyKRCkJQDUfjuGxW4vmdLmLx958vj2vved/QJCEUTCABBGIpMIViqp3plDKc+dkb0nGpM4zlBRM7OnesJoy05aEvnWSQlcFtHixp56ChHYVhdYAWnBeriKD9QBYF2931YVg24CsAYcIvGSlehmsFB12lpYXfV8EY91LNcECAkQUdAISSYSQU6TRP2EZJq7JseVfFct0kAa5W4kgSZgAmFvV56/fdxSXQLyBbPdHE1tsaE8OG50mqaBdPqnhMEyTtOioIbYwj9mZUtttlW1Usosv8xhRVFrmba7ZxnTwBPwE4UqbAP44gT4RSc3GeM1lvulifez8ZzdASTZt8ATMrVpXtLbGoqPW5Ll1BFav4sLC19C1jeig6CWGZmbrRSMo6yyTkmSYJgmxOSk1SoZO0pOBUEOAvArXRlh8NfgoSeH47syLMatOPcntkOxm2lEvQfLgsXsCJBkNi+5StwMBkFwEwK1bAuioF68iHvikjX5BEgugtidIbk9S/WOYm61ZVU5JsmCUmxwTwmxPPxOSUliJtCfEwJwVH1bOyk4qVYL0ECxrmQVLjBoFQVwTgpxY2n3S1NM9zUucPP2DW90YFj2LvzvIL2sKwgyILYCrAoAMb1pwqPej+CHjeMCQgVuhRkDMJmta50xmXnNcZGiKl040ECYFPRhdZJYs63U3LRa1aOWag1ZhrVci39u/nF23VnuusmxMrYuoZyzeBrzvHh/p3FqQvhm1TMHViqifjbFAqTE77ZIFub0Z+v5g5HYacWW/q6MVuB1Qs1SeDMEOuqqO6d59gaTnTqPOtgay6ZO5mpR0WGUN3iu3sG2D7Y928lhJPTmqiLsTMB0VlzNRDuQyeTNUqdvu7anW0GbqVuw8wbvkdV4+xzb0VTokeHhvKXOYe98uzbPwo+Oa2VIHYXvibYd1fweYtFsN2McWSEeHhJmbTpCXg2bI3a3jlB0FSOUjcsiN0lfFaGU2F0sUJxt2gzwtUPAPsc7FznC0QhLq3PdJDeKYxWknu2rzFotwU+y2LR8kzHNoxGElHMWZVaYsDk4quoX2vOW0a5Xe7ajhp2xKKjMwZlkSMFOnV5qx2SKyCRxRbWDlK59qRErsVObrk7nRV6pUlZlUry4lu3J2g7Br3JvVESTEPDwg7rzSreibM4nLMu8RfN2xd2EXyHh4bWqbUuSE9zVO+XHexqY7Crrm3WjOd301GDITHS5Iabh68HHEokmbvpdY+UHd3J9ktrCqD+yjaPvDw1Ck6lffdTiEdDOWvOP2tjw8HtaqXyQvKX1BzCN3VPqJzeN87mbxku91tl3/HJoowO987edd0c5KoKODdzKCCf1uTLSahcWnWnxXPq44spGI5mevDahsKMOK3r3c4W9s1UNsZaSFZ6Ks5ipn4nMNfdmvPvtWS7ZqDw8ML41SoOY7gkYrEFHa4ulxxK3YQcuu2ffL5Tl/mfn03851e2wmeWe/E9hEu6y6LxwXUMGDSs3N0XmPeFE1Cc66Yrs5TH19N9zcpSpHCl7hhmLJV0N28Ap24qFy9zi4Zckrc47mG8/JB3Zr1L838/Py/Z9+CqP4oV+Z+cglxNkmJ7Tms91GaM28e6H2zbs23kcSfjnRR343b52KU4K1KxR7wg30nc67Qso8vdVdlTn9fVyxiteuyI52S5AufyrxYutMN0pyobJ02+p52c5Q3cZ7SqeGhNS7OKfDio73q29rK0cgprMMl1pDtVk2WcxovJSsU+azdcd3N9DfLy07Z2obo2ZGrvonkp2uyeJjZfGjmgs2GJyrakOuVlquNcNrrZFLTMza4Peme61uJ7u8W8ppX2VlLdkTncNtWd5yDNplTnQylQzVWPo6llBJiLadrm281Y/qFlrUoPn9Qdm6PU6tE9PqWQYMHPa7G5205hpA7S2hrtyJmVfJdl694FjLIWlpyCA2bRXdTMWtZeWMMwOO5uXWWGRZl4MXyzZtaOF6G5V5zMYSuZBMqRrEHdVVSUHmVVXHG/IatIpzYEvtXJ9e7Y6LZMw5yF5SO2zfNF8Rr5cwnuLLQ3d2sDnudMrJiMrzMRNWG9TxpVXz7jczOBfWg69SbX2ZSFic1DqNex1q3T3a8eO8003iHJqyhwRyd1LTQJNNY1xqZPbT1J4YbSuwU6UGZm8Fxx/nSZv1Gvz8kP11+ZqOBhLrW+zNuf3/OfXF91fd+zGD+dS2hUXcy08Dt3DzsGU4sOYZ2McejNIkPlStunc06f12DVeTZeR/BsfKzAuvOGJDXWbns+D+fsqDvtX1ur+FyB3ixbY0j5vXY5fKccK260/Z96FVHspByzrFm6w6FmPMmEXF8njicSphjuxNUkOvXWu4kuv1i27PZiazn0uV5cudbBqZyK+XZoWTkctkURc1Ud9alx5PJPl3PqMXl3PhWXsTsm3K0ZfjUgvs4jZjljoTdlHm78yaIku7wORJp5U43MxsGxR1WFNeZ3anXbLlvM2YUzdvJyarearl6qWupxtRX113UwZe0y1NjLu01kt79iZaM+wzRyJBS3DQrIbc1O2CIs/03nnCz4oIPds+6UqKsJQwvBX5bZUa8bqTXvM3zyV1+d1XUUXlVM2YGDaina5nXlTb2a+rNPd2X1FXJvLBaMX+umSKwYhWvHp+1HJ8SJMdd1PG8nahBZ2bcSOUsVwSse8avpzEJ0alWYoREtx3Jt8Me6KxRm4d6F9vmaBre5EdUamK5trF2pt6mVnbHBa6mJks+UuSPknll09EkB2qmLlKmBK4Rtjcpunde8PArCNIRhc5WFWcecIDYmCsrq0Uty3EWTDmuY11CTpVYSqzDY2bu0O44O2u152ZHYKwi70LNCZQlVqYOoWZzjLvxbrpuZjsQYnI0LfO8ushCpduh5era0zSFobYwhA7JN/0u6OIcNjyyaMqbZGfIiCn9vHc2XMogvuRV8aBNRU/JXUEalSZgccl4FJ24MW3ThqsWXkYF8qxXgwPNuGzFRvFpRllOQUtgvBTiLVLcHX5q03zGmnmyhJLx8V128j1W8Mdnocyyf5/zgW8CEJShgYTCSXzP5sNrkaszk57Q5NqF+vETMgpBU7hYkitLNjj9/iNtGSPMMNRnE2UwgdgVwBxKjmWzdt87PaUpNou8FnYfB0MxmykOuac3YgsHh4PalQVdR1bkOmbj4upVydtdByhpPASrlYtzbkd5T5Y2wHd9djGEYqo1R09FPTJhsJVF5sTZXKZiZinpBl6Jc7E9KKsLKftRq06Yu/Ic5R7cztO6xCo92A1fs7gsns525N6m9sW9Xkefqx0s6pKk212mtqlswX1t47dxuVkV3KDd8sU6PZJFuNq6VnJeWrWYFLVROUHUlVLl1bgvRsXNRA50ccJaV7i9LfNeJvdoMGdqMCzMSdbUmK5kFBuy3rkGSxuZqW88wXqVVDgrDPFzpwjlTRBfFjESNeF5arX1Xt5rWSXbzMzqV9G5DLtPcy9RIu1mRHvS20II3t+ce3brZfFg0ZHvbzzXJdnt29vNbVK+mLcdbwR9uUps/qk++1rueWftV/FsccpvQuHh4JGcZf9MbM+T+y7q5FT4H3ZX0S0hajxO3dCHqZTD6g6GFNlU0Db9TU4XoZs1ormhYU4d2PsehOgmx2UPDws4Z2td123nczXcs6C2grewy3g1RsWmuzpTtDmoj2InixRTtUr0xIUZCxKQnXmCa8YgUWQgoiUKdtPE8GZL3u6I4ojQyrtDd7JL0GLpOd0LET6qbHh4atmqFwRRvDNUocQ7RzEjovhI92sxC5zFbtmrQZ5OKdJWu86V2ve7DbDRGHNuhDgzu7hpLNEpvIKsdaJEvnWxzjJxhFc+rMTC7lciSxZtjBg3NViOstVzreGdnQh8Nu+92sdMGBG+El23g5zWY8tN0wi8oyLM3Ghs1xKt5xvqEu+K1PGXOpVSvHne8PA47YRt+mDw8DupwKO+2yHYN3rzobkrIeaVCXtudzeVFCk8iKloLlQeY/y0nZD++539fwxWM+WnVaEyVMHyqzH2ZFFUO1J9XCu375TeSXtKeyzUTWfVUwUKkekNJTnJCRlLqk3U9WBBZYw5M0y3KC56xvJntIuupbj9mOinu52zemTsnJHrV4FuN+21HUuSm9Q6Vrad6sPszYIezLVFi6UFINVNLNx3RBypnYwVKLbeWhfq43rVbZUl028flnp+fY96yvlVs0NGfH4WWlLj4VLGZTf5WTMYe39ezVm29lS38be5lTLBO2SJYPjo+oN2hnuyuRhSalu7RWG2Ik/LuS7MxnL7putCGkWZNvU9dH2HHe9yC2ch4eGTF1ahXFfpDOxv5uWy9Uva75bk8iQXEOT7GeyA52YC2HuzLAsOipUquOOWqa42gbDrlYrcT6kHOayOkdvrzPOeSUGa+nvDw48Q1z3nvbyvL/Ps2Zhb+CR6qZ++NCzLedrMm06l4rUuF7nlbsPIx2WLmVhC0WwcaylC3pdF2DJQlW9OvmulWcK9DNGi3yhnpNvcfLPrqfD47ou2/n9r+Fcd2qxdBfGfrdofCIg9Wl80LN+bbFy1AVBWVlSyLH1QSmIX8hFRusqO4W2ERSwZJRNv3h4GseZs4VnLFrWRSBPZYYge83xN76lyl8PXx87U5crvEUbbk5rxmctjJm1FeM2tRBrcSYmDNRzAKxJa80padqGSpHpW0l+tbt1l7bCK9xXpqKqlz4Ytea8nig3aGZptmdBu3ujduZiOrMkNxop9REvtDdTFkre2VjJ26yGkvEljw8CB7wnMc9o7tFA23z1SU7xVSyOIVKpI7aj3OLvTz1K8b0q5EbQW50W5lLr5bbSw3ctKdIqG6cnOmmY2cGnqL5d++63u9v159FNtdlttaE62ZYuxg6lLqTtw4HXTM28zZbIV1qmdSyrwVMjRGc8qsZzszAdqC+oWcDkbKhe7M7s7DRQR2UhE2DqL9OQwrnmzd3puZt7MB1Haq+zNywp64YzFiQva01MSIsylx222mNDDWU8eixqw5nn17ymrW9LN4Or/L3tQRqXCpH9Z+rOuvq9M3Tw1PtqMq1Br6szNHVmBKtiJj3d5k2CVziqZWBFaS8Ssxb29zVidPbvQ1GrrLfTaUoSXU3HWLNF9NBhrDOIZFXTVKGxmQVjGRTeGyB4MW4a2VEV272bdWqqoJGNxSq4zRnbqyWFgqURL85yxkrpe85BXXBXazpwhHcrEKnN2xFw6NsRSpBT3sqB121hm4W8B4jnyvcNbs3ZZUyzrnQLILmw7vaVxXJjIDKBx9S49iw9tt9svnuHIqj3Vl28dzNDK6o/VE1tbWlQnMQvY6vN1QTTuguuWA5BXc6SmZeyHT2QjD1LSDE7fUF1aOPZYvXrx9vXa01m4NRbazfw6ORBEQt1ipbGiLO3aCWY5E4uHh4ZJf1RvDs29vYHdrshFurfPEG+iWc8uZSBPEY9srIMuaXagYe0xT4iNybHjettZ165L4V0+iwj4b9VFY50mZ33C7uRi/8mjM1ZpWLO9KmMPFsxS3fxRC8GYUgsWH/Xi74fnHzxq81eFNC8Xti16tZSy/Mpt+u+awjzdA8JSTJUiR+dk749Jy4192Q+D+f75hkFvh4eDn6FQd+sQvPryru0kqVdeFowPA92zmzMarlTnanvsTYvMHGXIdvLM4vXVSOclSkRB7b3FtKqCd3D2w1jYL40tku9obqgQtrzYKP8tbj37ty6JUTt3wpPXmoqp81VPHG0OhzZd4MqsbTdytT9YJvtlvK6PNy+aq9RFjJlUDsxhYxQlp1tMu0BSbo5G6mK7xQhXpsraNyZkgxrdG4VeZmHdg8PDGCndJDsyqmhLdBZdtJcYXkdGg7FqyOlBUptlTrrTq0UNrY1s7ZNzOpyjGHMoFUJoRHLQdSYUY/DA+sNOmuvfqXxi9Yv5vikL7o9q8nRpujhdQJ1Wa1l7S1PEdw9KF65SCrCQje4aHcceHLikIPcxgTVGHEopNdUdMpcrydzHYNze3N4jIUcuMKnNqn5lyZsd0sG9X4GoWfiH9pVO+zo/lpZoIPdDT48+0yyKlcczBWbsNKPsq37LQqhNvGm9kaDR2Cs51W4N5R6KLOLCCtpt9C9gQuNvKO5ku919uM3Fssc0qc1dumww3HQk15azKy+u7VpsZHu4ddx5SSyC8qXbQ7tJFh2YayhIILwZSG1ANWKU+BiyyqDyGKSDXUOjktI1a8sQ+p3G2m4JdwUuitNjYiUdqrygX3a8wGxsYWXfyH3DFhX2D5/Q/XYjVXBeVz4Oum6u4RPMxXubomw6WycuScxDfFq77LEMZWvX9tijnI8XFtB+K7468r7BOmiozfR9BXS5gZrWrakhyr71ZL2sWpmtGVyqdLuUWJQ8PCpAekrOXC9Y3J1maWjG2eYxqTq3NcMKl3kGa4OxDBvLTRzQZdK21pmWZai2yLWyJy2F2JMFGNGa8Ulh4h4eG8aq12rJseDRZXWQ6IVlzNqssYLhhgzGHhd0+x9L4rRlXZrIgbzygPSUNxkSsm5JLDQYqFtRh1mzgbg2uPYbKGPWnvdylyoaI2+y4MRhO5HevnWOZKkJVRrbsOS7y8OfXk++PMVWWb2+MDw3c0Ypv2TsfDHND7lK3ezoZzwZ7srfVqOxCVd1lV1yR9s7VzgocHw8PDFOznMrHDioLIluZXdim0reNCFS7PdR3NOixtbxBfP2MSo4IMVsPsHJPqEpwWVRpLNSkqvc9RWujIszemy3OPZ2ysfA+7HU3cVLLzMcBOAkvKyF0hPWIeV0qhhysmUiin8tonc2FoNOy65csouL7lbRH3xlJHV0R4d1izvXl9XdOyVbZ3hvSULGOufcngu2LymM2FTLZo1udrdYemmgndlHr1jmRuZu87avJmzpWYrgLsHLlzOy6lvLlhGOYFztkeHhyqZbw2lojQe9o2+s0dYJPPS6n6yVCO4jJvG28Rf0zU9t3pZeWTRGGlZ6YnS8+Nzz12G3Qk0RSVEb2ruVWIht82XRF8pl13Ea4X3crhei/xI4Mrsr4m9vsQstWxDSmZvfeGoHGTFYMve9JvVqSV1SIXT+L0T9bgrt2zgsraNnSJBsK1IWmG6y9Yg2bdoF9fB9XdW7msd+9Dc6fcPqu82M/Hjs3e7NuoN3bU68e2nrGM0UnV7lM8tuR6kMhLJF8bfaepTa2ArsGayuqzSJOwNvguzp0BGkFU0EdOK9eWmU5A+qzJLU3MqSjNNddChXI7Xd+1oJHwphL5Pb6ciWGGnLFWLh6KXVjNLuI7cmKaW6qpK2z+HRfd2Xa58SWIIoJse3UxbM3cy28/NFzi7iqZZ3CWXeX1MXyMnG8xjLE4ZsejLV5jFEGpTtEbFiQxArS1L6skNqG8sdQ4UdOuGNdc7L65qIZR3udF7e2senXq6CnXCuDzSpThaen77tqdNvfsFfd82wbrHmCfblH4l5dB4VC8dvd51up7pub7dF42zbp4tx0clfjndY94DiB1KOLY6rpHtP16p9jJvb89SF1BkpmyV1K57w8Jl3iHbc11SfRRTmd6BVA2jjF6487sy9wdmSoeV0R1jC1seac72Or5brGvn6LOPPq2IKzu0NkrQSTWTE89HhPFjLfb1XKHVOnRyzxjQZ7Q+26eM9dVdndsQPJvDOmgw889yzWtojpO9xaM0KnFkzBrvMUm7GktGdqlaX4pi5LXu7XuOXBm8CjNsVLQZtDe10wmD2pB492uLJ0qwtM3HeU+M2y50PF49qXA9qPlC+yCmNo9zGjldFVy5Ytq96PgtVs5LsdIqmod1lzE0xyj3Uhdtmo/Gx4eDV7JOSuFuhZsqGyNFBdU1Yk84eHhbxYY7vuRpzEsD7fzq+YVWH0+S769YIz57QhVBc/s+E+wL5FJESq+nzHCUytrfCxNaoU9v63vP93/Fgebbg8B/NIDwkrurtQdNYfmYYGCvw1f6MMtfk5fgHve9B/MHnfH8/vyI0sifdoPkiE1XFstSRQUETSlJEyaZKUEoRCESAZAmGaCZFMECCbBTMAxiEiUYgDMAwSkiWMSgSfjq5DBNEw00BImaUlINDMY2MQFiQ0YgzMUU0jKv1WdPxW7imgk/tdTYzRM7tyGRUlJkYRQZFrTMihJsZEzNCImYMlhCRliAExhMQEJEEMyQRCOXMihgwZJMNCQE2tMJGYSiImkCZIqWJkRRQhSZlRMeq3XX6rOmBmDGpmWVEmKExERKf45EoBRJUgQAAyRiJAQySM1IkmZMyFKbuuZBhBhQGhMUGFKkMsNk0BRiMQyzRAiCGGRJCyMxgEmBhpBTEMyYSEC4fJflPg80sw1ay1Jmv7NVeSOMnD/EZva3GY9dOkGEdWVDkgNbu0ZZEl3dJZNSkmXuODS28M3KxlKt2rQ1Qg+1q1t1ESYjjKeuy/NDdF4rFa/w1i7tgZMW6lUl9hhqut8s1bs686vW3jGDNvNOBYw9W5uEMRCU8q2rFDw8LqOLdp0VDgimWMadTdRy9VRGleSOtOYHLqZb15e5TyMuYdFmYM2SyctCre3ohy3SHh4bVSaQrBlaTGxKV1Mj2z3CDYKtJK3ylvZDf8NVNzTev7LiZbdwE1ewpCZFis2d3YXKEnmQKM8qRR0mTLmFY6dFZV0URNzFTvTrBxymdpa0MNxjW4cxzCle3eKbsIrU4qat2ZiGUPsWyFDJTTIYeFsu3IkVUK6iqrwHv0fAe4ge9o3F29Ap8Irjjjz7Vgra8YELpM+RRJAKFIOnqQWx7LLVrMuyS1SoXj4y0Vd0/eA8iEWBlc9nnC2qxrJloV2pF6pr3Fty9l7lp4mLtrRtQ5L3TmDW5TZJBBqqo4lupqgiZeuCqKmMw1Vj0pUcBGGH3tDsHZBBfUGcCD23XcOFXAsqrBC15bcZlFpnzWZmSpsyBxXtsokakKd5scFLRoO3yjniitsGjrUq5b185YvIZclVdEEPKO0z6SUHUFkO7YsEasd5jgqm2lgpkweHhiIlES7xRwOlTRS3fzeagyyc5VWR4ZHWYzS6Ojo2ro49UGymkttuWtWGHICNGqq2gsCVObAynp+jZQ3K5prRQkNqtmF2MuJVDF2LQhSgrYg61wsukkJKrXpd4buBC1kTulaORaqLFpFGRV4AAonQS3FpsNi9qYdIIwY9oEaQC5FdjMp+RGkFynrxujaoH0UEVY3YGkBM66pTVEEnarIw0zZWqnGxT01RlloAexOFU6tvaFWJfgFQwv1qC5ekBmtavb1liN+m6Lx0FJTs0GvA5TRCh1ZL9byko7onPQW3ro3q2s3RV1hGx6S6h9YqSJbeR3lyWJRMVXmYnFUpSWL2ApmYno0jBdHMd4yFpcMOSCIZKGmVdmvGylsLsSkCt0SqBkqQa26wiZq16ReqnFuJGHN3JlmY8EDsF6m7t3HKN1m0YiQauohTpQ4s8aibWHK2SEG8uqRirCoIlWUwmrYjeFrTa28CC3R4eCeityHJWOXso1tDA5cSDDtXVR087dqGxuncBLoxQQRinMrFOpDZeB5UAZ1LDBubSpu8vCXVsbJpYoOqXqQcimrXjlFsHUKjem5RVOS0etZj4NVHkklXoq6KeMqgsJaJuHHwx0STQeTdYWww3hpRnXQpUHhsEU5RUqqNiY/f3+xCQAT4+I8QT4kDFGGyaEhQoZkiEbFDMwSZNITEEzZhIySGjIIijKMUikjGJLMooBUURQSTRCKBmUjMgJSKERkZMhJRAbWv2rsJgmzMxgZfP+eeQRSBM2FMEIFJMDCRJmCYJmNJZA0BAgQGNlIxEzMiUMCbMYpRBgWMoqYJNIiyBIhIkkRACLJZMWUiJAYIlIpTZIhCCwJiYjMQbMB1ZxqflSnCWYrUdfflUlLr9IKRZMYTZ2+e5na5BlxrTcNrQU1BdxJ7UHh4G7HZ25YN1U3cqxqc3pa5vR3JW6GEMydnSUZKdG2d1E4CSTa/f6lEnfubf4qGn8q7s1DMheZn2NgN1E8Qel3WXd5tBiEv819A4VXrvSqB1pKdThJh6bdW71+x13XjghyxNjDwI8ZFlzqxNrAcLmQVTvITQkvMfth6aL7sOA2tDrTA6cs7d480yY9usaWbR4hY5xXtITN3K1oPRLMdI3nQFQot5oe3beGp0dWMPiCCSQCGyllApIxCIkIkSfEAk+M7fwhmX2tnEOoRhX2m7yMWZp20JuHQzstPzXMMgQECu+wXe/R0CC/u5YlMu1fZMvTTS0Mkyup0S7yUCxGQzdvR4eFLLyNrLMDoaqGMGxuvHL3p996t6AMpCRgZkRNAobt2b7BJ2onEVBNuYKas77CSDCEdXTDGai15SiC8JZJRWcNYNuygvNbd/P18/f19by++76fHnAlJCRiTYMAiBD6vjt5JI9Xh7+/L4vpe03mR6hVTrK7hm9XFzLDQYrT2YNERKVEQHaGunjdTdqgcZehSRCrgzB2TKd1UqlZNVM9hhSCrewuKTEi7IwM80Z1PSxhE6w082zuo6MphTdllsONF1tMnIFCubjJpA3SDIw9u12ZUxund0Kw5ucITuVWaMT3SeoNFkKUjtioW0rTgUZ9LvpmA9N1i1iGcUb13o3Lzqt1LgaNqqLVxdLkTYwXL7dzMzXleuh2sVG4EapGkFEFx2QPlss5uV6ggcvwJACaRiiTFQZH113ny+nvz5nXk7S+NWo7knbXacAyAtl4QI3TZV46E+76Tx36tSHveD8ffDn9hwusvBr6nXu9lU+0XX5EMlGsIQJHzQLpmvtlt30vGGWGmEhRLnrG7ReXlS0wQCQSSCfEA+JBIPiASfH1B0aa1krGNwRYFWk1TqhXjGKFEhEkflPk+rlAfRY2rJEdcFA6E53QHvDAdIWWnn6YYJdfuqBBBIJBJSGUyDDSUkQIsfp2KIBpIEWGkxkiJEJMMZkgBJsEEUFLENfruiQslEBJDKUhXjkixZYhIyEgKImMJmkEUjIGiWUwgYhMpGGSUTRKGMwmJkwTZZMQkQcukAEJN45CZoGRIiGGgMlCkBGIySMgSSjCKRoJrMpqsWSBJGxIZCRTGpkzDKMFMxZSEmSESQghkQEjRkwhJpCACGJolkRoyYUkoTIoYJDGBGzGCNMNRSJG8dmRJQJIIyGUklqyyyrVqqUtWXTfb99114x9zjOdML9+vj6+J758Ph835SQgAjEWSMaMpmLJJCCR+u3BlIphmKEyMSQC1LVtq2223OhP0ccd69da/2z3XD3lsmRtkzKBvtPtXr0i7tqOdpL2sPR3itR7u9pqNJvdgoybNShptUaqUG1f132ZydBNN9rPVig7Sd3Q9GEVf9z6sBIy5jSjp24bnZhl+l/VdHW0nqrEnMyTcLVh/OrnDMwzMmjw8MBTaYqkC1hubJlwEhy4MJ5I9Z2PNFHKg2gtmr3LNbFo9izNocY6BZh4A3TzGbcWlLe6U87sGrfdzrKV4gxy6+znRGHaSL54ptkitgxhY5rXvDwalu5iikU5vIKvq8geQvSXoq6YdzdG7kIS3HjKunuY8meKObitWbbt1HDg8PBSgq6e0x45t4z1et7mdgbF1lacEd5uXZCObwcltVZuVTRBeSUMzS42TUGHq5sWWZde7tidE4tvu3Fpu9onjJknHet8950jXK330dzr5ga8U+LeVUWiE4JQ2InM9nskCw4ZMqr1LNsGaUd+uSYSGdkl0NuiwXeWTTfHulCYL5Dic8sy5W7qzddnnQqew2S5vOM5e1lvlhN3XNpOOkLWLDV6nd067nwOK41Y1v4ufR657bpnqs5gUPfb21u1CLJjmjN0PhFmS3j6x3PZlvNeyIXaYmnbQnHt3s1bVbyRFxYpi5hkjsqRVm3FbZWrsj4Y9QimWRBY8PBestCtBDlMFDNbmV2aIyxpdULD24DMpDOKHh4JY3qyPFsHh4Zto8Iu3JFvsd63MeCjbgdWIhqiu9ptZ20qutm7eiCCBSgcwJ63NIpIXbBGS9mumEKKeUE/tfDR1PKvlJFctRqj9Z+u/uw71LRu3Gr6oGwK3c2Ej7vq1FbnQP3w++rjWYU9Jk6JZy7Mx1LZ6ncXXvaFoc2oQhClrm3UXU82vZiuL3a5YhFylV29KveRiMa3ozKw2Y3QUqVdVjBfU8PbN4Tb48+Ita9sWuSM0Uny4M6S1VSt6MHM5Sgtq2JJYyrLvxynyRD9WqzHK9zCMVV6GMZVpCiLNe+kvYV9np8RmIT68khKP2bOhQOJVeRjRFY7JVg1Hz7hsNPpp15CXphFKXVTmu2tNx9fMYcuLO7rv1rtPJ8C2hCmejlOchuDnrh7Ie94eFBs70rp5y2tKBy9bpmlY8PDcN8YXVIx1WGbdW+WUaeiZdnzF02Ji3Rqg44fSy+TquvWZmPqiXbmqsSA11j2ZenqPGZfCA4cbvYuTnDaM0dTF3GpTYLnVrJF5ofSAkXuCu6nV1aiswMMu2kHmI8pcvcGXdR3uEwGXgcuWQ1e1ZeCVHcyZglMoukLDLHFi8tiwqlVXV7ictbbu7eUjnYI5fYGQ4694eEIglNnlzhcm3alNohooT4rFuJyqTeVXwvDdYR2HJvy4ak6Wl19iq7f2r66k+EGufZt4U1S3mPDwq8RVYOZqGXUuHtbrBrKjXDGHdlDXdZ3CCTRsqIS7mKbuOMb9Pvn9r1BtfdQKjVwu+v1gu1vdxvO+bHWEKzmGkpj7nOxNM7nDDDBYMyXlkfuxhi+d6h30t/Fm1mP6yXncY9mHasN1b66PYNXnQ/jvvqc34HbnWqpqnJasT7dFZKYL4iSamDOhFddaF3XSwaT0116A1TtnIlS2pWYJLLenjiLfPeIXYDHVKrBoM0kW9dXIo1IE6QtuGG/LpZ0Gy37Mozpq/TmB/PUjPtHVPsOXjU0S87rzTeK6sXBRgyXeSNZSaXPclEZVJpJA0qx1M2rDdabGiCYz63h03j0Vvd7w8HMBNxs47snG9YophWdDyoKbQ5PV1aW9rJQ67u1DWzm1JDVdLGItTN6qw3b5HOybBgk00ykCTbNDXeOhizMKeYhCjFOE14Kl7rzWSU6N0bl83dynPqHXDJxqbkrtvMqeuBQfCXeLc3s5iiY3ydHldDKXP0OCBby4E7CHTzLfRCUDRwrx67oy08QqbNh0w7RBiFVElpsUiW0Ucpkb1A77L0XouuMrU9Mc1veqC+CcxVlZmbGsrM3jHI0OplcrTyQRnl1OQ+VR7rxHIGPlnZed2DAfZLP0+oTKHC83p3NPD1U9mBGtmOnjEFvqWDLDb9feJzKgoHsuuQiV8/7qVqX3w7h22wmbMjOZcJqT7NztQbys/wz7klhd0lH9oRSXL9XtQzcNH8vt+pzB2bM63mbHtZS7qq8my8pjOHOHpbuj1JvZbF3SLHh4dnZbyjePI3jFUhNU9ZkQfYM7UQzcy3otgwKYNV923lhlZqLOMUEqi1VzpXBm8XvZu7lv96PsRT4RagPpPgbAjm5YyVbUwV86uTDEkc0EOOKsGbdHqudCxF5sc7wbBrxDVm1Zs3NoCTxfb4Zke5kLo6N3zgzKUvcFnJh3PSFIX7DXGSiGs1eVHa1ycl/b+wn8fn8/1XP2se1rfveSDteaedUhou191acNM24V3IkBtAgFiut28a1D+515kgpG7D6gjXe6rdmcvT7CaMtPEHULpFVMuutfdIrkhLafdbYt6LLaGPTHMEpG2HwnKUd7UzB3Uc3EVCLzOGEUI3Xa6fSPZXBx7S7KraRbst8bxF2UqxHayu7K3LwToKAb6xUkF4aqVVyRbgj0Mt1rvBrO3RiyZejCczevhmcXQtNQ6ug0twWnZx3bt8YpTqhNOS+HGlVisrFqwGca5mnvW4N7dhkpkLzu8nbambWa8eS5dRtYiVbzt7aYnTobEfCqtkMx5eZhq9mt6qq3byEVb69zc/m38kZbp/GsHZ459nxEMCwWTmNBWoowZaUcZ9xFwhmMndRzBWS5VmUcrKvkMxmhu73TcUvbwmGYOEBHh4c1lm0RmGOx0zmttiBS7rSLxOrltliVhrYjnRaOnW+zTVd2kbgzlmXUJDyR63I3m4bczVQ8PCMN+I1zZDZu0C1Y16+zizxnG68PAZwU7pcbZ3jbzQx4eDmg4uKlGTNUvdgjDX2vVPvuN19kguE2ZQqbyOZYX2ag+i2/se9HgWck8kxDrGRyr4Jb0Eok5dXuRZ0SZyF6Y7OKjoZ0Ur3WdFdFtnXTYNXhOqrJItd8V9DxbHdDSIocXeLOf3X16GOL2Sqlg2VmKDaLzenvDwarE+lp7U5VZ7tE6Dmd1sVWCg1dQ8rdmSuK3as9C3mRjtqV0qusSQvyvI5qoN33bVRHaoOLzDmMPJbqk8KT1hBJOkIMV5GcFqcqMKaBsIpagUSXpqpq6TJHaM3DmtVAb3Mq8x7E+izq24bKMVM6OITNOcekfvDw62LoTC4tlZmtneFOJcL4YNO67wYzYblglBI3gvKJa2jokIN4bTB8sC1Q+hBg7aukzuA7LrmTp5tZ3WELQeSA71R3gNNZGdEDHLR4eG7etTO61xHNbNresvqat23bL1pVn0F3K7WKcJrfvqyxfxx/OdCdxQbdVCqVUnxqQgyfLZpqatr4pixxI4J4msymY+rXgchJtCauMybSqgUnwRu9ktU1u1oePqgopeyuVxILNaQJaWUNg1joMXK4K3d3Bi4tino8PDdqpMadLKdnqqCW1VGmkRc5XKLV5zLindIsIx1Tj5g7eI+xoO6PbAU4c0Q1ZmZbxO63Bmy68haytMg3jka3fNsiiq8UnhXY64OrOLUGbfN3cXJt3Ursx5WS+cvA6nYZzdsts+zMlsHA+mYtjWZ1phIWZnXTuW2TQKjw07IvdG524Cg450yb0Eq8zehw3u8ejXLcyT0ErBOsQXUzE0oPDwx1UzescCPDw43UNzbS3BKhDjJvIxXRA3ENV7DEtztJ9e9BkyjMs1pcmHEnCx2Se11SpQrm9LPe2mxWPIFlOKwyj7rEJqtqMeHhqoHEG0LdHLmxatE0I9aT3GOyIrDYJTiuoHebssctruW46nRSsMFN3grnzsLq0coJigq7t1MvRPdtW5uGsqb3TMSWPBBUm3SzKkjmXd1lHKU4qtrWuagS1SToh/A/f9foT8vK/X6f35VOL9Z6QU8ijdVIMtA7mS9h7OvlNg3Xg408p0d3kO6ZsfJ6Xt10tkaVR/ebUAhGHtWSs7GnVx23hg+N7c0duic5uULmVvYUsy9Mo43MkoBFG7BzkHltOY6LliPGLfZuULwPHDnqt0MoIRuXM3Zgo3owrGfeHhha3DHqqApKEMnWq01IuJjLtuWLD2Nbuvu9ryxQlDJ1qyFZslcxWQUbNWnrKmXZ5SdBlOxg3Eg3lE05uZE4RZxLngpDTLI0c/XTl8x3cjzejtQh60tw1SnVg7iCXlodOLJKtzt1lbDqa8uCrelFElLQXOVW+k3pOHbXE7w7HNtwbmnc6d2vBfbqO1SDGyzBuesNARainRQkgzVcYrVo0VsfYmreUtnLn2VmPrB609vXbVC4ipIXQXXursO7r00YMyXlA2UrSXEOmpkYrA32Tbmbx/vFbhPQdxyZWYRKxFW0PDwdqnOZVo6WbPrTYMjs6CvyLYqQX52Bxg8wer13T1zR2tj66stNzN+j0P7eqd1W9YquvMa3cqS1F1UarahvI5u+oGdrYPBbKfP0l8cph3qVu994eDcoXJeLFHMuyDedSax9s2+eHbLrWLte3t68u9OUNe5re1rkoTbG9lRxy1J81C+Sedyj++NHr0Vz68pMjMhk6sVBt428685PNWGigRmYcNh52VHmmtrd3dzTE0MobLDKC6IpbWK+1zN1Jt1TVFjYMH2Zkx2en3LMlY9LdZMNa3K0Ebj2ZebeC9IvN9cvRceD5ayjeTomFWi0DFV5HzFtTZ2Vj0G3xeXN4zEejudwuy+nJ1mzWqx9t8bmZN0uoLymOUyesN5S6h3BWUsivKmk5Tn6f8pLx4fz/GTGsVmNRyZK7tFGHysfUP937kPyuRi1aiwZaUiwGxiImaMiRiiwzKKREkrNGZGl89zTA0KCSijGqzMmISEyTUn+LpKRSUxkRIJgoNkNSJEyCDDIEIgiJCYFMaaDDGihqTRGTNIT6rLa6In6+rvGGJlDMYmGQmkJBkkxUSRklI0KSjJLAyEj33SSgSlKMKJlCwpLRDJkQ0kAkzYJiJEmNTJiwyDEIJopRiZJpmjMxmJERKamlCwQmAZlEyIMYwTxPv4aoH37z+HywWl+V74E47lk4zUYrpu3GsQv93tmyMYZeyFhbtCVjzHaW6FpYRaKc3FVcOTRvbum+5lOxIZ1qtgTquzNN4LoKnl+3BSLm9ezb6BrFYdQvH7OmnYi6UFA1kS1EKiF2Y/FTcrYedHVGgIIH0TeG9zbx5V1xXbkE2vPrlPFj0dMblYKuWKxQHaZzK7ap3w4dJSjVMyYrOTZpV0seLWR4eFZIkMFmU83jtBRnSgrJp2+IkByFM7pmh3iRi7RF1dtDlFokkzK3ou0GN5aojlmG7IIq8bVqpcxv2ZZrKJsUud7xYnWXFmzA500PYmKvVFv9W39g2181QUnzwVEUD9lPNr4LRtYOzaMyLozZa5915ZXPlN2n1dfPa4b0dQ7zObUiyoq6lZs4q0V5vE1Nrb3kN5SWVbnc8fBceyrVp9MOb3DrZJJJmVBpFDMGZkMwRiQCRSNThxYcuxxw8oK1qtwZTNzMeEHxIRs1aVCFsMnxNG2E5nUMlsiragtDi3S2ljegp1qW634aRjThb0ENYncclbS493LYKqxxmYNWzll5rZol6+q89e/Xed58+e/eSBFCZUxQp6XTYqqjvUaQl7b3Q3HoRJJprSBQLR5AUD4kE+JPmMg17zpp2KTGbKb9VvXj9ZBIuqmPs94eGY6rde1bjzwP6KBB8GKSSI2UTYSUCR97+kEAfA+xW8YZWN/k/FFTNG7xVlMs22fw5Uy3TkMwfjfSzfarvCPDwuDLOdtDuvTCi5U8jKPHDnRsLRzXctwigduNXsvJtwoNRuvZeiMaIuDFms2rR26lMK+ET7G6O3HWmrNMVAu1x1MqJ3Vt5Wcl0wY+rA1pVoKZWBM1ukrNqrYyEVkDo0DJpGNp9bIl22R3QUqwtnKFet0axBluKhvU800cNmqGpU9kt8w6wb2tNTdWjaJMKrvnC4736S2klYbpBU97T8aFCTO3bc+qXSWbtfPCIfrRr7G2M9urPn18+/r0+/n18VETBIFGEkASSQfEHwIJGNkEWV2anu08nStWPtvCL55yUPgQ92CQhkleJxafeoAjFgtahrshaHWcpYvEHrOlnxBJJXKryYdNjRxcVyyTpY1XZitMEEkgggkAkggggnw8QQCQSSQ1pTp68ccZeUK8SvlRyZlEn2HWi8KDPKpuUPithx2xsrTKDi6hMobICFfqq7sMyGyq4S468SmDwHjsIUPrRtxnTG5V1KG4k3vCtudnLx5vqGuI8+zCna3LG6NUre2tl5vDdwGOvHh+qguxz1VZvdLnXGW82jMatM+t5Ax8lYuY9lkPt2q07uWS6DO7hFIFuK9SnJ93RWyd0y33MRKbR05MLo7uHe49WVSdAHItayPSZdtVEmzbSmZnS6I8PDU4wVq3Dtdq3T0d97LUrNzUJHSaeNrqth0nusFpJHHjuRdmB7naF+KMfaQjvxCLHwxlKUp1MZjwbvw3qM63TCXVnYnj7XV5kvel7eukadMt1rOlB09FbJWYU9zQdtNdKpBZ1CiqbamytQzCsVTEemR5eDAq4uZlvlVp6RZYSlYQfGlgpTBHS2sd+dHmxeOSDSVJTCS7kXjTSYda8MkcqW2I4RsVzGLl0OIGeCQTTKZJKM0ZH47lEkJJkzTRGSBEEJIDMCJoaS5yiUsIYzQxTRL+S6ppmIhk0qMppSkYpGYNQGGEQAolLNhhsowyQSiaEEwhkPdbumMBZsmQI/11z863S1dxpRKCMYhlFGAjZSKSUtI0QQoMkwoCIiSGREkQJsxlDSQ0ygCSJJiiyjEmkXW7mjQU2AkbFKQMjHw5p5TdiMBBTRSYQpAYb+dckZjISM82r99eED3h97fyDw8Pyo3+9/a1a6x2dthQae2rchphEsu1RzWgglhrrCfsCxPIbidUSupSXkyVhD6tlNAjC7LrcUOZeI3Ikkgiq9M3LIde7ZMzahM6UiZUq1NWrKfuxN9rtrJVSa6699zVQN9b7FnVmTM6Ybi3WsYfN5PfydD6bhsVtVPvh+sH16M+48EC+Vv7iqEqU6sT57B9a1zNWWj85F9lyA7mRDLe1JgkTF0Fjzus7fay9NHsV5Pru91hHBDWr6sqHKqN4smYpSnhHVNqvjlOoWqz6+vQ3NiFAh1aSY2q/C4CfFEMFhmQYJAxIEKYkZiIzT7+fL49r8fjx+Pny78S9o027FsdjtvrPFzsoYBXPYrIJBBJVqj4kFnlNUGvsO1vJ5e+VXFrm4uqhVpu2DNoUAZdseHhL6qyWxB17BFcLSI56q6DE+0EVkVafE+BJJ/CiQT4g+BJ8SCSACASSKJ1u4c2c4ZhYwJQZF9zwTRh8r67YNbmkDSLSxoNoY6FNUMdzM1wInQWEqVuUGCRr3blm9jZBGd8efPv19evh5GKSRSZDSShAzCYYkhvz6+PPQIj775+5zHD7ZzBtde1qkx0SM1nOTFZnagsd520TW5Qe63cPrOdjzb18ha00OGSRYQySXOe6LlHAl0nBUKRdPpSsPZkC26VS5obehiNS5tvN6OtvKoNB9dqt2s66pQ4wXedCMUoQyTTKLh6UKIwlT2vXhdyeYV9vdehg9CZoy+Hh4ZjlmTsRfbe2d5RJ9Exw3jtZzSiL1E4UDM43NiUWurjyB4uSGVVPdFy4G9usBeK0psOeOW46yhlkdpLp6qWKYdsSIBlxQnCcqY4qs2Ntv21DsuaCSSSSSQQfA+JJJBBBBBO1024u3Ttvpt2E4lU0jO0adq7mQVYMWtl2QiTGsJ7BeXDiDTDj21IKnuW0IrnWLEUvuF2NB89wVQ0MeHhKiqnZkrRqN7VlR2uYqjMebojoT5+/b49sYTICDMMDMID4rsRF1nCkntHaYOPS5ZZANvKRGR62fZ4kHSNx0PDws91aOGeeYdgUaWHqFRSFzm4lHdQtQD37dEGf4/7Y8AMA97UAmBJPul/na/s81r3fegGvnde08Q0TduMzZbvrr0ZZZ05KrYCwm9MMo369e+u/iwLMawgIqxTHOk2btLoQiJyXRBRl2lbd/C08YNPRTEOXMdrN0eHgr/jFszc6Y7aG2XWmnhtizcFUsyEzE31VI+hYrDgV1ceyoGderM7CtlVbcOi8bDYxijL3FDFlMH2Mdox9tOkFhW7KYLxQYZYqtzSnsvRdZxjwZqwmzZ7k4rbuLd11REYhU0vrXkk5UW9Osl32zVS9QYgsRq+t8gJyQpd17smIR0eYo3xFbi5um+uFyDuO0plDgtXMaM3e2Cs7L3LyDXaQuBKV1mi3jvlzBxnZKOSku2EsGu66cnDlw0aLGZYqtCDBTuMcYXvEcc3o+6cYqHh4LKfSAyaKQO+IHmod5yELpkuC1pvyB4FcbXBGW3HuXurarH0mGR5w7qfVfI9mjexqBlrcvWu4VRG6+FjCdMLyNqOUd0gu+uuFjembHFY8PCzmXWyHQj2V19s1tTsVntey9FOOcqMsWCJSZFxdeGmEsF8Vs1YdM6TO7inWVadhUTdsVvKYYu3Kj2Tb6VO2gmO4dcuZiV8BLNXsx7RaWK82xtFOPzB6xzhV7t3xu9jK5SnkN3lHd589LTZ3L3WqJ4wqzl0wjJrzWwkc5yXtc82mqrLsVkiqdOmbYqJ9ZMwJSklILtYCx4eFYMo9dntStog97w8GKx0psJI3mDMmoXz7TTSwWzuTcvtb3UFnEIQCOh2Xmms49aEk7Hfa8MnbQy3aeayLRML7JzdSS09lDNqsyjruwptBB5jM0zxHXUqxgno6edJVs7jmHt3tNs3eDcLfumHs6Lx08socUUI84DScW2RmTYiVxUl0kMQNjH1mVdzsunnXXTZSHh4cvBa4pZSmdjwYcpC7GyqOW/GDXSjxtZmZka5psLOQevT2xwd2makOEq1VYJsVXyPVlt5iZrQeqsznRbdElUGGpzpJmhhyLShukeHgbDIhoneLkAyrMMeevqVbj2GM9Y5HmsTNZjFuOG7j9VYChaEqZkmVemxmr0WZFwxZg4EmxWTGjBvbOy2Abyry7mPOWISSqmIXRWMtXt9nEOJTRYxmG68Wr3APWjWJYG8Crm7ul2Rw6k062zuMHrbdc/3NmJtbtK6sqxu5iP1fbMPo6Wq0O0bRP8PXnYVdd8pPjsIW0SY2/pdZlY2u0VL07vYcp1fDry9HgE7hcohu7D2zmrrvmjNmyzkrTLm4t3BfWra5boVOcz2+11DXvusULtBE98FXYmLfyGQL4x9jQ21FxVh5aZlKcKwwXTPjUEzdzTpq0LzhvvDwJydRN9JXGTOFq044WOu9uKjbJbuFSngN3vpgEsRmrxA8ZlyWxyc0mXyl7VAx83bR5DTtrErkqCVfquygXevHZHYJimupZrY2ZsLlSUQlDo2htkR7ZVSCPHkT+enerh3CpXYby/tMmN1RvUH9mPKVmanhrO6S+dZ7s0rlYrbshjtKHac26cOZgjc04N3jMrRi9iFLSgRbkdzEY5T2GRCbQdNMUeuu6qNI73S4D2SblrT044c7WhvNrRfDw94IALA1JRKJRKJQN6jIkPTBgsgn8wTZztpj9sgIbwSSkI0xnG8tBcim2aIpDbq8ViTbvF8WlqyWYcZExhJUnpVUtdQq2b5DZUa5aulCCrTF883Rul02c2IUUn19uZozyaCNEU6WOmgqLewnKt7gcNWXaHVcdVuFTAhqroNetdhe4C1tC+eUsfP908Ng5tERp1dJ/QJJdBGDhbHZHFK1GjmUFhrGfqpE4XXDgrGk6LG+nEvak6Vr7G8nV12+NGjmYzTk5zSjUWuisr1B1qJ0OEw11WYCJ2drqMAjOoVmWIIdEkqojJ2DLfmaMUdqdnnJID29rlriNxXB21mPLqYzc1O+QqylbgV1eZMdimc0YOiap0xW5QWCqkSzIuqOaEytO1SdSrRMcymduEnxL1K4nT6MwnbvTCHdxUuM1SkGhVGTKOS2Tgqh23b5TaJByHRwT7uTdSYoVWGG5tXg2z0dUYw70QncpoXIcc7dUCuR1eVdpdPY8OsUOmNO4hceuYDfci+NuX2XUbvHtMwWiydouLKVs5fevepxmqgOBrCZdOUa0ZeOXWLGEjMds1G2vdJm5nXs6rrjz0mlgeNFFnKmydUnIo9XL59yRVzKzl8VW3lx7PpLvG42bhvavQSelwdtuoqVRdgiuY6zpQajIGyPIpaWnddqlMEeZuFKmCnb0Grjq325dRaWBsmJUqq6yjCI3j6M7SrDA9E1JP2mjUrPbnZUOMutFYDhzMVSZMlZMBlJqz1OLftwaLwPPjfcb25LLNfXTvaw5m73dUeTOeUHI7uZVR52WCR3bR3F2KLnOadGlRMGorSs93Dw8M2duXy6tbPHMw5tkaNeZiDgi4tulVKUhGpapbRObqq60jNfTppuueio2063REuG1FIYpTnumGqHWDW3w2LS1DMUpM15tyk8Itilx5kq0vfO+fNczVr1O0+aqZGnbV7TEar6xEWrTu/jQrswfyhldtdLd1VdTzg2T02Mimy3jbxEUbIXC1znQhPNm2Lw53lUQq2EM5diGtX2aba5BiMc6/x8c02R0h+YYrf7X4DpYtoSui49lE84/zkxt/GxyXdnib18rp5yWUrJtLuE4wYjdazs4vBybcwzBiW1lg5pFvrst559reiArL3LuRRLCL5yMynnZd5pooUt2bL29tjuzMxrC5MgOF4KpRrNXOgcp+g3dXXpGndxu9gNMxkS9MCG6uzSandSl73IabC65O0EKKHGV3DXDtPo9lajOebZdl05s3eaN4Y0em9MtiSstVczAiUTvXqScCS1aTsHVsPSXu80zlxtHrvjQusvSTM1G4s6g7Szmu1t7JZxbiN46vZdr0jgo1fNCOC8zptqF5W2U+Wli+rSNxVE6RfYZvPlOGhXubsBt1HpC3eHbQXasrOvuObx6becyisTmrrDmq1Hm7lYkNmVerczUEHNpk5mZLvIJAksuxlLgdgN5EZNQzujV8VU65oWu2YttEpOE6GjTfS6NyYXEvKrHSPZULimuZgDGrBs4hCvsdc+ZIqaQSbRPYsQRhEr19oyCQupr/r8vfs2u0U/E9GDmP4pqxjF9zKmt9TKzOjm60Rtbj63Y8PC3dk0DFdbmZ0JRi7MMOkVNZrBbF3xzXZi7kd2cC+Kd80s61prWr2nzZrcrtW7BIw8WYMIrK9trcqG6QgUyDFzHh4TIZbv2Qjw8OnZ/PTAL02Pjpiv9MUGfsLdCh+dT6wTarGetxD4YxKbQymH3ZUug+WKteKdevzTPtdI2r2e8PCHEajNsHtHc+eN9LDvVd1ZTJNWTW82KOat28WK3nbky2jmynBz2cOzrDWkPku1CF4X2V16OStHehubZdCWipHK5XM5l7Sc4VTYrupjcOaDl7dWgnzZdQXV5NYg6b13Ku3WKNNdgg8PCtZBtm0Vs2S4zoKIToybmut3SMEuWGaI3kITznM7i8PeB3mbyVUOHzm4aOXlMGms2c+0Mi6bBTWLab7nPpBX270b7Oa+dblwBVM1WDJS18b3avCc043hHKhmF0H328euaS6pjGhQgjvsy+3N26s22/i7FnA7uXWJdfGGpXSYFujGkYo5luhWZV5uM0XtrJxG7C4N4ZHqVbk1Awi56+gredaNtsqlmaNkGC9NI7WSQ2nXEa9prqk1b2sfN577Qe+BfVLuKsIifCslRIWy/kDaeB7jyzuKy4gbTr2ceM6Ze5fe1K9a6CWuPsnWJYldjSzRtbM0+Z7CZZmZw4Sz2Xv02t4OBvn8KSnNEWtYwv4Yqcm0Ntocx1zKupo5NzOYhLkOERWkgzbM1Z1M9xysyNBAtoJHC4sXvDwuiGsGeyhlzha10OFurTkEDTSM5y90Ww4eMpGj7M52RcFw3DmDssZJ2XjNvqzbFAy7E3lBIoz/n+f5+cEhL+/X5YKofkkCsV+fqsY34qcJuQzpVmh2WFky9OIjul2y3A9GBGDcN52FXY3fUWtVbbRtTdQST3t6U8clURNuS9NaCx4eDDD1BuZVeQLWYJUMS5rkelVnSlsvma0vUNzSFJMTwLe4J0N3lRo3eVGakyWrrrZguDewZtnBoLsmlkHc0ocHIKSNurl2KwvbpGnjxjnuDuXWcQ5TO7RxlDXmWdZbCdUuIWtDULyBfuVEn9bJi94eGxfGqjoTPoxhRju70zfoxYyX2KxtcI8lM0tT2dhrLi7D2hZtqHFzePME7Lcds93ZOWnqEsR3KQZi2vavq7FN+2jTNTfjXXg+msWvtb41Uez6nKyXs7otvr6trrZL3tEUerePZQjMx1pyOumXKq0m9YyVAdTYjMUzL8pOqnu860gqvdofu3BHQ2xb2IEuIm+MF31COQ244MzB915lrQ6b4X1n6kjNirdYZ2rW+z6FqHoFnKKu1nQSzMuoxPZHLupnEZclxhDIw0natwV1TMBJmXvaDeot90yqW2tp468zAszdrS7qt2XZbO3hscDk6QxXruS7BgzXbdujU04wq4UumDcuS3vHdvZc1bZkNUsBb2bUGdbnVuC7bLzj2WHN61lQuuQrSoZtnx7nOut0KTMDvHe6wrfdzIvUTp3p1MOpgq3uJcUi+dY64qm7FZL4afZblXGDrTN8uvsu66RiLX3TBQ4qydt9UYT2Crw5mY5WJuhVzo8dIsZw107dXt1xkh3phZzo6tLLWFZh9XOAg4SDB9OYkq9hD7PZYgcYjdiFZ99l9h7HKQvEsF4KyLZyvMESVXRHXHJH6jyyhuypjt1KarDL3MEyhV2k3jVMxNzFIu+InxuyK7tgk7TPrz1XmHdRC9OorVcicVRm/zQBYAB/oD+kSRsefrF1T9ZCTdaw80q172CcUx30RTPPmhBSove35x7aeLFT9+LRT/kI/kt/zeM+KKwqRMzpWXhLVWq/WOaM8PeB0+8PCVzoXLzlzx5u7Yo3jhNO+vd4ybNWdBdanmaqPGKW6Ya457bd9t0yV2hwHczJRGbYe6u0PIs3SZ2uA9wV6zabJxO3nDht+rrthibx3MxdG7yQvJ0NDLmX15nb4rkdWss3WMeHhS3uK6Xi6lQuTI9R6xei6IkiDwhE0daCezGuN9Euyueve3tXSs6hjDBQu/S+O7v8/z9VnIPzqRqz8/yZn4pmUkMKd6V0iTDJ5S+N6egRGYXwYN47eKzr6k67jL0LOi3e28V6zSMy6J2M9Zge6oSQaNGpImY9qNN9Yp9nKsJ0nlpo1j1VHz7KermqyKlxq6ijpylla4hwiYSFPL7tZ3pwxSyqxRVnRTs3Bzdas2nTOaF1tITixzXMvMVZXKme2mSNtx2bEssEyq6oe5uoyu15rKHX3qJAPdgSx1WURl68vSr7lI6Te4wXpeLtF3i4eHgrE7MT0o7tC3onE4tM6+VEs70eslEt821UrcnIWUEopebe5TDaGMbuZOlM9NpTE5E8ea4wlj5CWFGLHWdiXzv5Qj7rdX81f0qO6fFxQyaXW1jwipdNy6NZUNU9GGlqSXx2IjqoumGuF9znuWLsL16H0SFwbJCD9K3ur3NSfDfhuLBtA1oq9ZyfXx3tM2tVurNozJGcnF7fRVNuwwi6Zxo2eLuujNjRodKbiLgo5u51ysdndF4RHqy5vGYTFLzr6qGuHkeoVSdp9tMibdU5RvHK471mp9dW8+OGNFfcvmMkyy+0beStU3Ow7zK2hFtnkINuM1oy2zjKdDNqhhA5Sxm3BQGGbjPZhYmF7BpmKvKk8muuIOEPHmrLDNhJ32wH2i7oJRQmx4eFy8CCOvbCWqkeoKK3k7ZM2fdPT6ZvCCYKmOfFpi9SR1jMcO19uRHRpVylW6LOau4kPFAW0JndD244KnQalsPNzbA3CSabts3NRi3cZReb1VCqIod6ltYcK7oSr3FNyy6fs0WYq+6TbE++jtA2/ksI+soSJ3LZ4rn2YOOW63LCePL7azt2pTeVnnnbPOce1guceRjWHW2xZSpXG7BSc08ehVKGsd4DMwEu3rl2wW53LLYRNUEaG5gqTdnOJZvM1uUK3Figo5asZmOKHBixF1XOm93RZPoRypN9BxVac3luFQROmQtTnTKllQPLaD34/DTUFKvN7fqr499p+CaMe2+xm+j1YOBGBuDFe5VK77E+02L4NuiM7dFx7pGYNmcUZWGmxRhuzG7xPMqVw05BOGjr543RFZadM6TL64LvsTfXJ0usbzacxZeVQL3JZEkyU35OPZu5RO5lf5lyVh2bJr+eCjmV9ahyiTkuShU5W8dsLCRl7unS8B3OBkWm/S65um1lq5ppeJ9/SXq3RoZMRiCIZECkD6oX9mn4Ohghcv9MOrYsdXOeKsWaMEP4cjN0C/sc7aYvYeV184E6GfGfQY5267llRCnGpnKYY+emyTVblVWFHSJljphLy2eu5qDtzUWlInSx104y95K+6h6o+5a4TgqlNVOZs7MdmwyZd5aT00mLr7dhedQfznBQZLbu8+sXftpncCJ+yZduDpmmbfJA1t6HsPKDL2X5LmcKQ7WSgcuBo4eaQkR51eqN13ZBnaybdQTCELptOzVWjyoB98/s6uvjU0Zj+99JJzXLzlKQ91Bjw8OJz3L1bC5HKlG7dHR2Tp3WoqyVt3TKdZHXIdnS26XpMe1RrNmFNbhGM485vLxKpeWGSF2cO2acnXb25MrncjUvu11fuzjcJIJCM6sqiNphVQqttnMOQIu6bzcrmnSDjyDrBrQxFafEoR6nSUb3unezr6gltxWQKGR+0uEoBxDH1DQ4+b4WZks3WHSPDw3j1FVzBL1c6ukjd5m2J4jcgstDN7Zi+36DtXbjxnL37j9X29NwLVfSt0V3TZnrdTZT3Tzt5lvr1xM24KNRyPD5c8usVREUJjdt5vcsr3KHZsEBlnXLIqPMqOqku61LYI3V1rJMraMw1jmFzZ1rBMY7jBBDNnDCKptHO6THdRgY5bfNzJlrKQldbN7vbmnMIXDFWH7cmr4EjeTtCCUd+oO9qKxLD+24hlyF5VlKKRGHFlQUGaKiaB3XmWKpHqzScuOrnLIiCzTGV7WDyjePCHuk7vZnBkIaTiMPFjq68OcGdOUcdazvtixVRyDszrnFQTZ3mQeSsSY7sqIm9qPTnNdLru3pBywlHLmzRjhmjOierO4jMlkwUoNmQkvaOu4RerxS0Yil3tS+XwWoU1938aGiPtK7zvT4lubmIeJYtNHqztZrtSb2tsa1tPf77lwMc516thrjVBMkLQKghHb5zyaRb9AjSu+8uXkahzbw7M7Pp38xvnDzVZwl1bo7MSbz2Ii1MCnUR207QbZmTMYfd+NE7Y22NRwJtVrdvTjvPa+26oywwgRWmBbBMid5Fp91TCrV1S7by7lK4tPeVijlvc40lPPEuGuZe2fdiLcay2zLUQVYSltdW3pmaHSKVvfV5xh8Rt3p3CsM3GeBJGEkQ9zi84YJX3a4S3GLjFZvtJJAeriSA2wCEXebkzMxxsXiwszWmgdgpezurioqsFJ30xJMyNyLUseblGGBBR6d0CCpRuGOPCB7uN18Z3zsyTN+ScElUEKoFSoVCoUlSTRhDCKBUSUnMnP1el4rF4IZ+/XrYu9zfv2PcATzTIQ8eFc1XFvVVPdx62y/atNNHPXzsJzUp04tkpbatlKRmINL++uSRSCopGJDM0wf6cU1JQMDJJgiIihkzKMY0iKZikpMEed1LCTRMzSM0SUSRmIpMRkmJIQCJJKMkyUGhDNrRpCyYGRlNCYkHVtanPr4qvLyCKYQWFSlMaFIsQYGIeuukIiBMxExEpJBEkKRLIkUwbCSRiMmGgWcuwkkkMNNIpEKYDIbbRDIqRRkike3IZrxXEKBmTKISTFEaQSDEZAkYzQyKQhGgL1WbWuDM0jDGkZIohBMZSHrtcCM0TFME2LAiRAUxU0taDCCIYxGyG1pKaAiIkogaDCEoRQESYokpZM0ZMUiJQnLpAIZpEkiSkhoCRoYERZNJIxEUokopRYRKREESJCxIyGMJZyuqEBMskiQSBIRo2MGkoIQAkhigkCZAAmhDTJ+O7Gle3XOEikJAgYhRmBiQSZBIFJEogmEoYYYICRKCmRSNCBEsgxUUBhNHOyQz123SA0kiJQSMgZYzNrSDBGDRhJZpRhMspKawEKGPO5REzKbWipkRtaMNgREBRGGSlJmJAgiMpIyzZDAiQxAgYxgEgZSRLKffXX5vv+vPWDJfDtMYN3dCTMQmTJQiYzTFkRpIMgQki+euiQYMhvO4SGRDNCkS20xEJGgjEzzuxSFKaIZpCZQkTITMUBmEimYyFpZRjMW53jsUiMlCiSUTCaQVIgMpNIsIgUjMZSI2IRCRQTUQEQIwozRkk0oEaZSkRDDEUpkmIFGUhSgyUwjE0YIKYjSkCVBLGgGRohZkTKICJZBiREgittlqlKt1J/Of3Fn9bTVh3nhZXskk13QJphi2IZpEUwwDEMMYX5dMxiaJIwiRRLbTNKRRp/rt2yIxQlrQNKMkIQiYJSREQRkWUmRJDMjTQikmMAEpobMYTCbCCkMspslExEMxhkiyCDzt2JhZfztd77mJppGUpCCEMRBMyypTElEAQyTF666JgzRkifu1puJgSi3/HtyZExkxiGiM8c0skohLKIp3dBLeu4xUmZJIikZmUiNrSWM1AYJMICYoBJCMkACiZjJhGZmYBiRNBYpCmWUBZIzIlgaBtaMmGPF1GSGBopEhQaSkCMYpsbu5JBDUwmTMQYTNDMlL39d4p79+bwJJ53MaMQGllI0jUYyIkSGIyaECIURpFIjMRhN/n8+/PTP63n+V16vdfx+kmfzs1f53VKCP5X38oQisurZ0hB/OvjnddtnXuc572m6kjHGQh3dIewMyAAUQRAYV7wu+IvjA7YVmIu+Kmsqr6w6tqdWGKkb11UTfdt3ppo0zEd1ArkWsAKFIJCBuNcT5Ng+CLAtF/PfbbVppp31r3bOYht8cFq2v7cWqWrLJshGiLAEmQIolJimhKKUgjQhFAEyGZMk0M/zuEmRBoS0ZMmlJEjzromIEmJAyQEAQaRghNLNNIhQBBAjGhGST3W64pIQlEsAGBpkh87y1XVltavNdopJEIUSI990mg1MUESZHvuQxUUZjRiTm3SmAxDSZLRolhpA0sRMxCZSpDRmNBzVbsKUc5oU+lyiSAhhoTAppBITSGMhAvXcaA0pmKUweuulFSbFAv23RgaBsZIJHruRXruil553ndyUoRGCwmREYyXi5KiUSEkRTEpNLSRBSly6JgyEUNMuckwGEaRHjllGgYslDNEaJIGRIIMNMiDMYZJCNIySRCgEz4rdXx+vXiZg2gkMZjMxGKRBBsEhkJSSJZKKZkgTSWYKSYbEpGIkpZjMIaGQR7ckwiwSYohExAjTYyibnMpowYTREkhSpZEiZEkkgMIzZEke63888k8reK8rLWpWVqeAjLGJQUURmSMGPToIkgkZgaTGjGFIknz10kRCkoyMwI0iJmGTTESkyiSCRoZCJmQ9Zcmos0VtKmEghtkwYoDJM3VXckiMSIhGTCkyAlrZGZjKyCSJW+95q2/lmtLd39fVvdyviNMzRERNKYZQkyQBGEiNKGQimYNBEJliSIgUiApolNEDIyU2b57olEZaTNCCRIiEREECJpkoRDKjKApqMANJgXNcJGpMJZpQCoSjNDEpESnrt+b/LXpV9aqmKHEA5SpJUskQolSpSBgTCSYEmBMCZCxX7i27W58z958797hOAE3JCIWjCNiFJSVESUlKTnV0kkkam1l5wNyRDOImjG7nbtto09/ch+48faa2EOCUKJTO3jeVx441rmk4pvoqeEcI7qK/jX/jlsVJYAoRJGwZP/HksTIyIkMso0ZhE0N9YBpIaVakWzWGKsiy1VFlMzSmgoGZLKiW2hLBa25ZSSEQEZGBhpQQSlGgwJ3Xc6CjB3cmutUy13ltqtakIp/CHhDvsmvP72v8fsk2/CfgDiBUu4KusSb+zgAWHQucs7tS78z+gQbpzX61rd49rW72he73170gs5zmee3ve9797hzlud5305fuOW3ju221e6t7V9Nzfe9r7fjto73va+vOH7012O7i2um/X7duW7Pb+5yr05POc76M8NZrunO+btt8NvzVe+4Y5y/Oe9HM15uO99iOc1yOV77c75znO+jmd7fvfY1rm557W6Zz7fY3n2udz7Gu9z6+vc97G+etrXb4r72aHdUmSkX84kCEC4c9mnLci1u9v2x+43M6dyEkZJ0mGiz6ASlEkkjVNyg125aSWASSDaMdq/uMhmZgtlpz+/Y9ic0nWvOgl3Y/Zy6XyKpZcpRGSMoAYvPz9/eWr5+eheNTa7piI6Xm+9oxMwIS0gQmSBMs6BS8wI4JgtLDRaG2b3vE1cSALt00wMrLzUsT2gVeECrhAq9OlIb/PYiDSIgsI5ctiTVYhtPPvv9/VVfOqva/X9+XYcHSdMcIHD7peeLudbvLnSvLzx3nbl111w8u7zuolIby7+viq30ttL3e/NrbV8625vja75N3diu3Ya4onZdoLL3sEGnE67juuuRGMYYxhcMSrMN9pAw1jPWkhwsjdRIAE8ulaMkUQACm3CZDgABjDkyDgqUJgtfxLeHroe2h2+D4+O1z0dTJRR839c39vjfzFttvH8zm3u63lIEr2+X6632+37f09vp9K/j9vw5w5rePTx/T+n7e3y7z9vz+v7+Oftv458D7H05zbx8T5eL+nw/bb+m/T01+X6fDft+x+32Ob7fb5fx/Hj199+308flDn7X8etE/h8PfJcaH6WpMo4eOF/wvw8Wf2Y9t+3yD9ufI/j7f0/T+Nb5fx8v3537e38fl9ZPbbres4tze7rfYVZPca2Tu93a93q8XN8e35fh+ge3Ln8f08fb8t+/134HPwPw9sML++FyEkk6QysSMP4cOnT5n9mz+jhh/l/B9P4/p8v24H5enw/bfhufz3313fx7fp6fT+n2/p9vb0+n5fL6fh+Qfl+36bfoYft81qPPxBSHR1gajcWLk7fV6sGou92ZutxttRvy4AAH/l/n58N+X5+e+X93cAAzMzMzMzM1sgIvdKzZKrTfNLXvYX1mzsxUem1c7OF9cU47ZlAt1VToAWdhOemphg+9i+rNE6fvDmsvte5erZe7dFpHhel0mdnBndXZjNLRcaIdC4hBdkJwFznmnr9vi4xTB5t5yKfXtOiUkknEJxuHLgni+oReaRhX3m7Zuh0kIwCGBMArEpMUkh2SoFSpEpLJBUyFW9kp0ySb+aVz3e99qh7wgXTDzTxi6Yu3jjztOlEr131454o6SUTnXOkTRD1iQmn2iaS1VttlFMkUiiRowy0EYjMTIxhIS/vuxiUppIigyIEGAkmJIIERSKRSkgOcmJMIRGjCUMaQTSYRTDMslzkDINhJQmkDMAkTGZMhiTDBsEMjQIiECFDIzItaFEaRIaaBIGBMk0w7uRkmGLnEQkTIhIndXJkhFkptaN/wcDSFMjMzEYKUwkpJEhiEwspMTEzAYYZZjNCwFCyjCbJSSSTRmQoyTJmhpjGBTJhZmnp1JmYoJBRoja00gIFMhRFESSCjEL125MkhIIkCEgttGFIlBIAoRjGgyiU0zQwAMZlIkpM0jLElEmLAjTFkyJC1pkNrQAqKRY0aSUDJJLGQIk01qGISIFBETKS1pCRJGEBlE0kTKQiCUg7uIMhkEjJBlIRoUKTQYGEJIiiKaBUJBsASgn07E0zZiSbFmyMikEBAGGb32uGSRbBSLTSRk2JIl8dbkf8erqCkUlMfHc9t1rQBkZkhiJgSTBkMwaUzJLMAUyMJsKCwj07DFJYyYsZRIwbJKETGECIZEZpKTCMxMU0QSQwwYxIUND05QppkpJIiZGTGIhYvh8+V2ULBmaFhmRSCkjCGTRYmto/dauumaBMpqTSRntcYSkIxkJJKQSkgwZMEzMBiaIpa00AiSQSBiMZkssi7uzTShJb5a883SSZgQSZSkza0kDUZIMbu5jCUlO7g0osz+u3CDJESSgTJhgmUhIgASKISSJEokklmJiSmYpCmRMjAxEgpMUQUZjRJNmI0FCUEpNkB+O6FJEgoICNRS9r28yOdEiSESLuuzSSAUiJlCQMVtBn0/DzEGTISUz9yuZPbpGhXdwZFBjEREmFJgGkhmESRJqZKLEUUgmwCUUYaSUTJpQgxQAIPhcqQmMIf1W/1VbrxiESCUJhJIhGZIk0yNikljAglmIQxm99yJgkUjDJiIEufn9Pw894E01CBSgYgJQCAMY0fXdlJSKIpmRETW1y4pgpNrTDDuuCREQLEsxENMhEkZQFDBTDSkKFREmQSyMYQUyllmBjSCGiCkiShEkkIoICEoCTHx3QPrtyiWtIbCZEeldBiSJDISMKMkGmlEI0hDFL03TztxoQjZEILIkYQkRmTWKQyTEmEqUKaUwYzeOSMzBkNtoA1NJELApNJDnNGxMomYYXnc0iEmTDZ6CxyM4lVJK/o6W7xYTCYSSIcSSSGH+/vn7PO57nPe894fKEkkBVVevTVqmba5z5MZa76nYTze7y99d+puBYSSWQ2b8/z59ajbefNyFKRMGMRSCMiBo0iJMhIMpDJQQH9uoQ0KSTRKQgATJX+dXTGwiaWRkyc4kgwURkjMSSEiIRBGMTMTASKMIEDGhS2hSYyBQyjNMYyIISJMabNL+qrqt2kYtaQNmCSK2lAEUFDEhC87djCj/lcjMBYSIiQBQAAYNgaBsyTMTE2EhLMoJZEwTKYRjAle12AE95rpFKGGMUhUNCMhRDMosIG87sKeW2206kxALJl1q1ta6svOvCEUSJGkWITMZBEZDSpQSYxohRmDKO7cxDA2IEwGkEkzUMyMoCARCKRGYCJpok0GNCEgxiUyDZSEJQiYAWNFIkZQigJkBS8rdVq5VX+Xcg2SZlmExjYYmUmGZhkMaKCYjTJYgTLKMmTK2kISEDEmZGVMJ8dckyIpFBEpEvfbkvHRYkpEoy0Zert0kyYpiEoqEj04SFjJDRFkSNKECbu5EzBp/K67RpCElHxW+/h6vS+e9XnRjBICECQLWlKYEJIMl892UmJL/l6vPOwCRkiIUGmCZDKQTa0y0oxQSUEaDJNDEmYzAKYmYJiYxMSUZTCjIZJMliZgowwkymYmViZlEUhTRJlIGFISSF86y1X48v88rW7MEvbdk0QIpTCClCJmogsm5cQZRkIlGLKTOXRhpmCCMJRJSSgxCU0ZpQJEMxKL464MTJEZpGmgJjKzDIQIs0EJMwKiCbGLJMoyiRiSTGYJQkyt9p6rvwwCbf1nr85i3zudfz9/PMsCSSQzAIA6nvsI8VZUi2SLUqpViUpGhMBKikPYpMEKnWJIMESkRi/vPcmqLJvzkmSkoFJ/SWmqnXBqmKq9ZTVHGNQsy85AcFDAnBR2hTnOWEkXS6yVWIBYqZgQfBhCMPWrp9Ex3VnpSs/FkFHzFxdRYTcLWckGLVeN/MM/kII4cTtUMobeK6vUoxTWP23GvExOe6/E0hoZstklq0qrYW2KNJQYxgwvld7+vP1fHv35+/SwozEZmu+z6dVEkkhmw6OzmvcNyb0tnmbd11uIlmbx7GmSwwzDNx01K6ECjFWkx6mFpl9rz5HXPXWMU7vqDESSR1JP1ttiRVkApFklkpEWrU5nH4BNZZZUWq0hNb1Xz5849mthJJIp0svIKTecZ7mV1RoXqOUEx4lkzQ6NWVFReEbFpgUFaGO2xM6pUpvC5NSVMDQ0n5xPcbaOWZ9dQkj49Klls3xd9ftCRDGnL1Ee5E5fvNt1O2ZfcHHpigruLnBCRed2pWN2F0Qf4W/pYszVchsJppTQqlRUPDwPDEyY6znfbKpne2qbqVdjHOJR3NxrIghZez3Urct1mY72Huvgk8KUejHhTLSaQlPxFrW5HozdsK+67rno2hr4c47JVXNyqcYy8YO45o09miVWIGLHUN25kqhs0w8Zkx0KZRuurtw45a4vXV528128teZqWTqyxpEOrmKq+QLD27YeTkrmyk7CMzOJvCKzc1+vNxlGuIiQyZabWK6oVmcdjtqStW5QIvHrhYPKy4e4aTi3YsoYEdIIIupx5OZBp2CUtx6baV7MOdTvdSnXwi2X7qa2aToUuMPnXRY2lyYIayrFZn+bgxJaV1r7qYx/YqUyPL3XJQpOqJYy3v2ERl928LrZoldzvpOKJp10YQO204eypBdUMOU5gyy4LuVFVNRtJqaS5mKLkapTLnaq0U05pzt2amNENudVnlkZWDdzLh1rCS8t260PsFXeknairezb3c2+OzHpvHpMHZGzYuSVglMvtqyDelZx4E5yOiQFk9sjGugcblR9WrcxmKmEKdZL56Wt3FbVh3NyXS14r3uP1p/Y+kzsXCg3dz7cp1bmVI7BZNTATT5SuWBjMzLdwV3tth9t4tDyMNWbpBbILjvW5joW9Jd73WGesbVmuJrFVEW9cotq20xZLsYxpIqSgd+waHkRLxnvpnwVn4Z25go3eLs4bNdTpm5yEbOYwuuW+wtXNmW9ddoOSw8pJHblRMBI6sqYa2U465V9XD7plH29x0G01chr5x+wS5KahiqOy7JorM2UHtXQrbDRbSrVxnMLKGEnkGaMxZKcWPdG3r0ZkaYd20aQjkq9CTvsHGjL1XSnaL2cTUeMYwuQpNiFp3czgdWJZlMO1cWHcDT0aJnHTsz7u3ELEYz4cE8DG7pyBhfRsgy5t5TFb9dXNWvdQa75YqHh4aDtA2goopcSs7VFhTcWQa07WZ0e2/SGXWs66lRmbHlS9CYNFZjl6y9xTBWdxbMOLsvIwaD5k3261Ak7e5d9VW3YnWju112asiB7TwuO2LQUY1BTt3LjXE9esK+65DrOLcuI+7BuHdd0+WsxyiuTm1mdkmXt1se9GsUryBtCk8SYOvG5zybU45MdHdK0rSehnWGVUFhVZF71zpQOKRWkcki72qjXZl4zm5R0Y6t4yF2SXoKBHUuDI+qX1xi6dgZK+0yAfZk9cG5rEEiuMZfzHbt1Sm37w8HkvUynWLXLynWd0L2JsrQhUKlLsjO3nmS2D2VK2xk5U3Jo085u6eboUyygucgL4uTcto415sak85ALurOF853UuxuLIiVGHU3d5Cr7DyKbrKFU0RUNPJUZOVoJNgjIIzWESgV07Rcvk5bXdsewHNFVN7b2NqhYesXQnc6nR5t4m891Mqq9jQg7QtIRm5YqzMoHPESDXWkzIMWK18f6/mfn0/Nj++/Dl1Hn6yshs5ljXkmSlM6uG3VmXYNY+4c1iyaYK8pkIrRtUlcQaclUnpzJ27V42e6kTtrnxQ5NbjrThi3FOZJZoXohvRtxXsHUq850ySgVQvL5sLqeutk4i7znc27l5LMeaQ4+Oc13R5uiLc1irDaa2lu5e6OFDrG7okjyqdzic50Wbmp6mtWeJy2jXbMRmmlQnQaOF1ajuWq5LL55Ocb59MvrGXVwnspZhHOZk7bdQUwW9NbsTXbsS7O26W847d4uOuZz53qC4J1Ws1mWt0SsgCdED3vfgHsA9Uz6trY/vsxqvjkg/SRP3C/b8P9X5+an2habuS1sdWfz50849fVIlei60nTASQnjSrJjOa2+21NvhBSvMy+zNSQ1ZsW/h2pjxkYzcsONowHstfZSy8F7VPbr7UO1x3zRamHKuChXQcy0aI4aZj7E9o7fSQvV6wnaB4GtOL2mtrCsgPJhkwCGGDxCHvUcERP28VUvaBeUpOmfbdh83+Z99z4SsoUe+n17Vu8cYmU1IsHdgMarbgrZNWyupVQ5bJ3YGn5tDKoztFPMl8IKCCcGa7el71bfaqSdU1l05CbqahHasHUSc6xsfHRCB6/4z5j5XWP72nsXzdGhvUlpzGh3RprN17qV9BLd3r0HHWMQdtaqKBV4aaadVIgmE6Wp3sfLZY6t5OrnDWqpcjh2NEYxVJarT9xu8KbbIsbjKvhf+L226sojPowujnV9BMZ7dr9ZkzV3RgkyusZMd0e2TXmP52S4gd1lUDt4s+7r+7oOUo1fzS5EJmzaEoy24TRRQWKXu5Tp6MRx3rH26n2tnLV7oePBuUurcUyC5FHtOXN1s3cW1hDXS3CIKh1wXl6aOzMa3MRwrJU65FQ3PVB4eEt0PCMMNi8XrWDGBp2iwg4L5dVW1kF+BSDE/uqR2Tom6dEyVJknZPSSU4g0J8lSJ0QpP80JsmRFRSdE4E3QyHCY47ToTJqSkp8ThOlMi6TIlhWSW8rpTmnmmwLQI0Ctul+TfxqHOO5qKxeK1Ys0We416uWLsS2ZpQiQElIySCnaS05y2IhpnNM2phrLjQKmbZerF558qfdcbnTxUL4xg9VJdMO4iv46bxHh4GUp8/P625hVJdcXwM9MV7m93OSiH1ZNfOrI4js2ZqHQ2Txw4poo3t10BaLvMyVac6y6McQI7A32ikyQNyrZWiyps4S+ErNmXdcmXFxVQjDA6GF7pVi+eOKxtiQ5oUCje6NtLDu8sdLJWDw8NWGllR2aZR65KCiWmaYl2DrTjua+CPPBSrnix5AdFWOyXgmVt5m3edsfDsDoakslTl2bxfTSRt6jbd7e4/Q2Mbus5OXxha67cViPXaT3OPN5noEmsndS6d1RiqJ7JiUJHCULyjJxUycbwyVtYgJWXgd4qLPteEmZmHac3Hoi2IwHNHJDJYpU+QrUtuwoDMXCDdBByb0Zl9U6Zu48N+sXev/N0Ue2uzXXub99BSVQK9p52pa9sSmnT5XjgvXSKO68UFwvtD13ul7Op3g1zZgGvYqHGG951gvsynBkt0T6Tr6o1NyrGKmwU70RzTL24lkwaeP47+0/bkfwQ6kNTx5dX9NOu3kNQbUsbrd7Uy5VRoQNirVdjJNhrN3RQzq0QGsY4YDtth6CN3gXKwURrVXnrycmFeTGpMl7Bat3sp1ph9su7LDBW9XbYSLV8Rw3O81M0SskqQphZ2N8L4a83pk2y7DBBbvQmNOCsO9AT1zJKDSHPNHQXeUcO1AeVOJZJL3FUeuMYc15IZaxp9dMY2ik4tIdNLKMZ4uroSZjEtomPGayzIDBeYs3sWeNy1h0ZutDHdRJ3w3dYsVPPQY6ImFa6t9bO5nDM2U1La5HkdxngVe1rbGaKok9BdzXY3AqbqMbTss2XEbOy5eMZgKI4zu79fcNmtivl8pPq/DeYvRH80TiLlte/C7+qBG4e2aO7cVT2ycRhUru2O08oWcQuRU3LXbmxUauabna9gF5Me6cfZenBGW96SZNu1NiLmVkl1x9drbq0cmWjxx5AzDOK7eAfW2WrK3KeUm0TVwlcOOZOzK3MCwSHDV0jdEYVN3Ie09JxmW3MmA0sy+SeTdWus7bdS6v1xjw8MKxYt0O4mjqKOawxihqmzjV6s02G1Qbg6bKE53iyffT5y328qk+iEbFdGduIFyQCiqN8p0PFnuBlGZgx81fXdu3khDzdXLhzaW6hfXZR9hqsWvuNzX6MWMiQMpFx2K6ZQp/bmE9lWxv2Bznvx3TuXz100dTdnMz5r6aHM+VFi74izkm/EVmjit2DN6KKN8uLq+oM7SOU6huEOuKYzFtztgRXHEehW0O0biwHoaY1tx1NFDPYXOmqqWNqDw8DlShHYJD9oW3OPxHxgJRDb4HUeG5we/XYsHaBvG6SfzthXhG5EJt+z5Z8b92A4OzGqnwaRqOnhzNJ5fZWJBfaQ6EZXLocoH7fLoyL1Xm7KzDFlXlI9kyTMdN9vbuJzpJzUzOG3li5D2khirfPrnKBN8ZziHjbSO1pGeNONu5NjrNZcaV3VKszcdHPXr3ZmBqq0MTCo5Czl44zD+7EpHv0vl93bdhzD9fZNzWbFjg1NFbzGvX0cOkWNk0T8A972YQThYzHX1Qtt29cHVlR5m03j3BBS9m6LN1mQH4u9T9cQceCd0w6u7bLFR7tyY3OY2F1xKn4vt3n99uQZL9nyy6NZPfN9xcwTZY0qcpJ8q26XH77RZzvaVmSQkdauzHixNv7PrupW/XmnSjH6EHIQV0yNGqSubj6aUw8Lpytljtd9Eq5QsTpci++ErsOCqfzbVfDt+zdpBu3pNSVznTTnMx6nkidXz7bs30xqQzd9u72ZpGmuLS2ZauyKl9OUYRhhsUW+lFqxrjkEu2uO5c55VQNlp0FnPbcmTd0vE1m1O1UDm5hnJyDCOXOoLYs5dGaMw90u+mGrby6a9bge7kulc6aJukapmvK1WdZ7M3tUvRzV0jqoMxOjQdJ485zcokYw82oOw6ev7O4yhtD6auQgt80MI+hGLcTVjBNM5rOo4kCKJ7dDLK6Kq3HM04xeoatrLGVyQeCZAPeAfOq7a3mVRUtyDZU3rEEOFYhgoweHhWnds4NtNcXQS92yC43VKIHrukrUkcJi5N3Z2V228QdJiO1Nxi+Iy9s1jyOGQJ1wLk1+wZ3WpUa3KFNqPCJks7JXLDql1Yl9vEbZRy02aFR3G9octQ267Hg973veH8AD0dzG4dfCvpB8DexB7dcVmmuMjUnXWchxZnBVK6Hdqs2oWF2nsx7Fzcy+LFbBwYtIVThcWMlE8MwtHzN5LVjVuz1Wllw7KWFLFp59hzu0s7hwjg6O8SOmw3TNXh58G+N4qFV24lkc3Y+jopv26p+VlnLgX9lFJLehiltqnNMpvuOKrjj53MIm94sSUyNME6nDEtatL7cgWrV1j6FLXvdmCYZRhKSEygkZTNMgSAzEwmTSGQGJCiJj/XdKhJUQkkjFDI/ndSMMRIJJNRQxMMMhhKQv9O0o0oCiKMSYJohKJmZZhQySaRiiTFGEpBB4ul8/r6+/7+N8QZmUmGgjMEShEyCGSMlIkyYkTSJNKZEyZmIUpiZFDMkZm+10+u3EJoJpJW0EoJKEEQzImMhYF47DJREwkyRDBEQJSzZM0NhMUzMJIJkjGSJiRZ7/1r9V6vQJMkoDEhTCiWLCRITJiIZFIEBGmANCYSJPfcyNNRBNikZRKQJmUMJk0MIZIaYSRkv13EU00mQmxFBgEzSMiEiiJpkxJEJijCELJkACCfEkEE2PADv6E/pXVXX9XIR0bn7EjP9nKXBdZyxi15JBm7HVHay5BRO0d896d/RU+qluaZ8gcsrorxfHSDKbrKZMT6uLdrKwK9wKI5m6t2XvXbjec+linlfxo4VXbeG53aZtyHN2UfI7kWgRXDrkxmDast0nQVHZJXpzqzvVvfc8+mYhuIGzig6R9boSxXTARK4apuYUxdpu12LF0nJ3Mu9e8bE+2cPsyRdZq9dND7t4/dufOA8mRUkzvt5zA1vdV0mmdWTtvdzaZ3eWKReRDOQaKT85IuOPZfVCNF7hp1SZJqrLE6wzyKs6UawNrKEqOFoZqxSQPorrBQYPi6aUcPXTxmn8YCQfvlrazPhQYx4Psop/dnSG3lZ2rBWbXSGXwu4DSONzHMtzVeHNByuRfW0bNYeeK/PBNGlWXLu59a+Sw3q3b+mHMm7Rxz7ITwJSSMIgEooSY2Fj4EkkgkEgEkjq7dB2a7bv3JaNPGVMpY24KVq+eVMKdVglJ5Fet4EUNq0KVY/O4w0pKZPg8DCUq5Nwwby3bQuh1Ruqpc9m5lZwdamGCATkoCj+0iNd8z+O+C9XfH118e+oGFIpFiEEEkkeIJBBIPiCPqQWtNzdDNVhdZadZrczCCD8kSCSb1tXtC6WkCC203TcZXZwhV+2EWUEUFtHpX5MDxr4bV4fQjKQRaUCGAwIHx8SQfEEkkJI+JJBPo6QohTuzcmt3QwfOMQ93msosTJeCFC923Ou1Kk1FCU0dzHMtWuzNbNanDUVO8fWWWSEbPTMl9Gs7slvbw6wSL2A2XVlBw0eSOHKgwUe7K3X2bREIzrvc1rEWFcbnqBqneF1Dar+Ip96DaR+A4rM5QOvvYq2C3tUHhixbQO1u44iGoHc5u+5zt1LFpOXxMzh3Q7KctC+6twPbsupkzMrGRUqhPwzdxU26KFzrszQctYXzUo/VYwZweb5dtCxmx3J1U6mW90MqJXDmK75jg3VYCS0y8xZHdOr7bnkSASSQSSCQCCDhEbDMmGnx7vXq9/N58rz6+e9enr0EMzaLd8525jIuHHua9Pj4gcVVZTqvdzuG0lGsi8RwS7Ko3Urtp2YjGk45VKLNvrzEggm0HeU69jVhh42uygRWnL+Pfr6+PPPn31567kKSFSgJhInwIJPgRx24rNsFw1MM49a8U8dHm2qZBJIa/wH4irtv3h4aSGEyCCD9v0oX4gm/ojWusR6d5ut9hVRCENW7MHh4KIs85NJ9YeAq9pXvYTJJiYtJu2w1xNylUy9IW7TG5HfCZmrFuIRFdgCmJ48qWeCxoLZIGbymd6sReVTUBXXdZ059+LM3Mrlc5wZX31VvPnlZK7cfOt487mmm2OMxdM3WMq5JpqjnQikcq+3YdRKF9yu5t6aektsUtuzxJsS7bnIrTj9DnYGKyn5xvCqJgmbuXUEvRc7lWHYKuwVHHgJ1bKFRXpKyiFd8xhqNjHqiY0wX1LdXJ5RSMldysHTzRqTRjePMza3Uyel7mlULs4LTTu8ZOKTC6NzbTeqkjkc6wyzvRquXFsasrO01cFObYvLvhIX6utvpzdYzghueXbu1OSFWbo6U8qJ7qq9woPCc7iS9uW1FGCwbGcaJzUaUQeytRl3WDZcaQ3TDTypvdzxEqaZdHXtB7BWVFcaDFWg0p7xIBBBJIPgJpJpEppkkSySIiATGUUMMRbaaZAZkBkamWATIn7XUZNbQGGKUjZsGJBmSjJFCwJTEpkfl2ARMpoZNDCYSlFMKFGUIxNiZCZfn/O8v9f7P369aSRJISzIhAhCJJkiYYmKEoSTBo02tPnukkUQzGERGpMyQyYlojEzEKMIUhZRmKDCZKkYCkChMMZTCAICoqYQwIQiSMykZRpSQzGEYwu7dEKQLM+b9/nv57/Xry+OvP2pFMZbn8Z9sLlQKud26SNhHkqcmWJ2mPJTXTDUfZ1tmXc3cdHgbFXN3KyeIJ46cOI3e7l1jtuLD2OaMs4Oa7lT4uULEzNVMo3sMqHlrZ6kQk9QZywvbHoedKMK3KS3IFnPZW5d92TspbaA/Q8fD1Hbv4E18xIhxqyFXeWCpfzblZz60NEmOrKZag3gJW8OxMILg+UXqpkIYJ1XM7ljEzOks9ldxVzMtdu7LvzugczIjbHDErekfknfL7mrGGhLcOtC/t+wYqOCWWK7JId+6y3Sebiz77deKfCKi6I2v5VTfs9fV79989fWaBDEJAEMiQwyZSGUSZ+Pfx8+r09/L69Y36Xyvu5qVjQ9hBKwqs1sNkdHIFASARgWk7n5QXdmcDLa3kKcxVoigN/cK20SRYI3F0CAQW7ryUsZgaWnaLq8SwO1mNHDVQ8d0vH72AgEE+8QCfEEgEkkkgnxJeFGlk66j2JRKBHDm0vNr2kQW2CfUiSRTCPRsnxHFZMDaddZGy5IMfa1E0rzG/I1RQWSrWxgjOL8xMSMAGgkJKMhBIIB9Nx+oHxBPRM52Php7Lp7N2ycm4mHsrCNKsGMGXo7d1zHruVWKPtVkFjUsBwXrGMxIh697MpmsYeGoSsqrgTNAgnd24ZiM4wnjbF73jPW+PteI5golnxNuHsvKElNhkMYOsIsh5aXY+DGovYos0bNtZtWGNZVmxz9zyU6r2Yryqx0LcMhNbNF3JYrsW4Y9vK7UYjuDGx+D6Vaudb2H7FG/ld5sdVGbKvJJvXe2KjYRjBO7JZdn+ckuGIL6qe2vZV3cx9m7po6bVyS5AdufNua3MWwxlzsqsl2O9+s+r7vfepLvFrXzeyYTJBB8SD4EkEAvFjmvCMZDEQ2PO3dEM61QpvLWDtpAvTQZJpXbMCDDOULzXBARtrX1CsRmerUUmhni12uFzOOOeJnWvcCrhXLov0Wc1cZihY+wqXYPXQQm/OfH19+vu+qUpmRJSMSQhde5rizqYWbCd5evm+u9fqB8SE3yXEWRYJlPmlJQ0JmmO3NiHbmggEbitgVdcYrjU1Sm5Kem5OCfuGxJsTeDUn+xKTQlJ1RNsEi1bKtllyoRmSUKTa0ZWSTDMhAhSYkwS1ojCKYjQkxFJRn7XEjMhIhpMpkppIRiTMxJppjESkT+73Vf1OJSVQskpyJgMkyGCTBlb38ZJkBDL+241JolGIVKSIJmMITQaRAQUKQSxlJJEGF5V3A+VuoiTSIyChYkmwjKBRShCYX13UhQxGYKMBExQza0STQxRgoaFIkzDE0QoTCyKgmSSYSFMIhFCZMhokwoTMIYYxihkiIRZmGSRmkwzPwvfvr0JGCiAwxJE0IIxJMRQNMyKDIaBCSMoyJkI0khSbRMUJJhhDGKEmAREZGQvH8Xn14vz5fsRqv4bsa6vLNP+HDoEIPj4nxBII/S4UgJTEJMBGIgmkQYCKGykTEQk50oNMQhmIR+p+PH+2/rPH838tuWxD43J2lQySojJNiUABAeIZHfr6q/I8/gu4pjN4p9tVTc/hejGMsaRSbw1OW6JUqqRnsGa949dzBR1OgVeXkm3Ls53q54+jVWOmno1xZd1HXUwne06O9MpDtDfZsNJ2pmclJpHXTlVmjM6qLZEPP1Q5ro3hyoEQekrxi1FHxSRu75Lcm7DILvckPTHcG8VtnLdN3Tfdm0N1LUHtTrdC5bpZjOpJ29MDisGgzWXP4MZSD2nak+PfMYTX1UMIYVpWVZRqqZrakkoiMhOpaTkpwOrG0YcoqVVyqKZwuKxouemkyqO70tcxO5iSx0eZyDrNhwvRpdyUssOzWIEApzaDveIjo44ypMrBmA4lj3dqysQ8PBLVZuGqVq8yrD0jGPDwm8XuTjldzeYUEnWYjSNRSLLFg9g6uua9jZrYKmoQdpI7AczZm6LHQunLtKumCCkyNEvcoE6FwyCsKS3J1RwM0zF999rNVPs08pn3XtpszaqhpLzrdrZzkmc45KC081VocKlG9RE2q8txQui44tHDnMuzHvajCXSqslh7nA8m6GrxtIFsEoWJx3tG4yTh6cs7cWawRSaxNnXrlFHqZJDOv+s4WInksWLPz5quDqicGUG7blZPr/N+tD7XtnPsuqrvufjj83mtt13dE+cqCqzsqmWTri4WxnaavjuxOpTuBaXjDXys4NwOBDkF0yvoNX3NRJiiXFK7t29Wq85KDl01pakKvsVbiRo7UsGPtwLI47Ku1GDJkPbZ3eramrFk4azdvHCilMw3R8bx3C1kvAopkKwXqoazZyalSW0Ns6PDwdzNbGXh9iV4ELvaak16O7KnJLhBN26QfsdThM7chmjBuXiz4/T5PN341IcBTsuBDLccqnZydS9qbqkdxbSGkNDa+M1o7Omc7CbOp+Kudqu67Bp43U+65D3wfwtmlXK/klE9SBQlRq2mIrSssNsbC3eVoNbWFwY3Fry2sdfPTIrIh7FqkJ3OXdqjymXH3rZt0W3ixiCpYlGqYV6bLkk1IvMco4Cmao6RjtOFbqcHIxz7dztbzPu3WGdPmcv5QXtWhJzY282ERB4zgPEUQQLjrux8yu++3U+GvHDR41f2O1ojpYKu4qCL3PjpsOZuSPaZ6Lnddze8bGXlIZjV13Nl7OMG1mCrGwbWcSs3VnooNidDMd1iVRTKSMcykT27Wm8V+PHZ5vZEtfYr6SsmrZZqc75vtnm8OfYoNc36+vkjibBp3KmX9bjeg3XKmNa6ixKLoSi7yZqMzQl2WKuXIKlaFEL15SwEai4clYUDueW4ll7eG3yWLRjFdfTb7EWwiOPG8hRFCnyUBJlVUEy0RUwWc42R2XmBCELejudac3tq6kZ4L6nB98XDp158Wcho5sW6fizDrCmN5zcXqwwZdDUZ17w0Ik50MuntLMEtPnQeTBo430GAuzuMWrZbh/sTv96/wIIW8z6zxv8NBXcQ78rmueTb/anhUlTkZHSqbJBAzjurs40ftp2vtvJr1qsTvmLwwlhrHRcwpJhKKftD53tuhTyiTe0LZscZoLSq6uUa2OleiZNwP7W+sVGKvtExVk1GCSX1mxltK9pQuWo0QiP6A89jP1zaZKXwz69OTGvpnCcds6K58mO7EZm2mJzd8KuF2dhi2Sd2ndx3gRaqy6TlPdStbpW1nG3VCn+T6pl60Y+LdgsX9r0nLmHRmmZoPTRRMC9mvXfzqyyKosxffAi0dnbyTVjcirt3aRsImzvHnobDWdmK1CWyq1KPqGnZDLy72tFddt4lHbbd0IhScqnCJ1jJWLGDBg71wHJ25uVGX225rCPqyGYrS7ZFmq1JKHctV9WOhm5jq/JVCOg7Ja43tbSDUbzqcEnufUqRbMXqw4iJ0XbNd3wpljj158awU2Yx9j2R/WbubMrAt+qo9jlvNoUDVNwZbq+j1iuZy62ipuZ07MB5Mdes9HQ4bydpStw1VHyElZ0Mb8QeddLHNZYnZyWrFmMXlkc+Lda8S2xo3WNX7/sAe38/Sn5+jt/X+nRJ82/xBZ+kdpiMDHm8os463k0ObryTMq5tbS12GMOdq2gW+ndmQ9lldmQX1M7SgYQyLZQ0blY3ofZYbJrJXKhzT3GirA9mruDcCSikb6qPbVDH1QmlgYtiXvd4iktbwKQWRHwVDIQKrNrVIkPT5psDx1AJUElmX9v5b2XUY22TwrOUGTK51RV3+j3U2I2sxBsRMoy0b2H4/O5Dfa6JF7WdN4bY7jmvDYrJJtBKj5sIKg7SrdusVnZRoHzwvBAkM2rcLUxvEdLuVbex3xdrp1H62fo7rnROtA0tMD0tgfMMWxOMTLtWb17ICyCYFYFsE3GbIKAF0QWwCo0Ur9tzOM9+d77pPCk1Hej2tVq1meND1ic1hzTj4wXPMZnqEkkBk9Mwop63n13ePBe+59fl6WToBwXgS0CoJJxDJKkmiHZNBPxOyaojJKTcm5Mk1TdNUN3pMRGpNiaEykjJKkmqicGTYlQ0JT00JqnJKmoTUm6fapoJyUnRKmSWTvf7E/Z76ir/l2ZrvA/TTl5mOKiKI/nXHSudpwYdOhVXX/B76L50XjLPFfH4sFY/qwIRMVcO+bEkpsO3vK5u2aJrAtNK9ygskJQ2pjhpVpyqxtOsrYXQxBrbxDEdd+OZnGxLmwR5cTlmP0jqoK4XQwVuDw8DLLvULezf7m99mGDTdamVTUz7DWKU1WX9TWqB4ulDaFR2h4eCammZafKieSNWT2MXLFuW3DLjhudBpyGVl0aRHSo2XdjO5XFl2UOb3NPFZh4B/RWNWbPrc5S06+vLlPSqvKqVgypfwBxlJ6Y5Itc5OjabmG+e2h4eDmPFe4Sg+oY8qpUSa7FJ3GDtzFseGBU1TPZZrperq7U9ojM2B9ekbUvnmnMw92Xq6HBXcHOi29E7tyWz/LrPufcXU+qZ3vDw16PsOZpD65KODem006Q5E4qNTc/Tn0UGtI8+roTmrHv3VSlS8URnO0XWBXEt47natLN8tdKZUvDnISbHJzui1xSRajG8sWihoYsZd6YIlkQvSpgNvHZsyom3HyWdu3EshnRde6VyGid0zZi9tLXe1I9N6sxp4zojMNZQ4Zo2LuiozHmVjRDztuyJqQtUe3E6unkPJhKVS8WURyEvNbUWUOZrTLGLDV/YFig6iRb+lKoxsdxUhKNK6OHt1/LKT3idW4OHEY3aFNhJ+2+Y7jI8OaM+2Z1e74N/M/Kr+8Jp3JNVhobmPbUWNH5aUhVLXr0zTDufUz9kq8OiZuZDCL3dpP64/iO5Njjpqi203rxoxiK3Y8PBt0WXLmEddOttjKVQrNIRhBGOgwyyWT3TWp2hw7ZIwl6zebkN7usr3YGuqlLvlS0UkcyxMYXtdjppeyDW1Tb283INlzOQW5lsQ7p7QTYWs28joWDLGSDcl6jcWPWqbgoptjGr17uC3U09A8KzxvBJWCg1KgnWaFrBdQb0jp+MfXmak9Q6jG8LPTQuoSnFL2xBDy6uPFcjlbMmWxZ3r5l5tAjU5uBZkcqnVZsUfy7NpKWQ8cLE4k1mXC4RVOKUKsku5KgbpOQxHn69+WillDZPSrhaK0FapmUFNxyc+y9krh2u7ZG7x6V5b1dUDfVNQN7BgI0dNqrwRVMCbiwdpG1eY2o4rBG5U5m3turR7b3uTeLfWrLOrBhEviMw5iGLMkbGvsHZHLKzeeHQe3ufXnd1STc7Rm6NRvGcg0dYjCRec+jrevVxeO8gaqC+r0rITmVjuYkiahrjOk6nmo9tYTe8cc2F6kKYt2a3VGjqW9rtX3DnzQ51yIwkbT3W0s3ZeUEjW3O8w1eyOqTyjmzGTdRjpHubxOjcydJiPhRoFKPspNO6OE4NXF5UlLdaxWnULzCy6qW2PDwqpbjxlm1kqVJRl31Rr2YgaRdg70V2NoF5skuX2ZWex5+fwAAv6bS/wYP+bB1ACMC6vz9QtvWPe7iIpak0igVKHWNv+WXAhOKaOWiMTTp2SkqVYGGXbvo81jlzqu92Yw5mCsvThkVa9I7JYrMxnjhgT2sqysjynKmxyObmbWCUjDstOMbq3ddV3D3wHvnOS11lMbxDUN1IRzdPGqrCl9Ih/QHv9QHvx8dQ8PDt/HU+WfM4sl1JauOvPQjj9k3dek4CawbRpaKrZgYza/Jmrj2p252ZhoK6R13mYtjs72LOuTPXQfRbjq7G0TOa4l9st4+3LKeOQNR1eUTtXKvaxOV+UE32vfnfqzFaDzn9kUPJ2+rcA8UqaF120uJJb9tu8zt3rxi/qdvzK4iCggTiollct0IOCyu1TGcpgm0/sDx3kJSMCVJiufY61bdGqPuCGtGdYfHRcIg128TKFDcpsQlA9My9lWWtG4qZFKtfWQrprZDvRLZRBvBJSdh4Sm5sTswTvrfq9+Z84xjW50wJfodmSj9Oyo8loGxw2bkcrMbqVYvck+zS+501Q+p6omX9eV0X3CwnVZ1ayLZ0bm5pWjjMSS7sHO2d3B1aRwoZ6dDdVn6d7Of1fIkdzxLX9KWEr3aXuqSTY+xEUeFYGV2TureXHak2bwiYs91ewS3oQsjIKmkPEEGzdiDYzNnPzI1cx1atlonOepJjsh3iHmDNSGB4PDwtQNS3IonCVdGSKDM5BEalLbWrDesLKM1N1HBIddaZS5LM4XHUh26EWXnGVjr/QCxE0xWIMXfO63i1q2rGoJzMUU5u/vfNpJWQh7gvjv1RUfz8n5JHajstunsMmsK3P1s9l8YOGA3D0x8q1TH53WQr9/ZUBubAYi7+pufGtOKHE8Zs34paCNWM3NnlQqUElGZJI1g+UA9p4jfXsyKtUxl1M6Aod2a51VMuqKJTosslFRbHbzjok002Jrb77QFu4UHSm1OZqvum1PqmSblJ0SiYTnYngbDcmCZJSf52jrw2E/wOxNkKlnZOycJ6SyDUnSYEYuEOCaoOiYJySp/pDAm2tTJmn7b9Y2ua3xgtnF1041lQzjIKMzEsVYo1G4yjT3pMM00dapfmct7P19+vzVbaLzVZhIYqCaVMsGWGkkhkomDIRkSgxKRmaJIChUf8O6GNmEhAWTMIlEMlCRkZEd3INKA0BSaFjEmkFAQyRIskkyQhExBgxg0RskhSPKyt2WW95p5pgZO1NUyjIspqnHBEYkjEgakJNGJJRClGYmMMYUCDNEwEgkZJRNiMjKRmZRjRQCWZ89dlIzGWgRJJKUKFEkwUyRGJCiJRAJLSESEBRREgQR+AJE/D036x+8SYN/uhFRsn9XjnOXHp0iNKC0s3SJgd1BtjPznwa3dvK524TeuS2yncdR8lecLw7kBstOltbeylzoWz41M9q/isD5KbvPrG5vzx7e9l7r7f7OSEkR/J/KrYffLdFI06r9zh+KfaAj9L7ni+yaIuO7ZNwhRr1UCNP4ehWQVjEGLU2/qpMVQaIyItxzQRZGD43h5UaWHRh08Oya6gsHAlIJHTqiLyY1IrODDXHkajrCN6p2emRFyMGFQodcnnoOlUMe1GUUqwqCOpAQnroitORepBmbhzFWZOUsBPM6ufChOOkTIrnkLQ3QxusQvts0aYvBnNN92B3O7sjeFQSzlZTu6O3ewTZ21l72jC03nK5u1OmR87WbeWIXeY7BLgyjqq4xjoUnuUi1NhooUFzz49+Z8/Pq9Xl8CjGATEmmCV+nMJEQxl9fZ8fb383ee/Pf332/EsiXaSuzol65h6urm9HEEkk1vefmVVVLmQBYjieLoiLkvj6fkFYs+3HJRCfxZPzo0lryY0agPwuptR+zQpkTbudNHH2nv0+rkRMMJpkwxoiAQQQSQQeB7FHjplPqmQk7YObVq8FduAJalpl6/JkLMdH1EnNfGJ8cFla1rcGbAKEQwjdUnqKFv2xgAeok+IPhRFMSjJDEmKEQEEnxJJu+C3s26E1YJ9aM6OZPr+duw8HyqGqti5yylmY7t4K3sD08u+LDJI6blCzHx+pDw8NhFStp1SRC+GbF2Kai3cf3J5XK3ofVUcvMt4Gp9MpUzqdE6t+lvLR2WAS8WcTXTshwRjnLNiUzto+o20MooJrHTGXxfdBhqnOmhHOGge8NVnrgzXsemzrxyN12tvJmYbTPXoas5MxP25b2ZuOhEYy+w6xCjNdskJprI8sci32nOVUZJqk55u1JWixsz67++Q+yh9vVPiqrsYLo7sKIjXcrrtrIdRIEINIEN+m6RqAZTMvvr13w/M4n8+iX530fVPqF2OLf1Bk1KBGEFmiTYlOehGKxQuIMeW0j99evfVtOOQvbDPiC5Gnt5Qs2dQR8kNNna0Xx1ZBLXJuQiLrWahbzyXLaXVtPuwk+IIIJ8QQfEEgnx8T4HwPiT7xEC08KWZbTyKSNJwIIEkHFNjQNir1rcfoR4+Fmubl9K3qY4vHeeiavavlsKA3XrWyoTUY8PCgXW40+Z5BhAvOwhF8PDwmN/b8bXW39Wti6UgHvDZk5/HMRWd22ZQYtk0LTrFrjLHVlMhY7ta4cyiby2OKktnkypWcZ8/uyBdtbWWco5Tshu0L5xsJINxmNp2hMDFfZKGgo2rurGU2dy9naC/UZGw2nfDTlUb6roiZx0Petw3l1UKYdde2HIqYWTLaiHCjK2MmBHJGbunDfnpDCFdWbUzippWUtoFMOt7KphzWhbHH3ScrwO9Zqsk83lnBASNzDXauqC3uZzu4WJfHKnmYVxvFDI9arw10+Gjpmm71l+yuiUHZ1bBeUFmc65a6aT7Y5qNLcj2tzKZ+26IXT32LnMnAhVqolqTG4fozBZCunc5UR2Vu0dLYm547ebV8O0YLRO0ed0eTLIpvBlw0aTxJv0EOIPj39+V70IthRICZTEYiEwSMKYAhSZKIyQoJJEgiNBUwggSMMjBESKKbIAolBgNRMwzEskI2JSxImUgKTUpmEJkkJJJQm/K58y4hKQUIk/vunu23bhgxlEZGmQ0ImWrFEY0mkaIUyz26RikgEUChIEgIGIFkMiSmYmkVCUpmYQa2k0ojETJomEGbDAmZppERkmJX6XStpEMopslEwzMmTNIJ04AkjF9S+tRqsV3iazRq3rMCZjwXgoKLKiyXuqliFk2IZlkTSplswV6eXnhSYFaRqxY5Sqjt6DE0SZVb5Ldo0bs6INTDuiajK11H51bb3Yotlp5dumDL2XuXsVEDxBZzb3dzQV5TE1MgOSlrw2reHDVjQmN0KYbyinHbs3GtiMkPyxxQN2wlQ58XnTNmjMSe5gpCIanRDuydjypgZVW8YlUhWFDW7x5kt6HaBV7NWSVbZyu7enDhnh7zNVT7pFWHSSDp12Hl+QuZmX6/B6mR7G3VArUgXFBVRsndgG3KGFlQGqLwShtqG48wNkXhGObBLyhYctNXl6LGwD3hbG6vTQpZIju5OIZhDJvd0QcrWZBdL0RORsPa2BjczGiJTWeUqhtVl44cOIHwHvf6z4D4+8B8fAP4dM0k9DSxgIbrLumfqXsMYpPsGqYpN3ciN7jygyJW1BFtT1zHt2bzMVhPC2HT10pBV029vKv0DW7Ics4nAzLUXshxbcteo2G1rx6ndoFZm1bsurzNovdGsqqcyrLNTW3gyaaE2IlERh1plNyM2bhOZlobmq0m9OHMmM754pbGtC7c1RqqVTDuZAcwairvJcEuh4eFMh2GcwptHJA9pJbJBCMLLtLRAlHiyBXeeHvDI8OxTLLrSqpYtFj3vPMou5rTtGZdXkoBwpxIWWV3SEGe1hezGqa64Q9mYsJs0KZe1zssbLsP1C0iONpystvCaFltE1rNPSgAPCZFgkTxZLoxUt1OI7VWhmIVPegwJ6cvJ7RAhVIqg8cq9WFYi6sPQPRmXujJFtae/fzgTUlQylSak4JgP8JRNhfjOb3/U3W2vvuc77wgATrUcrT0TDbUapMRM51619J4kKTcwmEfexENfUSSClZHaoKmvXkVEMJPNTVRJIoAn9nKL71xjgDr7JJuJUOyUE59J0TonAUaQdPdZcRTMJBrvn4IthIJrjF438LKqkPqSc2JGP50ZQqWJKFSlTipp+12nfnf87n5DXw7gkKSUh2JURjjJMEyRKJgEoTeZ2nmdJ1u2ucz7582e29NPilmrytbbi9qX9resatvPvYrq0716fNjUcrOZ4zdrvXbvzfORFm53uiJ7OozmL+2+/XfvfUjtd0fb6tagTuZzTm89tq04MYtjG55Mdl7d5fu9X7SvK19Ed3PaYj1fczr3s+vZ66zCpx17mfR31e1tTFOZwZzverXqyzcfblrW1Wm9Z3nXO49n3ZG91981T3b+3qluZpTXXt3G337i7575BYq17hv7VuWvw23ktGqMm0aKpLREYLG2jRtf6auFsQlFtRi1EWxsVGos7mKtLVsho1d37DEiYbgJghJdqqpztAWZZ7zuAsyzL3vFVPgEtlssfyATc/u8gJ51IBygW1AWIFW8KlzgWWIq0ik3VSX7cJLvT0oK6WQmYUpmlQyK0xRl701JyLrIDDWMWNIuXNSy7rqbSmlTbWaS5dNBIUUgS7uEJO7XHd0xdzuGQNco2NFt1pVcNurJra0GwbYoxVY3d1tSzbWnOUzRptCCjctdtmucxucuRcprWwJd1wE5xIBEMu7g7rju1WZy+4CyAsgLuISdECr0gVfb5gL5b6+uWz55bOHhTbfnNaq6qJmIxsgQElaDGDfpunOS7uY0ZIiMWUNEDJTJYIoubkUm5cogIO7lPa8eKigyEUJsEoJ3d3dRtNLlzHOYjc5gxmNXk/MtV/xatX1/vQQwMhJv4uskxgzRS5nbsQyhpKZNEGTu3XTcmZYzJEMRJoQozGSnd3Xc7ukOXcuiCYoYsD/ah4W2JIg8B5COgYmIILL+4JE7SIRmAT/bUjRCCP1RX+7ba6a1Tes1FJKSmUarYrDpMkpgmxMEP6NSYJuSkyTO+CdEzsSTwkmxyThEZSk2JTENhMEpKnicxOiWbicIf4nCG6VP9E3JqITeTknOSf4TYlTQ3JU/cjkKSkqFI3JqmqaE+kmDBDxNER8TtNiaE3MBNSYJg1xm68R5r3n9sDyElKiSyoyqZViKyFmQZWBYkypbW+a22xmd07LuO5duUd12iCXOhXca6lc66cdK7I6U6LspXcudZrrJ0dODI67uLnTrkhSI3Z103JErnMR3d1yLrru43ckdcubrubOZdzu66/n7/Hq222vxeWr9amtoQGQg2pqltoIIRNqgMxmTMykyTNKkxZNKTFMQaUDK6JmQNXRBG3TjDKNV/o7/2u8O7sf3tUwsYh3/oujkspmFXdEstLJfGT2dWUOcccJcO34XO8O8ubs99dEvTeZq8vXetttholxRS2P7Hd3f+72D5nJBbUDwNXGInF0rQmXICy1m2q1plrNoC+Gmbamb7wFsd99Yz8ICytYzLMVfqrK97auunvxtmZmZmaP6/w+L77IVToyyX9kiJB4gPNMIea3okQr/aEJX9u9LY2jXG8/uMSmuEUiUL8Ei0kkkkn7zB8EEAyEmj4FgzEM5T95+O7u7u7u/aVCzOzu4zs6DSEvAFFpDpu84223mAV/XNkfteRFlCz5FunQPMfWx/Mu8a28xusTH9tVqWdIdaaAZv6fYfbdl5SVk5el3yzL7pz8yRzXOdr8eNmVP9MZzJjWRiTFX/UdXMmbrZjFwVnn2PJT4CYFMLwpSfNhfz3gXwEySKWALBuwSzqxtxp3x36mlltt4umoSkpKhUm2deomvBN/E9JhBQmCYJSUmxpknJLB5Nk3Jxkck1kTckmpQlJSVoSk7Jok3JOSck9Y1zjAbvata4n5bplrU+XGtJBWkVGBT0BcCUmApO323f28fCZg4JgmRNifikkwT0lJ1zhME2Q43IZJkmhDTO/V3epz18c1VivPS3KVZreaBym2rI1Xbv0752TxDcm56G8pNEwCiYIDpJOAMJxMEAtsjhZszHiGGnkuTtP3cNoonQnXcRKIUmSYEpMpJ6TQKJiJkhMjjCGZINSXREZJUpNSbMENSc5nnv5i/Y+/cYDWK5fXM1eNVtTnMP23gXQTAJwTAAwmE/aQvmHmfibEk2MiaRIbUmgWGSYE0EwJ7EcJZEapSSieppkQ+J02EpNiZJudk2k0JoTcSkR2e00TVEaBKQ9Q8FQVU4reJilvcHMODMO7Xak73n3otB7V+zgx7pxPFwyAoBMC7N5BOCkFcEwKZbGiYJhO+DwmglSn43wmUpwfidkpMjPrAamhMIGEpKFBZFALwgKL2n9CBAu1Are1jfcNHD1ayUZ6N6z+64Vv0172dPZstkroFsQGQTApMtBHBKTYnG8efeR7sYEpMpzOA1+TAlEUmwlJUk33JsJ0gpNkezdJNyQ8E2TYmwlJ42KAFARMWVwFl4jFa91bj641n5BzHEkgXzAXqmKfWKKvljHFW9NqZK6E5DQMmCYJSfwSmhMkqGJlMmqamSspwL8sFH54qtq4Umo2JpRNCf4mJEykDaiS2d4FtTCqtslTMIveRrFS3yRFqBaWyGLCbcbkyRDglkOMc5cJ5pxPE2r6m/WnKFxqh32JOCqArCyCeEy0H2Pv9+HiiVDSUAt7cPIH1CNSUPSapgRUPE0Q0JhOEqZQ0JiI3IjKFRkymVtTKS4bZXhAq3p0plMm+2g+sktEi1H6kkdUWon/FSGLEq3yHmVkRtkV06zPn1vLjElx+r7v3DpCBH9jY+xrGT2qXmjoQkhOJgQMC5TcmE4JSYJglJgT+kqZJ0TE7LP6TZNgwJqTQmhKmU0JlMknPn+9498IbJ4SpoWHJOEjCCNU80yHFMFNpYpeZ2mTfp+HPnjs67Uye1qGqbCK6TKbU37Y1OkpNDYmUwTBNyZgaEskSdE1PxIhoUnWp5qQekyRieUTQkJIR4F0BYAE9ntOd6x3Xec96B0YQpBzuu/xRNddCq2QKsqqt7I79TFyp7nWKWtFjvfVnjX5fd3txra3TFW7aIeMyJGME4pNOdOMk1iBNf1tS1ZxjFmKtFzdN3dXK4520muWKNaMaMWuHIuXIuYtY2Oc2uFsajU7tO6ijXMRtzW5pIjVzfe/za0lRkjQrCsir6ZFWpzQF9NrVlmCEJKZQEaTKaDCSdx2d1zEmIOlcRCkJDFjYi7u3NySJKJu7XMV3bqWJSYijRRQpWxtFtoqimW0bVJWQtBBBJioyRqYFjJERMKyWNSbFIFkjTaTREqTGwBixmYqYYZgwCLINRpMkWSFNFRmKBSCRSaIMJPj5+vrW9k1JWoqybGotkrRSaDaMUUmkrRWQDRVGpMbGk0ao0rVK9NiNKlpaTUm5NME0J3pPbtf+GMHl+Cck/pOiekqckTKNRPRKTkkcGBSCcuzMg8rtf61P6saCl42IDMb9P6vHjH2mvRIh4R/lCH+VBbCYsRbEeE/gT+En+NBA+4/negjqki2IPOcIIzYiO7EhzTSxJE7EvEyVp6698903Qu2IXG8PVNihlwXgVxF+0yC3FNggkAFM07mt7az/N/fv7+4JqAmDqgLIhklJ0ShP0TfaRCeVEh1l99+83fvCdpSUm375mvv1bSgQKtUJhTYWWN261hYNDAxSVKQ17wIZlJwlJJr1r3p16z1j79jD8q1r2o746x2uNoUgts5VgXBggFwQOI0Y5qO2zYXr6A92wJLYCY70vOLL1O9x0uzl+TejsUdQqgKwIMg4uAsgpBOJgnJKHwnG+6U7ME+6mtt014ZwL4wJWpi2b7EAZ2xq/b4VhAN8OtV0BtCd7gjfK5xLUo9lEZ57FHwFgVxV5LTywjT8j5Tzz2JaSo+qWdkMzMDMzLruzJgWeWmOaKBuJK+xy5xbGTgkIHZmGMWd91o1DLFXrkx6my3PUl6682svFa1t6gLYUY5mRsrKSQCBShJJAMkhxP4UnXjzEYvx5o5SOXw1NO8BnWa7o0y8amZlnxUpjuXuje9FHFxkkdTQwnAZmZM7CnI18RDVh+MUaks7bUd89cdee/t8OkIb13dXzGbPrHRJIeCgxPvMPiaxC6yJMURMrJJr9w09Mmb/AawJM2ysQCTGWxzfz4x6cMzz8h56xUvZ9hEfNcbFZkAn4xR/ihmNpAhAJ3NDI61xNtwT0s/HMvxqSTR2uCgGBMhCLW1upym4V6Ynz0ywizJol4FaHFQWpHuKyJCJvokts2XXvOhaTp+y2udbINUMJfeJK8fHOyofeax9a1VNsiX54dMosyZlVV+WSRrKLMUm+DWRtil796QLt71VXOQnbIV1yic9NVVyxG+CXODMnz82k3wKb4LayFrCW+FV8Yq2yJcZSQzCSdkkq4PjHhEue+mNM+4kLQFWs8scw4MViEd7/M7v+/mu61vnvSKo2mIbrOlxPlOYgq2s3GPc93rTzWtQyjYJJI5BpjHnjw4vXNq8POTkaYk5tRHhmfcXEA3d7UQoF3f6+Z+yzp1vxhIkOk8kkYgf2oki0RmLMl94FWsUq+8rWUPOAvGcqKspcZZiKvQyVrEbUYVzgtU8VhFxsKuMhVximsoMArPb2nxM/tfsfffvfv3vXEIBAwMkgEK4VVNUyidUr2Er0vnMqszaOU0UosmotG2vi+2oiYzSQkWaExAKZraq1J/zIg/5ykyBiEGUGZLbN4gmMyY0JYwauqkZUVUlotkJnNrSNYlKT2N6YLK3uPVxXNqaLU0yZapgzp9wJwTAI+AfkY4Z+sse2z2GKWDAKwKfWwMUAQhs0G7ZwSTBJ+ZVT/pFM1qAnASQfqtTTde4kiHcMv07bhUExu6jiEkkBMQXK+FRJFq48OdtENL0bFY7NGMBvGmy5HjfdVU+KJxO3XXwLwOvbUykxYmkNJE0lWRRQWyIJZxkjdsyKQAYKqHUjkKKYzq247Y6tPA0WrcqqZ795rzvbi2+PT3Re7BmGFYIqiQb5JJJH0RB3AJ+nSCJvSEjHVX3nWlqykWJREqWakqrUrRNtlptWjViKwkksotkEpUS21GNQE3YIvKQAL1UMOkgBYj1O+itzyM6zuokAKrIEArddAgE1l2kITwuzhlA1XN+azGZATzDe7XT3OZkBKUkkg+x3hsgVateNkUmji86cM3RSZx0JAhUCjgoZCSSAqdzR35XJdvvf6vub2vF3uur8CF9tPdvdvLdr0W8t3JkmMJb/wshoVEpLH86Jg9p/mnfP775t1sqHnXbPrdAq1iBVldszNIFXHHGduO0zflAq137Uh+3Kqr4ypLhAqymtIFX1zpAq/nR0AAm6vtqhDM2Qaqb3852szQ4wM1k47MjjuF9kv4cmGwmwzpbHrhQty4Mcc7Dw2ctLZfGaSTto58rOF3q43KbrdUUsqbo/PWtae8MQCfuEJp+0gE26+00gEqaTEAmJPLGmljCYaQCVYBOcQxAJXeMJYBLYBKWAT9jTRzLMwmZ2HYGlpmXtrPL8KDxWtMrwiMtztdM5OqVsDsjTbz5y0Wham2u63L4wye85xq07/baaekk9R5UMIRuSiOOaTWT7ZhmbwPhucx1oiqpqgjoL1AcMiYJDbAoizmnFe+/HqiX45J+GAeGUawkZknbnSkt8VL49aSL6xJXTJxZBzQhxZzj+51qRI2qI5+wCO8lLfKumPWIVvYpdMD88d/rUquKga2Ik1qTpymCa2QNbALQBvzCEZYRk+YPjwVblCl7s37r1pUYSBqgSSTICf3+c86f5EP5vJHwdpQxh4plLJi1lqYu9YywWUwSksKlRUsGtgjOmIh3uSIdb96d6z/DfNuYkTruQEwSESgIDI81Si97VWmnon0M1FazNE6EYEBNPRrlN93ru++96e9RgEIQC3R4BQAMkizA5DiQQ9W2Vk535958Yzts5vOpP+AjZ5aZSgp6MAMDCAYTJCrrFtvyY5m0K1n0uv1T2zjGnY5G/3/iGkGKyQczQbHyLuRU2Ls05dv8R+7lZvG0JJkgQ5LevzMYModYurixFjGmbddk2plpF7kYxylIyYRy7Bt3VvcgdWVjUubrtdtTmLdLlzO5WcL5sHjcLeGQmSqspUG8zYrl1ujS7vGzDtc5cyhevcOhR2E3ipFUAdi6mQQm85Ut3ng7tq73TOWO9oU+hkZrNx5QiLkapZbFKPVcogYFcFu8qqeK1JSEw9jwbgZeCmd3MFafTaqvLEMDDvDjDto0jL2wrQRgRmxyJRV1lwtGWGu5NC9GZqVpsF9rDZpkp8wtur5+TT7ijdKsyqMgAFyHr9y49ojPp7ua6q4GbQAg/jJS+soq7PqafXTQrjFR6MVVcU9x9kxCBSDbfLbViH+1SBwqSZWQjcnGWV/v7GYkN7JLUSb07gLJG2bZWsNgxVig601ZTzIeWKMhJJSCw7gus1dTH39u3PvNfPfJBJJLLBKXX4+7vT3iJdfjVK2phBr1okviVWU75nxqnW4+NknPxoRUYBAV1NLuIB/VAVZQgSRdpxlNNQpSz2tqmK89qDfZ1X0pAhAKtc03arU6vU2CXw1vt8roZhMsKV9Yoq0zMRIyYQkFUWwrpjFOGPNz5XF3t5d1MJAMhBbdqPYtWce2jgAAt8rVrhw0bw3zf3liW4VOf3dtpYpZe5I0fM2k446ztnjnGiem/OVttttttXKcVXltOrDj7g3xnrPd9S7v39vv+ftfz+CAgIn1+Pfd3d3TjjuzOalebPs3br2SABbxXtu8EYt3dO8IWwRARJBHv8z937+vf758f173xFq1i1CpZprZmtDNNqvcUBnQljaBha4+oFZrdrJarDcdhk2hIAWOYjOHfOJxC0hCf/X9/8f4b/0/vxb7/V76/VAq+3zrWszVVUzTMaQAIZnYEbQIFVxfx4/HweKBj86+3iKXxHFbf5ETrvT/WMa6bE8IYjR/veDWg/3sIZNa93GOadVI1rZJHuNP5pGdqb2zRPIXu/eJ0yQMjTIu7hLUbAkgOgqkoSiZqI9xoE/Y8r9LPMepAneme+uSINDG/sZk2gEuKjklN9+srLPE0lEjFmNMYugQCdD5zCyzsDMqsnehg7xMdc4J9jEBN71mEE2yzVv3UgghJeh7ez2++b37nfdOdV9OpsUu1crHcT7KdkB3GUUtn/XNliaAKA+QgAE4gAXZ1LmlY08zUBe/nrraZl846ee21bZ2wgTvOH81xH05wkkkdJybUzUWOtbq7Qta+Qr6BWi4SEgoQhxAwpYBdpk2+t75f5rXe+76+UJJIDPWEkkiCwK2hQIMZmMnDVZRurrzC8NgYxR1rWHmaslWk53RAJBLFcehIQKjRDhuuYDTJLvLQDbdK+4hb2nB2RsYpTUSruJ2OctD6YjUoLsfWTq61x6jjzTPpxbLauWnmmW1G1m0ka4jF5L1e+MCZpzXlylzwxJm5sgjthgsIrCS3p++ebujFX9V6X53dHoDnGHY3ZHdaRJC8HdU/fd6zziYsrrjExS3k4zgh2WtOeYF5hr4jKJZJzl/6R8bLANZmcNNthIQO7iZlWzoQAX8/jLpXZfM5GfpATD+WGdqXXGVlifsTP5hpH8wDdnCG+b9CKu9WFLObuQjcOQ1WO+cqJJJjgOB6Ac/+r5/Z/l/n/7b9c8caHdhoTRCeHdmVZhaE/ncSSAf6+x/sohjOEEM1EiH+rICa5wjgBKkCZsSQ1GcSHP8wdZwclk/tfzbENLOK1Bjf5zXHrx1DEsaj+BBVi8OF/6OabzVarJIE/aYaV0J7nJmrXmmDFdVov6+2LXI0TGJL7hJrlglaFOZHGEma6xSbbVdd+evS+1553Pr+fXn25ehAa8UXo5TDqjMw7F+OpZKMMJIEDj66Pn3d779z95599+yc7zOZ97JDgCgqSRZEiBkAJi5gTV5bF3zSfYYrexR1W0gQ4OLwMJ2ca9HM3cNMTv0M0+cqwYYswG2JlxV6/GGayZpmiBSMJDEaQkkgHL3aWdpZ5Gtbvb4v3eu+77ywCGXajkjIBxxzAMLmTULPKQXBjIxeqEkkBp6zAm51nOMoj0DMNgFSAOsWPOHHcPKyejShIEJgxfewlhu6KEFWNxMEAxVklduJi13tTat3mDnjG1GbvnXNtrNmlb/ph7X4nvkjPcUn1htFkOrb9e9mGnPbI/a4szWcZsyu+NdtTvlbQpXGANLVnTDIJBLP8Hj6iAPiCSB/JFq2N5AO0i0J7+11pa7tV3WK2rNXiMS28YMUbaYN89xIROGhp9ic39frzwYXOEtbVzXVZrOdsy3N2JfJ3+yjFb9sPuhgxXUpz3tyHOipSR2O0qhJJAQcYbfdBPWLsjmm2AbZ6uNx71m1bbZbX5GaP337WbV9EkKR554zM5wkjKyJE/X9zwNKK7BlgvjFzuuG78Nb6d752973vHvePe8e90d/e97x73j3vHvdOzEe9073p3vTu+nOUmY7z3e9O96b7w52taUnvu8Oc4a1w3vl7WrWnPc9zJzWjm9nb2tWve+7jmeaNc2b1nF72tznu+94znxrRnF72tX1vX9j2dmtENnF7Wtvfue7j3teV53qMTSnq79fi8HbPv1613TaRRs07ABy4oxD5QShOymmnjrFme8QiWSZAwoYFKa+nSnlHgQX03CZlJOwmEYJglJKSpUKSUlIV+nU0GbInvGONWHfzXJOrvAVQYBOms8b8G9ovUhtG2OX3UrIwENxkISTROXxb1Js7M7Z5VmyXiIw1+dtil791ho2++Y7ffN6773qUQkkgIj0sFDMXMeDmPVMy++75uEqEISSQFISjKMLwLnr+5iEqgHR9zlBw2wE73SSSDnMSFpHlFNgxVlIJi/KY5XG/Z73nvb6tJJCYSSSKewwZrFRAJu8l6xWgvJpCwheYNT7jVoAt9qhJJAcHaKM+zmtYAVEgfObtfrdgCq332+Pd7475nXqgVb0Tt60kq4Gt3bzwzwR4R2dgrXyXY7nQE6eNpi/cd2MwGN941q75S1OPdCSSAdrej+s6ta+YxSh2yEkkAyRl+a57GbV+JbAqqsyuzNScZnMaq15HlAAKpkRneMgUvzD/M35svDdobbRlG6PsKS+QJa7TTt76itAP6u4UuhU3k0NPG3nc5+3a99wuKqpLN35GKU3VE8zPUCBo5mnpV+6/Lt3cU3XadzcdcvLa5q3v93u9JV68XGT6vsYMV37hOyeE7y2pupxS/zDivK26930PvdszjbDeprVKfVmnnkFRAhwBSCg5pzLDgxl3IekFqxy1VbJc0+QpJ0JrBQFDl6uDMYBe3NyhcEx4YoHM5kylQSbbBBA+4HNIck5FJKbE5qvoM6ctjh7VC4xa3fR2t78plt7DnZ3HY2x1r0rGM13nvec9728pSm3qdBtsmuL2hSJ8eVuHRc33d0YruEIVgGBBhwXttBxjAJjjea4AlcFPEIQRyQU75X16V732t+97344KqeEDpO/u2ii+Mkj1nFPXhNmc0SDbACYD3eaBWmokM/jFBV1sz2Onmtw4L1aqivyXfYJNjSbPs1vVkAAq8Z+/c9aTjPn427xxGqCwghYkkipFBipGUZgsKqYgVY6sJ0kM8zqkWsgvJxmYbGW9HIjG6tcWB7Gu3K57hJHKWzm85p6MVmzl2ddZ98eGZ3h2zQWqxxhkbwUIMTd66EYPT4u9Ya0y7epfW7bdREVEIS7DNOcd09lQ5W/d75XV30FxMGvCvmM99DAZ9kSSX0/yn/P+7r6F1HWbwzuzMO7iqrjBhbirLgzIjOcJE1/2/qIk/v97VEnioV9/j+vvcX1eoCCofc/t81bu8jatGpcsvSMWec2vrvxj/Wlx5DvndNIkqFSCgkwJhCEV1D1VJe23KoEid5qSwKXCd+aGvaOtiGCUlq18+w63x1jp/rF/cyk7veWrFYVUMzMCZZz617CBqN6uKVv7W+d772lpD+BIXEyEIChSrZrmmDtQnFic79trdEC6U870imY1aapP3lECAWkDImJlCTF+5TERNqRHaUh15cIjklAayOlOHEki7AT683z7Hsdz7nfecSAF4QKus9ZOu0BdIC99rNuurx598TEHmvSaKRG1EPs0mCa8Pbp7WNWPXAFqLO2/cBYGq9g2qmZ9U/23H+cy3y+G215tQ1iia23i8mXLwkYhCg+drtpgZB3Hnp22f3+ds95uFEfDz5+Wfni+Pg94PwrvaW+Ac+Y0GmGYzveuvZAzszxtj7qzzYr/notHotXuPdnRfK38eiC0fIoqlhQ1ptmUi/MHHr8h8t1vk6T0pmzL368+O3xVR+df8/u2BL1XazHrUzGtaOh3d7fd6sCQg+Am88VGfHq1u8VvTydUdPi/v2mpWsXV8TOWDVX+lxantM4Gvvk0cEkVzoU2TbFnrO0P3AnJ5cSAFdR+pe7Mziw6wAt75E09mE9t5eAJz14TMSCaLP5h2izjcxIq0IhaFnPalebuaO5MYrtmKnOqbDUaaHR8UxTN753znPevfom49wvuvkAArw8NXJfDFOq3tIrWnceFkEkk7IQBgGSBdsm9S2OecWxHQWKuCS7InBKggv7PZEEAUQkFUKrdesc72uOexzWtc971riECFcFBMQjikkZCpIjXHnLrwxNCD6iZqQMksIXXBPmhNEiht7q0lA8pMWSJUikRzYeVM99wAV6eqJJLtKECSrq4QJlt1pepeoXO4rDYtzT4o4bi7IABYpeYpyNBhFd5JoboWZt42qMqh7phmZmleelRmIzWKZsgAF5ks4fwMi7YajIywo67aN4JxGYQACiXxF23xD5rf2IEgyyQkIY48MEWx7PV3nXRRmbb34ajDG22EwyqyAAV+KOtFIB2K1tq1XaSzUmIFQxusoABVtlqT16WYLiSSi/OawgJv1nOK65Zz5r00n1tbYjEXi4qy5uLAmMmMbzuiAJ153kzVVWLhVsz5CNo9stkWxMA2G8SSEVBIQUl4nz9tRtIABPaCwzEbZpz9+7464xcAkY4y/CRP0C2BbItSzMYsyLt+v+v63bbh+n6f/fp2r/P+Hj/rpx8fuunWv++l/jH/H3+n4P+GzpEnsGKf6fkVzz/HPz/BlU+fxVvI2D8mKf/i7kinChIKQOItgA='))) \ No newline at end of file diff --git a/irlc/project1/unitgrade_data/Kiosk1.pkl b/irlc/project1/unitgrade_data/Kiosk1.pkl new file mode 100644 index 0000000000000000000000000000000000000000..9b5467cdfccda39aa44e3d37c721875ed038a5ee Binary files /dev/null and b/irlc/project1/unitgrade_data/Kiosk1.pkl differ diff --git a/irlc/project1/unitgrade_data/Kiosk2.pkl b/irlc/project1/unitgrade_data/Kiosk2.pkl new file mode 100644 index 0000000000000000000000000000000000000000..066206a99d6cfaa3faf6427d526b8d872f6f8b4c Binary files /dev/null and b/irlc/project1/unitgrade_data/Kiosk2.pkl differ diff --git a/irlc/project1/unitgrade_data/Kiosk3.pkl b/irlc/project1/unitgrade_data/Kiosk3.pkl new file mode 100644 index 0000000000000000000000000000000000000000..066206a99d6cfaa3faf6427d526b8d872f6f8b4c Binary files /dev/null and b/irlc/project1/unitgrade_data/Kiosk3.pkl differ diff --git a/irlc/project1/unitgrade_data/Pacman1.pkl b/irlc/project1/unitgrade_data/Pacman1.pkl new file mode 100644 index 0000000000000000000000000000000000000000..f37fc807282017b9e603748155519d4e98aec43b Binary files /dev/null and b/irlc/project1/unitgrade_data/Pacman1.pkl differ diff --git a/irlc/project1/unitgrade_data/Pacman10.pkl b/irlc/project1/unitgrade_data/Pacman10.pkl new file mode 100644 index 0000000000000000000000000000000000000000..2d64b4d89e99ddca0c6962c11fd8ba8aa491825a Binary files /dev/null and b/irlc/project1/unitgrade_data/Pacman10.pkl differ diff --git a/irlc/project1/unitgrade_data/Pacman11.pkl b/irlc/project1/unitgrade_data/Pacman11.pkl new file mode 100644 index 0000000000000000000000000000000000000000..78b2e18bbd93181f3b8ce15001c4913c34de1d6d Binary files /dev/null and b/irlc/project1/unitgrade_data/Pacman11.pkl differ diff --git a/irlc/project1/unitgrade_data/Pacman12.pkl b/irlc/project1/unitgrade_data/Pacman12.pkl new file mode 100644 index 0000000000000000000000000000000000000000..5a930f541bb5dcc56c4ffc4cd181f270c7f6cd6d Binary files /dev/null and b/irlc/project1/unitgrade_data/Pacman12.pkl differ diff --git a/irlc/project1/unitgrade_data/Pacman3.pkl b/irlc/project1/unitgrade_data/Pacman3.pkl new file mode 100644 index 0000000000000000000000000000000000000000..2d64b4d89e99ddca0c6962c11fd8ba8aa491825a Binary files /dev/null and b/irlc/project1/unitgrade_data/Pacman3.pkl differ diff --git a/irlc/project1/unitgrade_data/Pacman4.pkl b/irlc/project1/unitgrade_data/Pacman4.pkl new file mode 100644 index 0000000000000000000000000000000000000000..78b2e18bbd93181f3b8ce15001c4913c34de1d6d Binary files /dev/null and b/irlc/project1/unitgrade_data/Pacman4.pkl differ diff --git a/irlc/project1/unitgrade_data/Pacman6a.pkl b/irlc/project1/unitgrade_data/Pacman6a.pkl new file mode 100644 index 0000000000000000000000000000000000000000..3e711ce40b06ce356765ef1172d9f9524d665ea8 Binary files /dev/null and b/irlc/project1/unitgrade_data/Pacman6a.pkl differ diff --git a/irlc/project1/unitgrade_data/Pacman6b.pkl b/irlc/project1/unitgrade_data/Pacman6b.pkl new file mode 100644 index 0000000000000000000000000000000000000000..3e711ce40b06ce356765ef1172d9f9524d665ea8 Binary files /dev/null and b/irlc/project1/unitgrade_data/Pacman6b.pkl differ diff --git a/irlc/project1/unitgrade_data/Pacman6c.pkl b/irlc/project1/unitgrade_data/Pacman6c.pkl new file mode 100644 index 0000000000000000000000000000000000000000..3e711ce40b06ce356765ef1172d9f9524d665ea8 Binary files /dev/null and b/irlc/project1/unitgrade_data/Pacman6c.pkl differ diff --git a/irlc/project1/unitgrade_data/Pacman7a.pkl b/irlc/project1/unitgrade_data/Pacman7a.pkl new file mode 100644 index 0000000000000000000000000000000000000000..2d64b4d89e99ddca0c6962c11fd8ba8aa491825a Binary files /dev/null and b/irlc/project1/unitgrade_data/Pacman7a.pkl differ diff --git a/irlc/project1/unitgrade_data/Pacman7b.pkl b/irlc/project1/unitgrade_data/Pacman7b.pkl new file mode 100644 index 0000000000000000000000000000000000000000..2d64b4d89e99ddca0c6962c11fd8ba8aa491825a Binary files /dev/null and b/irlc/project1/unitgrade_data/Pacman7b.pkl differ diff --git a/irlc/project1/unitgrade_data/Pacman8a.pkl b/irlc/project1/unitgrade_data/Pacman8a.pkl new file mode 100644 index 0000000000000000000000000000000000000000..78b2e18bbd93181f3b8ce15001c4913c34de1d6d Binary files /dev/null and b/irlc/project1/unitgrade_data/Pacman8a.pkl differ diff --git a/irlc/project1/unitgrade_data/Pacman8b.pkl b/irlc/project1/unitgrade_data/Pacman8b.pkl new file mode 100644 index 0000000000000000000000000000000000000000..78b2e18bbd93181f3b8ce15001c4913c34de1d6d Binary files /dev/null and b/irlc/project1/unitgrade_data/Pacman8b.pkl differ diff --git a/irlc/project1/unitgrade_data/Pacman9.pkl b/irlc/project1/unitgrade_data/Pacman9.pkl new file mode 100644 index 0000000000000000000000000000000000000000..5a930f541bb5dcc56c4ffc4cd181f270c7f6cd6d Binary files /dev/null and b/irlc/project1/unitgrade_data/Pacman9.pkl differ diff --git a/irlc/project2/Latex/02465project2_handin.tex b/irlc/project2/Latex/02465project2_handin.tex new file mode 100644 index 0000000000000000000000000000000000000000..045ca4d444de31ffaabdd612bd38a7d7e207cab4 --- /dev/null +++ b/irlc/project2/Latex/02465project2_handin.tex @@ -0,0 +1,146 @@ +\documentclass[12pt,twoside]{article} +%\usepackage[table]{xcolor} % important to avoid options clash. +%\input{02465shared_preamble} +%\usepackage{cleveref} +\usepackage{url} +\usepackage{graphics} +\usepackage{multicol} +\usepackage{rotate} +\usepackage{rotating} +\usepackage{booktabs} +\usepackage{hyperref} +\usepackage{pifont} +\usepackage{latexsym} +\usepackage[english]{babel} +\usepackage{epstopdf} +\usepackage{etoolbox} +\usepackage{amsmath} +\usepackage{amssymb} +\usepackage{multirow,epstopdf} +\usepackage{fancyhdr} +\usepackage{booktabs} +\usepackage{xcolor} +\newcommand\redt[1]{ {\textcolor[rgb]{0.60, 0.00, 0.00}{\textbf{ #1} } } } + + +\newcommand{\m}[1]{\boldsymbol{ #1}} +\newcommand{\yoursolution}{ \redt{(your solution here) } } + + + +\title{ Report 2 hand-in } +\date{ \today } +\author{Alice (\texttt{s000001})\and Bob (\texttt{s000002})\and Clara (\texttt{s000003}) } + +\begin{document} +\maketitle + +\begin{table}[ht!] +\caption{Attribution table. Feel free to add/remove rows and columns} +\begin{tabular}{llll} +\toprule + & Alice & Bob & Clara \\ +\midrule + 1: Formulate Yodas pendulum as a linear problem & 0-100\% & 0-100\% & 0-100\% \\ + 2: State at a later time & 0-100\% & 0-100\% & 0-100\% \\ + 3: State at a later time II & 0-100\% & 0-100\% & 0-100\% \\ + 4: Eigenvalues and powers & 0-100\% & 0-100\% & 0-100\% \\ + 5: Analytical expression of Eigenvalues using Euler discretization & 0-100\% & 0-100\% & 0-100\% \\ + 6: Bound using Euler discretization & 0-100\% & 0-100\% & 0-100\% \\ + 7: Matrix norm of Exponential discretization (harder) & 0-100\% & 0-100\% & 0-100\% \\ + 8: Stability & 0-100\% & 0-100\% & 0-100\% \\ + 9: Discretization & 0-100\% & 0-100\% & 0-100\% \\ + 10: Linearization & 0-100\% & 0-100\% & 0-100\% \\ + 11: Unitgrade self-check & 0-100\% & 0-100\% & 0-100\% \\ + 12: Optimal planning & 0-100\% & 0-100\% & 0-100\% \\ + 13: Control using simple linearization & 0-100\% & 0-100\% & 0-100\% \\ + 14: MPC & 0-100\% & 0-100\% & 0-100\% \\ +\bottomrule +\end{tabular} +\end{table} + +%\paragraph{Statement about collaboration:} +%Please edit this section to reflect how you have used external resources. The following statement will in most cases suffice: +%\emph{The code in the irls/project1 directory is entirely} + +%\paragraph{Main report:} +Headings have been inserted in the document for readability. You only have to edit the part which says \yoursolution. + +\section{Master Yodas pendulum (\texttt{yoda.py})}\label{yoda1} +\subsubsection*{{\color{red}Problem 1: Formulate Yodas pendulum as a linear problem}} + + \begin{align} + A & = \begin{bmatrix} \cdots \end{bmatrix} \\ + B & = \begin{bmatrix} \cdots \end{bmatrix} + \end{align} + \yoursolution + +\subsubsection*{{\color{red}Problem 2: State at a later time}} + + To solve the first part, we can write $\m x_N = \begin{bmatrix} \cdots \end{bmatrix}$ + + As for the second part we get: +\begin{align} +\tilde A_0 & = \begin{bmatrix} \cdots \end{bmatrix}, \quad A_0 = \begin{bmatrix} \cdots \end{bmatrix} +\end{align} + \yoursolution +\subsubsection*{{\color{red}Problem 4: Eigenvalues and powers}} + +Assume $\lambda_1, \lambda_2$ are the eigenvalues ... then the Eigenvalues of $M$ is ... similarly for $\tilde M$ ... +\yoursolution + +\subsubsection*{{\color{red}Problem 5: Analytical expression of Eigenvalues using Euler discretization}} + +... we get a characteristic polynomial of ... and therefore it follows from Mat1 that the two Eigenvalues are ... +\yoursolution + +\subsubsection*{{\color{red}Problem 6: Bound using Euler discretization}} + + Using Euler discretization we get the upper bound: + $$ +\| \m x_N \| \leq \cdots +$$ +\yoursolution + +\subsubsection*{{\color{red}Problem 7: Matrix norm of Exponential discretization (harder)}} + +Using exponential discretization we get an upper bound of: + $$ + \| \m x_N \| \leq \cdots + $$ + \yoursolution + +\section{R2D2 and control (\texttt{r2d2.py})} +\subsubsection*{{\color{red}Problem 9: Discretization}} + + $$ + \m x_{k+1} = \m f_k(\m x_k, \m u_k) = \begin{bmatrix} \cdots \\ \cdots \\ \cdots \end{bmatrix}$$ + +\subsubsection*{{\color{red}Problem 10: Linearization}} + +$$ + \m x_{k+1} \approx \begin{bmatrix} \cdots \\ \cdots \\ \cdots \end{bmatrix} \m x_k + + \begin{bmatrix} \cdots \\ \cdots \\ \cdots \end{bmatrix} \m u_k + + \begin{bmatrix} \vdots \end{bmatrix} +$$ + +\subsubsection*{{\color{red}Problem 12: Optimal planning}} + + \begin{center}\includegraphics[width=.5\linewidth]{figures/your_answer}~ + \includegraphics[width=.5\linewidth]{figures/your_answer} \end{center} + +\subsubsection*{{\color{red}Problem 13: Control using simple linearization}} + + % Just generate the figures using the script and change the path below. + \begin{center}\includegraphics[width=.5\linewidth]{figures/your_answer}~ + \includegraphics[width=.5\linewidth]{figures/your_answer} \end{center} +Intuitively, the second case fails because... \yoursolution + +\subsubsection*{{\color{red}Problem 14: MPC}} + + \begin{center}\includegraphics[width=.6\linewidth]{figures/your_answer}%~ + % \includegraphics[width=.5\linewidth]{figures/your_answer} + \end{center} + Iterative linearization solves the problem because... \yoursolution + +\end{document} \ No newline at end of file diff --git a/irlc/project2/Latex/figures/your_answer.pdf b/irlc/project2/Latex/figures/your_answer.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d8c092974e20aaaf1165958a53bdce3a2ebdbf8f Binary files /dev/null and b/irlc/project2/Latex/figures/your_answer.pdf differ diff --git a/irlc/project2/__init__.py b/irlc/project2/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8794db4fc72b62ae50ebe61fd5ce31a77a77992e --- /dev/null +++ b/irlc/project2/__init__.py @@ -0,0 +1,2 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""This file is required for the test system but should otherwise be empty.""" diff --git a/irlc/project2/project2_grade.py b/irlc/project2/project2_grade.py new file mode 100644 index 0000000000000000000000000000000000000000..4dfffbd5d90249e2df10dbb2faf48011c61185a1 --- /dev/null +++ b/irlc/project2/project2_grade.py @@ -0,0 +1,4 @@ +# irlc/project2/project2_tests.py +''' WARNING: Modifying, decompiling or otherwise tampering with this script, it's data or the resulting .token file will be investigated as a cheating attempt. ''' +import bz2, base64 +exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file diff --git a/irlc/project2/project2_tests.py b/irlc/project2/project2_tests.py new file mode 100644 index 0000000000000000000000000000000000000000..8b43727d460d025580c4a71c45e6da655252f98a --- /dev/null +++ b/irlc/project2/project2_tests.py @@ -0,0 +1,184 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from unitgrade import UTestCase, Report +import irlc +import numpy as np + +class YodaProblem1(UTestCase): + """ Test the get_A_B() function (Section 1, Problem 1) """ + def test_A_B(self): + from irlc.project2.yoda import get_A_B + for g in [9.82, 5.1]: + for L in [0.2, 0.5, 1.1]: + A,B = get_A_B(g,L) + # To get the expected output of a test (in the cases where it is not specified manually), + # simply use the line self.get_expected_test_value() *right before* running the test-function itself. + # print("The expected value is", self.get_expected_test_value()) + # If the code does not work, you need to upgrade unitgrade to the most recent version: + # pip install unitgrade --upgrade --no-cache + print(A) + self.assertLinf(A) + print(B) + self.assertLinf(B) + +class YodaProblem2(UTestCase): + r""" Yodas pendulum: Problem 2 """ + def test_A0(self): + from irlc.project2.yoda import A_ei, A_euler + for g in [9.2, 10]: + for L in [0.2, 0.4]: + for Delta in [0.1, 0.2]: + self.assertLinf(A_euler(g, L, Delta)) # Test Euler discretization + self.assertLinf(A_ei(g, L, Delta)) # Test exponential discretization + + +class YodaProblem3(UTestCase): + r""" Yodas pendulum: Problem 3 """ + def test_M(self): + from irlc.project2.yoda import M_ei, M_euler + for g in [9.2, 10]: + for L in [0.2, 0.4]: + for Delta in [0.1, 0.2]: + for N in [3, 5]: + self.assertLinf(M_ei(g, L, Delta, N)) # Test Euler discretization + self.assertLinf(M_euler(g, L, Delta, N)) # Test exponential discretization + + +class YodaProblem6(UTestCase): + r""" Yodas pendulum: Bound using Euler discretization Problem 6 """ + def test_xN_euler_bound(self): + from irlc.project2.yoda import xN_bound_euler + for g in [9.2, 10]: + for L in [0.2, 0.4]: + for Delta in [0.1, 0.2]: + for N in [3, 5]: + self.assertLinf(xN_bound_euler(g, L, Delta, N)) + +class YodaProblem7(UTestCase): + r"""Yodas pendulum: Bound using exponential discretization Problem 7 """ + def test_xN_euler_bound(self): + from irlc.project2.yoda import xN_bound_ei + for g in [9.2, 10]: + for L in [0.2, 0.4]: + for Delta in [0.1, 0.2]: + for N in [3, 5]: + self.assertLinf(xN_bound_ei(g, L, Delta, N)) + + +class R2D2Problem15(UTestCase): + r"""R2D2: Tests the linearization and discretization code in Problem 9 and Problem 10""" + def test_f_euler_zeros(self): + # Test in a simple case: + x = np.zeros((3,)) + u = np.asarray([1,0]) + from irlc.project2.r2d2 import f_euler + self.assertLinf(f_euler(x, u, Delta=0.05)) + self.assertLinf(f_euler(x, u, Delta=0.1)) + + def test_f_euler(self): + np.random.seed(42) + for _ in range(4): + x = np.random.randn(3) + u = np.random.randn(2) + from irlc.project2.r2d2 import f_euler + self.assertLinf(f_euler(x, u, Delta=0.05)) + self.assertLinf(f_euler(x, u, Delta=0.1)) + + def checklin(self, x_bar, u_bar): + from irlc.project2.r2d2 import linearize + A, B, d = linearize(x_bar, u_bar, Delta=0.05) + self.assertLinf(A) + self.assertLinf(B) + self.assertLinf(d) + + def test_linearization1(self): + x_bar = np.asarray([0, 0, 0]) + u_bar = np.asarray([1, 0]) + self.checklin(x_bar, u_bar) + + def test_linearization2(self): + x_bar = np.asarray([0, 0, 0.24]) + u_bar = np.asarray([1, 0]) + self.checklin(x_bar, u_bar) + + def test_linearization3(self): + np.random.seed(42) + for _ in range(10): + x_bar = np.random.randn(3) + u_bar = np.asarray([1, 0]) + self.checklin(x_bar, u_bar) + +class R2D2Direct(UTestCase): + r"""Problem 12: R2D2 and direct methods """ + def chk_direct(self, x_target): + from irlc.project2.r2d2 import drive_to_direct + states = drive_to_direct(x_target=x_target, plot=False) + self.assertIsInstance(states, np.ndarray) # Test states are an ndarray + self.assertEqualC(states.shape) # Test states have the right shape + self.assertL2(states, tol=0.03) + + def test_direct_1(self): + x_target = (2, 0, 0) + self.chk_direct(x_target) + + def test_direct_2(self): + x_target = (2, 2, np.pi / 2) + self.chk_direct(x_target) + + +class R2D2Linearization(UTestCase): + """Problem 13: R2D2 and simple linearization.""" + def chk_linearization(self, x_target): + from irlc.project2.r2d2 import drive_to_linearization + states = drive_to_linearization(x_target=x_target, plot=False) + self.assertIsInstance(states, np.ndarray) # Test states are an ndarray + self.assertEqualC(states.shape) # Test states have the right shape + self.assertL2(states, tol=0.03) + + def test_linearization_1(self): + x_target = (2, 0, 0) + self.chk_linearization(x_target) + + def test_linearization_2(self): + x_target = (2, 2, np.pi / 2) + self.chk_linearization(x_target) + +class R2D2_MPC(UTestCase): + r"""Problem 14: R2D2 and MPC.""" + def chk_mpc(self, x_target): + from irlc.project2.r2d2 import drive_to_mpc + states = drive_to_mpc(x_target=x_target, plot=False) + self.assertIsInstance(states, np.ndarray) # Test states are an ndarray + self.assertEqualC(states.shape) # Test states have the right shape + self.assertL2(states, tol=0.03) + + def test_mpc_1(self): + self.chk_mpc(x_target=(2,0,0) ) + + def test_mpc_2(self): + self.chk_mpc(x_target=(2, 2, np.pi / 2)) + +class Project2(Report): + title = "Project part 2: Control" + pack_imports = [irlc] + + yoda = [ + (YodaProblem1, 10), + (YodaProblem2, 10), + (YodaProblem3, 10), + (YodaProblem6, 8), + (YodaProblem7, 2) + ] + r2d2 = [ + (R2D2Problem15, 10), + (R2D2Direct, 10), + (R2D2Linearization, 10), + (R2D2_MPC, 10), + ] + + questions = [] + questions += yoda + questions += r2d2 + +if __name__ == '__main__': + from unitgrade import evaluate_report_student + evaluate_report_student(Project2() ) diff --git a/irlc/project2/project2_tests_complete_grade.py b/irlc/project2/project2_tests_complete_grade.py new file mode 100644 index 0000000000000000000000000000000000000000..70f9d0b9955521d888c23da7afff663de1a2ae20 --- /dev/null +++ b/irlc/project2/project2_tests_complete_grade.py @@ -0,0 +1,4 @@ +# irlc/project2/project2_tests_complete.py +''' WARNING: Modifying, decompiling or otherwise tampering with this script, it's data or the resulting .token file will be investigated as a cheating attempt. ''' +import bz2, base64 +exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file diff --git a/irlc/project2/r2d2.py b/irlc/project2/r2d2.py new file mode 100644 index 0000000000000000000000000000000000000000..624158bee199c654174d87db792fbd667de38d4c --- /dev/null +++ b/irlc/project2/r2d2.py @@ -0,0 +1,210 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import time +import numpy as np +import sympy as sym +import matplotlib.pyplot as plt +from gymnasium.spaces import Box +# matplotlib.use('Qt5Agg') This line may be useful if you are having matplotlib problems on Linux. +from irlc.ex04.discrete_control_model import DiscreteControlModel +from irlc.ex04.control_environment import ControlEnvironment +from irlc.ex03.control_model import ControlModel +from irlc.ex03.control_cost import SymbolicQRCost +from irlc.ex05.direct_agent import DirectAgent +from irlc.ex05.direct import get_opts, guess +from irlc.ex07.linearization_agent import LinearizationAgent +from irlc.project2.utils import R2D2Viewer +from irlc import Agent, train, plot_trajectory, savepdf + +dt = 0.05 # Time discretization Delta +Tmax = 5 # Total simulation time (in all instances). This means that N = Tmax/dt = 100. +x22 = (2, 2, np.pi / 2) # Where we want to drive to: x_target + +class R2D2Model(ControlModel): # This may help you get started. + state_labels = ["$x$", "$y$", r"$\gamma$"] + action_labels = ["Cart velocity $v$", r'Yaw rate $\omega$'] # Define constants as needed here (look at other environments); Note there is an easy way to add labels! + + def __init__(self, x_target=(2,2,np.pi/2), Q0=1.): # This constructor is one possible choice. + # Q0: The Q-matrix for the cF-term in the cost function (see problem description) + # x_target: The state we will drive towards. + self.x_target = np.asarray(x_target) + self.Q0 = Q0 + self.Tmax = 5 # Plan for a maximum of 5 seconds. + # Set up a variable for rendering (optional) and call superclass. + self.viewer = None + super().__init__() + + def get_cost(self) -> SymbolicQRCost: + # The cost function uses self.Q0 to define the appropriate cost. It has the same meaning as the lecture description + cost = SymbolicQRCost(Q=np.zeros(3), R=np.eye(2)) + cost += cost.goal_seeking_cost(x_target=self.x_target)*self.Q0 + return cost + + def tF_bound(self) -> Box: + return Box(self.Tmax, self.Tmax, shape=(1,)) + + def x0_bound(self) -> Box: + return Box(0, 0, shape=(self.state_size,)) + + def xF_bound(self) -> Box: + # TODO: 1 lines missing. + raise NotImplementedError("Complete this function to specify the target of R2D2.") + + # TODO: 3 lines missing. + raise NotImplementedError("Complete model dynamics here.") + + """ These are two helper functions. They add rendering functionality so you can eventually use the environment as + + > env = R2D2Environment(render_mode='human') + + and see a small animation. + """ + def close(self): + if self.viewer is not None: + self.viewer.close() + + def render(self, x, render_mode="human"): + if self.viewer is None: + self.viewer = R2D2Viewer(x_target=self.x_target) # Target is the red cross. + self.viewer.update(x) + time.sleep(0.05) + return self.viewer.blit(render_mode=render_mode) + + +class R2D2Environment(ControlEnvironment): + def __init__(self, Tmax=Tmax, Q0=0., x_target=x22, dt=None, render_mode=None): + assert dt is not None, "Remember to specify the discretization time!" + model = R2D2Model(Q0=Q0, x_target=x_target) # Create an R2D2 ControlModel with the given parameters. + dmodel = DiscreteControlModel(model, dt=dt) # Create a discrete version of the R2D2 ControlModel + super().__init__(dmodel, Tmax=Tmax, render_mode=render_mode) + +# TODO: 9 lines missing. +raise NotImplementedError("Your code here.") + +def f_euler(x : np.ndarray, u : np.ndarray, Delta=0.05) -> np.ndarray: + """ Solve Problem 9. The function should compute + > x_next = f_k(x, u) + """ + # TODO: 1 lines missing. + raise NotImplementedError("return next state") + return x_next + +def linearize(x_bar, u_bar, Delta=0.05): + """ Linearize R2D2's dynamics around the two vectors x_bar, u_bar + and return A, B, d so that + + x_{k+1} = A x_k + B u_k + d (approximately). + + The function should return linearization matrices A, B and d. + """ + # Create A, B, d as numpy ndarrays. + # TODO: 4 lines missing. + raise NotImplementedError("Insert your solution and remove this error.") + return A, B, d + +def drive_to_linearization(x_target, plot=True): + """ + Plan in a R2D2 model with specific value of x_target (in the cost function). We use Q0=1.0. + + this function will linearize the dynamics around xbar=0, ubar=0 to get a linear approximation of the model, + and then use that to plan on a horizon of N=50 steps to get a control law (L_0, l_0). This is then applied + to generate actions. + + Plot is an optional parameter to control plotting. the plot_trajectory(trajectory, env) method may be useful. + + The function should return the states visited as a (samples x state-dimensions) matrix, i.e. same format + as the default output of trajectories when you use train(...). + + Hints: + * The control method is identical to one we have seen in the exercises/notes. You can re-purpose the code from that week. + * Remember to set Q0=1 + """ + # TODO: 7 lines missing. + raise NotImplementedError("Implement function body") + return traj[0].state + +def drive_to_direct(x_target, plot=False): + """ + Optimal planning in the R2D2 model with specific value of x_target using the direct method. + Remember that for this problem we set Q0=0, and implement x_target as an end-point constraint (see examples from exercises). + + Plot is an optional parameter to control plotting, and to (optionally) visualize the environment using code such as:: + + env = R2D2Environment(..., render_mode='human' if plot else None) + + For making the actual plot, the plot_trajectory(trajectory, env) method may be useful (see examples from exercises to see how labels can be specified) + + The function should return the states visited as a (samples x state-dimensions) matrix, i.e. same format + as the default output of trajectories when you use train(...). + + Hints: + * The control method (Direct method) is identical to what we did in the exercises, but you have to specify the options + to implement the correct grid-refinement of N=10, N=20 and N=40. + * Remember to set Q0=0. + """ + # TODO: 10 lines missing. + raise NotImplementedError("Implement function body") + return traj[0].state + +def drive_to_mpc(x_target, plot=True) -> np.ndarray: + """ + Plan in a R2D2 model with specific value of x_target (in the cost function) using iterative MPC (see problem text). + Use Q0 = 1. in the cost function (see the R2D2 model class) + + Plot is an optional parameter to control plotting. the plot_trajectory(trajectory, env) method may be useful. + + The function should return the states visited as a (samples x state-dimensions) matrix, i.e. same format + as the default output of trajectories when you use train(...). + + Hints: + * The control method is *nearly* identical to the linearization control method. Think about the differences, + and how a solution to one can be used in another. + * A bit more specific: Linearization is handled similarly to the LinearizationAgent, however, we need to update + (in each step) the xbar/ubar states/actions we are linearizing about, and then just use the immediate action computed + by the linearization agent. + * My approach was to implement a variant of the LinearizationAgent. + """ + # TODO: 6 lines missing. + raise NotImplementedError("Implement function body") + return traj[0].state + +if __name__ == "__main__": + r2d2 = R2D2Model() + print(r2d2) # This will print out details of your R2D2 model. + + # Check Problem 10 + x = np.asarray( [0, 0, 0] ) + u = np.asarray( [1,0]) + print("x_k =", x, "u_k =", u, "x_{k+1} =", f_euler(x, u, dt)) + + A,B,d = linearize(x_bar=x, u_bar=u, Delta=dt) + print("x_{k+1} ~ A x_k + B u_k + d") + print("A:", A) + print("B:", B) + print("d:", d) + + # Test the simple linearization method (Problem 12) + states = drive_to_direct(x22, plot=True) + savepdf('r2d2_direct') + plt.show() + # Build plot assuming that states is in the format (samples x coordinates-of-state). + plt.plot(states[:,0], states[:,1], 'k-', label="R2D2's (x, y) trajectory") + plt.legend() + plt.xlabel("x") + plt.ylabel("y") + savepdf('r2d2_direct_B') + plt.show() + + # Test the simple linearization method (Problem 13) + drive_to_linearization((2,0,0), plot=True) + savepdf('r2d2_linearization_1') + plt.show() + + drive_to_linearization(x22, plot=True) + savepdf('r2d2_linearization_2') + plt.show() + + # Test iterative LQR (Problem 14) + state = drive_to_mpc(x22, plot=True) + print(state[-1]) + savepdf('r2d2_iterative_1') + plt.show() diff --git a/irlc/project2/unitgrade_data/R2D2Direct.pkl b/irlc/project2/unitgrade_data/R2D2Direct.pkl new file mode 100644 index 0000000000000000000000000000000000000000..eb3973b93e24e5cfcc9b5ca7c2289a3a1d3a71e0 Binary files /dev/null and b/irlc/project2/unitgrade_data/R2D2Direct.pkl differ diff --git a/irlc/project2/unitgrade_data/R2D2Linearization.pkl b/irlc/project2/unitgrade_data/R2D2Linearization.pkl new file mode 100644 index 0000000000000000000000000000000000000000..1977f1e9fe2e3c37bbcd85d178b77df83561e8ed Binary files /dev/null and b/irlc/project2/unitgrade_data/R2D2Linearization.pkl differ diff --git a/irlc/project2/unitgrade_data/R2D2Problem15.pkl b/irlc/project2/unitgrade_data/R2D2Problem15.pkl new file mode 100644 index 0000000000000000000000000000000000000000..ba6d982fc7ad02f136ba25a1dfa8984db6233555 Binary files /dev/null and b/irlc/project2/unitgrade_data/R2D2Problem15.pkl differ diff --git a/irlc/project2/unitgrade_data/R2D2_MPC.pkl b/irlc/project2/unitgrade_data/R2D2_MPC.pkl new file mode 100644 index 0000000000000000000000000000000000000000..b3670d7e508ed0fda0dd3ecd811d09893c3234e8 Binary files /dev/null and b/irlc/project2/unitgrade_data/R2D2_MPC.pkl differ diff --git a/irlc/project2/unitgrade_data/YodaProblem1.pkl b/irlc/project2/unitgrade_data/YodaProblem1.pkl new file mode 100644 index 0000000000000000000000000000000000000000..e8d95ca14e3032826dca52c996e197de4ff290d5 Binary files /dev/null and b/irlc/project2/unitgrade_data/YodaProblem1.pkl differ diff --git a/irlc/project2/unitgrade_data/YodaProblem2.pkl b/irlc/project2/unitgrade_data/YodaProblem2.pkl new file mode 100644 index 0000000000000000000000000000000000000000..472b8f3f9d46e309fb44636e16cab8ed585b5894 Binary files /dev/null and b/irlc/project2/unitgrade_data/YodaProblem2.pkl differ diff --git a/irlc/project2/unitgrade_data/YodaProblem3.pkl b/irlc/project2/unitgrade_data/YodaProblem3.pkl new file mode 100644 index 0000000000000000000000000000000000000000..7cfd67e961d93d87a739c9b7d69d5c97da114a0b Binary files /dev/null and b/irlc/project2/unitgrade_data/YodaProblem3.pkl differ diff --git a/irlc/project2/unitgrade_data/YodaProblem6.pkl b/irlc/project2/unitgrade_data/YodaProblem6.pkl new file mode 100644 index 0000000000000000000000000000000000000000..9978ddfe8f3a4fe1b09c0b73a0025163862b742d Binary files /dev/null and b/irlc/project2/unitgrade_data/YodaProblem6.pkl differ diff --git a/irlc/project2/unitgrade_data/YodaProblem7.pkl b/irlc/project2/unitgrade_data/YodaProblem7.pkl new file mode 100644 index 0000000000000000000000000000000000000000..e7d916b7cc20fd28de12c5da4fc1ece4dfd4be80 Binary files /dev/null and b/irlc/project2/unitgrade_data/YodaProblem7.pkl differ diff --git a/irlc/project2/utils.py b/irlc/project2/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..355be7a117d3babf8fc90c064fd7f4d67501a54f --- /dev/null +++ b/irlc/project2/utils.py @@ -0,0 +1,53 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.utils.graphics_util_pygame import UpgradedGraphicsUtil, rotate_around +import numpy as np + +""" This file contains code you can either use (or not) to render the R2D2 robot. class is already called correctly by your R2D2 class, +and you don't really have to think too carefully about what the code does unless you want to R2D2 to look better. +""" + + +class R2D2Viewer(UpgradedGraphicsUtil): + def __init__(self, x_target = (0,0)): + self.x_target = x_target + width = 800 + self.scale = width / 1000 + xlim = 3 + self.dw = self.scale * 0.1 + super().__init__(screen_width=width, xmin=-xlim, xmax=xlim, ymin=xlim, ymax=-xlim, title='R2D2') + self.xlim = xlim + def render(self): + # self. + self.draw_background(background_color=(255, 255, 255)) + dw = self.dw + self.line("t1", (-self.xlim, 0), (self.xlim, 0), width=1, color=(0,) * 3) + self.line("t1", (0, -self.xlim), (0, self.xlim), width=1, color=(0,) * 3) + + + self.circle("r2d2", pos=(self.x[0], self.x[1]), r=24, outlineColor=(100, 100, 200), fillColor=(100, 100, 200)) + self.circle("r2d2", pos=(self.x[0], self.x[1]), r=20, outlineColor=(100, 100, 200), fillColor=(150, 150, 255)) + self.circle("r2d2", pos=(self.x[0], self.x[1]), r=2, outlineColor=(100, 100, 200), fillColor=(0,)*3) + + dx = 0.13 + dy = dx/2.5 + wheel = [(-dx, dy), (dx, dy), (dx, -dy), (-dx, -dy) ] + ddy = 0.20 + w1 = [ (x, y + ddy) for x, y in wheel] + w1 = rotate_around(w1, (0,0), angle=self.x[2] / np.pi * 180) + + w2 = [(x, y - ddy) for x, y in wheel] + w2 = rotate_around(w2, (0, 0), angle=self.x[2] / np.pi * 180) + + + self.polygon("wheel1", coords=[ (x + self.x[0], self.x[1] + y) for x, y in w1], filled=True, fillColor=(200,)*3, outlineColor=(100,)*3, closed=True) + self.polygon("wheel2", coords=[ (x + self.x[0], self.x[1] + y) for x, y in w2], filled=True, fillColor=(200,)*3, outlineColor=(100,)*3, closed=True) + + dc = 0.1 + xx = self.x_target[0] + yy = self.x_target[1] + self.line("t1", (xx-dc, yy+dc), (xx+dc, yy-dc), width=4, color=(200, 100, 100)) + self.line("t1", (xx-dc, yy-dc), (xx+dc, yy+dc), width=4, color=(200, 100, 100)) + + + def update(self, x): + self.x = x diff --git a/irlc/project2/yoda.py b/irlc/project2/yoda.py new file mode 100644 index 0000000000000000000000000000000000000000..dfb70a45d25ceb150827269a4abf0625aab29245 --- /dev/null +++ b/irlc/project2/yoda.py @@ -0,0 +1,97 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +from scipy.linalg import expm # Computes the matrix exponential e^A for a square matrix A +from numpy.linalg import matrix_power # Computes A^n for matrix A and integer n + + +def get_A_B(g : float, L: float, m=0.1): + r""" Compute the two matrices A, B (see Problem 1) here and return them. + The matrices should be numpy ndarrays. """ + # TODO: 2 lines missing. + raise NotImplementedError("Compute numpy matrices A and B here") + return A, B + + +def A_euler(g : float,L : float, Delta : float) -> np.ndarray: + r""" Compute \tilde{A}_0 (Euler discretization), see Problem 2. + + Hints: + * get_A_B can perhaps save you a line or two. + """ + # TODO: 2 lines missing. + raise NotImplementedError("Implement function body") + return A0_tilde + +def A_ei(g : float,L : float, Delta : float) -> np.ndarray: + r""" Compute A_0 (Exponential discretization), see Problem 2 + + Hints: + * The special function expm(X) computes the matrix exponential e^X. See the lecture notes for more information. + """ + # TODO: 2 lines missing. + raise NotImplementedError("Implement function body") + return A0 + +def M_euler(g : float, L : float, Delta : float, N : int) -> np.ndarray: + r""" Compute \tilde{M} (Euler discretization), see Problem 3 + Hints: + * the matrix_power(X,n) function can compute expressions such as X^n where X is a square matrix and n is a number + """ + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + return M_tilde + +def M_ei(g : float,L : float, Delta : float, N : int) -> np.ndarray: + r""" Compute M (Exponential discretization), see Problem 3 """ + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + return M + +def xN_bound_euler(g : float, L : float,Delta : float,N : int) -> float: + r""" Compute upper bound on |x_N| when using Euler discretization, see Problem 6. + The function should just return a number. + + Hints: + * This function uses all input arguments. + """ + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + return bound + +def xN_bound_ei(g: float,L : float,Delta : float,N : int) -> float: + r""" Compute upper bound on |x_N| when using exponential discretization, see Problem 7. + + Hints: + * This function does NOT use all input arguments. + * This will be the hardest problem to solve, but the easiest function to implement. + """ + # TODO: 1 lines missing. + raise NotImplementedError("Implement function body") + return bound + +if __name__ == '__main__': + g = 9.82 # gravitational constant + L = 5 # Length of string + m = 0.1 # Mass of pendulum (in kg) + Delta = 0.3 # Time-discretization constant Delta (in seconds) + N = 100 # Time steps + + # Solve Problem 2 + print("A0_euler") + print(A_euler(g, L, Delta)) + + print("A0_ei") + print(A_ei(g, L, Delta)) + + # Solve Problem 3 + print("M_euler") + print(M_euler(g, L, Delta, N)) + + print("M_ei") + print(M_ei(g, L, Delta, N)) + + # Solve Problem 7, upper bound on x_N using Euler discretization + print("|x_N| <= ", xN_bound_euler(g, L, Delta, N)) + + # Solve Problem 8, upper bound on x_N using Exponential discretization + print("|x_N| <= ", xN_bound_ei(g, L, Delta, N)) diff --git a/irlc/project3/Latex/02465project3_handin.tex b/irlc/project3/Latex/02465project3_handin.tex new file mode 100644 index 0000000000000000000000000000000000000000..b69b431236b95b067b2bb47f78dedcdc1c52d358 --- /dev/null +++ b/irlc/project3/Latex/02465project3_handin.tex @@ -0,0 +1,74 @@ +\documentclass[12pt,twoside]{article} +%\usepackage[table]{xcolor} % important to avoid options clash. +%\input{02465shared_preamble} +%\usepackage{cleveref} +\usepackage{url} +\usepackage{graphics} +\usepackage{multicol} +\usepackage{rotate} +\usepackage{rotating} +\usepackage{booktabs} +\usepackage{hyperref} +\usepackage{pifont} +\usepackage{latexsym} +\usepackage[english]{babel} +\usepackage{epstopdf} +\usepackage{etoolbox} +\usepackage{amsmath} +\usepackage{amssymb} +\usepackage{multirow,epstopdf} +\usepackage{fancyhdr} +\usepackage{booktabs} +\usepackage{xcolor} +\newcommand\redt[1]{ {\textcolor[rgb]{0.60, 0.00, 0.00}{\textbf{ #1} } } } + + +\newcommand{\m}[1]{\boldsymbol{ #1}} +\newcommand{\yoursolution}{ \redt{(your solution here) } } + + + +\title{ Report 3 hand-in } +\date{ \today } +\author{Alice (\texttt{s000001})\and Bob (\texttt{s000002})\and Clara (\texttt{s000003}) } + +\begin{document} +\maketitle + +\begin{table}[ht!] +\caption{Attribution table. Feel free to add/remove rows and columns} +\begin{tabular}{llll} +\toprule + & Alice & Bob & Clara \\ +\midrule + 1: Optimal policy & 0-100\% & 0-100\% & 0-100\% \\ + 2: Simulating a finite approximation of the optimal action-value function & 0-100\% & 0-100\% & 0-100\% \\ + 3: Analytically computing the optimal action-value function & 0-100\% & 0-100\% & 0-100\% \\ + 4: Extend solution to all states and actions & 0-100\% & 0-100\% & 0-100\% \\ + 5: UCB-based exploration & 0-100\% & 0-100\% & 0-100\% \\ + 6: Sarlacc rules & 0-100\% & 0-100\% & 0-100\% \\ + 7: Escape the Sarlacc & 0-100\% & 0-100\% & 0-100\% \\ +\bottomrule +\end{tabular} +\end{table} + +%\paragraph{Statement about collaboration:} +%Please edit this section to reflect how you have used external resources. The following statement will in most cases suffice: +%\emph{The code in the irls/project1 directory is entirely} + +%\paragraph{Main report:} +Headings have been inserted in the document for readability. You only have to edit the part which says \yoursolution. + +\section{Jar-Jar at the battle of Naboo (\texttt{jarjar.py})} +\subsubsection*{{\color{red}Problem 3: Analytically computing the optimal action-value function}} + + Using that ... we obtain + \begin{align} + Q^*(0,1) & = \cdots \\ + Q^*(1,-1) & = \cdots + \end{align} + therefore... + +\section{Finding the rebels using UCB-exploration (\texttt{rebels.py})} +\section{Individual contribution: The great sarlacc (\texttt{sarlacc.py})} +\end{document} \ No newline at end of file diff --git a/irlc/project3/Latex/figures/your_answer.pdf b/irlc/project3/Latex/figures/your_answer.pdf new file mode 100644 index 0000000000000000000000000000000000000000..d8c092974e20aaaf1165958a53bdce3a2ebdbf8f Binary files /dev/null and b/irlc/project3/Latex/figures/your_answer.pdf differ diff --git a/irlc/project3/__init__.py b/irlc/project3/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8794db4fc72b62ae50ebe61fd5ce31a77a77992e --- /dev/null +++ b/irlc/project3/__init__.py @@ -0,0 +1,2 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""This file is required for the test system but should otherwise be empty.""" diff --git a/irlc/project3/jarjar.py b/irlc/project3/jarjar.py new file mode 100644 index 0000000000000000000000000000000000000000..898d4b5246c1b7475ec3c85bd9da19f6b71e5f19 --- /dev/null +++ b/irlc/project3/jarjar.py @@ -0,0 +1,44 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import matplotlib.pyplot as plt +import numpy as np + + +def pi_optimal(s : int) -> int: + """ Compute the optimal policy for Jar-Jar binks. Don't overthink this one! """ + # TODO: 1 lines missing. + raise NotImplementedError("Return the optimal action in state s.") + return action + +def Q0_approximate(gamma : float, N : int) -> float: + """ Return the (estimate) of the optimal action-value function Q^*(0,1) based on + the first N rewards using a discount factor of gamma. Note the similarity to the n-step estimator. """ + # TODO: 1 lines missing. + raise NotImplementedError("Return N-term approximation of the optimal action-value function Q^*(0,1)") + return return_estimate + +def Q_exact(s : int,a : int, gamma : float) -> float: + """ + Return the exact optimal action-value function Q^*(s,a) in the Jar-Jar problem. + I recommend focusing on simple cases first, such as the two cases in the problem. + Then try to look at larger values of s (for instance, s=2), first using actions that 'point in the right direction' (a = -1) + and then actions that point in the 'wrong' direction a=1. + + There are several ways to solve the problem, but the simplest is probably to use recursions. + + *Don't* use your solution to Q0_approximate; it is an approximate (finite-horizon) approximation. + """ + # TODO: 6 lines missing. + raise NotImplementedError("return optimal action-value function Q^*(s,a) as a float.") + + +if __name__ == "__main__": + gamma = 0.8 + + ss = np.asarray(range(-10, 10)) + # Make a plot of your (exact) action-value function Q(s,-1) and Q(s,1). + plt.plot(ss, [Q_exact(s, -1, gamma) for s in ss], 'k-', label='Exact, a=-1') + plt.plot(ss, [Q_exact(s, 1, gamma) for s in ss], 'r-', label='Exact, a=1') + plt.legend() + plt.grid() + plt.show() + print("All done") diff --git a/irlc/project3/project3_grade.py b/irlc/project3/project3_grade.py new file mode 100644 index 0000000000000000000000000000000000000000..46e8b69135822b4f49e77d24b1cd11d2cd4bd0d0 --- /dev/null +++ b/irlc/project3/project3_grade.py @@ -0,0 +1,4 @@ +# irlc/project3/project3_tests.py +''' WARNING: Modifying, decompiling or otherwise tampering with this script, it's data or the resulting .token file will be investigated as a cheating attempt. ''' +import bz2, base64 +exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file diff --git a/irlc/project3/project3_tests.py b/irlc/project3/project3_tests.py new file mode 100644 index 0000000000000000000000000000000000000000..a50927ecf35be0d1210892c9b0e2f6a116127f0b --- /dev/null +++ b/irlc/project3/project3_tests.py @@ -0,0 +1,142 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from unitgrade import UTestCase, Report +import irlc + +class JarJarPiOptimal(UTestCase): + """ Problem 1: Compute optimal policy. """ + def test_pi_1(self): + from irlc.project3.jarjar import pi_optimal + self.assertLinf(pi_optimal(1), -1) + + def test_pi_all(self): + from irlc.project3.jarjar import pi_optimal + for s in range(-10, 10): + if s != 0: + self.assertLinf(pi_optimal(s)) + +class JarJarQ0Estimated(UTestCase): + """ Problem 2: Implement Q0_approximate to (approximate) the Q-function for the optimal policy. """ + def test_Q0_N1(self): + from irlc.project3.jarjar import Q0_approximate + import numpy as np + self.assertLinf(np.abs(Q0_approximate(gamma=0.8, N=1))) # TODO: Remove abs. This was added due to typo. + + def test_Q0_N2(self): + from irlc.project3.jarjar import Q0_approximate + import numpy as np + self.assertLinf(np.abs(Q0_approximate(gamma=0.7, N=20))) # TODO: Remove abs. This was added due to typo. + + def test_Q0_N100(self): + from irlc.project3.jarjar import Q0_approximate + import numpy as np + self.assertLinf(np.abs(Q0_approximate(gamma=0.9, N=20))) # TODO: Remove abs. This was added due to typo. + + +class JarJarQExact(UTestCase): + """ Problem 4: Compute Q^*(s,a) exactly by extending analytical solution. """ + def test_Q_s0(self): + from irlc.project3.jarjar import Q_exact + self.assertLinf(Q_exact(0, gamma=0.8, a=1)) + self.assertLinf(Q_exact(0, gamma=0.8, a=-1)) + + def test_Q_s1(self): + from irlc.project3.jarjar import Q_exact + self.assertLinf(Q_exact(1, gamma=0.8, a=-1)) + self.assertLinf(Q_exact(1, gamma=0.95, a=-1)) + self.assertLinf(Q_exact(1, gamma=0.7, a=-1)) + + def test_Q_s_positive(self): + from irlc.project3.jarjar import Q_exact + for s in range(20): + self.assertLinf(Q_exact(s, gamma=0.75, a=-1)) + + def test_Q_all(self): + from irlc.project3.jarjar import Q_exact + for s in range(-20, 20): + self.assertLinf(Q_exact(s, gamma=0.75, a=-1)) + self.assertLinf(Q_exact(s, gamma=0.75, a=1)) + +class RebelsSimple(UTestCase): + """ Problem 5: Test the UCB-algorithm in the basic-environment with a single state """ + def test_simple_four_episodes(self): + """ Test the first four episodes in the simple grid problem. """ + from irlc.project3.rebels import get_ucb_actions, very_basic_grid + actions = get_ucb_actions(very_basic_grid, alpha=0.1, episodes=4, c=5, plot=False) + # Make sure we only have 4 actions (remember to truncate the action-sequences!) + self.assertEqual(len(actions), 4) # Check the number of actions are correct + self.assertEqual(actions[0], 0) # Check the first action is correct + self.assertEqualC(actions) # Check all actions. + + def test_simple_nine_episodes(self): + """ Test the first nine episodes in the simple grid problem. """ + from irlc.project3.rebels import get_ucb_actions, very_basic_grid + actions = get_ucb_actions(very_basic_grid, alpha=0.1, episodes=9, c=5, plot=False) + self.assertEqual(len(actions), 9) # Check the number of actions are correct + self.assertEqual(actions[0], 0) # Check the first action is correct + self.assertEqualC(actions) # Check all actions. + + def test_simple_environment(self): + from irlc.project3.rebels import get_ucb_actions, very_basic_grid + actions = get_ucb_actions(very_basic_grid, alpha=0.1, episodes=100, c=5, plot=False) + # Check the number of actions are correct + self.assertEqualC(len(actions)) + # Check the first action is correct + self.assertEqualC(actions[0]) + # Check all actions. + self.assertEqualC(actions) + + def test_bridge_environment(self): + from irlc.gridworld.gridworld_environments import grid_bridge_grid + from irlc.project3.rebels import get_ucb_actions, very_basic_grid + actions = get_ucb_actions(grid_bridge_grid, alpha=0.1, episodes=1000, c=2, plot=False) + self.assertEqualC(len(actions)) + # Check all actions. + self.assertEqualC(actions) + +class RebelsBridge(UTestCase): + """ Problem 5: Test the UCB-algorithm in the bridge-environment """ + def test_bridge_environment_one(self): + from irlc.gridworld.gridworld_environments import grid_bridge_grid + from irlc.project3.rebels import get_ucb_actions + actions = get_ucb_actions(grid_bridge_grid, alpha=0.1, episodes=1, c=2, plot=False) + self.assertEqualC(len(actions)) + self.assertEqualC(actions) + + def test_bridge_environment_two(self): + from irlc.gridworld.gridworld_environments import grid_bridge_grid + from irlc.project3.rebels import get_ucb_actions + actions = get_ucb_actions(grid_bridge_grid, alpha=0.1, episodes=2, c=2, plot=False) + self.assertEqualC(len(actions)) + self.assertEqualC(actions) + + def test_bridge_environment_short(self): + from irlc.gridworld.gridworld_environments import grid_bridge_grid + from irlc.project3.rebels import get_ucb_actions + actions = get_ucb_actions(grid_bridge_grid, alpha=0.1, episodes=30, c=2, plot=False) + self.assertEqualC(len(actions)) + self.assertEqualC(actions) + + def test_bridge_environment_long(self): + from irlc.gridworld.gridworld_environments import grid_bridge_grid + from irlc.project3.rebels import get_ucb_actions + actions = get_ucb_actions(grid_bridge_grid, alpha=0.1, episodes=1000, c=2, plot=False) + self.assertEqualC(len(actions)) + self.assertEqualC(actions) + +class Project3(Report): + title = "Project part 3: Reinforcement Learning" + pack_imports = [irlc] + + jarjar1 = [(JarJarPiOptimal, 10), + (JarJarQ0Estimated, 10), + (JarJarQExact, 10) ] + + rebels = [(RebelsSimple, 20), + (RebelsBridge, 20) ] + questions = [] + questions += jarjar1 + questions += rebels + +if __name__ == '__main__': + from unitgrade import evaluate_report_student + evaluate_report_student(Project3()) diff --git a/irlc/project3/project3_tests_complete_grade.py b/irlc/project3/project3_tests_complete_grade.py new file mode 100644 index 0000000000000000000000000000000000000000..17fda1138f74db71d0116934800cda3a05058c15 --- /dev/null +++ b/irlc/project3/project3_tests_complete_grade.py @@ -0,0 +1,4 @@ +# irlc/project3/project3_tests_complete.py +''' WARNING: Modifying, decompiling or otherwise tampering with this script, it's data or the resulting .token file will be investigated as a cheating attempt. ''' +import bz2, base64 +exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file diff --git a/irlc/project3/rebels.py b/irlc/project3/rebels.py new file mode 100644 index 0000000000000000000000000000000000000000..951a543d5327a1ae5202c14386498aaad6e47af2 --- /dev/null +++ b/irlc/project3/rebels.py @@ -0,0 +1,58 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +from irlc.ex11.q_agent import QAgent +from irlc.gridworld.gridworld_environments import GridworldEnvironment, grid_bridge_grid +from irlc import train +from irlc.ex09.rl_agent import TabularQ + +# A simple UCB action-selection problem (basic problem) +very_basic_grid = [['#',1, '#'], + [1, 'S', 2], + ['#',1, '#']] + + +# TODO: 21 lines missing. +raise NotImplementedError("I wrote an agent that inherited from the Q-agent, and updated the self.pi and self.train-functions to do UCB-based exploration.") + +def get_ucb_actions(layout : list, alpha : float, c : float, episodes : int, plot=False) -> list: + """ Return the sequence of actions the agent tries in the environment with the given layout-string when trained over 'episodes' episodes. + To create an environment, you can use the line: + + > env = GridworldEnvironment(layout) + + See also the demo-file. + + The 'plot'-parameter is optional; you can use it to add visualization using a line such as: + + if plot: + env = GridworldEnvironment(layout, render_mode='human') + + Or you can just ignore it. Make sure to return the truncated action list (see the rebels_demo.py-file or project description). + In other words, the return value should be a long list of integers corresponding to actions: + actions = [0, 1, 2, ..., 1, 3, 2, 1, 0, ...] + """ + # TODO: 6 lines missing. + raise NotImplementedError("Implement function body") + return actions + +if __name__ == "__main__": + actions = get_ucb_actions(very_basic_grid, alpha=0.1, c=5, episodes=4, plot=False) + print("Number of actions taken", len(actions)) + print("List of actions taken over 4 episodes", actions) + + actions = get_ucb_actions(very_basic_grid, alpha=0.1, c=5, episodes=8, plot=False) + print("Number of actions taken", len(actions)) + print("Actions taken over 8 episodes", actions) + + actions = get_ucb_actions(very_basic_grid, alpha=0.1, c=5, episodes=9, plot=False) + print("Number of actions taken", len(actions)) + print("Actions taken over 9 episodes", actions) # In this particular case, you can also predict the 9th action. Why? + + # Simulate 100 episodes. This should solve the problem. + actions = get_ucb_actions(very_basic_grid, alpha=0.1, c=5, episodes=100, plot=False) + print("Basic: Actions taken over 100 episodes", actions) + + # Simulate 100 episodes for the bridge-environment. The UCB-based method should solve the environment without being overly sensitive to c. + # You can compare your result with the Q-learning agent in the demo, which performs horribly. + actions = get_ucb_actions(grid_bridge_grid, alpha=0.1, c=5, episodes=300, plot=False) + print("Bridge: Actions taken over 300 episodes. The agent should solve the environment:", actions) diff --git a/irlc/project3/rebels_demo.py b/irlc/project3/rebels_demo.py new file mode 100644 index 0000000000000000000000000000000000000000..923c69fffbb2badb27fe581c4638516bc953577a --- /dev/null +++ b/irlc/project3/rebels_demo.py @@ -0,0 +1,50 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import numpy as np +from irlc import train, Agent, interactive, savepdf +from irlc.gridworld.gridworld_environments import GridworldEnvironment, grid_bridge_grid +from irlc.project3.rebels import very_basic_grid +from irlc.ex11.q_agent import QAgent +import matplotlib +import matplotlib.pyplot as plt +matplotlib.use('qtagg') + + +if __name__ == "__main__": + np.random.seed(42) # Fix the seed for reproduciability + env = GridworldEnvironment(very_basic_grid, render_mode='human') # Create an environment + env.reset() # Reset (to set up the visualization) + savepdf("rebels_basic", env=env) # Save a snapshot of the starting state + env.close() + + # Create an interactive version. + env = GridworldEnvironment(very_basic_grid, render_mode='human') # Create an environment + agent = QAgent(env) # This agent will display the Q-values. + # agent = Agent(env) # A random agent. + # env, agent = interactive(env, agent) # Uncomment this line to play in 'env' environment. Use space to let the agent move. + stats, trajectories = train(env, agent, num_episodes=16, return_trajectory=True) + env.close() + print("Trajectory 0: States traversed", trajectories[0].state, "actions taken", trajectories[0].action) + print("Trajectory 1: States traversed", trajectories[1].state, "actions taken", trajectories[1].action) + all_actions = [t.action[:-1] for t in trajectories] # Concatenate all action sequence excluding the last dummy-action. + print("All actions taken in 16 episodes, excluding the terminal (dummy) action", all_actions) + # Note the last list is of length 20 -- this is because the environment will always terminate after two actions, + # and since we discard the last (dummy) action we get 20 actions. + # In general, the list of actions will be longer, as only the last action should be discarded (as in the code above). + + # A more minimalistic example to plot the bridge-grid environment + bridge_env = GridworldEnvironment(grid_bridge_grid, render_mode='human') + bridge_env.reset() + savepdf("rebels_bridge", env=bridge_env) + bridge_env.close() + + # The following code will simulate a Q-learning agent for 3000 (!) episodes and plot the Q-functions. + np.random.seed(42) # Fix the seed for reproduciability + env = GridworldEnvironment(grid_bridge_grid) + agent = QAgent(env, alpha=0.1, epsilon=0.2, gamma=1) + """ Uncomment the next line to play in the environment. + Use the space-bar to let the agent take an action, p to unpause, and otherwise use the keyboard arrows """ + train(env, agent, num_episodes=3000) # Train for 3000 episodes. Surely the rebels must be found by now! + bridge_env, agent = interactive(env, agent) + bridge_env.reset() + bridge_env.savepdf("rebels_bridge_Q") + bridge_env.close() diff --git a/irlc/project3/unitgrade_data/JarJarPiOptimal.pkl b/irlc/project3/unitgrade_data/JarJarPiOptimal.pkl new file mode 100644 index 0000000000000000000000000000000000000000..efc6383731dea50b9985335b11ba3c5bb47cc889 Binary files /dev/null and b/irlc/project3/unitgrade_data/JarJarPiOptimal.pkl differ diff --git a/irlc/project3/unitgrade_data/JarJarQ0Estimated.pkl b/irlc/project3/unitgrade_data/JarJarQ0Estimated.pkl new file mode 100644 index 0000000000000000000000000000000000000000..36e3c87cea22839893c43b95665bde11a166e771 Binary files /dev/null and b/irlc/project3/unitgrade_data/JarJarQ0Estimated.pkl differ diff --git a/irlc/project3/unitgrade_data/JarJarQExact.pkl b/irlc/project3/unitgrade_data/JarJarQExact.pkl new file mode 100644 index 0000000000000000000000000000000000000000..0b7858760854f8e7e8dff5061a15a650a295130c Binary files /dev/null and b/irlc/project3/unitgrade_data/JarJarQExact.pkl differ diff --git a/irlc/project3/unitgrade_data/RebelsBridge.pkl b/irlc/project3/unitgrade_data/RebelsBridge.pkl new file mode 100644 index 0000000000000000000000000000000000000000..06affd8ab0a70532880f475bd1455f6f96f5da5b Binary files /dev/null and b/irlc/project3/unitgrade_data/RebelsBridge.pkl differ diff --git a/irlc/project3/unitgrade_data/RebelsSimple.pkl b/irlc/project3/unitgrade_data/RebelsSimple.pkl new file mode 100644 index 0000000000000000000000000000000000000000..3ca65445fc193f3aca2915c4959db9eb9afacd79 Binary files /dev/null and b/irlc/project3/unitgrade_data/RebelsSimple.pkl differ diff --git a/irlc/project3i/__init__.py b/irlc/project3i/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8794db4fc72b62ae50ebe61fd5ce31a77a77992e --- /dev/null +++ b/irlc/project3i/__init__.py @@ -0,0 +1,2 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +"""This file is required for the test system but should otherwise be empty.""" diff --git a/irlc/project3i/project3_individual_grade.py b/irlc/project3i/project3_individual_grade.py new file mode 100644 index 0000000000000000000000000000000000000000..f88cec94b4d102c20325699cc548f644e155011a --- /dev/null +++ b/irlc/project3i/project3_individual_grade.py @@ -0,0 +1,4 @@ +# irlc/project3i/project3_individual_tests.py +''' WARNING: Modifying, decompiling or otherwise tampering with this script, it's data or the resulting .token file will be investigated as a cheating attempt. ''' +import bz2, base64 +exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file diff --git a/irlc/project3i/project3_individual_tests.py b/irlc/project3i/project3_individual_tests.py new file mode 100644 index 0000000000000000000000000000000000000000..3bb6f833084908d9e4a03b8d7b90ddad150e5e2d --- /dev/null +++ b/irlc/project3i/project3_individual_tests.py @@ -0,0 +1,61 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from unitgrade import UTestCase, Report +import irlc +import numpy as np + +class SarlaccGameRules(UTestCase): + def check_rules(self, rules): + from irlc.project3i.sarlacc import game_rules + # Test what happens at the starting square s=0 for roll 1 + self.assertEqualC(game_rules(rules, state=0, roll=1)) + # Test what happens at the starting square s=0 for other rolls + for roll in [2, 3, 4, 5, 6]: + self.assertEqualC(game_rules(rules, state=0, roll=roll)) + + # Test all states: + for s in range(max(rules.keys())): + if s not in rules: # We skip because s is not a legal state to be in. + for roll in [1, 2, 3, 4, 5, 6]: + self.assertEqualC(game_rules(rules, s, roll)) + + def test_empty_board_rules(self): + rules = {55: -1} + self.check_rules(rules) + + def test_rules(self): + from irlc.project3i.sarlacc import rules + self.check_rules(rules) + +class SarlacReturn(UTestCase): + def check_return(self, rules, gamma): + from irlc.project3i.sarlacc import sarlacc_return + v = sarlacc_return(rules, gamma) + # Check that the keys (states) that are included in v are correct. I.e., that the return is computed for the right states. + states = list(sorted(v.keys())) + self.assertEqualC(states) + + for s in states: + self.assertL2(v[s], tol=1e-2) + + def test_sarlacc_return_empty_gamma1(self): + self.check_return({55: -1}, gamma=1) + + def test_sarlacc_return(self): + from irlc.project3i.sarlacc import rules + self.check_return(rules, gamma=.8) + + +class Project3Individual(Report): + title = "Project part 3: Reinforcement Learning (individual)" + pack_imports = [irlc] + + sarlacc = [(SarlaccGameRules, 20), + (SarlacReturn, 20)] + + questions = [] + questions += sarlacc + + +if __name__ == '__main__': + from unitgrade import evaluate_report_student + evaluate_report_student(Project3Individual()) diff --git a/irlc/project3i/project3_individual_tests_complete_grade.py b/irlc/project3i/project3_individual_tests_complete_grade.py new file mode 100644 index 0000000000000000000000000000000000000000..8cfcfa743853c5ae06a0d381b9a33d5c0262b9a2 --- /dev/null +++ b/irlc/project3i/project3_individual_tests_complete_grade.py @@ -0,0 +1,4 @@ +# irlc/project3i/project3_individual_tests_complete.py +''' WARNING: Modifying, decompiling or otherwise tampering with this script, it's data or the resulting .token file will be investigated as a cheating attempt. ''' +import bz2, base64 +exec(bz2.decompress(base64.b64decode('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'))) \ No newline at end of file diff --git a/irlc/project3i/sarlacc.py b/irlc/project3i/sarlacc.py new file mode 100644 index 0000000000000000000000000000000000000000..55e2463837c91ddbc65a07f32a514e1c61eeddb3 --- /dev/null +++ b/irlc/project3i/sarlacc.py @@ -0,0 +1,120 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" + +References: + [SB18] Richard S. Sutton and Andrew G. Barto. Reinforcement Learning: An Introduction. The MIT Press, second edition, 2018. (Freely available online). +""" +from irlc import savepdf +from irlc.ex09.mdp import MDP +from irlc.ex09.value_iteration import value_iteration +import matplotlib.pyplot as plt +import numpy as np + +# These are the game rules of the sarlac: If you land on a state s in the dictionary, you are teleported to rules[s]. +rules = { + 2: 16, + 4: 8, + 7: 21, + 10: 3, + 12: 25, + 14: 1, + 17: 27, + 19: 5, + 22: 3, + 23: 32, + 24: 44, + 26: 44, + 28: 38, + 30: 18, + 33: 48, + 35: 11, + 36: 34, + 40: 53, + 41: 29, + 42: 9, + 45: 51, + 47: 31, + 50: 25, + 52: 38, + 55: -1, + } + +def game_rules(rules : dict, state : int, roll : int) -> int: + """ Compute the next state given the game rules in 'rules', the current state 'state', and the roll + which can be roll = 1, 2, 3, 4, 5, 6. + The output should be -1 in case the game terminates, and otherwise the function should return the next state + as an integer. Read the description of the project for examples on the rules. """ + # TODO: 4 lines missing. + raise NotImplementedError("Return the next state") + return state_next + +# TODO: 19 lines missing. +raise NotImplementedError("Put your code here.") + +def sarlacc_return(rules : dict, gamma : float) -> dict: + """ Compute the value-function using a discount of gamma and the game rules 'rules'. + Result should be reasonable accurate. + + The value you return should be a dictionary v, so that v[state] is the value function in that state. + (i.e., the standard output format of the value_iteration function). + + Hints: + * One way to solve this problem is to create a MDP-class (see for instance the Gambler-problem in week 9) + and use the value_iteration function from week 9 to solve the problem. But I don't think the problem + is much harder to solve by just writing your own value-iteration method as in (SB18). + """ + # TODO: 2 lines missing. + raise NotImplementedError("Return the value function") + return v + + +if __name__ == "__main__": + """ + Rules for the snakes and ladder game: + The player starts in square s=0, and the game terminates when the player is in square s = 55. + When a player reaches the base of a ladder he/she climbs it, and when they reach a snakes mouth of a snake they are translated to the base. + When a player overshoots the goal state they go backwards from the goal state by the amount of moves they overshoot with. + + A few examples (using the rules in the 'rules' dictionary in this file): + If the player is in position s=0 (start) + > roll 2: Go to state s=16 (using the ladder) + > roll 3: Go to state s=3. + + Or if the player is in state s=54 + > Roll 1: Win the game + > Roll 2: stay in 54 + > Roll 3: Go to 53 + > Roll 4: Go to 38 + """ + # Test the game rules: + for roll in [1, 2, 3, 4, 5, 6]: + print(f"In state s=0 (start), using roll {roll}, I ended up in ", game_rules(rules, 0, roll)) + # Test the game rules again: + for roll in [1, 2, 3, 4, 5, 6]: + print(f"In state s=54, using roll {roll}, I ended up in ", game_rules(rules, 54, roll)) + + # Compute value function with the ordinary rules. + V_rules = sarlacc_return(rules, gamma=1) + # Compute value function with no rules, i.e. with an empty dictionary except for the winning state: + V_norule = sarlacc_return({55: -1}, gamma=1) + print("Time to victory when there are no snakes/ladders", V_norule[0]) + print("Time to victory when there are snakes/ladders", V_rules[0]) + + # Make a plot of the value-functions (optional). + width = .4 + def v2bar(V): + k, x = zip(*V.items()) + return np.asarray(k), np.asarray(x) + + plt.figure(figsize=(10,5)) + plt.grid() + k,x = v2bar(V_norule) + plt.bar(k-width/2, x, width=width, label="No rules") + + k, x = v2bar(V_rules) + plt.bar(k + width / 2, x, width=width, label="Rules") + plt.legend() + plt.xlabel("Current tile") + plt.ylabel("Moves remaining") + savepdf('sarlacc_value_function') + plt.show() diff --git a/irlc/project3i/unitgrade_data/SarlacReturn.pkl b/irlc/project3i/unitgrade_data/SarlacReturn.pkl new file mode 100644 index 0000000000000000000000000000000000000000..3ead9ae290d76fb309b7f682728dac309b6606f0 Binary files /dev/null and b/irlc/project3i/unitgrade_data/SarlacReturn.pkl differ diff --git a/irlc/project3i/unitgrade_data/SarlaccGameRules.pkl b/irlc/project3i/unitgrade_data/SarlaccGameRules.pkl new file mode 100644 index 0000000000000000000000000000000000000000..da00e5ccb061289943675d09fd862f10675071ab Binary files /dev/null and b/irlc/project3i/unitgrade_data/SarlaccGameRules.pkl differ diff --git a/irlc/tests/__init__.py b/irlc/tests/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/tests/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/tests/tests_week01.py b/irlc/tests/tests_week01.py new file mode 100644 index 0000000000000000000000000000000000000000..812c8fa77f27109db9e9e46f821a97c43085a08f --- /dev/null +++ b/irlc/tests/tests_week01.py @@ -0,0 +1,132 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from unitgrade import Report +import irlc +# from irlc.ex01.frozen_lake import FrozenAgentDownRight +import gymnasium as gym +from unitgrade import UTestCase +from irlc.ex01.inventory_environment import InventoryEnvironment, simplified_train, RandomAgent +from unitgrade import Capturing2 +import numpy as np +from gymnasium.envs.toy_text.frozen_lake import RIGHT, DOWN # The down and right-actions; may be relevant. +from irlc.ex01.pacman_hardcoded import GoAroundAgent, layout +from irlc.pacman.pacman_environment import PacmanEnvironment +from irlc import Agent, train +from irlc.ex01.bobs_friend import BobFriendEnvironment, AlwaysAction_u1, AlwaysAction_u0 + + +class Problem1BobsFriend(UTestCase): + def test_a_env_basic(self): + env = BobFriendEnvironment() + s0, _ = env.reset() + self.assertEqual(s0, 20, msg="Reset must return the initial state, i.e. the amount of money we start out with") + + def test_a_env_u0(self): + env = BobFriendEnvironment() + env.reset() + s1, r, done, _, _ = env.step(0) + self.assertEqual(r, 2, msg="When taking action u0, we must get a reward of 2.") + self.assertEqual(s1, 22, msg="When taking action u0, we must end in state x1=22") + self.assertEqual(done, True, msg="After taking an action, the environment must terminate") + +class Problem2BobsPolicy(UTestCase): + def test_a_env_u1(self): + env = BobFriendEnvironment() + env.reset() + s1, r, done, _, _ = env.step(1) + print(r) + self.assertTrue(r == 12 or r == -20, msg="When taking action u1, we must get a reward of 0 or 12.") + self.assertTrue(s1 == 0 or s1 == 32, msg="When taking action u1, we must end in state x1=0 or x1 = 34") + self.assertEqual(done, True, msg="After taking an action, the environment must terminate") + + def test_b_always_action_u0(self): + env = BobFriendEnvironment() + stats, _ = train(env, AlwaysAction_u0(env), num_episodes=1000) + avg = np.mean( [stat['Accumulated Reward'] for stat in stats] ) + self.assertL2(avg, 2, msg="Average reward when we always take action u=0 must be 2.") + + def test_b_always_action_u1(self): + env = BobFriendEnvironment() + stats, _ = train(env, AlwaysAction_u1(env), num_episodes=10000) + avg = np.mean( [stat['Accumulated Reward'] for stat in stats] ) + self.assertL2(avg, 4, tol=0.5, msg="Average reward when we always take action u=0 must be about 4.") + + def test_b_always_action_u1_starting_200(self): + env = BobFriendEnvironment(x0=200) + stats, _ = train(env, AlwaysAction_u1(env), num_episodes=10000) + avg = np.mean( [stat['Accumulated Reward'] for stat in stats] ) + self.assertL2(avg, -42, tol=4, msg="Average reward when we always take action u=0 must be about 4.") + + def test_b_always_action_u0_starting_200(self): + env = BobFriendEnvironment(x0=200) + stats, _ = train(env, AlwaysAction_u0(env), num_episodes=10000) + avg = np.mean( [stat['Accumulated Reward'] for stat in stats] ) + self.assertL2(avg, 20, msg="Average reward when we always take action u=0 must be about 4.") + + + +class Problem5PacmanHardcoded(UTestCase): + """ Test the hardcoded pacman agent """ + def test_pacman(self): + env = PacmanEnvironment(layout_str=layout) + agent = GoAroundAgent(env) + stats, _ = train(env, agent, num_episodes=1) + self.assertEqual(stats[0]['Length'] < 100, True) + + +class Problem6ChessTournament(UTestCase): + def test_chess(self): + """ Test the correct result in the little chess-tournament """ + from irlc.ex01.chess import main + with Capturing2() as c: + main() + # Extract the numbers from the console output. + print("Numbers extracted from console output was") + print(c.numbers) + self.assertLinf(c.numbers[-2], 26/33, tol=0.05) + +class Problem3InventoryInventoryEnvironment(UTestCase): + def test_environment(self): + env = InventoryEnvironment() + # agent = RandomAgent(env) + stats, _ = train(env, Agent(env), num_episodes=2000, verbose=False) + avg_reward = np.mean([stat['Accumulated Reward'] for stat in stats]) + self.assertLinf(avg_reward, tol=0.6) + + def test_random_agent(self): + env = InventoryEnvironment() + stats, _ = train(env, RandomAgent(env), num_episodes=2000, verbose=False) + avg_reward = np.mean([stat['Accumulated Reward'] for stat in stats]) + self.assertLinf(avg_reward, tol=0.6) + +class Problem4InventoryTrain(UTestCase): + def test_simplified_train(self): + env = InventoryEnvironment() + agent = Agent(env) + avg_reward_simplified_train = np.mean([simplified_train(env, agent) for i in range(1000)]) + self.assertLinf(avg_reward_simplified_train, tol=0.5) + +# class FrozenLakeTest(UTestCase): +# def test_frozen_lake(self): +# env = gym.make("FrozenLake-v1") +# agent = FrozenAgentDownRight(env) +# s = env.reset() +# for k in range(10): +# self.assertEqual(agent.pi(s, k), DOWN if k % 2 == 0 else RIGHT) + + +class Week01Tests(Report): #240 total. + title = "Tests for week 01" + pack_imports = [irlc] + individual_imports = [] + questions = [ + (Problem1BobsFriend, 10), + (Problem2BobsPolicy, 10), + (Problem3InventoryInventoryEnvironment, 10), + (Problem4InventoryTrain, 10), + (Problem5PacmanHardcoded, 10), + (Problem6ChessTournament, 10), # Week 1: Everything + ] + +if __name__ == '__main__': + from unitgrade import evaluate_report_student + evaluate_report_student(Week01Tests()) diff --git a/irlc/tests/tests_week02.py b/irlc/tests/tests_week02.py new file mode 100644 index 0000000000000000000000000000000000000000..f8c474c657d24ff73716e5ff1d124808a4263c5e --- /dev/null +++ b/irlc/tests/tests_week02.py @@ -0,0 +1,270 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# from irlc.ex02.graph_traversal import pi_inc, pi_smart, pi_silly, policy_rollout, SmallGraphDP +from irlc.ex02.graph_traversal import SmallGraphDP +from collections import defaultdict +# from irlc.ex02.chessmatch import ChessMatch +import gymnasium as gym +from unitgrade import Report +import irlc +from unitgrade import UTestCase +from irlc.ex02.inventory import InventoryDPModel, DP_stochastic +from irlc.ex02.graph_traversal import SmallGraphDP +# from irlc.ex02.frozen_lake_dp import Gym2DPModel + +def gN_dp(self, env): + for s in sorted(self.env.S(self.env.N)): + self.assertLinf(self.env.gN(s)) + +def f_dp(self, env): + self.assertEqualC(self.env.N) + + for k in range(self.env.N): + for s in sorted(self.env.S(k)): + for a in sorted(self.env.A(s,k)): + from collections import defaultdict + + dd_f = defaultdict(float) + # dd_g = defaultdict(float) + + for w, pw in self.env.Pw(s,a,k).items(): + dd_f[(s,a, self.env.f(s,a,w,k))] += pw + # dd_g[(s, a, self.env.g(s, a, w, k))] += pw + + # Check transition probabilities sum to 1. + self.assertAlmostEqual(sum(dd_f.values()), 1, places=6) + # self.assertAlmostEqual(sum(dd_g.values()), 1, places=6) + + for key in sorted(dd_f.keys()): + self.assertEqualC(key) + self.assertLinf(dd_f[key], tol=1e-7) + + # for key in sorted(dd_g.keys()): + # self.assertEqualC(key) + # self.assertLinf(dd_g[key], tol=1e-7) + +def g_dp(self, env): + for k in range(self.env.N): + for s in sorted(self.env.S(k)): + for a in sorted(self.env.A(s, k)): + + # dd_f = defaultdict(float) + dd_g = defaultdict(float) + + for w, pw in self.env.Pw(s, a, k).items(): + # dd_f[(s, a, self.env.f(s, a, w, k))] += pw + dd_g[(s, a, self.env.g(s, a, w, k))] += pw + + # Check transition probabilities sum to 1. + # self.assertAlmostEqual(sum(dd_f.values()), 1, places=6) + self.assertAlmostEqual(sum(dd_g.values()), 1, places=6) + + # for key in sorted(dd_f.keys()): + # self.assertEqualC(key) + # self.assertLinf(dd_f[key], tol=1e-7) + + for key in sorted(dd_g.keys()): + self.assertEqualC(key) + self.assertLinf(dd_g[key], tol=1e-7) + + +class Problem1SmallGraph(UTestCase): + @property + def env(self): + return SmallGraphDP(t=5) + + # @classmethod + # def setUpClass(cls) -> None: + # cls.env = SmallGraphDP(t=5) + + # def test_N(self): + # self.assertEqualC(self.__class__.env.N) + + # def test_states(self): + # # for k in range(self.class.model.S): + # # self.assertEqualC(len(cls.model.S)) + # # self.assertEqualC() + # for k in range(self.env.N+1): + # self.assertEqualC(set(self.env.S(k))) + # + # def test_actions(self): + # for k in range(self.env.N): + # for s in sorted(self.env.S(k)): + # self.assertEqualC(set(self.env.A(s, k))) + + def test_f(self): + f_dp(self, self.env) + + def test_g(self): + g_dp(self, self.env) + + + def test_gN(self): + gN_dp(self, self.env) + + # def test_states(self): + # for k in range(self.env.N+1): + # self.assertEqualC(set(self.env.S(k))) + # + # def test_actions(self): + # for k in range(self.env.N): + # for s in sorted(self.env.S(k)): + # self.assertEqualC(set(self.env.A(s, k))) + + + +class Problem3StochasticDP(UTestCase): + """ Inventory control """ + def test_policy(self): + inv = InventoryDPModel() + J, pi = DP_stochastic(inv) + + # Test action at time step N-1 + self.assertEqual(pi[-1][0], 1) + self.assertEqual(pi[-1][1], 0) + self.assertEqual(pi[-1][2], 0) + + # test all actions at time step N-1 + self.assertEqualC(pi[-1]) + + # Test all actions at all time steps + self.assertEqualC(pi) + + def test_J(self): + inv = InventoryDPModel() + J, pi = DP_stochastic(inv) + + self.assertLinf(J[-1][0], tol=1e-8) + self.assertLinf(J[-1][1], tol=1e-8) + self.assertLinf(J[-1][2], tol=1e-8) + + for k in range(len(J)): + for x in [0,1,2]: + print("testing", J[k][x]) + self.assertLinf(J[k][x], tol=1e-8) + +class Problem4DPAgent(UTestCase): + def test_agent(self): + from irlc.ex01.inventory_environment import InventoryEnvironment + from irlc.ex02.inventory import InventoryDPModel + from irlc.ex02.dp_agent import DynamicalProgrammingAgent + env = InventoryEnvironment(N=3) + inventory = InventoryDPModel(N=3) + agent = DynamicalProgrammingAgent(env, model=inventory) + s0, _ = env.reset() + self.assertEqualC(agent.pi(s0, 0)) # We just test the first action. + + +# class DPChessMatch(UTestCase): +# """ Chessmatch """ +# def test_J(self): +# N = 2 +# pw = 0.45 +# pd = 0.8 +# cm = ChessMatch(N, pw=pw, pd=pd) +# J, pi = DP_stochastic(cm) +# self.assertLinf(J[-1][0], tol=1e-4) +# self.assertLinf(J[-2][0], tol=1e-4) +# self.assertLinf(J[0][0], tol=1e-4) + + + + +# class SmallGraphPolicies(UTestCase): +# """ Test the policies in the small graph environment """ +# def test_pi_smart(self): +# self.assertEqual(pi_smart(1, 0), 5) +# +# def test_pi_inc(self): +# from irlc.ex02.graph_traversal import pi_inc, pi_smart, pi_silly +# for k in range(5): +# self.assertEqual(pi_inc(k+1, k), k+2) +# # self.assertEqual(pi_smart(k + 1, k), 5) +# # self.assertEqual(pi_smart(k + 1, k), 5) +# +# def test_rollout(self): +# # self.assertEqual(3, 1) +# t = 5 +# x0 = 1 # starting node +# model = SmallGraphDP(t=t) +# +# self.assertEqualC(policy_rollout(model, pi_silly, x0)[0]) +# self.assertEqualC(policy_rollout(model, pi_smart, x0)[0]) +# self.assertEqualC(policy_rollout(model, pi_inc, x0)[0]) + +class Problem2DeterministicDP(UTestCase): + def test_dp_deterministic(self): + model = SmallGraphDP(t=5) + J, pi = DP_stochastic(model) + + self.assertLinf(J[-1][1], tol=1e-5) + self.assertLinf(J[-1][2], tol=1e-5) + self.assertLinf(J[-1][3], tol=1e-5) + + self.assertLinf(J[0][1], tol=1e-5) + self.assertLinf(J[0][2], tol=1e-5) + self.assertLinf(J[0][3], tol=1e-5) + + +# class TestInventoryModel(UTestCase): +# @property +# def env(self): +# return InventoryDPModel() +# +# def test_gN(self): +# gN_dp(self, self.env) +# +# def test_f(self): +# f_dp(self, self.env) +# +# def test_g(self): +# g_dp(self, self.env) + + + +# class TestFrozenDP(UTestCase): +# @property +# def env(self): +# return Gym2DPModel(gym_env=gym.make("FrozenLake-v1")) +# +# def test_f(self): +# f_dp(self, self.env) +# +# def test_g(self): +# g_dp(self, self.env) + +class ExamQuestion7FlowersStore(UTestCase): + def test_a_get_policy(self): + from irlc.ex02.flower_store import a_get_policy + x0 = 0 + c = 0.5 + N = 3 + self.assertEqual(a_get_policy(N, c, x0), 1) + + def test_b_prob_empty(self): + from irlc.ex02.flower_store import b_prob_one + x0 = 0 + N = 3 + self.assertAlmostEqual(b_prob_one(N, x0), 0.492, places=2) + + +class Week02Tests(Report): + title = "Tests for week 02" + pack_imports = [irlc] + individual_imports = [] + questions = [ + (Problem1SmallGraph, 10), + (Problem2DeterministicDP, 10), + (Problem3StochasticDP, 10), + (Problem4DPAgent, 10), + (ExamQuestion7FlowersStore, 10), + ] + + +# (SmallGraphPolicies, 10), +# (TestInventoryModel, 10), +# (DPChessMatch, 10), +# (TestFrozenDP, 10), + +if __name__ == '__main__': + from unitgrade import evaluate_report_student + evaluate_report_student(Week02Tests() ) diff --git a/irlc/tests/tests_week03.py b/irlc/tests/tests_week03.py new file mode 100644 index 0000000000000000000000000000000000000000..403e29a53524dbc4250ec8b49a8dbd06bdc84e58 --- /dev/null +++ b/irlc/tests/tests_week03.py @@ -0,0 +1,88 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from unitgrade import Report +import irlc +from unitgrade import UTestCase +from irlc.ex03.kuramoto import KuramotoModel, f +import sympy as sym +import numpy as np + +class Problem1Kuramoto(UTestCase): + """ Test the Kuromoto Osscilator """ + def test_continious_model(self): + cmodel = KuramotoModel() + x, u = sym.symbols("x u") + expr = cmodel.sym_f([x], [u]) + # Check the expression has the right type. + self.assertIsInstance(expr, list) + # Evaluate the expression and check the result in a given point. + self.assertEqualC(expr[0].subs([(x, 0.2), (u, 0.93)])) + + def test_f(self): + self.assertLinf(f([0.1], [0.4]), tol=1e-6) + + + def test_RK4(self): + from irlc.ex03.kuramoto import rk4_simulate + + cmodel = KuramotoModel() + x0 = np.asarray(cmodel.x0_bound().low) # Get the starting state x=0. + u = 1.3 + xs, ts = rk4_simulate(x0, [u], t0=0, tF=20, N=100) + + # xs, us, ts = cmodel.simulate(x0, u_fun=u , t0=0, tF=20) + self.assertLinf(ts, tol=1e-6) + # self.assertLinf(us, tol=1e-6) + self.assertLinf(xs, tol=1e-6) + + # Test the same with a varying function: + xs, ts = rk4_simulate(x0, [u+1], t0=0, tF=10, N=50) + # xs, us, ts = cmodel.simulate(x0, u_fun=lambda x,t: np.sin(x + u) , t0=0, tF=10) + self.assertLinf(ts, tol=1e-6) + # self.assertLinf(us, tol=1e-6) + self.assertLinf(xs, tol=1e-6) + +class Exam5InventoryEvaluation(UTestCase): + def test_a_test_expected_items_next_day(self): + from irlc.ex03.inventory_evaluation import a_expected_items_next_day + self.assertAlmostEqual(a_expected_items_next_day(x=0, u=1), 0.1, places=5) + + def test_b_test_expected_items_next_day(self): + from irlc.ex03.inventory_evaluation import b_evaluate_policy + pi = self.get_pi() + self.assertAlmostEqual(b_evaluate_policy(pi, 1), 2.7, places=5) + + def get_pi(self): + from irlc.ex02.inventory import InventoryDPModel + model = InventoryDPModel() + pi = [{x: 1 if x == 0 else 0 for x in model.S(k)} for k in range(model.N)] + return pi + +class Exam6Toy2d(UTestCase): + def test_rk4_a(self): + from irlc.ex03.toy_2d_control import toy_simulation + w = toy_simulation(u0=0.4, T=5) + self.assertFalse(isinstance(w, np.ndarray), msg="Your toy_simulation function must return a float") + self.assertEqual(type(float(w)), float, msg="Your toy_simulation function must return a float") + self.assertLinf(w, tol=0.01, msg="Your simulation ended up at the wrong angle") + + def test_rk4_b(self): + from irlc.ex03.toy_2d_control import toy_simulation + w = toy_simulation(u0=-0.1, T=2) + self.assertFalse( isinstance(w, np.ndarray), msg="Your toy_simulation function must return a float") + self.assertEqual(type(float(w)), float, msg="Your toy_simulation function must return a float") + self.assertLinf(w, tol=0.01, msg="Your simulation ended up at the wrong angle") + + +class Week03Tests(Report): #240 total. + title = "Tests for week 03" + pack_imports = [irlc] + individual_imports = [] + questions = [ + (Problem1Kuramoto, 10), + (Exam5InventoryEvaluation, 10), + (Exam6Toy2d, 10), + ] + +if __name__ == '__main__': + from unitgrade import evaluate_report_student + evaluate_report_student(Week03Tests()) diff --git a/irlc/tests/tests_week04.py b/irlc/tests/tests_week04.py new file mode 100644 index 0000000000000000000000000000000000000000..b032c0bc49423607e2113a55d244181aa7763ec7 --- /dev/null +++ b/irlc/tests/tests_week04.py @@ -0,0 +1,131 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from unitgrade import Report +from unitgrade import UTestCase +import irlc +from irlc.car.car_model import CarEnvironment +from irlc.ex04.pid_car import PIDCarAgent +from irlc import train +from irlc.ex04.pid_locomotive_agent import LocomotiveEnvironment, PIDLocomotiveAgent +from irlc.ex03.kuramoto import KuramotoModel, f +from irlc.ex04.discrete_kuramoto import fk, dfk_dx +import sympy as sym +import numpy as np + +class Problem1DiscreteKuromoto(UTestCase): + """ Test the Kuromoto Osscilator """ + def test_continious_model(self): + cmodel = KuramotoModel() + x, u = sym.symbols("x u") + expr = cmodel.sym_f([x], [u]) + # Check the expression has the right type. + self.assertIsInstance(expr, list) + # Evaluate the expression and check the result in a given point. + self.assertEqualC(expr[0].subs([(x, 0.2), (u, 0.93)])) + + def test_f(self): + self.assertLinf(f([0.1], [0.4]), tol=1e-6) + + def test_fk(self): + self.assertLinf(fk([0.1], [0.4]), tol=1e-6) + + def test_dfk_dx(self): + self.assertLinf(dfk_dx([0.1], [0.4]), tol=1e-6) + +class Problem3PID(UTestCase): + """ PID Control """ + + def test_pid_class(self, Kp=40, Ki=0, Kd=0, target=0, x=0): + dt = 0.08 + from irlc.ex04.pid import PID + pid = PID(Kp=Kp, Kd=Kd, Ki=Ki, target=target, dt=0.8) + u = pid.pi(x) + self.assertL2(u, tol=1e-4) + + def test_pid_Kp(self): + self.test_pid_class(40, 0, 0, 0, 1) + self.test_pid_class(10, 0, 0, 0, 2) + + + def test_pid_target(self): + self.test_pid_class(40, 0, 0, 3, 1) + self.test_pid_class(20, 0, 0, 0, 2) + + + def test_pid_all(self): + self.test_pid_class(4, 3, 8, 1, 1) + self.test_pid_class(40, 10, 3, 0, 2) + + +class Problem4PIDAgent(UTestCase): + """ PID Control """ + + def pid_locomotive(self, Kp=40, Ki=0, Kd=0, slope=0, target=0): + dt = 0.08 + env = LocomotiveEnvironment(m=10, slope=slope, dt=dt, Tmax=5) + agent = PIDLocomotiveAgent(env, dt=dt, Kp=Kp, Ki=Ki, Kd=Kd, target=target) + stats, traj = train(env, agent, return_trajectory=True, verbose=False) + self.assertL2(traj[0].state, tol=1e-4) + + def test_locomotive_flat(self): + self.pid_locomotive() + + def test_locomotive_Kd(self): + """ Test the derivative term """ + self.pid_locomotive(Kd = 10) + + def test_locomotive_Ki(self): + """ Test the integral term """ + self.pid_locomotive(Kd = 10, Ki=5, slope=5) + + + def test_locomotive_all(self): + """ Test all terms """ + self.pid_locomotive(Kp=35, Kd = 10, Ki=5, slope=5, target=1) + + + + +class Problem7PIDCar(UTestCase): + lt = -1 + + @classmethod + def setUpClass(cls) -> None: + env = CarEnvironment(noise_scale=0, Tmax=80, max_laps=2) + agent = PIDCarAgent(env, v_target=1.0) + stats, trajectories = train(env, agent, num_episodes=1, return_trajectory=True) + d = trajectories[0].state[:, 4] + lt = len(d) * env.dt / 2 + print("Lap time", lt) + cls.lt = lt + + def test_below_60(self): + """ Testing if lap time is < 60 """ + self.assertTrue(0 < self.__class__.lt < 60) + + def test_below_40(self): + """ Testing if lap time is < 60 """ + self.assertTrue(0 < self.__class__.lt < 40) + + + def test_below_30(self): + """ Testing if lap time is < 60 """ + self.assertTrue(0 < self.__class__.lt < 30) + + def test_below_22(self): + """ Testing if lap time is < 22 """ + self.assertTrue(0 < self.__class__.lt < 22) + +class Week04Tests(Report): + title = "Tests for week 04" + pack_imports = [irlc] + individual_imports = [] + questions = [ + (Problem1DiscreteKuromoto, 10), + (Problem3PID, 10), + (Problem4PIDAgent, 10), # ok + (Problem7PIDCar, 10), # ok + ] + +if __name__ == '__main__': + from unitgrade import evaluate_report_student + evaluate_report_student(Week04Tests()) diff --git a/irlc/tests/tests_week05.py b/irlc/tests/tests_week05.py new file mode 100644 index 0000000000000000000000000000000000000000..4a7f813840b6670d6caa99c16576d2b90ff7572c --- /dev/null +++ b/irlc/tests/tests_week05.py @@ -0,0 +1,114 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from unitgrade import Report +from irlc.ex05.direct_agent import train_direct_agent +from unitgrade import UTestCase +import irlc +from irlc.ex05.direct import run_direct_small_problem + + +class DirectMethods(UTestCase): + title = "Direct methods z, z0, z_lb/z_ub definitions+" + + @classmethod + def setUpClass(cls) -> None: + env, solution = run_direct_small_problem() + cls.solution = solution[-1] + + def test_z_variable_vector(self): + self.assertEqualC(str(DirectMethods.solution['inputs']['z'])) + + def test_z0_initial_state(self): + self.assertL2(DirectMethods.solution['inputs']['z0'], tol=1e-6) + + def test_zU_upper_bound(self): + self.assertL2(DirectMethods.solution['inputs']['z_ub'], tol=1e-6) + + def test_zL_lower_bound(self): + self.assertL2(DirectMethods.solution['inputs']['z_lb'], tol=1e-6) + + +class DirectAgentPendulum(UTestCase): + """ Direct agent: Test of pendulum environment """ + def test_pendulum(self): + stats,_,_ = train_direct_agent(animate=False) + return self.assertL2(stats[0]['Accumulated Reward'], tol=0.03) + +class DirectSolverQuestion(UTestCase): + """ Test the Direct solver on the Pendulum using run_direct_small_problem() """ + @classmethod + def setUpClass(cls): + cls.solution = cls.compute_solution() + + @classmethod + def compute_solution(cls): + from irlc.ex05.direct import run_direct_small_problem + env, solution = run_direct_small_problem() + return solution + # cls.solution = solution + + def test_solver_success(self): + self.assertTrue(self.__class__.solution[-1]['solver']['success']) + + def test_solver_fun(self): + self.assertL2(self.__class__.solution[-1]['solver']['fun'], tol=0.01) + + def test_constraint_violation(self): + self.assertL2(self.__class__.solution[-1]['eqC_val'], tol=0.01) + + +class PendulumQuestion(DirectSolverQuestion): + """ Direct solver on the pendulum problem """ + @classmethod + def compute_solution(cls): + from irlc.ex05.direct_pendulum import compute_pendulum_solutions + return compute_pendulum_solutions()[1] + + +class CartpoleTimeQuestion(DirectSolverQuestion): + """ Direct solver on the cartpole (minimum time) task """ + @classmethod + def compute_solution(cls): + from irlc.ex05.direct_cartpole_time import compute_solutions + return compute_solutions()[1] + + +class CartpoleCostQuestion(DirectSolverQuestion): + """ Direct solver on the cartpole (kelly) task """ + @classmethod + def compute_solution(cls): + from irlc.ex05.direct_cartpole_kelly import compute_solutions + return compute_solutions()[1] + +class BrachistochroneQuestion(DirectSolverQuestion): + """ Brachistochrone (unconstrained) """ + + @classmethod + def compute_solution(cls): + from irlc.ex05.direct_brachistochrone import compute_constrained_solutions + return compute_constrained_solutions()[1] + +class BrachistochroneConstrainedQuestion(DirectSolverQuestion): + """ Brachistochrone (constrained) """ + @classmethod + def compute_solution(cls): + from irlc.ex05.direct_brachistochrone import compute_constrained_solutions + return compute_constrained_solutions()[1] + +class Week05Tests(Report): + title = "Tests for week 05" + pack_imports = [irlc] + individual_imports = [] + questions = [ + (DirectMethods, 10), # ok + (DirectSolverQuestion, 10), # ok + (PendulumQuestion, 5), # ok + (DirectAgentPendulum, 10), # ok + (CartpoleTimeQuestion, 5), # ok + (CartpoleCostQuestion, 5), # ok + (BrachistochroneQuestion, 5), # ok + (BrachistochroneConstrainedQuestion, 10), # ok + ] + +if __name__ == '__main__': + from unitgrade import evaluate_report_student + evaluate_report_student(Week05Tests()) diff --git a/irlc/tests/tests_week06.py b/irlc/tests/tests_week06.py new file mode 100644 index 0000000000000000000000000000000000000000..a72463838f7fba08f8db8d4bf789532d313e7e2d --- /dev/null +++ b/irlc/tests/tests_week06.py @@ -0,0 +1,147 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex06.model_boeing import BoeingEnvironment +from unitgrade import UTestCase, Report +import irlc +from irlc import train +import numpy as np +from irlc.ex04.locomotive import LocomotiveEnvironment +from irlc.ex04.model_harmonic import HarmonicOscilatorEnvironment + +matrices = ['L', 'l', 'V', 'v', 'vc'] + +class Problem3LQR(UTestCase): + title = "LQR, full check of implementation" + + @classmethod + def setUpClass(cls): + # def init(self): + from irlc.ex06.dlqr_check import check_LQR + (cls.L, cls.l), (cls.V, cls.v, cls.vc) = check_LQR() + # self.M = list(zip(matrices, [L, l, V, v, vc])) + + def chk_item(self, m_list): + self.assertIsInstance(m_list, list) + self.assertEqualC(len(m_list)) + for m in m_list: + self.assertIsInstance(m, np.ndarray) + self.assertEqualC(m.shape) + self.assertL2(m, tol=1e-6) + + def test_L(self): + self.chk_item(self.__class__.L) + + def test_l(self): + self.chk_item(self.__class__.l) + + def test_V(self): + self.chk_item(self.__class__.V) + + def test_v(self): + self.chk_item(self.__class__.v) + + def test_vc(self): + vc = self.__class__.vc + self.assertIsInstance(vc, list) + for d in vc: + self.assertL2(d, tol=1e-6) + + self.chk_item(self.__class__.l) + +class Problem4LQRAgent(UTestCase): + def _mkagent(self, val=0.): + A = np.ones((2, 2))* (1+val) + A[1, 0] = 0 + B = np.asarray([[0], [1]]) + Q = np.eye(2) * (3+val) + R = np.ones((1, 1)) * 2 + q = np.asarray([-1.1 + val, 0]) + from irlc.ex06.lqr_agent import LQRAgent + env = LocomotiveEnvironment(render_mode=None, Tmax=5, slope=1) + agent = LQRAgent(env, A=A, B=B, Q=Q, R=R, q=q) + return agent + + def test_policy_lqr_a(self): + agent = self._mkagent(0) + self.assertL2(agent.pi(np.asarray([1, 0]), k=0)) + self.assertL2(agent.pi(np.asarray([1, 0]), k=5)) + + def test_policy_lqr_b(self): + agent = self._mkagent(0.2) + self.assertL2(agent.pi(np.asarray([1, 0]), k=0)) + self.assertL2(agent.pi(np.asarray([1, 0]), k=5)) + +class Problem5_6_Boeing(UTestCase): + + def test_compute_A_B_d(self): + from irlc.ex06.boeing_lqr import compute_A_B_d, compute_Q_R_q + model = BoeingEnvironment(Tmax=10).discrete_model.continuous_model + A, B, d = compute_A_B_d(model, dt=0.2) + self.assertL2(A) + self.assertL2(B) + self.assertL2(d) + + def test_compute_Q_R_q(self): + from irlc.ex06.boeing_lqr import compute_A_B_d, compute_Q_R_q + model = BoeingEnvironment(Tmax=10).discrete_model.continuous_model + Q, R, q = compute_Q_R_q(model, dt=0.2) + self.assertL2(Q) + self.assertL2(R) + self.assertL2(q) + + def test_boing_path(self): + from irlc.ex06.boeing_lqr import boeing_simulation + stats, trajectories, env = boeing_simulation() + self.assertL2(trajectories[-1].state, tol=1e-6) + + +class Problem7_8_PidLQR(UTestCase): + def test_constant_lqr_agent(self): + Delta = 0.06 # Time discretization constant + # Define a harmonic osscilator environment. Use .., render_mode='human' to see a visualization. + env = HarmonicOscilatorEnvironment(Tmax=8, dt=Delta, m=0.5, R=np.eye(1) * 8, + render_mode=None) # set render_mode='human' to see the oscillator. + model = env.discrete_model.continuous_model # Get the ControlModel corresponding to this environment. + + from irlc.ex06.boeing_lqr import compute_A_B_d, compute_Q_R_q + from irlc.ex06.lqr_pid import ConstantLQRAgent + A, B, d = compute_A_B_d(model, Delta) + Q, R, q = compute_Q_R_q(model, Delta) + x0, _ = env.reset() + + # Run the LQR agent + lqr_agent = ConstantLQRAgent(env, A=A, B=B, d=d, Q=Q, R=R, q=q) + self.assertLinf(lqr_agent.pi(x0, k=0), tol=1e-3) + self.assertLinf(lqr_agent.pi(x0, k=10), tol=1e-3) + + + def test_KpKd(self): + Delta = 0.06 # Time discretization constant + # Define a harmonic osscilator environment. Use .., render_mode='human' to see a visualization. + env = HarmonicOscilatorEnvironment(Tmax=8, dt=Delta, m=0.5, R=np.eye(1) * 8, + render_mode=None) # set render_mode='human' to see the oscillator. + model = env.discrete_model.continuous_model # Get the ControlModel corresponding to this environment. + from irlc.ex06.boeing_lqr import compute_A_B_d, compute_Q_R_q + from irlc.ex06.lqr_pid import ConstantLQRAgent, get_Kp_Kd + A, B, d = compute_A_B_d(model, Delta) + Q, R, q = compute_Q_R_q(model, Delta) + lqr_agent = ConstantLQRAgent(env, A=A, B=B, d=d, Q=Q, R=R, q=q) + Kp, Kd = get_Kp_Kd(lqr_agent.L[0]) + self.assertAlmostEqualC(Kp, places=3) + self.assertAlmostEqualC(Kd, places=3) + + + + +class Week06Tests(Report): + title = "Tests for week 06" + pack_imports = [irlc] + individual_imports = [] + questions = [ + (Problem3LQR, 10), + (Problem4LQRAgent, 10), + (Problem5_6_Boeing, 10), + (Problem7_8_PidLQR, 10), + ] +if __name__ == '__main__': + from unitgrade import evaluate_report_student + evaluate_report_student(Week06Tests()) diff --git a/irlc/tests/tests_week07.py b/irlc/tests/tests_week07.py new file mode 100644 index 0000000000000000000000000000000000000000..1f46427025ca7e635d340fafa678f4a7e2c309a7 --- /dev/null +++ b/irlc/tests/tests_week07.py @@ -0,0 +1,62 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from unitgrade import Report +import irlc +from unitgrade import UTestCase +import numpy as np +from irlc import Agent, train + +class RendevouzItem(UTestCase): + def test_rendevouz_without_linesearch(self): + """ Rendevouz with iLQR (no linesearch) """ + from irlc.ex07.ilqr_rendovouz_basic import solve_rendovouz + (xs, us, J_hist, l, L), env = solve_rendovouz(use_linesearch=False) + # print(J_hist[-1]) + self.assertL2(xs[-1], tol=1e-2) + + def test_rendevouz_with_linesearch(self): + """ Rendevouz with iLQR (with linesearch) """ + from irlc.ex07.ilqr_rendovouz_basic import solve_rendovouz + (xs, us, J_hist, l, L), env = solve_rendovouz(use_linesearch=True) + # print(J_hist[-1]) + self.assertL2(xs[-1], tol=1e-2) + # return l, L, xs + + + + + +class ILQRAgentQuestion(UTestCase): + """ iLQR Agent on Rendevouz """ + def test_ilqr_agent(self): + from irlc.ex07.ilqr_agent import solve_rendevouz + stats, trajectories, agent = solve_rendevouz() + self.assertL2(trajectories[-1].state[-1], tol=1e-2) + + +class ILQRPendulumQuestion(UTestCase): + """ iLQR Agent on Pendulum """ + + def test_ilqr_agent_pendulum(self): + from irlc.ex07.ilqr_pendulum_agent import Tmax, N + from irlc.ex04.model_pendulum import GymSinCosPendulumEnvironment + from irlc.ex07.ilqr_agent import ILQRAgent + dt = Tmax / N + env = GymSinCosPendulumEnvironment(dt, Tmax=Tmax, supersample_trajectory=True) + agent = ILQRAgent(env, env.discrete_model, N=N, ilqr_iterations=200, use_linesearch=True) + stats, trajectories = train(env, agent, num_episodes=1, return_trajectory=True) + state = trajectories[-1].state[-1] + self.assertL2(state, tol=2e-2) + +class Week07Tests(Report): #240 total. + title = "Tests for week 07" + pack_imports = [irlc] + individual_imports = [] + questions = [ + (RendevouzItem, 10), # ok + (ILQRAgentQuestion, 10), # ok + (ILQRPendulumQuestion, 10), # ok + ] + +if __name__ == '__main__': + from unitgrade import evaluate_report_student + evaluate_report_student(Week07Tests()) diff --git a/irlc/tests/tests_week08.py b/irlc/tests/tests_week08.py new file mode 100644 index 0000000000000000000000000000000000000000..340d69c01c3ef2cae94901444ba52b9887a47bef --- /dev/null +++ b/irlc/tests/tests_week08.py @@ -0,0 +1,278 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from unitgrade import UTestCase, Report, cache +import numpy as np +from irlc import train + + +def train_recording(env, agent, trajectories): + for t in trajectories: + env.reset() + for k in range(len(t.action)): + s = t.state[k] + r = t.reward[k] + a = t.action[k] + sp = t.state[k+1] + agent.pi(s,k) + agent.train(s, a, r, sp, done=k == len(t.action)-1) + + +class BanditQuestion(UTestCase): + """ Value (Q) function estimate """ + tol = 1e-2 # tie-breaking in the gradient bandit is ill-defined. + # testfun = QPrintItem.assertL2 + + # def setUpClass(cls) -> None: + # from irlc.ex08.simple_agents import BasicAgent + # from irlc.ex08.bandits import StationaryBandit + # env = StationaryBandit(k=10, ) + # agent = BasicAgent(env, epsilon=0.1) + # _, cls.trajectories = train(env, agent, return_trajectory=True, num_episodes=1, max_steps=100) + # cls.Q = agent.Q + # cls.env = env + # cls.agent = agent + + def get_env_agent(self): + from irlc.ex08.simple_agents import BasicAgent + from irlc.ex08.bandits import StationaryBandit + env = StationaryBandit(k=10) + agent = BasicAgent(env, epsilon=0.1) + return env, agent + + @cache + def get_trajectories(self): + env, agent = self.get_env_agent() + _, trajectories = train(env, agent, return_trajectory=True, num_episodes=1, max_steps=100) + return trajectories + + # def precompute_payload(self): + # env, agent = self.get_env_agent() + # _, trajectories = train(env, agent, return_trajectory=True, num_episodes=1, max_steps=100) + # return trajectories, agent.Q + + + def test_agent(self): + trajectories = self.get_trajectories() + env, agent = self.get_env_agent() + train_recording(env, agent, trajectories) + self.assertL2(agent.Q, tol=1e-5) + # return agent.Q + # self.Q = Q + # self.question.agent = agent + # return agent.Q + + # testfun = QPrintItem.assertL2 + + def test_action_distributin(self): + T = 10000 + tol = 1 / np.sqrt(T) * 5 + trajectories = self.get_trajectories() + env, agent = self.get_env_agent() + train_recording(env, agent, trajectories) + # for k in self._cache.keys(): print(k) + + from collections import Counter + counts = Counter([agent.pi(None, k) for k in range(T)]) + distrib = [counts[k] / T for k in range(env.k)] + self.assertL2(np.asarray(distrib), tol=tol) + + + # def process_output(self, res, txt, numbers): + # return res + + # def process_output(self, res, txt, numbers): + # return res + # + # def test(self, computed, expected): + # super().test(computed, self.Q) + +# class BanditQuestion(QPrintItem): +# # tol = 1e-6 +# tol = 1e-2 # tie-breaking in the gradient bandit is ill-defined. +# title = "Value (Q) function estimate" +# testfun = QPrintItem.assertL2 +# +# def get_env_agent(self): +# from irlc.ex08.simple_agents import BasicAgent +# from irlc.ex08.bandits import StationaryBandit +# env = StationaryBandit(k=10, ) +# agent = BasicAgent(env, epsilon=0.1) +# return env, agent +# +# def precompute_payload(self): +# env, agent = self.get_env_agent() +# _, trajectories = train(env, agent, return_trajectory=True, num_episodes=1, max_steps=100) +# return trajectories, agent.Q +# +# def compute_answer_print(self): +# trajectories, Q = self.precomputed_payload() +# env, agent = self.get_env_agent() +# train_recording(env, agent, trajectories) +# self.Q = Q +# self.question.agent = agent +# return agent.Q +# +# def process_output(self, res, txt, numbers): +# return res +# +# def test(self, computed, expected): +# super().test(computed, self.Q) +# +# class BanditItemActionDistribution(QPrintItem): +# # Assumes setup has already been done. +# title = "Action distribution test" +# T = 10000 +# tol = 1/np.sqrt(T)*5 +# testfun = QPrintItem.assertL2 +# +# def compute_answer_print(self): +# # print("In agent print code") +# from collections import Counter +# counts = Counter( [self.question.agent.pi(None, k) for k in range(self.T)] ) +# distrib = [counts[k] / self.T for k in range(self.question.agent.env.k)] +# return np.asarray(distrib) +# +# def process_output(self, res, txt, numbers): +# return res +# +# class BanditQuestion(QuestionGroup): +# title = "Simple bandits" +# class SimpleBanditItem(BanditItem): +# #title = "Value function estimate" +# def get_env_agent(self): +# from irlc.ex08.simple_agents import BasicAgent +# from irlc.ex08.bandits import StationaryBandit +# env = StationaryBandit(k=10, ) +# agent = BasicAgent(env, epsilon=0.1) +# return env, agent +# class SimpleBanditActionDistribution(BanditItemActionDistribution): +# pass + + + +class GradientBanditQuestion(BanditQuestion): + """ Gradient agent """ + # class SimpleBanditItem(BanditItem): + # title = "Simple agent question" + def get_env_agent(self): + from irlc.ex08.bandits import StationaryBandit + from irlc.ex08.gradient_agent import GradientAgent + env = StationaryBandit(k=10) + agent = GradientAgent(env, alpha=0.05) + return env, agent + + # def precompute_payload(self): + # env, agent = self.get_env_agent() + # _, trajectories = train(env, agent, return_trajectory=True, num_episodes=1, max_steps=100) + # return trajectories + + def test_agent(self): + trajectories = self.get_trajectories() + env, agent = self.get_env_agent() + train_recording(env, agent, trajectories) + self.assertL2(agent.H, tol=1e-5) + + + # def test(self, computed, expected): + # self.testfun(computed, self.H) + # + # class SimpleBanditActionDistribution(BanditItemActionDistribution): + # pass + + +# class GradientBanditQuestion(QuestionGroup): +# title = "Gradient agent" +# class SimpleBanditItem(BanditItem): +# # title = "Simple agent question" +# def get_env_agent(self): +# from irlc.ex08.bandits import StationaryBandit +# from irlc.ex08.gradient_agent import GradientAgent +# env = StationaryBandit(k=10) +# agent = GradientAgent(env, alpha=0.05) +# return env, agent +# +# def precompute_payload(self): +# env, agent = self.get_env_agent() +# _, trajectories = train(env, agent, return_trajectory=True, num_episodes=1, max_steps=100) +# return trajectories, agent.H +# +# def compute_answer_print(self): +# trajectories, H = self.precomputed_payload() +# env, agent = self.get_env_agent() +# train_recording(env, agent, trajectories) +# self.H = H +# self.question.agent = agent +# return agent.H +# +# def test(self, computed, expected): +# self.testfun(computed, self.H) +# +# class SimpleBanditActionDistribution(BanditItemActionDistribution): +# pass + + + +class UCBAgentQuestion(BanditQuestion): + """ UCB agent """ + # class UCBAgentItem(BanditItem): + def get_env_agent(self): + from irlc.ex08.bandits import StationaryBandit + from irlc.ex08.ucb_agent import UCBAgent + env = StationaryBandit(k=10) + agent = UCBAgent(env) + return env, agent + + # class UCBAgentActionDistribution(BanditItemActionDistribution): + # pass + + +# class UCBAgentQuestion(QuestionGroup): +# title = "UCB agent" +# class UCBAgentItem(BanditItem): +# def get_env_agent(self): +# from irlc.ex08.bandits import StationaryBandit +# from irlc.ex08.ucb_agent import UCBAgent +# env = StationaryBandit(k=10) +# agent = UCBAgent(env) +# return env, agent +# +# class UCBAgentActionDistribution(BanditItemActionDistribution): +# pass + +# class NonstatiotnaryAgentQuestion(QuestionGroup): +# title = "Nonstationary bandit environment" +# class NonstationaryItem(BanditItem): +# def get_env_agent(self): +# epsilon = 0.1 +# from irlc.ex08.nonstationary import NonstationaryBandit, MovingAverageAgent +# bandit = NonstationaryBandit(k=10) +# agent = MovingAverageAgent(bandit, epsilon=epsilon, alpha=0.15) +# return bandit, agent +# +# class NonstationaryActionDistribution(BanditItemActionDistribution): +# pass + +class NonstatiotnaryAgentQuestion(BanditQuestion): + """ UCB agent """ + # class UCBAgentItem(BanditItem): + def get_env_agent(self): + epsilon = 0.1 + from irlc.ex08.nonstationary import NonstationaryBandit, MovingAverageAgent + bandit = NonstationaryBandit(k=10) + agent = MovingAverageAgent(bandit, epsilon=epsilon, alpha=0.15) + return bandit, agent + +import irlc +class Week08Tests(Report): + title = "Tests for week 08" + pack_imports = [irlc] + individual_imports = [] + questions = [ + (BanditQuestion, 10), + (GradientBanditQuestion, 10), + (UCBAgentQuestion, 5), + (NonstatiotnaryAgentQuestion, 5) + ] + +if __name__ == '__main__': + from unitgrade import evaluate_report_student + evaluate_report_student(Week08Tests()) diff --git a/irlc/tests/tests_week09.py b/irlc/tests/tests_week09.py new file mode 100644 index 0000000000000000000000000000000000000000..ca5d4aee9979bb4cfe60d95050bc27d10e031ad7 --- /dev/null +++ b/irlc/tests/tests_week09.py @@ -0,0 +1,314 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from unitgrade import UTestCase, Report +import numpy as np +import irlc +from irlc import train +from irlc.ex09.small_gridworld import SmallGridworldMDP +from irlc.ex09.policy_iteration import policy_iteration +from irlc.ex09.value_iteration import value_iteration +from irlc.gridworld.gridworld_environments import FrozenLake +from irlc.ex09.policy_evaluation import policy_evaluation + +class Problem1_to_3_Warmup(UTestCase): + def test_part1_average_reward(self): + from irlc.ex09.mdp_warmup import expected_reward + mdp = FrozenLake(living_reward=0.2).mdp # Get the MDP of this environment. + s0 = mdp.initial_state + ## Part 1: Expected reward + self.assertAlmostEqualC(expected_reward(mdp, s=s0, a=0), places=5) + self.assertAlmostEqualC(expected_reward(mdp, s=s0, a=2), places=5) + self.assertAlmostEqualC(expected_reward(mdp, s=(1,2), a=0), places=5) + mdp = FrozenLake(living_reward=0.2).mdp # Get the MDP of this environment. + self.assertAlmostEqualC(expected_reward(mdp, s=s0, a=2), places=5) + + def test_part2_v2q(self): + ## Part 2 + # First let's create a non-trivial value function + V = {} + mdp = FrozenLake(living_reward=0.3).mdp + + for k, s in enumerate(sorted(mdp.nonterminal_states)): + V[s] = 2 * (s[0] - s[1]) - 3.5 + + from irlc.ex09.mdp_warmup import value_function2q_function + + states = [(0, 1), (2, 3), (0, 3), (1,3), (1, 2)] + + s0 = mdp.initial_state + + q_ = value_function2q_function(mdp, s=s0, gamma=0.9, v=V) + self.assertIsInstance(q_, dict) + self.assertEqual(list(sorted(q_.keys())), [0, 1, 2, 3] ) + + self.assertEqual(len(q_), 4) + self.assertEqual(len(value_function2q_function(mdp, s=(1,2), gamma=0.9, v=V)), 1) + self.assertAlmostEqualC(q_[0],places=4) + self.assertAlmostEqualC(q_[2], places=4) + + + for s in sorted(states): + q_ = value_function2q_function(mdp, s=s, gamma=0.9, v=V) + for a in [0, 1, 2, 3]: + if a in mdp.A(s): + self.assertAlmostEqualC(q_[a], places=4) + + def test_part2_q2v(self): + ## Part 3 + mdp = FrozenLake(living_reward=0.2).mdp + from irlc.ex09.mdp_warmup import value_function2q_function, q_function2value_function + # Create a non-trivial Q-function for this problem. + Q = {} + s0 = mdp.initial_state + + for k, s in enumerate(mdp.nonterminal_states): + for a in mdp.A(s): + Q[s, a] = (s[0] - s[1]) - 5 * a # The particular values are not important in this example + # Create a policy. In this case pi(a=3) = 0.4. + pi = {0: 0.2, + 1: 0.4, + 2: 0.2, + 3: 0.2} + self.assertAlmostEqualC(q_function2value_function(pi, Q, s=s0), places=4) + +def train_recording(env, agent, trajectories): + for t in trajectories: + env.reset() + for k in range(len(t.action)): + s = t.state[k] + r = t.reward[k] + a = t.action[k] + sp = t.state[k+1] + info = t.info[k] + info_sp = t.info[k+1] + + agent.pi(s,k) + agent.train(s, a, r, sp, done=k == len(t.action)-1, info_s = info, info_sp=info_sp) + + +class ValueFunctionTest(UTestCase): + def check_value_function(self, mdp, V): + self.assertL2(np.asarray([V[s] for s in mdp.states]), tol=1e-3) + +class Problem5PolicyIteration(ValueFunctionTest): + """ Iterative Policy iteration """ + def test_policy_iteration(self): + env = SmallGridworldMDP() + pi, v = policy_iteration(env, gamma=0.91) + self.check_value_function(env, v) + + + +class Problem6ValueIteration(ValueFunctionTest): + """ Iterative value iteration """ + def test_value_iteration(self): + env = SmallGridworldMDP() + # from i + pi, v = value_iteration(env, gamma=0.91) + self.check_value_function(env, v) + + + +class Problem4PolicyEvaluation(ValueFunctionTest): + """ Iterative value iteration """ + def test_policy_evaluation(self): + mdp = SmallGridworldMDP() + pi = {s: {a: 1/len(mdp.A(s)) for a in mdp.A(s) } for s in mdp.nonterminal_states } + v = policy_evaluation(pi, mdp, gamma=0.91) + self.check_value_function(mdp, v) + + def test_policy_evaluation_b(self): + mdp = SmallGridworldMDP() + pi = {s: {a: 1 if a == 0 else 0 for a in mdp.A(s) } for s in mdp.nonterminal_states } + v = policy_evaluation(pi, mdp, gamma=0.91) + self.check_value_function(mdp, v) + + + + +class Problem9Gambler(ValueFunctionTest): + """ Gambler's problem """ + def test_gambler_value_function(self): + # from irlc.ex09.small_gridworld import SmallGridworldMDP, plot_value_function + # from irlc.ex09.policy_iteration import policy_iteration + # from irlc.ex09.value_iteration import value_iteration + from irlc.ex09.gambler import GamblerEnv + env = GamblerEnv() + pi, v = value_iteration(env, gamma=0.91) + self.check_value_function(env, v) + +# class JackQuestion(ValueFunctionTest): +# """ Gambler's problem """ +# def test_jacks_rental_value_function(self): +# # from irlc.ex09.small_gridworld import SmallGridworldMDP, plot_value_function +# # from irlc.ex09.policy_iteration import policy_iteration +# # from irlc.ex09.value_iteration import value_iteration +# # from irlc.ex09.gambler import GamblerEnv +# from irlc.ex09.jacks_car_rental import JackRentalMDP +# max_cars = 5 +# env = JackRentalMDP(max_cars=max_cars, verbose=True) +# pi, V = value_iteration(env, gamma=.9, theta=1e-3, max_iters=1000, verbose=True) +# self.check_value_function(env, V) + +# class JackQuestion(QuestionGroup): +# title = "Jacks car rental problem" +# +# class JackItem(GridworldDPItem): +# title = "Value function test" +# max_cars = 5 +# tol = 0.01 +# +# def get_value_function(self): +# from irlc.ex09.value_iteration import value_iteration +# from irlc.ex09.jacks_car_rental import JackRentalMDP +# env = JackRentalMDP(max_cars=self.max_cars, verbose=True) +# pi, V = value_iteration(env, gamma=.9, theta=1e-3, max_iters=1000, verbose=True) +# return V, env + + + # return v, env + # pass +# class DynamicalProgrammingGroup(QuestionGroup): +# title = "Dynamical Programming test" +# +# class PolicyEvaluationItem(GridworldDPItem): +# title = "Iterative Policy evaluation" +# +# +# +# class PolicyIterationItem(GridworldDPItem): +# title = "policy iteration" +# def get_value_function(self): +# from irlc.ex09.small_gridworld import SmallGridworldMDP +# from irlc.ex09.policy_iteration import policy_iteration +# env = SmallGridworldMDP() +# pi, v = policy_iteration(env, gamma=0.91) +# return v, env +# class ValueIteartionItem(GridworldDPItem): +# title = "value iteration" +# +# def get_value_function(self): +# from irlc.ex09.value_iteration import value_iteration +# from irlc.ex09.small_gridworld import SmallGridworldMDP +# env = SmallGridworldMDP() +# policy, v = value_iteration(env, gamma=0.92, theta=1e-6) +# return v, env + +# class GamlerQuestion(QuestionGroup): +# title = "Gamblers problem" +# class GamlerItem(GridworldDPItem): +# title = "Value-function test" +# def get_value_function(self): +# # from irlc.ex09.small_gridworld import SmallGridworldMDP, plot_value_function +# # from irlc.ex09.policy_iteration import policy_iteration +# from irlc.ex09.value_iteration import value_iteration +# from irlc.ex09.gambler import GamblerEnv +# env = GamblerEnv() +# pi, v = value_iteration(env, gamma=0.91) +# return v, env + +# class JackQuestion(QuestionGroup): +# title ="Jacks car rental problem" +# class JackItem(GridworldDPItem): +# title = "Value function test" +# max_cars = 5 +# tol = 0.01 +# def get_value_function(self): +# from irlc.ex09.value_iteration import value_iteration +# from irlc.ex09.jacks_car_rental import JackRentalMDP +# env = JackRentalMDP(max_cars=self.max_cars, verbose=True) +# pi, V = value_iteration(env, gamma=.9, theta=1e-3, max_iters=1000, verbose=True) +# return V, env + +class Problem8ValueIterationAgent(UTestCase): + """ Value-iteration agent test """ + + def test_sutton_gridworld(self): + tol = 1e-2 + from irlc.gridworld.gridworld_environments import SuttonCornerGridEnvironment + env = SuttonCornerGridEnvironment(living_reward=-1) + from irlc.ex09.value_iteration_agent import ValueIterationAgent + agent = ValueIterationAgent(env, mdp=env.mdp) + stats, _ = train(env, agent, num_episodes=1000) + self.assertL2(np.mean([s['Accumulated Reward'] for s in stats]), tol=tol) + + def test_bookgrid_gridworld(self): + tol = 1e-2 + from irlc.gridworld.gridworld_environments import BookGridEnvironment + env = BookGridEnvironment(living_reward=-1) + from irlc.ex09.value_iteration_agent import ValueIterationAgent + agent = ValueIterationAgent(env, mdp=env.mdp) + stats, _ = train(env, agent, num_episodes=1000) + self.assertL2(np.mean([s['Accumulated Reward'] for s in stats]), tol=tol) + + + # + # + # pass + # class ValueAgentItem(GridworldDPItem): + # title = "Evaluation on Suttons small gridworld" + # tol = 1e-2 + # def get_env(self): + # from irlc.gridworld.gridworld_environments import SuttonCornerGridEnvironment + # return SuttonCornerGridEnvironment(living_reward=-1) + # + # def compute_answer_print(self): + # env = self.get_env() + # from irlc.ex09.value_iteration_agent import ValueIterationAgent + # agent = ValueIterationAgent(env, mdp=env.mdp) + # # env = VideoMonitor(env, agent=agent, agent_monitor_keys=('v',)) + # stats, _ = train(env, agent, num_episodes=1000) + # return np.mean( [s['Accumulated Reward'] for s in stats]) + # + # def process_output(self, res, txt, numbers): + # return res + + # class BookItem(ValueAgentItem): + # title = "Evaluation on alternative gridworld (Bookgrid)" + # def get_env(self): + # from irlc.gridworld.gridworld_environments import BookGridEnvironment + # return BookGridEnvironment(living_reward=-0.6) + +# class DPAgentRLQuestion(QuestionGroup): +# title = "Value-iteration agent test" +# class ValueAgentItem(GridworldDPItem): +# title = "Evaluation on Suttons small gridworld" +# tol = 1e-2 +# def get_env(self): +# from irlc.gridworld.gridworld_environments import SuttonCornerGridEnvironment +# return SuttonCornerGridEnvironment(living_reward=-1) +# +# def compute_answer_print(self): +# env = self.get_env() +# from irlc.ex09.value_iteration_agent import ValueIterationAgent +# agent = ValueIterationAgent(env, mdp=env.mdp) +# # env = VideoMonitor(env, agent=agent, agent_monitor_keys=('v',)) +# stats, _ = train(env, agent, num_episodes=1000) +# return np.mean( [s['Accumulated Reward'] for s in stats]) +# +# def process_output(self, res, txt, numbers): +# return res +# +# class BookItem(ValueAgentItem): +# title = "Evaluation on alternative gridworld (Bookgrid)" +# def get_env(self): +# from irlc.gridworld.gridworld_environments import BookGridEnvironment +# return BookGridEnvironment(living_reward=-0.6) + +class Week09Tests(Report): + title = "Tests for week 09" + pack_imports = [irlc] + individual_imports = [] + questions = [ (Problem1_to_3_Warmup, 10), + (Problem4PolicyEvaluation, 10), + (Problem5PolicyIteration, 10), + (Problem6ValueIteration, 10), + (Problem8ValueIterationAgent, 10), + (Problem9Gambler, 10), + ] + # (JackQuestion, 10), + # (ValueFunctionTest, 20), + + +if __name__ == '__main__': + from unitgrade import evaluate_report_student + evaluate_report_student(Week09Tests()) diff --git a/irlc/tests/tests_week10.py b/irlc/tests/tests_week10.py new file mode 100644 index 0000000000000000000000000000000000000000..b5dd4e6580fd2cd8dcebf7de0ba5f90e9edd9ca8 --- /dev/null +++ b/irlc/tests/tests_week10.py @@ -0,0 +1,132 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from irlc.ex10.question_td0 import a_compute_deltas, b_perform_td0, c_perform_td0_batched +from unitgrade import Report, UTestCase, cache +from irlc import train +import irlc.ex10.envs +import gymnasium as gym +from gymnasium.wrappers import TimeLimit +from irlc.tests.tests_week08 import train_recording + + +class MCAgentQuestion(UTestCase): + """ Test of MC agent """ + def get_env_agent(self): + from irlc.ex10.mc_agent import MCAgent + env = gym.make("SmallGridworld-v0") + env = TimeLimit(env, max_episode_steps=1000) + gamma = .8 + agent = MCAgent(env, gamma=gamma, first_visit=True) + return env, agent + + @cache + def compute_trajectories(self): + env, agent = self.get_env_agent() + _, trajectories = train(env, agent, return_trajectory=True, num_episodes=1, max_steps=100) + return trajectories, agent.Q.to_dict() + + def test_Q_function(self): + trajectories, Q = self.compute_trajectories() + env, agent = self.get_env_agent() + train_recording(env, agent, trajectories) + Qc = [] + Qe = [] + for s, qa in Q.items(): + for a,q in qa.items(): + Qe.append(q) + Qc.append(agent.Q[s,a]) + + self.assertL2(Qe, Qc, tol=1e-5) + + +# class BlackjackQuestion(UTestCase): +# """ MC policy evaluation agent and Blacjack """ +# def test_blackjack_mc(self): +# env = gym.make("Blackjack-v1") +# episodes = 50000 +# from irlc.ex10.mc_evaluate import MCEvaluationAgent +# from irlc.ex10.mc_evaluate_blackjack import get_by_ace, to_matrix, policy20 +# agent = MCEvaluationAgent(env, policy=policy20, gamma=1) +# train(env, agent, num_episodes=episodes) +# w = get_by_ace(agent.v, ace=True) +# X, Y, Z = to_matrix(w) +# print(Z) +# print(Z.dtype) +# self.assertL2(Z, tol=2.5) + + +class TD0Question(UTestCase): + """ Test of TD(0) evaluation agent """ + gamma = 0.8 + + def get_env_agent(self): + from irlc.ex10.td0_evaluate import TD0ValueAgent + env = gym.make("SmallGridworld-v0") + # env = TimeLimit(env, max_episode_steps=1000) + agent = TD0ValueAgent(env, gamma=self.gamma) + return env, agent + + @cache + def compute_trajectories(self): + env, agent = self.get_env_agent() + _, trajectories = train(env, agent, return_trajectory=True, num_episodes=1, max_steps=100) + return trajectories, agent.v + + def test_value_function(self): + # for k in range(1000): + trajectories, v = self.compute_trajectories() + env, agent = self.get_env_agent() + train_recording(env, agent, trajectories) + Qc = [] + Qe = [] + for s, value in v.items(): + Qe.append(value) + Qc.append(agent.v[s]) + + self.assertL2(Qe, Qc, tol=1e-5) + +class MCEvaluationQuestion(TD0Question): + """ Test of MC evaluation agent """ + def get_env_agent(self): + from irlc.ex10.mc_evaluate import MCEvaluationAgent + env = gym.make("SmallGridworld-v0") + env = TimeLimit(env, max_episode_steps=1000) + gamma = .8 + agent = MCEvaluationAgent(env, gamma=gamma, first_visit=True) + return env, agent + + +class ExamQuestionTD0(UTestCase): + + def get_problem(self): + states = [1, 0, 2, -1, 2, 4, 5, 4, 3, 2, 1, -1] + rewards = [1, 1, -1, 0, 1, 2, 2, 0, 0, -1, 1] + v = {s: 0 for s in states} + gamma = 0.9 + alpha = 0.2 + return v, states, rewards, gamma, alpha + + def test_a(self): + v, states, rewards, gamma, alpha = self.get_problem() + self.assertEqualC(a_compute_deltas(v, states, rewards, gamma)) + + def test_b(self): + v, states, rewards, gamma, alpha = self.get_problem() + self.assertEqualC(b_perform_td0(v, states, rewards, gamma, alpha)) + + def test_c(self): + v, states, rewards, gamma, alpha = self.get_problem() + self.assertEqualC(c_perform_td0_batched(v, states, rewards, gamma, alpha)) +class Week10Tests(Report): + title = "Tests for week 10" + pack_imports = [irlc] + individual_imports = [] + questions = [(MCAgentQuestion, 10), + (MCEvaluationQuestion, 10), + # (BlackjackQuestion,5), + (TD0Question, 10), + (ExamQuestionTD0, 10), + ] + +if __name__ == '__main__': + from unitgrade import evaluate_report_student + evaluate_report_student(Week10Tests()) diff --git a/irlc/tests/tests_week11.py b/irlc/tests/tests_week11.py new file mode 100644 index 0000000000000000000000000000000000000000..1fc1087986f8a86071f2fc3ad9466d2b4b6c1d56 --- /dev/null +++ b/irlc/tests/tests_week11.py @@ -0,0 +1,199 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from unitgrade import UTestCase, Report, cache +import numpy as np +from irlc import train +import irlc.ex10.envs +import gymnasium as gym +from irlc.tests.tests_week08 import train_recording +from irlc.tests.tests_week10 import TD0Question, MCAgentQuestion + + +# This problem no longer exists. +# class NStepSarseEvaluationQuestion(TD0Question): +# """ Test of TD-n evaluation agent """ +# # class EvaluateTabular(VExperienceItem): +# # title = "Value-function test" +# gamma = 0.8 +# def get_env_agent(self): +# envn = "SmallGridworld-v0" +# from irlc.ex11.nstep_td_evaluate import TDnValueAgent +# env = gym.make(envn) +# agent = TDnValueAgent(env, gamma=self.gamma, n=5) +# return env, agent + + + +class QAgentQuestion(MCAgentQuestion): + """ Test of Q Agent """ + # class EvaluateTabular(QExperienceItem): + # title = "Q-value test" + + def get_env_agent(self): + from irlc.ex11.q_agent import QAgent + env = gym.make("SmallGridworld-v0") + agent = QAgent(env, gamma=.8) + return env, agent + + +# class LinearWeightVectorTest(UTestCase): + + + +# class LinearValueFunctionTest(LinearWeightVectorTest): +# title = "Linear value-function test" +# def compute_answer_print(self): +# trajectories, Q = self.precomputed_payload() +# env, agent = self.get_env_agent() +# train_recording(env, agent, trajectories) +# self.Q = Q +# self.question.agent = agent +# vfun = [agent.Q[s,a] for s, a in zip(trajectories[0].state, trajectories[0].action)] +# return vfun + +# class TabularAgentStub(UTestCase): +# +# pass + +class TabularAgentStub(UTestCase): + """ Average return over many simulated episodes """ + gamma = 0.95 + epsilon = 0.2 + tol = 0.1 + tol_qs = 0.3 + + def get_env(self): + return gym.make("SmallGridworld-v0") + + def get_env_agent(self): + raise NotImplementedError() + # from irlc.ex11.sarsa_agent import SarsaAgent + # agent = SarsaAgent(self.get_env(), gamma=self.gamma) + # return agent.env, agent + + def get_trained_agent(self): + env, agent = self.get_env_agent() + stats, _ = train(env, agent, num_episodes=9000) + return agent, stats + + def chk_accumulated_reward(self): + agent, stats = self.get_trained_agent() + s0, _ = agent.env.reset() + actions, qs = agent.Q.get_Qs(s0) + print("Tolerance is", self.tol_qs) + self.assertL2(qs, tol=self.tol_qs) + self.assertL2(np.mean([s['Accumulated Reward'] for s in stats]), tol=self.tol) + + # def test_accumulated_reward(self): + # env, agent = self.get_env_agent() + # stats, _ = train(env, agent, num_episodes=5000) + # s = env.reset() + # actions, qs = agent.Q.get_Qs(s) + # self.assertL2(qs, tol=0.3) + # self.assertL2(np.mean([s['Accumulated Reward'] for s in stats]), tol=self.tol) + +class SarsaQuestion(TabularAgentStub): + + + def get_env_agent(self): + from irlc.ex11.sarsa_agent import SarsaAgent + agent = SarsaAgent(self.get_env(), gamma=self.gamma) + return agent.env, agent + + def test_accumulated_reward(self): + self.tol_qs = 2.7 # Got 2.65 in one run. + self.chk_accumulated_reward() + + +class NStepSarsaQuestion(TabularAgentStub): + title = "N-step Sarsa" + # class SarsaReturnItem(SarsaQuestion): + def get_env_agent(self): + from irlc.ex11.nstep_sarsa_agent import SarsaNAgent + agent = SarsaNAgent(self.get_env(), gamma=self.gamma, n=5) + return agent.env, agent + + def test_accumulated_reward(self): + self.tol_qs = 2.7 + self.chk_accumulated_reward() + + +class LinearAgentStub(UTestCase): + # class LinearExperienceItem(LinearWeightVectorTest): + tol = 1e-6 + # title = "Linear sarsa agent" + alpha = 0.08 + num_episodes = 300 + # title = "Weight-vector test" + # testfun = QPrintItem.assertL2 + gamma = 0.8 + tol_w = 1e-5 + + + def get_env_agent(self): + raise NotImplementedError() + + def get_env(self): + return gym.make("MountainCar500-v0") + + # def get_env_agent(self): + # return None, None + + @cache + def compute_trajectories(self): + env, agent = self.get_env_agent() + _, trajectories = train(env, agent, return_trajectory=True, num_episodes=1, max_steps=100) + return trajectories, agent.Q.w + + def chk_Q_weight_vector_w(self): + trajectories, w = self.compute_trajectories() + env, agent = self.get_env_agent() + train_recording(env, agent, trajectories) + print(w) + print(agent.Q.w) + self.assertL2(agent.Q.w, w, tol=self.tol_w) + + pass +class LinearSarsaAgentQuestion(LinearAgentStub): + """ Sarsa Agent with linear function approximators """ + + def get_env_agent(self): + env = self.get_env() + from irlc.ex11.semi_grad_sarsa import LinearSemiGradSarsa + agent = LinearSemiGradSarsa(env, gamma=1, alpha=self.alpha, epsilon=0) + return env, agent + + def test_Q_weight_vector_w(self): + self.tol_w = 1.4 + self.chk_Q_weight_vector_w() + +class LinearQAgentQuestion(LinearAgentStub): + """ Test of Linear Q Agent """ + + def get_env_agent(self): + env = self.get_env() + alpha = 0.1 + from irlc.ex11.semi_grad_q import LinearSemiGradQAgent + agent = LinearSemiGradQAgent(env, gamma=1, alpha=alpha, epsilon=0) + return env, agent + + def test_Q_weight_vector_w(self): + # self.tol_qs = 1.9 + self.tol_w = 7 + self.chk_Q_weight_vector_w() + + +class Week11Tests(Report): + title = "Tests for week 11" + pack_imports = [irlc] + individual_imports = [] + questions =[ + # (NStepSarseEvaluationQuestion, 10), + (QAgentQuestion, 10), + (LinearQAgentQuestion, 10), + (LinearSarsaAgentQuestion, 10), + (SarsaQuestion, 10), + (NStepSarsaQuestion, 5), + ] +if __name__ == '__main__': + from unitgrade import evaluate_report_student + evaluate_report_student(Week11Tests()) diff --git a/irlc/tests/tests_week12.py b/irlc/tests/tests_week12.py new file mode 100644 index 0000000000000000000000000000000000000000..17c6c620939f3465a03a25c2862020bd6f8e7eec --- /dev/null +++ b/irlc/tests/tests_week12.py @@ -0,0 +1,64 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from unitgrade import UTestCase, cache, Report +import irlc.ex10.envs +## WEEK 12: +from irlc.tests.tests_week11 import TabularAgentStub, LinearAgentStub + +class LinearSarsaNstepAgentQuestion(LinearAgentStub): + """ Test of Linear n-step sarsa Agent """ + tol = 2200 + num_episodes = 150 + gamma = 1 + tol_w = 2.5 + + def get_env_agent(self): + env = self.get_env() + from irlc.ex12.semi_grad_nstep_sarsa import LinearSemiGradSarsaN + from irlc.ex12.semi_grad_sarsa_lambda import alpha + agent = LinearSemiGradSarsaN(env, gamma=self.gamma, alpha=alpha, epsilon=0) + return env, agent + + def test_Q_weight_vector_w(self): + + self.chk_Q_weight_vector_w() + + +class LinearSarsaLambdaAgentQuestion(LinearAgentStub): + """ Test of Linear sarsa(Lambda) Agent """ + tol = 2200 + num_episodes = 150 + gamma = 1 + tol_w = 15 + + def get_env_agent(self): + env = self.get_env() + from irlc.ex12.semi_grad_sarsa_lambda import LinearSemiGradSarsaLambda, alpha + agent = LinearSemiGradSarsaLambda(env, gamma=self.gamma, alpha=alpha, epsilon=0) + return env, agent + + def test_Q_weight_vector_w(self): + self.chk_Q_weight_vector_w() + +class SarsaLambdaQuestion(TabularAgentStub): + """ Sarsa(lambda) """ + def get_env_agent(self): + from irlc.ex12.sarsa_lambda_agent import SarsaLambdaAgent + agent = SarsaLambdaAgent(self.get_env(), gamma=self.gamma, lamb=0.7) + return agent.env, agent + + def test_reward_function(self): + self.tol_qs = 3.1 + self.chk_accumulated_reward() + +class Week12Tests(Report): + title = "Tests for week 12" + pack_imports = [irlc] + individual_imports = [] + questions = [ + (SarsaLambdaQuestion, 10), + (LinearSarsaLambdaAgentQuestion, 10), + (LinearSarsaNstepAgentQuestion, 10),] + +if __name__ == '__main__': + from unitgrade import evaluate_report_student + evaluate_report_student(Week12Tests()) diff --git a/irlc/tests/tests_week13.py b/irlc/tests/tests_week13.py new file mode 100644 index 0000000000000000000000000000000000000000..a405795d56f6bc556e0af30ad34b9fc585fed5fe --- /dev/null +++ b/irlc/tests/tests_week13.py @@ -0,0 +1,76 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from unitgrade import UTestCase, Report +import numpy as np +from irlc import train +import irlc.ex10.envs +from irlc.tests.tests_week11 import TabularAgentStub + +class DoubleQQuestion(TabularAgentStub): + """ Double Q learning """ + def test_accumulated_reward(self): + env, agent = self.get_env_agent() + stats, _ = train(env, agent, num_episodes=5000) + s, info = env.reset() + actions, qs = agent.Q1.get_Qs(s, info) + self.assertL2(qs, tol=10) + self.assertL2(np.mean([s['Accumulated Reward'] for s in stats]), tol=self.tol) + return stats + + def get_env_agent(self): + from irlc.ex13.tabular_double_q import TabularDoubleQ + agent = TabularDoubleQ(self.get_env(), gamma=self.gamma) + return agent.env, agent + + +class DynaQQuestion(TabularAgentStub): + """ Dyna Q learning """ + # class DynaQReturnItem(SarsaReturnTypeItem): + def get_env_agent(self): + from irlc.ex13.dyna_q import DynaQ + agent = DynaQ(self.get_env(), gamma=self.gamma) + return agent.env, agent + + def test_accumulated_reward(self): + self.chk_accumulated_reward() + +class Week13Tests(Report): + title = "Tests for week 13" + pack_imports = [irlc] + individual_imports = [] + questions = [(DoubleQQuestion, 10), + (DynaQQuestion, 10) + ] + +if __name__ == '__main__': + from unitgrade import evaluate_report_student + evaluate_report_student(Week13Tests()) + + # class DynaQItem(SarsaTypeQItem): + # title = "Dyna Q action distribution" + +# class DoubleQQuestion(QuestionGroup): +# title = "Double Q learning" +# class DQReturnItem(SarsaReturnTypeItem): +# def get_env_agent(self): +# from irlc.ex13.tabular_double_q import TabularDoubleQ +# agent = TabularDoubleQ(self.get_env(), gamma=self.gamma) +# return agent.env, agent +# +# class DoubleQItem(SarsaTypeQItem): +# tol = 1 +# def compute_answer_print(self): +# s = self.question.env.reset() +# actions, qs = self.question.agent.Q1.get_Qs(s) +# return qs +# title = "Double Q action distribution" +# +# class DynaQQuestion(QuestionGroup): +# title = "Dyna Q learning" +# class DynaQReturnItem(SarsaReturnTypeItem): +# def get_env_agent(self): +# from irlc.ex13.dyna_q import DynaQ +# agent = DynaQ(self.get_env(), gamma=self.gamma) +# return agent.env, agent +# +# class DynaQItem(SarsaTypeQItem): +# title = "Dyna Q action distribution" diff --git a/irlc/tests/unitgrade_data/BanditQuestion.pkl b/irlc/tests/unitgrade_data/BanditQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..2c5a18391d06f2648ea043f218bd0c21e42e5bf7 Binary files /dev/null and b/irlc/tests/unitgrade_data/BanditQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/BrachistochroneConstrainedQuestion.pkl b/irlc/tests/unitgrade_data/BrachistochroneConstrainedQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..f89ebd0b17dcf615ea283960334caa2a4c4a402d Binary files /dev/null and b/irlc/tests/unitgrade_data/BrachistochroneConstrainedQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/BrachistochroneQuestion.pkl b/irlc/tests/unitgrade_data/BrachistochroneQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..f89ebd0b17dcf615ea283960334caa2a4c4a402d Binary files /dev/null and b/irlc/tests/unitgrade_data/BrachistochroneQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/CartpoleCostQuestion.pkl b/irlc/tests/unitgrade_data/CartpoleCostQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..f89ebd0b17dcf615ea283960334caa2a4c4a402d Binary files /dev/null and b/irlc/tests/unitgrade_data/CartpoleCostQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/CartpoleTimeQuestion.pkl b/irlc/tests/unitgrade_data/CartpoleTimeQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..f89ebd0b17dcf615ea283960334caa2a4c4a402d Binary files /dev/null and b/irlc/tests/unitgrade_data/CartpoleTimeQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/DirectAgentPendulum.pkl b/irlc/tests/unitgrade_data/DirectAgentPendulum.pkl new file mode 100644 index 0000000000000000000000000000000000000000..0c8ed034d34e4f1249429c51e84cde358e9e0c53 Binary files /dev/null and b/irlc/tests/unitgrade_data/DirectAgentPendulum.pkl differ diff --git a/irlc/tests/unitgrade_data/DirectMethods.pkl b/irlc/tests/unitgrade_data/DirectMethods.pkl new file mode 100644 index 0000000000000000000000000000000000000000..7807f4c5c34901502f48afab8f9e4b5368ddb9c7 Binary files /dev/null and b/irlc/tests/unitgrade_data/DirectMethods.pkl differ diff --git a/irlc/tests/unitgrade_data/DirectSolverQuestion.pkl b/irlc/tests/unitgrade_data/DirectSolverQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..f89ebd0b17dcf615ea283960334caa2a4c4a402d Binary files /dev/null and b/irlc/tests/unitgrade_data/DirectSolverQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/DoubleQQuestion.pkl b/irlc/tests/unitgrade_data/DoubleQQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..338359a9b6b0183a8855b431d00c4230184b6531 Binary files /dev/null and b/irlc/tests/unitgrade_data/DoubleQQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/DynaQQuestion.pkl b/irlc/tests/unitgrade_data/DynaQQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..881f7e8445a02d321d4c116613de9ba2555be5b4 Binary files /dev/null and b/irlc/tests/unitgrade_data/DynaQQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/Exam5InventoryEvaluation.pkl b/irlc/tests/unitgrade_data/Exam5InventoryEvaluation.pkl new file mode 100644 index 0000000000000000000000000000000000000000..9fdf18dc7fa643011b14ff9347ca6e4d145fe2b1 Binary files /dev/null and b/irlc/tests/unitgrade_data/Exam5InventoryEvaluation.pkl differ diff --git a/irlc/tests/unitgrade_data/Exam6Toy2d.pkl b/irlc/tests/unitgrade_data/Exam6Toy2d.pkl new file mode 100644 index 0000000000000000000000000000000000000000..1670e52ae3c857948adaa2e31f95386afc7141ed Binary files /dev/null and b/irlc/tests/unitgrade_data/Exam6Toy2d.pkl differ diff --git a/irlc/tests/unitgrade_data/ExamQuestion7FlowersStore.pkl b/irlc/tests/unitgrade_data/ExamQuestion7FlowersStore.pkl new file mode 100644 index 0000000000000000000000000000000000000000..cb028ef6008443edeaf2d59477e056c3f5c3435b Binary files /dev/null and b/irlc/tests/unitgrade_data/ExamQuestion7FlowersStore.pkl differ diff --git a/irlc/tests/unitgrade_data/ExamQuestionTD0.pkl b/irlc/tests/unitgrade_data/ExamQuestionTD0.pkl new file mode 100644 index 0000000000000000000000000000000000000000..8058414ea4d73d7348da513847bf99750505a346 Binary files /dev/null and b/irlc/tests/unitgrade_data/ExamQuestionTD0.pkl differ diff --git a/irlc/tests/unitgrade_data/GradientBanditQuestion.pkl b/irlc/tests/unitgrade_data/GradientBanditQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..2c5a18391d06f2648ea043f218bd0c21e42e5bf7 Binary files /dev/null and b/irlc/tests/unitgrade_data/GradientBanditQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/ILQRAgentQuestion.pkl b/irlc/tests/unitgrade_data/ILQRAgentQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..e59b83ccdc4b3f776c5a856eec67e5fd46e7d7d0 Binary files /dev/null and b/irlc/tests/unitgrade_data/ILQRAgentQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/ILQRPendulumQuestion.pkl b/irlc/tests/unitgrade_data/ILQRPendulumQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..4f0c03f218e5d0b490f478d91439da34bd99266e Binary files /dev/null and b/irlc/tests/unitgrade_data/ILQRPendulumQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/LinearQAgentQuestion.pkl b/irlc/tests/unitgrade_data/LinearQAgentQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..b33a50f38070a14eeec2fae6b109a09352122a5e Binary files /dev/null and b/irlc/tests/unitgrade_data/LinearQAgentQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/LinearSarsaAgentQuestion.pkl b/irlc/tests/unitgrade_data/LinearSarsaAgentQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..aa697f66dfdc784268f10c345ef0eedd1d3aec6e Binary files /dev/null and b/irlc/tests/unitgrade_data/LinearSarsaAgentQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/LinearSarsaLambdaAgentQuestion.pkl b/irlc/tests/unitgrade_data/LinearSarsaLambdaAgentQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..ac6a2adfc3d2faa7e4931cd49899e28973477304 Binary files /dev/null and b/irlc/tests/unitgrade_data/LinearSarsaLambdaAgentQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/LinearSarsaNstepAgentQuestion.pkl b/irlc/tests/unitgrade_data/LinearSarsaNstepAgentQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..8488e90d29bc64d169076721ebc84563b582102a Binary files /dev/null and b/irlc/tests/unitgrade_data/LinearSarsaNstepAgentQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/MCAgentQuestion.pkl b/irlc/tests/unitgrade_data/MCAgentQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..713e0329e51abbb76d789ee80671d47f60c6f853 Binary files /dev/null and b/irlc/tests/unitgrade_data/MCAgentQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/MCEvaluationQuestion.pkl b/irlc/tests/unitgrade_data/MCEvaluationQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..f4f0406a0c565589ce80ec15300a0a6fb8a40aa2 Binary files /dev/null and b/irlc/tests/unitgrade_data/MCEvaluationQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/NStepSarsaQuestion.pkl b/irlc/tests/unitgrade_data/NStepSarsaQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..c98a9b05f5d8467d392af2386aa023b3f1b6751b Binary files /dev/null and b/irlc/tests/unitgrade_data/NStepSarsaQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/NonstatiotnaryAgentQuestion.pkl b/irlc/tests/unitgrade_data/NonstatiotnaryAgentQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..2c5a18391d06f2648ea043f218bd0c21e42e5bf7 Binary files /dev/null and b/irlc/tests/unitgrade_data/NonstatiotnaryAgentQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/PendulumQuestion.pkl b/irlc/tests/unitgrade_data/PendulumQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..f89ebd0b17dcf615ea283960334caa2a4c4a402d Binary files /dev/null and b/irlc/tests/unitgrade_data/PendulumQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem1BobsFriend.pkl b/irlc/tests/unitgrade_data/Problem1BobsFriend.pkl new file mode 100644 index 0000000000000000000000000000000000000000..d78b291c68506ecf4fd989c63798af420f4a6796 Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem1BobsFriend.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem1DiscreteKuromoto.pkl b/irlc/tests/unitgrade_data/Problem1DiscreteKuromoto.pkl new file mode 100644 index 0000000000000000000000000000000000000000..92cac8fbf95496ae843852ed47ffa0126de01034 Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem1DiscreteKuromoto.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem1Kuramoto.pkl b/irlc/tests/unitgrade_data/Problem1Kuramoto.pkl new file mode 100644 index 0000000000000000000000000000000000000000..be8942b1ca5a38b7fc7729522a74b70bfb9ff86f Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem1Kuramoto.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem1SmallGraph.pkl b/irlc/tests/unitgrade_data/Problem1SmallGraph.pkl new file mode 100644 index 0000000000000000000000000000000000000000..457e6cfae3680e41113fb761612b2d6549e0f33a Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem1SmallGraph.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem1_to_3_Warmup.pkl b/irlc/tests/unitgrade_data/Problem1_to_3_Warmup.pkl new file mode 100644 index 0000000000000000000000000000000000000000..5fe4c1c222730aae3e22f3624738a4c59c0eac1d Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem1_to_3_Warmup.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem2BobsPolicy.pkl b/irlc/tests/unitgrade_data/Problem2BobsPolicy.pkl new file mode 100644 index 0000000000000000000000000000000000000000..6a3aeda8f5e65ae42b2ef46f1e849800365db6cd Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem2BobsPolicy.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem2DeterministicDP.pkl b/irlc/tests/unitgrade_data/Problem2DeterministicDP.pkl new file mode 100644 index 0000000000000000000000000000000000000000..b2c9f862612e2ec027398436366b5a8529c55b9e Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem2DeterministicDP.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem3InventoryInventoryEnvironment.pkl b/irlc/tests/unitgrade_data/Problem3InventoryInventoryEnvironment.pkl new file mode 100644 index 0000000000000000000000000000000000000000..2884379ef4ad61d5c1f40f6b0d358ed37807e00a Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem3InventoryInventoryEnvironment.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem3LQR.pkl b/irlc/tests/unitgrade_data/Problem3LQR.pkl new file mode 100644 index 0000000000000000000000000000000000000000..dd9396d7727e03e60310dbeb194c6fa0e926ad71 Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem3LQR.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem3PID.pkl b/irlc/tests/unitgrade_data/Problem3PID.pkl new file mode 100644 index 0000000000000000000000000000000000000000..0a50350d1e3873dc5a0027ddef29807f8cb73567 Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem3PID.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem3StochasticDP.pkl b/irlc/tests/unitgrade_data/Problem3StochasticDP.pkl new file mode 100644 index 0000000000000000000000000000000000000000..a9e1f718b4a5fe6496c6ce865c9debb2532f36a8 Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem3StochasticDP.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem4DPAgent.pkl b/irlc/tests/unitgrade_data/Problem4DPAgent.pkl new file mode 100644 index 0000000000000000000000000000000000000000..4803c3df36a3efdeb10bbaf156bb55a8cbaf8a78 Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem4DPAgent.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem4InventoryTrain.pkl b/irlc/tests/unitgrade_data/Problem4InventoryTrain.pkl new file mode 100644 index 0000000000000000000000000000000000000000..1d5ec57b3873975837e96bcefd4032d25e793dff Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem4InventoryTrain.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem4LQRAgent.pkl b/irlc/tests/unitgrade_data/Problem4LQRAgent.pkl new file mode 100644 index 0000000000000000000000000000000000000000..fdf20eabff7843729b6fc296dc17968c489684c2 Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem4LQRAgent.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem4PIDAgent.pkl b/irlc/tests/unitgrade_data/Problem4PIDAgent.pkl new file mode 100644 index 0000000000000000000000000000000000000000..0f93099ff362ede4c65df1a0b1d97713b51cd828 Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem4PIDAgent.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem4PolicyEvaluation.pkl b/irlc/tests/unitgrade_data/Problem4PolicyEvaluation.pkl new file mode 100644 index 0000000000000000000000000000000000000000..e508cb77936ac1c0cf998ccfc2e2c6ef195f1fae Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem4PolicyEvaluation.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem5PacmanHardcoded.pkl b/irlc/tests/unitgrade_data/Problem5PacmanHardcoded.pkl new file mode 100644 index 0000000000000000000000000000000000000000..7456df2b732f41fded93f4777ebfbb7de7ac4635 Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem5PacmanHardcoded.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem5PolicyIteration.pkl b/irlc/tests/unitgrade_data/Problem5PolicyIteration.pkl new file mode 100644 index 0000000000000000000000000000000000000000..a735b4e1bfbb9327fef0896cac48c27181fd52e6 Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem5PolicyIteration.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem5_6_Boeing.pkl b/irlc/tests/unitgrade_data/Problem5_6_Boeing.pkl new file mode 100644 index 0000000000000000000000000000000000000000..8962e84a52523dd962cccb029fe7420f69b17262 Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem5_6_Boeing.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem6ChessTournament.pkl b/irlc/tests/unitgrade_data/Problem6ChessTournament.pkl new file mode 100644 index 0000000000000000000000000000000000000000..4f72ab97d7183e2c0a92e473bbc41b1995e7700e Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem6ChessTournament.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem6ValueIteration.pkl b/irlc/tests/unitgrade_data/Problem6ValueIteration.pkl new file mode 100644 index 0000000000000000000000000000000000000000..11808b575d0b326f4d0f2f81886157ec91205e30 Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem6ValueIteration.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem7PIDCar.pkl b/irlc/tests/unitgrade_data/Problem7PIDCar.pkl new file mode 100644 index 0000000000000000000000000000000000000000..c838b752379b807ec2bc13988d3e7a5185e68f1d Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem7PIDCar.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem7_8_PidLQR.pkl b/irlc/tests/unitgrade_data/Problem7_8_PidLQR.pkl new file mode 100644 index 0000000000000000000000000000000000000000..d72e621ef2aca6c202e903ee802dce60b23cabdd Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem7_8_PidLQR.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem8ValueIterationAgent.pkl b/irlc/tests/unitgrade_data/Problem8ValueIterationAgent.pkl new file mode 100644 index 0000000000000000000000000000000000000000..945ae0a0ae941571d4533d2c71c2f01c82beb9cc Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem8ValueIterationAgent.pkl differ diff --git a/irlc/tests/unitgrade_data/Problem9Gambler.pkl b/irlc/tests/unitgrade_data/Problem9Gambler.pkl new file mode 100644 index 0000000000000000000000000000000000000000..edce9bc7f5b6c047adacf9bc2b090c35dead8b63 Binary files /dev/null and b/irlc/tests/unitgrade_data/Problem9Gambler.pkl differ diff --git a/irlc/tests/unitgrade_data/QAgentQuestion.pkl b/irlc/tests/unitgrade_data/QAgentQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..016ea7eaeaa3fb5d36c759909b4f43314d6ed9d9 Binary files /dev/null and b/irlc/tests/unitgrade_data/QAgentQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/RendevouzItem.pkl b/irlc/tests/unitgrade_data/RendevouzItem.pkl new file mode 100644 index 0000000000000000000000000000000000000000..c007adc309fa60055fc59ec8c3bba3ce8aab72a2 Binary files /dev/null and b/irlc/tests/unitgrade_data/RendevouzItem.pkl differ diff --git a/irlc/tests/unitgrade_data/SarsaLambdaQuestion.pkl b/irlc/tests/unitgrade_data/SarsaLambdaQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..5ba0d797f173e178b1d8c28c2beabf4941532788 Binary files /dev/null and b/irlc/tests/unitgrade_data/SarsaLambdaQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/SarsaQuestion.pkl b/irlc/tests/unitgrade_data/SarsaQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..157e90bfa38a07c4ecb0813c73f687ce88904ca7 Binary files /dev/null and b/irlc/tests/unitgrade_data/SarsaQuestion.pkl differ diff --git a/irlc/tests/unitgrade_data/TD0Question.pkl b/irlc/tests/unitgrade_data/TD0Question.pkl new file mode 100644 index 0000000000000000000000000000000000000000..6a18f61d92942a8ae3c89fcaa3a6e16b94322e4f Binary files /dev/null and b/irlc/tests/unitgrade_data/TD0Question.pkl differ diff --git a/irlc/tests/unitgrade_data/UCBAgentQuestion.pkl b/irlc/tests/unitgrade_data/UCBAgentQuestion.pkl new file mode 100644 index 0000000000000000000000000000000000000000..2c5a18391d06f2648ea043f218bd0c21e42e5bf7 Binary files /dev/null and b/irlc/tests/unitgrade_data/UCBAgentQuestion.pkl differ diff --git a/irlc/update_files.py b/irlc/update_files.py new file mode 100644 index 0000000000000000000000000000000000000000..783901432a68d39a1853059cf06e79caada6b778 --- /dev/null +++ b/irlc/update_files.py @@ -0,0 +1,109 @@ +import fnmatch +import requests +from io import BytesIO +import zipfile +import os +import sys + +print("Hello! This is an automatic updating script that will perform the following operations:") +print("1) Download the most current version of the course material from gitlab") +print("2) Check if you are missing any files and create them") +print("3) update this script to the most recent version") +print("4) Update certain files that you should not edit (_grade-scripts and so on) to the most recent version") + +url_install = "https://02465material.pages.compute.dtu.dk/02465public/information/installation.html" +sdir = os.path.dirname(__file__) +dry = False + +if "02465public" in sdir and "tuhe" in sdir: + dry = True + print("-"*100) + print("It has been detected that this script is running on the teachers computer.") + print("This means that your files will not be overwritten normally.") + print("In the highly unusual case this is a mistake, please change dry=False in the code.") + print("-"*100) + # raise Exception("(teachers not to himself: Don't run this on your own computer)") + + +print("The script is being run using python version:", sys.executable) + +if not os.path.basename(sdir) == "irlc": + print("The script was unable to locate an 'irlc' folder. The most likely reason this occurs is that you have moved the location of the script, or that you have deleted the irlc folder. ") + print("The current location of the script is:", sdir) + print("Make sure this folder contains an irlc folder. If you have deleted it, simply start over with the installation instructions. ") + sys.exit(1) # Exit with error code 1 + +try: + import unitgrade # type: ignore + # import irlc +except ImportError as e: + print("Your python environment was unable to locate unitgrade") + print("This means that you either did not install the software correctly, or that you installed it in the wrong python interpreter (i.e., you have multiple versions of python installed).") + + print("VS Code: Please select a different Python through the Command Palette (Ctrl+Shift+P) and choose ""Python: Select Interpreter"".") + print("Try all the Pythons you can choose and run the script from them") + print(f"See also {url_install}") + sys.exit(1) # Exit with error code 1 + +def read_and_extract_zip(url): + # Download the zip file from the URL + base_dir = url.split("/main/")[-1].split(".zip")[0] + response = requests.get(url) + local_students_folder = os.path.dirname(os.path.dirname(__file__)) + always_overwrite = ['irlc/update_files.py', 'irlc/__init__.py', 'irlc/tests/*', '**/unitgrade_data/*.pkl', 'irlc/car/*', 'irlc/gridworld/*', 'irlc/pacman/*', 'irlc/utils/*', '*_grade.py', '*/project*_tests.py'] + # Check if the request was successful (status code 200) + if response.status_code == 200: + zip_content = BytesIO(response.content) + # Open the zip file using the zipfile module + with zipfile.ZipFile(zip_content, 'r') as zip_ref: + # List the files in the zip file + # Iterate over the files in the zip file + for file_name in zip_ref.filelist: + # Read the content of each file + if not file_name.is_dir(): + rp = os.path.relpath(file_name.filename, base_dir) + new_path = os.path.join(local_students_folder, rp) + overwrite = [p for p in always_overwrite if fnmatch.fnmatch(rp, p)] + if len(overwrite) > 0 or not os.path.isfile(new_path): + commit = True + try: + if os.path.isfile(new_path): + with open(new_path, 'rb') as newf: + if newf.read() == zip_ref.read(file_name.filename): + commit = False + else: + commit = True + except Exception as e: + print("Problem reading local file", new_path) + pass + + if commit: + print("> Overwriting...", new_path) + if not dry: + if not os.path.isdir(os.path.dirname(new_path)): + os.makedirs(os.path.dirname(new_path)) + with open(new_path, 'wb') as f: + f.write(zip_ref.read(file_name.filename)) + else: + pass + else: + print(f"Failed to download the zip file. Status code: {response.status_code}. The DTU Gitlab server may be overloaded, unavailable, or you have no network.") + a = 34 + +# Replace 'your_zip_file_url' with the actual URL of the zip file +zip_file_url = 'https://gitlab.compute.dtu.dk/02465material/02465students/-/archive/main/02465students-main.zip' +read_and_extract_zip(zip_file_url) + +try: + import irlc +except ImportError as e: + print("Oh no, Python encountered a problem during importing irlc.") + import site + print("") + print("This is possibly because you moved or renamed the 02465students folder after the installation was completed, ") + print("or because you selected another python interpreter than the one you used during install. ") + print("Please move/rename the students folder back so it can be found at the this path again, and/or select another interpreter from the command pallette") + print(f"See also {url_install}") + sys.exit(1) # Exit with error code 1 + +print("> The script terminated successfully. Your files should be up to date.") \ No newline at end of file diff --git a/irlc/utils/__init__.py b/irlc/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a56057c84d0ceac54aab1d40ba0f370c77fe10be --- /dev/null +++ b/irlc/utils/__init__.py @@ -0,0 +1 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. diff --git a/irlc/utils/__pycache__/__init__.cpython-311.pyc b/irlc/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d927f6899883d10645c4359d8b581fd82bf24276 Binary files /dev/null and b/irlc/utils/__pycache__/__init__.cpython-311.pyc differ diff --git a/irlc/utils/__pycache__/common.cpython-311.pyc b/irlc/utils/__pycache__/common.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..117dbad6cb0d38b3aafcc6ba26318720288fab93 Binary files /dev/null and b/irlc/utils/__pycache__/common.cpython-311.pyc differ diff --git a/irlc/utils/__pycache__/graphics_util_pygame.cpython-311.pyc b/irlc/utils/__pycache__/graphics_util_pygame.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a2de9b202c80da608b15438e243b1157f00709d Binary files /dev/null and b/irlc/utils/__pycache__/graphics_util_pygame.cpython-311.pyc differ diff --git a/irlc/utils/__pycache__/irlc_plot.cpython-311.pyc b/irlc/utils/__pycache__/irlc_plot.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7aa1b05105fae565302e536103d713e6c25f1e18 Binary files /dev/null and b/irlc/utils/__pycache__/irlc_plot.cpython-311.pyc differ diff --git a/irlc/utils/__pycache__/lazylog.cpython-311.pyc b/irlc/utils/__pycache__/lazylog.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..46db0cc630ee8ec0f509e98df741e7c06852dc91 Binary files /dev/null and b/irlc/utils/__pycache__/lazylog.cpython-311.pyc differ diff --git a/irlc/utils/__pycache__/player_wrapper.cpython-311.pyc b/irlc/utils/__pycache__/player_wrapper.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f57153c5e14540a2c97df79d5ceba88e28817fea Binary files /dev/null and b/irlc/utils/__pycache__/player_wrapper.cpython-311.pyc differ diff --git a/irlc/utils/__pycache__/ptext.cpython-311.pyc b/irlc/utils/__pycache__/ptext.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eb451879a9f6a5d67719c036754531e523e49a34 Binary files /dev/null and b/irlc/utils/__pycache__/ptext.cpython-311.pyc differ diff --git a/irlc/utils/__pycache__/timer.cpython-311.pyc b/irlc/utils/__pycache__/timer.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..92b2dccac3ab657ffcb6961d6f22f710e8c4a9ea Binary files /dev/null and b/irlc/utils/__pycache__/timer.cpython-311.pyc differ diff --git a/irlc/utils/common.py b/irlc/utils/common.py new file mode 100644 index 0000000000000000000000000000000000000000..43c9d705fefb113279d5337f49235f3c268b33b5 --- /dev/null +++ b/irlc/utils/common.py @@ -0,0 +1,206 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from gymnasium import spaces +import collections +import inspect +import types +import numpy as np +import os, glob, csv +from irlc.utils.lazylog import LazyLog + +class defaultdict2(collections.defaultdict): + def __missing__(self, key): + if self.default_factory is None: + raise KeyError((key,)) + + if isinstance(self.default_factory, types.FunctionType): + nargs = len(inspect.getfullargspec(self.default_factory).args) + self[key] = value = self.default_factory(key) if nargs == 1 else self.default_factory() + return value + else: + return super().__missing__(key) + +## Helper functions for saving/loading a time series +def load_time_series(experiment_name, exclude_empty=True): + """ + Load most recent non-empty time series (we load non-empty since lazylog creates a new dir immediately) + """ + files = list(filter(os.path.isdir, glob.glob(experiment_name+"/*"))) + if exclude_empty: + files = [f for f in files if os.path.exists(os.path.join(f, "log.txt")) and os.stat(os.path.join(f, "log.txt")).st_size > 0] + + if len(files) == 0: + return [], None + recent = sorted(files, key=lambda file: os.path.basename(file))[-1] + stats = [] + with open(recent + '/log.txt', 'r') as f: + csv_reader = csv.reader(f, delimiter='\t') + for i, row in enumerate(csv_reader): + if i == 0: + head = row + else: + def tofloat(v): + try: + return float(v) + except Exception: + return v + + stats.append( {k:tofloat(v) for k, v in zip(head, row) } ) + return stats, recent + +def average_trajectories(trajectories): + if len(trajectories) == 0: + return None + from irlc.ex01.agent import Trajectory, fields + t = trajectories[0] + # t._asdict() + # n = max( [len(t.time) for t in trajectories] ) + trajectories2 = sorted(trajectories, key=lambda t: len(t.time)) + tlong = trajectories2[-1] + dd = dict(state=[], action=[],reward=[]) + # keys = list(dd.keys()) + + for t in range(len(tlong.time)): + for k in ['state', 'action', 'reward']: + avg = [] + for traj in trajectories: + z = traj.__getattribute__(k) + if len(z) > t: + avg.append(z[t]) + if len(avg) > 0: + # avg = np.stack(avg) + avg = np.mean(avg, axis=0) + dd[k].append(avg) + + dd = {k: np.stack(v) for k, v in dd.items()} + tavg = Trajectory(**dd, time=tlong.time, env_info=[]) + return tavg + + # tlong.state *= 0 + # tlong.action *= 0 + + # for i in range(n): + + +def experiment_load(experiment_name, exclude_empty=True): + files = list(filter(os.path.isdir, glob.glob(experiment_name + "/*"))) + if exclude_empty: + files = [f for f in files if + os.path.exists(os.path.join(f, "log.txt")) and os.stat(os.path.join(f, "log.txt")).st_size > 0] + if len(files) == 0: + return [] + values = [] + files = sorted(files, key=lambda file: os.path.basename(file)) + for recent in files: + # recent = sorted(files, key=lambda file: os.path.basename(file))[-1] + stats = [] + with open(recent + '/log.txt', 'r') as f: + csv_reader = csv.reader(f, delimiter='\t') + for i, row in enumerate(csv_reader): + if i == 0: + head = row + else: + def tofloat(v): + try: + return float(v) + except Exception: + return v + + stats.append({k: tofloat(v) for k, v in zip(head, row)}) + + from irlc import cache_read, cache_write, cache_exists + tpath = recent + "/trajectories.pkl" + if cache_exists(tpath): + trajectories = cache_read(tpath) + else: + trajectories = None + values.append( (stats, trajectories, recent) ) + return values + +def log_time_series(experiment, list_obs, max_xticks_to_log=None, run_name=None): + logdir = f"{experiment}/" + + if max_xticks_to_log is not None and len(list_obs) > max_xticks_to_log: + I = np.round(np.linspace(0, len(list_obs) - 1, max_xticks_to_log)) + list_obs = [o for i, o in enumerate(list_obs) if i in I.astype(np.int).tolist()] + + akeys = list(list_obs[0].keys()) + akeys += [k for k in list_obs[-1].keys() if k not in akeys] + with LazyLog(logdir) as logz: + for n,l in enumerate(list_obs): + for k in akeys: + v = None + if k not in l: + for ll in list_obs[n:]: + if k in ll: + v = ll[k] + break + if v is None: + v = np.nan + else: + v = l.get(k) + logz.log_tabular(k,v) + if "Steps" not in l: + logz.log_tabular("Steps", n) + if "Episode" not in l: + logz.log_tabular("Episode",n) + logz.dump_tabular(verbose=False) + experiment_name = logz.experiment_name + return experiment_name + + +class DiscreteTextActionSpace(spaces.Space): + def __init__(self, actions, seed=None): + # self.env = env + # self._actions = actions + self.actions = actions + self.ds = spaces.Discrete(seed=seed, n=len(actions)) + # self.start = 0 + # self.actions = actions + # super().__init__(shape=(len(actions),)) + + # @property + # def actions(self): + # return self._actions + # return self.env.A(self.env.state) + + def sample(self, mask=None): + return self.actions[self.ds.sample(mask)] + + @property + def n(self): + return self.ds.n + + def _make_mask(self, actions): + mask = np.zeros((self.n,), dtype=np.int8) + for a in actions: + mask[self.actions.index(a)] = 1 + return mask + + def __str__(self): + return f"<ExplicitAction space with actions: {', '.join(self.actions)}>" + + # def __contains__(self, action): + # return + + +class ExplicitActionSpace(spaces.Discrete): + # Hacky stuff I don't think I need anymore. + + def __init__(self, env): + self.env = env + self.start = 0 + raise Exception() + # pass + # self.actions = actions + # super().__init__(len(actions)) + + @property + def actions(self): + return self.env.A(self.env.state) + + @property + def n(self): + return len(self.actions) + + def sample(self): + return np.random.choice(self.actions) diff --git a/irlc/utils/graphics/car.png b/irlc/utils/graphics/car.png new file mode 100644 index 0000000000000000000000000000000000000000..386a86e58b77fe213f662d638df86609c5294be2 Binary files /dev/null and b/irlc/utils/graphics/car.png differ diff --git a/irlc/utils/graphics/dtu_icon.png b/irlc/utils/graphics/dtu_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..9bcea902ea9d3e647d7b73e3a90dbc194dfdfd8b Binary files /dev/null and b/irlc/utils/graphics/dtu_icon.png differ diff --git a/irlc/utils/graphics/locomotive.png b/irlc/utils/graphics/locomotive.png new file mode 100644 index 0000000000000000000000000000000000000000..95e5dc6682930e7ecff714937e2f021e5c26b98d Binary files /dev/null and b/irlc/utils/graphics/locomotive.png differ diff --git a/irlc/utils/graphics_util_pygame.py b/irlc/utils/graphics_util_pygame.py new file mode 100644 index 0000000000000000000000000000000000000000..379244046b13dd3d776b366074d5ef77bd29418e --- /dev/null +++ b/irlc/utils/graphics_util_pygame.py @@ -0,0 +1,415 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# graphicsUtils.py +# ---------------- +# Licensing Information: You are free to use or extend these projects for +# educational purposes provided that (1) you do not distribute or publish +# solutions, (2) you retain this notice, and (3) you provide clear +# attribution to UC Berkeley, including a link to http://ai.berkeley.edu. +# +# Attribution Information: The Pacman AI projects were developed at UC Berkeley. +# The core projects and autograders were primarily created by John DeNero +# (denero@cs.berkeley.edu) and Dan Klein (klein@cs.berkeley.edu). +# Student side autograding was added by Brad Miller, Nick Hay, and +# Pieter Abbeel (pabbeel@cs.berkeley.edu). +import numpy as np +import os +import pygame +from pygame import gfxdraw +import threading +import time +import pygame +import platform +import sys + +ghost_shape = [ + (0, - 0.5), + (0.25, - 0.75), + (0.5, - 0.5), + (0.75, - 0.75), + (0.75, 0.5), + (0.5, 0.75), + (- 0.5, 0.75), + (- 0.75, 0.5), + (- 0.75, - 0.75), + (- 0.5, - 0.5), + (- 0.25, - 0.75) +] + +def _adjust_coords(coord_list, x, y): + for i in range(0, len(coord_list), 2): + coord_list[i] = coord_list[i] + x + coord_list[i + 1] = coord_list[i + 1] + y + return coord_list + +def formatColor(r, g, b): + return '#%02x%02x%02x' % (int(r * 255), int(g * 255), int(b * 255)) + +def colorToVector(color): + return list(map(lambda x: int(x, 16) / 256.0, [color[1:3], color[3:5], color[5:7]])) + +def h2rgb(color): + if color is None or isinstance(color, tuple): + return color + if color.startswith("#"): + color = color[1:] + return tuple(int(color[i:i + 2], 16) / 255 for i in (0, 2, 4)) + +def h2rgb255(color): + if isinstance(color, tuple): + return color + # c = + return tuple(int(cc*255) for cc in h2rgb(color)) + if color is None: + return None + if color.startswith("#"): + color = color[1:] + return tuple(int(color[i:i + 2], 16) / 255 for i in (0, 2, 4)) + +class GraphicsCache: + break_cache = False + def __init__(self, viewer, verbose=False): + self.viewer = viewer + # self._items_in_viewer = {} + # self._seen_things = set() + self.clear() + self.verbose = verbose + + def copy_all(self): + self._seen_things.update( set( self._items_in_viewer.keys() ) ) + + def clear(self): + self._seen_things = set() + self.viewer.geoms.clear() + self._items_in_viewer = {} + + def prune_frame(self): + s0 = len(self._items_in_viewer) + self._items_in_viewer = {k: v for k, v in self._items_in_viewer.items() if k in self._seen_things } + if self.verbose: + print("removed", len(self._items_in_viewer) - s0, "geom size", len(self._items_in_viewer)) + self.viewer.geoms = list( self._items_in_viewer.values() ) + self._seen_things = set() + + + def add_geometry(self, name, geom): + if self.break_cache: + if self._items_in_viewer == None: + self.viewer.geoms = [] + self._items_in_viewer = {} + + self._items_in_viewer[name] = geom + self._seen_things.add(name) + + + +class GraphicsUtilGym: + viewer = None + _canvas_xs = None # Size of canvas object + _canvas_ys = None + _canvas_x = None # Current position on canvas + _canvas_y = None + + def begin_graphics(self, width=640, height=480, color=formatColor(0, 0, 0), title="02465 environment", local_xmin_xmax_ymin_ymax=None, verbose=False, + frames_per_second=None): + """ Main interface for managing graphics. + The local_xmin_xmax_ymin_ymax controls the (local) coordinate system which is mapped onto screen coordinates. I.e. specify this + to work in a native x/y coordinate system. If not, it will default to screen coordinates familiar from Gridworld. + """ + width = int(width) + height = int(height) # For width/height to be integers to avoid crashes on some systems. + + icon = os.path.dirname(__file__) + "/../utils/graphics/dtu_icon.png" + pygame_icon = pygame.image.load(icon) + pygame.display.set_icon(pygame_icon) + screen_width = width + screen_height = height + pygame.init() + pygame.display.init() + self.frames_per_second = frames_per_second + + + self.screen = pygame.display.set_mode( + (screen_width, screen_height) + ) + self.screen_width = width + self.screen_height = height + + pygame.display.set_caption(title) + + if height % 2 == 1: + height += 1 # Must be divisible by 2. + self._bg_color = color + # viewer = Viewer(width=int(width), height=int(height)) + # viewer.window.set_caption(title) + # self.viewer = viewer + # self.gc = GraphicsCache(viewer, verbose=verbose) + self._canvas_xs, self._canvas_ys = width - 1, height - 1 + self._canvas_x, self._canvas_y = 0, self._canvas_ys + if local_xmin_xmax_ymin_ymax is None: + # local_coordinates = [] + # This will align the coordinate system so it begins in the top-left corner. + # This is the default behavior of pygame. + local_xmin_xmax_ymin_ymax = (0, width, 0, height) + self._local_xmin_xmax_ymin_ymax = local_xmin_xmax_ymin_ymax + + self.demand_termination = threading.Event() + self.pause_refresh = False + self.ask_for_pause = False + self.is_paused = False + self.time_last_blit = -1 + + + def refresh_window(gutils): + refresh_interval_seconds = 0.1 # Miliseconds + t0 = time.time() + while not gutils.demand_termination.is_set(): + t1 = time.time() + if t1 - t0 > refresh_interval_seconds: + if not self.ask_for_pause: + self.is_paused = False + if not (sys.platform == 'darwin' and platform.processor() == 'i386'): + pass # Disable the thread startup. This causes problems on linux (segfaults). Must find better fix, perhaps win-only. + # pygame.display.update() + else: + self.is_paused = True + t0 = t1 + time.sleep(refresh_interval_seconds/100) + + self.refresh_thread = threading.Thread(target=refresh_window, args=(self, )) + self.refresh_thread.start() + + def close(self): + self.demand_termination.set() + self.refresh_thread.join(timeout=1000) + pygame.display.quit() + pygame.quit() + # TH 2023: These two lines are super important. + # pdraw cache the fonts. So when pygame is loaded/quites, + # the font cache is not flushed. This is not a problem + # when determining the width of strings the font has seen, + # but causes a segfault with NEW strings. + from irlc.utils import ptext + ptext._font_cache = {} + self.isopen = False + + def render(self): + pass + + def blit(self, render_mode=None): + self.render() + self.screen.blit(self.surf, (0, 0)) + if render_mode == "human": + tc = time.time() + + if self.frames_per_second is not None: + + if tc - self.time_last_blit < 1/self.frames_per_second: + tw = 1/self.frames_per_second - (tc - self.time_last_blit ) + time.sleep(tw) + else: + tw = 0 + + self.time_last_blit = tc + + pygame.event.pump() + pygame.display.flip() + elif render_mode == "rgb_array": + return np.transpose(np.array(pygame.surfarray.pixels3d(self.screen)), axes=(1, 0, 2)) + + def rectangle(self, color, x, y, width, height, border=0, fill_color=None): + x2,y2 = self.fixxy((x+width, y+height)) + x, y = self.fixxy((x,y)) + + c1 = min([x, x2]) + c2 = min([y, y2]) + + w = abs(x-x2) + h = abs(y - y2) + + pygame.draw.rect(self.surf, color, pygame.Rect( int(c1), int(c2), int(w), int(h)), border) + + + def draw_background(self, background_color=None): + if background_color is None: + background_color = (0, 0, 0) + self._bg_color = background_color + x1, x2, y1, y2 = self._local_xmin_xmax_ymin_ymax + corners = [ (x1, y1), (x2, y1), (x2, y2), (x1, y2) ] + self.surf = pygame.Surface((self.screen_width, self.screen_height)) + self.polygon(name="background", coords=corners, outlineColor=self._bg_color, fillColor=self._bg_color, filled=True, smoothed=False) + + def fixxy(self, xy): + x,y = xy + x = (x - self._local_xmin_xmax_ymin_ymax[0]) / (self._local_xmin_xmax_ymin_ymax[1] - self._local_xmin_xmax_ymin_ymax[0]) * self.screen.get_width() + y = (y - self._local_xmin_xmax_ymin_ymax[2]) / (self._local_xmin_xmax_ymin_ymax[3] - self._local_xmin_xmax_ymin_ymax[2]) * self.screen.get_height() + return int(x), int(y) + + + def plot(self, name, x, y, color=None, width=1.0): + coords = [(x_,y_) for (x_, y_) in zip(x,y)] + if color is None: + color = "#000000" + return self.polygon(name, coords, outlineColor=color, filled=False, width=width) + + def polygon(self, name, coords, outlineColor=None, fillColor=None, filled=True, smoothed=1, behind=0, width=1.0, closed=False): + c = [] + for coord in coords: + c.append(coord[0]) + c.append(coord[1]) + + coords = [self.fixxy(c) for c in coords] + if fillColor == None: fillColor = outlineColor + poly = None + if not filled: fillColor = "" + + c = [self.fixxy(tuple(c[i:i+2])) for i in range(0, len(c), 2)] + if not filled: + gfxdraw.polygon(self.surf, coords, h2rgb255(outlineColor)) + pygame.draw.polygon(self.surf, h2rgb255(outlineColor), coords, width=int(width)) + + else: + gfxdraw.filled_polygon(self.surf, coords, h2rgb255(fillColor)) + + if outlineColor is not None and len(outlineColor) > 0 and filled: # Not sure why this cannot be merged with the filled case... + # gfxdraw.polygon(self.surf, coords, h2rgb255(outlineColor), width=int(width)) + pygame.draw.polygon(self.surf, h2rgb255(outlineColor), coords, width=int(width)) + + return poly + + def square(self, name, pos, r, color, filled=1, behind=0): + x, y = pos + coords = [(x - r, y - r), (x + r, y - r), (x + r, y + r), (x - r, y + r)] + return self.polygon(name, coords, color, color, filled, 0, behind=behind) + + def centered_arc(self, color, pos, r, start_angle, stop_angle, width=1): + # Draw a centered arc (pygame defaults to boxed arcs) + x, y = pos + tt = np.linspace(start_angle / 360 * 2 * np.pi,stop_angle / 360 * 2 * np.pi, int(r * 10)) + px = np.cos(tt) * r + py = -np.sin(tt) * r + pp = list(zip(px.tolist(), py.tolist())) + + pp = [((x + a, y + b)) for (a, b) in pp] + # if style == 'arc': # For pacman. I guess this one makes the rounded wall segments. + pp = [self.fixxy(p_) for p_ in pp] + + pygame.draw.lines(self.surf, h2rgb255(color), False, pp, width) + + def circle(self, name, pos, r, outlineColor=None, fillColor=None, endpoints=None, style='pieslice', width=2): + pos = self.fixxy(pos) + x, y = pos + if endpoints == None: + e = [0, 359] + else: + e = list(endpoints) + while e[0] > e[1]: e[1] = e[1] + 360 + if endpoints is not None and len(endpoints) > 0: + tt = np.linspace(e[0]/360 * 2*np.pi, e[-1]/360 * 2*np.pi, int(r*20) ) + px = np.cos(tt) * r + py = -np.sin(tt) * r + pp = list(zip(px.tolist(), py.tolist())) + if style == 'pieslice': + pp = [(0,0),] + pp + [(0,0),] + pp = [( (x+a, y+b)) for (a,b) in pp ] + if style == 'arc': # For pacman. I guess this one makes the rounded wall segments. + pp = [self.fixxy(p_) for p_ in pp] + pygame.draw.lines(self.surf, outlineColor, False, pp, width) + elif style == 'pieslice': + self.polygon(name, pp, fillColor=fillColor, outlineColor=outlineColor, width=width) + else: + raise Exception("bad style", style) + else: + gfxdraw.filled_circle(self.surf, x, y, int(r), h2rgb255(fillColor)) + + def text(self, name, pos, color, contents, font='Helvetica', size=12, style='normal', anchor="w", fontsize=24, + bold=False): + pos = self.fixxy(pos) + ax = "center" + ax = "left" if anchor == "w" else ax + ay = "center" + ay = "baseline" if anchor == "s" else ay + + from irlc.utils.ptext import draw + if anchor == 'w': + opts = dict(midleft=pos) + elif anchor == 'e': + opts = dict(midright=pos) + elif anchor == 's': + opts = dict(midbottom=pos) + elif anchor == 'n': + opts = dict(midtop=pos) + elif anchor == 'c': + opts = dict(center=pos) + else: + raise Exception("Unknown anchor", anchor) + opts['fontsize'] = fontsize + opts['bold'] = bold + draw(contents, surf=self.surf, color=h2rgb255(color), pos=pos, **opts) + return + + + def line(self, name, here, there, color=formatColor(0, 0, 0), width=2): + + here, there = self.fixxy(here), self.fixxy(there) + pygame.draw.line(self.surf, h2rgb255(color), here, there, width) + + def polyline(self, name, xs, ys, color=formatColor(0, 0, 0), width=2): + for i in range(len(xs) - 1): + self.line("asfasf", here=(xs[i] , ys[i]), + there=(xs[i + 1], ys[i + 1]), + color=color, width=width) + + +def rotate_around(pos, xy0, angle): + if isinstance(pos, list) and isinstance(pos[0], tuple): + return [rotate_around(p, xy0, angle) for p in pos] + return ((pos[0] - xy0[0]) * np.cos(angle / 180 * np.pi) - (pos[1] - xy0[1]) * np.sin(angle / 180 * np.pi) + xy0[0], + (pos[0] - xy0[0]) * np.sin(angle / 180 * np.pi) + (pos[1] - xy0[1]) * np.cos(angle / 180 * np.pi) + xy0[1]) + +class Object(pygame.sprite.Sprite): + def __init__(self, file, image_width=None, graphics=None): + super(Object, self).__init__() + fpath = os.path.dirname(__file__) +"/graphics/"+file + image = pygame.image.load(fpath).convert_alpha() + if image_width is not None: + image_height = int( image_width / image.get_width() * image.get_height() ) + self.og_surf = pygame.transform.smoothscale(image, (image_width, image_height)) + # raise Exception("Implement this") + else: + self.og_surf = image + # self.og_surf = pygame.transform.smoothscale(image, (100, 100)) + self.surf = self.og_surf + self.rect = self.surf.get_rect(center=(400, 400)) + self.ga = graphics + + def move_center_to_xy(self, x, y): + # Note: These are in the local coordinate system coordinates. + x,y = self.ga.fixxy((x,y)) + self.rect.center = (x,y) + + def rotate(self, angle): + """ Rotate sprite around it's center. """ + self.angle = angle + self.surf = pygame.transform.rotate(self.og_surf, self.angle) + self.rect = self.surf.get_rect(center=self.rect.center) + + def blit(self, surf): + surf.blit(self.surf, self.rect) + + +class UpgradedGraphicsUtil(GraphicsUtilGym): + def __init__(self, screen_width=800, screen_height=None, xmin=0., xmax=800., ymin=0., ymax=600., title="Gym window"): + if screen_height is None: + screen_height = np.abs( int(screen_width / (xmax - xmin) * (ymax-ymin)) ) + elif xmin is None: + xmin = 0 + xmax = screen_width + ymin = 0 + ymax = screen_height + else: + raise Exception() + self.begin_graphics(width=screen_width, height=screen_height, local_xmin_xmax_ymin_ymax=(xmin, xmax, ymin, ymax), title=title) + + def get_sprite(self, name): + """ Load a sprite from the graphics directory. """ + pass diff --git a/irlc/utils/irlc_plot.py b/irlc/utils/irlc_plot.py new file mode 100644 index 0000000000000000000000000000000000000000..fcbb498578a9b67468f0b86c9be1bb402d114dca --- /dev/null +++ b/irlc/utils/irlc_plot.py @@ -0,0 +1,266 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import os +import numpy as np + +""" +Using the plotter: + +Call it from the command line, and supply it with logdirs to experiments. +Suppose you ran an experiment with name 'test', and you ran 'test' for 10 +random seeds. The runner code stored it in the directory structure + + data + L test_EnvName_DateTime + L 0 + L log.txt + L params.json + L 1 + L log.txt + L params.json + . + . + . + L 9 + L log.txt + L params.json + +To plot learning curves from the experiment, averaged over all random +seeds, call + + python lmpc_plot.py data/test_EnvName_DateTime --value AverageReturn + +and voila. To see a different statistics, change what you put in for +the keyword --value. You can also enter /multiple/ values, and it will +make all of them in order. + + +Suppose you ran two experiments: 'test1' and 'test2'. In 'test2' you tried +a different set of hyperparameters from 'test1', and now you would like +to compare them -- see their learning curves side-by-side. Just call + + python lmpc_plot.py data/test1 data/test2 + +and it will plot them both! They will be given titles in the legend according +to their exp_name parameters. If you want to use custom legend titles, use +the --legend flag and then provide a title for each logdir. + +""" + +def plot_data(data, y="accumulated_reward", x="Episode", ci=95, estimator='mean', **kwargs): + import seaborn as sns + import matplotlib.pyplot as plt + import pandas as pd + if isinstance(data, list): # is this correct even? + data = pd.concat(data, ignore_index=True,axis=0) + plt.figure(figsize=(12, 6)) + sns.set(style="darkgrid", font_scale=1.5) + lp = sns.lineplot(data=data, x=x, y=y, hue="Condition", errorbar=('ci', 95), estimator=estimator, **kwargs) + plt.legend(loc='best') #.set_draggable(True) + +def existing_runs(experiment): + nex = 0 + for root, dir, files in os.walk(experiment): + if 'log.txt' in files: + nex += 1 + return nex + +def _get_most_recent_log_dir(fpath): + files = [os.path.basename(root) for root, dir, files in os.walk(fpath) if 'log.txt' in files] + return sorted(files, key=lambda file: os.path.basename(file))[-1] if len(files) > 0 else None + +def get_datasets(fpath, x, condition=None, smoothing_window=None, resample_key=None, resample_ticks=None, only_most_recent=False): + import pandas as pd + unit = 0 + if condition is None: + condition = fpath + datasets = [] + + if only_most_recent: + most_recent = _get_most_recent_log_dir(fpath) + + for root, dir, files in os.walk(fpath): + # print(files) + if 'log.txt' in files: + if only_most_recent and most_recent is not None and os.path.basename(root) != most_recent: # Skip this log. + continue + json = os.path.join(root, 'params.json') + if os.path.exists(json): + with open(json) as f: + param_path = open(json) + params = json.load(param_path) + # exp_name = params['exp_name'] + + log_path = os.path.join(root, 'log.txt') + if os.stat(log_path).st_size == 0: + print("Bad plot file", log_path, "size is zero. Skipping") + continue + experiment_data = pd.read_table(log_path) + + if smoothing_window: + ed_x = experiment_data[x] + experiment_data = experiment_data.rolling(smoothing_window,min_periods=1).mean() + experiment_data[x] = ed_x + + experiment_data.insert( + len(experiment_data.columns), + 'Unit', + unit + ) + experiment_data.insert( + len(experiment_data.columns), + 'Condition', + condition) + + datasets.append(experiment_data) + unit += 1 + + nc = f"({unit}x)"+condition[condition.rfind("/")+1:] + for i, d in enumerate(datasets): + datasets[i] = d.assign(Condition=lambda x: nc) + + if resample_key is not None: + nmax = 0 + vmax = -np.inf + vmin = np.inf + for d in datasets: + nmax = max( d.shape[0], nmax) + vmax = max(d[resample_key].max(), vmax) + vmin = min(d[resample_key].min(), vmin) + if resample_ticks is not None: + nmax = min(resample_ticks, nmax) + + new_datasets = [] + tnew = np.linspace(vmin + 1e-6, vmax - 1e-6, nmax) + for d in datasets: + nd = {} + cols = d.columns.tolist() + for c in cols: + if c == resample_key: + y = tnew + elif d[c].dtype == 'O': + y = [ d[c][0] ] * len(tnew) + else: + y = np.interp(tnew, d[resample_key].tolist(), d[c], left=np.nan, right=np.nan) + y = y.astype(d[c].dtype) + nd[c] = y + + ndata = pd.DataFrame(nd) + ndata = ndata.dropna() + new_datasets.append(ndata) + datasets = new_datasets + return datasets + + +def _load_data(experiments, legends=None, smoothing_window=None, resample_ticks=None, + x_key="Episode", + only_most_recent=False): + ensure_list = lambda x: x if isinstance(x, list) else [x] + experiments = ensure_list(experiments) + if legends is None: + legends = experiments + legends = ensure_list(legends) + + data = [] + for logdir, legend_title in zip(experiments, legends): + resample_key = x_key if resample_ticks is not None else None + data += get_datasets(logdir, x=x_key, condition=legend_title, smoothing_window=smoothing_window, resample_key=resample_key, resample_ticks=resample_ticks, + only_most_recent=only_most_recent) + return data + +def main_plot(experiments, legends=None, smoothing_window=None, resample_ticks=None, + x_key="Episode", + y_key='Accumulated Reward', + no_shading=False, + **kwargs): + if no_shading: + kwargs['units'] = 'Unit' + kwargs['estimator'] = None + + ensure_list = lambda x: x if isinstance(x, list) else [x] + experiments = ensure_list(experiments) + + if legends is None: + legends = experiments + legends = ensure_list(legends) + + data = [] + for logdir, legend_title in zip(experiments, legends): + resample_key = x_key if resample_ticks is not None else None + data += get_datasets(logdir, x=x_key, condition=legend_title, smoothing_window=smoothing_window, resample_key=resample_key, resample_ticks=resample_ticks) + + plot_data(data, y=y_key, x=x_key, **kwargs) + + +def main(): + import argparse + parser = argparse.ArgumentParser() + parser.add_argument('logdir', nargs='*') + parser.add_argument('--legend', nargs='*') + parser.add_argument('--value', default='AverageReturn', nargs='*') + parser.add_argument('--title', default="please specify title", help="The title to show") + parser.add_argument('--pdf_name', default=None, help="Name of pdf") + + args = parser.parse_args() + main_plot(args.logdir, args.legend, args.value, title=args.title) + +if __name__ == "__main__": + main() + + +#### TRAJECTORY PLOTTING HERE #### +def plot_trajectory(trajectory, env=None, xkeys=None, ukeys=None): + """ + Used to visualize trajectories returned from the :func:`~irlc.ex01.agent.train`-function. An example: + + .. plot:: + :include-source: + + import matplotlib.pyplot as plt + import numpy as np + from irlc import Agent, plot_trajectory, train + from irlc.ex04.model_pendulum import GymSinCosPendulumEnvironment + env = GymSinCosPendulumEnvironment() + stats, trajectories = train(env, Agent(env), num_episodes=1, return_trajectory=True) + plot_trajectory(trajectories[0], env) + + Labels will be derived from the ``env`` if supplied. The parameters ``xkeys`` and ``ukeys`` can be used to limit which + coordinates are plotted. For instance, if you only want to plot the first two x-coordinates you can set ``xkeys=[0,1]``: + + + .. plot:: + + import matplotlib.pyplot as plt + import numpy as np + from irlc import Agent, plot_trajectory, train + from irlc.ex04.model_pendulum import GymSinCosPendulumEnvironment + env = GymSinCosPendulumEnvironment() + stats, trajectories = train(env, Agent(env), num_episodes=1, return_trajectory=True) + plot_trajectory(trajectories[0], env, xkeys=[0,1], ukeys=[]) + + :param trajectory: A single trajectory computed using ``train`` (see example above) + :param env: A gym control environment (optional) + :param xkeys: List of integers corresponding to the coordinates of :math:`x` we wish to plot + :param ukeys: List of integers corresponding to the coordinates of :math:`u` we wish to plot + + .. tip:: + If the plot does not show, you might want to import matplotlib as ``import matplotlib.pyplot as plt`` and call ``plt.show()`` + """ + if xkeys is None: + xkeys = [i for i in range(trajectory.state.shape[1])] + if ukeys is None: # all + ukeys = [i for i in range(trajectory.action.shape[-1])] + import seaborn as sns + import matplotlib.pyplot as plt + plt.figure(figsize=(12, 6)) + sns.set(style="darkgrid", font_scale=1.5) + def fp(time, X, keys, labels): + for i, k in enumerate(keys): + label = labels[k] if labels is not None else None + sns.lineplot(x=time, y=X[:,k], label=label) + + time = trajectory.time.squeeze() + fp(time, trajectory.state, xkeys, labels=env.state_labels if env is not None else None) + fp(time[:-1], trajectory.action, ukeys, labels=env.action_labels if env is not None else None) + plt.xlabel("Time / seconds") + if env is not None: + plt.legend() diff --git a/irlc/utils/lazylog.py b/irlc/utils/lazylog.py new file mode 100644 index 0000000000000000000000000000000000000000..8b1fdb8c87320192abd2d022df45a2c37b8bfaf4 --- /dev/null +++ b/irlc/utils/lazylog.py @@ -0,0 +1,140 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +""" +Inspired by logz from berkleys deep RL course but re-written as a context manager like God intended. + +To load the learning curves, you can do, for yafcport + +A = np.genfromtxt('/tmp/expt_1468984536/log.txt',delimiter='\t',dtype=None, names=True) +A['EpRewMean'] + +""" +import json +import os +import time +from datetime import datetime + +color2num = dict( + gray=30, + red=31, + green=32, + yellow=33, + blue=34, + magenta=35, + cyan=36, + white=37, + crimson=38) + + +def colorize(string, color, bold=False, highlight=False): + attr = [] + num = color2num[color] + if highlight: num += 10 + attr.append(str(num)) + if bold: attr.append('1') + return '\x1b[%sm%s\x1b[0m' % (';'.join(attr), string) + + +class LazyLog(object): + output_dir = None + output_file = None + first_row = True + log_headers = [] + log_current_row = {} + + def __init__(self, experiment_name, run_name=None, data=None): + if run_name is None: + experiment_name += "/"+ datetime.utcnow().strftime("%Y-%m-%d_%H-%M-%S.%f")[:-3] + else: + experiment_name += "/" + run_name + self.experiment_name = experiment_name + configure_output_dir(self, experiment_name) + if data is not None: + self.save_params(data) + + def __enter__(self): + return self + + def save_params(self, data): + save_params(self, data) + + def dump_tabular(self, verbose=False): + dump_tabular(self, verbose) + + def log_tabular(self, key, value): + log_tabular(self, key, value) + + def __exit__(self, type, value, traceback): + self.output_file.close() + + +def configure_output_dir(G, d=None): + """ + Set output directory to d, or to /tmp/somerandomnumber if d is None + """ + # CDIR = os.path.dirname(os.path.realpath(__file__)).replace('\\', '/') + G.first_row = True + G.output_dir = d or "/tmp/experiments/%i" % int(time.time()) + assert not os.path.exists( + G.output_dir), "Log dir %s already exists! Delete it first or use a different dir" % G.output_dir + os.makedirs(G.output_dir) + G.output_file = open(os.path.join(G.output_dir, "log.txt"), 'w') + print(colorize("Logging data to %s" % G.output_file.name, 'green', bold=True)) + +def log_tabular(G, key, val): + """ + Log a value of some diagnostic + Call this once for each diagnostic quantity, each iteration + """ + if G.first_row: + G.log_headers.append(key) + else: + assert key in G.log_headers, "Trying to introduce a new key %s that you didn't include in the first iteration" % key + assert key not in G.log_current_row, "You already set %s this iteration. Maybe you forgot to call dump_tabular()" % key + G.log_current_row[key] = val + + +def save_params(G, params): + with open(os.path.join(G.output_dir, "params.json"), 'w') as out: + out.write(json.dumps(params, separators=(',\n', '\t:\t'), sort_keys=True)) + + +# def pickle_tf_vars(): +# import tensorflow as tf +# """ +# Saves tensorflow variables +# Requires them to be initialized first, also a default session must exist +# """ +# _dict = {v.name: v.eval() for v in tf.global_variables()} +# with open(osp.join(G.output_dir, "vars.pkl"), 'wb') as f: +# pickle.dump(_dict, f) + + +def dump_tabular(G, verbose=True): + """ + Write all of the diagnostics from the current iteration + """ + vals = [] + key_lens = [len(key) for key in G.log_headers] + max_key_len = max(15, max(key_lens)) + keystr = '%' + '%d' % max_key_len + fmt = "| " + keystr + "s | %15s |" + n_slashes = 22 + max_key_len + print("-" * n_slashes) if verbose else None + for key in G.log_headers: + val = G.log_current_row.get(key, "") + if hasattr(val, "__float__"): + valstr = "%8.3g" % val + else: + valstr = val + print(fmt % (key, valstr)) if verbose else None + vals.append(val) + print("-" * n_slashes) if verbose else None + if G.output_file is not None: + if G.first_row: + G.output_file.write("\t".join(G.log_headers)) + G.output_file.write("\n") + G.output_file.write("\t".join(map(str, vals))) + G.output_file.write("\n") + G.output_file.flush() + G.log_current_row.clear() + G.first_row = False diff --git a/irlc/utils/minigrid.py b/irlc/utils/minigrid.py new file mode 100644 index 0000000000000000000000000000000000000000..3498ea1fcfbd85f401caa416d2de9db3b8e9e74e --- /dev/null +++ b/irlc/utils/minigrid.py @@ -0,0 +1,102 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +import gymnasium as gym +from gymnasium.spaces.discrete import Discrete +from minigrid.core.constants import OBJECT_TO_IDX, COLOR_TO_IDX +from minigrid.wrappers import FullyObsWrapper +import numpy as np + + +class ProjectObservationSpaceWrapper(gym.core.ObservationWrapper): + """ + Use the image as the only observation output, no language/mission. + """ + def __init__(self, env, dims): + super().__init__(env) + os = self.observation_space.spaces['image'] + # if dims is not None: + os.high = os.high[:,:,dims] + os.low = os.low[:,:,dims] + + self.observation_space.spaces['image'] = os + self.dims = dims + + def observation(self, obs): + obs['image'] = obs['image'][:, :, self.dims] + return obs + + +class SaneBoundsWrapper(gym.core.ObservationWrapper): + """ + Use the image as the only observation output, no language/mission. + """ + def __init__(self, env): + super().__init__(env) + os = self.observation_space.spaces['image'] + os.high[:, :, 0] = max(OBJECT_TO_IDX.values()) + if os.high.shape[2] >= 2: + os.high[:, :, 1] = max(COLOR_TO_IDX.values()) + if os.high.shape[2] >= 3: + os.high[:, :, 2] = 3 + self.observation_space.spaces['image'] = os + + def observation(self, obs): + return obs + +class HashableImgObsWrapper(gym.core.ObservationWrapper): + """ + Use the image as the only observation output, no language/mission. + """ + + def __init__(self, env,dims=None): + super().__init__(env) + self.observation_space = env.observation_space.spaces['image'] + + def observation(self, obs): + # ls = obs['image'].flat.tolist() + return tuple( obs['image'].flat ) + # return obs['image'] + + +class LinearSpaceWrapper(gym.core.ObservationWrapper): + """ + Fully observable gridworld using a compact grid encoding + """ + def __init__(self, env): + super().__init__(env) + sz = self.observation_space.spaces['image'].shape + npo = np.zeros( sz, dtype=np.object) + for i in range(sz[0]): + for j in range(sz[1]): + for k in range(sz[2]): + if k == 0: + n = max(OBJECT_TO_IDX.values())+1 + elif k == 1: + n = max(COLOR_TO_IDX.values())+1 + elif k == 2: + n = 4 + else: + raise Exception("Bad k") + + npo[i,j,k] = Discrete(n) + ospace = tuple(npo.flat) + + sz = np.cumsum([o.n for o in ospace]) + sz = sz - sz[0] + self.sz = sz + # from gym.spaces.box import Box + self.observation_space = ospace + + def observation(self, obs): + s = obs['image'].reshape((obs['image'].size,)) + return s + + +if __name__ == "__main__": + """ Example use: """ + env = gym.make("MiniGrid-Empty-5x5-v0") + env = FullyObsWrapper(env) # use this + env = LinearSpaceWrapper(env) + s = env.reset() + print(s) + # Use with for instance: + # agent = LinearSemiGradSarsa(env, gamma=1, epsilon=0.1, alpha=0.5) diff --git a/irlc/utils/player_wrapper.py b/irlc/utils/player_wrapper.py new file mode 100644 index 0000000000000000000000000000000000000000..2d6a0b35c88d17e43e3f6ff15ea22ad99caf7939 --- /dev/null +++ b/irlc/utils/player_wrapper.py @@ -0,0 +1,370 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from gymnasium import logger +from irlc.ex01.agent import Agent +import time +import sys +import gymnasium as gym +import os + +try: + # Imports that may not be availble: + # Using this backend apparently clash with scientific mode. Not sure why it was there in the first place so + # disabling it for now. + # matplotlib.use('TkAgg') + import matplotlib.pyplot as plt + import pygame +except ImportError as e: + logger.warn('failed to set matplotlib backend, plotting will not work: %s' % str(e)) + plt = None + + +class AgentWrapper(Agent): + """Wraps the environment to allow a modular transformation. + + This class is the base class for all wrappers. The subclass could override + some methods to change the behavior of the original environment without touching the + original code. + + .. note:: + + Don't forget to call ``super().__init__(env)`` if the subclass overrides :meth:`__init__`. + + """ + def __init__(self, agent, env): + # print("AgentWrapper is deprecated. ") + self.agent = agent + self.env = env + + def __getattr__(self, name): + if name.startswith('_'): + raise AttributeError("attempted to get missing private attribute '{}'".format(name)) + return getattr(self.agent, name) + + @classmethod + def class_name(cls): + return cls.__name__ + + def pi(self, state, k, info=None): + return self.agent.pi(state, k, info) + # return self.env.step(action) + + def train(self, *args, **kwargs): + return self.agent.train(*args, **kwargs) + + def __str__(self): + return '<{}{}>'.format(type(self).__name__, self.agent) + + def __repr__(self): + return str(self) + + @property + def unwrapped(self): + return self.agent.unwrapped + +PAUSE_KEY = ord('p') +SPACEBAR = "_SPACE_BAR_PRESSED_" +class PlayWrapperPygame(AgentWrapper): + def __init__(self, agent : Agent, env : gym.Env, keys_to_action=None, autoplay=False): + super().__init__(agent, env) + if keys_to_action is None: + if hasattr(env, 'get_keys_to_action'): + keys_to_action = env.get_keys_to_action() + elif hasattr(env.env, 'get_keys_to_action'): + keys_to_action = env.env.get_keys_to_action() + elif hasattr(env.unwrapped, 'get_keys_to_action'): + keys_to_action = env.unwrapped.get_keys_to_action() + else: + print(env.spec.id +" does not have explicit key to action mapping, please specify one manually") + assert False, env.spec.id + " does not have explicit key to action mapping, " + \ + "please specify one manually" + # keys_to_action = dict() + self.keys_to_action = keys_to_action + self.env = env + self.human_wants_restart = False + self.human_sets_pause = False + self.human_agent_action = -1 + self.human_demand_autoplay = autoplay + # Now fix the train function + train2 = agent.train + def train_(s, a, r, sp, done, info1, info2): + train2(s, a, r, sp, done, info1, info2) + env.render() + + agent.train = train_ + env.agent = agent + + # space bar: 0x0020 + def key_press(self,key, mod): + if key == 0xff0d: self.human_wants_restart = True + if key == PAUSE_KEY: + self.human_demand_autoplay = not self.human_demand_autoplay + a = -1 + else: + a = self.keys_to_action.get((key,), -1) + + if a == -1 and hasattr(self.env, 'keypress'): + self.env.keypress(key) + + if key == 0x0020: + a = SPACEBAR + self.human_agent_action = a + + def key_release(self,key, mod): + pass + + # def _get_viewer(self): + # return None + # return self.env.viewer if hasattr(self.env, 'viewer') else self.env.unwrapped.viewer + + # def setup(self): + # # print("In play wrapper - setup") + # # print(self._get_viewer()) + # # return + # return + # viewer = self._get_viewer() + # if viewer is not None: + # viewer.window.on_key_press = self.key_press + # viewer.window.on_key_release = self.key_release + + + def pi(self,state, k, info=None): + pi_action = super().pi(state, k, info) # make sure super class pi method is called in case it has side effects. + # self.setup() + # If unpaused, don't use events given by keyboard until pause is hit again. + a = None + while True: + # Get pygame events: + # for event in pygame.event.get(): + # # get the pressed key + for event in pygame.event.get(): + if event.type == pygame.QUIT: + # print("Want to quit") + if hasattr(self, 'env'): + self.env.close() + time.sleep(0.1) + pygame.display.quit() + time.sleep(0.1) + pygame.quit() + time.sleep(0.1) + # print("Laila tov!") + sys.exit() + + + # checking if keydown event happened or not + if event.type == pygame.KEYDOWN: + # if keydown event happened + # than printing a string to output + # print("A key has been pressed", event) + # if event.key == pygame.K_LEFT: + # print("LEFT!") + # print(event.key, event.unicode) + # Determine if event is one environment should handle. + + if event.key == pygame.K_SPACE: + # Got space, autoplay. + a = pi_action + break + elif (event.key,) in self.keys_to_action: + a = self.keys_to_action[(event.key,)] + if info is not None and 'mask' in info: + # Consider refactoring the environment later. + from irlc.utils.common import DiscreteTextActionSpace + + if isinstance(self.env.action_space, DiscreteTextActionSpace): + aint = self.env.action_space.actions.index(a) + else: + aint = a + + if info['mask'][aint] == 0: + # The action was masked. This means that this action is unavailable, and we should select another. + # The default is to select one of the available actions from the mask. + a = info['mask'].argmax() + if isinstance(self.env.action_space, DiscreteTextActionSpace): + a = self.env.action_space.actions[a] + + + + else: + break + elif event.unicode == 'p': + # unpause + self.human_demand_autoplay = not self.human_demand_autoplay + break + else: + # try to pass event on to the game. + if hasattr(self.env, 'keypress'): + self.env.keypress(event) + # now broke and got event. + if self.human_demand_autoplay: + a = pi_action + + if a is not None: + # return a # We don't are if action is not in action-space. + # if hasattr(self.env, 'A') and a not in self.env.A(state): + # print(f"Got action {a} not available in action space {self.env.A(state)}") + # a = self.env.A(state)[-1] # Last because of the gym environment. + # else: + # return a + try: + from irlc.pacman.gamestate import GameState + if isinstance(state, GameState): + if a not in state.A(): + a = "Stop" + except Exception as e: + pass + + return a + # viewer = self._get_viewer() + time.sleep(0.1) + # if viewer is not None: + # viewer.window.dispatch_events() + # a = self.human_agent_action + # if a == SPACEBAR or self.human_demand_autoplay: + # # Just do what the agent wanted us to do + # action_okay = True + # a = pi_action + # elif hasattr(self.env, 'P'): + # if len(self.env.P[state]) == 1 and a != -1: + # a = next(iter(self.env.P[state])) + # action_okay = a in self.env.P[state] + # elif self.env.action_space is not None: + # action_okay = self.env.action_space.contains(a) + # else: + # action_okay = a != -1 + # if action_okay: + # self.human_agent_action = -1 + # break + # print("In keyboard wrapper, returning action", a) + # return a + + +def interactive(env : gym.Env, agent: Agent, autoplay=False) -> (gym.Env, Agent): + """ + This function is used for visualizations. It can + + - Allow you to input keyboard commands to an environment + - Allow you to save results + - Visualize reinforcement-learning agents in the gridworld environment. + + by adding a single extra line ``env, agent = interactive(env,agent)``. + The following shows an example: + + >>> from irlc.gridworld.gridworld_environments import BookGridEnvironment + >>> from irlc import train, Agent, interactive + >>> env = BookGridEnvironment(render_mode="human", zoom=0.8) # Pass render_mode='human' for visualization. + >>> env, agent = interactive(env, Agent(env)) # Make the environment interactive. Note that it needs an agent. + >>> train(env, agent, num_episodes=2) # You can train and use the agent and environment as usual. + >>> env.close() + + It also enables you to visualize the environment at a matplotlib figure or save it as a pdf file using ``env.plot()`` and ``env.savepdf('my_file.pdf)``. + + All demos and figures in the notes are made using this function. + + :param env: A gym environment (an instance of the ``Env`` class) + :param agent: An agent (an instance of the ``Agent`` class) + :param autoplay: Whether the simulation should be unpaused automatically + :return: An environment and agent which have been slightly updated to make them interact with each other. You can use them as usual with the ``train``-function. + """ + from PIL import Image # Let's put this one here in case we run the code in headless mode. + + agent = PlayWrapperPygame(agent, env, autoplay=autoplay) + + def plot(): + env.render_mode, rmt = 'rgb_array', env.render_mode + frame = env.render() + env.render_mode = rmt + im = Image.fromarray(frame) + plt.imshow(im) + plt.axis('off') + plt.axis('off') + plt.tight_layout() + + def savepdf(file): + env.render_mode, rmt = 'rgb_array', env.render_mode + frame = env.render() + env.render_mode = rmt + + im = Image.fromarray(frame) + snapshot_base = file + if snapshot_base.endswith(".png"): + sf = snapshot_base[:-4] + fext = 'png' + else: + fext = 'pdf' + if snapshot_base.endswith(".pdf"): + sf = snapshot_base[:-4] + else: + sf = snapshot_base + + sf = f"{sf}.{fext}" + dn = os.path.dirname(sf) + if len(dn) > 0 and not os.path.isdir(dn): + os.makedirs(dn) + print("Saving snapshot of environment to", os.path.abspath(sf)) + if fext == 'png': + im.save(sf) + from irlc import _move_to_output_directory + _move_to_output_directory(sf) + else: + plt.figure(figsize=(16, 16)) + plt.imshow(im) + plt.axis('off') + plt.tight_layout() + from irlc import savepdf + savepdf(sf, verbose=True) + plt.show() + env.plot = plot + env.savepdf = savepdf + return env, agent + + +def main(): + from irlc.ex11.q_agent import QAgent + + from irlc.gridworld.gridworld_environments import BookGridEnvironment + from irlc import train, Agent + env = BookGridEnvironment(render_mode="human", zoom=0.8) # Pass render_mode='human' for visualization. + env, agent = interactive(env, Agent(env)) # Make th + env.reset() # We always need to call reset + env.plot() # Plot the environment. + env.close() + + # Interaction with a random agent. + from irlc.gridworld.gridworld_environments import BookGridEnvironment + from irlc import train, Agent + env = BookGridEnvironment(render_mode="human", zoom=0.8) # Pass render_mode='human' for visualization. + env, agent = interactive(env, Agent(env)) # Make the environment interactive. Note that it needs an agent. + train(env, agent, num_episodes=100) # You can train and use the agent and environment as usual. + env.close() + + # Second example: plotting. + + + a = 234 + # from irlc.utils.berkley import BerkleyBookGridEnvironment + # from irlc.ex11.sarsa_agent import SarsaAgent + # from irlc.ex01.agent import train + # from irlc.utils.berkley import VideoMonitor + # env = BerkleyBookGridEnvironment(adaptor='gym') + # agent = SarsaAgent(env, gamma=0.95, alpha=0.5) + # """ + # agent = PlayWrapper(agent, env) + + # env = VideoMonitor(env, agent=agent, video_file="videos/SarsaGridworld.mp4", fps=30, continious_recording=True, + # label="SADSF", + # monitor_keys=("Q",)) + # """ + # env.reset() + # env.render() + # train(env, agent, num_episodes=3) + # env.close() + # parser = argparse.ArgumentParser() + # parser.add_argument('--env', type=str, default='MontezumaRevengeNoFrameskip-v4', help='Define Environment') + # args = parser.parse_args() + # env = gym.make(args.env) + # play(env, zoom=4, fps=60) + +if __name__ == "__main__": + + + main() diff --git a/irlc/utils/ptext.py b/irlc/utils/ptext.py new file mode 100644 index 0000000000000000000000000000000000000000..c552f09bf66657ae5936d520c9b7997f5da33b83 --- /dev/null +++ b/irlc/utils/ptext.py @@ -0,0 +1,991 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +# ptext module: place this in your import directory. + +# ptext.draw(text, pos=None, **options) + +# Please see README.md for explanation of options. +# https://github.com/cosmologicon/pygame-text + +from __future__ import division, print_function + +from math import ceil, sin, cos, radians, exp +from collections import namedtuple +import pygame + +# Global default values +DEFAULT_FONT_SIZE = 24 +REFERENCE_FONT_SIZE = 100 +DEFAULT_LINE_HEIGHT = 1.0 +DEFAULT_PARAGRAPH_SPACE = 0.0 +DEFAULT_FONT_NAME = None +DEFAULT_SYSFONT_NAME = None +FONT_NAME_TEMPLATE = "%s" +DEFAULT_COLOR = "white" +DEFAULT_BACKGROUND = None +DEFAULT_SHADE = 0 +DEFAULT_OUTLINE_WIDTH = None +DEFAULT_OUTLINE_COLOR = "black" +OUTLINE_UNIT = 1 / 24 +DEFAULT_SHADOW_OFFSET = None +DEFAULT_SHADOW_COLOR = "black" +SHADOW_UNIT = 1 / 18 +DEFAULT_ALIGN = "left" # left, center, or right +DEFAULT_ANCHOR = 0, 0 # 0, 0 = top left ; 1, 1 = bottom right +DEFAULT_STRIP = True +ALPHA_RESOLUTION = 16 +ANGLE_RESOLUTION_DEGREES = 3 +DEFAULT_UNDERLINE_TAG = None +DEFAULT_BOLD_TAG = None +DEFAULT_ITALIC_TAG = None +DEFAULT_COLOR_TAG = {} + +AUTO_CLEAN = True +MEMORY_LIMIT_MB = 64 +MEMORY_REDUCTION_FACTOR = 0.5 + +pygame.font.init() + + +# Options objects encapsulate the keyword arguments to functions that take a lot of optional keyword +# arguments. + +# Options object base class. Subclass for Options objects specific to different functions. +# Specify valid fields in the _fields list. All keyword fields are optional. Unspecified fields +# default to None, unless otherwise specified in the _defaults list. +class _Options(object): + _fields = () + _defaults = {} + + def __init__(self, **kwargs): + fields = self._allfields() + badfields = set(kwargs) - fields + if badfields: + raise ValueError("Unrecognized args: " + ", ".join(badfields)) + for field in fields: + value = kwargs[field] if field in kwargs else self._defaults.get(field) + setattr(self, field, value) + + @classmethod + def _allfields(cls): + return set(cls._fields) | set(cls._defaults) + + def asdict(self): + return {field: getattr(self, field) for field in self._allfields()} + + def copy(self): + return self.__class__(**self.asdict()) + + def keys(self): + return self._allfields() + + def __getitem__(self, field): + return getattr(self, field) + + def update(self, **newkwargs): + kwargs = self.asdict() + kwargs.update(**newkwargs) + return self.__class__(**kwargs) + + # For cached function calls, this is a hashable representation of the options object. Assumes + # that all field values are either hashable, or dicts whose keys are comparable and values are + # hashable. + def key(self): + values = [] + for field in sorted(self._allfields()): + value = getattr(self, field) + if isinstance(value, dict): + value = tuple(sorted(value.items())) + values.append(value) + return tuple(values) + + def getsuboptions(self, optclass): + return {field: getattr(self, field) for field in optclass._allfields() if hasattr(self, field)} + + # The following methods are just put here for code deduplication. A couple different functions + # use a lot of the same code. + def resolvetags(self): + if self.underlinetag is _default_sentinel: + self.underlinetag = DEFAULT_UNDERLINE_TAG + if self.boldtag is _default_sentinel: + self.boldtag = DEFAULT_BOLD_TAG + if self.italictag is _default_sentinel: + self.italictag = DEFAULT_ITALIC_TAG + if self.colortag is _default_sentinel: + self.colortag = DEFAULT_COLOR_TAG + + +# Used as the default value for any argument for which (1) None is a valid value, and (2) there's a +# global default value. +_default_sentinel = () + + +# Options argument for the draw function. Specifies both text styling and positioning. +class _DrawOptions(_Options): + _fields = ("pos", + "fontname", "fontsize", "sysfontname", "antialias", "bold", "italic", "underline", + "color", "background", + "top", "left", "bottom", "right", "topleft", "bottomleft", "topright", "bottomright", + "midtop", "midleft", "midbottom", "midright", "center", "centerx", "centery", + "width", "widthem", "lineheight", "pspace", "strip", "align", + "owidth", "ocolor", "shadow", "scolor", "gcolor", "shade", + "alpha", "anchor", "angle", + "underlinetag", "boldtag", "italictag", "colortag", + "surf", "cache") + _defaults = { + "fontname": _default_sentinel, + "sysfontname": _default_sentinel, + "antialias": True, "alpha": 1.0, "angle": 0, + "owidth": _default_sentinel, + "shadow": _default_sentinel, + "underlinetag": _default_sentinel, + "boldtag": _default_sentinel, + "italictag": _default_sentinel, + "colortag": _default_sentinel, + "surf": _default_sentinel, "cache": True} + + def __init__(self, **kwargs): + _Options.__init__(self, **kwargs) + self.expandposition() + self.expandanchor() + self.resolvesurf() + + # Expand each 2-element position specifier and overwrite the corresponding 1-element + # position specifiers. + def expandposition(self): + if self.topleft: self.left, self.top = self.topleft + if self.bottomleft: self.left, self.bottom = self.bottomleft + if self.topright: self.right, self.top = self.topright + if self.bottomright: self.right, self.bottom = self.bottomright + if self.midtop: self.centerx, self.top = self.midtop + if self.midleft: self.left, self.centery = self.midleft + if self.midbottom: self.centerx, self.bottom = self.midbottom + if self.midright: self.right, self.centery = self.midright + if self.center: self.centerx, self.centery = self.center + + # Update the pos and anchor fields, if unspecified, to be specified by the positional + # keyword arguments. + def expandanchor(self): + x, y = self.pos or (None, None) + hanchor, vanchor = self.anchor or (None, None) + if self.left is not None: x, hanchor = self.left, 0 + if self.centerx is not None: x, hanchor = self.centerx, 0.5 + if self.right is not None: x, hanchor = self.right, 1 + if self.top is not None: y, vanchor = self.top, 0 + if self.centery is not None: y, vanchor = self.centery, 0.5 + if self.bottom is not None: y, vanchor = self.bottom, 1 + if x is None: + raise ValueError("Unable to determine horizontal position") + if y is None: + raise ValueError("Unable to determine vertical position") + self.pos = x, y + + if self.align is None: self.align = hanchor + if hanchor is None: hanchor = DEFAULT_ANCHOR[0] + if vanchor is None: vanchor = DEFAULT_ANCHOR[1] + self.anchor = hanchor, vanchor + + # Unspecified surf values default to the display surface. + def resolvesurf(self): + if self.surf is _default_sentinel: + self.surf = pygame.display.get_surface() + + def togetsurfoptions(self): + return self.getsuboptions(_GetsurfOptions) + + +# Options for the layout function. By design, this has the same options as draw, although some of +# them are silently ignored. +class _LayoutOptions(_DrawOptions): + def __init__(self, **kwargs): + _Options.__init__(self, **kwargs) + self.expandposition() + self.expandanchor() + if self.lineheight is None: self.lineheight = DEFAULT_LINE_HEIGHT + if self.pspace is None: self.pspace = DEFAULT_PARAGRAPH_SPACE + self.resolvetags() + + def towrapoptions(self): + return self.getsuboptions(_WrapOptions) + + def togetfontoptions(self): + return self.getsuboptions(_GetfontOptions) + + +class _DrawboxOptions(_Options): + _fields = ( + "fontname", "sysfontname", "antialias", "bold", "italic", "underline", + "color", "background", + "lineheight", "pspace", "strip", "align", + "owidth", "ocolor", "shadow", "scolor", "gcolor", "shade", + "underlinetag", "boldtag", "italictag", "colortag", + "alpha", "anchor", "angle", "surf", "cache") + _defaults = { + "fontname": _default_sentinel, + "sysfontname": _default_sentinel, + "antialias": True, "alpha": 1.0, "angle": 0, "anchor": (0.5, 0.5), + "owidth": _default_sentinel, + "shadow": _default_sentinel, + "underlinetag": _default_sentinel, + "boldtag": _default_sentinel, + "italictag": _default_sentinel, + "colortag": _default_sentinel, + "surf": _default_sentinel, "cache": True} + + def __init__(self, **kwargs): + _Options.__init__(self, **kwargs) + if self.fontname is _default_sentinel: self.fontname = DEFAULT_FONT_NAME + if self.sysfontname is _default_sentinel: self.sysfontname = DEFAULT_SYSFONT_NAME + if self.lineheight is None: self.lineheight = DEFAULT_LINE_HEIGHT + if self.pspace is None: self.pspace = DEFAULT_PARAGRAPH_SPACE + + def todrawoptions(self): + return self.getsuboptions(_DrawOptions) + + def tofitsizeoptions(self): + return self.getsuboptions(_FitsizeOptions) + + +class _GetsurfOptions(_Options): + _fields = ("fontname", "fontsize", "sysfontname", "bold", "italic", "underline", "width", + "widthem", "strip", "color", "background", "antialias", "ocolor", "owidth", "scolor", + "shadow", "gcolor", "shade", "alpha", "align", "lineheight", "pspace", "angle", + "underlinetag", "boldtag", "italictag", "colortag", "cache") + _defaults = { + "fontname": _default_sentinel, + "sysfontname": _default_sentinel, + "antialias": True, "alpha": 1.0, "angle": 0, + "owidth": _default_sentinel, + "shadow": _default_sentinel, + "underlinetag": _default_sentinel, + "boldtag": _default_sentinel, + "italictag": _default_sentinel, + "colortag": _default_sentinel, + "cache": True} + + def __init__(self, **kwargs): + _Options.__init__(self, **kwargs) + if self.fontname is _default_sentinel: self.fontname = DEFAULT_FONT_NAME + if self.sysfontname is _default_sentinel: self.sysfontname = DEFAULT_SYSFONT_NAME + if self.fontsize is None: self.fontsize = DEFAULT_FONT_SIZE + self.fontsize = int(round(self.fontsize)) + if self.align is None: self.align = DEFAULT_ALIGN + if self.align in ["left", "center", "right"]: + self.align = [0, 0.5, 1][["left", "center", "right"].index(self.align)] + if self.lineheight is None: self.lineheight = DEFAULT_LINE_HEIGHT + if self.pspace is None: self.pspace = DEFAULT_PARAGRAPH_SPACE + self.color = _resolvecolor(self.color, DEFAULT_COLOR) + self.background = _resolvecolor(self.background, DEFAULT_BACKGROUND) + self.gcolor = _resolvecolor(self.gcolor, None) + if self.shade is None: self.shade = DEFAULT_SHADE + if self.shade: + self.gcolor = _applyshade(self.gcolor or self.color, self.shade) + self.shade = 0 + self.resolveoutlineshadow() + self.alpha = _resolvealpha(self.alpha) + self.angle = _resolveangle(self.angle) + self.strip = DEFAULT_STRIP if self.strip is None else self.strip + self.resolvetags() + + def resolveoutlineshadow(self): + if self.owidth is _default_sentinel: + self.owidth = DEFAULT_OUTLINE_WIDTH + if self.shadow is _default_sentinel: + self.shadow = DEFAULT_SHADOW_OFFSET + self.ocolor = None if self.owidth is None else _resolvecolor(self.ocolor, DEFAULT_OUTLINE_COLOR) + self.scolor = None if self.shadow is None else _resolvecolor(self.scolor, DEFAULT_SHADOW_COLOR) + self._opx = None if self.owidth is None else ceil(self.owidth * self.fontsize * OUTLINE_UNIT) + self._spx = None if self.shadow is None else tuple(ceil(s * self.fontsize * SHADOW_UNIT) for s in self.shadow) + + def checkinline(self): + if self.angle is None or self._opx is not None or self._spx is not None or self.align != 0 or self.gcolor or self.shade: + raise ValueError( + "Inline style not compatible with rotation, outline, drop shadow, gradient, or non-left-aligned text.") + + def towrapoptions(self): + return self.getsuboptions(_WrapOptions) + + def togetfontoptions(self): + return self.getsuboptions(_GetfontOptions) + + +class _WrapOptions(_Options): + _fields = ("fontname", "fontsize", "sysfontname", + "bold", "italic", "underline", "width", "widthem", "strip", + "color", + "underlinetag", "boldtag", "italictag", "colortag") + _defaults = { + "underlinetag": _default_sentinel, + "boldtag": _default_sentinel, + "italictag": _default_sentinel, + "colortag": _default_sentinel, + } + + def __init__(self, **kwargs): + _Options.__init__(self, **kwargs) + self.resolvetags() + if self.widthem is not None and self.width is not None: + raise ValueError("Can't set both width and widthem") + + if self.widthem is not None: + self.fontsize = REFERENCE_FONT_SIZE + self.width = self.widthem * self.fontsize + + if self.strip is None: + self.strip = DEFAULT_STRIP + + def togetfontoptions(self): + return self.getsuboptions(_GetfontOptions) + + +class _GetfontOptions(_Options): + _fields = ("fontname", "fontsize", "sysfontname", "bold", "italic", "underline") + _defaults = { + "fontname": _default_sentinel, + "sysfontname": _default_sentinel, + } + + def __init__(self, **kwargs): + _Options.__init__(self, **kwargs) + if self.fontname is _default_sentinel: self.fontname = DEFAULT_FONT_NAME + if self.sysfontname is _default_sentinel: self.sysfontname = DEFAULT_SYSFONT_NAME + if self.fontname is not None and self.sysfontname is not None: + raise ValueError("Can't set both fontname and sysfontname") + if self.fontsize is None: + self.fontsize = DEFAULT_FONT_SIZE + + def getfontpath(self): + return self.fontname if self.fontname is None else FONT_NAME_TEMPLATE % self.fontname + + +class _FitsizeOptions(_Options): + _fields = ("fontname", "sysfontname", "bold", "italic", "underline", + "lineheight", "pspace", "strip", + "underlinetag", "boldtag", "italictag", "colortag") + _defaults = { + "underlinetag": _default_sentinel, + "boldtag": _default_sentinel, + "italictag": _default_sentinel, + "colortag": _default_sentinel, + } + + def togetfontoptions(self): + return self.getsuboptions(_GetfontOptions) + + def towrapoptions(self): + return self.getsuboptions(_WrapOptions) + + +_font_cache = {} + + +def getfont(**kwargs): + options = _GetfontOptions(**kwargs) + key = options.key() + if key in _font_cache: return _font_cache[key] + if options.sysfontname is not None: + font = pygame.font.SysFont(options.sysfontname, options.fontsize, options.bold or False, + options.italic or False) + else: + try: + font = pygame.font.Font(options.getfontpath(), options.fontsize) + except IOError: + raise IOError("unable to read font filename: %s" % options.getfontpath()) + if options.bold is not None: + font.set_bold(options.bold) + if options.italic is not None: + font.set_italic(options.italic) + if options.underline is not None: + font.set_underline(options.underline) + _font_cache[key] = font + return font + + +# Return the largest integer in the range [xmin, xmax] such that f(x) is True. +def _binarysearch(f, xmin=1, xmax=256): + if not f(xmin): return xmin + if f(xmax): return xmax + # xmin is the largest known value for which f(x) is True + # xmax is the smallest known value for which f(x) is False + while xmax - xmin > 1: + x = (xmax + xmin) // 2 + if f(x): + xmin = x + else: + xmax = x + return xmin + + +_fit_cache = {} + + +def _fitsize(text, size, **kwargs): + options = _FitsizeOptions(**kwargs) + key = text, size, options.key() + if key in _fit_cache: return _fit_cache[key] + width, height = size + + def fits(fontsize): + opts = options.copy() + wmax, hmax = 0, 0 + for span in _wrap(text, fontsize=fontsize, width=width, **opts.towrapoptions()): + y = span.font.get_linesize() * (opts.pspace * span.jpara + opts.lineheight * span.jline) + w, h = span.font.size(span.text) + wmax = max(wmax, span.right) + hmax = max(hmax, y + h) + return wmax <= width and hmax <= height + + fontsize = _binarysearch(fits) + _fit_cache[key] = fontsize + return fontsize + + +# Returns the color as a color RGB or RGBA tuple (i.e. 3 or 4 integers in the range 0-255) +# If color is None, fall back to the default. If default is also None, return None. +# Both color and default can be a list, tuple, a color name, an HTML color format string, a hex +# number string, or an integer pixel value. See pygame.Color constructor for specification. +def _resolvecolor(color, default): + if color is None: color = default + if color is None: return None + try: + return tuple(pygame.Color(color)) + except ValueError: + return tuple(color) + + +def _applyshade(color, shade): + f = exp(-0.4 * shade) + r, g, b = [ + min(max(int(round((c + 50) * f - 50)), 0), 255) + for c in color[:3] + ] + return (r, g, b) + tuple(color[3:]) + + +def _resolvealpha(alpha): + if alpha >= 1: + return 1 + return max(int(round(alpha * ALPHA_RESOLUTION)) / ALPHA_RESOLUTION, 0) + + +def _resolveangle(angle): + if not angle: + return 0 + angle %= 360 + return int(round(angle / ANGLE_RESOLUTION_DEGREES)) * ANGLE_RESOLUTION_DEGREES + + +# Return the set of points in the circle radius r, using Bresenham's circle algorithm +_circle_cache = {} + + +def _circlepoints(r): + r = int(round(r)) + if r in _circle_cache: + return _circle_cache[r] + x, y, e = r, 0, 1 - r + _circle_cache[r] = points = [] + while x >= y: + points.append((x, y)) + y += 1 + if e < 0: + e += 2 * y - 1 + else: + x -= 1 + e += 2 * (y - x) - 1 + points += [(y, x) for x, y in points if x > y] + points += [(-x, y) for x, y in points if x] + points += [(x, -y) for x, y in points if y] + points.sort() + return points + + +# Rotate the given surface by the given angle, in degrees. +# If angle is an exact multiple of 90, use pygame.transform.rotate, otherwise fall back to +# pygame.transform.rotozoom. +def _rotatesurf(surf, angle): + if angle in (90, 180, 270): + return pygame.transform.rotate(surf, angle) + else: + return pygame.transform.rotozoom(surf, angle, 1.0) + + +# Apply the given alpha value to a copy of the Surface. +def _fadesurf(surf, alpha): + surf = surf.copy() + asurf = surf.copy() + asurf.fill((255, 255, 255, int(round(255 * alpha)))) + surf.blit(asurf, (0, 0), None, pygame.BLEND_RGBA_MULT) + return surf + + +def _istransparent(color): + return len(color) > 3 and color[3] == 0 + + +# Produce a 1xh Surface with the given color gradient. +_grad_cache = {} + + +def _gradsurf(h, y0, y1, color0, color1): + key = h, y0, y1, color0, color1 + if key in _grad_cache: + return _grad_cache[key] + surf = pygame.Surface((1, h)).convert_alpha() + r0, g0, b0 = color0[:3] + r1, g1, b1 = color1[:3] + for y in range(h): + f = min(max((y - y0) / (y1 - y0), 0), 1) + g = 1 - f + surf.set_at((0, y), ( + int(round(g * r0 + f * r1)), + int(round(g * g0 + f * g1)), + int(round(g * b0 + f * b1)), + 0 + )) + _grad_cache[key] = surf + return surf + + +# Tracks everything that can be updated by tags. +class TagSpec(namedtuple("TagSpec", ["underline", "bold", "italic", "color"])): + @staticmethod + def fromoptions(options): + return TagSpec( + underline=options.underline, + bold=options.bold, + italic=options.italic, + color=options.color + ) + + def updateoptions(self, options): + options.underline = self.underline + options.bold = self.bold + options.italic = self.italic + options.color = self.color + + def toggleunderline(self): + return self._replace(underline=not self.underline) + + def togglebold(self): + return self._replace(bold=not self.bold) + + def toggleitalic(self): + return self._replace(italic=not self.italic) + + def setcolor(self, color): + return self._replace(color=color) + + +# Splits a string into substrings with corresponding tag specs. +# Empty strings are skipped. Consecutive identical tag specs are not merged. +# e.g. if tagspec0.underline = False and underlinetag = "_" then: +# _splitbytags("_abc__def_ ghi_") yields three items: +# ("abc", TagSpec(underline=True)) +# ("def", TagSpec(underline=True)) +# (" ghi", TagSpec(underline=False)) +def _splitbytags(text, tagspec0, color0, underlinetag, boldtag, italictag, colortag): + colortag = {k: _resolvecolor(v, color0) for k, v in colortag.items()} + tags = sorted((set([underlinetag, boldtag, italictag]) | set(colortag.keys())) - set([None])) + if not tags: + yield text, tagspec0 + return + tagspec = tagspec0 + while text: + tagsin = [tag for tag in tags if tag in text] + if not tagsin: + break + a, tag = min((text.index(tag), tag) for tag in tagsin) + if a > 0: + yield text[:a], tagspec + text = text[a + len(tag):] + if tag == underlinetag: + tagspec = tagspec.toggleunderline() + if tag == boldtag: + tagspec = tagspec.togglebold() + if tag == italictag: + tagspec = tagspec.toggleitalic() + if tag in colortag: + tagspec = tagspec.setcolor(colortag[tag]) + if text: + yield text, tagspec + + +# The _Span class tracks many attributes of a single span of text, i.e. a string of text within a +# single line that has a single font and TagSpec. That is, a single span corresponds to a single +# call to font.render. +# This is not a clean abstraction, and some of the state of this object only makes sense in the +# context of the overall draw call. At various stages of the call, some of the fields will not yet +# be populated. +class _Span: + # Phase 1: set by _wrapline + def __init__(self, text, tagspec, x, font): + self.tagspec = tagspec + self.x = x # Offset from the beginning of the line + self.font = font + self.settext(text) + + # Phase 2: set by _wrap + def setlayout(self, jpara, jline, linewidth): + self.jpara = jpara + self.jline = jline + self.linewidth = linewidth + + # Phase 3: set by getsurf + # These are not required to determine layout or position, only for rendering. + def setdetails(self, antialias, gcolor, background): + self.antialias = antialias + self.gcolor = gcolor + self.background = background + + def settext(self, text): + self.text = text + self.width = self.getwidth(self.text) + self.right = self.x + self.width + + def getwidth(self, text): + if text == '0': + pass + return self.font.size(text)[0] + + def render(self): + if self.gcolor is None: + # Workaround: pygame.Font.render does not allow passing None as an argument value for + # background. We have to call the 3-argument form to specify no background. + args = self.text, self.antialias, self.tagspec.color + if self.background is not None and not _istransparent(self.background): + args += (self.background,) + self.surf = self.font.render(*args).convert_alpha() + else: + self.surf = self.font.render(self.text, self.antialias, (0, 0, 0)).convert_alpha() + w, h = self.surf.get_size() + asc = self.font.get_ascent() + gsurf0 = _gradsurf(h, 0.5 * asc, asc, self.tagspec.color, self.gcolor) + gsurf = pygame.transform.scale(gsurf0, (w, h)) + self.surf.blit(gsurf, (0, 0), None, pygame.BLEND_RGBA_ADD) + + +# Finds the last valid breakpoint in the line of text. A breakpoint is a position at which the line +# can be split without improperly breaking words. +# Returns (breaktext, breakpoint) +def _breaktext(text, width, font, canbreakatstart=False): + # TODO: binary search + # The text to be printed that actually comes from text. Does not include stripped characters, + # e.g. soft hyphens, trailing or otherwise. Does include trailing spaces. + btext = "" + # Index of the first character in text that does not appear in btext. + b = 0 if canbreakatstart else None + # Any additional characters to be appended on return, i.e. hyphen generated by soft hyphens. + bapp = "" + # Partial buildup of btext. + ptext = "" + + def isvalid(t): + return width is None or font.size(t)[0] <= width + + for j, c in enumerate(text): + atbreak, napp = False, "" + # Space and hyphen character allow for a breakpoint. + if c in [" ", "-"]: + atbreak = True + # Non-breaking space. No breakpoint here. Instead just add a space. + elif c == "\u00A0": + c = " " + # Non-breaking hyphen. No breakpoint here. Instead just add a hyphen. + elif c == "\u2011": + c = "-" + # Zero-width space. Allow a breakpoint but don't add anything (i.e. remove this character) + elif c == "\u200B": + atbreak = True + c = "" + # Soft hyphen. Allow a breakpoint with an appending string of hyphen ("-"). + elif c == "\u00AD": + atbreak = True + c = "" + napp = "-" + ptext += c + if atbreak: + if b is None or isvalid((ptext + napp).rstrip(" ")): + btext = ptext + b = j + 1 + bapp = napp + else: + break + else: + # One past the end of the line is always considered a breakpoint. + if b is None or isvalid(ptext): + return ptext, len(text) + # Invalid breakpoint found. Take trailing spaces starting from the last valid breakpoint. + while b < len(text) and text[b] == " ": + b += 1 + bapp += " " + return btext + bapp, b + + +# Split a single line of text. +# textandtags is the output of _splitbytags, i.e. a sequence of (string, tag spec) tuples. +def _wrapline(textandtags, width, getfontbytagspec): + x = 0 + canbreakatstart = False + lines = [] + line = [] + for text, tagspec in textandtags: + font = getfontbytagspec(tagspec) + while text: + rwidth = None if width is None else width - x + btext, b = _breaktext(text, rwidth, font, canbreakatstart) + if b == 0: + lines.append((line, x)) + line = [] + x = 0 + canbreakatstart = False + else: + span = _Span(btext, tagspec, x, font) + line.append(span) + x += span.width + text = text[b:] + canbreakatstart = True + lines.append((line, x)) + return lines + + +def _wrap(text, **kwargs): + options = _WrapOptions(**kwargs) + # Returns a function mapping strings to int widths in the specified font + opts = options.copy() + + def getfontbytagspec(tagspec): + tagspec.updateoptions(opts) + return getfont(**opts.togetfontoptions()) + + # Apparently Font.render accepts None for the text argument, in which case it's treated as the + # empty string. We match that behavior here. + if text is None: text = "" + spans = [] + tagspec0 = TagSpec.fromoptions(options) + jline = 0 + for jpara, para in enumerate(text.replace("\t", " ").split("\n")): + if options.strip: + para = para.rstrip(" ") + tagargs = options.underlinetag, options.boldtag, options.italictag, options.colortag + textandtags = list(_splitbytags(para, tagspec0, options.color, *tagargs)) + _, tagspec0 = textandtags[-1] + for line, linewidth in _wrapline(textandtags, options.width, getfontbytagspec): + if not line: + jline += 1 + continue + # Strip trailing spaces from the end of each line. + span = line[-1] + if options.strip: + span.settext(span.text.rstrip(" ")) + elif options.width is not None: + while span.text[-1] == " " and span.right > options.width: + span.settext(span.text[:-1]) + linewidth = span.right + for span in line: + span.setlayout(jpara, jline, linewidth) + spans.append(span) + jline += 1 + return spans + + +_surf_cache = {} +_surf_tick_usage = {} +_surf_size_total = 0 +_unrotated_size = {} +_tick = 0 + + +def getsurf(text, **kwargs): + global _tick, _surf_size_total + options = _GetsurfOptions(**kwargs) + key = text, options.key() + if key in _surf_cache: + _surf_tick_usage[key] = _tick + _tick += 1 + return _surf_cache[key] + + if options.angle: + surf0 = getsurf(text, **options.update(angle=0)) + surf = _rotatesurf(surf0, options.angle) + # draw() requires the unrotated size for proper positioning, but the unrotated surface will + # not necessarily be cached, so we add it to a global store here. In principle you could + # compute it from surf.get_size() and options.angle, were it not for rounding issues. + _unrotated_size[(surf.get_size(), options.angle, text)] = surf0.get_size() + elif options.alpha < 1.0: + surf = _fadesurf(getsurf(text, **options.update(alpha=1.0)), options.alpha) + elif options._spx is not None: + color = (0, 0, 0) if _istransparent(options.color) else options.color + surf0 = getsurf(text, **options.update(background=(0, 0, 0, 0), color=color, shadow=None, scolor=None)) + sopts = { + "color": options.scolor, + "shadow": None, + "scolor": None, + "background": (0, 0, 0, 0), + "gcolor": None, + "colortag": {k: None for k in options.colortag}, + } + ssurf = getsurf(text, **options.update(**sopts)) + w0, h0 = surf0.get_size() + sx, sy = options._spx + surf = pygame.Surface((w0 + abs(sx), h0 + abs(sy))).convert_alpha() + surf.fill(options.background or (0, 0, 0, 0)) + dx, dy = max(sx, 0), max(sy, 0) + surf.blit(ssurf, (dx, dy)) + x0, y0 = abs(sx) - dx, abs(sy) - dy + if _istransparent(options.color): + surf.blit(surf0, (x0, y0), None, pygame.BLEND_RGBA_SUB) + else: + surf.blit(surf0, (x0, y0)) + elif options._opx is not None: + color = (0, 0, 0) if _istransparent(options.color) else options.color + surf0 = getsurf(text, **options.update(color=color, ocolor=None, owidth=None)) + oopts = { + "color": options.ocolor, + "ocolor": None, + "owidth": None, + "background": (0, 0, 0, 0), + "gcolor": None, + "colortag": {k: None for k in options.colortag}, + } + osurf = getsurf(text, **options.update(**oopts)) + w0, h0 = surf0.get_size() + opx = options._opx + surf = pygame.Surface((w0 + 2 * opx, h0 + 2 * opx)).convert_alpha() + surf.fill(options.background or (0, 0, 0, 0)) + for dx, dy in _circlepoints(opx): + surf.blit(osurf, (dx + opx, dy + opx)) + if _istransparent(options.color): + surf.blit(surf0, (opx, opx), None, pygame.BLEND_RGBA_SUB) + else: + surf.blit(surf0, (opx, opx)) + else: + # Each span is rendered separately into a Surface, and then the different spans' Surfaces + # are blitted onto the final Surface. + spans = _wrap(text, **options.towrapoptions()) + for span in spans: + span.setdetails(options.antialias, options.gcolor, options.background) + span.render() + # Now to blit the span Surfaces together onto a single Surface. As an optimization, when + # there is only one span Surface, just use that. (We can't use this optimization if there's + # a gradient color, because the background color still needs to be applied.) + if not spans: + surf = pygame.Surface((0, 0)).convert_alpha() + elif len(spans) == 1 and options.gcolor is None: + surf = spans[0].surf + else: + font = spans[0].font + w = max(span.linewidth for span in spans) + linesize = font.get_linesize() * options.lineheight + parasize = font.get_linesize() * options.pspace + for span in spans: + span.y = int(round(span.jline * linesize + span.jpara * parasize)) + h = max(span.y for span in spans) + font.get_height() + surf = pygame.Surface((w, h)).convert_alpha() + surf.fill(options.background or (0, 0, 0, 0)) + for span in spans: + x = int(round(span.x + options.align * (w - span.linewidth))) + surf.blit(span.surf, (x, span.y)) + if options.cache: + w, h = surf.get_size() + _surf_size_total += 4 * w * h + _surf_cache[key] = surf + _surf_tick_usage[key] = _tick + _tick += 1 + return surf + + +# The actual position on the screen where the surf is to be blitted, rather than the specified +# anchor position. +def _blitpos(angle, pos, anchor, size, text): + angle = _resolveangle(angle) + x, y = pos + sw, sh = size + hanchor, vanchor = anchor + if angle: + w0, h0 = _unrotated_size[(size, angle, text)] + S, C = sin(radians(angle)), cos(radians(angle)) + dx, dy = (0.5 - hanchor) * w0, (0.5 - vanchor) * h0 + x += dx * C + dy * S - 0.5 * sw + y += -dx * S + dy * C - 0.5 * sh + else: + x -= hanchor * sw + y -= vanchor * sh + x = int(round(x)) + y = int(round(y)) + return x, y + + +def layout(text, **kwargs): + options = _LayoutOptions(**kwargs) + if options.angle != 0: + raise ValueError("Nonzero angle not yet supported for ptext.layout") + font = getfont(**options.togetfontoptions()) + fl = font.get_linesize() + linesize = fl * options.lineheight + parasize = fl * options.pspace + + spans = _wrap(text, **options.towrapoptions()) + + rects = [] + sw = max(span.linewidth for span in spans) + for span in spans: + y = int(round(span.jpara * parasize + span.jline * linesize)) + rect = pygame.Rect(span.x, y, *font.size(span.text)) + rect.x += int(round(options.align * (sw - span.linewidth))) + rects.append(rect) + sh = max(rect.bottom for rect in rects) + + x0, y0 = _blitpos(options.angle, options.pos, options.anchor, (sw, sh), None) + + # Adjust the rects as necessary to account for outline and shadow. + # TODO: the following is duplicated from _GetsurfOptions.__init__ + dx, dy = 0, 0 + if options.owidth is not None: + opx = ceil(options.owidth * options.fontsize * OUTLINE_UNIT) + dx, dy = max(dx, abs(opx)), max(dy, abs(opx)) + if options.shadow is not None: + spx, spy = (ceil(s * options.fontsize * SHADOW_UNIT) for s in options.shadow) + dx, dy = max(dx, -spx), max(dy, -spy) + rects = [rect.move(x0 + dx, y0 + dy) for rect in rects] + + return [(span.text, rect, span.font) for span, rect in zip(spans, rects)] + + +def draw(text, pos=None, **kwargs): + # if text == '0': + # print("herpaderp") + pass + options = _DrawOptions(pos=pos, **kwargs) + tsurf = getsurf(text, **options.togetsurfoptions()) + pos = _blitpos(options.angle, options.pos, options.anchor, tsurf.get_size(), text) + if options.surf is not None: + options.surf.blit(tsurf, pos) + if AUTO_CLEAN: + clean() + return tsurf, pos + + +def drawbox(text, rect, **kwargs): + options = _DrawboxOptions(**kwargs) + rect = pygame.Rect(rect) + hanchor, vanchor = options.anchor + x = rect.x + hanchor * rect.width + y = rect.y + vanchor * rect.height + fontsize = _fitsize(text, rect.size, **options.tofitsizeoptions()) + return draw(text, pos=(x, y), width=rect.width, fontsize=fontsize, **options.todrawoptions()) + + +def clean(): + global _surf_size_total + memory_limit = MEMORY_LIMIT_MB * (1 << 20) + if _surf_size_total < memory_limit: + return + memory_limit *= MEMORY_REDUCTION_FACTOR + keys = sorted(_surf_cache, key=_surf_tick_usage.get) + for key in keys: + w, h = _surf_cache[key].get_size() + del _surf_cache[key] + del _surf_tick_usage[key] + _surf_size_total -= 4 * w * h + if _surf_size_total < memory_limit: + break diff --git a/irlc/utils/timer.py b/irlc/utils/timer.py new file mode 100644 index 0000000000000000000000000000000000000000..14614f144d8c1d53db0648820bf456f5501c161a --- /dev/null +++ b/irlc/utils/timer.py @@ -0,0 +1,45 @@ +# This file may not be shared/redistributed without permission. Please read copyright notice in the git repo. If this file contains other copyright notices disregard this text. +from collections import defaultdict +import datetime + +class Timer: + def __init__(self, show_time_per_tic=True, start=False): + self.tspend = defaultdict(lambda: 0) + self.t_start = {} + self.n_tics = defaultdict(lambda: 0) + self.s_ = None + self.show_time_per_tic = show_time_per_tic + if start: + self.start() + + + def start(self): + self.s_ = datetime.datetime.now() + + def tic(self, name): + self.lst = name + self.t_start[name] = datetime.datetime.now() + + def toc(self, name=None): + name = name if name is not None else self.lst + self.tspend[name] += (datetime.datetime.now() - self.t_start[name]).total_seconds() + self.n_tics[name] += 1 + + def display(self): + Tknown = sum(self.tspend.values()) + if self.s_ is not None: + Ttot = (datetime.datetime.now() - self.s_).total_seconds() + + if self.show_time_per_tic: + spend = {k: v/self.n_tics[k] for k, v in self.tspend.items()} + # Tknown = + else: + spend = self.tspend + + s = ", ".join( [f"{k}: {v:.2f} ({int(self.tspend[k]/Tknown*100)} %)" for k, v in spend.items()] ) + + + if self.s_ is not None: + return f"{Ttot:.2f} ({(Tknown/Ttot*100):.1f} %). " + s + else: + return s diff --git a/requirements_conda.txt b/requirements_conda.txt new file mode 100644 index 0000000000000000000000000000000000000000..2b402cf578db0e423773c3f874ce0d9e79d35232 --- /dev/null +++ b/requirements_conda.txt @@ -0,0 +1,16 @@ +# On linux, you also need these packages: +# apt install build-essential python3.11-dev swig +# (replace 3.11 with your python version; this works on Ubuntu 23.10 mantic) +gymnasium[box2d]<=0.29.1 +torch +sympy +tqdm +seaborn +pillow +scikit-learn +matplotlib +requests # Required when updating the local files (read stuff from gitlab). +pyqt5 +pygame +numpy<=1.26.4 # Version 2 has a problem with gymnasium + diff --git a/requirements_pip.txt b/requirements_pip.txt new file mode 100644 index 0000000000000000000000000000000000000000..a375525d9963ced95672329d771550ff26c5d5ae --- /dev/null +++ b/requirements_pip.txt @@ -0,0 +1,3 @@ +# PyQt5>=5.15.9 # 5.15.8 has a problem with matplotlib; but newest version is 5.15.9 +unitgrade +-e . diff --git a/solutions/ex00/fruit_homework_TODO_1.py b/solutions/ex00/fruit_homework_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..b498ceb06d3fa5ab7a183da45428d83a52e1a5cb --- /dev/null +++ b/solutions/ex00/fruit_homework_TODO_1.py @@ -0,0 +1 @@ + return a+b \ No newline at end of file diff --git a/solutions/ex00/fruit_homework_TODO_2.py b/solutions/ex00/fruit_homework_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..f546843734ee5aa45c7345b9bd8d3bfdca5600ff --- /dev/null +++ b/solutions/ex00/fruit_homework_TODO_2.py @@ -0,0 +1 @@ + return ["mr " + a for a in animals] \ No newline at end of file diff --git a/solutions/ex00/fruit_homework_TODO_3.py b/solutions/ex00/fruit_homework_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..5be72c6f38c133f5b1a2f5a09acff6d2eefda3ee --- /dev/null +++ b/solutions/ex00/fruit_homework_TODO_3.py @@ -0,0 +1 @@ + return sum([x * p for x, p in p_dict.items()]) \ No newline at end of file diff --git a/solutions/ex00/fruit_homework_TODO_4.py b/solutions/ex00/fruit_homework_TODO_4.py new file mode 100644 index 0000000000000000000000000000000000000000..c836aa8e22e9b64e93d6c1874af9d506f1e36f22 --- /dev/null +++ b/solutions/ex00/fruit_homework_TODO_4.py @@ -0,0 +1 @@ + return list(order_dict.keys()) \ No newline at end of file diff --git a/solutions/ex00/fruit_homework_TODO_5.py b/solutions/ex00/fruit_homework_TODO_5.py new file mode 100644 index 0000000000000000000000000000000000000000..84c3b39c208f1eb0dbda3f5f8001c82e9af2cb4b --- /dev/null +++ b/solutions/ex00/fruit_homework_TODO_5.py @@ -0,0 +1 @@ + return self.prices[fruit] \ No newline at end of file diff --git a/solutions/ex00/fruit_homework_TODO_6.py b/solutions/ex00/fruit_homework_TODO_6.py new file mode 100644 index 0000000000000000000000000000000000000000..dc8b7ab2f626d208b2b5af689970745c6f663a09 --- /dev/null +++ b/solutions/ex00/fruit_homework_TODO_6.py @@ -0,0 +1 @@ + return sum([quantity * self.cost(fruit) for fruit, quantity in order.items()]) \ No newline at end of file diff --git a/solutions/ex00/fruit_homework_TODO_7.py b/solutions/ex00/fruit_homework_TODO_7.py new file mode 100644 index 0000000000000000000000000000000000000000..f02ea8d2bbf4b264771be3878c5caf72854b2666 --- /dev/null +++ b/solutions/ex00/fruit_homework_TODO_7.py @@ -0,0 +1,2 @@ + cs = [s.price_of_order(order) for s in fruit_shops] + best_shop = fruit_shops[cs.index(min(cs))] \ No newline at end of file diff --git a/solutions/ex01/bobs_friend_TODO_1.py b/solutions/ex01/bobs_friend_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..2d03d7c5a8beff870e8ced0000a122c5b9952d75 --- /dev/null +++ b/solutions/ex01/bobs_friend_TODO_1.py @@ -0,0 +1,3 @@ + + self.s = self.x0 + \ No newline at end of file diff --git a/solutions/ex01/bobs_friend_TODO_2.py b/solutions/ex01/bobs_friend_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..9caf28ad6988a09d81ea82bcebf257c4fd4caf0c --- /dev/null +++ b/solutions/ex01/bobs_friend_TODO_2.py @@ -0,0 +1,9 @@ + terminated = True + if a == 0: + s_next = self.s * 1.1 + else: + if np.random.rand() < 1/4: + s_next = 0 + else: + s_next = self.s + 12 + reward = s_next - self.s \ No newline at end of file diff --git a/solutions/ex01/bobs_friend_TODO_3.py b/solutions/ex01/bobs_friend_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..8399f7fba970e6acf6b370dadd567754b5f8bc7e --- /dev/null +++ b/solutions/ex01/bobs_friend_TODO_3.py @@ -0,0 +1 @@ + return 0 \ No newline at end of file diff --git a/solutions/ex01/bobs_friend_TODO_4.py b/solutions/ex01/bobs_friend_TODO_4.py new file mode 100644 index 0000000000000000000000000000000000000000..36a268f2fa289cbbb6a96ae75376e8e1cc5ea729 --- /dev/null +++ b/solutions/ex01/bobs_friend_TODO_4.py @@ -0,0 +1 @@ + return 1 \ No newline at end of file diff --git a/solutions/ex01/chess_TODO_1.py b/solutions/ex01/chess_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..f8752f92f9185d2b2e05cd157a1b5372b6b9560b --- /dev/null +++ b/solutions/ex01/chess_TODO_1.py @@ -0,0 +1 @@ + self.s = [] \ No newline at end of file diff --git a/solutions/ex01/chess_TODO_2.py b/solutions/ex01/chess_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..9b829905c94cfd57d43edfcfeecdfe8bd36039b6 --- /dev/null +++ b/solutions/ex01/chess_TODO_2.py @@ -0,0 +1,7 @@ + if np.random.rand() < self.p_draw: + game_outcome = 0 + else: + if np.random.rand() < self.p_win: + game_outcome = 1 + else: + game_outcome = -1 \ No newline at end of file diff --git a/solutions/ex01/chess_TODO_3.py b/solutions/ex01/chess_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..29e14434d6508e15f098119b60f2e0e6e15390d9 --- /dev/null +++ b/solutions/ex01/chess_TODO_3.py @@ -0,0 +1 @@ + done = len(self.s) >= 2 and self.s[-1] == self.s[-2] and self.s[-1] != 0 \ No newline at end of file diff --git a/solutions/ex01/chess_TODO_4.py b/solutions/ex01/chess_TODO_4.py new file mode 100644 index 0000000000000000000000000000000000000000..d45e38a4592d6c99a05d741916eb6655c2babebf --- /dev/null +++ b/solutions/ex01/chess_TODO_4.py @@ -0,0 +1 @@ + r = self.s[-1] == 1 if done else 0 \ No newline at end of file diff --git a/solutions/ex01/chess_TODO_5.py b/solutions/ex01/chess_TODO_5.py new file mode 100644 index 0000000000000000000000000000000000000000..c270359f1626954b313683214d93beb8a786521b --- /dev/null +++ b/solutions/ex01/chess_TODO_5.py @@ -0,0 +1 @@ + stats, _ = train(env, Agent(env), num_episodes=T) \ No newline at end of file diff --git a/solutions/ex01/inventory_environment_TODO_1.py b/solutions/ex01/inventory_environment_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..5f5a775b790f2dfb76f573b6c1b4ee7b4a8442fb --- /dev/null +++ b/solutions/ex01/inventory_environment_TODO_1.py @@ -0,0 +1,5 @@ + s_next = max(0, min(2, self.s-w+a)) # next state; x_{k+1} = f_k(x_k, u_k, w_k) + reward = -(a + (self.s + a - w)**2) # reward = -cost = -g_k(x_k, u_k, w_k) + terminated = self.k == self.N-1 # Have we terminated? (i.e. is k==N-1) + self.s = s_next # update environment state + self.k += 1 # update current time step \ No newline at end of file diff --git a/solutions/ex01/inventory_environment_TODO_2.py b/solutions/ex01/inventory_environment_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..bebe04bc25189f1dad2fbd10c30070cd823b5b9e --- /dev/null +++ b/solutions/ex01/inventory_environment_TODO_2.py @@ -0,0 +1 @@ + return np.random.choice(3) # Return a random action \ No newline at end of file diff --git a/solutions/ex01/inventory_environment_TODO_3.py b/solutions/ex01/inventory_environment_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..0855951dfbae56cfc204cab87b19954e2e4bf074 --- /dev/null +++ b/solutions/ex01/inventory_environment_TODO_3.py @@ -0,0 +1,7 @@ + a = agent.pi(s, k) + sp, r, terminated, truncated, metadata = env.step(a) + agent.train(s, a, sp, r, terminated) + s = sp + J += r + if terminated or truncated: + break \ No newline at end of file diff --git a/solutions/ex01/pacman_hardcoded_TODO_1.py b/solutions/ex01/pacman_hardcoded_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..5c532d7fae997f5c6a4d7e8383dbbb14af8a12e8 --- /dev/null +++ b/solutions/ex01/pacman_hardcoded_TODO_1.py @@ -0,0 +1,7 @@ + if k < 7: + return 'South' + elif k < 14: + return 'East' + elif k < 21: + return 'North' + elif k < 28: \ No newline at end of file diff --git a/solutions/ex02/dp_TODO_1.py b/solutions/ex02/dp_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..a266a96faac19f97576e30e2ab483c3518f64eb4 --- /dev/null +++ b/solutions/ex02/dp_TODO_1.py @@ -0,0 +1,4 @@ + Qu = {u: sum(pw * (model.g(x, u, w, k) + J[k + 1][model.f(x, u, w, k)]) for w, pw in model.Pw(x, u, k).items()) for u in model.A(x, k)} + umin = min(Qu, key=Qu.get) + J[k][x] = Qu[umin] # Compute the expected cost function + pi[k][x] = umin # Compute the optimal policy \ No newline at end of file diff --git a/solutions/ex02/dp_agent_TODO_1.py b/solutions/ex02/dp_agent_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..18f9f782a449efa896b4e9516a633aff48d78a37 --- /dev/null +++ b/solutions/ex02/dp_agent_TODO_1.py @@ -0,0 +1 @@ + action = self.pi_[k][s] \ No newline at end of file diff --git a/solutions/ex02/flower_store_TODO_1.py b/solutions/ex02/flower_store_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..4fec1d31c73c6e3a40dd0f340ef1e3c40f4a950f --- /dev/null +++ b/solutions/ex02/flower_store_TODO_1.py @@ -0,0 +1,23 @@ +class FlowerStoreModel(InventoryDPModel): + def __init__(self, N=3, c=0., prob_empty=False): + self.c = c + self.prob_empty = prob_empty + super().__init__(N=N) + + def g(self, x, u, w, k): # Cost function g_k(x,u,w) + if self.prob_empty: + return 0 + return u * self.c + np.abs(x + u - w) + + def f(self, x, u, w, k): # Dynamics f_k(x,u,w) + return max(0, min(max(self.S(k)), x + u - w)) + + def Pw(self, x, u, k): # Distribution over random disturbances + pw = {0: .1, 1: .3, 2: .6} + return pw + + def gN(self, x): + if self.prob_empty: + return -1 if x == 1 else 0 + else: + return 0 \ No newline at end of file diff --git a/solutions/ex02/flower_store_TODO_2.py b/solutions/ex02/flower_store_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..29eed8491b7026d6ca202083467679520c7cdb23 --- /dev/null +++ b/solutions/ex02/flower_store_TODO_2.py @@ -0,0 +1,3 @@ + model = FlowerStoreModel(N=N, c=c, prob_empty=False) + J, pi = DP_stochastic(model) + u = pi[0][x0] \ No newline at end of file diff --git a/solutions/ex02/flower_store_TODO_3.py b/solutions/ex02/flower_store_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..62bdb1dc5fd12c76ae2092df7cc21e98b72d55cc --- /dev/null +++ b/solutions/ex02/flower_store_TODO_3.py @@ -0,0 +1,3 @@ + model = FlowerStoreModel(N=N, prob_empty=True) + J, pi = DP_stochastic(model) + pr_empty = -J[0][x0] \ No newline at end of file diff --git a/solutions/ex02/graph_traversal_TODO_1.py b/solutions/ex02/graph_traversal_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..2eb7542942bd0d36aa3bfdc99abe428c6b1b52eb --- /dev/null +++ b/solutions/ex02/graph_traversal_TODO_1.py @@ -0,0 +1 @@ + return u \ No newline at end of file diff --git a/solutions/ex02/graph_traversal_TODO_2.py b/solutions/ex02/graph_traversal_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..cd59ca2ac4d2ef3ae0f0d2162383b16cdda1b597 --- /dev/null +++ b/solutions/ex02/graph_traversal_TODO_2.py @@ -0,0 +1 @@ + return self.G[(x,u)] \ No newline at end of file diff --git a/solutions/ex02/graph_traversal_TODO_3.py b/solutions/ex02/graph_traversal_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..3bdbfbc0087daff787fe6aad1152dde1401cf76b --- /dev/null +++ b/solutions/ex02/graph_traversal_TODO_3.py @@ -0,0 +1 @@ + return 0 if x == self.t else np.inf \ No newline at end of file diff --git a/solutions/ex02/inventory_TODO_1.py b/solutions/ex02/inventory_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..38ded4d6bdfbf7113936292bd1bb4857da6c48ac --- /dev/null +++ b/solutions/ex02/inventory_TODO_1.py @@ -0,0 +1 @@ + return {0:.1, 1:.7, 2:0.2} \ No newline at end of file diff --git a/solutions/ex03/inventory_evaluation_TODO_1.py b/solutions/ex03/inventory_evaluation_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..ec037ab6c0e4495d508b769cb3f844361bc27766 --- /dev/null +++ b/solutions/ex03/inventory_evaluation_TODO_1.py @@ -0,0 +1,2 @@ + k = 0 + expected_number_of_items = sum([p * model.f(x, u, w, k=0) for w, p in model.Pw(x, u, k).items()]) \ No newline at end of file diff --git a/solutions/ex03/inventory_evaluation_TODO_2.py b/solutions/ex03/inventory_evaluation_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..e2897b1bbe779fd70b1d103bc72fada72f0e465a --- /dev/null +++ b/solutions/ex03/inventory_evaluation_TODO_2.py @@ -0,0 +1,12 @@ + model = InventoryDPModel() + N = model.N + J = [{} for _ in range(N + 1)] + J[N] = {x: model.gN(x) for x in model.S(model.N)} + for k in range(N - 1, -1, -1): + for x in model.S(k): + Qu = {u: sum(pw * (model.g(x, u, w, k) + J[k + 1][model.f(x, u, w, k)]) for w, pw in model.Pw(x, u, k).items()) for u + in model.A(x, k)} + + umin = pi[k][x] # min(Qu, key=Qu.get) + J[k][x] = Qu[umin] # Compute the expected cost function + J_pi_x0 = J[0][x0] \ No newline at end of file diff --git a/solutions/ex03/kuramoto_TODO_1.py b/solutions/ex03/kuramoto_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..27e0fcd91a1912fcd9510bd5c42085b40f5f035d --- /dev/null +++ b/solutions/ex03/kuramoto_TODO_1.py @@ -0,0 +1 @@ + symbolic_f_list = [u[0] + sym.cos(x[0])] \ No newline at end of file diff --git a/solutions/ex03/kuramoto_TODO_2.py b/solutions/ex03/kuramoto_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..0f1d61147d8b54964f5a35d9493bd13ead61df13 --- /dev/null +++ b/solutions/ex03/kuramoto_TODO_2.py @@ -0,0 +1 @@ + f_value = cmodel.f(x, u, t=0) \ No newline at end of file diff --git a/solutions/ex03/kuramoto_TODO_3.py b/solutions/ex03/kuramoto_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..f28ac99d8a97bd8ed4423c5dbaff106350e41d07 --- /dev/null +++ b/solutions/ex03/kuramoto_TODO_3.py @@ -0,0 +1,7 @@ + Delta = tt[k + 1] - tt[k] + xn = xs[k] + k1 = np.asarray(f(xn, u)) + k2 = np.asarray(f(xn + Delta * k1/2, u)) + k3 = np.asarray(f(xn + Delta * k2/2, u)) + k4 = np.asarray(f(xn + Delta * k3, u)) + x_next = xn + 1/6 * Delta * (k1 + 2*k2 + 2*k3 + k4) \ No newline at end of file diff --git a/solutions/ex03/toy_2d_control_TODO_1.py b/solutions/ex03/toy_2d_control_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..137a63b9c00938de0a696f65e96e233a0fec8e0f --- /dev/null +++ b/solutions/ex03/toy_2d_control_TODO_1.py @@ -0,0 +1,2 @@ + def sym_f(self, x, u, t=None): + return [x[1], sym.cos(x[0] + u[0])] \ No newline at end of file diff --git a/solutions/ex03/toy_2d_control_TODO_2.py b/solutions/ex03/toy_2d_control_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..0b32ca6fe36c550f93539ec2737c272747af152a --- /dev/null +++ b/solutions/ex03/toy_2d_control_TODO_2.py @@ -0,0 +1,4 @@ + toy = Toy2DControl() + x0 = np.asarray([np.pi/2, 0]) + xs, us, ts = toy.simulate( x0=x0, u_fun = u0, t0=0, tF=T) + wT = xs[-1][0] \ No newline at end of file diff --git a/solutions/ex04/discrete_kuramoto_TODO_1.py b/solutions/ex04/discrete_kuramoto_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..e0e9d220bfca21780cfec5223e38ff4958686882 --- /dev/null +++ b/solutions/ex04/discrete_kuramoto_TODO_1.py @@ -0,0 +1 @@ + f_euler = dmodel.f(x, u, k=0) \ No newline at end of file diff --git a/solutions/ex04/discrete_kuramoto_TODO_2.py b/solutions/ex04/discrete_kuramoto_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..001427a3f9d5aed4c60fe95eb3a97b90aae10c8e --- /dev/null +++ b/solutions/ex04/discrete_kuramoto_TODO_2.py @@ -0,0 +1 @@ + f_euler_derivative, _ = dmodel.f_jacobian(x, u) \ No newline at end of file diff --git a/solutions/ex04/discrete_kuramoto_TODO_3.py b/solutions/ex04/discrete_kuramoto_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..5b50fea70c4ba210d1e03515ff6168f1a468badd --- /dev/null +++ b/solutions/ex04/discrete_kuramoto_TODO_3.py @@ -0,0 +1 @@ + next_x, cost, terminated, _, metadata = env.step([u]) \ No newline at end of file diff --git a/solutions/ex04/model_pendulum_TODO_1.py b/solutions/ex04/model_pendulum_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..bbdc0738a7080bfbd4b7f03fb06eb5c882a56314 --- /dev/null +++ b/solutions/ex04/model_pendulum_TODO_1.py @@ -0,0 +1 @@ + x_dot = model.f([1, 2], [0], t=0) \ No newline at end of file diff --git a/solutions/ex04/model_pendulum_TODO_2.py b/solutions/ex04/model_pendulum_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..59b6a6b42c376a52c882f8b7b8de42ad7d5cb0e2 --- /dev/null +++ b/solutions/ex04/model_pendulum_TODO_2.py @@ -0,0 +1 @@ + x_dot_numpy = model.f([1, 2], [0], t=0) \ No newline at end of file diff --git a/solutions/ex04/pid_TODO_1.py b/solutions/ex04/pid_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..be1769bbc56e0ee03a1640f22d0d6728e541d7d9 --- /dev/null +++ b/solutions/ex04/pid_TODO_1.py @@ -0,0 +1,6 @@ + e = self.target - x + # if self.e_prior == 0 and self.I == 0: + # self.e_prior = e + self.I = self.I + e * self.dt + u = self.Kp * e + self.Ki * self.I + self.Kd * (e - self.e_prior)/self.dt + self.e_prior = e \ No newline at end of file diff --git a/solutions/ex04/pid_TODO_2.py b/solutions/ex04/pid_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..7468df372bdf3409cc75603b2e2b18ebf479674f --- /dev/null +++ b/solutions/ex04/pid_TODO_2.py @@ -0,0 +1 @@ + u = pid.pi(x_cur[0]) \ No newline at end of file diff --git a/solutions/ex04/pid_car_TODO_1.py b/solutions/ex04/pid_car_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..4201c479926696c912381e7ffe10823507f4a151 --- /dev/null +++ b/solutions/ex04/pid_car_TODO_1.py @@ -0,0 +1,2 @@ + self.pid_angle = PID(dt=env.discrete_model.dt, Kp=1.0, Ki=0, Kd=0, target=0) + self.pid_velocity = PID(dt=env.discrete_model.dt, Kp=1.5, Ki=0, Kd=0, target=v_target) \ No newline at end of file diff --git a/solutions/ex04/pid_car_TODO_2.py b/solutions/ex04/pid_car_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..d7b67d3b6753bb62a99e28f23c8be52076b35e9d --- /dev/null +++ b/solutions/ex04/pid_car_TODO_2.py @@ -0,0 +1,2 @@ + xx = x[5] + x[3] if self.use_both_x5_x3 else x[5] + u = np.asarray([self.pid_angle.pi(xx), self.pid_velocity.pi(x[0])]) \ No newline at end of file diff --git a/solutions/ex04/pid_locomotive_agent_TODO_1.py b/solutions/ex04/pid_locomotive_agent_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..e8912a077ba579f6c8bd90e1688686f353692c49 --- /dev/null +++ b/solutions/ex04/pid_locomotive_agent_TODO_1.py @@ -0,0 +1 @@ + self.pid = PID(dt=dt, Kp=Kp, Ki=Ki, Kd=Kd, target=target) \ No newline at end of file diff --git a/solutions/ex04/pid_locomotive_agent_TODO_2.py b/solutions/ex04/pid_locomotive_agent_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..a512e14a83f51a2d69a4afee52e59b3528ae20f0 --- /dev/null +++ b/solutions/ex04/pid_locomotive_agent_TODO_2.py @@ -0,0 +1 @@ + u = self.pid.pi(x[0]) \ No newline at end of file diff --git a/solutions/ex04/pid_lunar_TODO_1.py b/solutions/ex04/pid_lunar_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..7a4671f8db2f159d0993f2b077721380e744a042 --- /dev/null +++ b/solutions/ex04/pid_lunar_TODO_1.py @@ -0,0 +1,2 @@ + alt_adj = self.pid_alt.pi( -(np.abs(x[0])- x[1]) ) + ang_adj = self.pid_ang.pi( -((.25 * np.pi) * (x[0] + x[2]) - x[4]) ) \ No newline at end of file diff --git a/solutions/ex04/pid_pendulum_TODO_1.py b/solutions/ex04/pid_pendulum_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..d21532b4596dacb5efbeccce0781561304ad8891 --- /dev/null +++ b/solutions/ex04/pid_pendulum_TODO_1.py @@ -0,0 +1,2 @@ + u = self.pid.pi(x[0]) + u = np.clip(u, self.env.action_space.low, self.env.action_space.high) \ No newline at end of file diff --git a/solutions/ex04/pid_pendulum_TODO_2.py b/solutions/ex04/pid_pendulum_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..6ae32326084e9f78717c46a596b0dc8e7174086e --- /dev/null +++ b/solutions/ex04/pid_pendulum_TODO_2.py @@ -0,0 +1 @@ + agent = PIDPendulumAgent(env, dt=env.dt, Kp=12, Ki=0, Kd=2, target_angle=0) \ No newline at end of file diff --git a/solutions/ex04/pid_pendulum_TODO_3.py b/solutions/ex04/pid_pendulum_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..388da4755975d6e6772c294f26dc97a4d2f3d9ed --- /dev/null +++ b/solutions/ex04/pid_pendulum_TODO_3.py @@ -0,0 +1 @@ + agent = PIDPendulumAgent(env, dt=env.dt, Kp=12, Ki=2, Kd=2, target_angle=np.pi/6) \ No newline at end of file diff --git a/solutions/ex05/direct_TODO_1.py b/solutions/ex05/direct_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..943a62d27dbd778b5eddd0c82122cfa0799174ad --- /dev/null +++ b/solutions/ex05/direct_TODO_1.py @@ -0,0 +1 @@ + guess = {k: solutions[i - 1]['fun'][k] for k in ['t0', 'tF', 'x', 'u'] } \ No newline at end of file diff --git a/solutions/ex05/direct_TODO_10.py b/solutions/ex05/direct_TODO_10.py new file mode 100644 index 0000000000000000000000000000000000000000..30c2fff9faabb09f8331bb95e183db33f0751221 --- /dev/null +++ b/solutions/ex05/direct_TODO_10.py @@ -0,0 +1 @@ + x_interp = xs[:, k] + tau * fs[:, k] + (tau ** 2 / (2 * hk)) * (fs[:, k + 1] - fs[:, k]) \ No newline at end of file diff --git a/solutions/ex05/direct_TODO_2.py b/solutions/ex05/direct_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..93276c3bc9f36521881d60e44ed738dfe6636d77 --- /dev/null +++ b/solutions/ex05/direct_TODO_2.py @@ -0,0 +1,2 @@ + z, z0, z_lb, z_ub = z + xs[k], z0 + list(guess['x'](tk).flat), z_lb + x_low, z_ub + x_high + z, z0, z_lb, z_ub = z + us[k], z0 + list(guess['u'](tk).flat), z_lb + u_low, z_ub + u_high \ No newline at end of file diff --git a/solutions/ex05/direct_TODO_3.py b/solutions/ex05/direct_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..2a48c18f75f3e1558e571ef4067568ee77821f80 --- /dev/null +++ b/solutions/ex05/direct_TODO_3.py @@ -0,0 +1,2 @@ + z, z0, z_lb, z_ub = z+[t0], z0+[guess['t0']], z_lb+[model.t0_bound().low[0]], z_ub+[model.t0_bound().high[0]] + z, z0, z_lb, z_ub = z+[tF], z0+[guess['tF']], z_lb+[model.tF_bound().low[0]], z_ub+[model.tF_bound().high[0]] \ No newline at end of file diff --git a/solutions/ex05/direct_TODO_4.py b/solutions/ex05/direct_TODO_4.py new file mode 100644 index 0000000000000000000000000000000000000000..a17d399eead11f94af4e6bb3c2603a16a9d5cf30 --- /dev/null +++ b/solutions/ex05/direct_TODO_4.py @@ -0,0 +1,2 @@ + fs.append(model.sym_f(x=xs[k], u=us[k], t=ts[k])) + cs.append(cost.sym_c(x=xs[k], u=us[k], t=ts[k])) \ No newline at end of file diff --git a/solutions/ex05/direct_TODO_5.py b/solutions/ex05/direct_TODO_5.py new file mode 100644 index 0000000000000000000000000000000000000000..4503612468fbb19116e8be806e3d13bbe045f6b8 --- /dev/null +++ b/solutions/ex05/direct_TODO_5.py @@ -0,0 +1,2 @@ + hk = (ts[k + 1] - ts[k]) + J += .5 * hk * (cs[k] + cs[k + 1]) \ No newline at end of file diff --git a/solutions/ex05/direct_TODO_6.py b/solutions/ex05/direct_TODO_6.py new file mode 100644 index 0000000000000000000000000000000000000000..d44ee27290c29626ebdf058261237ee65daaa73a --- /dev/null +++ b/solutions/ex05/direct_TODO_6.py @@ -0,0 +1 @@ + Ieq.append((xs[k+1][j] - xs[k][j]) - 0.5*hk*(fs[k+1][j] + fs[k][j])) \ No newline at end of file diff --git a/solutions/ex05/direct_TODO_7.py b/solutions/ex05/direct_TODO_7.py new file mode 100644 index 0000000000000000000000000000000000000000..40f3af49c7238ac22b21afc9d978e7fb5c661c7d --- /dev/null +++ b/solutions/ex05/direct_TODO_7.py @@ -0,0 +1 @@ + Iineq += model.sym_h(x=xs[k], u=us[k], t=ts[k]) \ No newline at end of file diff --git a/solutions/ex05/direct_TODO_8.py b/solutions/ex05/direct_TODO_8.py new file mode 100644 index 0000000000000000000000000000000000000000..10dcc178f95c3c856a6bc23168aa7b781101a71d --- /dev/null +++ b/solutions/ex05/direct_TODO_8.py @@ -0,0 +1 @@ + J_jac = sym.lambdify([z], sym.derive_by_array(J, z), modules='numpy') \ No newline at end of file diff --git a/solutions/ex05/direct_TODO_9.py b/solutions/ex05/direct_TODO_9.py new file mode 100644 index 0000000000000000000000000000000000000000..5ed2dd8f65a38e268a96829e5d32aa0a9602df1a --- /dev/null +++ b/solutions/ex05/direct_TODO_9.py @@ -0,0 +1,3 @@ + for k in range(len(ts) - 1): + if ts[k] <= t_new and t_new <= ts[k + 1]: + break \ No newline at end of file diff --git a/solutions/ex05/direct_agent_TODO_1.py b/solutions/ex05/direct_agent_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..dca993fcb24b2e3f44b7c0229c15225af74e5867 --- /dev/null +++ b/solutions/ex05/direct_agent_TODO_1.py @@ -0,0 +1 @@ + self.ufun = solutions[-1]['fun']['u'] \ No newline at end of file diff --git a/solutions/ex05/direct_agent_TODO_2.py b/solutions/ex05/direct_agent_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..311c0af323b01f6295f9936644d401545dc02ba5 --- /dev/null +++ b/solutions/ex05/direct_agent_TODO_2.py @@ -0,0 +1,7 @@ + t = info['time_seconds'] + if t > self.ts_grid[-1]: + print("Simulation time is", t, "which exceeds the maximal planning horizon t_F =", self.ts_grid[-1]) + raise Exception("Time exceed agents planning horizon") + + u = self.ufun(t) + u = np.asarray(self.env.discrete_model.phi_u(u)) \ No newline at end of file diff --git a/solutions/ex05/direct_cartpole_kelly_TODO_1.py b/solutions/ex05/direct_cartpole_kelly_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..c3da19ea709346d5b01a715fd7ecb31c099908ad --- /dev/null +++ b/solutions/ex05/direct_cartpole_kelly_TODO_1.py @@ -0,0 +1,2 @@ + Q = np.zeros((4, 4)) + return SymbolicQRCost(Q=Q, R=np.asarray([[1.0]]) ) \ No newline at end of file diff --git a/solutions/ex05/direct_cartpole_kelly_TODO_2.py b/solutions/ex05/direct_cartpole_kelly_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..11a9b85305f2ee81c9b9a659a221a68f1633e215 --- /dev/null +++ b/solutions/ex05/direct_cartpole_kelly_TODO_2.py @@ -0,0 +1,2 @@ + duration = 2 + return Box(duration, duration, shape=(1,)) \ No newline at end of file diff --git a/solutions/ex05/model_brachistochrone_TODO_1.py b/solutions/ex05/model_brachistochrone_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..056e07343fbf43a6270238dadee15550cc74c256 --- /dev/null +++ b/solutions/ex05/model_brachistochrone_TODO_1.py @@ -0,0 +1 @@ + cost = SymbolicQRCost(Q=np.zeros((3,3)), R = np.zeros((1,1)), qc=1.0) \ No newline at end of file diff --git a/solutions/ex05/model_brachistochrone_TODO_2.py b/solutions/ex05/model_brachistochrone_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..bce8341a0d9a7d21cb435d75bbc2a441467c406d --- /dev/null +++ b/solutions/ex05/model_brachistochrone_TODO_2.py @@ -0,0 +1,3 @@ + v = x[2] + uu = u[0] + xp = [v * sym.sin(uu), -v * sym.cos(uu), self.g * sym.cos(uu)] \ No newline at end of file diff --git a/solutions/ex05/model_brachistochrone_TODO_3.py b/solutions/ex05/model_brachistochrone_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..1e993c335fc4ac387dd382abfa490a269a7b3237 --- /dev/null +++ b/solutions/ex05/model_brachistochrone_TODO_3.py @@ -0,0 +1 @@ + return [ -x[1] - x[0]/2 - self.h ] \ No newline at end of file diff --git a/solutions/ex06/boeing_lqr_TODO_1.py b/solutions/ex06/boeing_lqr_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..649101b863582d56a84037200d9af8aa6f4f240a --- /dev/null +++ b/solutions/ex06/boeing_lqr_TODO_1.py @@ -0,0 +1 @@ + Q, R, q = compute_Q_R_q(model, dt) \ No newline at end of file diff --git a/solutions/ex06/boeing_lqr_TODO_2.py b/solutions/ex06/boeing_lqr_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..ba3dbc6d413831fcf93881aaa3cc7c57de0faa8a --- /dev/null +++ b/solutions/ex06/boeing_lqr_TODO_2.py @@ -0,0 +1 @@ + agent = LQRAgent(env, A=A, B=B, d=d, Q=Q, R=R, q=q) \ No newline at end of file diff --git a/solutions/ex06/boeing_lqr_TODO_3.py b/solutions/ex06/boeing_lqr_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..e5328ff95c26a95435a5b895e55ae51f313de1b6 --- /dev/null +++ b/solutions/ex06/boeing_lqr_TODO_3.py @@ -0,0 +1,3 @@ + Q = cost.Q * dt + R = cost.R * dt + q = cost.q * dt \ No newline at end of file diff --git a/solutions/ex06/boeing_lqr_TODO_4.py b/solutions/ex06/boeing_lqr_TODO_4.py new file mode 100644 index 0000000000000000000000000000000000000000..51001722bf435248844d7ff1017d944e8cb76515 --- /dev/null +++ b/solutions/ex06/boeing_lqr_TODO_4.py @@ -0,0 +1,2 @@ + B_discrete = scipy.linalg.inv(model.A) @ (A_discrete - np.eye(model.A.shape[0])) @ model.B + d_discrete = scipy.linalg.inv(model.A) @ (A_discrete - np.eye(model.A.shape[0])) @ d \ No newline at end of file diff --git a/solutions/ex06/dlqr_TODO_1.py b/solutions/ex06/dlqr_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..7615471215df32bb0a15c21521405bc6c5a98620 --- /dev/null +++ b/solutions/ex06/dlqr_TODO_1.py @@ -0,0 +1,2 @@ +import matplotlib +matplotlib.use('agg') \ No newline at end of file diff --git a/solutions/ex06/dlqr_TODO_2.py b/solutions/ex06/dlqr_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..f34eec89ded8b2cd4c0e26f7ae7c591acee7983c --- /dev/null +++ b/solutions/ex06/dlqr_TODO_2.py @@ -0,0 +1 @@ + V[N], v[N], vc[N] = QN, qN, qcN \ No newline at end of file diff --git a/solutions/ex06/dlqr_TODO_3.py b/solutions/ex06/dlqr_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..14bb8adaaca89f614c42617c8fc365d0942ef10a --- /dev/null +++ b/solutions/ex06/dlqr_TODO_3.py @@ -0,0 +1,4 @@ + Suu = R[k] + B[k].T @ (V[k+1] + mu * In) @ B[k] + Sux = H[k] + B[k].T @ (V[k+1] + mu * In) @ A[k] + Su = r[k] + B[k].T @ v[k + 1] + B[k].T @ V[k + 1] @ d[k] + L[k] = -np.linalg.solve(Suu, Sux) \ No newline at end of file diff --git a/solutions/ex06/lqr_agent_TODO_1.py b/solutions/ex06/lqr_agent_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..f82d69ea2e0a0dd9908e4225fc5d91d15aeba58d --- /dev/null +++ b/solutions/ex06/lqr_agent_TODO_1.py @@ -0,0 +1 @@ + (self.L, self.l), _ = LQR(A=[A]*N, B=[B]*N, d=[d]*N if d is not None else None, Q=[Q]*N, q=[q]*N if q is not None else None, R=[R]*N) \ No newline at end of file diff --git a/solutions/ex06/lqr_agent_TODO_2.py b/solutions/ex06/lqr_agent_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..e8994b09a036a2f4a3fdc42140cef04aa9f56c86 --- /dev/null +++ b/solutions/ex06/lqr_agent_TODO_2.py @@ -0,0 +1 @@ + u = self.L[k] @ x + self.l[k] \ No newline at end of file diff --git a/solutions/ex06/lqr_pid_TODO_1.py b/solutions/ex06/lqr_pid_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..920d0e7e101dad7c87a2df2be8ecd3209a946a8d --- /dev/null +++ b/solutions/ex06/lqr_pid_TODO_1.py @@ -0,0 +1,3 @@ + def pi(self,x, k, info=None): + action = self.L[0] @ x + self.l[0] + return action \ No newline at end of file diff --git a/solutions/ex06/lqr_pid_TODO_2.py b/solutions/ex06/lqr_pid_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..2f1ad2ab157ba7524df0a89e73c93cdf860e398f --- /dev/null +++ b/solutions/ex06/lqr_pid_TODO_2.py @@ -0,0 +1 @@ + Kp, Kd = (-L0).flat \ No newline at end of file diff --git a/solutions/ex07/ilqr_TODO_1.py b/solutions/ex07/ilqr_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..c9ada2c3a7adcd0a1d3995bd723716a749f4c08e --- /dev/null +++ b/solutions/ex07/ilqr_TODO_1.py @@ -0,0 +1,2 @@ + l, L = [np.zeros((m,))]*N, [np.zeros((m,n))]*N + x_bar, u_bar = forward_pass(model, x_bar, u_bar, L=L, l=l) \ No newline at end of file diff --git a/solutions/ex07/ilqr_TODO_10.py b/solutions/ex07/ilqr_TODO_10.py new file mode 100644 index 0000000000000000000000000000000000000000..97423181e2f17ec2e986788e79b4fd978a7c6838 --- /dev/null +++ b/solutions/ex07/ilqr_TODO_10.py @@ -0,0 +1 @@ + Delta, mu = max(1.0, Delta) * Delta_0, max(mu_min, mu * Delta) # Increase \ No newline at end of file diff --git a/solutions/ex07/ilqr_TODO_11.py b/solutions/ex07/ilqr_TODO_11.py new file mode 100644 index 0000000000000000000000000000000000000000..dafc65dda3f1d6f36c21e3a0a612e97e4606bafb --- /dev/null +++ b/solutions/ex07/ilqr_TODO_11.py @@ -0,0 +1,4 @@ + R = c_uu + H = c_ux + q, qN = c_x[:-1], c_x[-1] + r = c_u \ No newline at end of file diff --git a/solutions/ex07/ilqr_TODO_12.py b/solutions/ex07/ilqr_TODO_12.py new file mode 100644 index 0000000000000000000000000000000000000000..b5823c6a0680218628af756b10761f3211c04501 --- /dev/null +++ b/solutions/ex07/ilqr_TODO_12.py @@ -0,0 +1,4 @@ + # fs = [(v[1],v[2]) for v in [model.f(x, u, k, compute_jacobian=True) for k, (x, u) in enumerate(zip(x_bar[:-1], u_bar))]] + fs = [model.f_jacobian(x, u, k) for k, (x, u) in enumerate(zip(x_bar[:-1], u_bar))] + + A, B = zip(*fs) \ No newline at end of file diff --git a/solutions/ex07/ilqr_TODO_13.py b/solutions/ex07/ilqr_TODO_13.py new file mode 100644 index 0000000000000000000000000000000000000000..41ceba56deb0495616a22805f7a38bb9733ec181 --- /dev/null +++ b/solutions/ex07/ilqr_TODO_13.py @@ -0,0 +1,2 @@ + gs = [model.cost.c(x, u, i, compute_gradients=True) for i, (x, u) in enumerate(zip(x_bar[:-1], u_bar))] + c, c_x, c_u, c_xx, c_ux, c_uu = zip(*gs) \ No newline at end of file diff --git a/solutions/ex07/ilqr_TODO_14.py b/solutions/ex07/ilqr_TODO_14.py new file mode 100644 index 0000000000000000000000000000000000000000..8c925083e59871befab170fb5ec809e6311e5452 --- /dev/null +++ b/solutions/ex07/ilqr_TODO_14.py @@ -0,0 +1,3 @@ + c = c + (cN,) + c_x = c_x + (c_xN,) + c_xx = c_xx + (c_xxN,) \ No newline at end of file diff --git a/solutions/ex07/ilqr_TODO_15.py b/solutions/ex07/ilqr_TODO_15.py new file mode 100644 index 0000000000000000000000000000000000000000..73a0fa4ac59c0fedc7f4d7e87645ab4372a80399 --- /dev/null +++ b/solutions/ex07/ilqr_TODO_15.py @@ -0,0 +1 @@ + u_star[i] = u_bar[i] + alpha * l[i] + L[i] @ (x[i] - x_bar[i]) \ No newline at end of file diff --git a/solutions/ex07/ilqr_TODO_16.py b/solutions/ex07/ilqr_TODO_16.py new file mode 100644 index 0000000000000000000000000000000000000000..20904f4a15cb7b9db30da28ccb88bb21a1bb040d --- /dev/null +++ b/solutions/ex07/ilqr_TODO_16.py @@ -0,0 +1 @@ + x[i + 1] = model.f(x[i], u_star[i], i) \ No newline at end of file diff --git a/solutions/ex07/ilqr_TODO_2.py b/solutions/ex07/ilqr_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..a5ff3ca1047f1886bf4e94a5705d33b3eb88f1fc --- /dev/null +++ b/solutions/ex07/ilqr_TODO_2.py @@ -0,0 +1,2 @@ + A, B, c, c_x, c_u, c_xx, c_ux, c_uu = get_derivatives(model, x_bar, u_bar) + J = sum(c) \ No newline at end of file diff --git a/solutions/ex07/ilqr_TODO_3.py b/solutions/ex07/ilqr_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..417755d3e4956fbbf5bc0bb596377781412cb47b --- /dev/null +++ b/solutions/ex07/ilqr_TODO_3.py @@ -0,0 +1 @@ + L, l = backward_pass(A, B, c_x, c_u, c_xx, c_ux, c_uu, mu) \ No newline at end of file diff --git a/solutions/ex07/ilqr_TODO_4.py b/solutions/ex07/ilqr_TODO_4.py new file mode 100644 index 0000000000000000000000000000000000000000..6db866f5b27f7921e409d179462e51c8b8ea1420 --- /dev/null +++ b/solutions/ex07/ilqr_TODO_4.py @@ -0,0 +1 @@ + x_bar, u_bar = forward_pass(model, x_bar, u_bar, L=L, l=l, alpha=alpha) \ No newline at end of file diff --git a/solutions/ex07/ilqr_TODO_5.py b/solutions/ex07/ilqr_TODO_5.py new file mode 100644 index 0000000000000000000000000000000000000000..4ead664be5795faaebda22e5321ae4673eefdebf --- /dev/null +++ b/solutions/ex07/ilqr_TODO_5.py @@ -0,0 +1,2 @@ + l, L = [np.zeros((m,))] * N, [np.zeros((m, n))] * N + x_bar, u_bar = forward_pass(model, x_bar, u_bar, L=L, l=l) \ No newline at end of file diff --git a/solutions/ex07/ilqr_TODO_6.py b/solutions/ex07/ilqr_TODO_6.py new file mode 100644 index 0000000000000000000000000000000000000000..2e7b84c3c79530d3e24b420e11f4d15140e6fcd9 --- /dev/null +++ b/solutions/ex07/ilqr_TODO_6.py @@ -0,0 +1,2 @@ + A, B, c, c_x, c_u, c_xx, c_ux, c_uu = get_derivatives(model, x_bar, u_bar) + J_prime = sum(c) \ No newline at end of file diff --git a/solutions/ex07/ilqr_TODO_7.py b/solutions/ex07/ilqr_TODO_7.py new file mode 100644 index 0000000000000000000000000000000000000000..f23d1c9bd15a76d13938844705782aa675cc9e7e --- /dev/null +++ b/solutions/ex07/ilqr_TODO_7.py @@ -0,0 +1 @@ + L, l = backward_pass(A, B, c_x, c_u, c_xx, c_ux, c_uu, mu) \ No newline at end of file diff --git a/solutions/ex07/ilqr_TODO_8.py b/solutions/ex07/ilqr_TODO_8.py new file mode 100644 index 0000000000000000000000000000000000000000..123b9bb988a13d8f828a0af3b37c6a9f213495ff --- /dev/null +++ b/solutions/ex07/ilqr_TODO_8.py @@ -0,0 +1 @@ + J_new = cost_of_trajectory(model, x_hat, u_hat) \ No newline at end of file diff --git a/solutions/ex07/ilqr_TODO_9.py b/solutions/ex07/ilqr_TODO_9.py new file mode 100644 index 0000000000000000000000000000000000000000..1fe4de670d25185a976a1fb36ae4eb04e539a456 --- /dev/null +++ b/solutions/ex07/ilqr_TODO_9.py @@ -0,0 +1 @@ + Delta, mu = min(1.0, Delta) / Delta_0, max(0, mu*Delta) \ No newline at end of file diff --git a/solutions/ex07/ilqr_agent_TODO_1.py b/solutions/ex07/ilqr_agent_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..5632d146890b0bcadefd5b04fccf7393338debcf --- /dev/null +++ b/solutions/ex07/ilqr_agent_TODO_1.py @@ -0,0 +1 @@ + u = self.ubar[k] + self.L[k]@ (x-self.xbar[k]) + self.l[k] \ No newline at end of file diff --git a/solutions/ex07/ilqr_pendulum_TODO_1.py b/solutions/ex07/ilqr_pendulum_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..dee07f700ec7c23c71a96ddb29e0e4e1eb9ba7e5 --- /dev/null +++ b/solutions/ex07/ilqr_pendulum_TODO_1.py @@ -0,0 +1 @@ + xs, us, J_hist, L, l = ilqr(model, N, x0, n_iter=n_iter, use_linesearch=use_linesearch) \ No newline at end of file diff --git a/solutions/ex07/linearization_agent_TODO_1.py b/solutions/ex07/linearization_agent_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..9a9c162e64d62675433a1f5174da17654e528e42 --- /dev/null +++ b/solutions/ex07/linearization_agent_TODO_1.py @@ -0,0 +1,4 @@ + xp = model.f(xbar, ubar, k=0) + A, B = model.f_jacobian(xbar, ubar, k=0) + + d = xp - A @ xbar - B @ ubar \ No newline at end of file diff --git a/solutions/ex07/linearization_agent_TODO_2.py b/solutions/ex07/linearization_agent_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..f84fac4dd7bf0800e2734cee2d913762b410355e --- /dev/null +++ b/solutions/ex07/linearization_agent_TODO_2.py @@ -0,0 +1 @@ + (self.L, self.l), (V, v, vc) = LQR(A=[A]*N, B=[B]*N, d=[d]*N, Q=[Q]*N, q=[q]*N, R=[self.model.cost.R]*N) \ No newline at end of file diff --git a/solutions/ex07/linearization_agent_TODO_3.py b/solutions/ex07/linearization_agent_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..797bd934a4c3c08c245bbdbe956a9008510367a8 --- /dev/null +++ b/solutions/ex07/linearization_agent_TODO_3.py @@ -0,0 +1 @@ + u = self.L[0] @ x + self.l[0] \ No newline at end of file diff --git a/solutions/ex08/bandits_TODO_1.py b/solutions/ex08/bandits_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..f971a9c7fb74dc21be27bd92f6e8c5aec3af804b --- /dev/null +++ b/solutions/ex08/bandits_TODO_1.py @@ -0,0 +1,2 @@ + reward = self.q_star[a] + np.random.randn() + regret = self.q_star[self.optimal_action] - self.q_star[a] \ No newline at end of file diff --git a/solutions/ex08/gradient_agent_TODO_1.py b/solutions/ex08/gradient_agent_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..2c166f9787f05f6acd9b977fe814558616388c3d --- /dev/null +++ b/solutions/ex08/gradient_agent_TODO_1.py @@ -0,0 +1,9 @@ + pi_a = self.Pa() + for b in range(self.k): + if b == a: + self.H[b] += self.alpha * (r - self.R_bar) * (1 - pi_a[b]) + else: + self.H[b] -= self.alpha * (r - self.R_bar) * pi_a[b] + + if self.baseline: + self.R_bar = self.R_bar + (self.alpha if self.alpha is not None else 1/(self.t+1)) * (r - self.R_bar) \ No newline at end of file diff --git a/solutions/ex08/grand_bandit_race_TODO_1.py b/solutions/ex08/grand_bandit_race_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..06e8845cc54405e1d5cac07b51eb414b9170e1ef --- /dev/null +++ b/solutions/ex08/grand_bandit_race_TODO_1.py @@ -0,0 +1 @@ + bandit1 = StationaryBandit(k=10) \ No newline at end of file diff --git a/solutions/ex08/grand_bandit_race_TODO_2.py b/solutions/ex08/grand_bandit_race_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..c9ffb8db36b8c1313eab01a4668a45f30d3cc243 --- /dev/null +++ b/solutions/ex08/grand_bandit_race_TODO_2.py @@ -0,0 +1,5 @@ + agents = [BasicAgent(bandit1, epsilon=epsilon)] + agents += [MovingAverageAgent(bandit1, epsilon=epsilon, alpha=alpha)] + agents += [GradientAgent(bandit1, alpha=alpha,use_baseline=False) ] + agents += [GradientAgent(bandit1, alpha=alpha,use_baseline=True) ] + agents += [UCBAgent(bandit1, c=2)] \ No newline at end of file diff --git a/solutions/ex08/grand_bandit_race_TODO_3.py b/solutions/ex08/grand_bandit_race_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..807579c172456281f047d47aa499d87869460af6 --- /dev/null +++ b/solutions/ex08/grand_bandit_race_TODO_3.py @@ -0,0 +1 @@ + eval_and_plot(bandit1, agents, max_episodes=2000, labels=labels) \ No newline at end of file diff --git a/solutions/ex08/grand_bandit_race_TODO_4.py b/solutions/ex08/grand_bandit_race_TODO_4.py new file mode 100644 index 0000000000000000000000000000000000000000..3a9cb82fb4ef4d9adbd1f7d726d53189b2827710 --- /dev/null +++ b/solutions/ex08/grand_bandit_race_TODO_4.py @@ -0,0 +1 @@ + bandit2 = StationaryBandit(k=10, q_star_mean=4) \ No newline at end of file diff --git a/solutions/ex08/grand_bandit_race_TODO_5.py b/solutions/ex08/grand_bandit_race_TODO_5.py new file mode 100644 index 0000000000000000000000000000000000000000..1a6cfc7bd723679fac58db343018033760fcd8f9 --- /dev/null +++ b/solutions/ex08/grand_bandit_race_TODO_5.py @@ -0,0 +1 @@ + eval_and_plot(bandit2, agents, max_episodes=2000, labels=labels) \ No newline at end of file diff --git a/solutions/ex08/grand_bandit_race_TODO_6.py b/solutions/ex08/grand_bandit_race_TODO_6.py new file mode 100644 index 0000000000000000000000000000000000000000..20c9ba027fd3dc0a70ea72bf97622c69031199f3 --- /dev/null +++ b/solutions/ex08/grand_bandit_race_TODO_6.py @@ -0,0 +1 @@ + bandit3 = NonstationaryBandit(k=10) \ No newline at end of file diff --git a/solutions/ex08/grand_bandit_race_TODO_7.py b/solutions/ex08/grand_bandit_race_TODO_7.py new file mode 100644 index 0000000000000000000000000000000000000000..a2a5676b54581b39119cddc327bdf79459f97f57 --- /dev/null +++ b/solutions/ex08/grand_bandit_race_TODO_7.py @@ -0,0 +1 @@ + eval_and_plot(bandit3, agents, max_episodes=2000, steps=10000, labels=labels) \ No newline at end of file diff --git a/solutions/ex08/grand_bandit_race_TODO_8.py b/solutions/ex08/grand_bandit_race_TODO_8.py new file mode 100644 index 0000000000000000000000000000000000000000..b34bc62119f16531be2e9792bf8b442f365132d2 --- /dev/null +++ b/solutions/ex08/grand_bandit_race_TODO_8.py @@ -0,0 +1 @@ + eval_and_plot(bandit1, agents2, steps=10000, labels=labels) \ No newline at end of file diff --git a/solutions/ex08/nonstationary_TODO_1.py b/solutions/ex08/nonstationary_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..e53da8bcceffb8f6b6c54f0cdfe39eaef44f9959 --- /dev/null +++ b/solutions/ex08/nonstationary_TODO_1.py @@ -0,0 +1,2 @@ + self.q_star += self.reward_change_std * np.random.randn(self.k) + self.optimal_action = np.argmax(self.q_star) \ No newline at end of file diff --git a/solutions/ex08/nonstationary_TODO_2.py b/solutions/ex08/nonstationary_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..26dc95b4931d7fab277c37fff8b5894f3315c332 --- /dev/null +++ b/solutions/ex08/nonstationary_TODO_2.py @@ -0,0 +1,2 @@ + self.alpha=alpha + super().__init__(env, epsilon=epsilon) \ No newline at end of file diff --git a/solutions/ex08/nonstationary_TODO_3.py b/solutions/ex08/nonstationary_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..d45b3a0f69beac0af8bfd8249534906f13bdd167 --- /dev/null +++ b/solutions/ex08/nonstationary_TODO_3.py @@ -0,0 +1 @@ + self.Q[a] = self.Q[a] + self.alpha * (r-self.Q[a]) \ No newline at end of file diff --git a/solutions/ex08/nonstationary_TODO_4.py b/solutions/ex08/nonstationary_TODO_4.py new file mode 100644 index 0000000000000000000000000000000000000000..e4ffd80476a720982fcd5655d5b58f00ee8e6d42 --- /dev/null +++ b/solutions/ex08/nonstationary_TODO_4.py @@ -0,0 +1,4 @@ + bandit = NonstationaryBandit(k=10) + + agents = [BasicAgent(bandit, epsilon=epsilon)] + agents += [MovingAverageAgent(bandit, epsilon=epsilon, alpha=alpha) for alpha in alphas] \ No newline at end of file diff --git a/solutions/ex08/nonstationary_TODO_5.py b/solutions/ex08/nonstationary_TODO_5.py new file mode 100644 index 0000000000000000000000000000000000000000..9742313984f8906af30c34cac26785b1b2ec8791 --- /dev/null +++ b/solutions/ex08/nonstationary_TODO_5.py @@ -0,0 +1 @@ + labels += [f"Mov.avg. agent, epsilon={epsilon}, alpha={alpha}" for alpha in alphas] \ No newline at end of file diff --git a/solutions/ex08/simple_agents_TODO_1.py b/solutions/ex08/simple_agents_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..5416e07d0958fe82276fa186510f0402eafc7ece --- /dev/null +++ b/solutions/ex08/simple_agents_TODO_1.py @@ -0,0 +1,2 @@ + self.Q = np.zeros((self.k,)) + self.N = np.zeros((self.k,)) \ No newline at end of file diff --git a/solutions/ex08/simple_agents_TODO_2.py b/solutions/ex08/simple_agents_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..d91b3938f2b78d14de98a867793d8b33f61cae55 --- /dev/null +++ b/solutions/ex08/simple_agents_TODO_2.py @@ -0,0 +1 @@ + return np.random.randint(self.k) if np.random.rand() < self.epsilon else np.argmax(self.Q) \ No newline at end of file diff --git a/solutions/ex08/simple_agents_TODO_3.py b/solutions/ex08/simple_agents_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..df218f01d8927b72f1920bc2841a8f2ae0b0e616 --- /dev/null +++ b/solutions/ex08/simple_agents_TODO_3.py @@ -0,0 +1,2 @@ + self.N[a] = self.N[a] + 1 + self.Q[a] = self.Q[a] + 1/self.N[a] * (r-self.Q[a]) \ No newline at end of file diff --git a/solutions/ex08/ucb_agent_TODO_1.py b/solutions/ex08/ucb_agent_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..4812f63b0e4fc5378d676a4d2e0459c3d2b07ca8 --- /dev/null +++ b/solutions/ex08/ucb_agent_TODO_1.py @@ -0,0 +1,2 @@ + self.N[a] += 1 + self.Q[a] += 1/self.N[a] * (r - self.Q[a]) \ No newline at end of file diff --git a/solutions/ex08/ucb_agent_TODO_2.py b/solutions/ex08/ucb_agent_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..437563cd1c647bce69dc8778e93c5bf9854f0a5c --- /dev/null +++ b/solutions/ex08/ucb_agent_TODO_2.py @@ -0,0 +1,3 @@ + k = self.env.action_space.n + self.Q = np.zeros((k,)) + self.N = np.zeros((k,)) \ No newline at end of file diff --git a/solutions/ex08/ucb_agent_TODO_3.py b/solutions/ex08/ucb_agent_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..59255040547725524492f30aa7b91b3267f04c27 --- /dev/null +++ b/solutions/ex08/ucb_agent_TODO_3.py @@ -0,0 +1 @@ + return np.argmax( self.Q + self.c * np.sqrt( np.log(k+1)/(self.N+1e-8) ) ) \ No newline at end of file diff --git a/solutions/ex09/gambler_TODO_1.py b/solutions/ex09/gambler_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..5edd9179f3c7d5cecdf4ed62874f892d3b277a49 --- /dev/null +++ b/solutions/ex09/gambler_TODO_1.py @@ -0,0 +1 @@ + return state in [0, self.goal] \ No newline at end of file diff --git a/solutions/ex09/gambler_TODO_2.py b/solutions/ex09/gambler_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..63c4cf777199a52f2eea1c333f5eb98a692b6e61 --- /dev/null +++ b/solutions/ex09/gambler_TODO_2.py @@ -0,0 +1 @@ + return list( range(1, min(s, self.goal - s) + 1)) \ No newline at end of file diff --git a/solutions/ex09/gambler_TODO_3.py b/solutions/ex09/gambler_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..b4e0a660fca2209ea59af40b1b18bc7aad4e9c39 --- /dev/null +++ b/solutions/ex09/gambler_TODO_3.py @@ -0,0 +1,4 @@ + r = 1 if s + a == 100 else 0 + WIN = (s+a, r) + LOSS = (s-a, 0) + outcome_dict = {WIN: self.p_heads, LOSS: 1-self.p_heads } if WIN != LOSS else {WIN: 1.} \ No newline at end of file diff --git a/solutions/ex09/jacks_car_rental_TODO_1.py b/solutions/ex09/jacks_car_rental_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..14da723df9fff8944cca36e6f08033a1d9ba4cb7 --- /dev/null +++ b/solutions/ex09/jacks_car_rental_TODO_1.py @@ -0,0 +1,3 @@ + max_from_1 = min([self.max_move,c1]) + max_to_1 = min([self.max_move,c2]) + a = [s for s in range(-max_to_1, max_from_1 + 1 )] \ No newline at end of file diff --git a/solutions/ex09/jacks_car_rental_TODO_2.py b/solutions/ex09/jacks_car_rental_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..cf19c18a8ff14200b83dc572dade4894b8bd8c29 --- /dev/null +++ b/solutions/ex09/jacks_car_rental_TODO_2.py @@ -0,0 +1 @@ + s = (s[0]-a, s[1]+a) \ No newline at end of file diff --git a/solutions/ex09/jacks_car_rental_TODO_3.py b/solutions/ex09/jacks_car_rental_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..097e1969082e3be96b8a849084f5a8e35a3461e6 --- /dev/null +++ b/solutions/ex09/jacks_car_rental_TODO_3.py @@ -0,0 +1 @@ + d[((c1, c2), reward_1 + reward_2 + abs(a)*self.move_cost) ] += pc1 * pc2 \ No newline at end of file diff --git a/solutions/ex09/mdp_warmup_TODO_1.py b/solutions/ex09/mdp_warmup_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..b8ee7db7c1bc9e038ea07feff49720d151785490 --- /dev/null +++ b/solutions/ex09/mdp_warmup_TODO_1.py @@ -0,0 +1 @@ + q_dict = {a: sum([p*(r+ (gamma*v[sp] if not mdp.is_terminal(sp) else 0)) for (sp,r), p in mdp.Psr(s,a).items()]) for a in mdp.A(s)} \ No newline at end of file diff --git a/solutions/ex09/mdp_warmup_TODO_2.py b/solutions/ex09/mdp_warmup_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..f605ec389d9213a9f951ad32bf2d96ed93f83424 --- /dev/null +++ b/solutions/ex09/mdp_warmup_TODO_2.py @@ -0,0 +1 @@ + raise NotImplementedError("Insert your solution and remove this error.") \ No newline at end of file diff --git a/solutions/ex09/mdp_warmup_TODO_3.py b/solutions/ex09/mdp_warmup_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..c8f9a461ff9e0126baa93f2f1abbf35d365a52d6 --- /dev/null +++ b/solutions/ex09/mdp_warmup_TODO_3.py @@ -0,0 +1 @@ + expected_reward = sum( [r * p for (sp, r), p in mdp.Psr(s, a).items() ] ) \ No newline at end of file diff --git a/solutions/ex09/mdp_warmup_TODO_4.py b/solutions/ex09/mdp_warmup_TODO_4.py new file mode 100644 index 0000000000000000000000000000000000000000..bb8d2810f8b9e635f333b72539f47e18035d818e --- /dev/null +++ b/solutions/ex09/mdp_warmup_TODO_4.py @@ -0,0 +1 @@ + V_s = sum( [Q[s,a] * p for a, p in policy.items()] ) \ No newline at end of file diff --git a/solutions/ex09/policy_evaluation_TODO_1.py b/solutions/ex09/policy_evaluation_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..290d5ab43f92e519b08e9adbd01916b4f70cd81b --- /dev/null +++ b/solutions/ex09/policy_evaluation_TODO_1.py @@ -0,0 +1,2 @@ + q = value_function2q_function(mdp, s, gamma, v) + v_, v[s] = v[s], sum( [q[a] * pi_a for a,pi_a in pi[s].items()] ) \ No newline at end of file diff --git a/solutions/ex09/policy_iteration_TODO_1.py b/solutions/ex09/policy_iteration_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..00c8a957fb3b6a2650aec38a6da1f8fe541ea5ee --- /dev/null +++ b/solutions/ex09/policy_iteration_TODO_1.py @@ -0,0 +1,6 @@ + for s in [mdp.nonterminal_states[i] for i in np.random.permutation(len(mdp.nonterminal_states))]: + old_a = pi[s] # The best action we would take under the current policy + Qs = value_function2q_function(mdp, s, gamma, V) + pi[s] = max(Qs, key=Qs.get) + if old_a != pi[s]: + policy_stable = False \ No newline at end of file diff --git a/solutions/ex09/value_iteration_TODO_1.py b/solutions/ex09/value_iteration_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..d07abe42531f4a01ecb6e6ef69b30abdaef4c0b1 --- /dev/null +++ b/solutions/ex09/value_iteration_TODO_1.py @@ -0,0 +1,2 @@ + v, V[s] = V[s], max(value_function2q_function(mdp, s, gamma, V).values()) if len(mdp.A(s)) > 0 else 0 + Delta = max(Delta, np.abs(v - V[s])) \ No newline at end of file diff --git a/solutions/ex09/value_iteration_TODO_2.py b/solutions/ex09/value_iteration_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..89339fefc3a2ea8871c75d63d26586b2e07f0f1b --- /dev/null +++ b/solutions/ex09/value_iteration_TODO_2.py @@ -0,0 +1,2 @@ + Q = {a: v-(1e-8*a if isinstance(a, int) else 0) for a,v in value_function2q_function(mdp, s, gamma, V).items()} + pi[s] = max(Q, key=Q.get) \ No newline at end of file diff --git a/solutions/ex09/value_iteration_agent_TODO_1.py b/solutions/ex09/value_iteration_agent_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..49090726ea7151cfdb717088aa6b371b78baee41 --- /dev/null +++ b/solutions/ex09/value_iteration_agent_TODO_1.py @@ -0,0 +1 @@ + self.policy, self.v = value_iteration(mdp, gamma=gamma, **kwargs) \ No newline at end of file diff --git a/solutions/ex09/value_iteration_agent_TODO_2.py b/solutions/ex09/value_iteration_agent_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..5a41f1466ec31a8bbec8921330340cf1693f5de2 --- /dev/null +++ b/solutions/ex09/value_iteration_agent_TODO_2.py @@ -0,0 +1 @@ + action = self.policy[s] \ No newline at end of file diff --git a/solutions/ex10/mc_agent_TODO_1.py b/solutions/ex10/mc_agent_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..ae75aadcb9b72b66929a305601827d0ffe5c351b --- /dev/null +++ b/solutions/ex10/mc_agent_TODO_1.py @@ -0,0 +1,2 @@ + G = gamma * G + episode[t][2] + sa_t = episode[t][:2] \ No newline at end of file diff --git a/solutions/ex10/mc_agent_TODO_2.py b/solutions/ex10/mc_agent_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..0c62b909b68e8d986512f7a887cee22389c69203 --- /dev/null +++ b/solutions/ex10/mc_agent_TODO_2.py @@ -0,0 +1 @@ + returns.append( sa_t + (G,) ) \ No newline at end of file diff --git a/solutions/ex10/mc_agent_TODO_3.py b/solutions/ex10/mc_agent_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..dd67432e760d24ed7c8838b56ace568982d756bb --- /dev/null +++ b/solutions/ex10/mc_agent_TODO_3.py @@ -0,0 +1 @@ + return self.pi_eps(s, info) \ No newline at end of file diff --git a/solutions/ex10/mc_agent_TODO_4.py b/solutions/ex10/mc_agent_TODO_4.py new file mode 100644 index 0000000000000000000000000000000000000000..a910769dd5d99550a32fc31a3761aebf3a679d4c --- /dev/null +++ b/solutions/ex10/mc_agent_TODO_4.py @@ -0,0 +1,12 @@ + self.episode.append((s, a, r)) + if done: + returns = get_MC_return_SA(self.episode, self.gamma, self.first_visit) + for s, a, G in returns: + # s,a = sa + if self.alpha is None: + self.returns_sum[s, a] += G + self.returns_count[s, a] += 1 + self.Q[s, a] = self.returns_sum[s, a] / self.returns_count[s, a] + else: + self.Q[s, a] += self.alpha * (G - self.Q[s, a]) + self.episode = [] \ No newline at end of file diff --git a/solutions/ex10/mc_agent_blackjack_TODO_1.py b/solutions/ex10/mc_agent_blackjack_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..7d71b63bbe2107ab467f6ceeaf4e77dd46aee1cc --- /dev/null +++ b/solutions/ex10/mc_agent_blackjack_TODO_1.py @@ -0,0 +1 @@ + train(env, agent, expn, num_episodes=episodes, return_trajectory=False) \ No newline at end of file diff --git a/solutions/ex10/mc_evaluate_TODO_1.py b/solutions/ex10/mc_evaluate_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..a4192abc18fc36355ac9e6361da9325d2df65cfb --- /dev/null +++ b/solutions/ex10/mc_evaluate_TODO_1.py @@ -0,0 +1,2 @@ + G = gamma * G + episode[t][2] + s_t = episode[t][0] \ No newline at end of file diff --git a/solutions/ex10/mc_evaluate_TODO_2.py b/solutions/ex10/mc_evaluate_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..d653590006dd94c813935fbb5dc94a6b7012ffa6 --- /dev/null +++ b/solutions/ex10/mc_evaluate_TODO_2.py @@ -0,0 +1 @@ + returns.append((s_t, G)) \ No newline at end of file diff --git a/solutions/ex10/mc_evaluate_TODO_3.py b/solutions/ex10/mc_evaluate_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..9f00d39a4727224001cd994508cfea57c463cc2a --- /dev/null +++ b/solutions/ex10/mc_evaluate_TODO_3.py @@ -0,0 +1 @@ + self.v[s] = self.v[s] + self.alpha * (G - self.v[s]) \ No newline at end of file diff --git a/solutions/ex10/mc_evaluate_TODO_4.py b/solutions/ex10/mc_evaluate_TODO_4.py new file mode 100644 index 0000000000000000000000000000000000000000..587cd32680705be5b8039bfc14b5dbf42312e690 --- /dev/null +++ b/solutions/ex10/mc_evaluate_TODO_4.py @@ -0,0 +1,3 @@ + self.returns_sum_S[s] += G + self.returns_count_N[s] += 1.0 + self.v[s] = self.returns_sum_S[s] / self.returns_count_N[s] \ No newline at end of file diff --git a/solutions/ex10/mc_evaluate_TODO_5.py b/solutions/ex10/mc_evaluate_TODO_5.py new file mode 100644 index 0000000000000000000000000000000000000000..851276319c82264a4f758e6d2f108ee28b25e841 --- /dev/null +++ b/solutions/ex10/mc_evaluate_TODO_5.py @@ -0,0 +1 @@ + agent_every = MCEvaluationAgent(env, gamma=gamma, first_visit=False) \ No newline at end of file diff --git a/solutions/ex10/mc_evaluate_TODO_6.py b/solutions/ex10/mc_evaluate_TODO_6.py new file mode 100644 index 0000000000000000000000000000000000000000..67af451908b3dca96d9ceb318f1098c1e879cbad --- /dev/null +++ b/solutions/ex10/mc_evaluate_TODO_6.py @@ -0,0 +1 @@ + train(env, agent_every, num_episodes=episodes, verbose=False) \ No newline at end of file diff --git a/solutions/ex10/mc_evaluate_blackjack_TODO_1.py b/solutions/ex10/mc_evaluate_blackjack_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..ddcf6a0bb4c8617c85843d28abbdb017afebc130 --- /dev/null +++ b/solutions/ex10/mc_evaluate_blackjack_TODO_1.py @@ -0,0 +1 @@ + return 0 if s[0] >= 20 else 1 \ No newline at end of file diff --git a/solutions/ex10/mc_evaluate_blackjack_TODO_2.py b/solutions/ex10/mc_evaluate_blackjack_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..6918a6d557e57f850439242ab12979dd735959eb --- /dev/null +++ b/solutions/ex10/mc_evaluate_blackjack_TODO_2.py @@ -0,0 +1,2 @@ + agent = MCEvaluationAgent(env, policy=policy20, gamma=1) + train(env, agent, experiment_name=experiment, num_episodes=episodes) \ No newline at end of file diff --git a/solutions/ex10/question_td0_TODO_1.py b/solutions/ex10/question_td0_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..05e6e865fb5cc20f929632637b5be7fb6fb16024 --- /dev/null +++ b/solutions/ex10/question_td0_TODO_1.py @@ -0,0 +1,5 @@ + deltas = [] + for t, (s, r) in enumerate(zip(states[:-1], rewards)): + sp = states[t + 1] + delta = (r + gamma * v[sp]) - v[s] + deltas.append(delta) \ No newline at end of file diff --git a/solutions/ex10/question_td0_TODO_2.py b/solutions/ex10/question_td0_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..a32f983c69a6e90c69f19883548dd822722ef1ca --- /dev/null +++ b/solutions/ex10/question_td0_TODO_2.py @@ -0,0 +1,6 @@ + for t in range(len(rewards)): + s = states[t] + sp = states[t + 1] + r = rewards[t] + delta = r + gamma * v[sp] - v[s] + v[s] = v[s] + alpha * delta \ No newline at end of file diff --git a/solutions/ex10/question_td0_TODO_3.py b/solutions/ex10/question_td0_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..bceb6387836f6f75e597c150ad53acfbb18c0258 --- /dev/null +++ b/solutions/ex10/question_td0_TODO_3.py @@ -0,0 +1,4 @@ + deltas = a_compute_deltas(v, states, rewards, gamma) + for t in range(len(rewards)): + s = states[t] + v[s] = v[s] + alpha * deltas[t] \ No newline at end of file diff --git a/solutions/ex10/random_walk_example_TODO_1.py b/solutions/ex10/random_walk_example_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..e0f1b9919862220c6891bf28b3dff5bd0bd6ce84 --- /dev/null +++ b/solutions/ex10/random_walk_example_TODO_1.py @@ -0,0 +1 @@ + sp = s+(2*a-1) \ No newline at end of file diff --git a/solutions/ex10/td0_evaluate_TODO_1.py b/solutions/ex10/td0_evaluate_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..6953d59eb1b823953d481c6ad65c53898375b547 --- /dev/null +++ b/solutions/ex10/td0_evaluate_TODO_1.py @@ -0,0 +1,3 @@ + if isinstance(s, np.ndarray): + print("Bad type.") + self.v[s] += self.alpha * (r + self.gamma * (self.v[sp] if not done else 0) - self.v[s]) \ No newline at end of file diff --git a/solutions/ex11/nstep_sarsa_agent_TODO_1.py b/solutions/ex11/nstep_sarsa_agent_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..388556a4170a7877d0811962cb7c553de5e772c5 --- /dev/null +++ b/solutions/ex11/nstep_sarsa_agent_TODO_1.py @@ -0,0 +1,4 @@ + G = sum([self.gamma**(i-tau-1)*self.R[i%(n+1)] for i in range(tau+1, min(tau+n, T)+1)]) + S_tau_n, A_tau_n = self.S[(tau+n)%(n+1)], self.A[(tau+n)%(n+1)] + if tau+n < T: + G += self.gamma**n * self._q(S_tau_n, A_tau_n) \ No newline at end of file diff --git a/solutions/ex11/q_agent_TODO_1.py b/solutions/ex11/q_agent_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..15b029e16b421af014e30645ddf5bef6c15e2811 --- /dev/null +++ b/solutions/ex11/q_agent_TODO_1.py @@ -0,0 +1 @@ + action = self.pi_eps(s, info=info) \ No newline at end of file diff --git a/solutions/ex11/q_agent_TODO_2.py b/solutions/ex11/q_agent_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..b1109163b3163665c3627e5b1d617f4885084ef5 --- /dev/null +++ b/solutions/ex11/q_agent_TODO_2.py @@ -0,0 +1,3 @@ + if not done: + a_star = self.Q.get_optimal_action(sp, info_sp) + self.Q[s,a] += self.alpha * (r + self.gamma * (0 if done else self.Q[sp,a_star]) - self.Q[s,a]) \ No newline at end of file diff --git a/solutions/ex11/sarsa_agent_TODO_1.py b/solutions/ex11/sarsa_agent_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..5cabe83149ef61349da6e638cbe235ad8a87db09 --- /dev/null +++ b/solutions/ex11/sarsa_agent_TODO_1.py @@ -0,0 +1 @@ + return self.pi_eps(s, info) \ No newline at end of file diff --git a/solutions/ex11/sarsa_agent_TODO_2.py b/solutions/ex11/sarsa_agent_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..3a8a80cead115cdff3822df88d52e9c6607d1cc1 --- /dev/null +++ b/solutions/ex11/sarsa_agent_TODO_2.py @@ -0,0 +1 @@ + return self.a \ No newline at end of file diff --git a/solutions/ex11/sarsa_agent_TODO_3.py b/solutions/ex11/sarsa_agent_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..5c6eb6088708097da5ca156e80c56cebd03667d5 --- /dev/null +++ b/solutions/ex11/sarsa_agent_TODO_3.py @@ -0,0 +1 @@ + self.a = self.pi_eps(sp, info_sp) if not done else -1 \ No newline at end of file diff --git a/solutions/ex11/sarsa_agent_TODO_4.py b/solutions/ex11/sarsa_agent_TODO_4.py new file mode 100644 index 0000000000000000000000000000000000000000..cecfe59aa108c55ef37449f0c180c8c70e023b97 --- /dev/null +++ b/solutions/ex11/sarsa_agent_TODO_4.py @@ -0,0 +1,2 @@ + delta = r + (self.gamma * self.Q[sp,self.a] if not done else 0) - self.Q[s,a] + self.Q[s,a] += self.alpha * delta \ No newline at end of file diff --git a/solutions/ex11/semi_grad_q_TODO_1.py b/solutions/ex11/semi_grad_q_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..1da3194c7198d9471a6bf6cb60566c7e940dffd2 --- /dev/null +++ b/solutions/ex11/semi_grad_q_TODO_1.py @@ -0,0 +1,4 @@ + if not done: + a_star = self.Q.get_optimal_action(sp, info_sp) + td_delta = r + (0 if done else self.gamma * self.Q(sp, a_star)) - self.Q(s, a) + self.Q.w += self.alpha * td_delta * self.Q.x(s, a) \ No newline at end of file diff --git a/solutions/ex11/semi_grad_sarsa_TODO_1.py b/solutions/ex11/semi_grad_sarsa_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..4cc4b80940abd706d44bc514ad07a6898f50b1b5 --- /dev/null +++ b/solutions/ex11/semi_grad_sarsa_TODO_1.py @@ -0,0 +1 @@ + action = self.a if k > 0 else super().pi(s, k, info) \ No newline at end of file diff --git a/solutions/ex11/semi_grad_sarsa_TODO_2.py b/solutions/ex11/semi_grad_sarsa_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..0690b1e7194e11f607195a573ddb90eb24f76e2c --- /dev/null +++ b/solutions/ex11/semi_grad_sarsa_TODO_2.py @@ -0,0 +1,4 @@ + a_prime = super().pi(sp, k=0, info=info_sp) + delta = r + (0 if done else self.gamma * self.Q(sp, a_prime)) - self.Q(s, a) + self.Q.w += self.alpha * delta * self.Q.x(s,a) + self.a = a_prime \ No newline at end of file diff --git a/solutions/ex12/minigrid_wrappers_TODO_1.py b/solutions/ex12/minigrid_wrappers_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..8f7190396df4e0b97ee6bad8087da89ad4c0d48b --- /dev/null +++ b/solutions/ex12/minigrid_wrappers_TODO_1.py @@ -0,0 +1 @@ + box.high[:, :, i] = nbounds[i] \ No newline at end of file diff --git a/solutions/ex12/minigrid_wrappers_TODO_2.py b/solutions/ex12/minigrid_wrappers_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..1607b9a79357bbf3191133fd7cc928d5b38c173b --- /dev/null +++ b/solutions/ex12/minigrid_wrappers_TODO_2.py @@ -0,0 +1,2 @@ + box.high = box.high[:,:,dims] + box.low = box.low[:,:,dims] \ No newline at end of file diff --git a/solutions/ex12/minigrid_wrappers_TODO_3.py b/solutions/ex12/minigrid_wrappers_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..d967cd1e02c7e71cd86fa0a3a878e8e0e0570620 --- /dev/null +++ b/solutions/ex12/minigrid_wrappers_TODO_3.py @@ -0,0 +1 @@ + x = obs['image'][:, :, self.dims] \ No newline at end of file diff --git a/solutions/ex12/minigrid_wrappers_TODO_4.py b/solutions/ex12/minigrid_wrappers_TODO_4.py new file mode 100644 index 0000000000000000000000000000000000000000..bd9020da77bbe889da37788db22dda9c9b54397b --- /dev/null +++ b/solutions/ex12/minigrid_wrappers_TODO_4.py @@ -0,0 +1 @@ + return tuple( obs['image'].flat ) \ No newline at end of file diff --git a/solutions/ex12/mountain_car_TODO_1.py b/solutions/ex12/mountain_car_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..baeb97c73560e295b43eeb03d120294e5ef51262 --- /dev/null +++ b/solutions/ex12/mountain_car_TODO_1.py @@ -0,0 +1,16 @@ + for i, alpha in enumerate(alphas): + n = n_steps[i] + agent = LinearSemiGradSarsaN(env, gamma=1, alpha=alpha / num_of_tilings, epsilon=0, n=n) + experiment = f"experiments/mountaincar_10-2_{agent}_{episodes}" + train(env, agent, experiment_name=experiment, num_episodes=episodes, max_runs=max_runs) + experiments.append(experiment) + + agent = LinearSemiGradSarsaLambda(env, gamma=1, alpha=alphas[1]/num_of_tilings, epsilon=0, lamb=0.9) + experiment = f"experiments/mountaincar_10-2_{agent}_{episodes}" + train(env, agent, experiment_name=experiment, num_episodes=episodes, max_runs=max_runs) + experiments.append(experiment) + + agent = LinearSemiGradQAgent(env, gamma=1, alpha=alphas[1] / num_of_tilings, epsilon=0) + experiment = f"experiments/mountaincar_10-2_{agent}_{episodes}" + train(env, agent, experiment_name=experiment, num_episodes=episodes, max_runs=max_runs) + experiments.append(experiment) \ No newline at end of file diff --git a/solutions/ex12/sarsa_lambda_agent_TODO_1.py b/solutions/ex12/sarsa_lambda_agent_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..5723174eefa11ed0b20f1ef766b2f8eb334bd999 --- /dev/null +++ b/solutions/ex12/sarsa_lambda_agent_TODO_1.py @@ -0,0 +1 @@ + a_prime = self.pi_eps(sp, info_sp) if not done else -1 \ No newline at end of file diff --git a/solutions/ex12/sarsa_lambda_agent_TODO_2.py b/solutions/ex12/sarsa_lambda_agent_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..13d29a2bd7ea80e075b6211ef83e2b065a7445b9 --- /dev/null +++ b/solutions/ex12/sarsa_lambda_agent_TODO_2.py @@ -0,0 +1 @@ + delta = r + self.gamma * (self.Q[sp,a_prime] if not done else 0) - self.Q[s,a] \ No newline at end of file diff --git a/solutions/ex12/sarsa_lambda_agent_TODO_3.py b/solutions/ex12/sarsa_lambda_agent_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..5c85d462776cb6228079fc83452f7aef14fb165f --- /dev/null +++ b/solutions/ex12/sarsa_lambda_agent_TODO_3.py @@ -0,0 +1 @@ + self.e[(s,a)] += 1 \ No newline at end of file diff --git a/solutions/ex12/sarsa_lambda_agent_TODO_4.py b/solutions/ex12/sarsa_lambda_agent_TODO_4.py new file mode 100644 index 0000000000000000000000000000000000000000..e36f86331c3278f032c2857538b54180ec902b6e --- /dev/null +++ b/solutions/ex12/sarsa_lambda_agent_TODO_4.py @@ -0,0 +1,2 @@ + self.Q[s,a] += self.alpha * delta * ee + self.e[(s,a)] = self.gamma * self.lamb * ee \ No newline at end of file diff --git a/solutions/ex12/semi_grad_nstep_sarsa_TODO_1.py b/solutions/ex12/semi_grad_nstep_sarsa_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..290a33b00a5a0e236bf0975bf4cde9ca7fa5ae63 --- /dev/null +++ b/solutions/ex12/semi_grad_nstep_sarsa_TODO_1.py @@ -0,0 +1 @@ + return self.Q(s, a) \ No newline at end of file diff --git a/solutions/ex12/semi_grad_nstep_sarsa_TODO_2.py b/solutions/ex12/semi_grad_nstep_sarsa_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..89368e6624fe3eb464ea3da1b26e8bcdc1728828 --- /dev/null +++ b/solutions/ex12/semi_grad_nstep_sarsa_TODO_2.py @@ -0,0 +1 @@ + self.Q.w += self.alpha * delta * self.Q.x(s,a) # Update q(s,a)/weights given change in q-values: delta = [G-\hat{q}(..)] \ No newline at end of file diff --git a/solutions/ex12/semi_grad_sarsa_lambda_TODO_1.py b/solutions/ex12/semi_grad_sarsa_lambda_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..1fa61eb5c6da3a9948a225ba80d5ec5b9bd0ed67 --- /dev/null +++ b/solutions/ex12/semi_grad_sarsa_lambda_TODO_1.py @@ -0,0 +1,5 @@ + Q = self.Q.w @ self.x + Q_prime = self.Q.w @ x_prime if not done else None + delta = r + (self.gamma * Q_prime if not done else 0) - Q + self.z = self.gamma * self.lamb * self.z + (1-self.alpha * self.gamma * self.lamb *self.z @ self.x) * self.x + self.Q.w += self.alpha * (delta + Q - self.Q_old) * self.z - self.alpha * (Q-self.Q_old) * self.x \ No newline at end of file diff --git a/solutions/ex13/deepq_agent_TODO_1.py b/solutions/ex13/deepq_agent_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..824ede3c76517a24ed8ad2c1f9fcc2e4e3533032 --- /dev/null +++ b/solutions/ex13/deepq_agent_TODO_1.py @@ -0,0 +1,3 @@ + y = r[:,0] + self.gamma * np.max(self.Q(sp), axis=1) * (1-done) + target = self.Q(s) + target[range(len(a)), a] = y \ No newline at end of file diff --git a/solutions/ex13/double_deepq_agent_TODO_1.py b/solutions/ex13/double_deepq_agent_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..be47b430303aa44b57c4cb1dc57c456fb8800755 --- /dev/null +++ b/solutions/ex13/double_deepq_agent_TODO_1.py @@ -0,0 +1 @@ + self.target.update_Phi(self.Q, tau=self.tau) \ No newline at end of file diff --git a/solutions/ex13/double_deepq_agent_TODO_2.py b/solutions/ex13/double_deepq_agent_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..3edd701d8fb798e973f6536e5d329b1e7146c701 --- /dev/null +++ b/solutions/ex13/double_deepq_agent_TODO_2.py @@ -0,0 +1,5 @@ + sp[done, :] = 0 + astar = np.argmax(self.Q(sp), axis=1) * (1-np.asarray(done)) + y = r[:,0] + self.gamma * self.target(sp)[range(len(sp)), astar] * (1 - done) + target = self.Q(s) + target[range(len(a)), a] = y \ No newline at end of file diff --git a/solutions/ex13/dyna_q_TODO_1.py b/solutions/ex13/dyna_q_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..c7bd2b634d109ce7df7f938e9bd2aaede2a426e8 --- /dev/null +++ b/solutions/ex13/dyna_q_TODO_1.py @@ -0,0 +1 @@ + self.Q[s,a] += self.alpha * (r + (self.gamma * self.Q[sp, self.Q.get_optimal_action(sp, info_sp)] if not done else 0) - self.Q[s,a]) \ No newline at end of file diff --git a/solutions/ex13/dyna_q_TODO_2.py b/solutions/ex13/dyna_q_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..46704a9b39b54c32d5b19dd1a63c100d8999418b --- /dev/null +++ b/solutions/ex13/dyna_q_TODO_2.py @@ -0,0 +1,2 @@ + s_, a_, r_, sp_,done_ = self.Model[np.random.randint(len(self.Model))] + self.q_update(s_,a_,r_,sp_,done_, info_s, info_sp) \ No newline at end of file diff --git a/solutions/ex13/dyna_q_TODO_3.py b/solutions/ex13/dyna_q_TODO_3.py new file mode 100644 index 0000000000000000000000000000000000000000..0495ae68405d295cb8fba062f4772b1b22e4f9c4 --- /dev/null +++ b/solutions/ex13/dyna_q_TODO_3.py @@ -0,0 +1 @@ + experiments = dyna_experiment(env, env_name='cliff',num_episodes=200,epsilon=epsilon, alpha=alpha, gamma=gamma, runs=4) \ No newline at end of file diff --git a/solutions/ex13/keras_networks_TODO_1.py b/solutions/ex13/keras_networks_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..3f6fcaa6d661bec8d69e2c5ac4f4f4eb27eb33c3 --- /dev/null +++ b/solutions/ex13/keras_networks_TODO_1.py @@ -0,0 +1,6 @@ + adv_dense = layers.Dense(hidden_size, activation='relu', kernel_initializer=init())(dense2) + adv_out = layers.Dense(num_actions, kernel_initializer=init())(adv_dense) + v_dense = layers.Dense(hidden_size, activation='relu', kernel_initializer=init())(dense2) + v_out = layers.Dense(1, kernel_initializer=init())(v_dense) + norm_adv = layers.Lambda(lambda x: x - tf.reduce_mean(x))(adv_out) + combine = layers.add([v_out, norm_adv]) \ No newline at end of file diff --git a/solutions/ex13/maximization_bias_environment_TODO_1.py b/solutions/ex13/maximization_bias_environment_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..3f2bdf72de8a6adf0315eb8d48acf88138a9864e --- /dev/null +++ b/solutions/ex13/maximization_bias_environment_TODO_1.py @@ -0,0 +1 @@ + return {(t, 0): 1} \ No newline at end of file diff --git a/solutions/ex13/maximization_bias_environment_TODO_2.py b/solutions/ex13/maximization_bias_environment_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..ea4a34ee74e2c8300bf02cb098830cf0750ad0c5 --- /dev/null +++ b/solutions/ex13/maximization_bias_environment_TODO_2.py @@ -0,0 +1 @@ + return {(self.state_B, 0): 1} \ No newline at end of file diff --git a/solutions/ex13/tabular_double_q_TODO_1.py b/solutions/ex13/tabular_double_q_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..1320700514b4189dc8946e50617d8b7b9b3dc8ec --- /dev/null +++ b/solutions/ex13/tabular_double_q_TODO_1.py @@ -0,0 +1 @@ + return Agent.pi(self, s, k, info) if np.random.rand() < self.epsilon else a1[np.argmax(Q + np.random.rand(len(Q)) * 1e-8)] \ No newline at end of file diff --git a/solutions/ex13/tabular_double_q_TODO_2.py b/solutions/ex13/tabular_double_q_TODO_2.py new file mode 100644 index 0000000000000000000000000000000000000000..b9f397fcaa3473ba60895f1fa09dd3250c2bcc56 --- /dev/null +++ b/solutions/ex13/tabular_double_q_TODO_2.py @@ -0,0 +1,4 @@ + def train_(Q1,Q2, s, a, r, sp, done=False): + Q1[s,a] += self.alpha * (r + (self.gamma * Q2[sp,Q1.get_optimal_action(sp, info_sp)] if not done else 0) - Q1[s,a] ) + + train_(self.Q1, self.Q2, s, a, r, sp,done) if np.random.rand() < 0.5 else train_(self.Q2, self.Q1, s, a, r, sp,done) \ No newline at end of file diff --git a/solutions/ex13/torch_networks_TODO_1.py b/solutions/ex13/torch_networks_TODO_1.py new file mode 100644 index 0000000000000000000000000000000000000000..1ab89a8ad75f9c71bc6c82fd1c5f37e436812a12 --- /dev/null +++ b/solutions/ex13/torch_networks_TODO_1.py @@ -0,0 +1,4 @@ + s = Variable(torch.FloatTensor(s)) + x = self.feature(s) + advantage = self.advantage(x) + value = self.value(x) \ No newline at end of file