From 07a95eaf8eabdbce92fded5e2c1ca16fb6324ae5 Mon Sep 17 00:00:00 2001 From: Phillip Alday Date: Mon, 18 Nov 2024 05:40:33 -0700 Subject: [PATCH] CI improvements (#63) * add format and spellcheck CI * more CI * YAS Style * fix arch * wrap tests into testsets * typos * formatter tweak * bump Julia compat * drop macos from testing --- .JuliaFormatter.toml | 1 + .github/workflows/CI.yml | 8 +- .github/workflows/formatcheck.yml | 49 ++++++++ .github/workflows/spellcheck.yml | 21 ++++ Project.toml | 3 +- _typos.toml | 3 + assets/write_template-positions-bin.jl | 11 +- docs/make.jl | 35 +++--- docs/src/topo_series.jl | 131 ++++++++++---------- src/TopoPlots.jl | 9 +- src/core-recipe.jl | 81 +++++++------ src/eeg.jl | 140 ++++++++++++---------- src/extrapolation.jl | 5 +- src/interpolators.jl | 91 +++++++------- test/CondaPkg.toml | 2 +- test/nb_example.jl | 72 +++++------ test/runtests.jl | 159 +++++++++++++------------ test/test-scripts.jl | 32 ++--- 18 files changed, 470 insertions(+), 383 deletions(-) create mode 100644 .JuliaFormatter.toml create mode 100644 .github/workflows/formatcheck.yml create mode 100644 .github/workflows/spellcheck.yml create mode 100644 _typos.toml diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 0000000..857c3ae --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1 @@ +style = "yas" diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 3e31838..7355ccc 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,29 +13,29 @@ concurrency: cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} jobs: test: - name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} + name: Julia ${{ matrix.version }} - ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: version: - '1' + - 'min' os: - ubuntu-latest - arch: - - x64 + # - macos-latest # segfaults at the end of testing due to Python things steps: - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} - arch: ${{ matrix.arch }} - uses: julia-actions/cache@v2 with: cache-registries: "true" - uses: julia-actions/julia-buildpkg@v1 - uses: julia-actions/julia-runtest@v1 - name: Percy Upload + if: ${{ matrix.version }} == '1' && ${{ matrix.os }} == 'ubuntu-latest' run: 'npx @percy/cli upload ./test/test_images' env: PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }} diff --git a/.github/workflows/formatcheck.yml b/.github/workflows/formatcheck.yml new file mode 100644 index 0000000..4f26ca8 --- /dev/null +++ b/.github/workflows/formatcheck.yml @@ -0,0 +1,49 @@ +--- +name: Style Guide +on: + push: + branches: + - master + - main + - /^release-.*$/ + tags: ["*"] + paths: + - "**/*.jl" + - ".github/workflows/FormatCheck.yml" + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + paths: + - "**/*.jl" + - ".github/workflows/FormatCheck.yml" +jobs: + format-check: + name: Julia + # These permissions are needed to: + # - Delete old caches: https://github.com/julia-actions/cache#usage + # - Post formatting suggestions: https://github.com/reviewdog/action-suggester#required-permissions + permissions: + actions: write + contents: read + pull-requests: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + with: + version: "1" + - uses: julia-actions/cache@v2 + - name: Install JuliaFormatter + shell: julia --project=@format --color=yes {0} + run: | + using Pkg + Pkg.add(PackageSpec(; name="JuliaFormatter", version="1")) + - name: Check formatting + shell: julia --project=@format --color=yes {0} + run: | + using JuliaFormatter + format(".", YASStyle(); verbose=true) || exit(1) + # Add formatting suggestions to non-draft PRs even if when "Check formatting" fails + - uses: reviewdog/action-suggester@185c9c06d0a28fbe43b50aca4b32777b649e7cbd # v1.12.0 + if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.draft == false }} + with: + tool_name: JuliaFormatter diff --git a/.github/workflows/spellcheck.yml b/.github/workflows/spellcheck.yml new file mode 100644 index 0000000..f9c7b21 --- /dev/null +++ b/.github/workflows/spellcheck.yml @@ -0,0 +1,21 @@ +# adapted from https://github.com/JuliaDocs/Documenter.jl/blob/master/.github/workflows/SpellCheck.yml +# see docs at https://github.com/crate-ci/typos +name: Spell Check +on: [pull_request] + +jobs: + typos-check: + name: Spell Check with Typos + runs-on: ubuntu-latest + steps: + - name: Checkout Actions Repository + uses: actions/checkout@v4 + - name: Check spelling + uses: crate-ci/typos@b74202f74b4346efdbce7801d187ec57b266bac8 # v1.27.3 + with: + config: _typos.toml + write_changes: true + - uses: reviewdog/action-suggester@v1 + with: + tool_name: Typos + fail_on_error: true diff --git a/Project.toml b/Project.toml index 536bb12..1add77c 100644 --- a/Project.toml +++ b/Project.toml @@ -31,10 +31,11 @@ PrecompileTools = "1" ScatteredInterpolation = "0.3.6" Statistics = "1" Test = "1" -julia = "1.6" +julia = "1.10" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +# See test/Project.toml!! [targets] test = ["Test"] diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 0000000..1d60a6d --- /dev/null +++ b/_typos.toml @@ -0,0 +1,3 @@ +[default.extend-words] +# don't current this name +Shepard = "Shepard" diff --git a/assets/write_template-positions-bin.jl b/assets/write_template-positions-bin.jl index 726d829..d8ea801 100644 --- a/assets/write_template-positions-bin.jl +++ b/assets/write_template-positions-bin.jl @@ -1,13 +1,14 @@ using PyMNE, PythonCall, TopoPlots -channels = ["fp1", "f3", "c3", "p3", "o1", "f7", "t3", "t5", "fz", "cz", "pz", "fp2", "f4", "c4", "p4", "o2", "f8", "t4", "t6"] +channels = ["fp1", "f3", "c3", "p3", "o1", "f7", "t3", "t5", "fz", "cz", "pz", "fp2", "f4", + "c4", "p4", "o2", "f8", "t4", "t6"] info = pycall(PyMNE.mne.create_info, PyObject, channels, 120.0; ch_types="eeg") info.set_montage("standard_1020"; match_case=false) layout = PyMNE.mne.find_layout(info) -write(TopoPlots.assetpath("layout_10_20.bin"), hcat(layout.pos[:, 1], layout.pos[:, 2])) +write(TopoPlots.assetpath("layout_10_20.bin"), hcat(layout.pos[:, 1], layout.pos[:, 2])) #--- -using CSV,DataFrames +using CSV, DataFrames -loc2d = CSV.read(TopoPlots.assetpath("1005.tsv"),DataFrame) # taken from https://github.com/sappelhoff/eeg_positions/blob/main/data/Fpz-T8-Oz-T7/standard_1005_2D.tsv -write(TopoPlots.assetpath("layout_10_05.bin"),hcat(loc2d.x,loc2d.y)) +loc2d = CSV.read(TopoPlots.assetpath("1005.tsv"), DataFrame) # taken from https://github.com/sappelhoff/eeg_positions/blob/main/data/Fpz-T8-Oz-T7/standard_1005_2D.tsv +write(TopoPlots.assetpath("layout_10_05.bin"), hcat(loc2d.x, loc2d.y)) diff --git a/docs/make.jl b/docs/make.jl index f9b7d12..3f553e5 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -6,25 +6,20 @@ using TopoPlots: CHANNEL_TO_POSITION_10_05, CHANNEL_TO_POSITION_10_20 DocMeta.setdocmeta!(TopoPlots, :DocTestSetup, :(using TopoPlots); recursive=true) makedocs(; - modules=[TopoPlots], - authors="Benedikt Ehinger, Simon Danisch, Beacon Biosignals", - sitename="TopoPlots.jl", - checkdocs=:exports, - format=Documenter.HTML(; - prettyurls=get(ENV, "CI", "false") == "true", - canonical="https://MakieOrg.github.io/TopoPlots.jl", - assets=String[], - ), - pages=[ - "Home" => "index.md", - "General TopoPlots" => "general.md", - "EEG" => "eeg.md", - "Function reference" => "functions.md", - "Interpolator reference images" => "interpolator_reference.md" - ], -) + modules=[TopoPlots], + authors="Benedikt Ehinger, Simon Danisch, Beacon Biosignals", + sitename="TopoPlots.jl", + checkdocs=:exports, + format=Documenter.HTML(; + prettyurls=get(ENV, "CI", "false") == "true", + canonical="https://MakieOrg.github.io/TopoPlots.jl", + assets=String[],), + pages=["Home" => "index.md", + "General TopoPlots" => "general.md", + "EEG" => "eeg.md", + "Function reference" => "functions.md", + "Interpolator reference images" => "interpolator_reference.md"],) deploydocs(; - repo="github.com/MakieOrg/TopoPlots.jl", - devbranch="master", push_preview=true -) + repo="github.com/MakieOrg/TopoPlots.jl", + devbranch="master", push_preview=true) diff --git a/docs/src/topo_series.jl b/docs/src/topo_series.jl index 0f841aa..eed388d 100644 --- a/docs/src/topo_series.jl +++ b/docs/src/topo_series.jl @@ -6,39 +6,35 @@ using InteractiveUtils # ╔═╡ 2fafb0da-f3a9-11ec-0ddf-6725344070fe begin -using Pkg -Pkg.activate("../../devEnv") # docs -#Pkg.add("PyMNE") -#Pkg.add(path="../../../TopoPlotsjl/") -Pkg.develop(path="../../../TopoPlotsjl/") -#Pkg.add("DataFrames") -#Pkg.add("AlgebraOfGraphics") - #Pkg.add("StatsBase") - #Pkg.add("CategoricalArrays") - - #Pkg.add("JLD2") - - #Pkg.add("CairoMakie") + using Pkg + Pkg.activate("../../devEnv") # docs + #Pkg.add("PyMNE") + #Pkg.add(path="../../../TopoPlotsjl/") + Pkg.develop(; path="../../../TopoPlotsjl/") + #Pkg.add("DataFrames") + #Pkg.add("AlgebraOfGraphics") + #Pkg.add("StatsBase") + #Pkg.add("CategoricalArrays") + + #Pkg.add("JLD2") + + #Pkg.add("CairoMakie") end # ╔═╡ c4a25915-c7f5-453a-a4f0-4b40ebedea4c using Revise # ╔═╡ 59b87673-02d2-4deb-90be-74d923d170eb - using TopoPlots - +using TopoPlots # ╔═╡ 452a245c-773a-4303-a970-f2592c3e879f begin - #using TopoPlots - #using ../../../Topoplotsjl - using CairoMakie - using DataFrames - - + #using TopoPlots + #using ../../../Topoplotsjl + using CairoMakie + using DataFrames end - # ╔═╡ 77dc1ba9-9484-485b-a49d-9aa231ef4983 using Statistics @@ -56,54 +52,59 @@ revise(TopoPlots) # ╔═╡ f4b81740-d907-42ae-a0df-f46fb2f2cb15 begin + data = Array{Float32}(undef, 64, 400, 3) + #read!(TopoPlots.assetpath("example-data.bin"), data) + read!(splitdir(pathof(TopoPlots))[1] * "/../assets/example-data.bin", data) -data = Array{Float32}(undef, 64, 400, 3) -#read!(TopoPlots.assetpath("example-data.bin"), data) - read!(splitdir(pathof(TopoPlots))[1]*"/../assets/example-data.bin",data) - -positions = Vector{Point2f}(undef, 64) - read!(splitdir(pathof(TopoPlots))[1]*"/../assets/layout64.bin",positions) -#read!(TopoPlots.assetpath("layout64.bin"), positions) - -end; + positions = Vector{Point2f}(undef, 64) + read!(splitdir(pathof(TopoPlots))[1] * "/../assets/layout64.bin", positions) + #read!(TopoPlots.assetpath("layout64.bin"), positions) +end; # ╔═╡ 42f7755b-80f4-4185-8d21-42e11730e0fc begin - using Random - pos = positions[1:10] -eeg_topoplot(rand(MersenneTwister(1),length(pos)), string.(1:length(pos));positions=pos,pad_value=0.) + using Random + pos = positions[1:10] + eeg_topoplot(rand(MersenneTwister(1), length(pos)), string.(1:length(pos)); + positions=pos, pad_value=0.0) end # ╔═╡ aad784ee-6bb7-4f3c-8444-be050456ddea -eeg_topoplot(data[:, 340, 1], string.(1:length(positions));positions=positions) +eeg_topoplot(data[:, 340, 1], string.(1:length(positions)); positions=positions) # ╔═╡ 237e4f4a-cdf2-4bac-8096-de8050251745 -eeg_topoplot(data[:, 340, 1], string.(1:length(positions));positions=positions,pad_value=0.1) +eeg_topoplot(data[:, 340, 1], string.(1:length(positions)); positions=positions, + pad_value=0.1) # ╔═╡ f522329b-3653-4059-9955-8cd05570e923 -topoplot(rand(MersenneTwister(1),length(pos)),pos) +topoplot(rand(MersenneTwister(1), length(pos)), pos) # ╔═╡ a9d2a2e2-6c8c-4cfc-9fed-b5e082cb44af let -mon = PyMNE.channels.make_standard_montage("standard_1020") - -posMat = (Matrix(hcat(pos...)).-0.5).*0.5 - #pos = PyMNE.channels.make_eeg_layout(mon).pos -PyMNE.viz.plot_topomap(rand(MersenneTwister(1),length(pos)),posMat',cmap="RdBu_r",extrapolate="box",border=-1) + mon = PyMNE.channels.make_standard_montage("standard_1020") + + posMat = (Matrix(hcat(pos...)) .- 0.5) .* 0.5 + #pos = PyMNE.channels.make_eeg_layout(mon).pos + PyMNE.viz.plot_topomap(rand(MersenneTwister(1), length(pos)), posMat'; cmap="RdBu_r", + extrapolate="box", border=-1) end # ╔═╡ c358633f-8d18-4c5e-80f7-ab972e8860be Pkg.status("TopoPlots") # ╔═╡ c0a2ad2e-ccce-4e80-b52c-75f1428ed182 -e1eg_topoplot(data[:, 340, 1], string.(1:length(positions));positions=positions,interpolation = TopoPlots.NormalMixtureInterpolator() ) +e1eg_topoplot(data[:, 340, 1], string.(1:length(positions)); positions=positions, + interpolation=TopoPlots.NormalMixtureInterpolator()) # ╔═╡ d7620a42-d54c-4244-a820-d15aecdae626 -@time TopoPlots.eeg_topoplot_series(data[:,:,1],40;topoplotCfg=(positions=positions,label_scatter=false)) +@time TopoPlots.eeg_topoplot_series(data[:, :, 1], 40; + topoplotCfg=(positions=positions, label_scatter=false)) # ╔═╡ ec59c704-ae33-4a62-82ce-63acc6b17793 -f, ax, pl = TopoPlots.eeg_topoplot(1:length(TopoPlots.CHANNELS_10_20),TopoPlots.CHANNELS_10_20; interpolation=TopoPlots.NullInterpolator(),) +f, ax, pl = TopoPlots.eeg_topoplot(1:length(TopoPlots.CHANNELS_10_20), + TopoPlots.CHANNELS_10_20; + interpolation=TopoPlots.NullInterpolator(),) # ╔═╡ f3d1f3cc-f7c9-4ef4-ba4f-3d32f2509cad let @@ -124,36 +125,36 @@ let minx, miny = minimum(rect) maxx, maxy = maximum(rect) # recreate the coordinates of the data - x = range(minx, maxx, length=size(m, 1)) - y = range(miny, maxy, length=size(m, 2)) + x = range(minx, maxx; length=size(m, 1)) + y = range(miny, maxy; length=size(m, 2)) xys = Point2f.(x, y') # find the highest point _, i = findmax(x -> isnan(x) ? -Inf : x, m) xy = xys[i] - @show peak_xy - @show xy + @show peak_xy + @show xy #@test isapprox(xy, peak_xy; atol=0.02) - @show isapprox(xy, peak_xy; atol=0.02) - fig + @show isapprox(xy, peak_xy; atol=0.02) + fig end # ╔═╡ 872ac6a4-ddaa-4dfb-a40d-9d5ea55bdb3d let - f = Figure() - axis = Axis(f[1, 1], aspect = 1) - xlims!(low = -2, high = 2) - ylims!(low = -2, high = 2) - - data = [0, 0, 0] - pos1 = [Point2f(-1, -1), Point2f(-1.0, 0.0), Point2f(0, -1)] - pos2 = [Point2f(1, 1), Point2f(1.0, 0.0), Point2f(0, 1)] - - pos1 = pos1 .- mean(pos1) - pos2 = pos2 .- mean(pos2) - eeg_topoplot!(axis, data, positions=pos1) - eeg_topoplot!(axis, data, positions=pos2) - f + f = Figure() + axis = Axis(f[1, 1]; aspect=1) + xlims!(; low=-2, high=2) + ylims!(; low=-2, high=2) + + data = [0, 0, 0] + pos1 = [Point2f(-1, -1), Point2f(-1.0, 0.0), Point2f(0, -1)] + pos2 = [Point2f(1, 1), Point2f(1.0, 0.0), Point2f(0, 1)] + + pos1 = pos1 .- mean(pos1) + pos2 = pos2 .- mean(pos2) + eeg_topoplot!(axis, data; positions=pos1) + eeg_topoplot!(axis, data; positions=pos2) + f end # ╔═╡ Cell order: diff --git a/src/TopoPlots.jl b/src/TopoPlots.jl index 45efce5..a9c0de3 100644 --- a/src/TopoPlots.jl +++ b/src/TopoPlots.jl @@ -24,7 +24,7 @@ Load EEG example data. Returns a two-tuple: - data: a (64, 400, 3) Float32 array of channel x timepoint x stat array. - Timepoints correponds to samples at 500Hz from -0.3s to 0.5s relative to stimulus onset. + Timepoints corresponds to samples at 500Hz from -0.3s to 0.5s relative to stimulus onset. Stats are mean over subjects, standard errors over subjects, and associated p-value from a t-test. For demonstration purposes, the first stat dimension is generally the most applicable. - positions: a length-64 Point2f vector of positions for each channel in data. @@ -51,7 +51,8 @@ include("core-recipe.jl") include("eeg.jl") # Interpolators -export CloughTocher, SplineInterpolator, DelaunayMesh, NullInterpolator, ScatteredInterpolationMethod, NaturalNeighboursMethod +export CloughTocher, SplineInterpolator, DelaunayMesh, NullInterpolator, + ScatteredInterpolationMethod, NaturalNeighboursMethod @deprecate ClaughTochter(args...; kwargs...) CloughTocher(args...; kwargs...) true # Extrapolators export GeomExtrapolation, NullExtrapolation @@ -63,8 +64,8 @@ export GeomExtrapolation, NullExtrapolation @compile_workload begin # all calls in this block will be precompiled, regardless of whether # they belong to your package or not (on Julia 1.8 and higher) - eeg_topoplot(view(data, :, 340, 1); positions) - eeg_topoplot(data[:, 340, 1]; positions) + eeg_topoplot(view(data, :, 340, 1); positions) + eeg_topoplot(data[:, 340, 1]; positions) end end diff --git a/src/core-recipe.jl b/src/core-recipe.jl index d6a21de..5f9b09e 100644 --- a/src/core-recipe.jl +++ b/src/core-recipe.jl @@ -1,22 +1,20 @@ @recipe(TopoPlot, data, positions) do scene - return Attributes( - colormap = Reverse(:RdBu), - colorrange = Makie.automatic, - sensors = true, - interpolation = CloughTocher(), - extrapolation = GeomExtrapolation(), - bounding_geometry = Circle, - enlarge = 1.2, - markersize = 5, - pad_value = 0.0, - interp_resolution = (512, 512), - labels = nothing, - label_text = false, - label_scatter = false, - contours = false, - plotfnc! = heatmap!, - plotfnc_kwargs_names= [:colorrange, :colormap, :interpolate], - ) + return Attributes(; colormap=Reverse(:RdBu), + colorrange=Makie.automatic, + sensors=true, + interpolation=CloughTocher(), + extrapolation=GeomExtrapolation(), + bounding_geometry=Circle, + enlarge=1.2, + markersize=5, + pad_value=0.0, + interp_resolution=(512, 512), + labels=nothing, + label_text=false, + label_scatter=false, + contours=false, + (plotfnc!)=heatmap!, + plotfnc_kwargs_names=[:colorrange, :colormap, :interpolate]) end """ @@ -62,9 +60,9 @@ plot_or_defaults(value::Bool, defaults, name) = value ? defaults : nothing plot_or_defaults(value::Attributes, defaults, name) = merge(value, defaults) function plot_or_defaults(value, defaults, name) - error("Attribute $(name) has the wrong type: $(typeof(value)). - Use either a bool to enable/disable plotting with default attributes, - or a NamedTuple with attributes getting passed down to the plot command.") + return error("Attribute $(name) has the wrong type: $(typeof(value)). + Use either a bool to enable/disable plotting with default attributes, + or a NamedTuple with attributes getting passed down to the plot command.") end macro plot_or_defaults(var, defaults) @@ -77,10 +75,11 @@ function Makie.plot!(p::TopoPlot) # positions changes with with data together since it gets into convert_arguments positions = lift(identity, p, p.positions; ignore_equal_values=true) - geometry = lift(enclosing_geometry, p, p.bounding_geometry, positions, p.enlarge; ignore_equal_values=true) + geometry = lift(enclosing_geometry, p, p.bounding_geometry, positions, p.enlarge; + ignore_equal_values=true) - xg = Obs(LinRange(0f0, 1f0, p.interp_resolution[][1])) - yg = Obs(LinRange(0f0, 1f0, p.interp_resolution[][2])) + xg = Obs(LinRange(0.0f0, 1.0f0, p.interp_resolution[][1])) + yg = Obs(LinRange(0.0f0, 1.0f0, p.interp_resolution[][2])) f = onany(p, geometry, p.interp_resolution) do geometry, interp_resolution (xmin, ymin), (xmax, ymax) = extrema(geometry) @@ -93,7 +92,8 @@ function Makie.plot!(p::TopoPlot) p.geometry = geometry # store geometry in plot object, so others can access it - padded_pos_data_bb = lift(p, p.extrapolation, p.positions, p.data) do extrapolation, positions, data + padded_pos_data_bb = lift(p, p.extrapolation, p.positions, + p.data) do extrapolation, positions, data return extrapolation(positions, data) end @@ -108,29 +108,40 @@ function Makie.plot!(p::TopoPlot) if p.interpolation[] isa DelaunayMesh # TODO, delaunay works very differently from the other interpolators, so we can't switch interactively between them m = lift(delaunay_mesh, p, p.positions) - mesh!(p, m, color=p.data, colorrange=colorrange, colormap=p.colormap, shading=NoShading) + mesh!(p, m; color=p.data, colorrange=colorrange, colormap=p.colormap, + shading=NoShading) else - mask = lift(p, xg, yg, geometry) do xg,yg,geometry + mask = lift(p, xg, yg, geometry) do xg, yg, geometry pts = Point2f.(xg' .* ones(length(yg)), ones(length(xg))' .* yg) return in.(pts, Ref(geometry)) end - data = lift(p, p.interpolation, xg, yg, padded_pos_data_bb,mask) do interpolation, xg, yg, (points, data, _, _),mask - z = interpolation(xg, yg, points, data;mask=mask) -# z[mask] .= NaN + data = lift(p, p.interpolation, xg, yg, padded_pos_data_bb, + mask) do interpolation, xg, yg, (points, data, _, _), mask + z = interpolation(xg, yg, points, data; mask=mask) + # z[mask] .= NaN return z end - kwargs_all = Dict(:colorrange => colorrange, :colormap => p.colormap, :interpolate => true) + kwargs_all = Dict(:colorrange => colorrange, :colormap => p.colormap, + :interpolate => true) - p.plotfnc![](p, xg, yg, data; (p.plotfnc_kwargs_names[].=>getindex.(Ref(kwargs_all),p.plotfnc_kwargs_names[]))...) + p.plotfnc![](p, xg, yg, data; + (p.plotfnc_kwargs_names[] .=> + getindex.(Ref(kwargs_all), p.plotfnc_kwargs_names[]))...) contours = to_value(p.contours) - attributes = @plot_or_defaults contours Attributes(color=(:black, 0.5), linestyle=:dot, levels=6) + attributes = @plot_or_defaults contours Attributes(color=(:black, 0.5), + linestyle=:dot, levels=6) if !isnothing(attributes) && !(p.interpolation[] isa NullInterpolator) contour!(p, xg, yg, data; attributes...) end end label_scatter = to_value(p.label_scatter) - attributes = @plot_or_defaults label_scatter Attributes(markersize=p.markersize, color=p.data, colormap=p.colormap, colorrange=colorrange, strokecolor=:black, strokewidth=1) + attributes = @plot_or_defaults label_scatter Attributes(markersize=p.markersize, + color=p.data, + colormap=p.colormap, + colorrange=colorrange, + strokecolor=:black, + strokewidth=1) if !isnothing(attributes) scatter!(p, p.positions; attributes...) end @@ -138,7 +149,7 @@ function Makie.plot!(p::TopoPlot) label_text = to_value(p.label_text) attributes = @plot_or_defaults label_text Attributes(align=(:right, :top)) if !isnothing(attributes) - text!(p, p.positions, text=p.labels; attributes...) + text!(p, p.positions; text=p.labels, attributes...) end end return diff --git a/src/eeg.jl b/src/eeg.jl index c5bcb08..ab4d582 100644 --- a/src/eeg.jl +++ b/src/eeg.jl @@ -1,15 +1,14 @@ @recipe(EEG_TopoPlot, data) do scene return Attributes(; - head=(color=:black, linewidth=3), - labels=Makie.automatic, - positions = Makie.automatic, - # overwrite some topoplot defaults - default_theme(scene, TopoPlot)..., - label_scatter=true, - contours=true, - enlarge=1, - ) + head=(color=:black, linewidth=3), + labels=Makie.automatic, + positions=Makie.automatic, + # overwrite some topoplot defaults + default_theme(scene, TopoPlot)..., + label_scatter=true, + contours=true, + enlarge=1,) end """ @@ -35,8 +34,10 @@ Otherwise the recipe just uses the [`topoplot`](@ref) defaults and passes throug """ eeg_topoplot -@deprecate eeg_topoplot(data::AbstractVector{<:Real}, labels::Vector{<:AbstractString}) eeg_topoplot(data; labels) -@deprecate eeg_topoplot!(fig, data::AbstractVector{<:Real}, labels::Vector{<:AbstractString}) eeg_topoplot!(fig, data; labels) +@deprecate eeg_topoplot(data::AbstractVector{<:Real}, labels::Vector{<:AbstractString}) eeg_topoplot(data; + labels) +@deprecate eeg_topoplot!(fig, data::AbstractVector{<:Real}, + labels::Vector{<:AbstractString}) eeg_topoplot!(fig, data; labels) function draw_ear_nose!(parent, circle; kw...) # draw circle @@ -47,11 +48,10 @@ function draw_ear_nose!(parent, circle; kw...) nose = (Point2f[(-0.05, 0.5), (0.0, 0.55), (0.05, 0.5)] .* diameter) .+ (middle,) push!(points, Point2f(NaN)) append!(points, nose) - ear = (Point2f[ - (0.497, 0.0555), (0.51, 0.0775), (0.518, 0.0783), - (0.5299, 0.0746), (0.5419, 0.0555), (0.54, -0.0055), - (0.547, -0.0932), (0.532, -0.1313), (0.51, -0.1384), - (0.489, -0.1199)] .* diameter) + ear = (Point2f[(0.497, 0.0555), (0.51, 0.0775), (0.518, 0.0783), + (0.5299, 0.0746), (0.5419, 0.0555), (0.54, -0.0055), + (0.547, -0.0932), (0.532, -0.1313), (0.51, -0.1384), + (0.489, -0.1199)] .* diameter) push!(points, Point2f(NaN)) append!(points, ear .+ middle) @@ -60,51 +60,60 @@ function draw_ear_nose!(parent, circle; kw...) return points end - lines!(parent, head_points; kw...) - + return lines!(parent, head_points; kw...) end - -const CHANNELS_10_05 = ["af1","af10","af10h","af1h","af2","af2h","af3","af3h", - "af4","af4h","af5","af5h","af6","af6h","af7","af7h","af8", - "af8h","af9","af9h","aff1","aff10","aff10h","aff1h","aff2", - "aff2h","aff3","aff3h","aff4","aff4h","aff5","aff5h","aff6", - "aff6h","aff7","aff7h","aff8","aff8h","aff9","aff9h","affz", - "afp1","afp10","afp10h","afp1h","afp2","afp2h","afp3","afp3h", - "afp4","afp4h","afp5","afp5h","afp6","afp6h","afp7","afp7h", - "afp8","afp8h","afp9","afp9h","afpz","afz","c1","c1h","c2", - "c2h","c3","c3h","c4","c4h","c5","c5h","c6","c6h","ccp1", - "ccp1h","ccp2","ccp2h","ccp3","ccp3h","ccp4","ccp4h","ccp5", - "ccp5h","ccp6","ccp6h","ccpz","cp1","cp1h","cp2","cp2h","cp3", - "cp3h","cp4","cp4h","cp5","cp5h","cp6","cp6h","cpp1","cpp1h", - "cpp2","cpp2h","cpp3","cpp3h","cpp4","cpp4h","cpp5","cpp5h", - "cpp6","cpp6h","cppz","cpz","cz","f1","f10","f10h","f1h", - "f2","f2h","f3","f3h","f4","f4h","f5","f5h","f6","f6h", - "f7","f7h","f8","f8h","f9","f9h","fc1","fc1h","fc2","fc2h", - "fc3","fc3h","fc4","fc4h","fc5","fc5h","fc6","fc6h","fcc1", - "fcc1h","fcc2","fcc2h","fcc3","fcc3h","fcc4","fcc4h","fcc5", - "fcc5h","fcc6","fcc6h","fccz","fcz","ffc1","ffc1h","ffc2", - "ffc2h","ffc3","ffc3h","ffc4","ffc4h","ffc5","ffc5h","ffc6", - "ffc6h","ffcz","fft10","fft10h","fft7","fft7h","fft8","fft8h", - "fft9","fft9h","ft10","ft10h","ft7","ft7h","ft8","ft8h","ft9", - "ft9h","ftt10","ftt10h","ftt7","ftt7h","ftt8","ftt8h","ftt9", - "ftt9h","fp1","fp1h","fp2","fp2h","fpz","fz","i1","i1h","i2", - "i2h","iz","lpa","n1","n1h","n2","n2h","nas","nfp1","nfp1h", - "nfp2","nfp2h","nfpz","nz","o1","o1h","o2","o2h","oi1","oi1h", - "oi2","oi2h","oiz","oz","p1","p10","p10h","p1h","p2","p2h", - "p3","p3h","p4","p4h","p5","p5h","p6","p6h","p7","p7h","p8", - "p8h","p9","p9h","po1","po10","po10h","po1h","po2","po2h", - "po3","po3h","po4","po4h","po5","po5h","po6","po6h","po7", - "po7h","po8","po8h","po9","po9h","poo1","poo10","poo10h", - "poo1h","poo2","poo2h","poo3","poo3h","poo4","poo4h","poo5", - "poo5h","poo6","poo6h","poo7","poo7h","poo8","poo8h","poo9", - "poo9h","pooz","poz","ppo1","ppo10","ppo10h","ppo1h","ppo2", - "ppo2h","ppo3","ppo3h","ppo4","ppo4h","ppo5","ppo5h","ppo6", - "ppo6h","ppo7","ppo7h","ppo8","ppo8h","ppo9","ppo9h","ppoz", - "pz","rpa","t10","t10h","t7","t7h","t8","t8h","t9","t9h", - "tp10","tp10h","tp7","tp7h","tp8","tp8h","tp9","tp9h","tpp10", - "tpp10h","tpp7","tpp7h","tpp8","tpp8h","tpp9","tpp9h","ttp10", - "ttp10h","ttp7","ttp7h","ttp8","ttp8h","ttp9","ttp9h"] +const CHANNELS_10_05 = ["af1", "af10", "af10h", "af1h", "af2", "af2h", "af3", "af3h", + "af4", "af4h", "af5", "af5h", "af6", "af6h", "af7", "af7h", "af8", + "af8h", "af9", "af9h", "aff1", "aff10", "aff10h", "aff1h", "aff2", + "aff2h", "aff3", "aff3h", "aff4", "aff4h", "aff5", "aff5h", "aff6", + "aff6h", "aff7", "aff7h", "aff8", "aff8h", "aff9", "aff9h", "affz", + "afp1", "afp10", "afp10h", "afp1h", "afp2", "afp2h", "afp3", + "afp3h", + "afp4", "afp4h", "afp5", "afp5h", "afp6", "afp6h", "afp7", "afp7h", + "afp8", "afp8h", "afp9", "afp9h", "afpz", "afz", "c1", "c1h", "c2", + "c2h", "c3", "c3h", "c4", "c4h", "c5", "c5h", "c6", "c6h", "ccp1", + "ccp1h", "ccp2", "ccp2h", "ccp3", "ccp3h", "ccp4", "ccp4h", "ccp5", + "ccp5h", "ccp6", "ccp6h", "ccpz", "cp1", "cp1h", "cp2", "cp2h", + "cp3", + "cp3h", "cp4", "cp4h", "cp5", "cp5h", "cp6", "cp6h", "cpp1", + "cpp1h", + "cpp2", "cpp2h", "cpp3", "cpp3h", "cpp4", "cpp4h", "cpp5", "cpp5h", + "cpp6", "cpp6h", "cppz", "cpz", "cz", "f1", "f10", "f10h", "f1h", + "f2", "f2h", "f3", "f3h", "f4", "f4h", "f5", "f5h", "f6", "f6h", + "f7", "f7h", "f8", "f8h", "f9", "f9h", "fc1", "fc1h", "fc2", "fc2h", + "fc3", "fc3h", "fc4", "fc4h", "fc5", "fc5h", "fc6", "fc6h", "fcc1", + "fcc1h", "fcc2", "fcc2h", "fcc3", "fcc3h", "fcc4", "fcc4h", "fcc5", + "fcc5h", "fcc6", "fcc6h", "fccz", "fcz", "ffc1", "ffc1h", "ffc2", + "ffc2h", "ffc3", "ffc3h", "ffc4", "ffc4h", "ffc5", "ffc5h", "ffc6", + "ffc6h", "ffcz", "fft10", "fft10h", "fft7", "fft7h", "fft8", + "fft8h", + "fft9", "fft9h", "ft10", "ft10h", "ft7", "ft7h", "ft8", "ft8h", + "ft9", + "ft9h", "ftt10", "ftt10h", "ftt7", "ftt7h", "ftt8", "ftt8h", "ftt9", + "ftt9h", "fp1", "fp1h", "fp2", "fp2h", "fpz", "fz", "i1", "i1h", + "i2", + "i2h", "iz", "lpa", "n1", "n1h", "n2", "n2h", "nas", "nfp1", + "nfp1h", + "nfp2", "nfp2h", "nfpz", "nz", "o1", "o1h", "o2", "o2h", "oi1", + "oi1h", + "oi2", "oi2h", "oiz", "oz", "p1", "p10", "p10h", "p1h", "p2", "p2h", + "p3", "p3h", "p4", "p4h", "p5", "p5h", "p6", "p6h", "p7", "p7h", + "p8", + "p8h", "p9", "p9h", "po1", "po10", "po10h", "po1h", "po2", "po2h", + "po3", "po3h", "po4", "po4h", "po5", "po5h", "po6", "po6h", "po7", + "po7h", "po8", "po8h", "po9", "po9h", "poo1", "poo10", "poo10h", + "poo1h", "poo2", "poo2h", "poo3", "poo3h", "poo4", "poo4h", "poo5", + "poo5h", "poo6", "poo6h", "poo7", "poo7h", "poo8", "poo8h", "poo9", + "poo9h", "pooz", "poz", "ppo1", "ppo10", "ppo10h", "ppo1h", "ppo2", + "ppo2h", "ppo3", "ppo3h", "ppo4", "ppo4h", "ppo5", "ppo5h", "ppo6", + "ppo6h", "ppo7", "ppo7h", "ppo8", "ppo8h", "ppo9", "ppo9h", "ppoz", + "pz", "rpa", "t10", "t10h", "t7", "t7h", "t8", "t8h", "t9", "t9h", + "tp10", "tp10h", "tp7", "tp7h", "tp8", "tp8h", "tp9", "tp9h", + "tpp10", + "tpp10h", "tpp7", "tpp7h", "tpp8", "tpp8h", "tpp9", "tpp9h", + "ttp10", + "ttp10h", "ttp7", "ttp7h", "ttp8", "ttp8h", "ttp9", "ttp9h"] """ CHANNEL_TO_POSITION_10_05 @@ -136,7 +145,8 @@ const CHANNEL_TO_POSITION_10_05 = let end # even though these are not actively used, sometimes they can be helpful just to plot a default subset of channels. Therefore we havent deleted them yet (because 10_05 is a superset) -const CHANNELS_10_20 = ["fp1", "f3", "c3", "p3", "o1", "f7", "t3", "t5", "fz", "cz", "pz", "fp2", "f4", "c4", "p4", "o2", "f8", "t4", "t6"] +const CHANNELS_10_20 = ["fp1", "f3", "c3", "p3", "o1", "f7", "t3", "t5", "fz", "cz", "pz", + "fp2", "f4", "c4", "p4", "o2", "f8", "t4", "t6"] """ CHANNEL_TO_POSITION_10_20 @@ -180,15 +190,14 @@ end #function Makie.convert_arguments(::Type{<:EEG_TopoPlot}, data::AbstractVector{<:Real}) # return (data, labels2positions(labels))# - # +# #end function Makie.plot!(plot::EEG_TopoPlot) - positions = lift(plot.labels, plot.positions) do labels, positions - if positions isa Makie.Automatic - (!isnothing(labels) && labels != Makie.Automatic) || error("Either positions or labels (10/05-lookup) have to be specified") + (!isnothing(labels) && labels != Makie.Automatic) || + error("Either positions or labels (10/05-lookup) have to be specified") return labels2positions(labels) else @@ -197,9 +206,8 @@ function Makie.plot!(plot::EEG_TopoPlot) end end plot.labels = lift(plot.labels, plot.positions) do labels, positions - if isnothing(labels) || labels isa Makie.Automatic - return ["sensor $i" for i in 1:length(positions)] + return ["sensor $i" for i in 1:length(positions)] else return labels end diff --git a/src/extrapolation.jl b/src/extrapolation.jl index 58d1450..0aef844 100644 --- a/src/extrapolation.jl +++ b/src/extrapolation.jl @@ -7,7 +7,7 @@ The Geometry can be enlarged by 1.x, so e.g. `enclosing_geometry(Circle, positio """ function enclosing_geometry(::Type{Circle}, positions, enlarge=1.0) middle = mean(positions) - radius, idx = findmax(x-> norm(x .- middle), positions) + radius, idx = findmax(x -> norm(x .- middle), positions) return Circle(middle, radius * enlarge) end @@ -26,13 +26,12 @@ function enclosing_geometry(geometry::GeometryPrimitive, positions, enlarge=1.0) end function enclosing_geometry(type, positions, enlarge=1.0) - error("Wrong type for `bounding_geometry`: $(type)") + return error("Wrong type for `bounding_geometry`: $(type)") end points2mat(points) = vcat(first.(points)', last.(points)') mat2points(mat) = Point2f.(view(mat, 1, :), view(mat, 2, :)) - """ GeomExtrapolation( method = Shepard(), # extrapolation method diff --git a/src/interpolators.jl b/src/interpolators.jl index 6af7090..f27bad6 100644 --- a/src/interpolators.jl +++ b/src/interpolators.jl @@ -21,27 +21,27 @@ This is the default interpolator in MNE-Python. rescale::Bool = false end -function (ct::CloughTocher)( - xrange::LinRange, yrange::LinRange, - positions::AbstractVector{<: Point{2}}, data::AbstractVector{<:Number};mask=nothing) +function (ct::CloughTocher)(xrange::LinRange, yrange::LinRange, + positions::AbstractVector{<:Point{2}}, + data::AbstractVector{<:Number}; mask=nothing) + posMat = Float64.(vcat([[p[1], p[2]] for p in positions]...)) + interp = CloughTocher2DInterpolation.CloughTocher2DInterpolator(posMat, data; + tol=ct.tol, + maxiter=ct.maxiter, + rescale=ct.rescale) - posMat = Float64.(vcat([[p[1],p[2]] for p in positions]...)) - interp = CloughTocher2DInterpolation.CloughTocher2DInterpolator(posMat, data,tol=ct.tol, maxiter=ct.maxiter, rescale=ct.rescale) - - out = fill(NaN,size(mask)) + out = fill(NaN, size(mask)) x = (xrange)' .* ones(length(yrange)) - y = ones(length(xrange))' .* (yrange) - + y = ones(length(xrange))' .* (yrange) - icoords = hcat(x[:],y[:])' + icoords = hcat(x[:], y[:])' if isnothing(mask) out .= interp(icoords) else - out[mask[:]] .= interp(icoords[:,mask[:]]) + out[mask[:]] .= interp(icoords[:, mask[:]]) end return out' - end """ @@ -56,9 +56,9 @@ Uses [Dierckx.Spline2D](https://github.com/kbarbary/Dierckx.jl#2-d-splines) for smoothing::Float64 = 0.5 end -function (sp::SplineInterpolator)( - xrange::LinRange, yrange::LinRange, - positions::AbstractVector{<:Point{2}}, data::AbstractVector{<:Number}; mask=nothing) +function (sp::SplineInterpolator)(xrange::LinRange, yrange::LinRange, + positions::AbstractVector{<:Point{2}}, + data::AbstractVector{<:Number}; mask=nothing) # calculate 2D spline (Dierckx) # get extrema and extend by 20% x, y = first.(positions), last.(positions) @@ -71,9 +71,8 @@ function (sp::SplineInterpolator)( return out end - -# TODO how to properly integrade delauny with the interpolation interface, -# if the actualy interpolation happens inside the plotting framework (or even on the GPU for (W)GLMakie). +# TODO how to properly integrated delauny with the interpolation interface, +# if the actually interpolation happens inside the plotting framework (or even on the GPU for (W)GLMakie). """ DelaunayMesh() @@ -92,18 +91,16 @@ Really fast interpolation that happens on the GPU (for GLMakie), so optimal for struct DelaunayMesh <: Interpolator end -(::DelaunayMesh)(positions::AbstractVector{<: Point{2}}) = delaunay_mesh(positions) - -function delaunay_mesh(positions::AbstractVector{<: Point{2}}) +(::DelaunayMesh)(positions::AbstractVector{<:Point{2}}) = delaunay_mesh(positions) +function delaunay_mesh(positions::AbstractVector{<:Point{2}}) t = triangulate(positions) # triangulate them! # - simp = Int.(collect(reshape(t._triangles,3,:)')) + simp = Int.(collect(reshape(t._triangles, 3, :)')) m = GeometryBasics.Mesh(Makie.to_vertices(positions), Makie.to_triangles(simp)) return m end - """ ScatteredInterpolationMethod(InterpolationMethod) @@ -114,9 +111,9 @@ E.g. ScatteredInterpolationMethod(Shepard(P=4)) method::ScatteredInterpolation.InterpolationMethod = Shepard(4) end -function (sim::ScatteredInterpolationMethod)( - xrange::LinRange, yrange::LinRange, - positions::AbstractVector{<:Point{2}}, data::AbstractVector{<:Number}; mask=nothing) +function (sim::ScatteredInterpolationMethod)(xrange::LinRange, yrange::LinRange, + positions::AbstractVector{<:Point{2}}, + data::AbstractVector{<:Number}; mask=nothing) n = length(xrange) X = repeat(xrange, n)[:] Y = repeat(yrange', n)[:] @@ -130,7 +127,6 @@ function (sim::ScatteredInterpolationMethod)( gridded[.!mask] .= NaN end return gridded - end """ @@ -143,7 +139,7 @@ a variety of methods like `Sibson(::Int)`, `Nearest()`, and `Triangle()`. The advantage of Voronoi-diagram based methods is that they are more robust to irregularly distributed datasets and some discontinuities, which may throw off -some polynomial based methods as well as independant distance weighting (kriging). +some polynomial based methods as well as independent distance weighting (kriging). See [this Discourse post](https://discourse.julialang.org/t/ann-naturalneighbours-jl-natural-neighbour-interpolation-and-derivative-generation/99164/11) for more information on why NaturalNeighbours are cool! To access the methods easily, you should run `using NearestNeighbours`. @@ -180,30 +176,26 @@ function NaturalNeighboursMethod(method::NaturalNeighbours.AbstractInterpolator) return NaturalNeighboursMethod(; method) end -function (alg::NaturalNeighboursMethod)( - xrange::AbstractRange, yrange::AbstractRange, - positions::AbstractVector{<:Point{2}}, - data::AbstractVector{<:Number}; - mask=nothing - ) +function (alg::NaturalNeighboursMethod)(xrange::AbstractRange, yrange::AbstractRange, + positions::AbstractVector{<:Point{2}}, + data::AbstractVector{<:Number}; + mask=nothing) @assert length(positions) == length(data) "Positions (length $(length(positions))) and data (length $(length(data))) must have the same length." # First, create the triangulated interpolator - interpolator = NaturalNeighbours.interpolate( - first.(positions), last.(positions), data; - derivatives = true, method = alg.derivative_method, - use_cubic_terms = alg.use_cubic_terms, alpha = alg.alpha, - parallel = alg.parallel, - ) + interpolator = NaturalNeighbours.interpolate(first.(positions), last.(positions), data; + derivatives=true, + method=alg.derivative_method, + use_cubic_terms=alg.use_cubic_terms, + alpha=alg.alpha, + parallel=alg.parallel,) # Then, interpolate the data at the grid points. nx, ny = length(xrange), length(yrange) x_values = vec([x for x in xrange, _ in yrange]) y_values = vec([y for _ in xrange, y in yrange]) - z_values = interpolator( - x_values, y_values; - method = alg.method, - parallel = alg.parallel, - project = alg.project, - ) + z_values = interpolator(x_values, y_values; + method=alg.method, + parallel=alg.parallel, + project=alg.project,) # Reshape the data into a matrix. gridded = reshape(z_values, nx, ny) # Mask off if necessary. @@ -219,11 +211,10 @@ end Interpolator that returns "0", which is useful to display only the electrode locations + labels """ struct NullInterpolator <: TopoPlots.Interpolator - end -function (ni::NullInterpolator)( - xrange::LinRange, yrange::LinRange, - positions::AbstractVector{<:Point{2}}, data::AbstractVector{<:Number}; mask=nothing) +function (ni::NullInterpolator)(xrange::LinRange, yrange::LinRange, + positions::AbstractVector{<:Point{2}}, + data::AbstractVector{<:Number}; mask=nothing) return fill(NaN, length(xrange), length(yrange)) end diff --git a/test/CondaPkg.toml b/test/CondaPkg.toml index 3cac7b0..1dc5c64 100644 --- a/test/CondaPkg.toml +++ b/test/CondaPkg.toml @@ -2,7 +2,7 @@ channels = ["anaconda", "conda-forge"] [deps] matplotlib = "" -python = ">=3.7, <3.13" # we get segaults on 3.13 on Apple Silicon +python = ">=3.7, <3.13" # we get segfaults on 3.13 on Apple Silicon scipy = ">=1.9" [pip.deps] diff --git a/test/nb_example.jl b/test/nb_example.jl index 217c7e8..7b01e76 100644 --- a/test/nb_example.jl +++ b/test/nb_example.jl @@ -5,74 +5,74 @@ using Markdown using InteractiveUtils # ╔═╡ 31cea56f-f6d1-4412-a4b3-14bfe714491e - begin - using Pkg - Pkg.activate(".") - end +begin + using Pkg + Pkg.activate(".") +end # ╔═╡ f49917ee-d5de-11ec-2e00-315d3bbbf4fc # ╠═╡ show_logs = false begin - - Pkg.add(["JLD2","CairoMakie","PyMNE","PyPlot"]) - #Pkg.add(url="https://github.com/unfoldtoolbox/UnfoldMakie.jl",rev="topoplot") - + Pkg.add(["JLD2", "CairoMakie", "PyMNE", "PyPlot"]) + #Pkg.add(url="https://github.com/unfoldtoolbox/UnfoldMakie.jl",rev="topoplot") end # ╔═╡ dcdd7a47-fd6c-4ec6-ac18-dd8fc07924bf begin - using JLD2 - using CairoMakie - using UnfoldMakie - using PyMNE - using PyCall + using JLD2 + using CairoMakie + using UnfoldMakie + using PyMNE + using PyCall end # ╔═╡ 1ba342d1-ee1d-4a32-b750-4901b57c8df6 begin - data_dict = load(joinpath(@__DIR__, "example.jld2")) - pos = data_dict["pos"] - data = data_dict["data"] + data_dict = load(joinpath(@__DIR__, "example.jld2")) + pos = data_dict["pos"] + data = data_dict["data"] end; # ╔═╡ 17e7a862-ac44-4802-9449-a98894657ad0 - begin # convert x/y to mne position - info = pycall(PyMNE.io.meas_info.create_info, PyObject,string.(1:size(pos,2)), sfreq=256, ch_types="eeg") # fake info - raw = PyMNE.io.RawArray(data[:,:,1], info) # fake raw with fake info - chname_pos_dict = Dict(string.(1:size(pos,2)) .=> [pos[:,p] for p in 1:size(pos,2)]) - montage = PyMNE.channels.make_dig_montage(ch_pos=chname_pos_dict,coord_frame="head") +begin # convert x/y to mne position + info = pycall(PyMNE.io.meas_info.create_info, PyObject, string.(1:size(pos, 2)); + sfreq=256, ch_types="eeg") # fake info + raw = PyMNE.io.RawArray(data[:, :, 1], info) # fake raw with fake info + chname_pos_dict = Dict(string.(1:size(pos, 2)) .=> [pos[:, p] for p in 1:size(pos, 2)]) + montage = PyMNE.channels.make_dig_montage(; ch_pos=chname_pos_dict, coord_frame="head") raw.set_montage(montage) # set montage (why??) - layout_from_raw = PyMNE.channels.make_eeg_layout(get_info(raw)) - pos2 = layout_from_raw.pos - - end; + layout_from_raw = PyMNE.channels.make_eeg_layout(get_info(raw)) + pos2 = layout_from_raw.pos +end; # ╔═╡ a5dc2801-9fa8-4193-9c1e-254bd692c1b7 begin -CairoMakie.scatter(pos[1,:]*0.05 .+1.5,pos[2,:]*0.05.+0.5,color="blue") # no project -CairoMakie.scatter!(pos2[:,1],pos2[:,2],color="red") # projection -JLD2.save(joinpath(@__DIR__, "example.jld2"), Dict("data"=>data, "pos" => pos, "pos2" => pos2)) + CairoMakie.scatter(pos[1, :] * 0.05 .+ 1.5, pos[2, :] * 0.05 .+ 0.5; color="blue") # no project + CairoMakie.scatter!(pos2[:, 1], pos2[:, 2]; color="red") # projection + JLD2.save(joinpath(@__DIR__, "example.jld2"), + Dict("data" => data, "pos" => pos, "pos2" => pos2)) end # ╔═╡ 91da8540-5cda-43b3-8bc1-72d9ede1fcc9 begin - times = range(-0.3000,stop=0.4980,step=1/500) - lines(data[1,:,1]) # channel x time x [estimate,std,pvalue] + times = range(-0.3000; stop=0.4980, step=1 / 500) + lines(data[1, :, 1]) # channel x time x [estimate,std,pvalue] end # ╔═╡ 5edd1570-e6cd-4eeb-ae1b-bcc0d0901e47 begin - f = CairoMakie.Figure() - f[1,1] = Axis(f) - ix = argmin(abs.(times .- .400)) - plot_topoplot(f[1,1],data[:,ix,1],pos2,labels=string.(1:size(pos,2))) - f + f = CairoMakie.Figure() + f[1, 1] = Axis(f) + ix = argmin(abs.(times .- 0.400)) + plot_topoplot(f[1, 1], data[:, ix, 1], pos2; labels=string.(1:size(pos, 2))) + f end # ╔═╡ c29e489b-bc21-4f17-b676-a26c3788cad1 -PyMNE.viz.plot_topomap(data[:,ix,1],get_info(raw),names=string.(1:size(pos,2)),show_names=true, vmin=-0.7, vmax=0.7) +PyMNE.viz.plot_topomap(data[:, ix, 1], get_info(raw); names=string.(1:size(pos, 2)), + show_names=true, vmin=-0.7, vmax=0.7) # ╔═╡ Cell order: # ╠═31cea56f-f6d1-4412-a4b3-14bfe714491e diff --git a/test/runtests.jl b/test/runtests.jl index 09e45bb..297fd8d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -12,7 +12,7 @@ catch e # Now, Conda started making problems (in a fresh CI env?!) https://github.com/MakieOrg/TopoPlots.jl/pull/20#issuecomment-1224822002 # So, lets go back to install matplotlib manually, and let mne install automatically! #run(PyCall.python_cmd(`-m pip install matplotlib`))# - error("to be adressed") + error("to be addressed") end using PyMNE @@ -32,14 +32,15 @@ function mne_topoplot(fig, data, positions) # Seems like the only way to plot positions with matching head is to norm the positions.. # Anything else leads to anarchy! positions_normed = map(positions) do pos - (pos .- circle.center) ./ (circle.r) + return (pos .- circle.center) ./ (circle.r) end x, y = first.(positions_normed), last.(positions_normed) posmat = hcat(x, y) f = PythonPlot.figure() - PyMNE.viz.plot_topomap(data, Py(posmat).to_numpy(), sphere=1.1, extrapolate="box", cmap="RdBu_r", sensors=false, contours=6) - PythonPlot.scatter(x, y, c=data, cmap="RdBu_r") - PythonPlot.savefig("pymne_plot.png", bbox_inches="tight", pad_inches = 0, dpi = 200) + PyMNE.viz.plot_topomap(data, Py(posmat).to_numpy(); sphere=1.1, extrapolate="box", + cmap="RdBu_r", sensors=false, contours=6) + PythonPlot.scatter(x, y; c=data, cmap="RdBu_r") + PythonPlot.savefig("pymne_plot.png"; bbox_inches="tight", pad_inches=0, dpi=200) img = load("pymne_plot.png") rm("pymne_plot.png") s = Axis(fig; aspect=DataAspect()) @@ -50,158 +51,160 @@ end function compare_to_mne(data, positions; kw...) f, ax, pl = TopoPlots.eeg_topoplot(data; - interpolation=CloughTocher( - fill_value = NaN, - tol = 0.001, - maxiter = 1000, - rescale = false), - positions=positions, axis=(aspect=DataAspect(),), contours=(levels=6,), - label_scatter=(markersize=10, strokewidth=0,), kw...) + interpolation=CloughTocher(; fill_value=NaN, + tol=0.001, + maxiter=1000, + rescale=false), + positions=positions, axis=(aspect=DataAspect(),), + contours=(levels=6,), + label_scatter=(markersize=10, strokewidth=0), kw...) hidedecorations!(ax) - mne_topoplot(f[1,2], data, positions) + mne_topoplot(f[1, 2], data, positions) return f end -begin - f = Makie.Figure(resolution=(1000, 1000)) +@testset "interpolations" begin + f = Makie.Figure(; resolution=(1000, 1000)) interpolators = [DelaunayMesh(), CloughTocher(), SplineInterpolator()] - s = Slider(f[:, 1], range=1:size(data, 2), startvalue=351) + s = Slider(f[:, 1]; range=1:size(data, 2), startvalue=351) data_obs = map(s.value) do idx - data[:, idx, 1] + return data[:, idx, 1] end for (i, interpolation) in enumerate(interpolators) - TopoPlots.topoplot( - f[2, 1][1, i], data_obs, positions; - contours=true, - interpolation=interpolation, labels = string.(1:length(positions)), colorrange=(-1, 1), - axis=(type=Axis, title="$interpolation", aspect=DataAspect(),)) + TopoPlots.topoplot(f[2, 1][1, i], data_obs, positions; + contours=true, + interpolation=interpolation, labels=string.(1:length(positions)), + colorrange=(-1, 1), + axis=(type=Axis, title="$interpolation", aspect=DataAspect())) end @test_figure("all-interpolations", f) end -let - f = Makie.Figure(resolution=(1000, 1000)) +@testset "ClaughTochter" begin + f = Makie.Figure(; resolution=(1000, 1000)) @test_deprecated interpolation = ClaughTochter() f, ax, pl = TopoPlots.eeg_topoplot(1:length(TopoPlots.CHANNELS_10_20); - labels=TopoPlots.CHANNELS_10_20, interpolation) + labels=TopoPlots.CHANNELS_10_20, interpolation) @test_figure("ClaughTochter", f) end -begin # empty eeg topoplot - f, ax, pl = TopoPlots.eeg_topoplot(1:length(TopoPlots.CHANNELS_10_20); labels=TopoPlots.CHANNELS_10_20, interpolation=TopoPlots.NullInterpolator(),) +@testset "null interpolator" begin # empty eeg topoplot + f, ax, pl = TopoPlots.eeg_topoplot(1:length(TopoPlots.CHANNELS_10_20); + labels=TopoPlots.CHANNELS_10_20, + interpolation=TopoPlots.NullInterpolator(),) @test_figure("nullInterpolator", f) end - -begin - f = Makie.Figure(size=(1000, 1000)) - s = Makie.Slider(f[:, 1], range=1:size(data, 2), startvalue=351) +@testset "Delaunay with Slider" begin + f = Makie.Figure(; size=(1000, 1000)) + s = Makie.Slider(f[:, 1]; range=1:size(data, 2), startvalue=351) data_obs = map(s.value) do idx - data[:, idx, 1] + return data[:, idx, 1] end - TopoPlots.topoplot( - f[2, 1], - data_obs, positions; - interpolation=DelaunayMesh(), - labels = string.(1:length(positions)), - colorrange=(-1, 1), - axis=(title="delaunay mesh", aspect=DataAspect(),)) + TopoPlots.topoplot(f[2, 1], + data_obs, positions; + interpolation=DelaunayMesh(), + labels=string.(1:length(positions)), + colorrange=(-1, 1), + axis=(title="delaunay mesh", aspect=DataAspect())) display(f) @test_figure("delaunay-with-slider", f) end -begin - f, ax, pl = TopoPlots.topoplot( - data[:, 340, 1], positions; - axis=(; aspect=DataAspect()), - colorrange=(-1, 1), - bounding_geometry = Rect, - labels = string.(1:length(positions)), - label_text=(; color=:white), - label_scatter=(; strokewidth=2), - contours=(linestyle=:dot, linewidth=2)) +@testset "keyword customization" begin + f, ax, pl = TopoPlots.topoplot(data[:, 340, 1], positions; + axis=(; aspect=DataAspect()), + colorrange=(-1, 1), + bounding_geometry=Rect, + labels=string.(1:length(positions)), + label_text=(; color=:white), + label_scatter=(; strokewidth=2), + contours=(linestyle=:dot, linewidth=2)) @test_figure("more-parameters", f) end -begin +@testset "compare to MNE" begin + data, positions = TopoPlots.example_data() f = compare_to_mne(data[:, 340, 1], positions) @test_figure("eeg-topoplot", f) -end -begin labels = TopoPlots.CHANNELS_10_20 pos = TopoPlots.labels2positions(TopoPlots.CHANNELS_10_20) f = compare_to_mne(data[1:19, 340, 1], pos) @test_figure("eeg-topoplot2", f) -end -begin f = compare_to_mne(data[:, 340, 1], positions) @test_figure("eeg-topoplot3", f) -end -begin positions = Point2f[(-1, 0), (0, -1), (1, 0), (0, 1), (0, 0)] posmat = hcat(first.(positions), last.(positions)) data = zeros(length(positions)) data[1] = 1.0 f = compare_to_mne(data, positions) @test_figure("eeg-topoplot4", f) -end -begin positions = Point2f[(-1, 0), (0, -1), (1, 0), (0, 1), (0, 0)] posmat = hcat(first.(positions), last.(positions)) data = zeros(length(positions)) data[1] = 1.0 - f = compare_to_mne(data, positions; extrapolation=TopoPlots.GeomExtrapolation(geometry=Circle)) + f = compare_to_mne(data, positions; + extrapolation=TopoPlots.GeomExtrapolation(; geometry=Circle)) @test_figure("eeg-topoplot5", f) end -begin +@testset "extrapolation" begin data, positions = TopoPlots.example_data() extra = TopoPlots.GeomExtrapolation() pos_extra, data_extra, rect, rect_extended = extra(positions[1:19], data[1:19, 340, 1]) - f, ax, p = Makie.scatter(pos_extra, color=data_extra, axis=(aspect=DataAspect(),), markersize=10) - scatter!(ax, positions[1:19]; color=data[1:19, 340, 1], markersize=5, strokecolor=:white, strokewidth=0.5) + f, ax, p = Makie.scatter(pos_extra; color=data_extra, axis=(aspect=DataAspect(),), + markersize=10) + scatter!(ax, positions[1:19]; color=data[1:19, 340, 1], markersize=5, + strokecolor=:white, strokewidth=0.5) lines!(ax, rect) - lines!(ax, rect_extended, color=:red) + lines!(ax, rect_extended; color=:red) @test_figure("test-extrapolate-data", f) -end -begin data, positions = TopoPlots.example_data() - extra = TopoPlots.GeomExtrapolation(geometry=Circle) + extra = TopoPlots.GeomExtrapolation(; geometry=Circle) pos_extra, data_extra, rect, rect_extended = extra(positions[1:19], data[1:19, 340, 1]) - f, ax, p = Makie.scatter(pos_extra, color=data_extra, axis=(aspect=DataAspect(),), markersize=10) - scatter!(ax, positions[1:19]; color=data[1:19, 340, 1], markersize=5, strokecolor=:white, strokewidth=0.5) + f, ax, p = Makie.scatter(pos_extra; color=data_extra, axis=(aspect=DataAspect(),), + markersize=10) + scatter!(ax, positions[1:19]; color=data[1:19, 340, 1], markersize=5, + strokecolor=:white, strokewidth=0.5) lines!(ax, rect) - lines!(ax, rect_extended, color=:red) + lines!(ax, rect_extended; color=:red) @test_figure("test-extrapolate-data-circle", f) end -let +@testset "custom plotfnc" begin data, positions = TopoPlots.example_data() - f, ax, pl = topoplot(1:10,positions[1:10];plotfnc! = contourf!,plotfnc_kwargs_names=[:colormap]) + f, ax, pl = topoplot(1:10, positions[1:10]; (plotfnc!)=contourf!, + plotfnc_kwargs_names=[:colormap]) @test_figure("test-contour-plotfnc!", f) - f, ax, pl = topoplot(1:10,positions[1:10];plotfnc! = (args...;kwargs...)->heatmap!(args...;alpha=0.3,kwargs...)) + f, ax, pl = topoplot(1:10, positions[1:10]; + (plotfnc!)=(args...; kwargs...) -> heatmap!(args...; alpha=0.3, + kwargs...)) @test_figure("test-heatmap-with-alphs-plotfnc!", f) end -begin +@testset "channel labels" begin f = TopoPlots.eeg_topoplot(1:10; labels=TopoPlots.CHANNELS_10_20[1:10], label_text=true) @test_figure("test-eeg-channel-labels", f) end -let +@testset "10/05 channel positions" begin pos = TopoPlots.labels2positions(TopoPlots.CHANNELS_10_20) pos10_05 = TopoPlots.labels2positions(TopoPlots.CHANNELS_10_05) - @test pos[TopoPlots.CHANNELS_10_20 .== "t3"] == pos10_05[TopoPlots.CHANNELS_10_05 .== "t7"] - @test pos[TopoPlots.CHANNELS_10_20 .== "t4"] == pos10_05[TopoPlots.CHANNELS_10_05 .== "t8"] - @test pos[TopoPlots.CHANNELS_10_20 .== "t5"] == pos10_05[TopoPlots.CHANNELS_10_05 .== "p7"] - @test pos[TopoPlots.CHANNELS_10_20 .== "t6"] == pos10_05[TopoPlots.CHANNELS_10_05 .== "p8"] + @test pos[TopoPlots.CHANNELS_10_20 .== "t3"] == + pos10_05[TopoPlots.CHANNELS_10_05 .== "t7"] + @test pos[TopoPlots.CHANNELS_10_20 .== "t4"] == + pos10_05[TopoPlots.CHANNELS_10_05 .== "t8"] + @test pos[TopoPlots.CHANNELS_10_20 .== "t5"] == + pos10_05[TopoPlots.CHANNELS_10_05 .== "p7"] + @test pos[TopoPlots.CHANNELS_10_20 .== "t6"] == + pos10_05[TopoPlots.CHANNELS_10_05 .== "p8"] end diff --git a/test/test-scripts.jl b/test/test-scripts.jl index e8bb268..3c94886 100644 --- a/test/test-scripts.jl +++ b/test/test-scripts.jl @@ -3,14 +3,15 @@ using TopoPlots, PyCall, PyPlot, PyMNE data, positions = TopoPlots.example_data() pos = vcat(first.(positions)', last.(positions)') -info = pycall(PyMNE.mne.create_info, PyObject, TopoPlots.CHANNELS_10_20, 120; ch_types="eeg") +info = pycall(PyMNE.mne.create_info, PyObject, TopoPlots.CHANNELS_10_20, 120; + ch_types="eeg") info.set_montage("standard_1020"; match_case=false) layout = PyMNE.mne.find_layout(info) X = layout.pos[:, 1] Y = layout.pos[:, 2] -PyMNE.viz.plot_topomap(rand(19), layout.pos) |> display +display(PyMNE.viz.plot_topomap(rand(19), layout.pos)) gcf() @@ -18,20 +19,20 @@ using TopoPlots, PyCall, PyPlot, PyMNE data, positions = TopoPlots.example_data() pos = hcat(first.(positions), last.(positions)) -info = pycall(PyMNE.io.meas_info.create_info, PyObject, string.(1:size(pos,2)), sfreq=256, ch_types="eeg") # fake info -raw = PyMNE.io.RawArray(data[:,:,1], info) # fake raw with fake info -chname_pos_dict = Dict(string.(1:size(pos,2)) .=> [pos[:, p] for p in 1:size(pos,2)]) -montage = PyMNE.channels.make_dig_montage(ch_pos=chname_pos_dict,coord_frame="head") +info = pycall(PyMNE.io.meas_info.create_info, PyObject, string.(1:size(pos, 2)); sfreq=256, + ch_types="eeg") # fake info +raw = PyMNE.io.RawArray(data[:, :, 1], info) # fake raw with fake info +chname_pos_dict = Dict(string.(1:size(pos, 2)) .=> [pos[:, p] for p in 1:size(pos, 2)]) +montage = PyMNE.channels.make_dig_montage(; ch_pos=chname_pos_dict, coord_frame="head") # raw.set_montage(montage) # set montage (why??) -PyMNE.viz.plot_topomap(data[:,340,1], get_info(raw), names=string.(1:size(pos,2)), show_names=true, vmin=-0.7, vmax=0.7) +PyMNE.viz.plot_topomap(data[:, 340, 1], get_info(raw); names=string.(1:size(pos, 2)), + show_names=true, vmin=-0.7, vmax=0.7) positions = TopoPlots.labels2positions(TopoPlots.CHANNELS_10_20) pos = hcat(first.(positions), last.(positions)) PyMNE.viz.plot_topomap(rand(19), pos) - - using TopoPlots, CairoMakie begin # 4 coordinates with one peak @@ -53,7 +54,7 @@ begin data = zeros(length(positions)) data[1] = 1.0 f = PyPlot.figure() - PyMNE.viz.plot_topomap(data, posmat, sphere=1.1, extrapolate="head", cmap="RdBu_r") + PyMNE.viz.plot_topomap(data, posmat; sphere=1.1, extrapolate="head", cmap="RdBu_r") f end @@ -62,7 +63,8 @@ begin posmat = hcat(first.(positions), last.(positions)) data = zeros(length(positions)) data[1] = 1.0 - fig = eeg_topoplot(data, nothing; positions=positions, label_scatter=(markersize=20,), axis=(aspect=DataAspect(),)) + fig = eeg_topoplot(data, nothing; positions=positions, label_scatter=(markersize=20,), + axis=(aspect=DataAspect(),)) end begin @@ -72,19 +74,19 @@ begin data = rand(length(positions)) pos, data = TopoPlots.get_bounded(Rect(-1, -1, 2, 2), positions, data, 0.0) - CairoMakie.scatter(pos, color=data) + CairoMakie.scatter(pos; color=data) end Rect(Circle(Point2f(0), 1.0)) middle(rect) begin - positions = rand(Point2f, 10) data = rand(10) rect = Rect(positions) pos_extra, rect_extended, data_extra = TopoPlots.extrapolate_data(rect, positions, data) - f, ax, p = Makie.scatter(pos_extra, color=data_extra, markersize=10, axis=(aspect=DataAspect(),)) + f, ax, p = Makie.scatter(pos_extra; color=data_extra, markersize=10, + axis=(aspect=DataAspect(),)) scatter!(ax, positions) lines!(ax, rect) - lines!(ax, rect_extended, color=:red) + lines!(ax, rect_extended; color=:red) f end