コンテンツにスキップ

天文計算モジュール仕様 (Astronomical Utilities)


Copyright (c) 2026 Masanori Sakai

Licensed under the MIT License


概要

astro_utils.py は、天文薄暮期間の計算と検出時間帯の判定を行うモジュールです。

目次


モジュール概要

依存ライブラリ

from datetime import datetime, timedelta
from typing import Tuple
from astral import LocationInfo
from astral.sun import sun
from zoneinfo import ZoneInfo

外部依存: - astral: 太陽・月の位置計算ライブラリ - zoneinfo: Python 3.9+ 標準ライブラリ

インストール:

pip install astral

ファイル構成

astro_utils.py (82行)
├── get_detection_window()  # 検出時間帯を取得
└── is_detection_active()   # 現在が検出時間内か判定

関数リファレンス

get_detection_window()

シグネチャ:

def get_detection_window(
    latitude: float = 35.3606,
    longitude: float = 138.7274,
    timezone: str = "Asia/Tokyo"
) -> Tuple[datetime, datetime]:

説明: 天文薄暮期間(前日の日没から翌日の日出まで)を計算

パラメータ:

名前 デフォルト 説明
latitude float 35.3606 観測地の緯度(度)
longitude float 138.7274 観測地の経度(度)
timezone str "Asia/Tokyo" タイムゾーン名

戻り値: - Tuple[datetime, datetime]: (検出開始時刻, 検出終了時刻) - 両方ともタイムゾーン付きdatetimeオブジェクト

動作:

flowchart TD
    Start["関数呼び出し"]
    GetNow["現在時刻を取得<br/>(指定タイムゾーン)"]
    CalcSun["太陽情報を計算<br/>- 昨日<br/>- 今日<br/>- 明日"]
    CheckTime{"現在時刻 < 今日の日出?"}
    SetYesterday["前日日没 ~ 今日日出"]
    SetToday["今日日没 ~ 明日日出"]
    Return["(開始時刻, 終了時刻)"]

    Start --> GetNow
    GetNow --> CalcSun
    CalcSun --> CheckTime
    CheckTime -->|"Yes<br/>(夜が続いている)"| SetYesterday
    CheckTime -->|"No<br/>(日出後)"| SetToday
    SetYesterday --> Return
    SetToday --> Return

    style CheckTime fill:#ffe3c4

使用例:

from astro_utils import get_detection_window

# デフォルト座標(富士山頂)
start, end = get_detection_window()
print(f"検出期間: {start} ~ {end}")
# 出力例: 検出期間: 2026-02-01 16:45:23+09:00 ~ 2026-02-02 06:12:45+09:00

# 東京
start, end = get_detection_window(
    latitude=35.6762,
    longitude=139.6503,
    timezone="Asia/Tokyo"
)

# ニューヨーク
start, end = get_detection_window(
    latitude=40.7128,
    longitude=-74.0060,
    timezone="America/New_York"
)

is_detection_active()

シグネチャ:

def is_detection_active(
    latitude: float = 35.3606,
    longitude: float = 138.7274,
    timezone: str = "Asia/Tokyo"
) -> Tuple[bool, datetime, datetime]:

説明: 現在が検出時間帯内かどうかを判定

パラメータ:

名前 デフォルト 説明
latitude float 35.3606 観測地の緯度
longitude float 138.7274 観測地の経度
timezone str "Asia/Tokyo" タイムゾーン名

戻り値: - Tuple[bool, datetime, datetime]: - bool: 検出時間内かどうか - datetime: 検出開始時刻 - datetime: 検出終了時刻

動作:

flowchart TD
    Start["関数呼び出し"]
    GetWindow["get_detection_window()を呼び出し"]
    GetNow["現在時刻を取得"]
    Compare{"開始時刻 <= 現在 <= 終了時刻?"}
    ReturnTrue["(True, 開始, 終了)"]
    ReturnFalse["(False, 開始, 終了)"]

    Start --> GetWindow
    GetWindow --> GetNow
    GetNow --> Compare
    Compare -->|"Yes"| ReturnTrue
    Compare -->|"No"| ReturnFalse

    style Compare fill:#ffe3c4
    style ReturnTrue fill:#dff6e8
    style ReturnFalse fill:#f8d7da

使用例:

from astro_utils import is_detection_active

# 現在が検出時間内か確認
is_active, start, end = is_detection_active()

