Skip to content

Commit 7a4cb78

Browse files
authored
Merge pull request #16 from JuliaAstro/mtk-extension
Add ModelingToolkit.jl extension
2 parents ee93181 + 5e1824b commit 7a4cb78

21 files changed

+834
-219
lines changed

Project.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,19 +12,24 @@ TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53"
1212

1313
[weakdeps]
1414
Makie = "ee78f7c6-11fb-53f2-987a-cfe4a2b5a57a"
15+
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
16+
Symbolics = "0c5d862f-8b57-4792-8d23-62f2024744c7"
1517

1618
[extensions]
1719
SolarPositionMakieExt = "Makie"
20+
SolarPositionModelingToolkitExt = ["ModelingToolkit", "Symbolics"]
1821

1922
[compat]
2023
Aqua = "0.8"
2124
Dates = "1"
2225
DocStringExtensions = "0.8, 0.9"
2326
Makie = "0.24"
27+
ModelingToolkit = "10.3.0 - 10.26.1"
2428
StructArrays = "0.6, 0.7"
2529
Tables = "1"
2630
Test = "1"
2731
TimeZones = "1.22.0"
32+
Symbolics = "6,7"
2833
julia = "1.10"
2934

3035
[extras]

docs/Project.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae"
66
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
77
DocumenterCitations = "daee34ce-89f3-4625-b898-19384cb65244"
88
LiveServer = "16fef848-5104-11e9-1b77-fb7a48bbb589"
9+
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
10+
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
911
SolarPosition = "5b9d1343-a731-5a90-8730-7bf8d89bf3eb"
1012
TimeZones = "f269a46b-ccf7-5d73-abea-4c690281aa53"
1113

