### A Pluto.jl notebook ### # v0.14.5 using Markdown using InteractiveUtils # ╔═╡ d941d6c2-55bf-11eb-0002-35c7474e4050 using NiLang, Test # ╔═╡ 2061b434-0ad1-46eb-a0c7-1a5f432bfa62 begin twocol(left, right; llabel="forward", rlabel="backward") = HTML("
$llabel $rlabel
$(html(left)) $(html(right))
") example(str) = HTML("""
$str
""") title1(str) = HTML("""

'_'))>$str

""") title2(str) = HTML("""

'_'))>$str

""") titleref(str) = HTML("""$str""") using PlutoUI: TableOfContents using Pkg pkgversion(m::Module) = Pkg.TOML.parsefile(NiLang.project_relative_path("Project.toml"))["version"] hightlight(str) = HTML("$str") end; # ╔═╡ 8c2c4fa6-172f-4dde-a279-5d0aecfdbe46 module M using NiLang # define two functions function new_forward(x) if x > 0 return x * 2 elseif x < 0 return x / 2 end end function new_backward(x) if x > 0 return x / 2 elseif x < 0 return x * 2 end end # declare them as reversible to each other @dual new_forward new_backward # The following is need only when your function is differentiable using NiLang.AD: GVar function new_backward(x::GVar) if x.x > 0 GVar(new_backward(x.x), x.g * 2) elseif x.x < 0 GVar(new_backward(x.x), x.g / 2) end end function new_forward(x::GVar) if x.x > 0 GVar(new_forward(x.x), x.g / 2) elseif x.x < 0 GVar(new_forward(x.x), x.g * 2) end end end # ╔═╡ 3199a048-7b39-40f8-8183-6a54cccd91b6 using BenchmarkTools # ╔═╡ 0e1ba158-a6bc-401c-9ba7-ed78020ad068 using Base.Threads # ╔═╡ a4e76427-f051-4b29-915a-fdfce3a299bb html"""

