diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 000000000..e00875c8d --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,52 @@ +name: CI +on: + - push + - pull_request +jobs: + test: + name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + version: + - '1.3' + - '1.4' + - 'nightly' + os: + - ubuntu-latest + - macOS-latest + - windows-latest + arch: + - x64 + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: julia-actions/julia-buildpkg@latest + - uses: julia-actions/julia-runtest@latest + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v1 + with: + file: ./lcov.info + flags: unittests + name: codecov-umbrella + fail_ci_if_error: false + token: ${{ secrets.CODECOV }} + docs: + name: Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: '1.4' + - run: | + julia --project=docs -e ' + using Pkg + Pkg.develop(PackageSpec(path=pwd())) + Pkg.instantiate()' + - run: julia --project=docs docs/make.jl + env: + GITHUB_TOKEN: ${{ secrets.TOKEN }} diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml new file mode 100644 index 000000000..bccaf37f3 --- /dev/null +++ b/.github/workflows/CompatHelper.yml @@ -0,0 +1,24 @@ +name: CompatHelper + +on: + schedule: + - cron: '00 * * * *' + +jobs: + CompatHelper: + runs-on: ${{ matrix.os }} + strategy: + matrix: + julia-version: [1.4.0] + julia-arch: [x86] + os: [ubuntu-latest] + steps: + - uses: julia-actions/setup-julia@latest + with: + version: ${{ matrix.julia-version }} + - name: Pkg.add("CompatHelper") + run: julia -e 'using Pkg; Pkg.add("CompatHelper")' + - name: CompatHelper.main() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: julia -e 'using CompatHelper; CompatHelper.main()' diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0ef146773..000000000 --- a/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: julia - -os: - - linux - -julia: - - 1.0 - -before_script: - - julia -e 'using Pkg; Pkg.add("Test")' - -after_success: - - julia -e 'using Pkg; cd(Pkg.dir("EcologicalNetworks")); Pkg.add("Coverage"); using Coverage; Coveralls.submit(Coveralls.process_folder()); Codecov.submit(Codecov.process_folder())' - - julia -e 'using Pkg; Pkg.add("Documenter")' - - julia -e 'using Pkg; cd(Pkg.dir("EcologicalNetworks")); include(joinpath("docs", "make.jl"))' diff --git a/Project.toml b/Project.toml index 08b4b5cb9..db366db7a 100644 --- a/Project.toml +++ b/Project.toml @@ -2,7 +2,7 @@ name = "EcologicalNetworks" uuid = "f03a62fe-f8ab-5b77-a061-bb599b765229" authors = ["Timothée Poisot "] repo = "https://github.com/PoisotLab/EcologicalNetworks.jl" -version = "0.2.2" +version = "0.3.0" [deps] Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" @@ -16,7 +16,7 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" StatsBase = "2913bbd2-ae8a-5f71-8c99-4fb6c76f3a91" [compat] -julia = "1" +julia = "1.3" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/README.md b/README.md index e70a738b8..6e233b0db 100644 --- a/README.md +++ b/README.md @@ -39,12 +39,14 @@ That's it. Now head over to the ### On `master` -[![Build Status](https://travis-ci.org/PoisotLab/EcologicalNetworks.jl.svg?branch=master)](https://travis-ci.org/PoisotLab/EcologicalNetworks.jl) -[![Coverage Status](https://coveralls.io/repos/PoisotLab/EcologicalNetworks.jl/badge.svg?branch=master&service=github)](https://coveralls.io/github/PoisotLab/EcologicalNetworks.jl?branch=master) +![CI](https://github.com/PoisotLab/EcologicalNetworks.jl/workflows/CI/badge.svg?branch=master) +![CI](https://github.com/PoisotLab/EcologicalNetworks.jl/workflows/TagBot/badge.svg?branch=master) +![CI](https://github.com/PoisotLab/EcologicalNetworks.jl/workflows/CompatHelper/badge.svg?branch=master) [![codecov.io](http://codecov.io/github/PoisotLab/EcologicalNetworks.jl/coverage.svg?branch=master)](http://codecov.io/github/PoisotLab/EcologicalNetworks.jl?branch=master) ### On `develop` -[![Build Status](https://travis-ci.org/PoisotLab/EcologicalNetworks.jl.svg?branch=develop)](https://travis-ci.org/PoisotLab/EcologicalNetworks.jl) -[![Coverage Status](https://coveralls.io/repos/github/PoisotLab/EcologicalNetworks.jl/badge.svg?branch=develop)](https://coveralls.io/github/PoisotLab/EcologicalNetworks.jl?branch=develop) +![CI](https://github.com/PoisotLab/EcologicalNetworks.jl/workflows/CI/badge.svg?branch=develop) +![CI](https://github.com/PoisotLab/EcologicalNetworks.jl/workflows/TagBot/badge.svg?branch=develop) +![CI](https://github.com/PoisotLab/EcologicalNetworks.jl/workflows/CompatHelper/badge.svg?branch=develop) [![codecov.io](http://codecov.io/github/PoisotLab/EcologicalNetworks.jl/coverage.svg?branch=develop)](http://codecov.io/github/PoisotLab/EcologicalNetworks.jl?branch=develop) diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 000000000..2eb7258e6 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,6 @@ +[deps] +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" + +[compat] +Documenter = "0.24" diff --git a/docs/make.jl b/docs/make.jl index 87ca0000a..c83399052 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,29 +1,19 @@ -using Pkg - -tmp_packages = ["EcologicalNetworksPlots", "Plots", "Documenter"] - push!(LOAD_PATH,"../src/") -Pkg.activate(".") - -Pkg.add.(tmp_packages) # IMPORTANT - using Documenter using EcologicalNetworks -using EcologicalNetworksPlots using Random makedocs( sitename = "EcologicalNetworks", authors = "Timothée Poisot", - modules = [EcologicalNetworks, EcologicalNetworksPlots], + modules = [EcologicalNetworks], pages = [ "Index" => "index.md", "Interface" => [ "Types" => "interface/types.md", "Conversions" => "interface/conversions.md", - "Core functions" => "interface/highlevel.md", - "Plotting" => "var/plots.md" + "Core functions" => "interface/highlevel.md" ], "Network measures" => [ "Links" => "properties/links.md", @@ -39,14 +29,13 @@ makedocs( "Random networks" => [ "Null models" => "random/null.md", "Structural models" => "random/structure.md" - ] + ] ] ) deploydocs( deps = Deps.pip("pygments", "python-markdown-math"), repo = "github.com/PoisotLab/EcologicalNetworks.jl.git", - devbranch = "master" + devbranch = "master", + push_preview = true ) - -Pkg.rm.(tmp_packages) # IMPORTANT diff --git a/docs/src/assets/documenter.css b/docs/src/assets/documenter.css deleted file mode 100644 index 8faa09129..000000000 --- a/docs/src/assets/documenter.css +++ /dev/null @@ -1,232 +0,0 @@ -@import url('https://fonts.googleapis.com/css?family=Fira+Mono:400,500,700|Fira+Sans+Condensed:400,400i,500,500i|Fira+Sans:400,400i,500,500i&subset=greek,greek-ext'); - -:root { - --toc-width: 15em; - --max-width: 45em; - --int-padding: 1em; - - --color-bg: #fcfcfc; - --color-fg: #303030; - --color-accent: #1C9696; - - --font-mono: "Fira Mono"; - --font-body: "Fira Sans"; - --font-meta: "Fira Sans Condensed"; - - --package-name: "EcologicalNetworks" -} - -body { - color: var(--color-fg); - background-color: var(--color-bg); - font-family: var(--font-body); - line-height: 1.5; - font-size: 16px; -} - -b, strong { - font-weight: 500; -} - -h1, h2, h3, h4, h5, h6, .nav-anchor { - font-weight: 500; - font-family: var(--font-meta); - color: #777; -} - -a, a:hover, a:visited { - text-decoration: none; - color: var(--color-accent); -} - -nav.toc, article { - padding: var(--int-padding); -} - -nav.toc { - font-family: var(--font-meta); - width: var(--toc-width); - position: fixed; - height: 100vh; - top: 0; - left: 0; - transition-property: left; - transition-duration: 0.3s; - transition-timing-function: ease-out; - z-index: 2; - display: flex; - flex-flow: column nowrap; - overflow-y: auto; - padding: 1em 0 0 0; - text-align: center; - background-color: var(--color-bg); -} - -nav.toc.show { - left: 0; - box-shadow: 0px 0px 4px 0px #888; - font-size: 95%; -} - -nav .logo { - max-width: var(--toc-width); - max-height: 7em; - text-align: center; -} - -nav.toc h1 { - color: var(--color-fg); - font-weight: 500; - font-size: 1.35em; -} - -nav.toc ul { - padding-left: 15px; -} - -nav.toc li { - line-height: 28px; -} - -nav.toc li.current { - font-weight: 500; -} - -nav.toc li.current ul.internal { - font-weight: 400; -} - -nav.toc li.current ul.internal a { - color: #777 !important; -} - - -nav.toc ul, nav.toc li { - text-align: left; - list-style: none; -} - -article { - max-width: var(--max-width); -} - -section.docstring { - margin-bottom: 4em; -} - -section.docstring:last-of-type { - margin-bottom: 0; -} - -.docstring-header { - font-size: 110%; - font-weight: 500; - color: #777; -} - -.docstring-binding { - color: black; - -} - -pre, code, kbd { - font-family: var(--font-mono); -} - -header { - font-family: var(--font-meta); -} - -header a, header a:hover, header a:visited{ - font-weight: 500; -} - -header ul, header li { - list-style: none; - padding-left: 0; - display: inline; -} - -header li { - display: inline-block; -} - -header li::before { - content: "›"; - color: #777; - padding-left: 5px; - padding-right: 5px; -} - -header li:first-of-type::before { - content: var(--package-name); - color: black; - font-weight: 500; - padding-right: 1em; - padding-left: 0px; -} - -header hr { - display: none; -} - -.edit-page { - display: none; -} - -@media screen and (max-width: 768px) { - header nav { - display: none; - } - nav.toc { - left: -15em; - } - article { - margin-left: 0em !important; - } - header #topbar { - position: fixed; - left: 0; - top: 0; - width: 100vw; - background-color: var(--color-bg); - z-index: 10; - box-shadow: 0px 0px 2px 0px #777; - padding: var(--int-padding); - font-size: 1.2em; - } - header #topbar a { - padding-left: 2em; - } - article { - margin-top: 4em; - } - .logo { - display: none; - } -} - -@media screen and (min-width: 768px) { - header #topbar { - display: none; - } - header nav { - margin-bottom: 3em; - } -} - -@media screen and (max-width: 75em) { - article { - margin-left: var(--toc-width); - } -} - -@media screen and (min-width: 75em) { - article { - margin: 0px auto; - } -} - -.footer { - display: none; -} diff --git a/docs/src/index.md b/docs/src/index.md index 4442de064..06283d307 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -52,10 +52,11 @@ speaking a tool for analysis, it is not part of this package. Second, it helps to keep software dependency small. Most of our work using this package is done on clusters of one sort of the other, and having fewer -dependencies means that installation is easier. +dependencies means that installation is easier. `EcologicalNetworksPlots` can be +installed like any other Julia package. It is also documented on [its own +website][ENP]. -`EcologicalNetworksPlots` can be installed as with any other Julia package. It -is also documented on this website. +[ENP]: https://poisotlab.github.io/EcologicalNetworksPlots.jl/stable/ ### And worse, you forgot my favorite method! diff --git a/docs/src/interface/types.md b/docs/src/interface/types.md index 151539cee..13a14252c 100644 --- a/docs/src/interface/types.md +++ b/docs/src/interface/types.md @@ -72,7 +72,7 @@ AbstractEcologicalNetwork ``` The type of nodes that are allowed is determined by the *non-exported* -`EcologicalNetworks.is_valid_species` function. To allow an additional type of +`EcologicalNetworks.check_species_validity` function. To allow an additional type of node, you can write the following: ~~~ julia @@ -83,11 +83,13 @@ struct Foo end import EcologicalNetworks -EcologicalNetworks.is_valid_species(::Type{Foo}) = true +function EcologicalNetworks.check_species_validity(::Type{Foo}) +end ~~~ Note that **integers are never valid species identifiers**. By default, `String` -and `Symbol` are used. +and `Symbol` are used. The function `check_species_validity` should do *nothing* +for an accepted type (and it will throw an error for any other type). ### By partiteness diff --git a/docs/src/var/plots.md b/docs/src/var/plots.md deleted file mode 100644 index b8a4853f4..000000000 --- a/docs/src/var/plots.md +++ /dev/null @@ -1,158 +0,0 @@ -Plotting functions are part of the `EcologicalNetworksPlot`, which requires -`Plot` to work. `EcologicalNetworksPlot` can be installed from the central -*Julia* package repository. - -## Initial layouts - -```@docs -initial -``` - -```@docs -RandomInitialLayout -BipartiteInitialLayout -FoodwebInitialLayout -CircularInitialLayout -``` - -## Layouts - -### Force-directed layout - -```@docs -ForceDirectedLayout -``` - -### Circular layout - -```@docs -CircularLayout -``` - -### Static layouts - -```@docs -NestedBipartiteLayout -``` - -## Apply layout to network - -```@docs -position! -``` - -## Examples - -```@setup default -using EcologicalNetworks -using EcologicalNetworksPlots -using Plots -``` - -## Static layouts - -### Nested - -```@example default -Unes = web_of_life("M_SD_033") -I = initial(BipartiteInitialLayout, Unes) -position!(NestedBipartiteLayout(0.4), I, Unes) -plot(I, Unes, aspectratio=1) -scatter!(I, Unes, bipartite=true) -``` - -### Circular - -```@example default -Unes = web_of_life("M_SD_033") -I = initial(CircularInitialLayout, Unes) -position!(CircularLayout(), I, Unes) -plot(I, Unes, aspectratio=1) -scatter!(I, Unes, bipartite=true) -``` - -## Dynamic layouts - -### Force directed - -```@example default -Umod = web_of_life("M_PA_003") -I = initial(RandomInitialLayout, Umod) -for step in 1:4000 - position!(ForceDirectedLayout(2.5), I, Umod) -end -plot(I, Umod, aspectratio=1) -scatter!(I, Umod, bipartite=true) -``` - -### Food web layout - -```@example default -Fweb = simplify(nz_stream_foodweb()[5]) -I = initial(FoodwebInitialLayout, Fweb) -for step in 1:4000 - position!(ForceDirectedLayout(true, false, 2.5), I, Fweb) -end -plot(I, Fweb) -scatter!(I, Fweb) -``` - -Note that we can replace some properties of the nodes in the layout *after* the -positioning algorithm occurred -- so we can, for example, use the actual -(instead of fractional) trophic level: - -```@example default -Fweb = simplify(nz_stream_foodweb()[5]) -I = initial(FoodwebInitialLayout, Fweb) -for step in 1:4000 - position!(ForceDirectedLayout(true, false, 2.5), I, Fweb) -end -tl = trophic_level(Fweb) -for s in species(Fweb) - I[s].y = tl[s] -end -plot(I, Fweb) -scatter!(I, Fweb) -``` - -## Node properties - -### Color - -```@example default -Unes = web_of_life("M_SD_033") -I = initial(BipartiteInitialLayout, Unes) -position!(NestedBipartiteLayout(0.4), I, Unes) -plot(I, Unes, aspectratio=1) -scatter!(I, Unes, bipartite=true, nodefill=degree(Unes)) -``` - -### Size - -## Advanced examples - -One important feature of the package is that the layout can contain *more* nodes -than the network. For example, we can use this to our advantage, to represent -species with a degree larger than 3 in red: - -```@example default -Umod = web_of_life("M_PA_003") -I = initial(RandomInitialLayout, Umod) -for step in 1:4000 - position!(ForceDirectedLayout(2.5), I, Umod) -end -plot(I, Umod, aspectratio=1) -scatter!(I, Umod) -N = convert(AbstractUnipartiteNetwork, convert(BinaryNetwork, Umod)) -core3 = collect(keys(filter(p -> p.second ≥ 3, degree(N)))) -plot!(I, N[core3], lc=:red) -scatter!(I, N[core3], mc=:red) -``` - -### Heatmaps - - -```@example default -Umod = web_of_life("M_PA_003") -heatmap(Umod) -``` diff --git a/src/EcologicalNetworks.jl b/src/EcologicalNetworks.jl index 9a8219fe5..9b4eee802 100644 --- a/src/EcologicalNetworks.jl +++ b/src/EcologicalNetworks.jl @@ -38,9 +38,16 @@ include(joinpath(".", "misc/data.jl")) export web_of_life, nz_stream_foodweb, pajek function __init__() - @require Mangal="b8b640a6-63d9-51e6-b784-5033db27bef2" is_valid_species(::Mangal.MangalNode) = true - @require Mangal="b8b640a6-63d9-51e6-b784-5033db27bef2" is_valid_species(::Mangal.MangalReferenceTaxon) = true - @require GBIF="ee291a33-5a6c-5552-a3c8-0f29a1181037" is_valid_species(::GBIF.GBIFTaxon) = true + @require Mangal="b8b640a6-63d9-51e6-b784-5033db27bef2" begin + function check_species_validity(::Mangal.MangalReferenceTaxon) + end + function check_species_validity(::Mangal.MangalNode) + end + end + @require GBIF="ee291a33-5a6c-5552-a3c8-0f29a1181037" begin + function check_species_validity(::GBIF.GBIFTaxon) + end + end end # General useful manipulations @@ -147,6 +154,9 @@ export KGL01, KGL02, KGL03, KGL04, KGL05, KGL06, KGL07, KGL08, KGL09, KGL10, include(joinpath(".", "foodwebs/trophiclevels.jl")) export fractional_trophic_level, trophic_level +include(joinpath(".", "foodwebs/omnivory.jl")) +export omnivory + include(joinpath(".", "information/entropy.jl")) export entropy, make_joint_distribution, mutual_information, conditional_entropy, variation_information, diff_entropy_uniform, information_decomposition, diff --git a/src/foodwebs/omnivory.jl b/src/foodwebs/omnivory.jl new file mode 100644 index 000000000..9f6ee28e1 --- /dev/null +++ b/src/foodwebs/omnivory.jl @@ -0,0 +1,24 @@ +function omnivory(N::T) where {T <: UnipartiteNetwork} + OI = Dict([s => 0.0 for s in species(N)]) + + TL = fractional_trophic_level(N) + k = degree(N; dims=1) + + for sp_i in species(N) + + # Species with no interaction have an omnivory index of 0 + k[sp_i] > 0 || continue + + # For every species, we set its initial omnivory to 0 + oi = 0.0 + for (j, sp_j) in enumerate(species(N)) + # Then for every species it consumes, we ha + tl_diff = (TL[sp_j] - (TL[sp_i]-1.0)).^2.0 + corr = N[sp_i,sp_j]/k[sp_i] + oi += tl_diff * corr + end + OI[sp_i] = oi + end + + return OI +end diff --git a/src/types/constructors.jl b/src/types/constructors.jl index 60e55ff37..792f9c73e 100644 --- a/src/types/constructors.jl +++ b/src/types/constructors.jl @@ -5,7 +5,7 @@ function UnipartiteNetwork(A::M) where {M<:AbstractMatrix{Bool}} end function UnipartiteNetwork(A::M, S::Vector{NT}) where {M<:AbstractMatrix{Bool}, NT} - is_valid_species(NT) || throw(ArgumentError("Not allowed")) + check_species_validity(NT) check_unipartiteness(A, S) UnipartiteNetwork{Bool,NT}(A, S) end @@ -18,7 +18,7 @@ function BipartiteNetwork(A::M) where {M<:AbstractMatrix{Bool}} end function BipartiteNetwork(A::M, T::Vector{NT}, B::Vector{NT}) where {M<:AbstractMatrix{Bool}, NT} - is_valid_species(NT) || throw(ArgumentError("Not allowed")) + check_species_validity(NT) check_bipartiteness(A, T, B) BipartiteNetwork{Bool,eltype(T)}(A, T, B) end @@ -32,7 +32,7 @@ function BipartiteProbabilisticNetwork(A::Matrix{IT}) where {IT<:AbstractFloat} end function BipartiteProbabilisticNetwork(A::Matrix{IT}, T::Vector{NT}, B::Vector{NT}) where {IT<:AbstractFloat, NT} - is_valid_species(NT) || throw(ArgumentError("Not allowed")) + check_species_validity(NT) check_bipartiteness(A, T, B) check_probability_values(A) BipartiteProbabilisticNetwork{IT,NT}(A, T, B) @@ -46,7 +46,7 @@ function BipartiteQuantitativeNetwork(A::Matrix{IT}) where {IT <: Number} end function BipartiteQuantitativeNetwork(A::Matrix{IT}, T::Vector{NT}, B::Vector{NT}) where {IT<:Number,NT} - is_valid_species(NT) || throw(ArgumentError("Not allowed")) + check_species_validity(NT) check_bipartiteness(A, T, B) BipartiteQuantitativeNetwork{IT,NT}(A, T, B) end @@ -58,7 +58,7 @@ function UnipartiteQuantitativeNetwork(A::Matrix{IT}) where {IT<:Number} end function UnipartiteQuantitativeNetwork(A::Matrix{IT}, S::Vector{NT}) where {IT<:Number,NT} - is_valid_species(NT) || throw(ArgumentError("Not allowed")) + check_species_validity(NT) check_unipartiteness(A, S) UnipartiteQuantitativeNetwork{IT,NT}(A, S) end @@ -73,7 +73,7 @@ function UnipartiteProbabilisticNetwork(A::Matrix{IT}) where {IT<:AbstractFloat} end function UnipartiteProbabilisticNetwork(A::Matrix{IT}, S::Vector{NT}) where {IT<:AbstractFloat,NT} - is_valid_species(NT) || throw(ArgumentError("Not allowed")) + check_species_validity(NT) check_unipartiteness(A, S) check_probability_values(A) UnipartiteProbabilisticNetwork{IT,NT}(A, S) diff --git a/src/types/utilities.jl b/src/types/utilities.jl index 0920cabba..3c6ea9c87 100644 --- a/src/types/utilities.jl +++ b/src/types/utilities.jl @@ -1,8 +1,11 @@ import Base: getindex, setindex!, permutedims, permutedims!, size, copy, !, show, +, inv, similar -is_valid_species(::Type{T}) where {T <: Any} = false +function check_species_validity(::Type{T}) where {T <: Any} + throw(ArgumentError("The type $(T) is not an allowed species type")) +end -is_valid_species(::Type{T}) where {T <: Union{Symbol,String}} = true +function check_species_validity(::Type{T}) where {T <: Union{Symbol,String}} +end """ show(io::IO, N::AbstractEcologicalNetwork) @@ -70,7 +73,7 @@ for the presence of an interaction. Use `N[i,j]` if you need to get the value of the interaction. """ function has_interaction(N::AbstractEcologicalNetwork, i::NT, j::NT) where {NT} - @assert is_valid_species(NT) + check_species_validity(NT) @assert i ∈ species(N; dims=1) @assert j ∈ species(N; dims=2) i_pos = something(findfirst(isequal(i), species(N; dims=1)),0)