├── .formatter.exs ├── README.md ├── lib ├── math │ └── vector.ex ├── autonomous_car.ex ├── brain │ ├── memory.ex │ ├── model.ex │ ├── learn.ex │ └── training.ex ├── objects │ └── car.ex └── scenes │ └── environment.ex ├── config └── config.exs ├── .gitignore ├── mix.exs └── mix.lock /.formatter.exs: -------------------------------------------------------------------------------- 1 | # Used by "mix format" 2 | [ 3 | inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] 4 | ] 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Autonomous Car 2 | 3 | Using: 4 | - [Elixir](https://elixir-lang.org/) 5 | - [Nx](https://github.com/elixir-nx/nx) 6 | - [Axon](https://github.com/elixir-nx/axon) 7 | - [Scenic](https://github.com/boydm/scenic) 8 | 9 | `mix scenic.run` 10 | -------------------------------------------------------------------------------- /lib/math/vector.ex: -------------------------------------------------------------------------------- 1 | defmodule AutonomousCar.Math.Vector do 2 | import Math 3 | 4 | def rotate({x, y} = vector, angle) do 5 | angle = degrees_to_radians(angle) 6 | {x * cos(angle) - y * sin(angle), x * sin(angle) + y * cos(angle)} 7 | end 8 | 9 | def degrees_to_radians(angle) do 10 | angle * (Math.pi / 180) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | 6 | 7 | # It is also possible to import configuration files, relative to this 8 | # directory. For example, you can emulate configuration per environment 9 | # by uncommenting the line below and defining dev.exs, test.exs and such. 10 | # Configuration from the imported file will override the ones defined 11 | # here (which is why it is important to import them last). 12 | # 13 | # import_config "prod.exs" 14 | -------------------------------------------------------------------------------- /lib/autonomous_car.ex: -------------------------------------------------------------------------------- 1 | defmodule AutonomousCar do 2 | @moduledoc """ 3 | Starter application using the Scenic framework. 4 | """ 5 | 6 | def start(_type, _args) do 7 | children = [ 8 | {Scenic, [main_viewport_config()]} 9 | ] 10 | 11 | Supervisor.start_link(children, strategy: :one_for_one) 12 | end 13 | 14 | def main_viewport_config() do 15 | [ 16 | name: :main_viewport, 17 | size: {1200, 600}, 18 | default_scene: {AutonomousCar.Scene.Environment, nil}, 19 | drivers: [ 20 | [ 21 | module: Scenic.Driver.Local, 22 | name: :local 23 | ] 24 | ] 25 | ] 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build/ 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover/ 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps/ 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc/ 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore package tarball (built via "mix hex.build"). 23 | foo-*.tar 24 | 25 | 26 | # Ignore scripts marked as secret - usually passwords and such in config files 27 | *.secret.exs 28 | *.secrets.exs 29 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule AutonomousCar.MixProject do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :autonomous_car, 7 | version: "0.1.0", 8 | elixir: "~> 1.11", 9 | build_embedded: true, 10 | start_permanent: Mix.env() == :prod, 11 | deps: deps() 12 | ] 13 | end 14 | 15 | # Run "mix help compile.app" to learn about applications. 16 | def application do 17 | [ 18 | mod: {AutonomousCar, []}, 19 | extra_applications: [] 20 | ] 21 | end 22 | 23 | # Run "mix help deps" to learn about dependencies. 24 | defp deps do 25 | [ 26 | {:scenic, "~> 0.11.2"}, 27 | {:scenic_driver_local, "~> 0.11.0"}, 28 | {:math, "~> 0.7.0"}, 29 | {:nx, "~> 0.6.2"}, 30 | {:axon, "~> 0.6.0"} 31 | ] 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/brain/memory.ex: -------------------------------------------------------------------------------- 1 | defmodule AutonomousCar.Brain.Memory do 2 | use GenServer 3 | 4 | def init(pid) do 5 | {:ok, pid} 6 | end 7 | 8 | def handle_cast({:push, item}, state) do 9 | {:noreply, [item | state]} 10 | end 11 | 12 | def handle_call(:list, _from, state) do 13 | {:reply, state, state} 14 | end 15 | 16 | def handle_call(:count, _from, state) do 17 | {:reply, Enum.count(state), state} 18 | end 19 | 20 | def handle_cast(:reset, state) do 21 | {:noreply, state = []} 22 | end 23 | 24 | # Public API 25 | def start_link() do 26 | GenServer.start_link(AutonomousCar.Brain.Memory, []) 27 | end 28 | 29 | def push(pid, item) do 30 | GenServer.cast(pid, {:push, item}) 31 | end 32 | 33 | def list(pid) do 34 | GenServer.call(pid, :list) 35 | end 36 | 37 | def count(pid) do 38 | GenServer.call(pid, :count) 39 | end 40 | 41 | def reset(pid) do 42 | GenServer.cast(pid, :reset) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/brain/model.ex: -------------------------------------------------------------------------------- 1 | defmodule AutonomousCar.Brain.Model do 2 | use GenServer 3 | 4 | def init(pid) do 5 | {:ok, pid} 6 | end 7 | 8 | def handle_cast({:push, params}, state) do 9 | {:noreply, params} 10 | end 11 | 12 | def handle_call(:pull, _from, state) do 13 | {:reply, state, state} 14 | end 15 | 16 | def handle_cast(:reset, state) do 17 | {:noreply, state = []} 18 | end 19 | 20 | # Public API 21 | def model do 22 | Axon.input({nil, 5}) 23 | |> Axon.dense(30, activation: :relu) 24 | |> Axon.dense(3, activation: :softmax) 25 | end 26 | 27 | def start_link() do 28 | GenServer.start_link(AutonomousCar.Brain.Model, []) 29 | end 30 | 31 | def push(params, pid) do 32 | GenServer.cast(pid, {:push, params}) 33 | end 34 | 35 | def pull(pid) do 36 | GenServer.call(pid, :pull) 37 | end 38 | 39 | def count(pid) do 40 | GenServer.call(pid, :count) 41 | end 42 | 43 | def reset(pid) do 44 | GenServer.cast(pid, :reset) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/objects/car.ex: -------------------------------------------------------------------------------- 1 | defmodule AutonomousCar.Objects.Car do 2 | 3 | def move(%{objects: %{car: car}} = state) do 4 | car_angle = car.angle 5 | car_velocity_rotate = AutonomousCar.Math.Vector.rotate(car.velocity, car_angle) 6 | new_coords = Scenic.Math.Vector2.add(car.coords, car_velocity_rotate) 7 | 8 | # Keep car inside environment 9 | new_new_coords = 10 | with {car_coords_x, car_coords_y} <- new_coords, 11 | viewport_width <- state.viewport_width, 12 | viewport_height <- state.viewport_height do 13 | 14 | car_coords_x = if car_coords_x + 20 >= viewport_width, do: viewport_width - 20, else: car_coords_x 15 | car_coords_x = if car_coords_x <= 20, do: 20, else: car_coords_x 16 | 17 | car_coords_y = if car_coords_y + 20 >= viewport_height, do: viewport_height - 20, else: car_coords_y 18 | car_coords_y = if car_coords_y <= 20, do: 20, else: car_coords_y 19 | 20 | {car_coords_x, car_coords_y} 21 | end 22 | 23 | car_angle = if new_new_coords != new_coords, do: car_angle + 20, else: car_angle 24 | 25 | state 26 | |> put_in([:objects, :car, :angle], car_angle) 27 | |> put_in([:objects, :car, :last_coords], car.coords) 28 | |> put_in([:objects, :car, :coords], new_new_coords) 29 | end 30 | 31 | def update_angle(state, action) do 32 | rotation = action?(action) 33 | 34 | state 35 | |> put_in([:objects, :car, :angle], state.objects.car.angle + rotation) 36 | end 37 | 38 | defp action?(0), do: -20 39 | defp action?(2), do: 20 40 | defp action?(_), do: 0 41 | end 42 | -------------------------------------------------------------------------------- /lib/brain/learn.ex: -------------------------------------------------------------------------------- 1 | defmodule AutonomousCar.Brain.Learn do 2 | 3 | alias AutonomousCar.Brain.{Memory,Model} 4 | 5 | require Axon 6 | import Nx.Defn 7 | 8 | def learning(state, batch_size) do 9 | if Memory.count(state.memory_pid) != batch_size do 10 | state.model_fit 11 | else 12 | IO.inspect(label: "Training ->") 13 | 14 | memories = 15 | state.memory_pid 16 | |> Memory.list() 17 | |> Enum.shuffle 18 | 19 | train_samples = memories 20 | |> Enum.map(fn x -> x.state_initial end) 21 | |> Nx.tensor() 22 | |> Nx.to_batched_list(batch_size) 23 | 24 | train_labels = generate_train_labels(memories, state) 25 | |> Nx.tensor() 26 | |> Nx.to_batched_list(batch_size) 27 | 28 | params = Model.pull(state.model_pid) 29 | 30 | {new_params, _} = 31 | Model.model 32 | |> AutonomousCar.Brain.Training.step({params, Nx.tensor(0.0)}, :mean_squared_error, Axon.Optimizers.adamw(0.005)) 33 | |> AutonomousCar.Brain.Training.train(train_samples, train_labels, epochs: 1) 34 | 35 | Model.push(new_params, state.model_pid) 36 | 37 | state.memory_pid |> Memory.reset() 38 | 39 | true 40 | end 41 | end 42 | 43 | defp generate_train_labels([], _), do: [] 44 | 45 | defp generate_train_labels([mem | samples], state) do 46 | pred_initial = get_values(mem.state_initial, state) |> Nx.to_flat_list() 47 | vr = calc_r(mem.reward, 0.99, get_values(mem.state_final, state)) 48 | labels = List.replace_at(pred_initial, mem.action, Nx.to_scalar(vr)) 49 | [labels | generate_train_labels(samples, state)] 50 | end 51 | 52 | defp get_values(s, state) do 53 | inputs = Nx.tensor(s) |> Nx.new_axis(0) 54 | 55 | params = state.model_pid |> Model.pull 56 | Model.model |> Axon.predict(params, inputs) 57 | end 58 | 59 | defn calc_r(reward, gamma, values) do 60 | reward + (gamma * Nx.reduce_max(values)) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/brain/training.ex: -------------------------------------------------------------------------------- 1 | defmodule AutonomousCar.Brain.Training do 2 | require Axon 3 | require Axon.Updates 4 | 5 | def step({params, _update_state}, objective_fn, {init_update_fn, update_fn}) 6 | when is_function(objective_fn, 3) and is_function(init_update_fn, 1) and 7 | is_function(update_fn, 3) do 8 | optim_params = init_update_fn.(params) 9 | 10 | step_fn = fn model_state, input, target -> 11 | {params, update_state} = model_state 12 | 13 | {batch_loss, gradients} = 14 | Nx.Defn.Kernel.value_and_grad(params, &objective_fn.(&1, input, target)) 15 | 16 | {updates, new_update_state} = update_fn.(gradients, update_state, params) 17 | {{Axon.Updates.apply_updates(params, updates), new_update_state}, batch_loss} 18 | end 19 | 20 | {{params, optim_params}, step_fn} 21 | end 22 | 23 | def step(%Axon{} = model, model_state, loss, optimizer) when is_function(loss, 2) do 24 | {_init_fn, predict_fn} = Axon.compile(model) 25 | 26 | objective_fn = fn params, input, target -> 27 | preds = predict_fn.(params, input) 28 | loss.(target, preds) 29 | end 30 | 31 | step(model_state, objective_fn, optimizer) 32 | end 33 | 34 | def step(%Axon{} = model, model_state, loss, optimizer) when is_atom(loss) do 35 | loss_fn = &apply(Axon.Losses, loss, [&1, &2, [reduction: :mean]]) 36 | step(model, model_state, loss_fn, optimizer) 37 | end 38 | 39 | def train({model_state, step_fn}, inputs, targets, opts \\ []) do 40 | epochs = opts[:epochs] || 5 41 | compiler = opts[:compiler] || Nx.Defn.Evaluator 42 | 43 | for epoch <- 1..epochs, reduce: model_state do 44 | model_state -> 45 | {time, {model_state, avg_loss}} = 46 | :timer.tc(&train_epoch/6, [ 47 | step_fn, 48 | model_state, 49 | inputs, 50 | targets, 51 | compiler, 52 | epoch 53 | ]) 54 | 55 | epoch_avg_loss = 56 | avg_loss 57 | |> Nx.backend_transfer() 58 | |> Nx.to_scalar() 59 | 60 | IO.puts("\n") 61 | IO.puts("Epoch #{epoch} Time: #{time / 1_000_000}s") 62 | IO.puts("Epoch #{epoch} Loss: #{epoch_avg_loss}") 63 | model_state 64 | end 65 | end 66 | 67 | ## Helpers 68 | 69 | defp train_epoch(step_fn, model_state, inputs, targets, compiler, epoch) do 70 | total_batches = Enum.count(inputs) 71 | 72 | dataset = 73 | inputs 74 | |> Enum.zip(targets) 75 | |> Enum.with_index() 76 | 77 | for {{inp, tar}, i} <- dataset, reduce: {model_state, Nx.tensor(0.0)} do 78 | {model_state, state} -> 79 | {model_state, batch_loss} = 80 | Nx.Defn.jit(step_fn, [model_state, inp, tar], compiler: compiler) 81 | 82 | avg_loss = 83 | state 84 | |> Nx.multiply(i) 85 | |> Nx.add(Nx.backend_transfer(batch_loss)) 86 | |> Nx.divide(i + 1) 87 | 88 | IO.write( 89 | "\rEpoch #{epoch}, batch #{i + 1} of #{total_batches} - " <> 90 | "Average Loss: #{Nx.to_scalar(avg_loss)}" 91 | ) 92 | 93 | {model_state, avg_loss} 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "array_vector": {:hex, :array_vector, "0.3.0", "3afc0abb82a2e0bfe0ffe54948b78163a4aae013818f86f343e9de85b9d479ca", [:mix], [], "hexpm", "743a15b97df26fe83a11f093d8a3b9308c2a13e2f286e537d37c978269a3a2a2"}, 3 | "axon": {:hex, :axon, "0.6.0", "fd7560079581e4cedebaf0cd5f741d6ac3516d06f204ebaf1283b1093bf66ff6", [:mix], [{:kino, "~> 0.7", [hex: :kino, repo: "hexpm", optional: true]}, {:kino_vega_lite, "~> 0.1.7", [hex: :kino_vega_lite, repo: "hexpm", optional: true]}, {:nx, "~> 0.6.0", [hex: :nx, repo: "hexpm", optional: false]}, {:polaris, "~> 0.1", [hex: :polaris, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: true]}], "hexpm", "204e7aeb50d231a30b25456adf17bfbaae33fe7c085e03793357ac3bf62fd853"}, 4 | "complex": {:hex, :complex, "0.5.0", "af2d2331ff6170b61bb738695e481b27a66780e18763e066ee2cd863d0b1dd92", [:mix], [], "hexpm", "2683bd3c184466cfb94fad74cbfddfaa94b860e27ad4ca1bffe3bff169d91ef1"}, 5 | "elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"}, 6 | "ex_image_info": {:hex, :ex_image_info, "0.2.4", "610002acba43520a9b1cf1421d55812bde5b8a8aeaf1fe7b1f8823e84e762adb", [:mix], [], "hexpm", "fd1a7e02664e3b14dfd3b231d22fdd48bd3dd694c4773e6272b3a6228f1106bc"}, 7 | "exla": {:git, "https://github.com/elixir-nx/nx.git", "70522417c71425d4871090114481d1b9effbdfef", [sparse: "exla"]}, 8 | "font_metrics": {:hex, :font_metrics, "0.5.1", "10ce0b8b1bf092a2d3d307e05a7c433787ae8ca7cd1f3cf959995a628809a994", [:mix], [{:nimble_options, "~> 0.3", [hex: :nimble_options, repo: "hexpm", optional: false]}], "hexpm", "192e4288772839ae4dadccb0f5b1d5c89b73a0c3961ccea14b6181fdcd535e54"}, 9 | "input_event": {:hex, :input_event, "1.4.2", "8ad280387595110cf4ac4d92cfcfd2f5d61f7285084f35adc5feec077df23058", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "eb12e3a5d83f2c570d965ad0a5de7bea7c9f387b425ae882c00f3a2a00971e3f"}, 10 | "math": {:hex, :math, "0.7.0", "12af548c3892abf939a2e242216c3e7cbfb65b9b2fe0d872d05c6fb609f8127b", [:mix], [], "hexpm", "7987af97a0c6b58ad9db43eb5252a49fc1dfe1f6d98f17da9282e297f594ebc2"}, 11 | "msgpax": {:hex, :msgpax, "2.2.3", "02be0be1b440a12d7e6f6c45662463d53d01a30b5bd4ab806276453b455f820a", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b45d5d37426699cf089d65ba4aed2ed838e3981a4537e339e685e2cb462a536"}, 12 | "nimble_options": {:hex, :nimble_options, "0.5.2", "42703307b924880f8c08d97719da7472673391905f528259915782bb346e0a1b", [:mix], [], "hexpm", "4da7f904b915fd71db549bcdc25f8d56f378ef7ae07dc1d372cbe72ba950dce0"}, 13 | "nx": {:hex, :nx, "0.6.2", "f1d137f477b1a6f84f8db638f7a6d5a0f8266caea63c9918aa4583db38ebe1d6", [:mix], [{:complex, "~> 0.5", [hex: :complex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ac913b68d53f25f6eb39bddcf2d2cd6ea2e9bcb6f25cf86a79e35d0411ba96ad"}, 14 | "polaris": {:hex, :polaris, "0.1.0", "dca61b18e3e801ecdae6ac9f0eca5f19792b44a5cb4b8d63db50fc40fc038d22", [:mix], [{:nx, "~> 0.5", [hex: :nx, repo: "hexpm", optional: false]}], "hexpm", "13ef2b166650e533cb24b10e2f3b8ab4f2f449ba4d63156e8c569527f206e2c2"}, 15 | "scenic": {:hex, :scenic, "0.11.2", "5d29aac74ee85899651716af47f72d167d064939fc2b4e514cd7c43810293cda", [:make, :mix], [{:elixir_make, "~> 0.7.7", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:ex_image_info, "~> 0.2.4", [hex: :ex_image_info, repo: "hexpm", optional: false]}, {:font_metrics, "~> 0.5.0", [hex: :font_metrics, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.3.4 or ~> 0.4.0 or ~> 0.5.0 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:truetype_metrics, "~> 0.6", [hex: :truetype_metrics, repo: "hexpm", optional: false]}], "hexpm", "6f846cfe8457163d28ad6b8b147d251bd15524c249ddad3e75608cbdb739beaa"}, 16 | "scenic_driver_glfw": {:hex, :scenic_driver_glfw, "0.10.1", "df7a11bc1d23415dd48c7796a70a6ad98b9cf77554078d79115d6c6e58b87eda", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:scenic, "~> 0.10", [hex: :scenic, repo: "hexpm", optional: false]}], "hexpm", "d2f2ef0a1d099a6e1904616c0a95ff0fc554342cf7936e6bb73c2d7027e7557e"}, 17 | "scenic_driver_local": {:hex, :scenic_driver_local, "0.11.0", "c26f7665c3d4aa634a0f8873bd958cb3bfcc99cb96e2381422de3e78d244357c", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:input_event, "~> 0.4 or ~> 1.0", [hex: :input_event, repo: "hexpm", optional: false]}, {:scenic, "~> 0.11.0", [hex: :scenic, repo: "hexpm", optional: false]}], "hexpm", "77b27b82a8fe41d5fade5c88cf413af098d3f3d56717c988097e7902ab9b9d03"}, 18 | "table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"}, 19 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 20 | "truetype_metrics": {:hex, :truetype_metrics, "0.6.1", "9119a04dc269dd8f63e85e12e4098f711cb7c5204a420f4896f40667b9e064f6", [:mix], [{:font_metrics, "~> 0.5", [hex: :font_metrics, repo: "hexpm", optional: false]}], "hexpm", "5711d4a3e4fc92eb073326fbe54208925d35168dc9b288c331ee666a8a84759b"}, 21 | } 22 | -------------------------------------------------------------------------------- /lib/scenes/environment.ex: -------------------------------------------------------------------------------- 1 | defmodule AutonomousCar.Scene.Environment do 2 | use Scenic.Scene 3 | 4 | alias Scenic.Graph 5 | alias Scenic.ViewPort 6 | 7 | alias AutonomousCar.Objects.Car 8 | alias AutonomousCar.Brain.{Memory,Model,Learn} 9 | 10 | import Scenic.Primitives 11 | require Axon 12 | import Nx.Defn 13 | 14 | @batch_size 150 15 | 16 | # Initial parameters for the game scene! 17 | def init(scene, _args, options) do 18 | viewport = options[:viewport] 19 | 20 | # Initializes the graph 21 | graph = Graph.build(theme: :dark) 22 | 23 | # Calculate the transform that centers the car in the viewport 24 | {:ok, %ViewPort{size: {viewport_width, viewport_height}}} = ViewPort.info(viewport) 25 | 26 | # start a very simple animation timer 27 | # {:ok, timer} = :timer.send_interval(60, :frame) 28 | 29 | # Start neural network 30 | {:ok, model_pid} = Model.start_link() 31 | 32 | # start memory 33 | {:ok, memory_pid} = Memory.start_link() 34 | 35 | # Init model params 36 | Model.model 37 | |> Axon.init() 38 | |> Model.push(model_pid) 39 | 40 | car_coords = {trunc(viewport_width / 2), trunc(viewport_height / 2)} 41 | goal_coords = {20,20} 42 | car_velocity = {6,0} 43 | 44 | state = %{ 45 | viewport: viewport, 46 | viewport_width: viewport_width, 47 | viewport_height: viewport_height, 48 | graph: graph, 49 | frame_count: 0, 50 | model_pid: model_pid, 51 | model_fit: false, 52 | memory_pid: memory_pid, 53 | distance: Scenic.Math.Vector2.distance(car_coords, goal_coords), 54 | action: 0, 55 | reward: 0, 56 | objects: %{ 57 | goal: %{coords: goal_coords}, 58 | car: %{ 59 | dimension: %{width: 20, height: 10}, 60 | coords: car_coords, 61 | velocity: car_velocity, 62 | angle: 0, 63 | orientation: 0, 64 | orientation_rad: 0, 65 | orientation_grad: 0, 66 | signal: %{ 67 | left: 0, 68 | center: 0, 69 | right: 0 70 | } 71 | } 72 | } 73 | } 74 | 75 | graph = 76 | graph 77 | |> draw_objects(state.objects) 78 | |> draw_vector(car_coords, goal_coords, :blue) 79 | 80 | scene = 81 | scene 82 | |> assign( state: state, graph: graph ) 83 | |> push_graph( graph ) 84 | 85 | IO.puts("\n") 86 | IO.puts("--STARTED--") 87 | 88 | {:ok, scene} 89 | end 90 | 91 | def handle_info(:frame, %{frame_count: frame_count} = state) do 92 | IO.puts("\n") 93 | 94 | sensor_center = Graph.get(state.graph, :sensor_center) 95 | %{transforms: %{translate: sensor_center}} = List.first(sensor_center) 96 | 97 | car_object = Graph.get(state.graph, :car) 98 | %{transforms: %{rotate: car_rotate, pin: car_coords}} = List.first(car_object) 99 | 100 | car_look_goal = Graph.get(state.graph, :base) 101 | %{data: {car_look_goal_from, car_look_goal_to}} = List.first(car_look_goal) 102 | 103 | car_look_forward = Graph.get(state.graph, :velocity) 104 | %{data: {car_look_forward_from, car_look_forward_to}} = List.first(car_look_forward) 105 | 106 | {car_x, car_y} = car_coords 107 | 108 | v_car_look_goal = Scenic.Math.Vector2.sub(car_look_goal_from, car_look_goal_to) 109 | v_car_look_goal_normalized = Scenic.Math.Vector2.normalize(v_car_look_goal) 110 | 111 | v_car_look_forward = Scenic.Math.Vector2.sub(car_look_forward_from, car_look_forward_to) 112 | v_car_look_forward_rotate = AutonomousCar.Math.Vector.rotate(v_car_look_forward, state.objects.car.angle) 113 | v_car_look_forward_normalized = Scenic.Math.Vector2.normalize(v_car_look_forward_rotate) 114 | 115 | orientation = Scenic.Math.Vector2.dot(v_car_look_goal_normalized, v_car_look_forward_normalized) 116 | orientation_rad = Math.acos(orientation) 117 | orientation_grad = (180 / :math.pi) * orientation_rad 118 | 119 | _distance = Scenic.Math.Vector2.distance(car_coords, {20,20}) 120 | 121 | signals = 122 | cond do 123 | car_x + 20 >= state.viewport_width -> {1,1,1} 124 | car_x <= 20 -> {1,1,1} 125 | car_y + 20 >= state.viewport_height -> {1,1,1} 126 | car_y <= 20 -> {1,1,1} 127 | true -> {0,0,0} 128 | end 129 | {signal_left, signal_center, signal_right} = signals 130 | 131 | state_final = [signal_left, signal_center, signal_right, orientation, -orientation] 132 | 133 | Memory.push(state.memory_pid, %{ 134 | state_initial: [ 135 | state.objects.car.signal.left, 136 | state.objects.car.signal.center, 137 | state.objects.car.signal.right, 138 | state.objects.car.orientation, 139 | -state.objects.car.orientation], 140 | action: state.action, 141 | reward: state.reward, 142 | state_final: state_final, 143 | frame_count: frame_count, 144 | distance: state.distance 145 | }) 146 | 147 | IO.inspect(state_final, label: "State -> ") 148 | 149 | prob_actions = 150 | case state.model_fit do 151 | true -> 152 | if :rand.uniform() <= 0.3 do 153 | Axon.predict(Model.model, Model.pull(state.model_pid), Nx.tensor(state_final)) 154 | else 155 | get_random_values() 156 | end 157 | _ -> get_random_values() 158 | end 159 | 160 | IO.inspect(prob_actions, label: "Probs -> ") 161 | 162 | action = 163 | Nx.argmax(prob_actions) 164 | |> Nx.to_scalar() 165 | 166 | IO.puts("Action -> #{action}") 167 | 168 | state = 169 | state 170 | |> Car.update_angle(action) 171 | |> Car.move() 172 | 173 | {car_x, car_y} = state.objects.car.coords 174 | 175 | distance = Scenic.Math.Vector2.distance(state.objects.car.coords, state.objects.goal.coords) 176 | 177 | reward = 178 | case {car_x, car_y, distance} do 179 | {car_x, car_y, distance} when car_x <= 20 -> 180 | -1 181 | {car_x, car_y, distance} when car_x + 20 >= state.viewport_width -> 182 | -1 183 | {car_x, car_y, distance} when car_y <= 20 -> 184 | -1 185 | {car_x, car_y, distance} when car_y + 20 >= state.viewport_height -> 186 | -1 187 | {car_x, car_y, distance} when distance < state.distance -> 188 | 0.4 189 | _ -> 190 | -0.5 191 | end 192 | 193 | IO.puts("Reward -> #{reward}") 194 | 195 | # ---------------------------------------------- 196 | graph = 197 | Graph.build(theme: :dark) 198 | |> draw_objects(state.objects) 199 | |> draw_vector(sensor_center, state.objects.goal.coords, :blue) 200 | |> draw_model_fit(state.model_fit) 201 | # ---------------------------------------------- 202 | 203 | model_fit = Learn.learning(state, @batch_size) 204 | 205 | # See if goal is get it 206 | goal_coords = 207 | cond do 208 | distance < 50 && state.objects.goal.coords == {20,20} -> 209 | {state.viewport_width - 20, state.viewport_height - 20} 210 | 211 | distance < 50 && state.objects.goal.coords != {20,20} -> 212 | {20,20} 213 | 214 | distance > 50 -> 215 | state.objects.goal.coords 216 | end 217 | 218 | new_state = 219 | state 220 | |> update_in([:frame_count], &(&1 + 1)) 221 | |> put_in([:objects, :goal, :coords], goal_coords) 222 | |> put_in([:objects, :car, :signal, :left], signal_left) 223 | |> put_in([:objects, :car, :signal, :center], signal_center) 224 | |> put_in([:objects, :car, :signal, :right], signal_right) 225 | |> put_in([:objects, :car, :orientation], orientation) 226 | |> put_in([:objects, :car, :orientation_rad], orientation_rad) 227 | |> put_in([:objects, :car, :orientation_grad], orientation_grad) 228 | |> put_in([:action], action) 229 | |> put_in([:distance], distance) 230 | |> put_in([:reward], reward) 231 | |> put_in([:model_fit], model_fit) 232 | |> put_in([:graph], graph) 233 | 234 | {:noreply, new_state, push: graph} 235 | end 236 | 237 | defp get_random_values do 238 | Nx.random_uniform({3}) 239 | end 240 | 241 | defp draw_vector(graph, from, to, color) do 242 | # graph |> line( {from, to}, stroke: {1, color}, cap: :round, id: :base ) 243 | graph |> line( {from, to}, cap: :round, id: :base ) 244 | end 245 | 246 | defp draw_model_fit(graph, model_fit) do 247 | if model_fit do 248 | graph |> circle(5, fill: :green, translate: {10,10}) 249 | else 250 | graph |> circle(5, fill: :red, translate: {10,10}) 251 | end 252 | end 253 | 254 | defp draw_objects(graph, object_map) do 255 | Enum.reduce(object_map, graph, fn {object_type, object_data}, graph -> 256 | draw_object(graph, object_type, object_data) 257 | end) 258 | end 259 | 260 | defp draw_object(graph, :goal, data) do 261 | %{coords: coords} = data 262 | graph |> circle(10, fill: :yellow, translate: coords) 263 | end 264 | 265 | defp draw_object(graph, :car, data) do 266 | %{width: width, height: height} = data.dimension 267 | 268 | {x, y} = data.coords 269 | 270 | angle_radians = data.angle |> degrees_to_radians 271 | 272 | new_graph = 273 | graph 274 | |> group(fn(g) -> 275 | g 276 | |> rect({width, height}, [fill: :white, translate: {x, y}]) 277 | |> circle(4, fill: :red, translate: {x + 22, y - 5}, id: :sensor_left) 278 | |> circle(4, fill: :green, translate: {x + 28, y + 5}, id: :sensor_center) 279 | |> circle(4, fill: :blue, translate: {x + 22, y + 15}, id: :sensor_right) 280 | |> line({ {x + 28, y + 5}, {x + 28 + 10, y + 5} }, cap: :round, id: :velocity ) 281 | end, rotate: angle_radians, pin: {x, y}, id: :car) 282 | end 283 | 284 | defp degrees_to_radians(angle) do 285 | angle * (Math.pi / 180) 286 | end 287 | end 288 | --------------------------------------------------------------------------------