Engee 文档
Notebook

使用回调自动计算日出日落时间

本示例描述了一个根据给定时间和坐标计算日出日落时间的模型。该模型有望成为物联网设备模型的子系统。为了自动计算和测试模型的算法,我们积极使用了模型的回调。因此,该模型无需使用脚本即可自动确定当前日期、位置和时区。

模型简介

模型开发的主要目标是创建一个子系统,用于计算太阳运动参数,并对当前日期和坐标进行不同类型的可能设置。该子系统的使用方向是 "物联网 "的各种设备,支持从实时时钟或 NTP 服务器接收当前日期、静态设置坐标或通过 GPS 自动确定坐标。
要测试这样一个子系统的算法,就必须能够改变设置当前日期的方式。该模型使用 回调,可以自动定义不同设置方式下的必要输入参数。

模型示例

示例模型是IoT_sunrise_sunset.engee 。其主要计算模块是"时间等式 ""太阳偏角 ""时角 ""日出日落时间 "子系统。

sunrise_sunset.png

Multiport Switch (紫色)用于切换计算模型的输入数据。块Constant (绿松石色)--从工作区向模型传递常量和变量。图块InportOutport (橙色)用作子系统的输入和输出,以便在控制器程序中交换数值。程序块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$ - 时间等式,min;
$D$ - 辅助变量,rad;
$Y_{now}$ - 当前年份;
$D_{all}=365.256$ - sideric year;
$D_{now}$ - 当前日期(按年份顺序排列,其中第 1 天为 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$ - 辅助变量,rad;
$\varepsilon = 23.4372 \cdot \pi / 180$ - 地球自转轴的倾角,rad;
$\delta$ - 太阳偏角,弧度。

子系统 "时钟角度 " (hour_angle):

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

其中$\varphi$ 是观测点的纬度,rad;
$t$ - 时角,小时。
这个表达式没有考虑水平视差、视半径和太阳折射的影响。

子系统 "日出、日落时间 " (Sunrise_Suncet_time):

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

其中$t_в$ - 日出时间,h;
$t_з$ - 日落时间,小时
$12^h = 12$ - 直接上升时间,小时
$\lambda$ - 观测点经度,度;
$UTC$ - 小时区,小时。

计算表达式来自示例末尾给出的文献资料。

设置输入参数

模型示例支持几种设置输入变量的方法:
*$Y_{now}$ - 可通过工作区中的变量current_year 或子系统端口from_MCU_Y 设置。两者之间的切换由工作区中的变量year_set_mode 的值完成。默认情况下,year_set_mode = 1 ; *$\varphi,\ \lambda,\ UTC$ - 可由工作区或子系统端口from_MCU_GCS 的变量矢量latitudelongitudetime_zone 设置。它们之间的切换由工作区中的变量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 = Axial_tilt = 23.4372 * pi / 180; # 地球自转轴的倾斜度,弧度(23°26′14″)
days_in_year = 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
    日数 = 0
    纬度 = 0.0
    经度 = 0.0
    时区 = 0

    # 设置模型的默认模式
    day_number_set_mode = 1 
    year_set_mode = 1 
    GCS_set_mode = 1

    # 通过 IP 进行 "计算
    ## 加载库
    import Pkg.add("Gumbo")
    Pkg.add("Gumbo")
    Pkg.add("HTTP")
    Pkg.add("AbstractTrees")

    ## 连接库
    使用 HTTP、Gumbo、AbstractTrees

    ## 从网上获取页面
    site = HTTP.get("https://ip2geolocation.com/")
    site_code = parsehtml(String(site.body))
    site_body = site_code.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]
    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)

    # 默认时间和位置设置
    latitude = latitude_IP
    longitude = longitude_IP
    time_zone = time_zone

    current_year = year_now = Dates.value(Year(now())
结束

从代码注释中可以看到,这个回调函数声明了常量和变量,定义了常量和变量的默认值。请注意,为确定位置和时区,模型会调用一个外部网站,该网站会根据连接点的 IP 确定地理坐标,并将页面作为 HTML 对象传递到 Engee 工作区。在代码生成过程中,您可以在CG_start.jl 文件中设置CG_start = 1 标志,以手动定义变量赋值通道。

</> PresaveFunc 回调选项卡包含以下代码:

``朱莉娅 如果 (day_number_set_mode == 1)

year_year = current_year = Dates.value(Year(now())

latitude = latitude_IP
longitude = longitude_IP
time_zone = time_zone

结束

如果 (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

结束


该函数起辅助作用--当日工作模式改变时,需要更新输入变量的值。因此,在更改工作模式并保存模型后,新的变量值将被传送到工作区。

</> 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) / 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.普通天文学教程 / P. I. Bakulin, E. V. Kononovich, V. I. Moroz.- Moscow: Nauka, 1976.
  2. 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