docs/make.jl

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,11 @@ makedocs(;
3434
plugins = [bib],
3535
pages = [
3636
"index.md",
37-
"Examples" => ["examples/getting-started.md", "examples/plotting.md"],
37+
"Examples" => [
38+
"examples/getting-started.md",
39+
"examples/plotting.md",
40+
"examples/modelingtoolkit.md",
41+
],
3842
"reference.md",
3943
"positioning.md",
4044
"refraction.md",
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
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

docs/src/index.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ azimuth angles, which are essential for various applications where the sun is im
2424
- Climate studies
2525
- Astronomy
2626

27+
## Extensions
28+
29+
SolarPosition.jl provides package extensions for advanced use cases:
30+
31+
- **ModelingToolkit Extension**: Integrate solar position calculations into symbolic modeling workflows. Create composable solar energy system models with ModelingToolkit.jl. See the [ModelingToolkit Extension](examples/modelingtoolkit.md) guide for details.
32+
33+
- **Makie Extension**: Plotting recipes for solar position visualization.
34+
2735
## Acknowledgement
2836

2937
This package is based on the work done by readers in the field of solar photovoltaics

examples/Project.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[deps]
2+
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
3+
ModelingToolkit = "961ee093-0014-501f-94e3-6117800e7a78"
4+
OrdinaryDiffEq = "1dea7af3-3e70-54e6-95c3-0bf5283fa5ed"
5+
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"
6+
SolarPosition = "5b9d1343-a731-5a90-8730-7bf8d89bf3eb"
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
module SolarPositionModelingToolkitExt
2+
3+
using SolarPosition: Observer, SolarAlgorithm, RefractionAlgorithm, PSA, NoRefraction
4+
using SolarPosition: SolPos, ApparentSolPos, SPASolPos, AbstractApparentSolPos
5+
using ModelingToolkit: @parameters, @variables, System
6+
using ModelingToolkit: t_nounits as t
7+
using Dates: DateTime, Millisecond
8+
import Symbolics
9+
10+
import SolarPosition: SolarPositionBlock, solar_position
11+
12+
13+
seconds_to_datetime(t_sec, t0::DateTime) = t0 + Millisecond(round(Int, t_sec * 1e3))
14+
15+
# helper functions to extract fields from solar position
16+
get_azimuth(pos) = pos.azimuth
17+
18+
# for SolPos: use elevation and zenith
19+
get_elevation(pos::SolPos) = pos.elevation
20+
get_zenith(pos::SolPos) = pos.zenith
21+
22+
# for ApparentSolPos and SPASolPos: use apparent_elevation and apparent_zenith
23+
get_elevation(pos::AbstractApparentSolPos) = pos.apparent_elevation
24+
get_zenith(pos::AbstractApparentSolPos) = pos.apparent_zenith
25+
26+
Symbolics.@register_symbolic seconds_to_datetime(t_sec, t0::DateTime)
27+
Symbolics.@register_symbolic solar_position(
28+
observer::Observer,
29+
time::DateTime,
30+
algorithm::SolarAlgorithm,
31+
refraction::RefractionAlgorithm,
32+
)
33+
34+
Symbolics.@register_symbolic get_azimuth(pos)
35+
Symbolics.@register_symbolic get_elevation(pos)
36+
Symbolics.@register_symbolic get_zenith(pos)
37+
38+
function SolarPositionBlock(; name)
39+
@parameters t0::DateTime [tunable = false] observer::Observer [tunable = false]
40+
@parameters algorithm::SolarAlgorithm = PSA() [tunable = false]
41+
@parameters refraction::RefractionAlgorithm = NoRefraction() [tunable = false]
42+
43+
@variables azimuth(t) [output = true]
44+
@variables elevation(t) [output = true]
45+
@variables zenith(t) [output = true]
46+
47+
time_expr = Symbolics.term(seconds_to_datetime, t, t0; type = DateTime)
48+
pos = solar_position(observer, time_expr, algorithm, refraction)
49+
50+
eqs = [
51+
azimuth ~ get_azimuth(pos),
52+
elevation ~ get_elevation(pos),
53+
zenith ~ get_zenith(pos),
54+
]
55+
56+
return System(eqs, t; name = name)
57+
end
58+
59+
end # module SolarPositionModelingToolkitExt

src/Positioning/Positioning.jl

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,10 @@ struct Observer{T<:AbstractFloat}
7474
# apply pole corrections to avoid numerical issues
7575
if lat == 90.0
7676
lat -= 1e-6
77-
@warn "Latitude was 90°. Adjusted to $(lat)° to avoid singularities."
77+
@warn "Latitude is 90°. Adjusted to $(lat)° to avoid singularities." maxlog = 1
7878
elseif lat == -90.0
7979
lat += 1e-6
80-
@warn "Latitude was -90°. Adjusted to $(lat)° to avoid singularities."
80+
@warn "Latitude is -90°. Adjusted to $(lat)° to avoid singularities." maxlog = 1
8181
end
8282

8383
lat_rad = deg2rad(lat)
@@ -97,6 +97,7 @@ Base.show(io::IO, obs::Observer) = print(
9797
)
9898

9999
abstract type AbstractSolPos end
100+
abstract type AbstractApparentSolPos <: AbstractSolPos end
100101

101102
"""
102103
$(TYPEDEF)
@@ -126,7 +127,7 @@ Also includes apparent elevation and zenith angles.
126127
# Fields
127128
$(TYPEDFIELDS)
128129
"""
129-
struct ApparentSolPos{T} <: AbstractSolPos where {T<:AbstractFloat}
130+
struct ApparentSolPos{T} <: AbstractApparentSolPos where {T<:AbstractFloat}
130131
"Azimuth (degrees, 0=N, +clockwise, range [-180, 180])"
131132
azimuth::T
132133
"Elevation (degrees, range [-90, 90])"
@@ -159,7 +160,7 @@ Solar position result from SPA algorithm including equation of time.
159160
# Fields
160161
$(TYPEDFIELDS)
161162
"""
162-
struct SPASolPos{T} <: AbstractSolPos where {T<:AbstractFloat}
163+
struct SPASolPos{T} <: AbstractApparentSolPos where {T<:AbstractFloat}
163164
"Azimuth (degrees, 0=N, +clockwise, range [-180, 180])"
164165
azimuth::T
165166
"Elevation (degrees, range [-90, 90])"

src/Positioning/deltat.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ The polynomial expressions for ΔT are from [NASADeltaT](@cite), based on the wo
106106
"""
107107
function calculate_deltat(year::Real, month::Real)
108108
if year < -1999 || year > 3000
109-
@warn "ΔT is undefined for years before -1999 or after 3000."
109+
@warn "ΔT is undefined for years before -1999 or after 3000." maxlog = 1
110110
end
111111

112112
y = year + (month - 0.5) / 12

0 commit comments

Comments
 (0)