if is_active:
    print(f"検出期間中です: {start} ~ {end}")
else:
    print(f"検出期間外です。次回: {start} ~ {end}")

# 使用例: meteor_detector_rtsp_web.pyでの使用
if is_detection_active and is_detection_active(latitude, longitude, timezone)[0]:
    objects = detector.detect_bright_objects(gray, prev_gray)
    is_detecting_now = True
else:
    objects = []
    is_detecting_now = False

天文薄暮とは

定義

天文薄暮(Astronomical Twilight): 太陽が地平線下6度~18度の時間帯

graph TD
    subgraph "太陽の高度と時間帯"
        Day["昼間<br/>太陽高度 > 0°"]
        Civil["市民薄明<br/>0° ~ -6°"]
        Nautical["航海薄明<br/>-6° ~ -12°"]
        Astro["天文薄明<br/>-12° ~ -18°"]
        Night["完全な夜<br/>< -18°"]
    end

    Day --> Civil
    Civil --> Nautical
    Nautical --> Astro
    Astro --> Night

    style Day fill:#fff9c4
    style Civil fill:#ffe7bf
    style Nautical fill:#ffd8a8
    style Astro fill:#e3dcff
    style Night fill:#d6d9f8

流星観測に適した時間帯

時間帯 太陽高度 流星観測 本システムでの扱い
昼間 > 0° 不可 検出OFF
市民薄明 0° ~ -6° 困難 検出OFF
航海薄明 -6° ~ -12° やや困難 検出OFF
天文薄明 -12° ~ -18° 可能 検出OFF
完全な夜 < -18° 最適 検出ON

本システムの実装: - 簡略化: 日没(sunset)~日出(sunrise)を検出期間とする - 理由: 実用上十分な精度、計算が簡単


検出期間の例(東京、2026年2月2日)

2026-02-01
  16:45  前日の日没 ← 検出開始
  17:15  市民薄明終了
  17:45  航海薄明終了
  18:15  天文薄明終了 ← 完全な夜

2026-02-02
  05:30  天文薄明開始 ← 完全な夜終了
  06:00  航海薄明開始
  06:30  市民薄明開始
  06:55  日出 ← 検出終了

検出期間: 前日16:45 ~ 当日06:55(約14時間10分)


使用例

基本的な使い方

from astro_utils import get_detection_window, is_detection_active

# 東京での検出時間帯を取得
start, end = get_detection_window(
    latitude=35.6762,
    longitude=139.6503,
    timezone="Asia/Tokyo"
)

print(f"検出期間: {start.strftime('%H:%M')} ~ {end.strftime('%H:%M')}")
# 出力: 検出期間: 16:45 ~ 06:55

# 現在が検出時間内か確認
is_active, _, _ = is_detection_active(35.6762, 139.6503, "Asia/Tokyo")
print(f"検出可能: {'はい' if is_active else 'いいえ'}")

meteor_detector_rtsp_web.py での使用

# meteor_detector_rtsp_web.py (抜粋)
from astro_utils import is_detection_active

# 環境変数から設定を取得
enable_time_window = os.environ.get('ENABLE_TIME_WINDOW', 'false').lower() == 'true'
latitude = float(os.environ.get('LATITUDE', '35.3606'))
longitude = float(os.environ.get('LONGITUDE', '138.7274'))
timezone = os.environ.get('TIMEZONE', 'Asia/Tokyo')

# 検出ループ内
if enable_time_window and is_detection_active:
    # 1分ごとに検出時間をチェック
    if (current_time - last_time_check) > 60:
        is_detection_time, _, _ = is_detection_active(latitude, longitude, timezone)
        last_time_check = current_time

    if is_detection_time:
        # 検出処理を実行
        objects = detector.detect_bright_objects(gray, prev_gray)
        is_detecting_now = True
    else:
        # 検出処理をスキップ
        objects = []
        is_detecting_now = False
else:
    # 時間帯制限なし(常時検出)
    objects = detector.detect_bright_objects(gray, prev_gray)
    is_detecting_now = True

dashboard.py での使用

# dashboard.py (抜粋)
from astro_utils import get_detection_window

