Документация Engee
Notebook

Автоматизация расчёта времени восхода и заката Солнца для при помощи обратных вызовов

Этот пример описывает модель расчёта времени восхода и заката Солнца исходя из заданного времени и координат. Эта модель, как предполагается, будет служить подсистемой для моделей устройств "Интернета вещей". Для автоматизации расчётов и тестирования алгоритмов модели активно задействованы её обратные вызовы. Благодаря этому модель без использования скриптов может автоматически определять текущую дату, местоположение и часовой пояс.

Введение

Первоочередная цель разработки модели - создание подсистемы расчёта параметров движения Солнца с различными типа возможного задания текущей даты и координат. Направление использования такой подсистемы - различные устройства "Интернета вещей", поддерживающие получение текущей даты от часов реального времени или NTP-сервера, статичную установку координат или их автоматическое определение по GPS.
Для тестирования алгоритмов такой подсистемы необходима возможность изменения способов задания текущей даты. В модели используются обратные вызовы, которые позволяют автоматизировать определение необходимых входных параметров при различных способах их задания.

Модель примера

Модель примера - IoT_sunrise_sunset.engee. Её основные расчётные блоки - подсистемы "Уравнение времени", "Склонение Солнца", "Часовой угол" и "Время восхода, заката".

sunrise_sunset.png

Блоки Multiport Switch (лиловые) используются для переключения входных данных для модели расчётов. Блоки Constant (бирюзовые) - передают в модель константы и переменные из рабочей области. Блоки Inport и Outport (оранжевые) используются в качестве входов и выходов подсистемы для обмена значениями в программе контроллера. Блок Ramp формирует изменяющийся сигнал, имитирующий изменение порядкового номера текущего дня, для тестирования работы алгоритма в течение года. Также для преобразования единиц измерения между расчётными подсистемами используются блоки Gain.

Расчёт времени восхода и заката

В подсистемах модели осуществляются расчёты согласно следующим выражениям:

Подсистема "Уравнение времени" (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),$$

где $T$ - уравнение времени, мин;
$D$ - вспомогательная переменная, рад;
$Y_{now}$ - текущий год;
$D_{all}=365.256$ - сидерический год;
$D_{now}$ - текущий день (по порядку в году, где день №1 - 1 января).

Подсистема "Склонение Солнца" (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)),$$

где $n=\left(2\cdot \pi \right) / D_{all}$ - угол пройденный Землёй по орбите за 1 день, рад;
$A$ - угол пройденный Землёй по орбите на текущий день, начиная со дня зимнего солнцестояния, рад;
$e=0.0167$ - эксцентриситет земной орбиты;
$B$ - вспомогательная переменная, рад;
$\varepsilon = 23.4372 \cdot \pi / 180$ - наклон оси вращения Земли, рад;
$\delta$ - склонение Солнца, рад.

Подсистема "Часовой угол" (hour_angle):

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

где $\varphi$ - широта точки наблюдения, рад;
$t$ - часовой угол, час.
Это выражение не учитывает влияние горизонтального параллакса, видимого радиуса и рефракции Солнца.

Подсистема "Время восхода, заката" (Sunrise_Suncet_time):

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

где $t_в$ - время восхода, час;
$t_з$ - время заката, час;
$12^h = 12$ - прямое восхождение, час;
$\lambda$ - долгота точки наблюдения, град;
$UTC$ - часовая зона, час.

Расчётные выражения получены из литературных источников, приведённых в конце примера.

Задание входных параметров

Модель пример поддерживает несколько способов задания входных переменных:

  • $Y_{now}$ - может быть задана переменной current_year из рабочей области или из порта подсистемы from_MCU_Y. Переключение между ними производится по значению переменной year_set_mode из рабочей области. По умолчанию, year_set_mode = 1;
  • $\varphi,\ \lambda,\ UTC$ - могут быть заданы вектором переменных latitude, longitude и time_zone из рабочей области или из порта подсистемы from_MCU_GCS. Переключение между ними производится по значению переменной GCS_set_mode из рабочей области. По умолчанию, GCS_set_mode = 1;
  • $D_{now}$ - может быть задана блоком Ramp, переменной day_number из рабочей области или из порта подсистемы from_MCU_D. Переключение между ними производится по значению переменной day_number_set_mode из рабочей области. По умолчанию, day_number_set_mode = 1;Блок Ramp имеет наклон = 1, поэтому за 366 сек времени моделирования с шагом моделирования 1 сек будет смоделированы расчёты для каждого дня по порядку в течение всего заданного года.

