diff --git a/.dev/Project.toml b/.dev/Project.toml new file mode 100644 index 00000000..71708c80 --- /dev/null +++ b/.dev/Project.toml @@ -0,0 +1,5 @@ +[deps] +JuliaFormatter = "98e50ef6-434e-11e9-1051-2b60c6c9e899" + +[compat] +JuliaFormatter = "1" diff --git a/.dev/clima_formatter_options.jl b/.dev/clima_formatter_options.jl new file mode 100644 index 00000000..1db65e8c --- /dev/null +++ b/.dev/clima_formatter_options.jl @@ -0,0 +1,8 @@ +clima_formatter_options = ( + indent = 4, + margin = 80, + always_for_in = true, + whitespace_typedefs = true, + whitespace_ops_in_indices = true, + remove_extra_newlines = false, +) diff --git a/.dev/climaformat.jl b/.dev/climaformat.jl new file mode 100644 index 00000000..713a5189 --- /dev/null +++ b/.dev/climaformat.jl @@ -0,0 +1,85 @@ +#!/usr/bin/env julia +# +# This is an adapted version of format.jl from JuliaFormatter with the +# following license: +# +# MIT License +# Copyright (c) 2019 Dominique Luna +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to permit +# persons to whom the Software is furnished to do so, subject to the +# following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +# NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +# USE OR OTHER DEALINGS IN THE SOFTWARE. +# +using Pkg +Pkg.activate(@__DIR__) +Pkg.instantiate() + +using JuliaFormatter + +include("clima_formatter_options.jl") + +help = """ +Usage: climaformat.jl [flags] [FILE/PATH]... + +Formats the given julia files using the CLIMA formatting options. If paths +are given it will format the julia files in the paths. Otherwise, it will +format all changed julia files. + + -v, --verbose + Print the name of the files being formatted with relevant details. + + -h, --help + Print this message. +""" + +function parse_opts!(args::Vector{String}) + i = 1 + opts = Dict{Symbol, Union{Int, Bool}}() + while i ≤ length(args) + arg = args[i] + if arg[1] != '-' + i += 1 + continue + end + if arg == "-v" || arg == "--verbose" + opt = :verbose + elseif arg == "-h" || arg == "--help" + opt = :help + else + error("invalid option $arg") + end + if opt in (:verbose, :help) + opts[opt] = true + deleteat!(args, i) + end + end + return opts +end + +opts = parse_opts!(ARGS) +if haskey(opts, :help) + write(stdout, help) + exit(0) +end +if isempty(ARGS) + filenames = readlines(`git ls-files "*.jl"`) +else + filenames = ARGS +end + +format(filenames; clima_formatter_options..., opts...) diff --git a/.dev/pre-commit b/.dev/pre-commit new file mode 100644 index 00000000..0e56ed08 --- /dev/null +++ b/.dev/pre-commit @@ -0,0 +1,41 @@ +#!/usr/bin/env julia +# +# Called by git-commit with no arguments. This checks to make sure that all +# .jl files are indented correctly before a commit is made. +# +# To enable this hook, make this file executable and copy it in +# $GIT_DIR/hooks. + +toplevel_directory = chomp(read(`git rev-parse --show-toplevel`, String)) + +using Pkg +Pkg.activate(joinpath(toplevel_directory, ".dev")) +Pkg.instantiate() + +using JuliaFormatter + +include(joinpath(toplevel_directory, ".dev", "clima_formatter_options.jl")) + +needs_format = false + +for diffoutput in split.(readlines(`git diff --name-status --cached`)) + status = diffoutput[1] + filename = diffoutput[end] + (!startswith(status, "D") && endswith(filename, ".jl")) || continue + + a = read(`git show :$filename`, String) + b = format_text(a; clima_formatter_options...) + + if a != b + fullfilename = joinpath(toplevel_directory, filename) + + @error """File $filename needs to be indented with: + julia $(joinpath(toplevel_directory, ".dev", "climaformat.jl")) $fullfilename + and added to the git index via + git add $fullfilename + """ + global needs_format = true + end +end + +exit(needs_format ? 1 : 0) diff --git a/.dev/up_deps.jl b/.dev/up_deps.jl new file mode 100644 index 00000000..2ca708eb --- /dev/null +++ b/.dev/up_deps.jl @@ -0,0 +1,36 @@ +#= +A simple script for updating the manifest +files in all of our environments. +=# + +root = dirname(@__DIR__) +dirs = ( + root, + joinpath(root, "test"), + joinpath(root, ".dev"), + joinpath(root, "perf"), + joinpath(root, "docs"), + joinpath(root, "examples"), +) + +cd(root) do + for dir in dirs + reldir = relpath(dir, root) + @info "Updating environment `$reldir`" + cmd = if dir == root + `$(Base.julia_cmd()) --project -e """import Pkg; Pkg.update()"""` + elseif dir == joinpath(root, ".dev") + `$(Base.julia_cmd()) --project=$reldir -e """import Pkg; Pkg.update()"""` + else + `$(Base.julia_cmd()) --project=$reldir -e """import Pkg; Pkg.develop(;path=\".\"); Pkg.update()"""` + end + run(cmd) + end +end + +# https://github.com/JuliaLang/Pkg.jl/issues/3014 +for dir in dirs + cd(dir) do + rm("LocalPreferences.toml"; force = true) + end +end diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml new file mode 100644 index 00000000..bcdb51ab --- /dev/null +++ b/.github/workflows/CompatHelper.yml @@ -0,0 +1,16 @@ +name: CompatHelper +on: + schedule: + - cron: '00 00 * * *' + workflow_dispatch: +jobs: + CompatHelper: + runs-on: ubuntu-latest + steps: + - name: Pkg.add("CompatHelper") + run: julia -e 'using Pkg; Pkg.add("CompatHelper")' + - name: CompatHelper.main() + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMPATHELPER_PRIV: ${{ secrets.COMPATHELPER_PRIV }} # optional + run: julia -e 'using CompatHelper; CompatHelper.main()' diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml new file mode 100644 index 00000000..f2dbf34c --- /dev/null +++ b/.github/workflows/TagBot.yml @@ -0,0 +1,67 @@ +name: TagBot +on: + issue_comment: + types: + - created + workflow_dispatch: +jobs: + TagBot: + if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' + runs-on: ubuntu-latest + steps: + - uses: JuliaRegistries/TagBot@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ssh: ${{ secrets.DOCUMENTER_KEY }} + changelog: | + ## {{ package }} {{ version }} + {% if previous_release %} + [Diff since {{ previous_release }}]({{ compare_url }}) + {% endif %} + {% if custom %} + {{ custom }} + {% endif %} + ### 📢 API Changes: + {% if issues %} + {% for issue in issues if 'API' in issue.labels %} + - {{ issue.title }} (#{{ issue.number }}) + {% endfor %} + {% endif %} + {% if pulls %} + {% for pull in pulls if 'API' in pull.labels %} + - {{ pull.title }} (#{{ pull.number }}) (@{{ pull.author.username }}) + {% endfor %} + {% endif %} + ### 🚀 Features + {% if issues %} + {% for issue in issues if 'enhancement' in issue.labels or 'feature' in issue.labels %} + - {{ issue.title }} (#{{ issue.number }}) + {% endfor %} + {% endif %} + {% if pulls %} + {% for pull in pulls if 'enhancement' in pull.labels or 'feature' in pull.labels %} + - {{ pull.title }} (#{{ pull.number }}) (@{{ pull.author.username }}) + {% endfor %} + {% endif %} + ### 📑 Documentation + {% if issues %} + {% for issue in issues if 'documentation' in issue.labels %} + - {{ issue.title }} (#{{ issue.number }}) + {% endfor %} + {% endif %} + {% if pulls %} + {% for pull in pulls if 'documentation' in pull.labels %} + - {{ pull.title }} (#{{ pull.number }}) (@{{ pull.author.username }}) + {% endfor %} + {% endif %} + ### 🐛 Fixes + {% if issues %} + {% for issue in issues if 'bug' in issue.labels or 'bugfix' in issue.labels %} + - {{ issue.title }} (#{{ issue.number }}) + {% endfor %} + {% endif %} + {% if pulls %} + {% for pull in pulls if 'bug' in pull.labels or 'bugfix' in pull.labels %} + - {{ pull.title }} (#{{ pull.number }}) (@{{ pull.author.username }}) + {% endfor %} + {% endif %} diff --git a/.github/workflows/julia_formatter.yml b/.github/workflows/julia_formatter.yml new file mode 100644 index 00000000..706dd84f --- /dev/null +++ b/.github/workflows/julia_formatter.yml @@ -0,0 +1,44 @@ +name: JuliaFormatter + +on: + push: + branches: + - main + - trying + - staging + tags: '*' + pull_request: + +jobs: + format: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Cancel Previous Runs + uses: styfle/cancel-workflow-action@0.4.0 + with: + access_token: ${{ github.token }} + + - uses: actions/checkout@v2.2.0 + + - uses: dorny/paths-filter@v2.9.1 + id: filter + with: + filters: | + julia_file_change: + - added|modified: '**.jl' + + - uses: julia-actions/setup-julia@latest + if: steps.filter.outputs.julia_file_change == 'true' + with: + version: '1.8' + + - name: Apply JuliaFormatter + if: steps.filter.outputs.julia_file_change == 'true' + run: | + julia --project=.dev .dev/climaformat.jl . + + - name: Check formatting diff + if: steps.filter.outputs.julia_file_change == 'true' + run: | + git diff --color=always --exit-code diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f21dd09c --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# Simulation outputs +*.png +*.hdf5 + +# System-specific files and directories generated by the BinaryProvider and BinDeps packages +# They contain absolute paths specific to the host computer, and so should not be committed +deps/deps.jl +deps/build.log +deps/downloads/ +deps/usr/ +deps/src/ + +# Build artifacts for creating documentation generated by the Documenter package +docs/build/ +docs/site/ + +# Julia package dependencies +/Manifest.toml +/*/Manifest.toml + +# misc +.DS_Store \ No newline at end of file diff --git a/Project.toml b/Project.toml new file mode 100644 index 00000000..87dde3bc --- /dev/null +++ b/Project.toml @@ -0,0 +1,4 @@ +name = "CalibrateClimaAtmos" +uuid = "4347a170-ebd6-470c-89d3-5c705c0cacc2" +authors = ["Climate Modeling Alliance"] +version = "0.1.0" diff --git a/examples/Project.toml b/examples/Project.toml new file mode 100644 index 00000000..e6043503 --- /dev/null +++ b/examples/Project.toml @@ -0,0 +1,2 @@ +[deps] +ClimaAtmos = "b2c96348-7fb7-4fe0-8da9-78d88439e717" diff --git a/plot/Project.toml b/plot/Project.toml new file mode 100644 index 00000000..50f1320c --- /dev/null +++ b/plot/Project.toml @@ -0,0 +1,4 @@ +[deps] +CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" +ClimaComms = "3a4d1b5c-c61d-41fd-a00a-5873ba7a1b0d" +ClimaCore = "d414da3d-4745-48bb-8d80-42e94e092884" diff --git a/plot/latitude_contour_plots.jl b/plot/latitude_contour_plots.jl new file mode 100644 index 00000000..4ccd99a4 --- /dev/null +++ b/plot/latitude_contour_plots.jl @@ -0,0 +1,125 @@ +import CairoMakie: Makie +import ClimaComms +import ClimaCore: Fields, Geometry, Spaces, InputOutput +import Statistics: mean + +# Adapted from contours_and_plots.jl in ClimaAtmos + +function time_from_filename(file) + arr = split(basename(file), ".") + day = parse(Float64, replace(arr[1], "day" => "")) + sec = parse(Float64, arr[2]) + return day * (60 * 60 * 24) + sec +end + +function read_hdf5_file(file_path) + reader = InputOutput.HDF5Reader( + file_path, + ClimaComms.SingletonCommsContext(ClimaComms.CPUSingleThreaded()), + ) + diagnostics = InputOutput.read_field(reader, "diagnostics") + close(reader) + return diagnostics +end + +horizontal_space(diagnostics) = + Spaces.horizontal_space(axes(diagnostics.temperature)) + +is_on_sphere(diagnostics) = + eltype(Fields.coordinate_field(horizontal_space(diagnostics))) <: + Geometry.LatLongPoint + +get_column_1(diagnostics) = + isnothing(diagnostics) ? (nothing, nothing) : + column_view_diagnostics(diagnostics, ((1, 1), 1)) + +function column_view_diagnostics(diagnostics, column) + ((i, j), h) = column + is_extruded_field(object) = + axes(object) isa Spaces.ExtrudedFiniteDifferenceSpace + column_view(field) = Fields.column(field, i, j, h) + column_zs(object) = + is_extruded_field(object) ? + vec(parent(Fields.coordinate_field(column_view(object)).z)) / 1000 : + nothing + column_values(object) = + is_extruded_field(object) ? vec(parent(column_view(object))) : nothing + objects = Fields._values(diagnostics) + + column_zs_and_values = map(column_zs, objects), map(column_values, objects) + + # Assume that all variables have the same horizontal coordinates. + coords = Fields.coordinate_field(column_view(diagnostics.temperature)) + + coord_strings = map(filter(!=(:z), propertynames(coords))) do symbol + # Add 0 to every horizontal coordinate value so that -0.0 gets + # printed without the unnecessary negative sign. + value = round(mean(getproperty(coords, symbol)); sigdigits = 6) + 0 + return "$symbol = $value" + end + col_string = "column data from $(join(coord_strings, ", "))" + + return column_zs_and_values, col_string +end + +column_at_coord_getter(latitude, longitude) = + diagnostics -> begin + isnothing(diagnostics) && return (nothing, nothing) + column = if is_on_sphere(diagnostics) + horz_space = horizontal_space(diagnostics) + horz_coords = Fields.coordinate_field(horz_space) + FT = eltype(eltype(horz_coords)) + target_column_coord = + Geometry.LatLongPoint(FT(latitude), FT(longitude)) + distance_to_target(((i, j), h)) = + Geometry.great_circle_distance( + Spaces.column(horz_coords, i, j, h)[], + target_column_coord, + horz_space.global_geometry, + ) + argmin(distance_to_target, Spaces.all_nodes(horz_space)) + else + ((1, 1), 1) # If the data is not on a sphere, extract column 1. + end + return column_view_diagnostics(diagnostics, column) + end + +""" + latitude_contour_plot(file_path, variable; longitude = 0, out_path = nothing) + +Saves a contour plot of latitude versus height for the given variable at the +given longitude. Default longitude = 0 +""" +function latitude_contour_plot( + file_path, + variable; + longitude = 0, + out_path = nothing, +) + diagnostics = read_hdf5_file(file_path) + latitudes = -90:5:90 + cols = map(latitudes) do lat + column_at_coord_getter(lat, longitude)(diagnostics)[1] + end + variable_cols = map(cols) do (zs, values) + getproperty(values, variable) + end + zs = getproperty(cols[1][1], variable) + values = hcat(variable_cols...) + + figure = Makie.Figure() + axis = Makie.Axis( + figure[1, 1], + xlabel = "Latitude", + ylabel = "Height (km)", + title = "$variable at $longitude long", + ) + Makie.contourf!(axis, latitudes, zs, values') + Makie.contourf!(axis, latitudes, zs, values') + if isnothing(out_path) + out_path = + chop(file_path, head = 0, tail = 5) * "_$(variable)_$longitude.png" + end + Makie.save(out_path, figure) + return nothing +end diff --git a/src/CalibrateClimaAtmos.jl b/src/CalibrateClimaAtmos.jl new file mode 100644 index 00000000..62ef4498 --- /dev/null +++ b/src/CalibrateClimaAtmos.jl @@ -0,0 +1,4 @@ +module CalibrateClimaAtmos + + +end # module CalibrateClimaAtmos diff --git a/test/runtests.jl b/test/runtests.jl new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/test/runtests.jl @@ -0,0 +1 @@ +