def do_GET(self):
    if self.path.startswith('/detection_window'):
        # ブラウザから送信された座標を取得
        from urllib.parse import urlparse, parse_qs
        query = parse_qs(urlparse(self.path).query)

        latitude = float(query.get('lat', [35.3606])[0])
        longitude = float(query.get('lon', [138.7274])[0])
        timezone = 'Asia/Tokyo'

        try:
            start, end = get_detection_window(latitude, longitude, timezone)
            result = {
                'start': start.strftime('%Y-%m-%d %H:%M:%S'),
                'end': end.strftime('%Y-%m-%d %H:%M:%S'),
                'enabled': os.environ.get('ENABLE_TIME_WINDOW', 'false').lower() == 'true',
                'latitude': latitude,
                'longitude': longitude
            }
        except Exception as e:
            result = {
                'error': str(e)
            }

        self.wfile.write(json.dumps(result).encode('utf-8'))

計算原理

Astralライブラリの使用

from astral import LocationInfo
from astral.sun import sun

# 観測地点の情報を作成
location = LocationInfo(
    name="Observer",
    region="",
    timezone="Asia/Tokyo",
    latitude=35.6762,
    longitude=139.6503
)

# 今日の太陽情報を取得
from zoneinfo import ZoneInfo
tz = ZoneInfo("Asia/Tokyo")
today = datetime.now(tz).date()

sun_info = sun(location.observer, date=today, tzinfo=tz)

# 取得できる情報
print(sun_info['sunrise'])    # 日出
print(sun_info['sunset'])     # 日没
print(sun_info['dawn'])       # 明け方(市民薄明開始)
print(sun_info['dusk'])       # 夕方(市民薄明終了)

計算フロー

sequenceDiagram
    participant Client as 呼び出し元
    participant AstroUtils as astro_utils.py
    participant Astral as astralライブラリ
    participant ZoneInfo as zoneinfo

    Client->>AstroUtils: get_detection_window(lat, lon, tz)
    AstroUtils->>ZoneInfo: ZoneInfo(timezone)
    ZoneInfo-->>AstroUtils: タイムゾーンオブジェクト

    AstroUtils->>AstroUtils: 現在時刻を取得

    AstroUtils->>Astral: LocationInfo作成
    AstroUtils->>Astral: sun(observer, 昨日, tz)
    Astral-->>AstroUtils: 昨日の太陽情報

    AstroUtils->>Astral: sun(observer, 今日, tz)
    Astral-->>AstroUtils: 今日の太陽情報

    AstroUtils->>Astral: sun(observer, 明日, tz)
    Astral-->>AstroUtils: 明日の太陽情報

    AstroUtils->>AstroUtils: 現在時刻 < 今日の日出?
    alt 夜が続いている
        AstroUtils->>AstroUtils: 前日日没 ~ 今日日出
    else 日出後
        AstroUtils->>AstroUtils: 今日日没 ~ 明日日出
    end

    AstroUtils-->>Client: (開始時刻, 終了時刻)

太陽位置の計算式

Astralライブラリ内部では以下の計算を行っています:

  1. ユリウス日の計算
  2. 太陽の赤経・赤緯の計算
  3. 時角の計算
  4. 高度角の計算

詳細はAstralドキュメントを参照


タイムゾーン処理

サポートされるタイムゾーン

Python 3.9+のzoneinfoがサポートする全タイムゾーン:

from zoneinfo import available_timezones

# 利用可能なタイムゾーン一覧
print(list(available_timezones())[:10])
# ['Africa/Abidjan', 'Africa/Accra', 'Africa/Addis_Ababa', ...]

主要なタイムゾーン

地域 タイムゾーン UTC offset
日本 Asia/Tokyo +09:00
韓国 Asia/Seoul +09:00
中国 Asia/Shanghai +08:00
アメリカ東部 America/New_York -05:00/-04:00
アメリカ西部 America/Los_Angeles -08:00/-07:00
イギリス Europe/London +00:00/+01:00
オーストラリア Australia/Sydney +10:00/+11:00

夏時間(DST)の処理

from zoneinfo import ZoneInfo
from datetime import datetime

# ニューヨーク(夏時間あり)
tz = ZoneInfo("America/New_York")

# 冬(標準時)
winter = datetime(2026, 1, 1, 12, 0, 0, tzinfo=tz)
print(winter.strftime('%Z %z'))  # EST -0500

# 夏(夏時間)
summer = datetime(2026, 7, 1, 12, 0, 0, tzinfo=tz)
print(summer.strftime('%Z %z'))  # EDT -0400

