Engee documentation
Notebook

Automating the calculation of sunrise and sunset times for using callbacks

This example describes a model for calculating the time of sunrise and sunset based on a given time and coordinates. This model is supposed to serve as a subsystem for models of "Internet of Things" devices. Its callbacks are actively involved in automating calculations and testing of the model's algorithms. This allows the model to 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 calculating the parameters of the Sun's motion with various types of possible setting of the current date and coordinates. The direction of use of such a subsystem is various Internet of Things devices that support receiving the current date from a real-time clock or an NTP server, static coordinate setting or automatic GPS detection.
To test the algorithms of such a subsystem, it is necessary to change the ways of setting the current date. The model uses callbacks, which allow you to automate the determination of the necessary input parameters for various ways of setting them.

The example model

The example model - IoT_sunrise_sunset.engee. Its main calculation blocks are the subsystems * "Equation of time", "Declination of the Sun", "Hour angle" and * "Time of sunrise, sunset".

sunrise_sunset.png

Blocks Multiport Switch (purple) are used to switch the 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 for exchanging values in the controller program. Block Ramp generates a changing signal that simulates a change in the ordinal number of the current day to test the algorithm's operation over the course of a year. The following blocks are also used to convert units of measurement between calculation subsystems: Gain.

Calculating sunrise and sunset times

Calculations are performed in the subsystems of the model according to the following expressions:

Subsystem * "Equation of time"* (Equation_of_Time):

