package fehu

  1. Overview
  2. Docs
Reinforcement learning framework for OCaml

Install

dune-project
 Dependency

Authors

Maintainers

Sources

raven-1.0.0.alpha2.tbz
sha256=93abc49d075a1754442ccf495645bc4fdc83e4c66391ec8aca8fa15d2b4f44d2
sha512=5eb958c51f30ae46abded4c96f48d1825f79c7ce03f975f9a6237cdfed0d62c0b4a0774296694def391573d849d1f869919c49008acffca95946b818ad325f6f

doc/src/fehu.visualize/sink.ml.html

Source file sink.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
open Fehu

type t = { push : Render.t -> unit; close : unit -> unit }

let custom ?(close = fun () -> ()) push = { push; close }
let noop = custom (fun _ -> ())
let push sink frame = sink.push frame
let close sink = sink.close ()

let string_of_status = function
  | Unix.WEXITED code -> Printf.sprintf "exit status %d" code
  | Unix.WSIGNALED signal -> Printf.sprintf "signal %d" signal
  | Unix.WSTOPPED signal -> Printf.sprintf "stopped (signal %d)" signal

let ensure_ffmpeg () =
  match Unix.system "ffmpeg -version >/dev/null 2>&1" with
  | Unix.WEXITED 0 -> ()
  | _ -> invalid_arg "ffmpeg executable not found in PATH"

let rec mkdir_p dir =
  if dir = "" || dir = "." || dir = Filename.dir_sep then ()
  else if Sys.file_exists dir then ()
  else (
    mkdir_p (Filename.dirname dir);
    try Unix.mkdir dir 0o755 with Unix.Unix_error (Unix.EEXIST, _, _) -> ())

type ffmpeg_state = {
  fps : int;
  path : string;
  extra_args : string list;
  mutable channel : out_channel option;
  mutable width : int;
  mutable height : int;
}

let build_command state width height =
  let extra =
    match state.extra_args with
    | [] -> ""
    | args -> " " ^ String.concat " " args
  in
  Printf.sprintf
    "ffmpeg -loglevel error -y -f rawvideo -pix_fmt rgb24 -r %d -s %dx%d -i \
     -%s %s"
    state.fps width height extra
    (Filename.quote state.path)

let start_process (state : ffmpeg_state) (image : Render.image) =
  mkdir_p (Filename.dirname state.path);
  let cmd = build_command state image.width image.height in
  let channel =
    try Unix.open_process_out cmd
    with Unix.Unix_error (err, _, _) ->
      invalid_arg
        (Printf.sprintf "Failed to launch ffmpeg: %s" (Unix.error_message err))
  in
  state.channel <- Some channel;
  state.width <- image.width;
  state.height <- image.height;
  channel

let ensure_channel (state : ffmpeg_state) (image : Render.image) =
  match state.channel with
  | Some channel ->
      if image.width <> state.width || image.height <> state.height then
        invalid_arg "ffmpeg sink expects consistent frame dimensions";
      channel
  | None -> start_process state image

let close_state state =
  match state.channel with
  | None -> ()
  | Some channel ->
      flush channel;
      let status = Unix.close_process_out channel in
      state.channel <- None;
      if status <> Unix.WEXITED 0 then
        invalid_arg
          (Printf.sprintf "ffmpeg exited abnormally (%s)"
             (string_of_status status))

let create_ffmpeg_sink ~fps ~path ~extra_args =
  ensure_ffmpeg ();
  let state =
    { fps; path; extra_args; channel = None; width = 0; height = 0 }
  in
  let push frame =
    match frame with
    | Render.None -> ()
    | Render.Image image ->
        let channel = ensure_channel state image in
        let bytes = Utils.rgb24_bytes_of_image image in
        output_bytes channel bytes;
        flush channel
    | Render.Text _ | Render.Svg _ ->
        invalid_arg "ffmpeg sink expects image frames"
  in
  let close () = close_state state in
  custom ~close push

let ffmpeg ?(fps = 30) ~path () =
  create_ffmpeg_sink ~fps ~path
    ~extra_args:[ "-an"; "-vcodec"; "libx264"; "-pix_fmt"; "yuv420p" ]

let gif ?(fps = 30) ~path () =
  create_ffmpeg_sink ~fps ~path ~extra_args:[ "-f"; "gif" ]

let log_to_wandb ~project ~run ~path ~fps =
  let script_path = Filename.temp_file "fehu-wandb" ".py" in
  let python =
    Option.value (Sys.getenv_opt "FEHU_WANDB_PYTHON") ~default:"python3"
  in
  let write_script () =
    let oc = open_out script_path in
    Fun.protect
      ~finally:(fun () -> close_out oc)
      (fun () ->
        Printf.fprintf oc
          "import wandb\n\
           run = wandb.init(project=%S, name=%S, reinit=True)\n\
           run.log({'rollout': wandb.Video(%S, fps=%d)})\n\
           run.finish()\n"
          project run path fps)
  in
  Fun.protect
    (fun () ->
      write_script ();
      match
        Unix.system (Printf.sprintf "%s %s" python (Filename.quote script_path))
      with
      | Unix.WEXITED 0 -> ()
      | status ->
          invalid_arg
            (Printf.sprintf
               "wandb logging failed (%s). Ensure wandb is installed and you \
                are logged in."
               (string_of_status status)))
    ~finally:(fun () ->
      if Sys.file_exists script_path then Sys.remove script_path)

let wandb ?(fps = 30) ~project ~run () =
  let temp_path = Filename.temp_file "fehu-wandb" ".mp4" in
  let inner = ffmpeg ~fps ~path:temp_path () in
  let push frame = push inner frame in
  let close () =
    Fun.protect
      ~finally:(fun () ->
        if Sys.file_exists temp_path then Sys.remove temp_path)
      (fun () ->
        close inner;
        log_to_wandb ~project ~run ~path:temp_path ~fps)
  in
  custom ~close push

let with_sink create f =
  let sink = create () in
  Fun.protect ~finally:(fun () -> close sink) (fun () -> f sink)

let with_ffmpeg ?fps ~path f = with_sink (fun () -> ffmpeg ?fps ~path ()) f
let with_gif ?fps ~path f = with_sink (fun () -> gif ?fps ~path ()) f

let with_wandb ?fps ~project ~run f =
  with_sink (fun () -> wandb ?fps ~project ~run ()) f