From 177a3bd82a5de4b99264fa64625c85b19f1e2f03 Mon Sep 17 00:00:00 2001 From: Skylar A Gering Date: Wed, 31 Jul 2024 15:06:11 -0400 Subject: [PATCH] Update versions and dependencies (#98) * Add missing compat entries * Remove unneccesary packages * Move plotting into an extension * Rename extension * Add export * Fix extension name * Remove unneeded dep * Updating import order * Function stubs * Adding different exports * Add JLD2 dependency * Cleanup plotting and extension code * Add CairoMakie dependency --- Project.toml | 19 ++- examples/converge_diverge_flow.jl | 2 +- examples/forcing_contained_floes.jl | 2 +- examples/moving_bounds.jl | 2 +- examples/restart_sim.jl | 2 +- examples/shear_flow.jl | 2 +- examples/simple_strait.jl | 2 +- examples/uniform_flow.jl | 2 +- ext/SubzeroMakieExt.jl | 170 +++++++++++++++++++ src/Subzero.jl | 8 +- src/simulation_components/floe.jl | 2 +- src/tools/plotting.jl | 249 +++++++--------------------- test/runtests.jl | 2 +- 13 files changed, 251 insertions(+), 213 deletions(-) create mode 100644 ext/SubzeroMakieExt.jl diff --git a/Project.toml b/Project.toml index ded0781..4197a2b 100644 --- a/Project.toml +++ b/Project.toml @@ -4,9 +4,7 @@ authors = ["Skylar Gering"] version = "0.1.0" [deps] -CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" CoordinateTransformations = "150eb455-5306-5404-9cee-2592286d6298" -DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" Extents = "411431e0-e8b7-467b-b5e0-f676ba4f2910" GeometryOps = "3251bfac-6a57-4b6d-aa61-ac1fef2975ab" @@ -14,10 +12,8 @@ Interpolations = "a98d9a8b-a2ab-59e6-89dd-64a1c18fca59" JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" -Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a" Measures = "442fdcdd-2543-5da2-b0f3-8c86c306513e" NCDatasets = "85f8d34a-cbdd-5861-8df4-14fed0d494ab" -NetCDF = "30363a11-5582-574a-97bb-aa9a979735b9" Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Rotations = "6038ab10-8711-5258-84ad-4b1120ba62dc" @@ -27,23 +23,28 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" StructArrays = "09ab397b-f2b6-538f-b94a-2f83cf4a842a" VoronoiCells = "e3e34ffb-84e9-5012-9490-92c94d0c60a4" +[weakdeps] +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" + +[extensions] +SubzeroMakieExt = "CairoMakie" + [compat] CairoMakie = "0.12" -DataStructures = "0.18" +CoordinateTransformations = "0.6" Extents = "0.1" GeometryOps = "0.1.10" Interpolations = "0.14, 0.15" JLD2 = "0.4" -Makie = "0.19, 0.20, 0.21" Measures = "0.3" -NCDatasets = "0.12, 0.13, 0.14" -NetCDF = "0.11, 0.12" +NCDatasets = "0.14" +Rotations = "1" SplitApplyCombine = "1" StaticArrays = "1" Statistics = "1" StructArrays = "0.6" VoronoiCells = "0.4" -julia = "1.6" +julia = "1.9" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/examples/converge_diverge_flow.jl b/examples/converge_diverge_flow.jl index b63e11b..4aee666 100644 --- a/examples/converge_diverge_flow.jl +++ b/examples/converge_diverge_flow.jl @@ -1,4 +1,4 @@ -using JLD2, Random, Statistics, Subzero +using JLD2, Random, Statistics, Subzero, CairoMakie # User Inputs const FT = Float64 diff --git a/examples/forcing_contained_floes.jl b/examples/forcing_contained_floes.jl index df2d169..307873a 100644 --- a/examples/forcing_contained_floes.jl +++ b/examples/forcing_contained_floes.jl @@ -1,4 +1,4 @@ -using JLD2, Random, Statistics, Subzero +using JLD2, Random, Statistics, Subzero, CairoMakie # User Inputs const FT = Float64 diff --git a/examples/moving_bounds.jl b/examples/moving_bounds.jl index 9374159..fedee0a 100644 --- a/examples/moving_bounds.jl +++ b/examples/moving_bounds.jl @@ -1,4 +1,4 @@ -using JLD2, Random, Statistics, Subzero +using JLD2, Random, Statistics, Subzero, CairoMakie # User Inputs const FT = Float64 diff --git a/examples/restart_sim.jl b/examples/restart_sim.jl index 5e89c8e..9972b1b 100644 --- a/examples/restart_sim.jl +++ b/examples/restart_sim.jl @@ -1,4 +1,4 @@ -using JLD2, Subzero, Random, Statistics +using JLD2, Subzero, Random, Statistics, CairoMakie const FT = Float64 const Δt = 10 diff --git a/examples/shear_flow.jl b/examples/shear_flow.jl index fdd9270..3b9f29b 100644 --- a/examples/shear_flow.jl +++ b/examples/shear_flow.jl @@ -1,4 +1,4 @@ -using JLD2, Random, Statistics, Subzero +using JLD2, Random, Statistics, Subzero, CairoMakie # User Inputs const FT = Float64 diff --git a/examples/simple_strait.jl b/examples/simple_strait.jl index 82a0e0d..80d17da 100644 --- a/examples/simple_strait.jl +++ b/examples/simple_strait.jl @@ -1,4 +1,4 @@ -using JLD2, Random, Statistics, Subzero +using JLD2, Random, Statistics, Subzero, CairoMakie # User Inputs const FT = Float64 diff --git a/examples/uniform_flow.jl b/examples/uniform_flow.jl index 4d83d8a..885700d 100644 --- a/examples/uniform_flow.jl +++ b/examples/uniform_flow.jl @@ -1,4 +1,4 @@ -using JLD2, Random, Statistics, Subzero +using JLD2, Random, Statistics, Subzero, CairoMakie # User Inputs const FT = Float64 diff --git a/ext/SubzeroMakieExt.jl b/ext/SubzeroMakieExt.jl new file mode 100644 index 0000000..e117982 --- /dev/null +++ b/ext/SubzeroMakieExt.jl @@ -0,0 +1,170 @@ +module SubzeroMakieExt + +using CairoMakie +using Subzero, JLD2 +import Subzero: prettytime, plot_sim, plot_sim_with_ocean_field + +""" + CoordPlot + +Recipe for plotting list of `PolyVec`s that creates two new functions: `coordplot` and +`coordplot!`. + +These functions each take in a vector of `PolyVec`s to plot the floe field at a given +timestep. + +Note: I would like to remove this recipe and it's corresponding `plot!` function eventually. +Now that all of the floes' carry around a `GeoInterface` polygon, we can simply use +`GeoInterfaceMakie` to plot them and not deal with plotting from coordiantes. +""" +CairoMakie.@recipe(CoordPlot, coord_list) do scene + Attributes( + color = :lightblue, # floe fill color (could give transparent color) + strokecolor = :black, # outline color of floes + strokewidth = 1, # width of floe outline + ) +end + +""" + Makie.plot!(coordplot) + +Defines coordplot and coordplot! for plotting vectors of PolyVecs, representing the floe +field at a given timestep. + +Note: I would like to remove this function and it's corresponding `CoordPlot` recipe +eventually. Now that all of the floes' carry around a `GeoInterface` polygon, we can simply +use `GeoInterfaceMakie` to plot them and not deal with plotting from coordiantes. +""" +function CairoMakie.plot!(coordplot::CoordPlot{<:Tuple{<:Vector{<:PolyVec}}}) + coord_list = coordplot[1] + poly_list = @lift([[Point2f(verts) for verts in c[1]] for c in $coord_list]) + Makie.poly!( + coordplot, + poly_list, + color = coordplot[:color], + strokecolor = coordplot[:strokecolor], + strokewidth = coordplot[:strokewidth], + ) + coordplot +end + +""" + plot_sim(floe_fn, initial_state_fn, title, Δt, output_fn) + +Basic plotting of a simulation using the simulation's floe and initial state files. This +function is meant for basic plotting and as an example of how to create video. +Does not have underlying ocean. + +## Arguments: +- `floe_fn::String`: $(Subzero.FLOE_FN_DEF) +- `initial_state_fn::String`: $(Subzero.INITIAL_STATE_FN_DEF) +- `title::String`: plot title +- `Δt::Int`: $(Subzero.ΔT_DEF) +- `output_fn::String`: $(Subzero.MP4_OUTPUT_FN) +""" +function plot_sim( + floe_fn, + initial_state_fn, + Δt, + output_fn; +) + # Open files + file = jldopen(floe_fn) + domain = load(initial_state_fn)["sim"].model.domain + timesteps = keys(file["centroid"]) + # Set up observables + floes = Observable(file["coords"][timesteps[1]]) + # Plot floes + fig, ax, _ = coordplot(floes) + # Set axis limits and names + xlims!(domain.west.val, domain.east.val) + ylims!(domain.south.val, domain.north.val) + ax.xlabel = "Meters" + ax.ylabel = "Meters" + # Plot topography + if !isempty(domain.topography) + coordplot!(domain.topography.coords, color = :lightgrey) + end + # Create movie + record(fig, output_fn, timesteps; framerate = 20) do time + ax.title = Subzero.prettytime(parse(Float64, time) * Δt) + new_coords = file["coords"][time] + floes[] = new_coords + end + close(file) +end + +""" + plot_sim_with_ocean_field(floe_fn, initial_state_fn, Δt, ocean_fn, ocean_func, output_fn) + +Basic plotting of sim as an example of how to create video. Does not have +underlying ocean. + +Basic plotting of a simulation using the simulation's floe and initial state files, as well +as an surface data file output by `Oceananigans` that can be run through an `ocean_func` to +get a gridded output. This function is meant for basic plotting and as an example of how to +create video. + +## Arguments: +- `floe_fn::String`: $(Subzero.FLOE_FN_DEF) +- `initial_state_fn::String`: $(Subzero.INITIAL_STATE_FN_DEF) +- `Δt::Int`: $(Subzero.ΔT_DEF) +- `ocean_fn::String`: $(Subzero.OCEAN_FN_DEF) +- `ocean_func::Function`: function that takes in `ocean_fn` and returns an Nx by Ny by timesteps surface field for plotting as well as xc and yc fields (see `calc_ro_field` for example) +- `colorbar_title::String`: name for colorbar associated with ocean_func vals +- `output_fn::String`: $(Subzero.MP4_OUTPUT_FN) +""" +function plot_sim_with_ocean_field( + floe_fn, + initial_state_fn, + Δt, + ocean_fn, + ocean_func, + colorbar_title, + output_fn, +) + # Open files + file = jldopen(floe_fn) + domain = load(initial_state_fn)["sim"].model.domain + timesteps = keys(file["centroid"]) + ocean_data, xc, yc = ocean_func(ocean_fn) + # Set up observables needed for plotting + floes = Observable(file["coords"][timesteps[1]]) + ocean_vals = Observable(@view ocean_data[:, :, 1]) + min_ocn_val, max_ocn_val = extrema(ocean_data) + fig = Figure() + # Plot ocean + ax, hm = heatmap( + fig[1, 1], + xc, + yc, + ocean_vals, + colormap = :RdBu_9, + colorrange = (min_ocn_val, max_ocn_val) + ) + # Add axis limits and titles + xlims!(domain.west.val, domain.east.val) + ylims!(domain.south.val, domain.north.val) + ax.xlabel = "Meters" + ax.ylabel = "Meters" + # Add colorbar + Colorbar(fig[1, 2], hm, label = colorbar_title) + # Plot floes + coordplot!(fig[1, 1], floes) + # Plot topography + if !isempty(domain.topography) + coordplot!(fig[1, 1], domain.topography.coords, color = :lightgrey) + end + + # Create movie + record(fig, output_fn, 1:length(timesteps), framerate = 20) do i + time = timesteps[i] + ax.title = prettytime(parse(Float64, time) * Δt) + new_coords = file["coords"][time] + ocean_vals[] = @view ocean_data[:, :, i] + floes[] = new_coords + end + close(file) +end + +end # module \ No newline at end of file diff --git a/src/Subzero.jl b/src/Subzero.jl index d86b317..f466b10 100644 --- a/src/Subzero.jl +++ b/src/Subzero.jl @@ -64,15 +64,15 @@ export IceStressCell, CellFloes, MonteCarloPointsGenerator, - SubGridPointsGenerator, - plot_sim + SubGridPointsGenerator import Base.@kwdef # this is being exported as of version 1.9 import GeometryOps as GO import GeometryOps.GeoInterface as GI +import GeometryOps.GeometryBasics as GB import StaticArrays as SA -using CairoMakie, CoordinateTransformations, DataStructures, Dates, Extents, - Interpolations, JLD2, LinearAlgebra, Logging, Makie, Measures, NCDatasets, NetCDF, +using CoordinateTransformations, Dates, Extents, + Interpolations, JLD2, LinearAlgebra, Logging, Measures, NCDatasets, Printf, Random, Rotations, SplitApplyCombine, Statistics, StructArrays, VoronoiCells diff --git a/src/simulation_components/floe.jl b/src/simulation_components/floe.jl index bdb9b40..bddb09e 100644 --- a/src/simulation_components/floe.jl +++ b/src/simulation_components/floe.jl @@ -488,7 +488,7 @@ function generate_voronoi_coords( tess_cells = voronoicells( xpoints, ypoints, - Rectangle(Point2(0.0, 0.0), Point2(1.0, 1.0)), + Rectangle(GB.Point2((0.0, 0.0)), GB.Point2((1.0, 1.0))), rng = rng ).Cells # Scale and translate voronoi coordinates diff --git a/src/tools/plotting.jl b/src/tools/plotting.jl index c0700ea..e66f47b 100644 --- a/src/tools/plotting.jl +++ b/src/tools/plotting.jl @@ -1,24 +1,54 @@ -""" -Plotting functions for Subzero Simulation -""" +# # Basic plotting functions (and stub functions) for Subzero simulations +export plot_sim, plot_sim_with_ocean_field, prettytime, get_curl, calc_ro_field + +#= +## What plotting functionality is availible for Subzero simulations? + +We provide very basic plotting functionalities for Subzero simulations. Given plotting is so +customizable, we just wanted to provide a roadmap for how a user might plot a simulation and +provide a few utility functions. + +Below are the stubs of functions that require `CairoMakie`. The actual code for these +functions is in `ext/SubzeroMakieExt`. These functions are only loaded when the user loads +`CairoMakie`. This prevents unneccesarily long compiling times when the user just wants to +run simulations. They can then load `CairoMakie` once post simulation runs and create all of +the needed videos at once, rather than loading `CairoMakie` prior to each simulation. + +We also provide a few basic plotting utility functions, like `prettytime` here as they do +not depend on `CairoMakie` and therefore don't need to be abstracted away into the +extension. +=# + +# Stub functions that depend on CairoMakie implemented in ext/SubzeroMakieExt.jl + +function plot_sim end +function plot_sim_with_ocean_field end + +# Constants used in plotting code +const FLOE_FN_DEF = "floe outputwriter output file path and name" +const INITIAL_STATE_FN_DEF = "initial state outputwriter output file path and name" +const OCEAN_FN_DEF = "`Oceananigans` output surface.nc file" +const ΔT_DEF = "length of timestep in integer seconds" +const MP4_OUTPUT_FN = "output video file path and name (should end with .mp4)" + +# Utility functions (and example functions) that don't depend on CairoMakie """ prettytime(t) Turn time in seconds into units of minutes, hours, days, or years as appropriate -Input: - t number of seconds -Output: - String with value and units -Note: - modified from - https://github.com/JuliaCI/BenchmarkTools.jl/blob/master/src/trials.jl +## Aguments: +- `t::Real`: number of seconds +## Returns: +- `::String`: number of seconds converted to a string value in minutes, hours, days, or years with units +## Note: + This code was modified from the this [source code](https://github.com/JuliaCI/BenchmarkTools.jl/blob/master/src/trials.jl). """ function prettytime(t) minute = 60 - hour = 3600 - day = 24*3600 - year = 360*day + hour = 60 * minute + day = 24 * hour + year = 365 * day iszero(t) && return "0 seconds" if t < minute @@ -44,13 +74,13 @@ end get_curl(fldx,fldy,dx,dy) Calculate curl from ocean u and v velocity fields -Inputs: - fldx ocean u velocity field - fldy ocean v velocity field - dx x-distance over which velocity fields are provided - dy y-distance over which velocity fields are provided -Output: - curl +## Arguments: +-`fldx::Matrix{AbstractFloat}`: ocean `u` velocity field +-`fldy::Matrix{AbstractFloat}`: ocean `v` velocity field +-`dx::AbstractFloat`:: x-distance over which velocity fields are provided +-`dy::AbstractFloat`: y-distance over which velocity fields are provided +## Returns: +- `::Matrix{AbstractFloat}`: ocean curl at the center of each grid cell """ function get_curl(fldx,fldy,dx,dy) # fldx must be on the u-grid point and fldy on the v-grid @@ -66,14 +96,14 @@ end """ calc_ro_field(ocean_fn) -Calculate surface vorticity for ocean file. -Inputs: - ocean_fn filename for ocean NetCDF filename -Outputs: - ro 3D array where first two dimensions are ocean size (Nx, Ny) and the third - dimension is time over the simulation - xc x grid points for ro values - yc y grid points for ro values +Calculate surface vorticity from an Oceananigan's ocean file. + +## Arguments: +- `ocean_fn::String`: $OCEAN_FN_DEF +Returns: +- `ro::Array{AbstractFloat}`: 3D array where first two dimensions are ocean size (Nx, Ny) and the third dimension is time over the simulation +- `xc::Vector{AbstractFloat}`: x grid points for ro values +- `yc::Vector{AbstractFloat}`: y grid points for ro values """ function calc_ro_field(ocean_fn) xc = NetCDF.ncread(ocean_fn, "xC") @@ -92,167 +122,4 @@ function calc_ro_field(ocean_fn) ro[:, :, i] .= get_curl(usurf[:,:,1,i], vsurf[:,:,1,i], dx,dy) ./ f end return ro, xc, yc -end - -""" - CoordPlot - -Recipe for plotting list of PolyVecs that creates two new function coordplot and -coordplot! -""" -@recipe(CoordPlot, coord_list) do scene - Attributes( - color = :lightblue, # floe fill color (could give transparent color) - strokecolor = :black, # outline color of floes - strokewidth = 1, # width of floe outline - ) -end - -""" - Makie.plot!(coordplot) - -Defines coordplot and coordplot! for plotting lists of PolyVecs. -""" -function Makie.plot!(coordplot::CoordPlot{<:Tuple{<:Vector{<:PolyVec}}}) - coord_list = coordplot[1] - poly_list = @lift([[Point2f(verts) for verts in c[1]] for c in $coord_list]) - Makie.poly!( - coordplot, - poly_list, - color = coordplot[:color], - strokecolor = coordplot[:strokecolor], - strokewidth = coordplot[:strokewidth], - ) - coordplot -end - -""" - plot_sim( - floe_fn, - initial_state_fn, - title, - Δt, - output_fn - ) - -Basic plotting of sim as an example of how to create video. Does not have -underlying ocean. -Inputs: - floe_fn Subzero floe outputwriter output file - initial_state_fn Subzero initial state writer output file - title plot title - Δt length of timestep in seconds - output_fn output filename (should be .mp4) -Output: - Saves video as output_fn -""" -function plot_sim( - floe_fn, - initial_state_fn, - Δt, - output_fn; -) - # Open files - file = jldopen(floe_fn) - domain = load(initial_state_fn)["sim"].model.domain - timesteps = keys(file["centroid"]) - # Set up observables - floes = Observable(file["coords"][timesteps[1]]) - # Plot floes - fig, ax, _ = coordplot(floes) - # Set axis limits and names - xlims!(domain.west.val, domain.east.val) - ylims!(domain.south.val, domain.north.val) - ax.xlabel = "Meters" - ax.ylabel = "Meters" - # Plot topography - if !isempty(domain.topography) - coordplot!(domain.topography.coords, color = :lightgrey) - end - # Create movie - record(fig, output_fn, timesteps; framerate = 20) do time - ax.title = Subzero.prettytime(parse(Float64, time) * Δt) - new_coords = file["coords"][time] - floes[] = new_coords - end - close(file) -end - - -""" - plot_sim_with_ocean_field( - floe_fn, - initial_state_fn, - Δt, - ocean_fn, - ocean_func, - output_fn, - ) - -Basic plotting of sim as an example of how to create video. Does not have -underlying ocean. -Inputs: - floe_fn Subzero floe outputwriter output file - initial_state_fn Subzero initial state writer output file - Δt length of timestep in seconds - ocean_fn Oceananigans output surface.nc file - ocean_func function that takes in ocean_fn and returns a - Nx by Ny by timesteps surface field for plotting as well - as xc and yc fields (see calc_ro_field for example) - colorbar_title name for colorbar associated with ocean_func vals - output_fn output filename (should be .mp4) -Output: - Saves video as output_fn. -""" -function plot_sim_with_ocean_field( - floe_fn, - initial_state_fn, - Δt, - ocean_fn, - ocean_func, - colorbar_title, - output_fn, -) - # Open files - file = jldopen(floe_fn) - domain = load(initial_state_fn)["sim"].model.domain - timesteps = keys(file["centroid"]) - ocean_data, xc, yc = ocean_func(ocean_fn) - # Set up observables needed for plotting - floes = Observable(file["coords"][timesteps[1]]) - ocean_vals = Observable(@view ocean_data[:, :, 1]) - min_ocn_val, max_ocn_val = extrema(ocean_data) - fig = Figure() - # Plot ocean - ax, hm = heatmap( - fig[1, 1], - xc, - yc, - ocean_vals, - colormap = :RdBu_9, - colorrange = (min_ocn_val, max_ocn_val) - ) - # Add axis limits and titles - xlims!(domain.west.val, domain.east.val) - ylims!(domain.south.val, domain.north.val) - ax.xlabel = "Meters" - ax.ylabel = "Meters" - # Add colorbar - Colorbar(fig[1, 2], hm, label = colorbar_title) - # Plot floes - coordplot!(fig[1, 1], floes) - # Plot topography - if !isempty(domain.topography) - coordplot!(fig[1, 1], domain.topography.coords, color = :lightgrey) - end - - # Create movie - record(fig, output_fn, 1:length(timesteps), framerate = 20) do i - time = timesteps[i] - ax.title = prettytime(parse(Float64, time) * Δt) - new_coords = file["coords"][time] - ocean_vals[] = @view ocean_data[:, :, i] - floes[] = new_coords - end - close(file) end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index 16bbf3a..3212803 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,4 +1,4 @@ -using DataStructures, JLD2, Logging, NCDatasets, Random, SplitApplyCombine, +using JLD2, Logging, NCDatasets, Random, SplitApplyCombine, Statistics, StructArrays, Subzero, VoronoiCells import GeometryOps as GO import GeometryOps.GeoInterface as GI