Константы, приведённые в выражениях, заданы в обратных вызовах.

Обратные вызовы модели

Обратные вызовы модели этого примера используются для автоматизации получения актуальных переменных, тестирования во всех заданных режимах, конфигурирования под различные задачи.

Вкладка </> PostLoadFunc обратных вызовов содержит следующий код:

# Астрономические константы
Эксцентриситет = Eccentricity = 0.0167;                 # эксцентриситет Земли, 2017 г.
Наклон_оси= Axial_tilt = 23.4372 * pi / 180;            # наклон оси вращения Земли, радиан (23°26′14″)
дней_в_году = days_in_year = 365.256;                   # цикл вращения Земли вокруг Солнца, дней

# Подключение файла с флагом генерации кода
include("/user/start/examples/codegen/iot_sunrise_sunset_callbacks/CG_start.jl")

if (CG_start == 0) # Для генерации кода не нужно инициализировать переменные

    # Объявление переменных
    current_year = 0
    current_month = 0
    current_day = 0
    day_number = 0
    latitude = 0.0
    longitude = 0.0
    time_zone = 0

    # Установление режимов работы модели по умолчанию
    day_number_set_mode = 1 
    year_set_mode = 1 
    GCS_set_mode = 1

    # "Вычисление" по IP
    ## Загружаем библиотеки
    import Pkg;
    Pkg.add("Gumbo")
    Pkg.add("HTTP")
    Pkg.add("AbstractTrees")

    ## Подключаем библиотеки
    using HTTP, Gumbo, AbstractTrees

    ## Получаем страницу из сети
    сайт = HTTP.get("https://ip2geolocation.com/");
    код_сайта = parsehtml(String(сайт.body));
    тело_сайта = код_сайта.root[2];

    ## Выбираем из страницы интересующие данные
    широта_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]
    широта_IP = parse(Float64, широта_IP)
    долгота_IP = parse(Float64, долгота_IP)
    часовой_пояс = тело_сайта[1][1][2][2][1][1][3][8][1][1][1][1][1][12][2][1].text[20:22]
    часовой_пояс = parse(Int64, часовой_пояс)

    # Установки времени и местоположения по умолчанию
    latitude = широта_IP
    longitude = долгота_IP
    time_zone = часовой_пояс

    current_year = год_сейчас = Dates.value(Year(now()))
end

Как видно из комментариев кода, эта функция обратных вызовов объявляет константы и переменные, определяет константы и значения по умолчанию для переменных. Следует обратить внимание на то, что для определения местоположения и часовой зоны модель обращается на внешний сайт, который по IP точки подключения определяет географические координаты, и передаёт страницу в качестве HTML-объекта в рабочую область Engee. При этом в ходе генерации кода можно установить флаг CG_start = 1 в файле CG_start.jl для того, чтобы вручную определять каналы задания переменных.

Вкладка </> PresaveFunc обратных вызовов содержит следующий код:

if (day_number_set_mode == 1)
    
    год_сейчас = current_year = Dates.value(Year(now()))

    latitude = широта_IP
    longitude = долгота_IP
    time_zone = часовой_пояс

end

if (day_number_set_mode == 2)
    
    год_сейчас = current_year = Dates.value(Year(now()))
    месяц_сейчас = current_month = Dates.value(Month(now()))
    день_сейчас = current_day = Dates.value(Day(now()))

    N1 = floor(275 * месяц_сейчас / 9);
    N2 = floor((месяц_сейчас + 9) / 12);
    N3 = (1 + floor((год_сейчас - 4 * floor(год_сейчас / 4) + 2) / 3));

    день_по_порядку = day_number = Int(N1 - (N2 * N3) + день_сейчас - 30)

    latitude = широта_IP
    longitude = долгота_IP
    time_zone = часовой_пояс