where
- equation of time, min;
- auxiliary variable, rad;
- current year;
- sidereal year;
- the current day (in order of the year, where day #1 is January 1st).

Subsystem * "Declination of the Sun"* (Declination):

where - the angle traveled by the Earth in orbit in 1 day, rad;
- the angle passed by the Earth in orbit on the current day, starting from the winter solstice, is rad;
- the eccentricity of the Earth's orbit;
- auxiliary variable, rad;
- the tilt of the Earth's rotation axis, rad;
- declination of the Sun, I'm glad.

Subsystem * "Clock angle"* (hour_angle):

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

Subsystem * "Sunrise, sunset time"* (Sunrise_Suncet_time):

where - sunrise time, one hour;
- sunset time, one hour;
- right ascension, hour;
- longitude of the observation point, degree;
- time zone, hour.

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

Setting the input parameters

The example model supports several ways to set input variables:

  • - can be set to a variable current_year from the workspace or from the subsystem port from_MCU_Y. Switching between them is performed by the value of the variable year_set_mode from the workspace. By default, year_set_mode = 1;
  • - can be specified by a vector of variables latitude, longitude and time_zone from the workspace or from the subsystem port from_MCU_GCS. Switching between them is performed by the value of the variable GCS_set_mode from the workspace. By default, GCS_set_mode = 1;
  • - can be set as a block Ramp, variable day_number from the workspace or from the subsystem port from_MCU_D. Switching between them is performed by the value of the variable day_number_set_mode from the workspace. By default, day_number_set_mode = 1;Block Ramp It has a slope = 1, therefore, in 366 seconds of simulation time, with a simulation step of 1 second, calculations will be simulated for each day in order for the entire specified year.

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

Model callbacks

[Callbacks](https://engee.com/helpcenter/stable/ru/modeling/callbacks-engee.html The models in this example are used to automate obtaining relevant variables, testing in all specified modes, and configuring for various tasks.

Tab </> PostLoadFunc The callback code contains the following code:

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

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

if (CG_start == 0) # No need to initialize variables to generate code

    # Declaring variables
    current_year = 0
    current_month = 0
    current_day = 0
    day_number = 0
    latitude = 0.0
    longitude = 0.0
    time_zone = 0

    # Setting the default operating modes of the model
    day_number_set_mode = 1 
    year_set_mode = 1 
    GCS_set_mode = 1

    # "Calculation" by IP
    ## Loading libraries
    import Pkg;
    Pkg.add("Gumbo")
    Pkg.add("HTTP")
    Pkg.add("AbstractTrees")

    ## Connecting libraries
    using HTTP, Gumbo, AbstractTrees

    ## Getting a page from the web
    website = HTTP.get("https://ip2geolocation.com /");
    site code_ = parsehtml(String(site.body));
    site body = site code.root[2];

    ## Select the data you are interested in from the page
    LATITUDE_IR = body_site[1][1][2][2][1][1][3][8][1][1][1][1][1][10][2][1]. text[16:22]
    Longitude_ir = body_site[1][1][2][2][1][1][3][8][1][1][1][1][1][11][2][1]. text[16:22]
LATITUDE_IR = parse(Float64, latitude_ir)
longitude_ir = parse(Float64, longitude_ir)
clock_race = body_site[1][1][2][2][1][1][3][8][1][1][1][1][1][12][2][1].text[20:22]
    watch_box = parse(Int64, watch_box)

    # Default time and location
settings latitude = latitude_ir
longitude = longitude_ir
time_zone = clock_range

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

As can be seen from the code comments, this callback function declares constants and variables, defines constants and default values for variables. You should pay attention to the fact that to determine the location and time zone, the model accesses an external site, which determines the geographical coordinates using the IP connection point, and transmits the page as an HTML object to the Engee workspace. At the same time, during code generation, you can set the flag CG_start = 1 in the file CG_start.jl in order to manually define the channels for setting variables.

Tab </> PresaveFunc The callback code contains the following code:

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

    latitude = latitude_p
longitude = longitude_p
time_zone = hours

end

if (day_number_set_mode == 2)
    
    year_such = current_year = Dates.value(Year(now()))
month_such = current_month = Dates.value(Month(now()))
day_such = current_day = Dates.value(Day(now()))

    N1 = floor(275 * month_such / 9);
    N2 = floor((month_such + 9) / 12);
    N3 = (1 + floor((year_number - 4 * floor(year_number / 4) + 2) / 3));

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

    latitude = latitude_p
longitude = longitude_p
time_zone = hours

end

This function performs an auxiliary role - when changing the current day's task mode, the values of the input variables must be updated. Thus, after changing the task mode and saving the model, new variable values will be transferred to the workspace.

Tab </> CloseFunc The callback contains the function engee.clear(), "wrapped" in a code generation flag check loop. Thus, when the model is closed, the workspace will be automatically cleared, and executing this callback function as a result of code generation will not clear the workspace.

Full-cycle model testing

Let's move on to testing the model algorithm. Let's upload 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 equation of time for each day of the year, you 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 the values of the equation of time and declination of the Sun during the year, it is possible to construct an 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]:

At the end of testing the algorithm, we will plot sunrise and sunset time graphs for each day of 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 graphs describing the position of the Sun for a given observation point correspond to those expected, taking into account the assumptions made above.

Determining the sunrise and sunset time today

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

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

To control the calculations of the model, we use [software control] functions (https://engee.com/helpcenter/stable/ru/modeling/programmatic-modeling-functions.html ).
Now let's print a message to make sure that the model gets a single value for the current day.:

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

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

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

We will convert the received sunrise and sunset time values from decimal hour format to the format of whole hours, minutes and seconds, after which we will display a message with the set and received 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 a given observation point correspond to those expected, taking into account previously accepted assumptions.

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

The variables of the current day, year, and observation point can be set using a script. For example, consider how the model works 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 input variables, there is no need to save the model. Let's run the model with new input variables:

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

We will convert the received sunrise and sunset time values from decimal hour format to the format of whole hours, minutes and seconds, after which we will display a message with the set and received 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 a given observation point correspond to those expected, taking into account previously accepted assumptions.

Configuring the subsystem to set coordinates and time from the peripheral of the controller

As a result of the development and testing of the model, let's move on to the final configuration of the model - determining the 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;

Generating the 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, you can make sure that the input variables are determined from the specified channels.:

sunrise_sunset_CG.png
sunrise_sunset_CG2.png
sunrise_sunset_CG3.png

Conclusion

In this demo, we discussed the ways and possibilities of working with the callbacks of the Engee model to efficiently operate and automate the processes of parameterization, configuration, testing, and code generation of the model. The described algorithm reproduces the specified calculations based on the accepted assumptions, and the model can be reconfigured for various 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. Gazhulina; edited by S.M. Ponomarev. – N. Novgorod: Publishing House of N.I. Lobachevsky National Research University, 2018. – 351 p.
  2. Course of general astronomy / P. I. Bakulin, E. V. Kononovich, V. I. Moroz. — M.: Nauka, 1976.
  3. Cherny M.A. Aviation astronomy. - M.: Transport, 1978. - 208 p.
  4. Almanac for Computers, 1990 published by Nautical Almanac Office United States Naval Observatory Washington, DC 20392