|
| 1 | +# Building models with ModelingToolkit.jl |
| 2 | + |
| 3 | +SolarPosition.jl provides a [`ModelingToolkit.jl`](https://github.com/SciML/ModelingToolkit.jl) |
| 4 | +extension that enables integration of solar position calculations into symbolic modeling |
| 5 | +workflows. This allows you to compose solar position components with other physical |
| 6 | +systems for applications like solar energy modeling, building thermal analysis, and |
| 7 | +solar tracking systems. |
| 8 | + |
| 9 | +## Installation |
| 10 | + |
| 11 | +The ModelingToolkit extension is loaded automatically when both [`SolarPosition.jl`](https://github.com/JuliaAstro/SolarPosition.jl) and [`ModelingToolkit.jl`](https://github.com/SciML/ModelingToolkit.jl) |
| 12 | +are loaded: |
| 13 | + |
| 14 | +```julia |
| 15 | +using SolarPosition |
| 16 | +using ModelingToolkit |
| 17 | +``` |
| 18 | + |
| 19 | +## Quick Start |
| 20 | + |
| 21 | +The extension provides the [`SolarPositionBlock`](@ref) component, which outputs solar |
| 22 | +azimuth, elevation, and zenith angles as time-varying quantities. |
| 23 | + |
| 24 | +```@example mtk |
| 25 | +using SolarPosition |
| 26 | +using ModelingToolkit |
| 27 | +using ModelingToolkit: t_nounits as t |
| 28 | +using Dates |
| 29 | +using OrdinaryDiffEq |
| 30 | +``` |
| 31 | + |
| 32 | +```@example mtk |
| 33 | +# Create a solar position block |
| 34 | +@named sun = SolarPositionBlock() |
| 35 | +
|
| 36 | +# Define observer location and reference time |
| 37 | +obs = Observer(51.50274937708521, -0.17782150375214803, 15.0) # Natural History Museum |
| 38 | +t0 = DateTime(2024, 6, 21, 12, 0, 0) # Summer solstice noon |
| 39 | +
|
| 40 | +# Compile the system |
| 41 | +sys = mtkcompile(sun) |
| 42 | +
|
| 43 | +# Set parameters using the compiled system's parameter references |
| 44 | +pmap = [ |
| 45 | + sys.observer => obs, |
| 46 | + sys.t0 => t0, |
| 47 | + sys.algorithm => PSA(), |
| 48 | + sys.refraction => NoRefraction(), |
| 49 | +] |
| 50 | +
|
| 51 | +# Solve over 24 hours (time in seconds) |
| 52 | +tspan = (0.0, 86400.0) |
| 53 | +prob = ODEProblem(sys, pmap, tspan) |
| 54 | +sol = solve(prob; saveat = 3600.0) # Save every hour |
| 55 | +
|
| 56 | +# Show some results |
| 57 | +println("Solar position at noon (t=12 hours):") |
| 58 | +println(" Azimuth: ", round(sol[sys.azimuth][1], digits=2), "°") |
| 59 | +println(" Elevation: ", round(sol[sys.elevation][1], digits=2), "°") |
| 60 | +println(" Zenith: ", round(sol[sys.zenith][1], digits=2), "°") |
| 61 | +``` |
| 62 | + |
| 63 | +## SolarPositionBlock |
| 64 | + |
| 65 | +The [`SolarPositionBlock`](@ref) is a [`ModelingToolkit.jl`](https://github.com/SciML/ModelingToolkit.jl) component that computes solar position angles based on time, observer location, and |
| 66 | +chosen positioning and refraction algorithms. |
| 67 | + |
| 68 | +```@docs |
| 69 | +SolarPositionBlock |
| 70 | +``` |
| 71 | + |
| 72 | +## Composing with Other Systems |
| 73 | + |
| 74 | +The real power of the ModelingToolkit extension comes from composing solar position with other physical systems. |
| 75 | + |
| 76 | +### Example: Solar Panel Power Model |
| 77 | + |
| 78 | +```@example mtk |
| 79 | +using CairoMakie: Figure, Axis, lines! |
| 80 | +
|
| 81 | +# Create solar position block |
| 82 | +@named sun = SolarPositionBlock() |
| 83 | +
|
| 84 | +# Create a simple solar panel model |
| 85 | +@parameters begin |
| 86 | + area = 10.0 # Panel area (m²) |
| 87 | + efficiency = 0.2 # Panel efficiency (20%) |
| 88 | + dni_peak = 1000.0 # Peak direct normal irradiance (W/m²) |
| 89 | +end |
| 90 | +
|
| 91 | +@variables begin |
| 92 | + irradiance(t) = 0.0 # Effective irradiance on panel (W/m²) |
| 93 | + power(t) = 0.0 # Power output (W) |
| 94 | +end |
| 95 | +
|
| 96 | +# Simplified model: irradiance depends on sun elevation |
| 97 | +# In reality, you'd account for panel orientation, azimuth, etc. |
| 98 | +eqs = [ |
| 99 | + irradiance ~ dni_peak * max(0, sind(sun.elevation)), |
| 100 | + power ~ area * efficiency * irradiance, |
| 101 | +] |
| 102 | +
|
| 103 | +# Compose the complete system |
| 104 | +@named model = System(eqs, t; systems = [sun]) |
| 105 | +sys_model = mtkcompile(model) |
| 106 | +
|
| 107 | +# Set up and solve |
| 108 | +obs = Observer(37.7749, -122.4194, 100.0) |
| 109 | +t0 = DateTime(2024, 6, 21, 0, 0, 0) |
| 110 | +
|
| 111 | +pmap = [ |
| 112 | + sys_model.sun.observer => obs, |
| 113 | + sys_model.sun.t0 => t0, |
| 114 | + sys_model.sun.algorithm => PSA(), |
| 115 | + sys_model.sun.refraction => NoRefraction(), |
| 116 | +] |
| 117 | +
|
| 118 | +prob = ODEProblem(sys_model, pmap, (0.0, 86400.0)) |
| 119 | +sol = solve(prob; saveat = 600.0) # Save every 10 minutes |
| 120 | +
|
| 121 | +# Plot results |
| 122 | +fig = Figure(size = (1000, 400)) |
| 123 | +
|
| 124 | +ax1 = Axis(fig[1, 1]; xlabel = "Time (hours)", ylabel = "Elevation (°)", title = "Solar Elevation") |
| 125 | +lines!(ax1, sol.t ./ 3600, sol[sys_model.sun.elevation]) |
| 126 | +
|
| 127 | +ax2 = Axis(fig[1, 2]; xlabel = "Time (hours)", ylabel = "Power (W)", title = "Solar Panel Power") |
| 128 | +lines!(ax2, sol.t ./ 3600, sol[sys_model.power]) |
| 129 | +
|
| 130 | +fig |
| 131 | +``` |
| 132 | + |
| 133 | +### Example: Building Thermal Model with Solar Gain |
| 134 | + |
| 135 | +```@example mtk |
| 136 | +using CairoMakie: Figure, Axis, lines! |
| 137 | +using ModelingToolkit: D_nounits as D |
| 138 | +
|
| 139 | +# Solar position component |
| 140 | +@named sun = SolarPositionBlock() |
| 141 | +
|
| 142 | +# Building thermal model with solar gain |
| 143 | +@parameters begin |
| 144 | + mass = 1000.0 # Thermal mass (kg) |
| 145 | + cp = 1000.0 # Specific heat capacity (J/(kg·K)) |
| 146 | + U = 0.5 # Overall heat transfer coefficient (W/(m²·K)) |
| 147 | + wall_area = 50.0 # Wall area (m²) |
| 148 | + window_area = 5.0 # Window area (m²) |
| 149 | + window_trans = 0.7 # Window transmittance |
| 150 | + T_outside = 20.0 # Outside temperature (°C) |
| 151 | + dni_peak = 800.0 # Peak solar irradiance (W/m²) |
| 152 | +end |
| 153 | +
|
| 154 | +@variables begin |
| 155 | + T(t) = 20.0 # Room temperature (°C) |
| 156 | + Q_loss(t) # Heat loss through walls (W) |
| 157 | + Q_solar(t) # Solar heat gain (W) |
| 158 | + irradiance(t) # Solar irradiance (W/m²) |
| 159 | +end |
| 160 | +
|
| 161 | +eqs = [ |
| 162 | + # Solar irradiance based on sun elevation |
| 163 | + irradiance ~ dni_peak * max(0, sind(sun.elevation)), |
| 164 | + # Solar heat gain through windows |
| 165 | + Q_solar ~ window_area * window_trans * irradiance, |
| 166 | + # Heat loss through walls |
| 167 | + Q_loss ~ U * wall_area * (T - T_outside), |
| 168 | + # Energy balance |
| 169 | + D(T) ~ (Q_solar - Q_loss) / (mass * cp), |
| 170 | +] |
| 171 | +
|
| 172 | +@named building = System(eqs, t; systems = [sun]) |
| 173 | +sys_building = mtkcompile(building) |
| 174 | +
|
| 175 | +# Simulate |
| 176 | +obs = Observer(40.7128, -74.0060, 100.0) # New York City |
| 177 | +t0 = DateTime(2024, 6, 21, 0, 0, 0) |
| 178 | +
|
| 179 | +pmap = [ |
| 180 | + sys_building.sun.observer => obs, |
| 181 | + sys_building.sun.t0 => t0, |
| 182 | + sys_building.sun.algorithm => PSA(), |
| 183 | + sys_building.sun.refraction => NoRefraction(), |
| 184 | +] |
| 185 | +
|
| 186 | +prob = ODEProblem(sys_building, pmap, (0.0, 86400.0)) |
| 187 | +sol = solve(prob; saveat = 600.0) |
| 188 | +
|
| 189 | +# Plot temperature evolution |
| 190 | +fig = Figure(size = (1200, 400)) |
| 191 | +
|
| 192 | +ax1 = Axis(fig[1, 1]; xlabel = "Time (hours)", ylabel = "Temperature (°C)", title = "Room Temperature") |
| 193 | +lines!(ax1, sol.t ./ 3600, sol[sys_building.T]) |
| 194 | +
|
| 195 | +ax2 = Axis(fig[1, 2]; xlabel = "Time (hours)", ylabel = "Solar Gain (W)", title = "Solar Heat Gain") |
| 196 | +lines!(ax2, sol.t ./ 3600, sol[sys_building.Q_solar]) |
| 197 | +
|
| 198 | +ax3 = Axis(fig[1, 3]; xlabel = "Time (hours)", ylabel = "Elevation (°)", title = "Sun Elevation") |
| 199 | +lines!(ax3, sol.t ./ 3600, sol[sys_building.sun.elevation]) |
| 200 | +
|
| 201 | +fig |
| 202 | +``` |
| 203 | + |
| 204 | +## Implementation Details |
| 205 | + |
| 206 | +The extension works by registering the [`solar_position`](@ref) function and helper functions as |
| 207 | +symbolic operations in ModelingToolkit. The actual solar position calculation happens |
| 208 | +during ODE solving, with the simulation time `t` being converted to a [`DateTime`](https://docs.julialang.org/en/v1/stdlib/Dates/#Dates.DateTime) relative to the reference time `t0`. |
| 209 | + |
| 210 | +## Limitations |
| 211 | + |
| 212 | +The solar position calculation is treated as a black-box function by MTK's symbolic |
| 213 | +engine, so its internals cannot be symbolically simplified. |
| 214 | + |
| 215 | +## See Also |
| 216 | + |
| 217 | +- [Solar Positioning](@ref solar-positioning-algorithms) - Available positioning algorithms |
| 218 | +- [Refraction Correction](@ref refraction-correction) - Atmospheric refraction methods |
| 219 | +- [ModelingToolkit.jl Documentation](https://docs.sciml.ai/ModelingToolkit/stable/) - MTK framework documentation |
0 commit comments