end

Эта функция выполняет вспомогательную роль - при изменении режима задания текущего дня необходимо обновить значения входных переменных. Таким образом, после изменения режима задания и сохранения модели в рабочую область будут переданы новые значения переменных.

Вкладка </> CloseFunc обратных вызовов содержит функцию engee.clear(), "обернутую" в цикл проверки флага генерации кода. Таким образом, при закрытии модели будет автоматически очищена рабочая область, а исполнение этой функции обратных вызовов в результате генерации кода рабочую область не очистит .

Тестирование модели за полный цикл

Перейдем к тестированию алгоритма модели. Загрузим и выполним модель.

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`

Имея вектор значений уравнения времени для каждого дня в году, можно построить его график:

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]:

Из векторов значений уравнения времени и склонения Солнца в течение года можно построить аналемму Солнца для заданной точки наблюдения:

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]:

В завершение тестирования алгоритма построим графики времени восхода и заката для каждого дня в текущем году для координат точки наблюдения, определенных по 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]:

Графики, описывающие положение Солнца для заданной точки наблюдения соответствуют ожидаемым с учётом принятых выше допущений.

Определение времени восхода и заката сегодня

Для определения характеристик положения Солнца, в том числе, времени восхода и заката для сегодняшнего дня перейдём в соответствующий режим установки текущего дня, после чего сохраним:

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

Для управления расчётами модели мы используем функции программного управления. Теперь выведем сообщение, чтобы убедиться, что модель получает одно значение текущего дня:

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

Выполним модель, чтобы рассчитать время восхода и захода сегодня:

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

Переведем полученные значения времени восхода и заката Солнца из десятичного часового формата в формат целых часов, минут и секунд, после чего выведем сообщение с заданными и полученными значениями:

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 

Время восхода и захода Солнца для заданной точки наблюдения соответствуют ожидаемым с учётом принятых ранее допущений.

Определение времени восхода и заката в заданный день по заданным координатам

Переменные текущего дня, года и точки наблюдения можно задать скриптом. Например, рассмотрим работу модели со следующими данными:

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

Так как здесь не происходит изменения режима задания входных переменных, сохранять модель не нужно. Выполним модель с новыми входными переменными:

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

Переведем полученные значения времени восхода и заката Солнца из десятичного часового формата в формат целых часов, минут и секунд, после чего выведем сообщение с заданными и полученными значениями:

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 

Время восхода и захода Солнца для заданной точки наблюдения соответствуют ожидаемым с учётом принятых ранее допущений.

Настройка подсистемы для установки координат и времени из периферии контроллера

Как итог разработки и тестирования модели перейдём к завершающей конфигурации модели - определения режимов задания входных переменных от микроконтроллера:

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

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

Генерируем код из модели:

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

Открыв сгенерированные файлы, можно убедиться, что входные переменные определяются из заданных каналов:

sunrise_sunset_cg.png

sunrise_sunset_cg2.png

sunrise_sunset_cg3.png

Заключение

В этом демонстрационном примере были рассмотрены способы и возможности работы с обратными вызовами модели Engee для эффективной работы и автоматизации процессов параметрирования, конфигурирования, тестирования и генерации кода модели. Описанный алгоритм воспроизводит с учетом принятых допущений заданные расчёты, а модель может быть переконфигурирована под различные задачи тестирования и генерации кода.

Список источников

  1. Астрономический календарь на 2019 год. Выпуск 19 (116) / С.М. Пономарев, Н.И. Лапин, М.А. Фаддеев, А.П. Гажулина; под ред. С.М. Пономарева. – Н. Новгород: Изд-во ННГУ им. Н.И. Лобачевского, 2018. – 351 с.
  2. Курс общей астрономии / П. И. Бакулин, Э. В. Кононович, В. И. Мороз. — М.: Наука, 1976.
  3. Черный М.А. Авиационная астрономия. - М.: Транспорт, 1978. - 208 с.
  4. Almanac for Computers, 1990 published by Nautical Almanac Office United States Naval Observatory Washington, DC 20392

Блоки, использованные в примере