### A Pluto.jl notebook ### # v0.15.1 using Markdown using InteractiveUtils # ╔═╡ f64db95e-f314-11ea-137d-0138a6221cd3 begin # This sets up a clean package environment import Pkg Pkg.activate(mktempdir()) end # ╔═╡ 970bf2d6-f315-11ea-3e8d-b9c8f86edc97 begin # This adds the Plots package to our environment, and imports it using Plots plotly() end # ╔═╡ adfdeca6-f318-11ea-03bc-3f23d976cc2f using Random # ╔═╡ 1225b3c0-f315-11ea-2e4c-f396a9c06ddd md"# Notebook 2 -- Math 2121, Fall 2021 Today we will see some plotting functions in Julia, as we explore the meaning of (reduced) echelon form and the row reduction algorithm." # ╔═╡ 36794426-f315-11ea-30b4-45aa90d6954a md"## Running *this* notebook (optional) If you have Pluto up and running, you can access the notebook we are currently viewing by entering [this link](http://www.math.ust.hk/~emarberg/teaching/2021/Math2121/julia/02_Math2121_Fall2021.jl) (right click -> Copy Link) in the *Open from file* menu in Pluto. " # ╔═╡ a017566e-f315-11ea-1c33-c11f3d45f4de md"## Plotting in Pluto There are some good packages for making graphics in Julia. Today we'll use _Plots_. " # ╔═╡ f0c85042-f315-11ea-0ace-c98aa35ad856 md"## Echelon form and random matrices Let's write some functions to determine whether a matrix is in echelon form. " # ╔═╡ 67370032-f318-11ea-0bef-a9c6ddd2e4ea begin # Returns true if row has all zeros. function is_zero_row(A, row, tolerance=10e-16) (m, n) = size(A) return all([abs(A[row,col]) < tolerance for col=1:n]) end # Returns first index of nonzero entry in row, or 0 if none exists. function find_leading(A, row=1, tolerance=10e-16) (m, n) = size(A) for col=1:n if abs(A[row, col]) > tolerance return col end end return 0 end end # ╔═╡ 7ad07024-f318-11ea-3d77-89de37d7a65a find_leading([0 0 4 0 1]) # ╔═╡ 5aee6236-f2bc-11ea-07c6-d1220cb67164 function in_echelon_form(A, tolerance=10e-12) (m, n) = size(A) # check locations of zero rows seen_zero_row = false for row=1:m if is_zero_row(A, row, tolerance) seen_zero_row = true elseif seen_zero_row return false end end # check alignment of leading entries in nonzero rows for row=2:m if is_zero_row(A, row, tolerance) continue end if find_leading(A, row, tolerance) <= find_leading(A, row - 1, tolerance) return false end end return true end # ╔═╡ 7bcd0b68-f2be-11ea-30df-03b21020b956 function in_echelon_form_verbose(A, tolerance=10e-12) (m, n) = size(A) seen_zero_row = false for row=1:m if is_zero_row(A, row, tolerance) seen_zero_row = true elseif seen_zero_row return Text(string("false: row ", row, " is a nonzero row below a zero row")) end end for row=2:m if is_zero_row(A, row, tolerance) continue end if find_leading(A, row, tolerance) <= find_leading(A, row - 1, tolerance) return Text(string("false: leading entry in row ", row, " is not to the right of the leading entry in row ", row - 1)) end end return Text("true: matrix is in echelon form") end # ╔═╡ e6a37880-f2e8-11ea-2139-bbbd365b71f8 function in_reduced_echelon_form(A, tolerance=10e-12) (m, n) = size(A) # check if in echelon form if ! in_echelon_form(A, tolerance) return false end for row=1:m if ! is_zero_row(A, row, tolerance) col = find_leading(A, row, tolerance) # check that leading entry is 1 if abs(A[row,col] - 1) > tolerance return false end # check that leading entry is only nonzero entry in column if any([i != row && abs(A[i,col]) > tolerance for i=1:m]) return false end end end return true end # ╔═╡ 74d0f590-f2e9-11ea-382f-ff337fe4d689 function in_reduced_echelon_form_verbose(A, tolerance=10e-12) (m, n) = size(A) if ! in_echelon_form(A, tolerance) return Text("false: not in echelon form") end for row=1:m if ! is_zero_row(A, row, tolerance) col = find_leading(A, row, tolerance) if abs(A[row,col] - 1) > tolerance return Text(string("false: leading entry in row ", row, " is not 1")) end if any([i != row && abs(A[i,col]) > tolerance for i=1:m]) return Text(string("false: leading entry in row ", row, " is not the only nonzero entry in its column")) end end end return Text("true: matrix is in reduced echelon form") end # ╔═╡ 3414f2f0-f317-11ea-177f-ed0bfcf38783 A = [ 0 0 1 1 0 3 0 5 6; 0 0 0 0 1 9 0 2 0; 0 0 0 0 0 0 1 8 9; 0 0 0 0 0 0 0 0 0] # ╔═╡ 4284b2e4-f317-11ea-3d15-67e92439cb26 in_echelon_form(A) # ╔═╡ 479e2850-f317-11ea-0460-d9c43b64d843 in_echelon_form_verbose(A) # ╔═╡ 889dcd64-f318-11ea-3fa3-112c7dc644a5 in_reduced_echelon_form(A) # ╔═╡ 8dd79e36-f318-11ea-2a21-517e6488af19 in_reduced_echelon_form_verbose(A) # ╔═╡ 860e1c76-f317-11ea-2505-4f8f4321672b md"It's easy to make random matrices using the _Random_ package." # ╔═╡ 9957476c-f317-11ea-1cad-471f187afc18 B = rand(Float64, 3, 2) # ╔═╡ cadc1510-f317-11ea-399c-f51fc7f6851e in_echelon_form_verbose(B) # ╔═╡ 62e50be2-f2bd-11ea-3e86-79f7bc779674 in_reduced_echelon_form_verbose(B) # ╔═╡ cee12c56-f318-11ea-107f-db98e6090933 md"If we generate a random matrix in this way, where each entry is chosen at random from a specified list or range, then what we get is almost never in echelon form. This is because too many entries are nonzero. For a random matrix to have good chances of being in echelon form, we must make it more likely that a given entry is zero. Whether a matrix is in echelon form does not depend on the values of its nonzero entries, only their positions (note that this is not true for *reduced* echelon form). So let's write a function that generates a random matrix with all entries either $0$ (zero) or $1$ (nonzero). This function takes a parameter that controls how likely a given entry is to be zero." # ╔═╡ cd9a87f2-f2c0-11ea-236e-e7f217f8d610 function random_boolean_matrix(m, n, zero_probability) A = rand(m, n) for i=1:m for j=1:n A[i, j] = Int(A[i, j] > zero_probability) end end return A end # ╔═╡ f01dfff0-f2ed-11ea-2fa9-6d5020e471b0 C = random_boolean_matrix(3, 5, 0.9) # ╔═╡ 467e27c0-f2f1-11ea-02db-ad60b47ab9f2 in_echelon_form_verbose(C) # ╔═╡ d2201b08-f319-11ea-29e8-c13c64e5532a md""" Now let's try the following experiment. For different values of `p` between $0.0$ and $1.0$, we'll generate a bunch of random matrices of a given size, where the chance that a given entry is zero is `p`. We'll graph what proportion of the time this matrix is in echelon form, as a function of `p`. """ # ╔═╡ 78418fa4-f2c4-11ea-1a08-f5c5780ec41e begin # returns average number of times that random 01-matrix is in echelon form function acc_echelon(m, n, zero_prob, trials) s = 0 for i=1:trials s += Int(in_echelon_form(random_boolean_matrix(m, n, zero_prob))) end return s / trials end function plot_echelon_fraction(; nrows, ncols, ntrials) zero_probs = 0:0.001:1.0 likelihoods = [acc_echelon(nrows, ncols, p, ntrials) for p in zero_probs] plot( zero_probs, likelihoods, xlabel="Probablity p that individual matrix entry is zero", ylabel="Fraction of $(nrows)-by-$(ncols) matrices", label=["Echelon form" "Reduced echelon form"], title="Chance of random $(nrows)-by-$(ncols) matrix in echelon form", legend=false ) plot!(zero_probs, [x^(ncols * (nrows - 1)) for x in zero_probs]) end end # ╔═╡ b52e1141-d44a-40fa-b8b8-fb665504cf76 plot_echelon_fraction(nrows=4, ncols=4, ntrials=100) # ╔═╡ 965732ea-f31a-11ea-0fb9-d12095065a67 md"If `p == 1.0` then our random matrix is always the *zero matrix* which is in echelon form. This is why when `p` is $1.0$ the value of the graph is $1.0$." # ╔═╡ f03a2f4c-f31a-11ea-0d27-03c4ef0fe80c md"If `p == 0.0` then our random matrix is always the *matrix of all ones* which is never in echelon form if `nrows > 1`. This is why when `p` is $0.0$ the value of the graph is $0.0$." # ╔═╡ ba200c6b-51a4-4cd4-862c-628b1d6faaab md"The orange line shows the graph of the function $f(p) = p^{\mathsf{ncols}(\mathsf{nrows}-1)}$. This exactly matches the empirical chance when $\mathsf{ncols}=1$, and is a good approximation whenever $\mathsf{nrows}$ is not too small." # ╔═╡ 4a4cffa6-f31a-11ea-3393-df9106bb2663 md"## Steady-state temperature distributions Below is a function `RREF` that converts a matrix to its reduced echelon form. You can unhide the code by clicking the icon on the left, if you want to take a look. " # ╔═╡ 9b16aeec-f2c4-11ea-1d0a-c3e8c4286736 begin function find_nonzeros_in_column(A, row_start, col, tol=10e-12) (m, n) = size(A) return [i for i=row_start:m if abs(A[i, col]) > tol] end function _rowop_replace(A, source_row, target_row, scalar_factor) @assert source_row != target_row A[target_row,:] += A[source_row,:] * scalar_factor return A end function _rowop_scale(A, target_row, scalar_factor) @assert scalar_factor != 0 A[target_row,:] *= scalar_factor A end function _rowop_swap(A, s, t) A[t,:], A[s,:] = A[s,:], A[t,:] A end function RREF(A, tol=10e-12) A = float(copy(A)) (m, n) = size(A) row = 1 for col=1:n nonzeros = find_nonzeros_in_column(A, row, col, tol) if length(nonzeros) == 0 continue end i = nonzeros[1] if abs(A[i, col] - 1) < tol A[i, col] = 1 else A = _rowop_scale(A, i, 1 / A[i, col]) end for j=nonzeros[2:end] A = _rowop_replace(A, i, j, -A[j, col]) end if i != row A = _rowop_swap(A, row, i) end row += 1 end for row=m:-1:1 l = find_leading(A, row, tol) if l > 0 for k=1:row - 1 if abs(A[k,l]) > tol _rowop_replace(A, row, k, -A[k,l]) end end end end # round entries to give nicer output; could omit this step for row=1:m for col=1:n if abs(A[row,col] - round(A[row,col])) < tol A[row,col] = round(A[row,col]) end end end return A end end # ╔═╡ e47f6a68-e3f3-4de5-9c4e-a651cfeef41e matrix = [-22 4 -16; -121 20 -89; -55 8 -41] # ╔═╡ f089ef72-fca8-11ea-084c-a5b18d2535d3 RREF(matrix) # ╔═╡ 23f4c434-f31c-11ea-3f9a-a163e2bcf421 md"Here is some more code to compute and count the pivot positions in a matrix." # ╔═╡ 335899de-f31c-11ea-3e75-e7931ce5c1b4 function pivot_positions(A, tol=10e-12) rref = RREF(A, tol) return [ (i, find_leading(rref, i)) for i=1:size(A)[1] if find_leading(rref, i) > 0 ] end # ╔═╡ 12ce7d5a-f2d4-11ea-0cdf-ffc6215c7153 function npivots(A, tol=10e-12) return length(pivot_positions(A, tol)) end # ╔═╡ 132dcea5-9139-4c29-a927-7aa22dea4232 pivot_positions(matrix) # ╔═╡ 76e31fac-9442-4ae0-b69a-dceb85f60a28 npivots(matrix) # ╔═╡ 92c9b5b2-801e-400f-a5b9-b40a32a04f86 md"If `A` is the augmented matrix of a consistent linear system with exactly one solution, then the last column of `RREF(A)` is this unique solution. We can use this fact to compute the steady-state temperature distribution in a region with known boundary temperatures." # ╔═╡ 6b83d0ff-db4a-4051-bfda-4dec475f2af7 function display_grid(width, height) adim = width + 1 bdim = height + 1 _x = [] _y = [] for i=0:adim for j=0:bdim if (i != 0 && i != adim) || (j != 0 && j != bdim) append!(_x, i) append!(_y, j) end end end scatter(_x, _y, legend=false, aspect_ratio=:equal, border=:none) end # ╔═╡ 75235b14-bb8f-40b9-84ff-f4385c3622bc md"Imagine we have a thin rectangular metal plate. Each point on the boundary of this plate is kept at fixed temperature which we can control. Can we model the temperature at all of the interior points?" # ╔═╡ 1eb7edb1-4086-4e51-ad4f-30d9c6b2e579 md"To do this, we divide the interior of the plate into a grid of $m$-by-$n$ points. These points, including the adjacent boundary points are shown below for $m=n=20$:" # ╔═╡ 980f6545-7d0a-4f40-9a5c-9cde3a195934 display_grid(20, 20) # ╔═╡ b1c5d01f-cd69-4407-8cc2-15e810cf7534 md"The relevant physics: the temperature at each interior point should be approximately equal to the **average of the temperatures at the four adjacent points**. If we associate a variable $x_i$ to each interior point then this property determines a linear system with $mn$ equations and variables. We can try to solve this system." # ╔═╡ 69bc6229-1a6d-40be-804f-261729874136 md"Let's walk through two examples. The simplest possible case is when there is just one interior point:" # ╔═╡ 3bff7c17-4815-4c87-9b97-7049dd3396e1 display_grid(1, 1) # ╔═╡ 109416c7-e94f-4bd1-a8e5-a9fe57874178 md"Assign the variable $x_1$ to the interior point. Let $a_1,a_2,a_3,a_4$ be the fixed temperatures at the boundary: $$\begin{array}{cccc} & a_1 \\ a_2 & x_{1}& a_3 \\ & a_{4} \end{array}$$ The corresponding linear system has just one equation which is trivial to solve: $$ x_{1} = (a_1 + a_{2} +a_{3} + a_4)/4.$$" # ╔═╡ d4b8a83f-270d-4776-9a27-d43cb2341c06 md"A more interesting case is to take $m=n=2$:" # ╔═╡ 84914942-5929-4325-a8e3-cf125c32b9da display_grid(2, 2) # ╔═╡ c54558e7-1a97-4d08-9046-52400db92c5e md"Assign variables $x_1,\dots,x_4$ to the interior points and label the boundary temperatures $a_1,\dots,a_8$: $$\begin{array}{cccc} & a_1 & a_2 \\ a_5 & x_{1} & x_{2} & a_7 \\ a_6 & x_{3} & x_{4} & a_8 \\ & a_3 & a_4 \end{array}$$ The corresponding linear system is $$\begin{cases} x_{1} = (a_1 + x_{2} + x_{3} + a_5)/4 \\ x_{2} = (a_2 + a_7 + x_{4} + x_{1})/4 \\ x_{3} = (x_{1} + x_{4} + a_3 + a_6)/4 \\ x_{4} = (x_{2} + a_8 + a_4 + x_{3})/4. \end{cases}$$ If we multiply all the equations by $4$ we can rewrite this linear system as $$\begin{cases} 4x_{1} -x_{2} - x_{3}= a_1 + a_5 \\ - x_{1}+4x_{2} -x_{4}= a_2 + a_7 \\ -x_{1}+4x_{3} - x_{4} = a_3 + a_6 \\ -x_{2}- x_{3}+ 4x_{4} = a_4 + a_8. \end{cases}$$ The function below automates the process of constructing the augmented matrix of this linear system. " # ╔═╡ 99496a64-d181-4c17-a75c-9412f5fbad91 function temperature_system_augmented_matrix(top, bottom, left, right) n = length(top) m = length(left) @assert length(top) == length(bottom) @assert length(left) == length(right) ans = zeros(m * n, m * n + 1) for i=1:m for j=1:n a = (i - 1) * n + j ans[a, a] = 4 if i == 1 ans[a, end] += top[j] else b = (i - 2) * n + j ans[a, b] = -1 end if i == m ans[a, end] += bottom[j] else b = i * n + j ans[a, b] = -1 end if j == 1 ans[a, end] += left[i] else b = (i - 1) * n + j - 1 ans[a, b] = -1 end if j == n ans[a, end] += right[i] else b = (i - 1) * n + j + 1 ans[a, b] = -1 end end end ans end # ╔═╡ 3b707df2-302b-417b-9c0a-9e04ebbf139f # the inputs [1, 2], [3, 4], [5, 6], and [7, 8] are the # top, bottom, left, and right boundary temperatures # # these choices are equivalent to setting each a_i = i T = temperature_system_augmented_matrix([1, 2], [3, 4], [5, 6], [7, 8]) # ╔═╡ 0454c654-6a94-41cd-bbbd-85b3635318bf md"The linear system with this augmented matrix has a unique solution:" # ╔═╡ 33b5dfac-c5c9-4713-9429-5d2202d5293f npivots(T) == size(T, 1) # ╔═╡ 87cfa440-30ef-4976-81f8-19f87f2f40cb RREF(T) # ╔═╡ 70bb6cb5-705c-487d-b4e0-e19445159b1b md"The following method, provided inputs for the temperatures on the four boundaries of the grid, solves the linear system governing the interior temperatures and then reshapes this into a matrix. The entry in position $(i, j)$ in this matrix corresponds to the temperature at interior point $(i, j)$." # ╔═╡ 37ffa122-c0f8-4465-9c9c-67a85eac3b12 function solve_temperature_system(top, bottom, left, right) # the inputs are the fixed boundary temperatures # compute augmented matrix A of linear system A = temperature_system_augmented_matrix(top, bottom, left, right) # solve system by taking last column RREF(A) solution = RREF(A)[:, end] # reshape solution into a matrix m = length(top) reshaped_solution = transpose(reshape(solution, (m, :))) return reshaped_solution end # ╔═╡ f6dc2a94-719d-4c32-8b39-d3cdf0350716 solve_temperature_system([1, 2], [3, 4], [5, 6], [7, 8]) # ╔═╡ 4f912368-a3ff-4c3b-b088-f118fe4dbedc md"Some method to produce nice plots of all of these outputs:" # ╔═╡ 66bfd27f-4b50-4673-ba5f-a4f70f7cd34a function plot_temperature_system_boundary(top, bottom, left, right) p = -1 + minimum([0 minimum(top) minimum(bottom) minimum(left) minimum(right)]) q = 1 + maximum([maximum(top) maximum(bottom) maximum(left) maximum(right)]) top_plot = bar(top, title="top boundary", xlim=(0, length(top) + 1), ylim=(p, q), st=:sticks, m=3,c=[1]) bottom_plot = bar(bottom, title="bottom boundary", xlim=(0, length(top) + 1), ylim=(p, q), st=:sticks, m=3,c=[1]) left_plot = bar(left, title="left boundary", xlim=(0, length(left) + 1), ylim=(p, q), st=:sticks, m=3,c=[1]) right_plot = bar(right, title="right boundary", xlim=(0, length(left) + 1), ylim=(p, q), st=:sticks, m=3,c=[1]) plot(top_plot, left_plot, right_plot, bottom_plot, layout=@layout([_ ° _; ° _ °; _ ° _]), legend=false) end # ╔═╡ a87af94c-7dda-4522-bed4-f27a45799806 function plot_solution_to_temperature_system(top, bottom, left, right) sol = solve_temperature_system(top, bottom, left, right) surface(1:length(top), 1:length(left), sol, colorbar=false, title="Steady-state temperature distribution") end # ╔═╡ 55415f88-eb2e-4f34-aab7-02f5eb11ba7a plot_temperature_system_boundary([1, 2], [3, 4], [5, 6], [7, 8]) # ╔═╡ 1a69b26e-0160-4546-885b-8ce14e7605d3 plot_solution_to_temperature_system([1, 2], [3, 4], [5, 6], [7, 8]) # ╔═╡ 9584d4bc-63f9-425b-9c9a-c2ec8e8ffd5e begin rng = range(0, length=25, stop=1) top = [sin(x * pi) for x=rng] bottom = [x^2 for x=rng] left = [cos(x * pi) for y=1:2 for x=rng] right = rand(length(rng)*2) md"We can try some more complicated examples." end # ╔═╡ d1f0aed0-5cf0-4deb-864a-1404633b9885 plot_temperature_system_boundary(top, bottom, left, right) # ╔═╡ 0f93af26-001b-4a77-a4e2-db13702669cd plot_solution_to_temperature_system(top, bottom, left, right) # ╔═╡ f7111c78-ee90-48ed-a7b7-cdb49c0c3190 begin top2 = [x == 15 ? 10 : 0 for x=0:30] bottom2 = [0 for x=0:30] left2 = [0 for x=0:30] right2 = [0 for x=0:30] md"If there is only a single boundary point with nonzero temperature, the interior points will not be evenly heated." end # ╔═╡ 0e51194b-43e5-412e-b23a-54a002ce596d plot_temperature_system_boundary(top2, bottom2, left2, right2) # ╔═╡ d9f2308e-8574-4dcc-8e2d-66420590cdbb plot_solution_to_temperature_system(top2, bottom2, left2, right2) # ╔═╡ 1d4dcb56-8819-4c8c-bcee-b03d5a8a8650 begin topr = 1 .+ rand(30) bottomr = 1 .+ rand(30) leftr = 1 .+ rand(30) rightr = 1 .+ rand(30) md"But if the boundary temperatures are chosen uniformly at random with average $\mu$, there is a good chance that the center of the plate will have a relatively constant temperature close to $\mu$." end # ╔═╡ 6c1d00aa-78fa-47f6-af6f-69b9539a4c3c plot_temperature_system_boundary(topr, bottomr, leftr, rightr) # ╔═╡ fef7ee80-f7eb-4fb6-a09c-0d875a2f8ed3 plot_solution_to_temperature_system(topr, bottomr, leftr, rightr) # ╔═╡ 5d0f85a5-145f-4ef4-8043-6dd221ef4877 begin top3 = [sin(x * pi / 30) for x=0:30] bottom3 = [sin(3 * x * pi / 30) for x=0:30] left3 = [0 for x=0:30] right3 = [0 for x=0:30] md"If the left and right boundaries are zero then the interior temperatures will form a surface smoothly interpolating between the top and bottom boundaries." end # ╔═╡ 50a622b0-a9cd-432d-97ee-ab71fc7d98a5 plot_temperature_system_boundary(top3, bottom3, left3, right3) # ╔═╡ e5bdd2c3-c441-4022-936a-dd7461c5c9ac plot_solution_to_temperature_system(top3, bottom3, left3, right3) # ╔═╡ Cell order: # ╟─1225b3c0-f315-11ea-2e4c-f396a9c06ddd # ╟─36794426-f315-11ea-30b4-45aa90d6954a # ╟─a017566e-f315-11ea-1c33-c11f3d45f4de # ╠═f64db95e-f314-11ea-137d-0138a6221cd3 # ╠═970bf2d6-f315-11ea-3e8d-b9c8f86edc97 # ╟─f0c85042-f315-11ea-0ace-c98aa35ad856 # ╠═67370032-f318-11ea-0bef-a9c6ddd2e4ea # ╠═7ad07024-f318-11ea-3d77-89de37d7a65a # ╠═5aee6236-f2bc-11ea-07c6-d1220cb67164 # ╟─7bcd0b68-f2be-11ea-30df-03b21020b956 # ╠═e6a37880-f2e8-11ea-2139-bbbd365b71f8 # ╟─74d0f590-f2e9-11ea-382f-ff337fe4d689 # ╠═3414f2f0-f317-11ea-177f-ed0bfcf38783 # ╠═4284b2e4-f317-11ea-3d15-67e92439cb26 # ╠═479e2850-f317-11ea-0460-d9c43b64d843 # ╠═889dcd64-f318-11ea-3fa3-112c7dc644a5 # ╠═8dd79e36-f318-11ea-2a21-517e6488af19 # ╟─860e1c76-f317-11ea-2505-4f8f4321672b # ╠═adfdeca6-f318-11ea-03bc-3f23d976cc2f # ╠═9957476c-f317-11ea-1cad-471f187afc18 # ╠═cadc1510-f317-11ea-399c-f51fc7f6851e # ╠═62e50be2-f2bd-11ea-3e86-79f7bc779674 # ╟─cee12c56-f318-11ea-107f-db98e6090933 # ╠═cd9a87f2-f2c0-11ea-236e-e7f217f8d610 # ╠═f01dfff0-f2ed-11ea-2fa9-6d5020e471b0 # ╠═467e27c0-f2f1-11ea-02db-ad60b47ab9f2 # ╟─d2201b08-f319-11ea-29e8-c13c64e5532a # ╟─78418fa4-f2c4-11ea-1a08-f5c5780ec41e # ╠═b52e1141-d44a-40fa-b8b8-fb665504cf76 # ╟─965732ea-f31a-11ea-0fb9-d12095065a67 # ╟─f03a2f4c-f31a-11ea-0d27-03c4ef0fe80c # ╟─ba200c6b-51a4-4cd4-862c-628b1d6faaab # ╟─4a4cffa6-f31a-11ea-3393-df9106bb2663 # ╟─9b16aeec-f2c4-11ea-1d0a-c3e8c4286736 # ╠═e47f6a68-e3f3-4de5-9c4e-a651cfeef41e # ╠═f089ef72-fca8-11ea-084c-a5b18d2535d3 # ╟─23f4c434-f31c-11ea-3f9a-a163e2bcf421 # ╠═335899de-f31c-11ea-3e75-e7931ce5c1b4 # ╠═12ce7d5a-f2d4-11ea-0cdf-ffc6215c7153 # ╠═132dcea5-9139-4c29-a927-7aa22dea4232 # ╠═76e31fac-9442-4ae0-b69a-dceb85f60a28 # ╟─92c9b5b2-801e-400f-a5b9-b40a32a04f86 # ╟─6b83d0ff-db4a-4051-bfda-4dec475f2af7 # ╟─75235b14-bb8f-40b9-84ff-f4385c3622bc # ╟─1eb7edb1-4086-4e51-ad4f-30d9c6b2e579 # ╠═980f6545-7d0a-4f40-9a5c-9cde3a195934 # ╟─b1c5d01f-cd69-4407-8cc2-15e810cf7534 # ╟─69bc6229-1a6d-40be-804f-261729874136 # ╠═3bff7c17-4815-4c87-9b97-7049dd3396e1 # ╟─109416c7-e94f-4bd1-a8e5-a9fe57874178 # ╟─d4b8a83f-270d-4776-9a27-d43cb2341c06 # ╟─84914942-5929-4325-a8e3-cf125c32b9da # ╟─c54558e7-1a97-4d08-9046-52400db92c5e # ╟─99496a64-d181-4c17-a75c-9412f5fbad91 # ╠═3b707df2-302b-417b-9c0a-9e04ebbf139f # ╟─0454c654-6a94-41cd-bbbd-85b3635318bf # ╠═33b5dfac-c5c9-4713-9429-5d2202d5293f # ╠═87cfa440-30ef-4976-81f8-19f87f2f40cb # ╟─70bb6cb5-705c-487d-b4e0-e19445159b1b # ╟─37ffa122-c0f8-4465-9c9c-67a85eac3b12 # ╠═f6dc2a94-719d-4c32-8b39-d3cdf0350716 # ╟─4f912368-a3ff-4c3b-b088-f118fe4dbedc # ╟─66bfd27f-4b50-4673-ba5f-a4f70f7cd34a # ╟─a87af94c-7dda-4522-bed4-f27a45799806 # ╠═55415f88-eb2e-4f34-aab7-02f5eb11ba7a # ╠═1a69b26e-0160-4546-885b-8ce14e7605d3 # ╠═9584d4bc-63f9-425b-9c9a-c2ec8e8ffd5e # ╠═d1f0aed0-5cf0-4deb-864a-1404633b9885 # ╠═0f93af26-001b-4a77-a4e2-db13702669cd # ╠═f7111c78-ee90-48ed-a7b7-cdb49c0c3190 # ╠═0e51194b-43e5-412e-b23a-54a002ce596d # ╠═d9f2308e-8574-4dcc-8e2d-66420590cdbb # ╠═1d4dcb56-8819-4c8c-bcee-b03d5a8a8650 # ╟─6c1d00aa-78fa-47f6-af6f-69b9539a4c3c # ╠═fef7ee80-f7eb-4fb6-a09c-0d875a2f8ed3 # ╠═5d0f85a5-145f-4ef4-8043-6dd221ef4877 # ╠═50a622b0-a9cd-432d-97ee-ab71fc7d98a5 # ╠═e5bdd2c3-c441-4022-936a-dd7461c5c9ac