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 for 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 you can see 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 uses the IP connection point to determine the geographical coordinates, 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 loop checking the code generation flag. 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 **Loading and opening the model:**
# @markdown Requires you to enter only the model name
имя_модели = "IoT_sunrise_sunset" # @param {type:"string"}
папка_модели = "$(@__DIR__)/"

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

data = engee.run(model);
   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 **Plotting graphs:**
# @markdown Library `Plots.jl', backend `plotly()`
using Plots
plotly()
Ширина = 900 # @param {type:"integer"}
Высота = 300 # @param {type:"integer"}

Сигнал_X = "all_days" # @param {type:"string"}
Сигнал_Y = "discrepancy_min" # @param {type:"string"}
Заголовок = "The equation of time" # @param {type:"string"}
Подпись_X = "The day is in order" # @param {type:"string"}
Подпись_Y = "Equation of time, min" # @param {type:"string"}
plot(size = (Width, Height), legend = :none, title=Title, xlabel=Caption_X, ylabel=SIGN_U)
plot!(the data is [Signal_X].value, the data of [Signal_U].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 **Plotting graphs:**
# @markdown Library `Plots.jl', backend `plotly()`
using Plots
plotly()
Ширина = 900 # @param {type:"integer"}
Высота = 300 # @param {type:"integer"}

Сигнал_X = "discrepancy_min" # @param {type:"string"}
Сигнал_Y = "declination" # @param {type:"string"}
Заголовок = "The analemma" # @param {type:"string"}
Подпись_X = "Equation of time, min" # @param {type:"string"}
Подпись_Y = "Declination, rad" # @param {type:"string"}
plot(size = (Width, Height), xlims=(-45.45), legend = :none, title=Title, xlabel=Caption_X, ylabel=Caption_U)
plot!(the data is [Signal_X].value, data[Signal_U].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 **Plotting graphs:**
# @markdown Library `Plots.jl', backend `plotly()`
using Plots
plotly()
Ширина = 900 # @param {type:"integer"}
Высота = 300 # @param {type:"integer"}

Сигнал_X = "all_days" # @param {type:"string"}
Сигнал_Y1 = "sunrise" # @param {type:"string"}
Подпись_1 = "Sunrise time" # @param {type:"string"}
Сигнал_Y2 = "sunset" # @param {type:"string"}
Подпись_2 = "Sunset time" # @param {type: "string"}
Расположение_подписей = :topleft # @param [":none", ":topleft", ":top", ":topright", ":left", ":right", ":bottomleft",":bottom",":bottomright", ":outerright", ":outerleft", ":outertop", ":outerbottom", ":outertopright", ":outertopleft", ":outerbottomright", :outerbottomleft] {type:"raw"}
Заголовок = "Sunrises and sunsets" # @param {type:"string"}
Подпись_X = "The day is in order" # @param {type:"string"}
Подпись_Y = "Time, h" # @param {type:"string"}
plot(size = (Width, Height), legend = Location of Labels, title=Title, xlabel=Caption_X, ylabel=Caption_U)
plot!(the data is [Signal_X].value, data[signal_1].value;
      label = Caption_1, lw = 2, fillcolor = :darkblue, fillrange = 0)
plot!(the data is [Signal_X].value, the data of [Signal_U2].value;
      label = Caption_2, lw = 2, fillcolor = :yellow, fillrange = 0.01)
plot!(the data is [Signal_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 "Today is %i day in order, where day #1 is 01.01. %i" день_по_порядку год_сейчас
Сегодня 303 день по порядку, где день №1 - 01.01.2024

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

In [ ]:
data = engee.run(model );

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[день_по_порядку])
minute_b = fractional_b*60
(fractional in, minute_in) = modf(minute_in)
seconds in = Int(round(fractional in*60))
clock_b=Int(clock_b)
minute_b=Int(minute_b)

(дробная_з, часы_з) = modf(данные["sunset"].value[день_по_порядку])
minute_z = fractional_z*60
(fractional seconds, minute_z) = modf(minute_z)
secund_z = Int(round(fractional_z*60))
clock_z=Int(clock_z)
minute_z=Int(minute_z)

Printf.@printf "Today, %i-%i-%i\n" день_сейчас месяц_сейчас год_сейчас
Printf.@printf "in the current coordinates: %.2f s.w. %.2f w.d.\n" широта_IP долгота_IP
Printf.@printf "in the UTC+%i\n time zone" часовой_пояс
Printf.@printf "sunrise time - %i: %i:%i \n" часы_в минуты_в секунды_в
Printf.@printf "sunset time - %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        # Programmer's Day
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 [ ]:
data = engee.run(model);

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[день_по_порядку])
minute_b = fractional_b*60
(fractional in, minute_in) = modf(minute_in)
seconds in = Int(round(fractional in*60))
clock_b=Int(clock_b)
minute_b=Int(minute_b)

(дробная_з, часы_з) = modf(данные["sunset"].value[день_по_порядку])
minute_z = fractional_z*60
(fractional seconds, minute_z) = modf(minute_z)
secund_z = Int(round(fractional_z*60))
clock_z=Int(clock_z)
minute_z=Int(minute_z)

println("On Programmer's Day")
Printf.@printf "in coordinates: %.2f north. %.2f east\n" latitude longitude
Printf.@printf "in the UTC time zone+%i\n" часовой_пояс
Printf.@printf "sunrise time - %i: %i:%i \n" часы_в минуты_в секунды_в
Printf.@printf "sunset time - %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 [ ]:
# at the beginning, we set the code generation flag from the model.
write(папка_модели*"CG_start.jl", "CG_start = 1")

# defining channels for receiving input variables
day_number_set_mode = 3
GCS_set_mode = 2
year_set_mode = 2;

Generating the code from the model:

In [ ]:
# @markdown **Code generation:**
# @markdown A folder for the code generation results will be created in the script folder:
папка = "code" # @param {type:"string"}

# @markdown Code generation for the subsystem:
включить = false # @param {type:"boolean"}
if(enable)
    подсистема = "" # @param {type:"string"}
    engee.generate_code( папка_модели*имя_модели*".engee", папка_модели*папка;
                     subsystem_name = subsystem)
else
    engee.generate_code( папка_модели*имя_модели*".engee", папка_модели*папка)
end

# Resetting the code generation flag
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