Download this notebook
""" # ╔═╡ c2c7b4d4-f8c9-4ebf-8da2-0103f03136e7 md"# NiLang's (v$(pkgversion(NiLang))) Documentation NiLang is a embeded domain specific language (eDSL) in Julia, so one need to install [Julia](https://julialang.org/) first. Before reading this documentation, you need to know basic Julia grammar, and how to install and use packages. Also, it might be good to read the [README](https://github.com/GiggleLiu/NiLang.jl) first. In this tutorial, we focus on * NiLang grammar and design patterns, * Automatic differentiation based on reversible programming. " # ╔═╡ 12f07cc7-979c-43c3-9dc9-36ea1463c1f6 md"The symbols used in this notebook" # ╔═╡ 611b577f-4722-42bf-8f8e-aeb2fb30be71 md""" | symbol | meaning | how to type | | ------- | --------- | ----------- | | ← | allocate | \leftarrow + TAB | | → | deallocate | \rightarrow + TAB | | ↔ | exchange | \leftrightarrow + TAB | | ∅ | empty variable | \emptyset + TAB | | ~ | inverse | ~ | """ # ╔═╡ 605872cf-f3fd-462e-a2b1-7d1c5ae45efd title1("Getting started") # ╔═╡ fb3dee44-5fa9-4773-8b7f-a83c44358545 md" After installing NiLang in a Julia REPL by typing `]add NiLang`, one can use NiLang and use the macro `@i` to define a reversible function ``f_1: (x, y) → (x+5y, y)``." # ╔═╡ 70088425-6779-4a2d-ba6d-b0a34c8e93a6 @i @inline function f1(x, y; constant) x += y * constant end # ╔═╡ af738f89-3214-429c-9c7d-18a6ea0d9401 f1(1.0, 2.0; constant=5.0) # call # ╔═╡ 48d7ebc1-5def-4a57-9ec1-3fc370a4543f (~f1)(11.0, 2.0; constant = 5.0) # uncall # ╔═╡ f0e94247-f615-472b-8218-3fa287b38aa1 md"A NiLang function defines a ``\mathbb{R}^n\rightarrow\mathbb{R}^n`` (notice inputs and outputs have the same shapes) mapping, it can take both keyword and positional arguments, where positional arguments can only be used as constants. There is no `return` statement because this function returns input non-keyword variables automatically, it is forbiden to write `return` statement inside a NiLang function. One can aslo put macros like `@inline` after NiLang's `@i` macro. NiLang's macro will render the body of the function first and pass it to other macros. " # ╔═╡ 2581aa33-1dc5-40b1-aa9f-6a11cc750c93 md"`x += y * constant` is an instruction that defines a bijective mapping ``\mathbb{R}^3\rightarrow\mathbb{R}^3``. All NiLang instructions change the variable inplace. Here, we accumulate the result to `x` rather than using `y *= constant` to modify the variable directly. This is because in a regular number system, one can easily use the zero element as the eraser to erase all information, which will cause irreversibility." # ╔═╡ 60575978-081a-4bca-a3ed-2b51cd6abc92 md"One can differentiate a NiLang function with the `NiLang.gradient(f, args; iloss, kwargs...)` where `args` and `kwargs` are positional and keyword arguments for `f`, and `iloss` is the index of the loss variable." # ╔═╡ f98305cb-4ba2-404a-a5c3-65510e059504 NiLang.AD.gradient(f1, (1.0, 2.0); iloss=1, constant=5.0) # ╔═╡ e8cd6667-597f-458b-8465-1822e09a7891 md"Here, we specify the first variable as the one that stores the loss. We get ```math \begin{cases} \frac{\partial x+5y}{\partial x}=1\\ \frac{\partial x+5y}{\partial y}=5\\ \end{cases} ``` " # ╔═╡ 20145d75-004a-4c2f-b7ff-c400ca846d42 let content = md"""The above macro generates two functions, one is `f` and another is `~f` (or `Inv(f)`). The `x += y * constant` is translated to function `(x, y, constant) = PlusEq(*)(x, y, constant)`, where the function `PlusEq(*)` is bijective. ```julia julia> using MacroTools julia> MacroTools.prettify(@macroexpand @i function f(x, y; constant) x += y * constant end) quote $(Expr(:meta, :doc)) function $(Expr(:where, :(f(x, y; constant)))) hare = wrap_tuple(((PlusEq)(*))(x, y, constant)) x = hare[1] y = hare[2] constant = hare[3] (x, y) end if (NiLangCore)._typeof(f) != _typeof(~f) function $(Expr(:where, :((newt::_typeof(~f))(x, y; constant)))) boar = wrap_tuple(((MinusEq)(*))(x, y, constant)) x = boar[1] y = boar[2] constant = boar[3] (x, y) end end end ``` """ HTML("
How does the compiler work?$(html(content))
") end # ╔═╡ c682a17f-600f-4034-bfe3-a851ab645c10 title1("Instructions and operands") # ╔═╡ 5239dfe2-ea6d-4e07-a1b1-90954fe8ddc9 md""" The basic form of an instruction is `y ⊙= f(args...)`, where `⊙` can be `+`, `-`, `*`, `/` or `⊻`, where `*=` and `/=` are only reversible in the logarithmic number systems. See section $(titleref("Integers, floating-point numbers, fixed-point numbers and logarithmic numbers")) for details. A function/instruction can be used in NiLang only if its reverse is defined. A NiLang function is differentiable if the function body is composed of differentiable instructions, differentiable NiLang functions and NiLang control flows. A list of differentiable NiLang instructions are | instruction | output | | ----------- | ---------- | | ``{\rm FLIP}(y)`` | ``\sim y`` | | ``{\rm NEG}(y)`` | ``-y`` | | ``{\rm INC}(y)`` | ``y+1`` | | ``{\rm DEC}(y)`` | ``y-1`` | | ``{\rm INV}(y)`` | ``y ^ {-1}`` | | ``{\rm HADAMARD}(x, y)`` | ``\frac{1}{\sqrt{2}}(x+y), \frac{1}{\sqrt{2}}(x-y)`` | ``{\rm SWAP}(a, b)`` | ``b, a`` | | ``{\rm ROT}(a, b, \theta)`` | ``a \cos\theta - b\sin\theta, b \cos\theta + a\sin\theta, \theta`` | | ``{\rm IROT}(a, b, \theta)`` | ``a \cos\theta + b\sin\theta, b \cos\theta - a\sin\theta, \theta`` | | ``y \mathrel{\{+,-\}}= f_{+-}(args...)`` | ``y\{+, -\}f_{+-}(args...), args...`` | | ``y \mathrel{\{*, /\}}= f_{*/}(args...)`` | ``y\{*, /\}f_{*/}(args...), args...`` | Functions ``f_{+-} ∈ \rm \{identity, +, -, *, /, ^\wedge, abs, abs2, sqrt, exp, log, sin, sinh, asin, cos, cosh,`` ``\rm acos, tan, tanh, atan, sincos, convert\}`` and ``f_{*/}∈\rm \{identity, +, -, *, /, ^\wedge, convert\}.`` Functions `FLIP`, `NEG`, `INV`, `HADAMARD`, `SWAP` and `y ⊻= f_{⊻}(args...)` are self-reversible (or reflexive). {`ROT`, `IROT`} and {`INC`, `DEC`}, {`y += f_{+-}(args...)`, `y -= f_{+-}(args...)`} and {`y *= f_{*/}(args...)`, `y /= f_{*/}(args...)`} are pair-wise reversible. For Jacobians and Hessians defined on these instructions, please check this [blog post](https://giggleliu.github.io/2020/01/18/jacobians.html). This set of instructions is extensible, see section $(titleref("Extending the instruction set")) for an example. """ # ╔═╡ e4d86a5a-e820-4a70-8a87-08bac416291b md"The operands of an instruction can be a composition of the following expressions" # ╔═╡ c307b6a4-906d-4be7-9fd7-57c942aded51 md""" | expression | meaning | | ---------- | ------- | | x | change a variable | | x.field | change a field | | x.:1 | change a tuple element | | x' | change the adjoint | | -x | change the inverse | | x[i] | change an array/dict element | | (x, y, z) | change multiple variables | | @fields x | change the fields of an object | | A{Float64}(x, y) | wrap, update fields and unwrap | | x \|> subarray(:,i) | change the view of an array | | x \|> f | change a field map or a bijective mapping | | @const 3 | target is a constant that can not be changed | | @skip! f(x) | an expression that can not be assigned back | """ # ╔═╡ 47b502d4-e8af-4d58-9067-9700784ea435 md"In the following example, one modifies the negated real part of a complex number in a vector inside a tuple directly." # ╔═╡ faecc0a7-55d7-42a1-8e9f-7e30143eef9c @i function dataview_func1(x::Vector, y::Tuple, θ) (-x[2].re, y.:1) += sincos(θ) end # ╔═╡ 4cbe69d6-b68f-4bda-a0dd-209f9ee54f18 dataview_func1([1+2.0im, 3+4im], (1.0,2.0), π/6) # ╔═╡ 7dc82f28-77bf-40da-b520-800ed1bc80c9 md"""One can check the real part of the second element of `x` is decreased by `sin(π/6)`, while the first element of the tuple is increased by `cos(π/6)`. A NiLang instruction is different to a regular Julia instruction in that $(hightlight("A NiLang instruction changes variables inplace")), eventhough `ComplexF64`, `-x` and `tuple` are all considered as immutable in Julia.""" # ╔═╡ 9f5f9de3-9558-4c18-9d98-b77d19b570ec example("Complex valued log") # ╔═╡ 6dfcfa19-f78f-4dac-89f7-d3c5dbe17987 @i function complex_log(y!::Complex{T}, x::Complex{T}) where T n ← zero(T) n += abs(x) y!.re += log(n) y!.im += angle(x) n -= abs(x) n → zero(T) end; @test complex_log(0.0im, 3.0+2.0im)[1] ≈ log(3.0+2.0im) # ╔═╡ dad1c6c0-d61b-4f9f-a71e-e683fe143aaa title2("Broadcasting") # ╔═╡ 3e4a3916-8fe4-4262-bcd3-3014822717a3 md"The interfaces is similar to julia's native broadcast, but expanded to native NiLang loops." # ╔═╡ 34906208-e6f1-4a67-860a-a7b056a86dde @i function complex_log_broadcast!(ys!, xs) complex_log.(ys!, xs) end; @test (x=[1.0+2im, 3.0+4im, 4.0+5im]; complex_log_broadcast!(zeros(ComplexF64, 3), x)[1] ≈ log.(x)) # ╔═╡ 4601df35-679f-465d-9191-c18748b2fd83 title2("Avoid shared read-write") # ╔═╡ 4fc72b9d-19a2-40f1-a4a8-5e97d3d5e529 md"Shared read-write is not allowed" # ╔═╡ a0fde16f-8454-4f5c-a29c-a9e415c0c311 # `y -= y` effectively clears the content in `y`, this is why shared read-write is so dangerous. @test_throws LoadError macroexpand(NiLang, :(@i function shared_readwrite_error(y) y -= y end)) # ╔═╡ 633ff8f3-8d93-4f73-bec2-c42070e6ece9 md"NiLang is more restrictive, it forbids shared read too. This is in purpose, shared read will become shared write to the gradient field in the back-propagation of gradients - the main goal of NiLang." # ╔═╡ 57f2d890-b5a0-47b7-9e3d-af4d03b10605 # `shared read is also forbidden` @test_throws LoadError macroexpand(NiLang, :(@i function shared_read_error(x, y) x -= y * y # should be written as `x -= y^2` end)) # ╔═╡ 34063cd0-171e-46ce-80dd-52a341fa50a1 md"The correct way of avoiding shared read is renaming one of the variable." # ╔═╡ 5d5d01db-8ff9-434c-8771-1fec6393e1fb @i function avoid_shared_read(x, y) tmp ← zero(y) tmp += y x -= y * tmp tmp -= y tmp → zero(y) end # ╔═╡ 10d85a50-f2f9-403e-8f6c-baef61cf702a avoid_shared_read(0.0, 3.0) # ╔═╡ b52648bf-a28a-48af-8912-31729d943ce0 md"Shared read-write issue is more tricky when one uses NiLang to write kernel function in a parallel program (multi-threading, MPI and CUDA). See $(titleref(\"Multi-threading and CUDA\")) for details." # ╔═╡ f45db10f-a836-40f3-9d8d-054ea6540e87 title2("Protect constant variables") # ╔═╡ 1903563e-ccc2-44d9-9dbe-e5dede275b3c md""" To achieve the goal of "everything is mutable", NiLang assigns the output value back to the input variable after a call. Sometime it can cause issues for special variables like function, type parameters and constants (including the results generated from a function call). One can use `@const` (assert a variable is a constant) or `@skip!` (skip assigning back) to avoid such complications. """ # ╔═╡ 583f2585-15a3-47c6-a70e-e2f002754028 md"When using a function (e.g. `exp` as shown bellow) as an variable, one should be careful about the scope issue." # ╔═╡ 60e6ff80-3593-4ae4-a273-914847f692db @i function func_arg(y, f, x) y += f(x) end # ╔═╡ 9e5cfd68-b58d-4d83-aae2-447e5f805c97 @i function use_func_arg(y, x) func_arg(y, exp, x) end # ╔═╡ dc85a942-cf52-4405-ad03-32a768e1b6e7 @test_throws UndefVarError use_func_arg(0.0, 3.0) # ╔═╡ 5cdd346b-10a5-485c-ba78-4c0b3cb0e02f md"We see an error, but why calling `f2g` causes an error? If one check the generated code with `macroexpand`, one will see the `exp` is assigned in the local scope. The compiler takes it as a local variable and compaints that `exp` is not defined." # ╔═╡ af9287b7-6131-46f6-beb8-6885e55e1975 macroexpand(NiLang, :(@i function use_func_arg(y, x) f2g(y, exp, x) end)) |> NiLangCore.rmlines # ╔═╡ e20eeabf-1c80-431e-8cfc-4d1b79c52b5a @i function avoid_assignback(y, x) func_arg(y, (@const exp), x) end; @test avoid_assignback(0.0, 3.0)[1] == exp(3) # ╔═╡ 90d30eea-53de-48a0-9700-ff35681fdf38 md"Type parameter can not be assigned back too." # ╔═╡ 390f58a5-6f5f-4d3a-bb16-ba04e43a07e7 @test_throws ErrorException Core.eval(NiLang, :(@i function type_arg(t::Type{T}, x) where T x += one(T) end)) # ╔═╡ 2b57443e-a516-434b-be86-80616a98e2f5 @i function avoid_type_assignback(t::Type{T}, x) where T x += one(@skip! T) end; @test avoid_type_assignback(Float64, 0.0)[2] == 1.0 # ╔═╡ fc2e27f9-b7ba-44cc-a953-6745548ad733 md"A function call that returning a constant should also be decorated with the `@const` or `@skip!` macro." # ╔═╡ fc744931-360b-4478-9f77-c50f048de243 @test_throws LoadError macroexpand(NiLang, :(@i function funccall_arg(y, x::Matrix) where T y += size(x, 1) * size(x, 2) end)) # ╔═╡ 9a152b36-f377-44da-9700-ca9e05e365ff @i function avoid_funccall_assignback(y, x::Matrix) where T y += (@const size(x, 1)) * (@const size(x, 2)) end; @test avoid_funccall_assignback(0, randn(3,4))[1] == 12 # ╔═╡ b6dcd18c-606f-4340-b2ec-163e8bad03f5 title1("Variable manipulation") # ╔═╡ a1a29f34-f8a9-4e9f-9afe-7d0096771440 title2("Allocate and deallocate a variable") # ╔═╡ 90bd6ad4-3dd8-4e7c-b445-aed1d248a2ec md""" One can allocate a new variable `x` like `x ← constant` and deallocate a variable with known value with `x → known value`. They are reversible to each other with the following relation. $( twocol(md" ```julia x ← constant ```", md" ```julia x → constant ``` ") ) For example """ # ╔═╡ c0259e48-1973-486c-a828-1fcd3e4331c6 @i function alloc_func1() tmp ← 1 # some code that uses `tmp` for computing and restores it to `1` tmp → 1 end # ╔═╡ 8bbffa31-04a6-49ca-b36f-4d4140d75992 md"allocate multiple variable of the same type at one time" # ╔═╡ a6f18c34-80ee-4b52-9ff8-f3c1b1d80f90 @i function power12(y, x::T) where T @zeros T a b c # three variable of type `T` a += x^2 b += a^2 c += b*a y += c^2 c -= b*a b -= a^2 a -= x^2 @safe @show a b c x y ~@zeros T a b c end # ╔═╡ a694132b-4f52-467f-8bc4-dc32fe2812db @test power12(0, 2)[1] == 4096 # ╔═╡ 8c2c82f2-1240-4f2f-830e-ee8021c1a41a md"One can copy and push a value into a stack and use it later. It inverse operation will pop out a variable and assert its value." # ╔═╡ 6203cf10-f8cc-4fb9-b814-7552b68c01dc twocol(md" ```julia stack[end+1] ← variable ```", md" ```julia stack[end] → variable ``` ") # ╔═╡ f97a6bab-b9f9-4b95-98a9-381c51397526 @i function stack_push_and_pop!(stack, x, y, z) z += y stack[end+1] ← x # copy a variable into a stack stack[end+1] ← y stack[end] → z # pop a variable from a stack, `z` must have the same value as the variable. end # ╔═╡ 2a2970f4-ab01-486b-89a2-6ff96f734018 md"A less recommended approach is using the global stacks in NiLang, since NiLang is an eDSL, it can not guarantee the access order. Available global stacks are `FLOAT64_STACK`, `COMPLEXF64_STACK`, `INT64_STACK` and their 32 bit counter parts, as well as a `BOOL_STACK`." # ╔═╡ 0b80d9be-53d7-4bf3-a558-659607af4709 @i function stack_push_and_pop!(x, y) GLOBAL_STACK[end+1] ← x # copy a variable into a stack FLOAT64_STACK[end+1] ← y end # ╔═╡ 4ca48a2e-43da-457a-8e9f-6476097e4d7b let stack = FastStack{Float64}(1000) # a preallocated stack of size 1000 stack_push_and_pop!(stack, 5.0, 1.0, 0.0) # you will get a stack of size 1 @test length(stack) == 1 end # ╔═╡ 92362fda-bae2-4e35-bfe4-dcaea853d50b let NiLang.empty_global_stacks!() # empty stacks stack_push_and_pop!(5.0, 1.0) @test length(GLOBAL_STACK) == 1 @test length(FLOAT64_STACK) == 1 end # ╔═╡ db9e7940-39f1-4ccf-ac70-146a521daa6e md"one can also allocate and deallocate on dicts" # ╔═╡ 93936612-1447-4114-b864-aba43adef4bd md""" The forward pass of dictionary allocation adds an entry to the dictionary (cause key error if the key already exists), the backward pass. The backward pass checks the variable in the dictionary is consistent with the asserted variable, and delete the key. $( twocol(md" ```julia dict[key] ← variable ```", md" ```julia dict[key] → variable ``` ") ) """ # ╔═╡ b2add70c-c5d6-4e0f-a153-43e21a197181 @i function dict_assign(dict::Dict) var1 ← 3.14 # copy a new variable to a dict dict["data"] ← var1 # deallocate the original variables var1 → dict["data"] end # ╔═╡ 5c03d5a5-99f0-4efd-9a32-ce6d7c2b266c dict_assign(Dict()) # ╔═╡ 2349e3ea-3053-42a4-b9d9-f97a76e4abd7 title2("Exchange two variables") # ╔═╡ 269f18ee-3cd8-466a-a522-7c624503e31b let expr = twocol(md" ```julia var1 ↔ var2 ```", md" ```julia var1 ↔ var2 ``` ") md"""One can exchange two variables is using `↔`. $expr """ end # ╔═╡ 89139719-c478-4066-9452-f9893f36d561 @i function exchange_func1(x, y) x ↔ y end # ╔═╡ d620c5ee-7d9c-4d3f-9e87-0c828dfab9ca exchange_func1(3, 5) # ╔═╡ 255d01b9-a873-4e63-9298-9d8f073348b0 md"One can also make a \"link\" by exchanging a variable with an empty variable such as `var::∅` and `stack[end+1]`. The forward pass push a variable to the stack and deallocate variable. The backward pass pops a variable and asserts its value." # ╔═╡ f6cf1729-766c-4ed7-b004-c8c8ec6c7e07 let expr = twocol(md" ```julia stack[end+1] ↔ var2 var1::∅ ↔ var2 ```", md" ```julia stack[end] ↔ var2::∅ var1 ↔ var2::∅ ``` ") end # ╔═╡ 3645d672-423f-4ac8-805f-0452793fee5a @i function exchange_func2(x, y) anc ← 0.0 anc += x * y anc ↔ z::∅ # declare `z` as an empty variable # after exchange, `anc` is empty and deallocated automatically. z -= x * y z → 0.0 end # ╔═╡ c2a0024e-11dd-4ef7-8346-4374d98cafc0 exchange_func2(3, 4) # ╔═╡ b20004e9-3c73-4dfb-8fd5-f377786fd53b md"When exchanging with a stack top + 1, it means push and deallocate." # ╔═╡ 5c1952b1-5016-4c87-b23c-8e6a235bf8cd @i function stack_exchange(stack, y, x) stack[end+1] ↔ y # push a variable into a stack and deallocate `y` y ← 1.2 # since `y` is deallocated, you can assign any value to it stack[end] ↔ anc::∅ # pop a variable to `anc` anc ↔ x # exchange `anc` and x stack[end+1] ↔ anc # push `anc` back to stack end # ╔═╡ 8e4470ee-01da-4547-b091-c4f65cd729b0 let stack = FastStack{Float64}(1000) # a preallocated stack stack_exchange(stack, 2.0, 3.0) # you will get a stack of size 2 @test length(stack) == 1 && stack.data[1] == 3.0 end # ╔═╡ 0863bd06-cc70-4dde-b3b2-0a466805a356 md"""$(title2("Integers, floating-point numbers, fixed-point numbers and logarithmic numbers")) A fixed-point zero with 43 fraction bits can be declared as `x ← Fixed43(0.0)` or `x ← zero(Fixed43)`, a logarithmic one can be declared as `x ← ULogarithmic(1.0)`, `x ← one(ULogarithmic{Float64})` or `x ← ULogarithmic(Fixed43(1.0))`. A complex number zero can be defined as `x ← Complex(0.0, 0.0)`. | Number Type | += | *= | ⊻= | Source | | ---- | ---- | ---- | ---- | ---- | | boolean | - | - | ✓ | JuliaLang | | integer | ✓ | × | ✓ | JuliaLang | | floating-point number | ✓ (rounding error) | × | - | JuliaLang | | fixed-point number | ✓ | × | - | [FixedPointNumbers.jl](https://github.com/JuliaMath/FixedPointNumbers.jl) | logarithmic number | × | ✓ | - | [LogarithmicNumbers.jl](https://github.com/cjdoris/LogarithmicNumbers.jl) * `✓`: an operation has its reverse when operating on a number type. * `×`: an operation does not have a reverse when operating on a number type. * `-`: an operation does not apply on a number type. The `+=` operation is not regoriously reversible on floating point numbers, but we ignore the rounding errors in NiLang and use the reversibility check to detect the potential too-large rounding errors. Whether logarithmic number has rounding errors depends on its content type. If it uses floating point numbers as storage, then yes, otherwise if it uses fixed point number as the content type, then no. One can use the `y ⊙= convert(x)` statement to convert `x` to the target type `typeof(y)` and accumulate it to `y`. Here `⊙=` can be one of `+=`, `*=` and `⊻=` that has its reverse on type `typeof(y)`. """ # ╔═╡ a0bae195-04e1-4642-9e14-fe4691e0906b md"Fixed point numbers and Floating point numbers are reversible under the `+=` operation" # ╔═╡ 20d6e8a0-2cf5-48ad-9549-60506b42b970 @i function fixed_pluseq(y1::T, x::T) where T<:Union{Fixed43, AbstractFloat} y1 += x end; @test fixed_pluseq(Fixed43(0.5), Fixed43(0.6))[1] === Fixed43(1.1) && fixed_pluseq(0.5, 0.6)[1] === 1.1 # ╔═╡ 7dee5748-ed73-4e13-aa80-7a50efbc8449 @i function fixed_muleq(y1::T, x::T) where T<:Union{Fixed43, AbstractFloat} y1 *= x end; @test_throws MethodError fixed_muleq(Fixed43(1.0), Fixed43(2.0)) # ╔═╡ 4c719e9b-641e-404e-9ab7-59e89135f3ba md"Logarithmic numbers are reversible under the `*=` operation" # ╔═╡ 77947e00-42c3-4c9e-b62a-b4b29489db43 @i function ulog_pluseq(y1::ULogarithmic{T}, x::ULogarithmic{T}) where T y1 += x end; @test_throws MethodError ulog_pluseq(ULogarithmic(1.0), ULogarithmic(3.0)) # ╔═╡ 2614127d-34fb-4c3d-b678-42693f3c9341 @i function ulog_muleq(y1::ULogarithmic{T}, x::ULogarithmic{T}) where T y1 *= x end; @test ulog_muleq(ULogarithmic(1.0), ULogarithmic(3.0))[1] === ULogarithmic(3.0) # ╔═╡ 8f169235-3bd1-4cc4-a083-79736d306ad5 example("computing x ^ 3 with logarithmic numbers") # ╔═╡ dfc9d305-5bce-4555-bfa3-d8d61fe4ca09 @i function power3(y::ULogarithmic{T}, x::T) where T for i=1:3 y *= convert(x) end end; @test power3(ULogarithmic(1.0), 3.0)[1] ≈ 27 # ╔═╡ f6bfa015-c101-45e8-995c-2bb6a3b7dc7d title2("Types and arrays") # ╔═╡ 8651d7ec-6bcd-4dbe-a062-c4bde32e5e91 md" A Julia type can be accessed in NiLang if its default constructor is not overloaded, because NiLang requires the default constructor to \"modify\" a field of a immutable type. " # ╔═╡ edaa9fdb-3af8-4554-a701-0e3bff2107a5 md"It is also possbile to extract the fields directly." # ╔═╡ 7551a880-340e-4e3f-815b-188e73f7eb9a @i function complex_add(y::Complex{T}, x::Complex{T}) where T ((a, b), (c, d))::∅ ↔ ((@fields x), (@fields y)) a += c b += d ((a, b), (c, d)) ↔ ((@fields x), (@fields y))::∅ end # ╔═╡ 0489e51b-781f-4441-bb7f-ff3bd2e848ad @test complex_add(1+2im, 3+4im) == (1+2im, 4+6im) # ╔═╡ 7b0d30d6-39ff-4f6e-b13c-0ddbfcb576e5 md"Type cast is also possible" # ╔═╡ 042297d8-6ab3-4ae6-b6e7-3b1ab2d5553b @i function add4(a, b, c, d) complex_add(Complex{}(a, b), Complex{}(c, d)) # do not omit `{}` end # ╔═╡ 57d65a36-bfa8-4dc2-8e11-d87fa1324122 @test add4(1, 2, 3, 4) == (1, 2, 4, 6) # ╔═╡ c21d81c3-981f-4472-ad61-d1661bfe5c4e example("Implementing \"axpy\" function") # ╔═╡ 99d6fe7b-d704-48f3-b115-2b3159a78068 md"`axpy` function is defined as ``\vec y += a \vec x``. One can modify the `Array` directly" # ╔═╡ 1950ff70-54eb-4ece-a26d-a23fd0e90f5a @i function arrayaxpy!(y!::Vector{T}, a::T, x::Vector{T}) where T for i=1:length(x) y![i] += a * x[i] end end; @test arrayaxpy!(zeros(10), 2.0, collect(1.0:10.0))[1] ≈ collect(2.0:2.0:20.0) # ╔═╡ 21458f81-9007-46f8-92e0-7a17c60beb36 md"To modify an element of a `Tuple`, we need to use a different style to avoid confusion with array" # ╔═╡ 7813f4ce-6e98-45f3-94a8-7f5981129f2b @i function tupleaxpy!(y!::NTuple{N,T}, a::T, x::NTuple{N,T}) where {N, T} for i=1:length(x) (y! |> tget(i)) += a * (x |> tget(i)) end end; @test tupleaxpy!((0,0,0), 2, (1,2,3))[1] == (2, 4, 6) # ╔═╡ 59ec7cb7-6011-456d-9f57-a55bb8ea51a0 md"Here `data |> tget(i)` represents the `i`th field of the tuple (note it is not allowed to write `data.:i`)." # ╔═╡ aacf63a2-9708-40db-8928-049621a7bbc4 md"## Control flows" # ╔═╡ ad0097e7-c8ad-457a-82a9-18b998a9e9fb md""" #### If statement The condition expression in `if` statement contains two parts, one is precondition and another is postcondition. $( twocol(md" ```julia if (precondition[, postcondition]) ... end ``` ", md" ```julia if (postcondition[, precondition]) ~(...) end ``` ") ) where `...` are statements and `~(...)` are the backward execution of them. """ # ╔═╡ 94cd1345-3132-4882-86fe-d2429f610d1d md"""If no postcondition is provided, it means the precondition is the same as the postcondition. It is translated to `if (cond, cond) ... end`. `elseif` is also supported.""" # ╔═╡ 4a558bd3-6e42-4c61-bd23-888b7f33ae25 @i function f7a(x) if x > 1 x -= 1 end end; @test_throws InvertibilityError f7a(1.2) # ╔═╡ 004f727a-e0c8-49cb-8858-dfdf4d3ac57a @i function f7b(x, branch_keeper) branch_keeper ⊻= x > 1 if (x > 1, branch_keeper) x += 1 end end; @test f7b(1.2, false)[1] == 2.2 # ╔═╡ 4c03cde9-b643-40ff-b275-f1795f88949e title2("For statement") # ╔═╡ fae7c74e-d25e-4c1e-ac97-199e6dae3365 md""" The reversible `for` statement is similar to its irreversible counterpart. $( twocol(md" ```julia for iter = start:step:stop ... end ``` ", md" ```julia for iter = stop:-step:start ~(...) end ``` ") ) """ # ╔═╡ 2f2b24ea-66d0-4b3b-a460-53b6b3f28ef0 md"The iterator length should not be changed during the iteration." # ╔═╡ e9dbb64a-27b9-443a-b917-69d55c290235 @i function f7c(x, y) for i=1:length(x) POP!(x, y[i]) end end; @test_throws InvertibilityError f7c([1,2,3], [0,0,0]) # ╔═╡ 0d56ce96-81a5-4102-acbc-7d88f80adcb3 md"There is an `InvertibilityError` because the length of the `x` has been changed. The inverse execution will give incorrect result." # ╔═╡ 95c41bd1-e50a-42e8-93c3-3b754a458c13 title2("While statement") # ╔═╡ b8629aeb-6c9a-44ed-87a1-9ab22d9485ed md""" The reversible `while` statement starts with the `@from` macro. $( twocol(md" ```julia @from condition1 while condition2 ... end ``` ", md" ```julia @from !(condition2) while !(condition1) ~(...) end ``` ") ) """ # ╔═╡ 3b211406-041f-4b41-acae-3958e4a37224 md"where the `condition1` in the forward pass is a condition that holds before entering the loop body, but broken at the first iteration, while `condition2` is just a normal while condition. In the backward pass, `!(condition1)` becomes the criteria to break the loop." # ╔═╡ 62522772-cb59-4d13-acdd-d5067b223910 @i function f7d(x, i) @from i==0 while i<10 i += 1 x += 1 end end # ╔═╡ 2ba68a0f-6e36-4ea2-a91d-6af43741bad1 f7d(1, 0) # ╔═╡ 75cebaf1-38de-475f-892e-346fd2b46f6f @test (~f7d)(11, 10) == (1, 0) # ╔═╡ 72dcf2fe-eb48-4dee-8121-efafc87637e3 title2("Compute-copy-uncompute statement") # ╔═╡ 84321198-93d4-4d22-8c0f-a5a10b884e1f md"The *compute-copy-uncompute* statement is a widely used design pattern in reversible programming. We compute the forward pass for the result, then we copy the result to the output variable, and run the backward pass to erase intermediate results. For example, to compute `y = x * exp(k)`, we might write the following code" # ╔═╡ 32244789-afbf-4215-97cd-15483f438eee @i function f7e(y, x, k) expk ← zero(k) expk += exp(k) y += x * expk # uncompute the ancilla and deallocate it expk -= exp(k) expk → zero(k) end; @test f7e(0.0, 2.0, 3.0)[1] ≈ 2.0 * exp(3.0) # ╔═╡ 4b7f0baf-0316-4da7-9ded-50c064ddbaa3 md"It is equivalent to the following statement that generates the backward pass automatically for you." # ╔═╡ 0e02952c-7589-4606-b006-16a9f3e52ae1 @i function f7f(y, x, k) # record the forward pass @routine begin expk ← zero(k) expk += exp(k) end y += x * expk # reverse execute the recorded the program ~@routine end; @test f7f(0.0, 2.0, 3.0)[1] ≈ 2.0 * exp(3.0) # ╔═╡ f0904d3f-1bf1-459c-9959-b53c0f774e3f example("Computing Fibonacci numbers") # ╔═╡ 19bb2af5-2a67-453d-82b0-7d3059b1fa47 md"The sequence of Fibonacci numbers are: 1, 1, 2, 3, 5, 8" # ╔═╡ 5b5858bf-63ac-4e31-a516-055a9cd18ffe @i function rfib(out!, n::T) where T @routine begin n1 ← zero(T) n2 ← zero(T) n1 += n - 1 n2 += n - 2 end if (value(n) <= 2, ~) out! += 1 else rfib(out!, n1) rfib(out!, n2) end ~@routine end # ╔═╡ 95060588-f24b-4eeb-9b0b-ed7159962a3c @test rfib(0, 6)[1] == 8 # ╔═╡ c4cd9f88-9cd6-4364-b016-78f90aba6a66 title1("Extending the instruction set") # They are translated to `y += f(args...)` is translated to `PlusEq(f)(y, args...)`, `y -= f(args...)` is translated to `MinusEq(f)(y, args...)`, `y *= f(args...)` is translated to `MulEq(f)(y, args...)`, `y /= f(args...)` is translated to `DivEq(f)(y, args...)` and `y ⊻= f(args...)` is translated to `XorEq(f)(y, args...)`. # ╔═╡ f6049c78-7468-47ce-a4a5-84fab34d115a title2("How to create a new elementary reversible function") # ╔═╡ b0f73825-bbb1-448c-b491-bf634fdd398a md"To define a pair of elementary functions that **reverse to each other**, 1. declare two functions `f` and `g` that each of them defines a mapping ``\mathbb{R}^n \rightarrow \mathbb{R}^n`` 2. use `@dual f g` to tell NiLang they are reversible to each other. 3. if you want to make `f` and `g` differentiable, you can specify backward rules on these two function by defining two mappings on ``\mathbb{G}^n\rightarrow \mathbb{G}^n``, where ``\mathbb{G}`` is a 2-tuple of ``\mathbb{R}`` (or `NiLang.AD.GVar`) in NiLang. It is similar to `ForwardDiff.Dual` (check [ForwardDiff](https://github.com/JuliaDiff/ForwardDiff.jl)) but defined for the backward pass. To define a **self-reversible** elementary function 1. declare a functions `f` that defines a mapping ``\mathbb{R}^n \rightarrow \mathbb{R}^n`` 2. use `@selfdual f` 3. define the backward rule on `f` to make it differentiable. " # ╔═╡ 648cdcd6-f4f5-461f-a525-4b350cae9eb0 example("defining a new elementary function") # ╔═╡ d6b1abd6-749d-4591-99e8-64aaa9199ab5 md""" One can use the invertibility checker to check if the function is really reversible (under a certain tolerance `NiLangCore.GLOBAL_ATOL[]` = $(NiLangCore.GLOBAL_ATOL[])). """ # ╔═╡ f502b8c1-9b80-4e67-80e8-a64ddb88fb0f @test NiLang.check_inv(M.new_forward, (3.0,)) # ╔═╡ 0bce342e-9a8e-4005-8b88-82da2d2c7163 md""" To check of the gradients are properly defined, one can use `NiLang.AD.check_grad` """ # ╔═╡ fd9cf757-2698-4886-9f0a-c6c23ff0d331 @test NiLang.AD.check_grad(M.new_forward, (3.0,); iloss=1) # ╔═╡ dda6652a-d063-4511-8041-e869bb88ca26 @test NiLang.AD.check_grad(M.new_backward, (3.0,); iloss=1) # ╔═╡ a7d47e83-7f44-49d0-a43d-e01316fc6eba title1("Performance Tips") # ╔═╡ eca3efef-f35b-4623-8af8-0b830a55566d md"The following trick still work in NiLang * Removing boundary check with `@inbounds` (it works on FastStack), * Add `@inline` before short functions, * Add `@simd` before a for loop, Other tricks like type stability are introduced in the [Julia documentation](https://docs.julialang.org/en/v1/manual/performance-tips/). " # ╔═╡ 45985244-adbf-4d6d-9732-a963cca62212 title2("Remove reversibility check") # ╔═╡ 83d7e75f-7273-4c6a-bec1-a2180ebc3fb9 md"This can be done by putting an `@invcheckoff` before a code block." # ╔═╡ 7fb05c65-f47c-430a-b588-c2f9bade40a9 example("computing the exp function by Taylor expansion") # ╔═╡ 14c0caa1-51ea-448c-a7dc-d06e34dd0895 md"Note: this is not a clever implementation. There is an approach of defining it without allocation." # ╔═╡ 457d07bb-e999-413e-8f29-58714670296f @i function exp_with_reversibility_check(y::T, x::T) where T @routine begin N ← 1_000 anc ← zeros(T, N) anc[1] += 1 anc_y ← T(2.0) for i=2:N @routine begin temp ← zero(T) temp += x * anc[i-1] end anc[i] += temp / i anc_y += anc[i] ~@routine end end y += anc_y ~@routine end # ╔═╡ ac53eac0-1a59-4407-8bf6-3d8b966a9bff @benchmark exp_with_reversibility_check(0.0, 1.0) seconds=0.3 # ╔═╡ 85c8ac7b-54f5-47dc-bd50-e78ffd6cf1cf @i function exp_without_reversibility_check(y::T, x::T) where T @routine @invcheckoff begin N ← 1_000 anc ← zeros(T, N) anc[1] += 1 anc_y ← T(2.0) for i=2:N @routine begin temp ← zero(T) temp += x * anc[i-1] end anc[i] += temp / i anc_y += anc[i] ~@routine end end y += anc_y ~@routine end # ╔═╡ 95c55847-0591-4f7f-b9a1-aa974ccfef69 @benchmark exp_without_reversibility_check(0.0, 1.0) seconds=0.3 # ╔═╡ 91f8cfc6-e261-4945-8506-eed8caa607c2 title1("Multi-threading and CUDA") # ╔═╡ c82b3b5c-c4e2-4bf6-b4ec-0d05ba9a669b @i function multi_thread_exp(y::Vector, x::Vector) # check the size of `x` and `y`. `@assert` is not a valid statement in NiLang, so one should decorate it with `@safe` to tell the compiler, doing this is safe, do not check this statement. @safe @assert length(x) == length(y) @threads for i=1:length(y) y[i] += exp(x[i]) end end; @test multi_thread_exp(zeros(3), [1.0, 2.0, 3.0])[1] ≈ [exp(1.0), exp(2.0), exp(3.0)] # ╔═╡ 32d75270-60d7-4326-a4ff-8674d0fbd491 md"With [CUDA](https://github.com/JuliaGPU/CUDA.jl), one can also define parallel reversile and differentiable GPU device functions. Use the broadcasting version `y += x` as an example." # ╔═╡ 4b8834c1-8bb3-49f2-ae9e-1dbb8832d7f0 @i function addkernel(target, source) @invcheckoff begin @routine b ← (blockIdx().x-1) * blockDim().x + threadIdx().x if (b <= length(target), ~) @inbounds target[b] += source[b] end ~@routine end end # ╔═╡ d8f0ae56-e643-48a1-86ee-1cd907ecb662 md"One can launch the kernel function in NiLang with `@cuda`" # ╔═╡ e72b9dc2-dfac-4631-b114-01ec14297427 md""" ```julia using CUDA @i function :(+=)(identity)(target::CuArray, source::CuArray) @safe @assert length(target) == length(source) @cuda threads=256 blocks=ceil(Int,length(target)/256) addkernel(target, source) end ``` """ # ╔═╡ 8c93a773-edc0-4ec2-88ef-1b58b7deddc5 title2("Shared read-write in parallel computing and autodiff") # ╔═╡ 16d08950-0575-4a4b-afc8-11ddca3198c7 md"The parallel code may suffer from the shared read issue when computing gradients. Let's take a look at a parallel code that computes the loss ``\mathcal{L} = \sum x \vec z``." # ╔═╡ 7c594d19-59fc-433a-bffa-c63bad46869e @i function shared_read(loss::Real, y::Vector, x::Real, z::Vector) @safe @assert length(z) == length(y) @threads for i=1:length(y) y[i] += x * z[i] end for i=1:length(y) loss += y[i] end end; @test shared_read(0.0, zeros(3), 2.0, [1.0, 2.0, 3.0])[1] ≈ 12 # ╔═╡ 345b344d-afda-4ce1-a0e7-ce6063a69206 md"However, when computing the gradients, the gradient on `x` will not be computed correctly." # ╔═╡ c55eb045-daca-42f9-a357-095edef24644 let z = randn(100) _, gy, gx, gz = NiLang.AD.gradient(shared_read, (0.0, zeros(100), 2.0, z); iloss=1) @test gx ≈ sum(z) end # ╔═╡ c6903b65-a8b8-4aef-8c43-c822077b9d0e md"The error is expected. Because the variable `x` is shared by multiple threads, when updating the gradient field of `x` in the backward pass, all threads will try to update the same gradient field, this is famous [race condition](https://en.wikipedia.org/wiki/Race_condition) in parallel computing." # ╔═╡ f80353d6-0dfe-4b0a-a1af-655d344473bf title1("Resources") # ╔═╡ 4ca276fb-859d-4c5a-81c3-8e4b28922fa4 title2("Help and Discussion") # ╔═╡ 3d020209-b8dd-4605-9329-78a985f0a6a3 md""" `reversible-computing` channel of [Julia slack](https://julialang.org/slack/) and [Julia Zulip](https://julialang.zulipchat.com/register/). """ # ╔═╡ c7ec3496-79ea-4956-976c-b88dd22207c7 title2("Learning") # ╔═╡ 8530a9d1-5a27-4a1d-883f-4b033a6f8fe4 md""" 1. Reversible computing book: Perumalla, Kalyan S. Introduction to reversible computing. CRC Press, 2013. 2. Our paper: Liu, Jin-Guo, and Taine Zhao. "Differentiate Everything with a Reversible Programming Language." arXiv:2003.04617 (2020). """ # ╔═╡ 7ce31932-0447-4445-99aa-7ebced7d0bad TableOfContents() # ╔═╡ Cell order: # ╟─2061b434-0ad1-46eb-a0c7-1a5f432bfa62 # ╟─a4e76427-f051-4b29-915a-fdfce3a299bb # ╟─c2c7b4d4-f8c9-4ebf-8da2-0103f03136e7 # ╟─12f07cc7-979c-43c3-9dc9-36ea1463c1f6 # ╟─611b577f-4722-42bf-8f8e-aeb2fb30be71 # ╟─605872cf-f3fd-462e-a2b1-7d1c5ae45efd # ╟─fb3dee44-5fa9-4773-8b7f-a83c44358545 # ╠═d941d6c2-55bf-11eb-0002-35c7474e4050 # ╠═70088425-6779-4a2d-ba6d-b0a34c8e93a6 # ╠═af738f89-3214-429c-9c7d-18a6ea0d9401 # ╠═48d7ebc1-5def-4a57-9ec1-3fc370a4543f # ╟─f0e94247-f615-472b-8218-3fa287b38aa1 # ╟─2581aa33-1dc5-40b1-aa9f-6a11cc750c93 # ╟─60575978-081a-4bca-a3ed-2b51cd6abc92 # ╠═f98305cb-4ba2-404a-a5c3-65510e059504 # ╟─e8cd6667-597f-458b-8465-1822e09a7891 # ╟─20145d75-004a-4c2f-b7ff-c400ca846d42 # ╟─c682a17f-600f-4034-bfe3-a851ab645c10 # ╟─5239dfe2-ea6d-4e07-a1b1-90954fe8ddc9 # ╟─e4d86a5a-e820-4a70-8a87-08bac416291b # ╟─c307b6a4-906d-4be7-9fd7-57c942aded51 # ╟─47b502d4-e8af-4d58-9067-9700784ea435 # ╠═faecc0a7-55d7-42a1-8e9f-7e30143eef9c # ╠═4cbe69d6-b68f-4bda-a0dd-209f9ee54f18 # ╟─7dc82f28-77bf-40da-b520-800ed1bc80c9 # ╟─9f5f9de3-9558-4c18-9d98-b77d19b570ec # ╠═6dfcfa19-f78f-4dac-89f7-d3c5dbe17987 # ╟─dad1c6c0-d61b-4f9f-a71e-e683fe143aaa # ╟─3e4a3916-8fe4-4262-bcd3-3014822717a3 # ╠═34906208-e6f1-4a67-860a-a7b056a86dde # ╟─4601df35-679f-465d-9191-c18748b2fd83 # ╟─4fc72b9d-19a2-40f1-a4a8-5e97d3d5e529 # ╠═a0fde16f-8454-4f5c-a29c-a9e415c0c311 # ╟─633ff8f3-8d93-4f73-bec2-c42070e6ece9 # ╠═57f2d890-b5a0-47b7-9e3d-af4d03b10605 # ╟─34063cd0-171e-46ce-80dd-52a341fa50a1 # ╠═5d5d01db-8ff9-434c-8771-1fec6393e1fb # ╠═10d85a50-f2f9-403e-8f6c-baef61cf702a # ╟─b52648bf-a28a-48af-8912-31729d943ce0 # ╟─f45db10f-a836-40f3-9d8d-054ea6540e87 # ╟─1903563e-ccc2-44d9-9dbe-e5dede275b3c # ╟─583f2585-15a3-47c6-a70e-e2f002754028 # ╠═60e6ff80-3593-4ae4-a273-914847f692db # ╠═9e5cfd68-b58d-4d83-aae2-447e5f805c97 # ╠═dc85a942-cf52-4405-ad03-32a768e1b6e7 # ╟─5cdd346b-10a5-485c-ba78-4c0b3cb0e02f # ╠═af9287b7-6131-46f6-beb8-6885e55e1975 # ╠═e20eeabf-1c80-431e-8cfc-4d1b79c52b5a # ╟─90d30eea-53de-48a0-9700-ff35681fdf38 # ╠═390f58a5-6f5f-4d3a-bb16-ba04e43a07e7 # ╠═2b57443e-a516-434b-be86-80616a98e2f5 # ╟─fc2e27f9-b7ba-44cc-a953-6745548ad733 # ╠═fc744931-360b-4478-9f77-c50f048de243 # ╠═9a152b36-f377-44da-9700-ca9e05e365ff # ╟─b6dcd18c-606f-4340-b2ec-163e8bad03f5 # ╟─a1a29f34-f8a9-4e9f-9afe-7d0096771440 # ╟─90bd6ad4-3dd8-4e7c-b445-aed1d248a2ec # ╠═c0259e48-1973-486c-a828-1fcd3e4331c6 # ╟─8bbffa31-04a6-49ca-b36f-4d4140d75992 # ╠═a6f18c34-80ee-4b52-9ff8-f3c1b1d80f90 # ╠═a694132b-4f52-467f-8bc4-dc32fe2812db # ╟─8c2c82f2-1240-4f2f-830e-ee8021c1a41a # ╟─6203cf10-f8cc-4fb9-b814-7552b68c01dc # ╠═f97a6bab-b9f9-4b95-98a9-381c51397526 # ╠═4ca48a2e-43da-457a-8e9f-6476097e4d7b # ╟─2a2970f4-ab01-486b-89a2-6ff96f734018 # ╠═0b80d9be-53d7-4bf3-a558-659607af4709 # ╠═92362fda-bae2-4e35-bfe4-dcaea853d50b # ╟─db9e7940-39f1-4ccf-ac70-146a521daa6e # ╟─93936612-1447-4114-b864-aba43adef4bd # ╠═b2add70c-c5d6-4e0f-a153-43e21a197181 # ╠═5c03d5a5-99f0-4efd-9a32-ce6d7c2b266c # ╟─2349e3ea-3053-42a4-b9d9-f97a76e4abd7 # ╟─269f18ee-3cd8-466a-a522-7c624503e31b # ╠═89139719-c478-4066-9452-f9893f36d561 # ╠═d620c5ee-7d9c-4d3f-9e87-0c828dfab9ca # ╟─255d01b9-a873-4e63-9298-9d8f073348b0 # ╟─f6cf1729-766c-4ed7-b004-c8c8ec6c7e07 # ╠═3645d672-423f-4ac8-805f-0452793fee5a # ╠═c2a0024e-11dd-4ef7-8346-4374d98cafc0 # ╟─b20004e9-3c73-4dfb-8fd5-f377786fd53b # ╠═5c1952b1-5016-4c87-b23c-8e6a235bf8cd # ╠═8e4470ee-01da-4547-b091-c4f65cd729b0 # ╟─0863bd06-cc70-4dde-b3b2-0a466805a356 # ╟─a0bae195-04e1-4642-9e14-fe4691e0906b # ╠═20d6e8a0-2cf5-48ad-9549-60506b42b970 # ╠═7dee5748-ed73-4e13-aa80-7a50efbc8449 # ╟─4c719e9b-641e-404e-9ab7-59e89135f3ba # ╠═77947e00-42c3-4c9e-b62a-b4b29489db43 # ╠═2614127d-34fb-4c3d-b678-42693f3c9341 # ╟─8f169235-3bd1-4cc4-a083-79736d306ad5 # ╠═dfc9d305-5bce-4555-bfa3-d8d61fe4ca09 # ╟─f6bfa015-c101-45e8-995c-2bb6a3b7dc7d # ╟─8651d7ec-6bcd-4dbe-a062-c4bde32e5e91 # ╟─edaa9fdb-3af8-4554-a701-0e3bff2107a5 # ╠═7551a880-340e-4e3f-815b-188e73f7eb9a # ╠═0489e51b-781f-4441-bb7f-ff3bd2e848ad # ╟─7b0d30d6-39ff-4f6e-b13c-0ddbfcb576e5 # ╠═042297d8-6ab3-4ae6-b6e7-3b1ab2d5553b # ╠═57d65a36-bfa8-4dc2-8e11-d87fa1324122 # ╟─c21d81c3-981f-4472-ad61-d1661bfe5c4e # ╟─99d6fe7b-d704-48f3-b115-2b3159a78068 # ╠═1950ff70-54eb-4ece-a26d-a23fd0e90f5a # ╟─21458f81-9007-46f8-92e0-7a17c60beb36 # ╠═7813f4ce-6e98-45f3-94a8-7f5981129f2b # ╟─59ec7cb7-6011-456d-9f57-a55bb8ea51a0 # ╟─aacf63a2-9708-40db-8928-049621a7bbc4 # ╟─ad0097e7-c8ad-457a-82a9-18b998a9e9fb # ╟─94cd1345-3132-4882-86fe-d2429f610d1d # ╠═4a558bd3-6e42-4c61-bd23-888b7f33ae25 # ╠═004f727a-e0c8-49cb-8858-dfdf4d3ac57a # ╟─4c03cde9-b643-40ff-b275-f1795f88949e # ╟─fae7c74e-d25e-4c1e-ac97-199e6dae3365 # ╟─2f2b24ea-66d0-4b3b-a460-53b6b3f28ef0 # ╠═e9dbb64a-27b9-443a-b917-69d55c290235 # ╟─0d56ce96-81a5-4102-acbc-7d88f80adcb3 # ╟─95c41bd1-e50a-42e8-93c3-3b754a458c13 # ╟─b8629aeb-6c9a-44ed-87a1-9ab22d9485ed # ╟─3b211406-041f-4b41-acae-3958e4a37224 # ╠═62522772-cb59-4d13-acdd-d5067b223910 # ╠═2ba68a0f-6e36-4ea2-a91d-6af43741bad1 # ╠═75cebaf1-38de-475f-892e-346fd2b46f6f # ╟─72dcf2fe-eb48-4dee-8121-efafc87637e3 # ╟─84321198-93d4-4d22-8c0f-a5a10b884e1f # ╠═32244789-afbf-4215-97cd-15483f438eee # ╟─4b7f0baf-0316-4da7-9ded-50c064ddbaa3 # ╠═0e02952c-7589-4606-b006-16a9f3e52ae1 # ╟─f0904d3f-1bf1-459c-9959-b53c0f774e3f # ╟─19bb2af5-2a67-453d-82b0-7d3059b1fa47 # ╠═5b5858bf-63ac-4e31-a516-055a9cd18ffe # ╠═95060588-f24b-4eeb-9b0b-ed7159962a3c # ╟─c4cd9f88-9cd6-4364-b016-78f90aba6a66 # ╟─f6049c78-7468-47ce-a4a5-84fab34d115a # ╟─b0f73825-bbb1-448c-b491-bf634fdd398a # ╟─648cdcd6-f4f5-461f-a525-4b350cae9eb0 # ╠═8c2c4fa6-172f-4dde-a279-5d0aecfdbe46 # ╟─d6b1abd6-749d-4591-99e8-64aaa9199ab5 # ╠═f502b8c1-9b80-4e67-80e8-a64ddb88fb0f # ╟─0bce342e-9a8e-4005-8b88-82da2d2c7163 # ╠═fd9cf757-2698-4886-9f0a-c6c23ff0d331 # ╠═dda6652a-d063-4511-8041-e869bb88ca26 # ╟─a7d47e83-7f44-49d0-a43d-e01316fc6eba # ╟─eca3efef-f35b-4623-8af8-0b830a55566d # ╟─45985244-adbf-4d6d-9732-a963cca62212 # ╟─83d7e75f-7273-4c6a-bec1-a2180ebc3fb9 # ╟─7fb05c65-f47c-430a-b588-c2f9bade40a9 # ╟─14c0caa1-51ea-448c-a7dc-d06e34dd0895 # ╠═457d07bb-e999-413e-8f29-58714670296f # ╠═3199a048-7b39-40f8-8183-6a54cccd91b6 # ╠═ac53eac0-1a59-4407-8bf6-3d8b966a9bff # ╠═85c8ac7b-54f5-47dc-bd50-e78ffd6cf1cf # ╠═95c55847-0591-4f7f-b9a1-aa974ccfef69 # ╟─91f8cfc6-e261-4945-8506-eed8caa607c2 # ╠═0e1ba158-a6bc-401c-9ba7-ed78020ad068 # ╠═c82b3b5c-c4e2-4bf6-b4ec-0d05ba9a669b # ╟─32d75270-60d7-4326-a4ff-8674d0fbd491 # ╠═4b8834c1-8bb3-49f2-ae9e-1dbb8832d7f0 # ╟─d8f0ae56-e643-48a1-86ee-1cd907ecb662 # ╟─e72b9dc2-dfac-4631-b114-01ec14297427 # ╟─8c93a773-edc0-4ec2-88ef-1b58b7deddc5 # ╟─16d08950-0575-4a4b-afc8-11ddca3198c7 # ╠═7c594d19-59fc-433a-bffa-c63bad46869e # ╟─345b344d-afda-4ce1-a0e7-ce6063a69206 # ╠═c55eb045-daca-42f9-a357-095edef24644 # ╟─c6903b65-a8b8-4aef-8c43-c822077b9d0e # ╟─f80353d6-0dfe-4b0a-a1af-655d344473bf # ╟─4ca276fb-859d-4c5a-81c3-8e4b28922fa4 # ╟─3d020209-b8dd-4605-9329-78a985f0a6a3 # ╟─c7ec3496-79ea-4956-976c-b88dd22207c7 # ╟─8530a9d1-5a27-4a1d-883f-4b033a6f8fe4 # ╟─7ce31932-0447-4445-99aa-7ebced7d0bad