diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8ed9dbb..48c77b0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -54,3 +54,23 @@ jobs: - uses: codecov/codecov-action@v5 with: files: lcov.info + docs: + name: Documentation + runs-on: ubuntu-latest + timeout-minutes: 40 + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-docdeploy@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} + - run: | + julia --project=docs -e ' + using Documenter: DocMeta, doctest + using AtmosphericModels + DocMeta.setdocmeta!(AtmosphericModels, :DocTestSetup, :(using AtmosphericModels); recursive=true) + doctest(AtmosphericModels)' + diff --git a/.gitignore b/.gitignore index 7990bbb..8a9a7d0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ data/windfield_4050_500_1.0_8.2.npz examples/Manifest.toml examples/Manifest-v1.10.toml examples/Manifest-v1.11.toml +docs/build +docs/Manifest.toml diff --git a/CHANGELOG.md b/CHANGELOG.md index 5052098..f406696 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,4 +4,5 @@ - BREAKING: When constructing an atmospheric model, you MUST pass the parameter set::Settings. This ensures that all parts of the simulation use the same settings struct, and that you can run different simulations with different settings in parallel. ## Added -- The function `get_wind(am, x, y, z, t)` which returns a wind vector for the given position and time. It creates a 3D wind field if it does not exist in the data folder. The parameters of this wind field are configured in `settings.yaml`. \ No newline at end of file +- The function `get_wind(am, x, y, z, t)` which returns a wind vector for the given position and time. It creates a 3D wind field if it does not exist in the data folder. The parameters of this wind field are configured in `settings.yaml`. +- Documenter generated documentation. \ No newline at end of file diff --git a/Project.toml b/Project.toml index 41b8c6a..adfe8b7 100644 --- a/Project.toml +++ b/Project.toml @@ -16,6 +16,7 @@ Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" [compat] ControlPlots = "0.2.7" +Documenter = "1.13.0" FFTW = "1.9.0" HypergeometricFunctions = "0.3" KiteUtils = "0.6, 0.7, 0.8, 0.9, 0.10" @@ -30,8 +31,9 @@ julia = "1.10, 1.11" [extras] BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" ControlPlots = "23c2ee80-7a9e-4350-b264-8e670f12517c" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" Remez = "2e7db186-766a-50e7-8928-5c30181fb135" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test", "BenchmarkTools", "Remez", "ControlPlots"] +test = ["Test", "BenchmarkTools", "Remez", "ControlPlots", "Documenter"] diff --git a/data/settings.yaml b/data/settings.yaml index 37b6e43..f8cfab3 100644 --- a/data/settings.yaml +++ b/data/settings.yaml @@ -108,7 +108,7 @@ winch: environment: v_wind: 5.324 # wind speed at reference height [m/s] - v_wind_ref: [5.324, 0.0] # wind speed vector at reference height [m/s] + upwind_dir: -90.0 # upwind direction [deg] temp_ref: 15.0 # temperature at reference height [°C] height_gnd: 0.0 # height of groundstation above see level [m] h_ref: 6.0 # reference height for the wind speed [m] diff --git a/docs/Project.toml b/docs/Project.toml new file mode 100644 index 0000000..c2770b8 --- /dev/null +++ b/docs/Project.toml @@ -0,0 +1,3 @@ +[deps] +AtmosphericModels = "c59cac55-771d-4f45-b14d-1c681463a295" +Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" diff --git a/docs/make.jl b/docs/make.jl new file mode 100644 index 0000000..fc246b3 --- /dev/null +++ b/docs/make.jl @@ -0,0 +1,35 @@ +using AtmosphericModels +using Pkg +if ("TestEnv" ∈ keys(Pkg.project().dependencies)) + if ! ("Documenter" ∈ keys(Pkg.project().dependencies)) + using TestEnv; TestEnv.activate() + end +end +using Documenter + +DocMeta.setdocmeta!(AtmosphericModels, :DocTestSetup, :(using AtmosphericModels); recursive=true) + +makedocs(; + modules=[AtmosphericModels], + authors="Uwe Fechner and contributors", + repo="https://github.com/OpenSourceAWE/KiteUtils.jl/blob/{commit}{path}#{line}", + sitename="AtmosphericModels.jl", + checkdocs=:none, + format=Documenter.HTML(; + repolink = "https://github.com/OpenSourceAWE/AtmosphericModels.jl", + prettyurls=get(ENV, "CI", "false") == "true", + canonical="https://OpenSourceAWE.github.io/AtmosphericModels.jl", + assets=String[], + ), + pages=[ + "Home" => "index.md", + "API" => "api.md", + "Settings" => "settings.md", + ], +) + +deploydocs(; + repo="github.com/OpenSourceAWE/AtmosphericModels.jl", + devbranch="main", + push_preview=true, +) diff --git a/doc/airdensity.png b/docs/src/airdensity.png similarity index 100% rename from doc/airdensity.png rename to docs/src/airdensity.png diff --git a/docs/src/api.md b/docs/src/api.md new file mode 100644 index 0000000..fd4f4bf --- /dev/null +++ b/docs/src/api.md @@ -0,0 +1,44 @@ +```@meta +CurrentModule = AtmosphericModels +``` + +## Introduction +Most functions need an instance of the struct `AtmosphericModel` as first parameter, +which can be created using the following code: +```julia +using AtmosphericModels, KiteUtils + +set_data_path("data") +set = load_settings("system.yaml") +am::AtmosphericModel = AtmosphericModel(set) +``` +This requires that the files `system.yaml` and `settings.yaml` exist in the folder `data`. See also [Settings](@ref). + +## Types + +### Exported types +```@docs +ProfileLaw +AtmosphericModel +AtmosphericModel(set::Settings; nowindfield::Bool=false) +``` +### Private types +```@docs +WindField +``` + +## Functions + +### Wind shear and air density calculation +```@docs +clear +calc_rho +calc_wind_factor +``` +### Wind turbulence calculation +```@docs +get_wind +rel_turbo +new_windfield +new_windfields +``` \ No newline at end of file diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 0000000..832c1d4 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,129 @@ +```@meta +CurrentModule = AtmosphericModels +``` +# AtmosphericModels +This package provides functions for modelling the influence of the atmosphere on wind energy systems. It models the air density, the vertical wind profile and the wind turbulence. Further functions to import measured data are planned. + +## Installation +Install [Julia 1.10](http://www.julialang.org) or later, if you haven't already. You can add AtmosphericModels from Julia's package manager, by typing +```julia +using Pkg +pkg"add AtmosphericModels" +``` +at the Julia prompt. + +### Running the tests +Launch Julia using this project and run the tests: +```julia +julia --project +using Pkg +Pkg.test("AtmosphericModels") +``` + +### Running the examples +If you check out the project using git, you can more easily run the examples: +``` +git clone https://github.com/OpenSourceAWE/AtmosphericModels.jl +cd AtmosphericModels.jl +``` +Launch Julia using this project and run the example menu: +```julia +julia --project +include("examples/menu.jl") +``` +The first time will take some time, because the graphic libraries will get installed, the second time it is fast. + +## Usage +### Calculate the height dependant wind speed +```julia +using AtmosphericModels +am = AtmosphericModel() + +const profile_law = Int(EXPLOG) +height = 100.0 +wf = calc_wind_factor(am, height, profile_law) +``` +The result is the factor with which the ground wind speed needs to be multiplied +to get the wind speed at the given height. + +![Wind Profile](wind_profile.png) + +The `EXPLOG` profile law is the fitted linear combination of the exponential and the log law. + +### Using the turbulent wind field +You can get a wind vector as function of x,y,z and time using the following code: +```julia +using AtmosphericModels, KiteUtils + +set_data_path("data") +set = load_settings("system.yaml") +am::AtmosphericModel = AtmosphericModel(set) + +@info "Ground wind speed: $(am.set.v_wind) m/s" + +wf::WindField = WindField(am, am.set.v_wind) +x, y, z = 20.0, 0.0, 200.0 +t = 0.0 +vx, vy, vz = get_wind(wf, am, x, y, z, t) +@time get_wind(am, x, y, z, t) +@info "Wind at x=$(x), y=$(y), z=$(z), t=$(t): v_x=$(vx), v_y=$(vy), v_z=$(vz)" +@info "Wind speed: $(sqrt(vx^2 + vy^2 + vz^2)) m/s" +``` +It is suggested to check out the code using git before executing this example, +because it requires that a data directory with the correct files `system.yaml` +and `settings.yaml` exists. See below how to do that. + +### Plot a wind profile +```julia +using AtmosphericModels, KiteUtils, ControlPlots +am = AtmosphericModel(se()) + +heights = 6:1000 +wf = [calc_wind_factor(am, height, Int(EXPLOG)) for height in heights] + +plot(heights, wf, xlabel="height [m]", ylabel="wind factor") +``` + +```julia +using AtmosphericModels, ControlPlots, KiteUtils +am = AtmosphericModel(se()) +AtmosphericModels.se().alpha = 0.234 # set the exponent of the power law + +heights = 6:200 +wf = [calc_wind_factor(am, height, Int(EXP)) for height in heights] + +plot(heights, wf, xlabel="height [m]", ylabel="wind factor") +``` + +### Air density +```julia +using AtmosphericModels, BenchmarkTools, KiteUtils +am = AtmosphericModel(se()) +@benchmark calc_rho(am, height) setup=(height=Float64((6.0+rand()*500.0))) +``` +This gives 4.85 ns as result. Plot the air density: +```julia +heights = 6:1000 +rhos = [calc_rho(am, height) for height in heights] +plot(heights, rhos, legend=false, xlabel="height [m]", ylabel="air density [kg/m³]") +``` +![Airdensity](airdensity.png) + +## Further reading +These models are described in detail in [Dynamic Model of a Pumping Kite Power System](http://arxiv.org/abs/1406.6218). + +## Licence +This project is licensed under the MIT License. Please see the below WAIVER in association with the license. + +## WAIVER +Technische Universiteit Delft hereby disclaims all copyright interest in the package “AtmosphericModels.jl” (models for airborne wind energy systems) written by the Author(s). + +Prof.dr. H.G.C. (Henri) Werij, Dean of Aerospace Engineering + +## See also +- [Research Fechner](https://research.tudelft.nl/en/publications/?search=Uwe+Fechner&pageSize=50&ordering=rating&descending=true) +- The application [KiteViewer](https://github.com/ufechner7/KiteViewer) +- the package [KiteUtils](https://github.com/ufechner7/KiteUtils.jl) +- the packages [KiteModels](https://github.com/ufechner7/KiteModels.jl) and [WinchModels](https://github.com/aenarete/WinchModels.jl) and [KitePodModels](https://github.com/aenarete/KitePodModels.jl) +- the packages [KiteControllers](https://github.com/aenarete/KiteControllers.jl) and [KiteViewers](https://github.com/aenarete/KiteViewers.jl) + diff --git a/docs/src/settings.md b/docs/src/settings.md new file mode 100644 index 0000000..46e0fd7 --- /dev/null +++ b/docs/src/settings.md @@ -0,0 +1,44 @@ +```@meta +CurrentModule = AtmosphericModels +``` + +## Settings +The parameters af the atmospheric model can be configured in the section `environment` of the `settings.yaml` file in the `data` folder. + +The file `system.yaml` specifies which `yaml` files are used to configure +the current project. + +### Example for system.yaml +```yaml +system: + project: "settings.yaml" # simulator settings +``` +Often additional `yaml` files, for example for the controller settings are used. + +### Example for settings.yaml +```yaml +environment: + v_wind: 5.324 # wind speed at reference height [m/s] + upwind_dir: -90.0 # upwind direction [deg] + temp_ref: 15.0 # temperature at reference height [°C] + height_gnd: 0.0 # height of groundstation above see level [m] + h_ref: 6.0 # reference height for the wind speed [m] + + rho_0: 1.225 # air density at zero height and 15 °C [kg/m³] + alpha: 0.234 # exponent of the wind profile law + z0: 0.0002 # surface roughness [m] + profile_law: 1 # 1=EXP, 2=LOG, 3=EXPLOG, 4=FAST_EXP, 5=FAST_LOG, 6=FAST_EXPLOG + # the following parameters are for calculating the turbulent wind field using the Mann model + use_turbulence: 1.0 # turbulence intensity relative to Cabauw, NL + v_wind_gnds: [3.483, 5.324, 8.163] # wind speeds at ref height for calculating the turbulent wind field [m/s] + avg_height: 200.0 # average height during reel out [m] + rel_turbs: [0.342, 0.465, 0.583] # relative turbulence at the v_wind_gnds + i_ref: 0.14 # is the expected value of the turbulence intensity at 15 m/s. + v_ref: 42.9 # five times the average wind speed in m/s at hub height over the full year [m/s] + # Cabauw: 8.5863 m/s * 5.0 = 42.9 m/s + height_step: 2.0 # use a grid with 2m resolution in z direction [m] + grid_step: 2.0 # grid resolution in x and y direction [m] +``` + +## Remarks +- If the parameter `use_turbulence` is zero, no windfield is loaded. \ No newline at end of file diff --git a/doc/wind_profile.png b/docs/src/wind_profile.png similarity index 100% rename from doc/wind_profile.png rename to docs/src/wind_profile.png diff --git a/scripts/build_docu.jl b/scripts/build_docu.jl new file mode 100644 index 0000000..f88f437 --- /dev/null +++ b/scripts/build_docu.jl @@ -0,0 +1,24 @@ +# build and display the html documentation locally +# you must have installed the package LiveServer in your global environment + +using Pkg + +function globaldependencies() + projectpath = Pkg.project().path + basepath, _ = splitdir(projectpath) + Pkg.activate() + globaldependencies = keys(Pkg.project().dependencies) + Pkg.activate(basepath) + globaldependencies +end + +if !("LiveServer" in globaldependencies()) + println("Installing LiveServer globally!") + run(`julia -e 'using Pkg; Pkg.add("LiveServer")'`) +end + +if !("Documenter" ∈ keys(Pkg.project().dependencies)) + using TestEnv + TestEnv.activate() +end +using LiveServer; servedocs(launch_browser=true) diff --git a/src/AtmosphericModels.jl b/src/AtmosphericModels.jl index f3ac2e1..57e843a 100644 --- a/src/AtmosphericModels.jl +++ b/src/AtmosphericModels.jl @@ -13,6 +13,30 @@ export new_windfield, new_windfields, get_wind const ABS_ZERO = -273.15 const SRL = StepRangeLen{Float64, Base.TwicePrecision{Float64}, Base.TwicePrecision{Float64}, Int64} +""" + struct WindField + +Struct that is storing a 3D model of wind vectors of the atmosphere. The Fields +x, y and z store the grid coordinates, the fields u, v and w the wind turbulence +vectors. + +# Fields +- x_max::Float64 = NaN +- x_min::Float64 = NaN +- y_max::Float64 = NaN +- y_min::Float64 = NaN +- z_max::Float64 = NaN +- z_min::Float64 = NaN +- last_speed::Float64 = 0.0 +- valid::Bool = false +- x::Union{SRL, Array{Float64, 3}} +- y::Union{SRL, Array{Float64, 3}} +- z::Union{SRL, Array{Float64, 3}} +- u::Array{Float64, 3} +- v::Array{Float64, 3} +- w::Array{Float64, 3} +- param::Vector{Float64} = [0, 0] # [alpha, `v_wind_gnd`] +""" Base.@kwdef struct WindField x_max::Float64 = NaN x_min::Float64 = NaN @@ -35,6 +59,11 @@ end mutable struct AtmosphericModel Struct that is storing the settings and the state of the atmosphere. + +# Fields +- set::Settings: The Settings struct +- `rho_zero_temp` +- wf::Union{WindField, Nothing}: The 3D [`WindField`](@ref) or `nothing` """ Base.@kwdef mutable struct AtmosphericModel set::Settings @@ -42,6 +71,18 @@ Base.@kwdef mutable struct AtmosphericModel wf::Union{WindField, Nothing} = nothing end +""" + AtmosphericModel(set::Settings; nowindfield::Bool=false) + +Constructs an `AtmosphericModel` using the provided `Settings`. + +# Arguments +- `set::Settings`: The settings object containing configuration parameters for the atmospheric model. +- `nowindfield::Bool=false`: Optional keyword argument. If `true`, the wind field will not be loaded. + +# Returns +- An instance of `AtmosphericModel` configured according to the provided settings. +""" function AtmosphericModel(set::Settings; nowindfield::Bool=false) am = AtmosphericModel(set=set) if set.use_turbulence > 0 && !nowindfield @@ -52,8 +93,20 @@ end const AM = AtmosphericModel +""" + clear(s::AM) + +Clears or resets the state of the given `AM` (Atmospheric Model) instance `s`. + +# Arguments +- `s::AM`: An instance of the `AM` (Atmospheric Model) struct containing atmospheric parameters. + +# Returns +- nothing +""" function clear(s::AM) s.rho_zero_temp = (15.0 - ABS_ZERO) / (s.set.temp_ref - ABS_ZERO) * s.set.rho_0 + nothing end @inline function fastexp(x) @@ -63,7 +116,24 @@ end y end -# Calculate the air densisity as function of height +""" + calc_rho(s::AM, height) + +Calculates the air density at a given height above ground level. + +# Arguments +- `s::AM`: An instance of the `AM` (Atmospheric Model) struct containing atmospheric parameters. +- `height`: The height above ground level (in meters) at which to calculate the air density. + +# Returns +- The air density at the specified height (in kg/m³). + +# Notes +- The calculation assumes an exponential decrease of air density with altitude. +- `s.rho_zero_temp` is the reference air density at ground level. +- `s.set.height_gnd` is the ground height offset. +- The scale height used is 8550.0 meters. +""" calc_rho(s::AM, height) = s.rho_zero_temp * fastexp(-(height+s.set.height_gnd) / 8550.0) """ @@ -144,6 +214,20 @@ function calc_wind_factor6(s::AM, height) evalpoly(1/height, (1.0, 1735.2333827029918, 279373.0012683715)) end +""" + calc_wind_factor(am::AM, height; profile_law::Int64=am.set.profile_law) + +Calculates the wind factor at a given `height` using the specified wind profile law. + +# Arguments +- `am::AM`: An instance of the `AM` type containing atmospheric model parameters. +- `height`: The height (in meters) at which to calculate the wind factor. +- `profile_law::Int64`: (Optional) The wind profile law to use for the calculation. + Defaults to `am.set.profile_law`. + +# Returns +- The wind factor at the specified height as determined by the chosen profile law. +""" @inline function calc_wind_factor(am::AM, height, profile_law::Int64=am.set.profile_law) if profile_law == 1 calc_wind_factor1(am, height) diff --git a/src/windfield.jl b/src/windfield.jl index 3961e46..34bfab0 100644 --- a/src/windfield.jl +++ b/src/windfield.jl @@ -50,6 +50,18 @@ function calc_sigma1(am, v_wind_gnd) am.set.i_ref * (0.75 * v_height + 5.6) end +""" + rel_turbo(am::AtmosphericModel, v_wind = am.set.v_wind) + +Find the closest relative turbulence value for a given ground wind speed. + +# Arguments +- `am::AtmosphericModel`: The atmospheric model instance containing relevant parameters. +- `v_wind`: (Optional) The wind velocity to use for the calculation. Defaults to `am.set.v_wind`. + +# Returns +- The computed relative turbulence value. +""" function rel_turbo(am::AtmosphericModel, v_wind = am.set.v_wind) # Find the closest relative turbulence value for a given ground wind speed min_dist, idx = findmin(abs.(am.set.v_wind_gnds .- v_wind)) @@ -271,13 +283,12 @@ function create_windfield(x, y, z; sigma1=nothing, gamma=3.9, ae=0.1, length_sca end """ - get_wind(wf::WindField, am::AtmosphericModel, x, y, z, t; interpolate=false) + get_wind(am::AtmosphericModel, x, y, z, t; interpolate=false) Returns the wind vector at the specified position (`x`, `y`, `z`) and time `t` using the given -`WindField` (`wf`) and `AtmosphericModel` (`am`). +`AtmosphericModel` (`am`). # Arguments -- `wf::WindField`: The wind field object containing wind data. - `am::AtmosphericModel`: The atmospheric model providing environmental parameters. - `x`, `y`, `z`: Coordinates specifying the location where the wind is to be evaluated. [m] - `t`: Time at which the wind is to be evaluated. [s] @@ -346,7 +357,7 @@ end """ new_windfield(am::AtmosphericModel, v_wind_gnd; prn=true) -Create a new wind field object using the given ground wind velocity vector `v_wind_gnd`. +Create a new wind field file using the given, scalar ground wind velocity `v_wind_gnd`. # Parameters - `am::AtmosphericModel`: The atmospheric model for which the wind field is created. @@ -354,7 +365,7 @@ Create a new wind field object using the given ground wind velocity vector `v_wi - `prn`: Optional boolean flag to control printing of progress messages (default is `true`). # Returns -nothing +- nothing """ function new_windfield(am::AtmosphericModel, v_wind_gnd; prn=true) Random.seed!(1234) @@ -369,13 +380,14 @@ function new_windfield(am::AtmosphericModel, v_wind_gnd; prn=true) end """ - new_windfields(am::AtmosphericModel) + new_windfields(am::AtmosphericModel; prn=true) Create and initialize new wind fields for all ground wind speeds, defined in `am.set.v_wind_gnds` and save them for the given `AtmosphericModel` instance `am`. # Arguments - `am::AtmosphericModel`: The atmospheric model for which wind fields are to be generated. +- `prn`: Optional boolean flag to control printing of progress messages (default is `true`). # Returns - nothing