Astralライブラリの対応: - 自動的に夏時間を考慮 - タイムゾーンを正しく指定すれば自動処理


タイムゾーン変換

from datetime import datetime
from zoneinfo import ZoneInfo

# 東京時間で取得
tokyo_tz = ZoneInfo("Asia/Tokyo")
start, end = get_detection_window(35.6762, 139.6503, "Asia/Tokyo")

print(f"東京: {start.strftime('%Y-%m-%d %H:%M %Z')}")
# 出力: 東京: 2026-02-01 16:45 JST

# ニューヨーク時間に変換
ny_tz = ZoneInfo("America/New_York")
start_ny = start.astimezone(ny_tz)

print(f"NY: {start_ny.strftime('%Y-%m-%d %H:%M %Z')}")
# 出力: NY: 2026-02-01 02:45 EST

エラーハンドリング

よくあるエラー

1. 無効なタイムゾーン

# エラー
try:
    start, end = get_detection_window(timezone="Invalid/Timezone")
except ZoneInfoNotFoundError as e:
    print(f"エラー: {e}")

対策:

from zoneinfo import ZoneInfo, available_timezones

def validate_timezone(tz: str) -> bool:
    return tz in available_timezones()

# 使用前にチェック
if validate_timezone("Asia/Tokyo"):
    start, end = get_detection_window(timezone="Asia/Tokyo")

2. 極地での計算

極地(北緯66.5度以北、南緯66.5度以南)では白夜・極夜が発生:

# 北極圏(白夜の時期)
try:
    start, end = get_detection_window(
        latitude=80.0,  # 北極圏
        longitude=0.0,
        timezone="UTC"
    )
except ValueError as e:
    print(f"エラー: {e}")
    # エラー: Sun never sets on this day

対策:

from astral import AstralError

try:
    start, end = get_detection_window(latitude, longitude, timezone)
except AstralError as e:
    print(f"極地エラー: {e}")
    # フォールバック: 24時間検出
    start = datetime.now(ZoneInfo(timezone))
    end = start + timedelta(days=1)

パフォーマンス

計算時間

import time
from astro_utils import get_detection_window

# ベンチマーク
start_time = time.time()
for _ in range(1000):
    get_detection_window()
elapsed = time.time() - start_time

print(f"1000回の計算: {elapsed:.3f}秒")
print(f"1回あたり: {elapsed/1000*1000:.3f}ミリ秒")

# 出力例:
# 1000回の計算: 0.523秒
# 1回あたり: 0.523ミリ秒

結論: 非常に高速(1回あたり約0.5ms)


キャッシング

頻繁に呼び出す場合はキャッシュを推奨:

from functools import lru_cache
from datetime import datetime, timedelta

@lru_cache(maxsize=128)
def cached_detection_window(date_str: str, lat: float, lon: float, tz: str):
    """日付ベースでキャッシュ"""
    return get_detection_window(lat, lon, tz)

# 使用例
today = datetime.now().date().isoformat()
start, end = cached_detection_window(today, 35.6762, 139.6503, "Asia/Tokyo")

テストケース

import unittest
from datetime import datetime
from zoneinfo import ZoneInfo
from astro_utils import get_detection_window, is_detection_active

class TestAstroUtils(unittest.TestCase):
    def test_get_detection_window_tokyo(self):
        """東京での検出時間帯を取得"""
        start, end = get_detection_window(35.6762, 139.6503, "Asia/Tokyo")

        # 検証
        self.assertIsInstance(start, datetime)
        self.assertIsInstance(end, datetime)
        self.assertLess(start, end)  # 開始 < 終了
        self.assertEqual(start.tzinfo, ZoneInfo("Asia/Tokyo"))

    def test_is_detection_active(self):
        """検出時間の判定"""
        is_active, start, end = is_detection_active()

        # 検証
        self.assertIsInstance(is_active, bool)
        self.assertIsInstance(start, datetime)
        self.assertIsInstance(end, datetime)

    def test_different_timezones(self):
        """異なるタイムゾーンでの動作"""
        timezones = ["Asia/Tokyo", "America/New_York", "Europe/London"]

        for tz in timezones:
            start, end = get_detection_window(35.0, 139.0, tz)
            self.assertEqual(start.tzinfo, ZoneInfo(tz))

if __name__ == '__main__':
    unittest.main()

関連ドキュメント

外部リンク