Engee documentation
Notebook

Automating the calculation of sunrise and sunset times for using callbacks

This example describes a model for calculating sunrise and sunset times based on a given time and coordinates. This model is expected to serve as a subsystem for models of Internet of Things devices. To automate the calculations and test the algorithms of the model, its callbacks are actively used. Thanks to this, the model can automatically determine the current date, location and time zone without using scripts.

Introduction

The primary goal of the model development is to create a subsystem for calculation of the Sun motion parameters with different types of possible setting of the current date and coordinates. The direction of use of such a subsystem - various devices of the "Internet of Things", supporting the receipt of the current date from a real-time clock or NTP-server, static setting of coordinates or their automatic determination by GPS.
To test the algorithms of such a subsystem, it is necessary to be able to change the ways of setting the current date. The model uses callbacks, which allow to automate the definition of the necessary input parameters at different ways of their setting.

Example model

The example model is IoT_sunrise_sunset.engee. Its main calculation blocks are subsystems "Time equation ", "Sun declination ", "Hour angle " and "Sunrise, sunset time ".

sunrise_sunset.png

Blocks Multiport Switch (purple) are used to switch input data for the calculation model. Blocks Constant (turquoise) - pass constants and variables from the workspace to the model. Blocks Inport and Outport (orange) are used as inputs and outputs of the subsystem to exchange values in the controller programme. Block Ramp forms a changing signal, simulating the change of the sequence number of the current day, to test the algorithm operation during the year. Also blocks Gain are used to convert units of measurement between the calculation subsystems.

Calculation of sunrise and sunset times

The model subsystems perform calculations according to the following expressions:

Subsystem "Time Equation " (Equation_of_Time):

$$D = 6.24004077 + 0.01720197 \cdot (D_{all} \cdot (Y_{now} - 2000) + D_{now}),$$

$$T = -7.659 \cdot \sin(D) + 9.863 \cdot \sin(2 \cdot D + 3.5932),$$

