PSBoardDataBase/examples/skew_stats.jl
2025-04-15 09:05:14 +00:00

1522 lines
45 KiB
Julia
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

### A Pluto.jl notebook ###
# v0.20.4
using Markdown
using InteractiveUtils
# ╔═╡ 7c69d12c-80a5-11ef-2674-e155a3483342
begin
using Pkg
Pkg.activate(".")
# Pkg.status()
true || include("../src/PSBoardDataBase.jl")
end
# ╔═╡ effa7ed9-2ac4-4468-a474-e2bb662580fe
begin
using PSBoardDataBase
using SQLite
using DataFrames
using FHist
using DBInterface
using Tables
using CairoMakie
using ColorSchemes
using Statistics
using PlutoUI
using Dates
using Random
using Printf
end
# ╔═╡ f25e7e08-8a73-4cac-ac7c-d310725c558d
md"""
# Detailed research on clock skew measurement
- distribution of clock skews
- consistency of multiple measurement on the same board
"""
# ╔═╡ 1a6322d4-9deb-4709-aa2e-df7d8be5b16f
TableOfContents(depth = 4)
# ╔═╡ 11537f91-e16b-45f0-9768-6df842371d36
db = SQLite.DB("../psboard_qaqc.db")
# ╔═╡ 268d3015-b8d3-48d9-b74a-062e258e0ec1
SQLite.tables(db)
# ╔═╡ 62105832-df1f-4834-8da6-c542e22207d1
md"""
## Single runs
"""
# ╔═╡ dea6f143-7916-4765-92f6-2bfb97a72835
qaqc_single_results =
DBInterface.execute(db, sql"select * from qaqc_single_run_results") |> DataFrame
# ╔═╡ 41543c0c-d7c4-447b-a268-0d356c88d92c
md"""
## PS Board list
"""
# ╔═╡ 633ecdee-7e2f-4de8-833a-21cd0351c1f1
DBInterface.execute(
db,
sql"""
select * from ps_boards
""",
) |> DataFrame
# ╔═╡ 87f1966e-3b07-4f9d-8fc4-7b7aa4319d50
md"""
## Run list
"""
# ╔═╡ f379d43c-9300-41f4-b0fc-3c9d749e3105
qaqc_runs = let df
df = DBInterface.execute(db, sql"select * from qaqc_runs") |> DataFrame
transform!(df, :run_datetime => ByRow(passmissing(s -> DateTime(s))) => :run_datetime)
df
end
# ╔═╡ 33e099bc-ac4b-4b5f-88e7-20f4463c98ef
md"""
## Positions
"""
# ╔═╡ 0e13f848-0efb-4775-9e3e-518b32588a79
qaqc_positions = DBInterface.execute(db, sql"select * from qaqc_positions") |> DataFrame
# ╔═╡ 181c3fe6-d087-42e2-b175-3fb84c42e3e8
position_id_skew_map = select(qaqc_positions, [:id, :rising_ns]) |> Tables.rowtable |> Dict
# ╔═╡ dd669f14-989b-45ee-87d8-5d9cf282fafd
md"""
## Dispatch
"""
# ╔═╡ 1e9c3944-0cd4-40da-9014-a9153d4e95ed
qaqc_dispatch = DBInterface.execute(db, sql"select * from qaqc_dispatch") |> DataFrame
# ╔═╡ 322cb530-65a5-4973-86f8-01ccc2439cc4
md"""
# Clock Analysis
main part
"""
# ╔═╡ c1caca5f-4cfd-4f22-82b4-7925002359e6
clk_files =
readdir("../test/input/slavelogs/main/", join = true) |>
filter(endswith("_clk.txt")) |>
filter(!contains("nagoya")) |>
filter(!contains("630_190")) |>
filter(!contains("627_344"))
# ╔═╡ 3e5607fd-2a8a-4a1a-9e7b-3f23ef216fad
"""
Get `(psbid, runid)`.
"""
function parse_filename(filename::AbstractString)
m = match(r"(?<psbid>\d+)_(?<runid>\d+)_clk\.txt", filename)
parse(Int64, m[:psbid]), parse(Int64, m[:runid])
end
# ╔═╡ c1b9c0c3-00f8-4199-b07f-8888f1be625c
parse_filename("190_23_clk.txt")
# ╔═╡ d6d04013-e0e4-49d5-a450-07ae164bfaa3
# Get skew and rise up time from clock measurement files
# Use measurements recorded in qaqc_single_results
df_rawskews =
clk_files .|>
(
file -> begin
skew_width = PSBoardDataBase.ClockParser.get_skew_and_riseup(file)
riseup = PSBoardDataBase.ClockParser.count_riseup(file)
psbid, runid = parse_filename(file)
(
psbid = psbid,
runid = runid,
skew = skew_width[1],
width = skew_width[2],
riseup = riseup,
)
end
) |>
filter(
x ->
filter(
[:psboard_id, :runid] =>
((psbid, runid) -> (psbid == x.psbid && runid == x.runid)),
qaqc_single_results,
) |> !isempty,
) |>
DataFrame
# ╔═╡ 0e680044-e4e1-4f39-a5c5-afa5c53fc7a7
clk_files |> filter(contains("168")) .|> PSBoardDataBase.ClockParser.get_skew_and_riseup
# ╔═╡ d7541b93-4c49-4dcd-bda0-91e447f44596
# substract result of measurements of position dependency
df_skews = let
df = leftjoin(
df_rawskews,
@view(qaqc_single_results[!, [:psboard_id, :runid, :position]]),
on = [:psbid => :psboard_id, :runid],
)
leftjoin!(df, @view(qaqc_positions[!, [:id, :rising_ns]]), on = [:position => :id])
transform!(df, [:skew, :rising_ns] => ByRow((x, y) -> x - y) => :skew)
select!(df, Not(:rising_ns))
select!(df, Not(:position))
end
# ╔═╡ e7faa647-79cd-4247-a3f2-9868c7b9d4ca
md"""
## skewの分布
``skew = rising\_time_{psbid, position} - rising\_time_{position}``
"""
# ╔═╡ 3a412a98-4a2d-4bfb-8053-b0f480fae921
let
skews = skipmissing(df_skews.skew) |> collect
npsbid = df_skews.psbid |> unique |> length
fig = Figure()
ax = Axis(
fig[1, 1],
title = "skews of all measurements (n_meas = $(length(skews)), n_psb = $(npsbid))",
xlabel = "skew / ns",
)
stephist!(ax, skews, bins = range(minimum(skews), maximum(skews), step = 1 / 56))
fig
end
# ╔═╡ b38bbed4-8721-4e92-a546-f7926cc07dd3
md"""
## 立ち上がり時間の分布(生)
"""
# ╔═╡ 420dce0e-4757-48d9-84ec-7ddfac2fdff6
let
skew_widths = df_skews.width |> skipmissing |> collect
bins = range(0, maximum(skew_widths) + 1 / 56, step = 1 / 56)# .- 0.01
hist(
skew_widths,
bins = bins,
bar_labels = :y,
label_size = 14,
label_formatter = x -> "$(round(Int, x))",
axis = (
title = "clock rise span distribution",
xlabel = "rise up span / ns",
ylabel = "counts",
limits = ((0, 0.33), (0, nothing)),
# xticks = (bins, string.(round.(bins, digits = 3))),
# xticklabelrotation = π / 3,
),
)
end
# ╔═╡ 99902640-fee3-4502-9c7e-cb08834bad0b
maximum(skipmissing(df_skews.width)) / (1 / 56)
# ╔═╡ c79c6684-1b03-41b5-aa90-ef8c7a8eb69c
md"""
この結果を元に、クロック試験のしきい値は$(round(9 * 1 / 56; digits = 2))ns以上に設定
"""
# ╔═╡ a1056aba-e484-4594-908b-4709896d0da0
let
fig = Figure()
ax = Axis(
fig[1, 1],
limits = ((-0.5, 1), nothing),
title = "turn on curves for PSBID 1166",
xlabel = "delay / ns (position dependency is removed)",
ylabel = "counts",
)
clk_files_1166 = filter(clk_files) do filename
psbid, runid = parse_filename(filename)
psbid == 1166
end
for clk_file in clk_files_1166
psbid, runid = parse_filename(clk_file)
single_runs = filter(
[:psboard_id, :runid] => (
(ref_psbid, ref_runid) -> begin
psbid == ref_psbid && runid == ref_runid
end
),
qaqc_single_results,
)
offset_pos = let
qaqc_positions.rising_ns[single_runs.position[1]]
end
points =
eachline(clk_file) .|>
PSBoardDataBase.ClockParser._parse_line .|>
(x -> (x[1] - offset_pos, x[2]))
@info "" single_runs points
stds = map(points) do ((time, count))
sqrt(count * (1000 - count) / 1000)
end
scatterlines!(ax, points, label = "psbid $(psbid), runid $(runid)")
errorbars!(ax, points, stds)
end
axislegend(ax, position = :rb)
fig
end
# ╔═╡ 875bec26-e576-4f48-ba14-464bce503d75
filter(:width => (x -> ismissing(x) || x < 0.06), df_skews)
# ╔═╡ ec774495-c0be-47a4-9d2c-b48159c07013
md"""
## 各PSBoardごとの統計
"""
# ╔═╡ d082e07c-3b42-4362-bebf-63356979a49b
gdf_skews_on_psbid = groupby(df_skews, :psbid)
# ╔═╡ 2d41c98b-630e-41e7-9332-25394a6285e8
md"""
### 同じPSBoardで同じ場所で複数回測定し、測定のばらつきが大きかったもの
すべて異常個体リストで把握済み
"""
# ╔═╡ 6ff9f397-5db9-45af-9bca-7a8d7756f8da
combine(filter(groupby(qaqc_single_results, [:psboard_id, :position])) do sdf
nrow(dropmissing(sdf, [:lvds_tx_skew])) > 1
end) do sdf
sdf = dropmissing(sdf, [:lvds_tx_skew])
# @info "" select(sdf, [:psboard_id, :position, :lvds_tx_skew])
(
skew_mean = mean(sdf.lvds_tx_skew),
skew_std = std(sdf.lvds_tx_skew),
nrow = nrow(sdf),
)
end |> (df -> begin
filter(:skew_std => >(0.5), df)
end)
# ╔═╡ 25688d24-5aee-43d3-aff9-b9efa0556070
combine(nrow, gdf_skews_on_psbid)
# ╔═╡ 0d3f7f0a-1713-4e22-acbb-7d370fabf5a7
combine(gdf_skews_on_psbid, :skew => (v -> all(ismissing, v)) => :hasmissing) |>
(df -> sort(df, :hasmissing, rev = true))
# ╔═╡ 239a808c-0411-4542-ae68-6ae6af333bd2
df_nrow_ordered = let
df = combine(nrow, gdf_skews_on_psbid)
sort!(df, :nrow, rev = true)
end
# ╔═╡ 8e57bde1-5f97-483d-906e-8ebfb65016d0
@view(df_nrow_ordered[findall(>(1), df_nrow_ordered.nrow), :])
# ╔═╡ 92c2ac3f-8034-4e9e-aadb-8bb166fbc948
df_skew_stats = let
df = combine(
gdf_skews_on_psbid,
sdf -> begin
if nrow(sdf) == 1
(; mean_skew = mean(sdf.skew), std_skew = missing, n = 1)
else
(; mean_skew = mean(sdf.skew), std_skew = std(sdf.skew), n = nrow(sdf))
end
end,
)
dropmissing!(df)
df
end
# ╔═╡ 893253c3-f0b2-401f-b892-b23291bcf5c1
fig_skew_stats = let
fig, ax, sc = scatter(
df_skew_stats.mean_skew,
df_skew_stats.std_skew,
marker = :x,
color = (Makie.wong_colors()[1], 0.8),
axis = (
title = "mean vs std of multiple skew measurements for same PS Boards",
xlabel = "mean / ns",
ylabel = "std / ns",
),
)
text!(
ax,
df_skew_stats.mean_skew,
df_skew_stats.std_skew,
text = string.(df_skew_stats.psbid),
color = (:gray, 0.5),
)
fig
end
# ╔═╡ 6467dcaa-6bd6-45c7-8c08-b310a09b8b0b
save("clock_skew_stats.svg", fig_skew_stats)
# ╔═╡ 19f85f1b-07d3-48c2-9ca6-a5a8eb1ab746
let
fig, ax, sc = scatter(
df_skew_stats.mean_skew,
df_skew_stats.std_skew,
marker = :x,
color = (Makie.wong_colors()[1], 0.8),
axis = (
title = "mean vs std of multiple skew measurements for same PS Boards",
xlabel = "mean / ns",
ylabel = "std / ns",
limits = ((-0.6, 1.2), (-0.01, 0.09)),
),
)
text!(
ax,
df_skew_stats.mean_skew,
df_skew_stats.std_skew,
text = string.(df_skew_stats.psbid),
color = (:gray, 0.5),
)
fig
end
# ╔═╡ 0d758cdf-0dda-4dc3-b489-35831812b718
md"""
### 異常個体リスト
- 分散が1より大きいもの
"""
# ╔═╡ 79e2f5d8-4609-4e9f-949e-6dc1f88c0b19
df_skew_stats_abnormals = let
df = filter([:mean_skew, :std_skew] => ((m, s) -> s > 1), df_skew_stats)
sort!(df, :psbid)
df
end
# ╔═╡ eae649db-6b2b-4530-83a8-3438f29423cc
let
df = filter(:psboard_id => in(df_skew_stats_abnormals.psbid), qaqc_single_results)
select!(df, [:runid, :psboard_id, :lvds_tx_skew, :note])
df
end
# ╔═╡ d607e10e-854f-4652-9a34-9e22a188e315
let
df = df_skew_stats_abnormals
fig, ax, sc = scatter(
df.mean_skew,
df.std_skew,
marker = :x,
color = (Makie.wong_colors()[1], 0.8),
axis = (
title = "mean vs std of multiple skew measurements for same PS Boards",
xlabel = "mean / ns",
ylabel = "std / ns",
),
)
text!(
ax,
df.mean_skew,
df.std_skew,
text = string.(df.psbid) .* "," .* string.(df.n),
color = (:gray, 0.7),
)
fig
end
# ╔═╡ 5bef43cf-0f9b-40a2-bd47-620f42c36d8d
gdf_skews_on_psbid[(psbid = 215,)]
# ╔═╡ 2795fd06-2f59-4e5b-829d-a8e428646790
md"""
### 分散が異常に大きいやつ
基本的に予想通り、分散は小さく複数回の測定で整合的な結果が得られているが、いくつか例外があった。
はじめはpsbid 127(4回測定)が含まれていたが、これはデータベースの編集ミスであることがわかり、修正した結果、消えた。
!!! todo
他の例外も確認する。
`df_skew_stats_abnormals`を確認
#### psbid: 215
- run: 43, 47
- run 43で"QSPI device ID does not match. inclueded in RUN45-, RUN47と49で問題が無いことを確認"
- run 47を使用
#### psbid: 291
- run: 83, 94
- 83でcommunication error(SFP半抜け)
#### psbid: 442
- run: 103, 132
- 103の結果がinvalid(460と同じ)
#### psbid: 460
- run: 105, 132
- psbid 444と同じく電源の抜き差しによってクロックの0と1000が繰り返されたパターン
- 追試に送られてる
#### psbid: 462
- run: 105, 132
- psbid 460と同じく105がだめ
#### psbid: 545
- run: 126, 132
- どちらも測定結果自体には変なところはない
- どちらも1回だけ立ち上がりがある
- 立ち上がりもそれほど長くない
- 126でQSPIpが失敗してるので結果は使わない、132を使う
- 126が電源が不安定なときだったかもしれないが、記録がない
#### psbid: 799
- run: 217, 236
- 217の結果が壊れてる
#### psbid: 802
- run: 217, 236
- 217の結果が壊れてる
#### psbid: 1001
- run: 319, 299
- 299でQSPIpが失敗している。319を使う
#### psbid: 1034
- run: 305, 319
- 305で振動、319を使う
"""
# ╔═╡ 26976b6c-3954-4a41-a99b-c1aaebdc645d
md"""
### skewの分布
"""
# ╔═╡ 38d472ca-6347-4096-828d-fd1256130a59
df_skews_selected = let df
df = combine(
gdf_skews_on_psbid,
sdf -> begin
all(ismissing, sdf.skew) && @info "" sdf
if nrow(sdf) == 1
# TODO: remove 1563 bypass
sdf.psbid[1] == 1563 || @assert sdf.skew |> first |> !ismissing sdf
(
skew = sdf.skew |> first,
width = sdf.width |> first,
riseup = sdf.riseup |> first,
runid = sdf.runid |> first,
)
else
if sdf.psbid[1] == 291
df = filter(:runid => ==(94), sdf)
@assert nrow(df) == 1
(
skew = df.skew |> first,
width = sdf.width |> first,
riseup = sdf.riseup |> first,
runid = sdf.runid |> first,
)
elseif sdf.psbid[1] == 460
df = filter(:runid => ==(132), sdf)
@assert nrow(df) == 1
(
skew = df.skew |> first,
width = sdf.width |> first,
riseup = sdf.riseup |> first,
runid = sdf.runid |> first,
)
elseif sdf.psbid[1] == 545
df = filter(:runid => ==(132), sdf)
@assert nrow(df) == 1
(
skew = df.skew |> first,
width = sdf.width |> first,
riseup = sdf.riseup |> first,
runid = sdf.runid |> first,
)
else
# assume that runid is chronological
i = argmax(sdf.runid .|> (id -> ismissing(id) ? -1 : id))
(
skew = sdf.skew[i],
width = sdf.width[i],
riseup = sdf.riseup[i],
runid = sdf.runid[i],
)
end
end
end,
)
leftjoin!(df, select(qaqc_runs, [:id, :campaign_id]), on = :runid => :id)
df
end
# ╔═╡ 310710da-ebb2-4f54-b238-38d493a6a533
let
skews = df_skews_selected.skew |> skipmissing |> collect
fig = Figure()
ax = Axis(fig[1, 1], title = "skews (n = $(length(skews)))", xlabel = "skew / ns")
stephist!(ax, skews, bins = range(minimum(skews), maximum(skews), step = 1 / 56))
fig
end
# ╔═╡ d92e0f9f-ed53-4ad6-a390-5a395e8ab8bc
md"""
#### 異常に小さい値 (psbid: 444, 1215)
psbid: 444
- 出荷済み
psbid: 1215
- QSPIp fail
- 未出荷
"""
# ╔═╡ e9964184-1a2a-4ab9-bc22-2705a25393ec
filter(:skew => (x -> !ismissing(x) && x < -2), df_skews_selected)
# ╔═╡ bedf8dff-e109-4757-82e5-3232fcad752d
md"""
#### 欠損値(psbid: 255)
"""
# ╔═╡ f2d0ea9b-ede7-496a-8d1f-9f748f9d1666
transform(
filter(:skew => ismissing, df_skews_selected),
:psbid => ByRow(psbid -> begin
psbid in qaqc_dispatch.psb_id
end) => :is_dispatched,
)
# ╔═╡ f5bc8b1a-3fee-48ed-9910-367a1cda632e
md"""
- psbid: 255
- Si not locked
"""
# ╔═╡ 106b8abe-0040-48f1-b663-c00ca7673520
filter(:riseup => !=(1), df_skews_selected)
# ╔═╡ a06d16ee-a4be-40de-8dac-fd818754373c
md"""
#### 立ち上がりが複数回(psbid: 255, 435, 444, 460)
skew結果として使った測定について、立ち上がり回数が1ではないもののリスト
##### psbid: 435
- 立ち上がり後途中で何故か0が入ってる
- 出荷済み
"""
# ╔═╡ 9f3f780d-da6e-44b4-a002-c2f088681e9c
df_skews_selected_sorted = let
df = sort(df_skews_selected, :skew)
dropmissing!(df, :skew)
df
end
# ╔═╡ 5c74d0a8-f856-4988-b8f4-d2e02dc26f52
sort(df_skews_selected_sorted, :skew, order = Base.Order.Reverse)
# ╔═╡ 07342d98-3889-4a19-8419-6d113fffb42e
md"""
#### 立ち上がり時間の分布
"""
# ╔═╡ 939aba6b-b03c-42b6-83b3-9cea5f4eb858
let
skew_widths = df_skews_selected.width |> skipmissing |> collect
bins = range(0, maximum(skew_widths), step = 1 / 56)# .- 0.01
hist(
skew_widths,
bins = bins,
bar_labels = :y,
label_size = 14,
label_formatter = x -> "$(round(Int, x))",
axis = (
title = "clock rise span distribution",
xlabel = "rise up span / ns",
ylabel = "counts",
limits = ((0, 0.28), (0, nothing)),
# xticks = (bins, string.(round.(bins, digits = 3))),
# xticklabelrotation = π / 3,
),
)
end
# ╔═╡ aa9b78bc-8d66-4df2-bd06-1cbf21190488
filter(:width => (x -> ismissing(x) || x < 0.06), df_skews_selected)
# ╔═╡ 76f44129-34c6-451d-af3f-4593dd1dda5c
md"""
## skewの時間依存性
以下のrun
- 382: ファームウェア最新
"""
# ╔═╡ 0dd4c5fe-71d0-45d0-897e-51d1aa06bd3b
df_extra_measurements = let
df_configs = DataFrame(
position_name = ["B-$(i)-$(j)" for j in 1:9 for i in 0:1],
psbid = [
001142,
000973,
000990,
000992,
001113,
001030,
001121,
001141,
001050,
001053,
001110,
001248,
001242,
001276,
000872,
000861,
000525,
000862,
],
position = [1, 10, 2, 11, 3, 12, 4, 13, 5, 14, 6, 15, 7, 16, 8, 17, 9, 18],
)
@info "" df_configs
combine(
groupby(df_configs, :psbid),
AsTable(:) =>
(
sdf -> begin
@assert nrow(sdf) == 1
map([382, 404]) do runid
clkfile = "../test/input/slavelogs/main/$(sdf.psbid[1])_$(runid)_clk.txt"
skew, width =
PSBoardDataBase.ClockParser.get_skew_and_riseup(clkfile)
offset = qaqc_positions.rising_ns[findfirst(
==(sdf.position[1]),
qaqc_positions.id,
)]
return (;
position_name = sdf.position_name |> first,
psbid = sdf.psbid |> first,
position = sdf.position |> first,
skew = skew - offset,
width,
runid,
campaign_id = 6,
)
end
end
) => [
:position_name,
:psbid,
:position,
:skew,
:width,
:runid,
:campaign_id,
],
)
end
# ╔═╡ 148e42fc-d168-4e07-b4f8-f9c3f8c18efc
df_compare = let
df_old_measurements =
filter(:psbid => in(df_extra_measurements.psbid), df_skews_selected)
left_cols = [:psbid, :skew, :width, :runid, :campaign_id]
df = vcat(
select!(df_old_measurements, left_cols),
select(df_extra_measurements, left_cols),
)
sort!(df, :runid)
end
# ╔═╡ 3266d1fd-b8f0-4eb0-9a8e-2050bc9a626f
# "comparison of multiple measurements of clock skew for 18 PS Boards" was deleted
# because of new measurements for all 18 boards in campaign 6.5
let
fig = Figure(size = (600, 500))
grd1 = fig[1, 1] = GridLayout()
Label(
grd1[0, 1:2],
"comparison of multiple measurements of clock skew for 18 PS Boards",
tellwidth = false,
)
ax1 = Axis(
grd1[1, 1],
limits = ((-0.15, 2.35), nothing),
xticks = (0:2, ["old", "new(382)", "new(404)"]),
ylabel = "skew / ns",
)
ax2 = Axis(
grd1[1:2, 2],
limits = ((2.9, 6.7), nothing),
ylabel = "Δskew / ns",
xticks = (3:6, string.(3:6)),
xlabel = "campaign",
yminorticksvisible = true,
yminorgridvisible = true,
)
colsize!(grd1, 2, Relative(0.4))
for (key, sdf) in pairs(groupby(df_compare, :psbid))
@info "" sdf.runid
# @assert sdf.runid[2] == 382 && sdf.runid[3] == 404
scatterlines!(
ax1,
0:1:2,
sdf.skew,
color = Makie.wong_colors()[sdf.campaign_id[1] |> Int64],
alpha = 0.7,
)
scatter!(
ax2,
sdf.campaign_id[1],
mean(@view(sdf.skew[2:3])) - sdf.skew[1],
marker = :x,
color = Makie.wong_colors()[sdf.campaign_id[1] |> Int64],
alpha = 0.7,
)
errorbars!(
ax2,
[sdf.campaign_id[1]],
[mean(@view(sdf.skew[2:3])) - sdf.skew[1]],
[std(@view(sdf.skew[2:3]))],
color = Makie.wong_colors()[sdf.campaign_id[1] |> Int64],
alpha = 0.4,
whiskerwidth = 5,
)
text!(
ax1,
2,
sdf.skew |> last,
text = string(sdf.psbid[1]),
color = (:black, 0.5),
align = (:left, :center),
)
text!(
ax2,
sdf.campaign_id[1],
mean(@view(sdf.skew[2:3])) - sdf.skew[1],
text = string(sdf.psbid[1]),
color = (:black, 0.5),
align = (:left, :center),
)
end
campaigns = df_compare.campaign_id |> unique |> sort! .|> ceil .|> Int64
Legend(
grd1[2, 1],
[
[LineElement(color = color), MarkerElement(color = color, marker = :circle)] for
color in Makie.wong_colors()[campaigns]
],
string.(campaigns),
"campaign id for old measurements",
orientation = :horizontal,
)
# fig
end
# ╔═╡ 660f2bd1-d4bc-45a8-9cf6-4e875aa9f7a2
let
df = filter(:campaign_id => ==(6), df_skews_selected)
leftjoin!(df, select(qaqc_runs, [:id, :run_datetime]), on = :runid => :id)
# from UTC to JST(+9h)
transform!(df, :run_datetime => ByRow(dt -> dt + Hour(9)) => :run_datetime)
@assert all(!ismissing, df.run_datetime)
dropmissing!(df, :run_datetime)
transform!(df, :run_datetime => ByRow(Date) => :run_date)
fig = Figure()
gdf = groupby(df, :run_date)
for (i, sdf) in enumerate(gdf)
unit_converter = Makie.DateTimeConversion(Time)
ax = Axis(
fig[i, 1],
title = string(keys(gdf)[i]),
dim1_conversion = unit_converter,
limits = (
(
Makie.convert_dim_value(unit_converter, Time(9)),
Makie.convert_dim_value(unit_converter, Time(20)),
),
(-2, 1),
),
)
scatter!(ax, sdf.run_datetime .|> Time, sdf.skew, markersize = 7, alpha = 0.7)
mean_runs = Union{Float64, Missing}[]
std_runs = Union{Float64, Missing}[]
datetime_runs = Time[]
for sdf_samerun in groupby(sdf, :runid)
push!(mean_runs, mean(sdf_samerun.skew |> skipmissing))
push!(std_runs, std(sdf_samerun.skew |> skipmissing))
push!(datetime_runs, sdf_samerun.run_datetime |> first)
end
scatterlines!(
ax,
datetime_runs,
mean_runs,
marker = :hline,
markersize = 10,
color = Makie.wong_colors()[2],
)
errorbars!(ax, datetime_runs, mean_runs, std_runs, color = Makie.wong_colors()[2])
end
fig
end
# ╔═╡ 86437ee6-ccea-43fa-bd93-d86fe055f28d
md"""
# プロットまとめ
- 何らかの異常が見られたものをすべて除外してプロット
!!! todo
skew測定が使い物になるかのフラグをデータベースにいれる
"""
# ╔═╡ 2a24c277-6679-49f3-b387-6937b7661a25
invalid_measurements =
[215, 291, 442, 460, 462, 545, 799, 802, 444, 255, 435, 1213, 1215, 1563] #= TODO: remove =#
# ╔═╡ 13b4112f-96ab-41a1-8ef1-d940771f6ece
df_skews_selected_valids = let
df = filter(:psbid => !in(invalid_measurements), df_skews_selected)
df
end
# ╔═╡ 2dde2b39-8f8c-473b-8fed-393a9e3286d8
sort(df_skews_selected_valids, :skew)
# ╔═╡ 2a579bc2-79f8-4773-8588-a413acb8a6d6
sort(df_skews_selected_valids, :skew, rev = true)
# ╔═╡ cf658de8-a4c5-413e-b5e3-56b77a80336f
sort(df_skews_selected, :width)
# ╔═╡ 7efd380b-a976-4a86-8dff-8afd551f03fb
sort(df_skews_selected, :width, rev = true)
# ╔═╡ 6a856a55-2611-41d0-a2c7-3645c066fc3c
begin
@assert all(!ismissing, df_skews_selected_valids.skew)
@assert all(!ismissing, df_skews_selected_valids.width)
@assert all(==(1), df_skews_selected_valids.riseup)
end
# ╔═╡ 4f45c81e-df2d-481f-9d30-580e44e03c72
let
rng = Random.Xoshiro(1000)
example_results = rand(rng, eachrow(df_skews_selected_valids), 5)
fig = Figure(size = (600, 500))
ax_1 = Axis(
fig[1, 1],
title = "example clock skew measurement result",
xlabel = "offset / ns",
ylabel = "count",
limits = ((10, 16), nothing),
)
ax_2 = Axis(
fig[2, 1],
title = "example clock after substracting position contribution",
xlabel = "offset / ns",
ylabel = "count",
limits = ((-3, 3), nothing),
)
for example_result in example_results
psbid = example_result.psbid
runid = example_result.runid
single_run =
filter(
[:psboard_id, :runid] => (
(ref_psbid, ref_runid) -> begin
psbid == ref_psbid && runid == ref_runid
end
),
qaqc_single_results,
) |> first
pos_offset = qaqc_positions.rising_ns[single_run.position]
rawfilename = "../test/input/slavelogs/main/$(psbid)_$(runid)_clk.txt"
raw_points = map(eachline(rawfilename)) do line
PSBoardDataBase.ClockParser._parse_line(line)
end
stds = map(raw_points) do ((time, count))
sqrt(count * (1000 - count) / 1000)
end
collected_points = map(raw_points) do ((time, count))
(time - pos_offset, count)
end
scatterlines!(ax_1, raw_points, markersize = 6, label = basename(rawfilename))
scatterlines!(ax_2, collected_points, markersize = 6, label = basename(rawfilename))
errorbars!(ax_1, raw_points, stds, whiskerwidth = 10)
errorbars!(ax_2, collected_points, stds, whiskerwidth = 10)
end
axislegend(ax_2, position = :lt)
fig
end
# ╔═╡ 92d701aa-ab90-4c91-977d-2ce92823d130
md"""
## skew分布
"""
# ╔═╡ 4a4ef945-b312-44ed-ab62-ce01fc33f926
let
bins = range(
minimum(df_skews_selected_valids.skew) - 5 / 56,
maximum(df_skews_selected_valids.skew) + 5 / 56,
step = 2 / 56,
)
fig = Figure()
ax = Axis(
fig[1, 1],
title = "skews",
limits = (nothing, (nothing, nothing)),
xlabel = "skew / ns",
)
sh1 = stephist!(ax, df_skews_selected_valids.skew, bins = bins)
Legend(
fig[1, 1],
[sh1],
["""
n = $(nrow(df_skews_selected_valids))
μ = $(@sprintf "%.2g" mean(df_skews_selected_valids.skew))
σ = $(@sprintf "%.2g" std(df_skews_selected_valids.skew))
"""],
tellwidth = false,
tellheight = false,
halign = :right,
valign = :top,
margin = (10, 10, 10, 10),
)
save("psboard_skew_histogram.svg", fig)
fig
end
# ╔═╡ bf62eaca-dd23-4470-9c02-20ce6f9f34d7
let
df_with_raw = leftjoin(
df_skews_selected_valids,
select(df_rawskews, [:psbid, :runid, :skew]);
on = [:psbid, :runid],
renamecols = (identity => (s -> s * "_raw")),
)
fig = Figure(size = (600, 600))
Label(
fig[0, 1],
"PS board clock skews measured in QAQC",
tellwidth = false,
fontsize = 20,
)
ax_raw = Axis(
fig[1, 1],
limits = ((10, 16), nothing),
title = "before substracting position contrib.",
)
ax_new = Axis(
fig[2, 1],
limits = ((-3, 3), nothing),
title = "after substracting position contrib.",
xlabel = "skew / ns",
)
sh1 = stephist!(
ax_raw,
df_with_raw.skew_raw,
color = Makie.wong_colors()[2],
bins = range(10, 16, step = 2 / 56),
)
sh2 = stephist!(
ax_new,
df_with_raw.skew,
color = Makie.wong_colors()[1],
bins = range(-3, 3, step = 2 / 56),
)
Legend(
fig[1, 1],
[sh1],
["""
n = $(nrow(df_with_raw))
μ = $(@sprintf "%.4g" mean(df_with_raw.skew_raw))
σ = $(@sprintf "%.2g" std(df_with_raw.skew_raw))
"""],
tellwidth = false,
tellheight = false,
halign = :right,
valign = :top,
margin = (10, 10, 10, 10),
)
Legend(
fig[2, 1],
[sh2],
["""
n = $(nrow(df_skews_selected_valids))
μ = $(@sprintf "%.2g" mean(df_skews_selected_valids.skew))
σ = $(@sprintf "%.2g" std(df_skews_selected_valids.skew))
"""],
tellwidth = false,
tellheight = false,
halign = :right,
valign = :top,
margin = (10, 10, 10, 10),
)
save("plots/skew_distrib_before_after.svg", fig)
fig
end
# ╔═╡ 82063c4b-c0cf-4524-83fe-5207bb8363d8
md"""
### skew分布のcampaign依存性
"""
# ╔═╡ dff359b1-4827-40c6-86e4-0915974ef27d
let
bins = range(
minimum(df_skews_selected_valids.skew) - 5 / 56,
maximum(df_skews_selected_valids.skew) + 5 / 56,
step = 8 / 56,
)
fig = Figure()
ax = Axis(
fig[1, 1],
title = "skews",
limits = (nothing, (nothing, nothing)),
xlabel = "skew / ns",
)
hists = Hist1D[]
for gdf in groupby(df_skews_selected_valids, :campaign_id, sort = true)
push!(hists, Hist1D(gdf.skew, binedges = bins))
end
@info "" hists |> length
sh1 = stackedhist!(ax, hists, error_color = (:black, 0), color = ColorSchemes.tab10)
labels = ["campaign $i" for i in [1:6; 6.5; 7; 7.5]]
elements =
[PolyElement(polycolor = sh1.attributes.color[][i]) for i in 1:length(labels)]
Legend(
fig[1, 1],
elements,
labels,
["""
n = $(nrow(df_skews_selected_valids))
μ = $(@sprintf "%.2g" mean(df_skews_selected_valids.skew))
σ = $(@sprintf "%.2g" std(df_skews_selected_valids.skew))
"""],
tellwidth = false,
tellheight = false,
halign = :right,
valign = :top,
margin = (10, 10, 10, 10),
)
save("plots/skew_distrib_campaings.svg", fig)
fig
end
# ╔═╡ 7920b03b-1d1a-4b51-bfc0-86d1361f2ff1
let
fig = Figure()
ax = Axis(
fig[1, 1],
xlabel = "psbid",
ylabel = "skew / ns",
title = "clock skew time dependency",
)
scatter!(ax, df_skews_selected_valids.psbid, df_skews_selected_valids.skew)
fig
end
# ╔═╡ e640424b-7f7b-4cca-a634-92749ceee170
let
fig = Figure()
ax = Axis(
fig[1, 1],
xlabel = "runid",
ylabel = "skew / ns",
title = "clock skew time dependency",
)
@info "" qaqc_runs
df_campaign_runlimits = let
df = combine(groupby(qaqc_runs, :campaign_id), :id => extrema)
dropmissing!(df)
df
end
@info "" df_campaign_runlimits
scatter!(ax, df_skews_selected_valids.runid, df_skews_selected_valids.skew)
rangebars!(ax,
2.2,
df_campaign_runlimits.id_extrema,
direction = :x,
color = :black,
whiskerwidth = 10,
)
text!(
ax,
df_campaign_runlimits.id_extrema .|> mean,
fill(2.3, nrow(df_campaign_runlimits)),
text = df_campaign_runlimits.campaign_id .|> string,
align = (:center, :center)
)
save("plots/skew_vs_runid.svg", fig)
fig
end
# ╔═╡ c3236e51-12a3-43e1-afc4-71be54c0a869
let
fig = Figure()
ax = Axis(
fig[1, 1],
xlabel = "runid",
ylabel = "skew / ns",
title = "clock skew time dependency",
limits = ((290, 320), nothing),
xminorticksvisible = true,
xminorgridvisible = true,
)
scatter!(ax, df_skews_selected_valids.runid, df_skews_selected_valids.skew)
text!(
ax,
df_skews_selected_valids.runid,
df_skews_selected_valids.skew |> disallowmissing,
text = string.(df_skews_selected_valids.psbid),
color = (:black, 0.3),
align = (:left, :center),
)
df_byruns = combine(
groupby(df_skews_selected_valids, :runid),
AsTable(:) =>
(sdf -> begin
(mean = mean(sdf.skew), std = std(sdf.skew))
end) => [:mean, :std],
)
scatter!(ax, df_byruns.runid, df_byruns.mean, label = "mean")
# vlines!(ax, [304])
let
rangebars!(
ax,
[-1, -1],
[293, 305],
[304, 329],
direction = :x,
whiskerwidth = 10,
color = :black,
)
text!(ax, [(293 + 304) / 2, (305 + 329) / 2], [-1, -1], text = ["11-11", "11-12"])
end
df_highlighted = filter(
:psbid => in([973, 990, 992, 1030, 1050, 1053, 1110, 1113, 1121, 1141, 1142]),
df_skews_selected_valids,
)
@info "" df_highlighted
scatter!(
ax,
df_highlighted.runid,
df_highlighted.skew |> disallowmissing,
marker = :x,
markersize = 20,
alpha = 0.3,
)
axislegend(ax)
fig
end
# ╔═╡ 126f5825-25c1-4628-b4ae-ffbd9830833c
let
df = leftjoin(
df_skews_selected_valids,
select(qaqc_runs, [:id, :run_datetime]),
on = :runid => :id,
)
# from UTC to JST(+9h)
transform!(df, :run_datetime => ByRow(dt -> dt + Hour(9)) => :run_datetime)
@assert all(!ismissing, df.run_datetime)
dropmissing!(df, :run_datetime)
transform!(df, :run_datetime => ByRow(Date) => :run_date)
fig = Figure(size = (600, 900))
gdf = groupby(df, :campaign_id)
for (i, sdf) in enumerate(gdf)
unit_converter = Makie.DateTimeConversion(Time)
ax = Axis(
fig[i, 1],
title = string(keys(gdf)[i]),
dim1_conversion = unit_converter,
limits = (
(
Makie.convert_dim_value(unit_converter, Time(0)),
Makie.convert_dim_value(unit_converter, Time(23, 59, 59)),
),
# (-2, 2),
nothing,
),
)
sdf = transform(sdf, :run_datetime => ByRow(Time) => :run_time)
sort!(sdf, :run_time)
scatter!(ax, sdf.run_datetime .|> Time, sdf.skew, markersize = 7, alpha = 0.7)
df_runs = combine(
groupby(sdf, :runid),
AsTable(:) =>
(
sdf -> begin
(;
mean = mean(sdf.skew |> skipmissing),
std = std(sdf.skew |> skipmissing),
run_time = first(sdf.run_time),
)
end
) => [:mean, :std, :run_time],
)
sort!(df_runs, :run_time)
scatterlines!(
ax,
df_runs.run_time,
df_runs.mean,
marker = :hline,
markersize = 10,
color = Makie.wong_colors()[2],
)
errorbars!(
ax,
df_runs.run_time,
df_runs.mean,
df_runs.std,
color = Makie.wong_colors()[2],
)
end
fig
end
# ╔═╡ 46b2a3cd-d2e6-4277-8b65-9c61f25f69e8
3 / 56
# ╔═╡ 55bad662-cfdd-45c8-81bf-4e65e5c8434e
md"""
## 立ち上がり時間分布
"""
# ╔═╡ 13bb4978-b98d-44a3-a4b6-4241cadc609b
let
bins = range(
minimum(df_skews_selected_valids.width) - 1 / 56,
maximum(df_skews_selected_valids.width) + 2 / 56,
step = 1 / 56,
)
fig = Figure()
ax = Axis(
fig[1, 1],
title = "distribution of rise up spans",
xlabel = "time / ns",
xticks = (bins, string.(round.(bins, digits = 3))),
xticklabelrotation = π / 3,
)
h1 = hist!(
ax,
df_skews_selected_valids.width,
bins = bins,
bar_labels = :y,
label_formatter = x -> "$(round(Int, x))",
flip_labels_at = 330,
)
Legend(
fig[1, 1],
[h1],
["""
n = $(nrow(df_skews_selected_valids))
μ = $(@sprintf "%.2g" mean(df_skews_selected_valids.width))
σ = $(@sprintf "%.2g" std(df_skews_selected_valids.width))
"""],
tellwidth = false,
tellheight = false,
halign = :right,
valign = :top,
margin = (10, 10, 10, 10),
)
save("psboard_clock_rise_span_histogram.svg", fig)
fig
end
# ╔═╡ 2835ff7c-d6cc-49d8-b35c-0071a8364376
scatter(
df_skews_selected_valids.runid,
df_skews_selected_valids.width,
markersize = 7,
alpha = 0.2,
axis = (xlabel = "runid", ylabel = "width"),
)
# ╔═╡ 6c3c7669-d594-425d-bf05-9aa217c7656e
md"""
# turn on curves
- lines on
- the minimum and maximum skews
- the minimum and maximum widths
"""
# ╔═╡ e9b3f541-d87d-4424-8b82-be1b7b3273d8
let
psbid_width_min, psbid_width_max, psbid_width_typical = let
df_normal = filter(:width => >(0.06), df_skews_selected_sorted)
sort!(df_normal, :width)
df_typical = filter(:width => (x -> 0.08 < x < 0.11), df_normal)
rng = Random.Xoshiro(123)
id_rand_typical = rand(rng, 1:nrow(df_typical))
first(df_normal).psbid, last(df_normal).psbid, df_typical[id_rand_typical, :psbid]
end
psbid_with_lines = [psbid_width_max, psbid_width_min, psbid_width_typical]
fig = Figure()
ax = Axis(
fig[1, 1],
title = "clock turn on curves",
subtitle = "aligned on the last count = 0 points",
xlabel = "ns",
ylabel = "counts",
limits = ((-0.05, 0.15), (0, 1000)),
)
for file in clk_files
psbid, runid = parse_filename(file)
if psbid == 255
@info "Si not locked: skipped" psbid
continue
end
single_runs = filter(
[:psboard_id, :runid] => (
(ref_psbid, ref_runid) -> begin
psbid == ref_psbid && runid == ref_runid
end
),
qaqc_single_results,
)
if nrow(single_runs) != 1
if nrow(single_runs) != 0
@info "skipped" psbid runid nrow(single_runs)
end
continue
end
# offset = qaqc_positions.rising_ns[single_runs.position[1]]
offset_pos = let
# df = filter(:psbid => ==(psbid), df_skews_selected_sorted)
# @assert nrow(df) == 1 "nrow: $(nrow(df)), psbid: $(psbid)"
# df.skew[1] +
qaqc_positions.rising_ns[single_runs.position[1]]
end
if psbid in psbid_with_lines
points =
eachline(file) .|>
PSBoardDataBase.ClockParser._parse_line .|>
(x -> (x[1] - offset_pos, x[2]))
id_first_over0 = findfirst(points) do ((time, count))
count > 0
end
points = map(points) do ((time, count))
time - points[id_first_over0][1], count
end
stds = map(points) do ((time, count))
sqrt(count * (1000 - count) / 1000)
end
label, color = if psbid == psbid_width_max
"longest ($psbid)", (:red, 0.8)
elseif psbid == psbid_width_min
"shortest ($psbid)", (:blue, 0.8)
elseif psbid == psbid_width_typical
"typical ($psbid)", (:green, 0.8)
end
scatterlines!(ax, points, label = label, color = color)
errorbars!(ax, points, stds, whiskerwidth = 10, color = color)
end
end
axislegend(ax, position = :rb)
save("psboard_clock_turnoncurve.svg", fig)
fig
end
# ╔═╡ Cell order:
# ╟─f25e7e08-8a73-4cac-ac7c-d310725c558d
# ╠═7c69d12c-80a5-11ef-2674-e155a3483342
# ╠═effa7ed9-2ac4-4468-a474-e2bb662580fe
# ╠═1a6322d4-9deb-4709-aa2e-df7d8be5b16f
# ╠═11537f91-e16b-45f0-9768-6df842371d36
# ╠═268d3015-b8d3-48d9-b74a-062e258e0ec1
# ╟─62105832-df1f-4834-8da6-c542e22207d1
# ╠═dea6f143-7916-4765-92f6-2bfb97a72835
# ╟─41543c0c-d7c4-447b-a268-0d356c88d92c
# ╠═633ecdee-7e2f-4de8-833a-21cd0351c1f1
# ╟─87f1966e-3b07-4f9d-8fc4-7b7aa4319d50
# ╠═f379d43c-9300-41f4-b0fc-3c9d749e3105
# ╟─33e099bc-ac4b-4b5f-88e7-20f4463c98ef
# ╠═0e13f848-0efb-4775-9e3e-518b32588a79
# ╠═181c3fe6-d087-42e2-b175-3fb84c42e3e8
# ╟─dd669f14-989b-45ee-87d8-5d9cf282fafd
# ╠═1e9c3944-0cd4-40da-9014-a9153d4e95ed
# ╟─322cb530-65a5-4973-86f8-01ccc2439cc4
# ╠═c1caca5f-4cfd-4f22-82b4-7925002359e6
# ╠═3e5607fd-2a8a-4a1a-9e7b-3f23ef216fad
# ╠═c1b9c0c3-00f8-4199-b07f-8888f1be625c
# ╠═d6d04013-e0e4-49d5-a450-07ae164bfaa3
# ╠═0e680044-e4e1-4f39-a5c5-afa5c53fc7a7
# ╠═d7541b93-4c49-4dcd-bda0-91e447f44596
# ╟─e7faa647-79cd-4247-a3f2-9868c7b9d4ca
# ╠═3a412a98-4a2d-4bfb-8053-b0f480fae921
# ╟─b38bbed4-8721-4e92-a546-f7926cc07dd3
# ╠═420dce0e-4757-48d9-84ec-7ddfac2fdff6
# ╠═99902640-fee3-4502-9c7e-cb08834bad0b
# ╠═c79c6684-1b03-41b5-aa90-ef8c7a8eb69c
# ╠═a1056aba-e484-4594-908b-4709896d0da0
# ╠═875bec26-e576-4f48-ba14-464bce503d75
# ╟─ec774495-c0be-47a4-9d2c-b48159c07013
# ╠═d082e07c-3b42-4362-bebf-63356979a49b
# ╟─2d41c98b-630e-41e7-9332-25394a6285e8
# ╠═6ff9f397-5db9-45af-9bca-7a8d7756f8da
# ╠═25688d24-5aee-43d3-aff9-b9efa0556070
# ╠═0d3f7f0a-1713-4e22-acbb-7d370fabf5a7
# ╠═239a808c-0411-4542-ae68-6ae6af333bd2
# ╠═8e57bde1-5f97-483d-906e-8ebfb65016d0
# ╠═92c2ac3f-8034-4e9e-aadb-8bb166fbc948
# ╠═893253c3-f0b2-401f-b892-b23291bcf5c1
# ╠═6467dcaa-6bd6-45c7-8c08-b310a09b8b0b
# ╠═19f85f1b-07d3-48c2-9ca6-a5a8eb1ab746
# ╟─0d758cdf-0dda-4dc3-b489-35831812b718
# ╠═79e2f5d8-4609-4e9f-949e-6dc1f88c0b19
# ╠═eae649db-6b2b-4530-83a8-3438f29423cc
# ╠═d607e10e-854f-4652-9a34-9e22a188e315
# ╠═5bef43cf-0f9b-40a2-bd47-620f42c36d8d
# ╠═2795fd06-2f59-4e5b-829d-a8e428646790
# ╟─26976b6c-3954-4a41-a99b-c1aaebdc645d
# ╠═38d472ca-6347-4096-828d-fd1256130a59
# ╠═310710da-ebb2-4f54-b238-38d493a6a533
# ╟─d92e0f9f-ed53-4ad6-a390-5a395e8ab8bc
# ╠═e9964184-1a2a-4ab9-bc22-2705a25393ec
# ╟─bedf8dff-e109-4757-82e5-3232fcad752d
# ╠═f2d0ea9b-ede7-496a-8d1f-9f748f9d1666
# ╠═f5bc8b1a-3fee-48ed-9910-367a1cda632e
# ╠═106b8abe-0040-48f1-b663-c00ca7673520
# ╠═a06d16ee-a4be-40de-8dac-fd818754373c
# ╠═9f3f780d-da6e-44b4-a002-c2f088681e9c
# ╠═5c74d0a8-f856-4988-b8f4-d2e02dc26f52
# ╠═07342d98-3889-4a19-8419-6d113fffb42e
# ╠═939aba6b-b03c-42b6-83b3-9cea5f4eb858
# ╠═aa9b78bc-8d66-4df2-bd06-1cbf21190488
# ╠═76f44129-34c6-451d-af3f-4593dd1dda5c
# ╠═0dd4c5fe-71d0-45d0-897e-51d1aa06bd3b
# ╠═148e42fc-d168-4e07-b4f8-f9c3f8c18efc
# ╠═3266d1fd-b8f0-4eb0-9a8e-2050bc9a626f
# ╠═660f2bd1-d4bc-45a8-9cf6-4e875aa9f7a2
# ╠═86437ee6-ccea-43fa-bd93-d86fe055f28d
# ╠═2a24c277-6679-49f3-b387-6937b7661a25
# ╠═13b4112f-96ab-41a1-8ef1-d940771f6ece
# ╠═2dde2b39-8f8c-473b-8fed-393a9e3286d8
# ╠═2a579bc2-79f8-4773-8588-a413acb8a6d6
# ╠═cf658de8-a4c5-413e-b5e3-56b77a80336f
# ╠═7efd380b-a976-4a86-8dff-8afd551f03fb
# ╠═6a856a55-2611-41d0-a2c7-3645c066fc3c
# ╠═4f45c81e-df2d-481f-9d30-580e44e03c72
# ╟─92d701aa-ab90-4c91-977d-2ce92823d130
# ╠═4a4ef945-b312-44ed-ab62-ce01fc33f926
# ╠═bf62eaca-dd23-4470-9c02-20ce6f9f34d7
# ╟─82063c4b-c0cf-4524-83fe-5207bb8363d8
# ╠═dff359b1-4827-40c6-86e4-0915974ef27d
# ╠═7920b03b-1d1a-4b51-bfc0-86d1361f2ff1
# ╠═e640424b-7f7b-4cca-a634-92749ceee170
# ╠═c3236e51-12a3-43e1-afc4-71be54c0a869
# ╠═126f5825-25c1-4628-b4ae-ffbd9830833c
# ╠═46b2a3cd-d2e6-4277-8b65-9c61f25f69e8
# ╟─55bad662-cfdd-45c8-81bf-4e65e5c8434e
# ╠═13bb4978-b98d-44a3-a4b6-4241cadc609b
# ╠═2835ff7c-d6cc-49d8-b35c-0071a8364376
# ╠═6c3c7669-d594-425d-bf05-9aa217c7656e
# ╠═e9b3f541-d87d-4424-8b82-be1b7b3273d8