where $T$ - time equation, min;
$D$ - auxiliary variable, rad;
$Y_{now}$ - current year;
$D_{all}=365.256$ - sideric year;
$D_{now}$ - current day (in order of the year, where day #1 is 1 January).

Subsystem "Declination of the Sun " (Declination):

$$A = (D_{now} + 9) \cdot n,$$ $$B = A + 2 \cdot e \cdot \sin((D_{now} - 3)\cdot n),$$ $$δ = -\arcsin ( \sin (\varepsilon) \cdot \cos (B)),$$

where $n=\left(2\cdot \pi \right) / D_{all}$ is the angle travelled by the Earth along the orbit for 1 day, rad;
$A$ - is the angle travelled by the Earth along the orbit on the current day, starting from the day of the winter solstice, rad;
$e=0.0167$ - eccentricity of the Earth's orbit;
$B$ - auxiliary variable, rad;
$\varepsilon = 23.4372 \cdot \pi / 180$ - inclination of the Earth's rotation axis, rad;
$\delta$ - declination of the Sun, rad.

Subsystem "Clock Angle " (hour_angle):

$$t = \arccos(-\tan(\varphi)\cdot \tan(δ))\cdot180/(\pi\cdot 15),$$

where $\varphi$ is the latitude of the observation point, rad;
$t$ - hour angle, hr.
This expression does not take into account the influence of horizontal parallax, apparent radius and refraction of the Sun.

Subsystem "Time of sunrise, sunset " (Sunrise_Suncet_time):

$$t_в = 12^h - t - T^h - \lambda /15 +UTC,$$ $$t_з = 12^h + t - T^h - \lambda/15 +UTC,$$

where $t_в$ - sunrise time, h;
$t_з$ - sunset time, hr;
$12^h = 12$ - direct ascent, hr;
$\lambda$ - longitude of the observation point, deg;
$UTC$ - hour zone, hour.

The calculated expressions are obtained from the literature sources given at the end of the example.

Setting the input parameters

The model example supports several ways to set input variables:

  • $Y_{now}$ - can be set by the variable current_year from the workspace or from the subsystem port from_MCU_Y. Switching between them is done by the value of the variable year_set_mode from the workspace. By default, year_set_mode = 1;
  • $\varphi,\ \lambda,\ UTC$ - can be set by the vector of variables latitude, longitude and time_zone from the workspace or from the subsystem port from_MCU_GCS. Switching between them is made by the value of the variable GCS_set_mode from the workspace. By default, GCS_set_mode = 1;
  • $D_{now}$ - can be set by the block Ramp, by the variable day_number from the working area or from the subsystem port from_MCU_D. Switching between them is made by the value of the variable day_number_set_mode from the workspace. By default, day_number_set_mode = 1;Block Ramp has slope = 1, so for 366 sec of modelling time with a modelling step of 1 sec will be modelled calculations for each day in order for the whole specified year.

The constants given in the expressions are set in the callbacks.

Model callbacks

Callbacks models of this example are used to automate obtaining actual variables, testing in all specified modes, and configuring for various tasks.

The </> PostLoadFunc callbacks tab contains the following code:

# Astronomical constants
Eccentricity = Eccentricity = 0.0167; # the eccentricity of the Earth, 2017.
Axial_tilt = Axial_tilt = 23.4372 * pi / 180; # the tilt of the Earth's axis of rotation, radians (23°26′14″)
days_in_year = days_in_year = 365.256; # Earth's rotation cycle around the Sun, days

# Connection of the file with the code generation flag
include("/user/start/examples/codegen/iot_sunrise_sunset_callbacks/CG_start.jl")

if (CG_start == 0) # No variables need to be initialised for code generation

    # Variable declaration
    current_year = 0
    current_month = 0
    current_day = 0
    day_number = 0
    latitude = 0.0
    longitude = 0.0
    time_zone = 0

    # Set the default modes of the model
    day_number_set_mode = 1 
    year_set_mode = 1 
    GCS_set_mode = 1

    # "Calculate" by IP
    ## Load libraries
    import Pkg;
    Pkg.add("Gumbo")
    Pkg.add("HTTP")
    Pkg.add("AbstractTrees")

    ## Connect libraries
    using HTTP, Gumbo, AbstractTrees

    ## Get the page from the web
    site = HTTP.get("https://ip2geolocation.com/");
    site_code = parsehtml(String(site.body));
    site_body = site_code.root[2];

    ## Select the data of interest from the page
    широта_IP = тело_сайта[1][1][2][2][1][1][3][8][1][1][1][1][1][10][2][1].text[16:22]
    долгота_IP = тело_сайта[1][1][2][2][1][1][3][8][1][1][1][1][1][11][2][1].text[16:22]
    latitude_IP = parse(Float64, latitude_IP)
    longitude_IP = parse(Float64, longitude_IP)
    часовой_пояс = тело_сайта[1][1][2][2][1][1][3][8][1][1][1][1][1][12][2][1].text[20:22]
    hour_zone = parse(Int64, hour_zone)

    # Default time and location settings
    latitude = latitude_IP
    longitude = longitude_IP
    time_zone = time_zone

    current_year = year_now = Dates.value(Year(now()))
end

As you can see from the code comments, this callback function declares constants and variables, defines constants and default values for variables. Note that to determine the location and time zone, the model calls an external site that determines the geographic coordinates from the IP of the connection point, and passes the page as an HTML object to the Engee workspace. During code generation, you can set the CG_start = 1 flag in the CG_start.jl file to manually define the variable assignment channels.

The </> PresaveFunc callbacks tab contains the following code:

if (day_number_set_mode == 1)
    
    year_year = current_year = Dates.value(Year(now()))

    latitude = latitude_IP
    longitude = longitude_IP
    time_zone = time_zone

end

if (day_number_set_mode == 2)
    
    year_now = current_year = Dates.value(Year(now()))
    month_now = current_month = Dates.value(Month(now()))
    day_now = current_day = Dates.value(Day(now()))

    N1 = floor(275 * month_month / 9);
    N2 = floor((month_now + 9) / 12);
    N3 = (1 + floor((year_now - 4 * floor(year_now / 4) + 2) / 3));

    day_by_order = day_number = Int(N1 - (N2 * N3) + day_now - 30)

    latitude = latitude_IP
    longitude = longitude_IP
    time_zone = time_zone

end

This function fulfils an auxiliary role - when the job mode of the current day is changed, it is necessary to update the values of the input variables. Thus, after changing the job mode and saving the model, new values of variables will be transferred to the workspace.

The </> CloseFunc callbacks tab contains the function engee.clear(), "wrapped" in the loop of checking the code generation flag. Thus, closing the model will automatically clear the workspace, while executing this callback function will not clear the workspace as a result of code generation.

Testing the model over a full cycle

Let's move on to testing the model algorithm. Let's load and execute the model.

In [ ]:
# @markdown **Загрузка и открытие модели:**  
# @markdown Требуется ввести только имя модели
имя_модели = "IoT_sunrise_sunset" # @param {type:"string"}
папка_модели = "$(@__DIR__)/"

if имя_модели in [m.name for m in engee.get_all_models()]
    модель = engee.open( имя_модели );
    # модель = engee.gcm()
else
    модель = engee.load( папка_модели*имя_модели*".engee" );
    # модель = engee.gcm()
end

данные = engee.run(модель);
   Resolving package versions...
  No Changes to `~/.project/Project.toml`
  No Changes to `~/.project/Manifest.toml`
   Resolving package versions...
  No Changes to `~/.project/Project.toml`
  No Changes to `~/.project/Manifest.toml`
   Resolving package versions...
  No Changes to `~/.project/Project.toml`
  No Changes to `~/.project/Manifest.toml`

Having a vector of values of the time equation for each day of the year, we can plot its graph:

In [ ]:
# @markdown **Построение графиков:**  
# @markdown Библиотека `Plots.jl`, бэкэнд `plotly()` 
using Plots
plotly()
Ширина = 900 # @param {type:"integer"}
Высота = 300 # @param {type:"integer"}

Сигнал_X = "all_days" # @param {type:"string"} 
Сигнал_Y = "discrepancy_min" # @param {type:"string"}
Заголовок = "Уравнение времени" # @param {type:"string"} 
Подпись_X = "День по порядку" # @param {type:"string"} 
Подпись_Y = "Уравнение времени, мин" # @param {type:"string"} 
plot(size = (Ширина, Высота), legend = :none, title=Заголовок, xlabel=Подпись_X, ylabel=Подпись_Y)
plot!(данные[Сигнал_X].value, данные[Сигнал_Y].value;
      lw = 2, line=:stem)
Out[0]:

From the vectors of values of the equation of time and declination of the Sun during the year, we can construct the analemma of the Sun for a given observation point:

In [ ]:
# @markdown **Построение графиков:**  
# @markdown Библиотека `Plots.jl`, бэкэнд `plotly()` 
using Plots
plotly()
Ширина = 900 # @param {type:"integer"}
Высота = 300 # @param {type:"integer"}

Сигнал_X = "discrepancy_min" # @param {type:"string"} 
Сигнал_Y = "declination" # @param {type:"string"}
Заголовок = "Аналемма" # @param {type:"string"} 
Подпись_X = "Уравнение времени, мин" # @param {type:"string"} 
Подпись_Y = "Склонение, рад" # @param {type:"string"} 
plot(size = (Ширина, Высота), xlims=(-45,45), legend = :none, title=Заголовок, xlabel=Подпись_X, ylabel=Подпись_Y)
plot!(данные[Сигнал_X].value, данные[Сигнал_Y].value;
      lw = 2, seriestype=:scatter, color=:yellow, background_color_subplot = :cyan)
Out[0]:

To complete the testing of the algorithm, we plot the sunrise and sunset times for each day in the current year for the coordinates of the observation point determined by IP:

In [ ]:
# @markdown **Построение графиков:**  
# @markdown Библиотека `Plots.jl`, бэкэнд `plotly()` 
using Plots
plotly()
Ширина = 900 # @param {type:"integer"}
Высота = 300 # @param {type:"integer"}

Сигнал_X = "all_days" # @param {type:"string"} 
Сигнал_Y1 = "sunrise" # @param {type:"string"}
Подпись_1 = "Время восхода" # @param {type:"string"} 
Сигнал_Y2 = "sunset" # @param {type:"string"}
Подпись_2 = "Время заката" # @param {type: "string"} 
Расположение_подписей = :topleft # @param [":none", ":topleft", ":top", ":topright", ":left", ":right", ":bottomleft",":bottom",":bottomright", ":outerright", ":outerleft", ":outertop", ":outerbottom", ":outertopright", ":outertopleft", ":outerbottomright", :outerbottomleft] {type:"raw"}    
Заголовок = "Восходы и закаты" # @param {type:"string"} 
Подпись_X = "День по порядку" # @param {type:"string"} 
Подпись_Y = "Время, ч" # @param {type:"string"} 
plot(size = (Ширина, Высота), legend = Расположение_подписей, title=Заголовок, xlabel=Подпись_X, ylabel=Подпись_Y)
plot!(данные[Сигнал_X].value, данные[Сигнал_Y1].value;
      label = Подпись_1, lw = 2, fillcolor = :darkblue, fillrange = 0)
plot!(данные[Сигнал_X].value, данные[Сигнал_Y2].value;
      label = Подпись_2, lw = 2, fillcolor = :yellow, fillrange = 0.01)
plot!(данные[Сигнал_X].value, 24*ones(366);
      label = :none, lw = 2, fillcolor = :darkblue, fillrange = 0.01, color = :darkblue)
Out[0]:

The plots describing the position of the Sun for a given observation point correspond to the expected ones given the assumptions made above.

Determining the time of sunrise and sunset today

To determine the characteristics of the Sun's position, including the sunrise and sunset times for today, let's go to the appropriate setting mode of the current day, and then save:

In [ ]:
day_number_set_mode = 2;
engee.save(модель, папка_модели*имя_модели*".engee"; force = true);

We use the programme control functions to control the model calculations. Now let's display a message to make sure that the model gets one value of the current day:

In [ ]:
import Printf
Printf.@printf "Сегодня %i день по порядку, где день №1 - 01.01.%i" день_по_порядку год_сейчас
Сегодня 303 день по порядку, где день №1 - 01.01.2024

Let's run the model to calculate the sunrise and sunset times today:

In [ ]:
данные = engee.run( модель );

Convert the obtained values of sunrise and sunset times from decimal hour format to whole hours, minutes, and seconds format, then display a message with the given and obtained values:

In [ ]:
(дробная_в, часы_в) = modf(данные["sunrise"].value[день_по_порядку])
минуты_в = дробная_в*60
(дробная_в, минуты_в) = modf(минуты_в)
секунды_в = Int(round(дробная_в*60))
часы_в=Int(часы_в)
минуты_в=Int(минуты_в)

(дробная_з, часы_з) = modf(данные["sunset"].value[день_по_порядку])
минуты_з = дробная_з*60
(дробная_з, минуты_з) = modf(минуты_з)
секунды_з = Int(round(дробная_з*60))
часы_з=Int(часы_з)
минуты_з=Int(минуты_з)

Printf.@printf "Сегодня, %i-%i-%i г.\n" день_сейчас месяц_сейчас год_сейчас
Printf.@printf "в текущих координатах: %.2f с.ш. %.2f в.д.\n" широта_IP долгота_IP
Printf.@printf "в часовом поясе UTC+%i \n" часовой_пояс
Printf.@printf "время восхода Солнца - %i:%i:%i \n" часы_в минуты_в секунды_в
Printf.@printf "время заката Солнца - %i:%i:%i \n" часы_з минуты_з секунды_з
Сегодня, 29-10-2024 г.
в текущих координатах: 55.75 с.ш. 37.62 в.д.
в часовом поясе UTC+3 
время восхода Солнца - 7:35:44 
время заката Солнца - 16:50:17 

The sunrise and sunset times for the given observation point correspond to the expected ones, taking into account the assumptions made earlier.

Determining the time of sunrise and sunset on a given day using the given coordinates

Variables of the current day, year and observation point can be set by script. For example, consider the model operation with the following data:

In [ ]:
current_year = 2024
day_number = 256        # День программиста
latitude, longitude = (67.048060, 64.060560);

Since there is no change in the mode of setting the input variables, there is no need to save the model. Let's run the model with new input variables:

In [ ]:
данные = engee.run(модель);

Let's convert the obtained values of sunrise and sunset times from decimal hour format to whole hours, minutes and seconds format, and then output a message with the given and obtained values:

In [ ]:
(дробная_в, часы_в) = modf(данные["sunrise"].value[день_по_порядку])
минуты_в = дробная_в*60
(дробная_в, минуты_в) = modf(минуты_в)
секунды_в = Int(round(дробная_в*60))
часы_в=Int(часы_в)
минуты_в=Int(минуты_в)

(дробная_з, часы_з) = modf(данные["sunset"].value[день_по_порядку])
минуты_з = дробная_з*60
(дробная_з, минуты_з) = modf(минуты_з)
секунды_з = Int(round(дробная_з*60))
часы_з=Int(часы_з)
минуты_з=Int(минуты_з)

println("В день программиста")
Printf.@printf "в координатах: %.2f с.ш. %.2f в.д. \n" latitude longitude
Printf.@printf "в часовом поясе UTC+%i \n" часовой_пояс
Printf.@printf "время восхода Солнца - %i:%i:%i \n" часы_в минуты_в секунды_в
Printf.@printf "время заката Солнца - %i:%i:%i \n" часы_з минуты_з секунды_з
В день программиста
в координатах: 67.05 с.ш. 64.06 в.д. 
в часовом поясе UTC+3 
время восхода Солнца - 3:59:50 
время заката Солнца - 17:18:18 

The sunrise and sunset times for the given observation point correspond to the expected ones, taking into account the assumptions made earlier.

Configuring the subsystem to set coordinates and time from the controller periphery

As a result of development and testing of the model, let's move to the final configuration of the model - definition of modes of setting input variables from the microcontroller:

In [ ]:
# в начале устанавливаем флаг генерации кода из модели
write(папка_модели*"CG_start.jl", "CG_start = 1")

# определяем каналы получения входных переменных
day_number_set_mode = 3
GCS_set_mode = 2
year_set_mode = 2;

Generate code from the model:

In [ ]:
# @markdown **Генерация кода:**  
# @markdown Папка для результатов генерации кода будет создана в папке скрипта:
папка = "code" # @param {type:"string"}

# @markdown Генерация кода для подсистемы:
включить = false # @param {type:"boolean"}
if(включить)
    подсистема = "" # @param {type:"string"}
    engee.generate_code( папка_модели*имя_модели*".engee", папка_модели*папка;
                     subsystem_name = подсистема)
else
    engee.generate_code( папка_модели*имя_модели*".engee", папка_модели*папка)
end

# Сбрасываем флаг генерации кода
write(папка_модели*"CG_start.jl", "CG_start = 0");
[ Info: Generated code and artifacts: /user/start/examples/codegen/iot_sunrise_sunset_callbacks/code

By opening the generated files, we can verify that the input variables are defined from the given channels:

sunrise_sunset_cg.png

sunrise_sunset_cg2.png

sunrise_sunset_cg3.png

Conclusion

In this demo, the methods and possibilities of working with Engee model callbacks for efficient operation and automation of parameterisation, configuration, testing and code generation of the model were discussed. The algorithm described reproduces the given calculations with the assumptions made, and the model can be reconfigured for different testing and code generation tasks.

List of sources

  1. astronomical calendar for 2019. Issue 19 (116) / S.M. Ponomarev, N.I. Lapin, M.A. Faddeev, A.P. Gajulina; ed. by S.M. Ponomarev. - N. Novgorod: Izd-voor NNGU named after N.I. Lobachevsky, 2018. - 351 с.
  2. Course of general astronomy / P. I. Bakulin, E. V. Kononovich, V. I. Moroz. - Moscow: Nauka, 1976.
  3. Cherny M.A. Aviation astronomy. - Moscow: Transport, 1978. - 208 с.
  4. Almanac for Computers, 1990 published by Nautical Almanac Office United States Naval Observatory Washington, DC 20392