Skip to content
Snippets Groups Projects
Commit ddbc48d1 authored by Florian Obersteiner's avatar Florian Obersteiner :octopus:
Browse files

Merge branch 'dev-0012' into 'main'

Dev 0012

See merge request !3
parents b55bfe10 75d9cc60
No related branches found
No related tags found
1 merge request!3Dev 0012
......@@ -15,9 +15,7 @@ Types of changes
## TODOs
- add precision multiplier as a quantity to play around with
- limit data loading to columns that are actually used / should be visible
- add info: departure and destination airports
- get version number from latest git tag
### Data Processing
......@@ -36,6 +34,21 @@ Types of changes
## [Unreleased]
## v0.0.12 - 2025-03-24
### Added
- flight info
- variable info
### Changed
- get version number from latest git tag
- exclude processing variables from Variables dropdown
- clean-up of flight and variable selection
## v0.0.11 - 2025-03-24
### Added
- DotDict class for configuration
......
IATA;PosLat;PosLon;masl;Name;Continent;Pltflght_col;Plttxt_lat;Plttxt_lon;show_on_map
BKK;13.68109989;100.7470016;1.524;Bangkok, Thailand;Asia;r;14.7;100.8;1
BOG;4.70159;-74.1469;2548.4328;Bogota, Columbia;South_America;lime;1.2;-74.1;1
BOS;42.36429977;-71.00520325;6.096;Boston, USA;North_America;cyan;43;-70;1
CAN;23.39240074;113.2990036;15.24;Guangzhou, China;Asia;r;23.7;104.5;1
CCS;10.601194;-66.991222;71.3232;Caracas, Venezuela;South_America;lime;11.6;-75;1
CMB;7.180759907;79.88410187;9.144;Colombo, Sri Lanka;Asia;r;7.9;79.9;0
CPT;-33.96480179;18.60169983;46.0248;Cape Town, South Africa;Africa;gold;-32.9;18.6;1
DEN;39.86169815;-104.6729965;1655.3688;Denver, USA;North_America;cyan;40.8;-104.6;1
DUS;51.289501;6.76678;44.8056;Duesseldorf, Germany;Europe;w;52.3;-4.2;0
EZE;-34.8222;-58.5358;20.4216;Buenos Aires, Argentina;South_America;lime;-33.8;-58.5;0
FRA;50.033333;8.570556;110.9472;Frankfurt, Germany;Europe;w;51;9.5;1
GIG;-22.80999947;-43.25055695;8.5344;Rio de Janeiro, Brazil;South_America;lime;-25.9;-43.2;1
GRU;-23.43555641;-46.47305679;749.5032;Sao Paulo, Brazil;South_America;lime;-22.7;-55.6;1
HKG;22.32030404;114.1980743;8.5344;Hong Kong, China;Asia;r;19;113.9;1
HND;35.552299;139.779999;10.668;Tokyo, Japan;Asia;r;36.5;139.7;1
HOG;20.78560066;-76.31510162;110.0328;Holguin, Cuba;South_America;lime;16.8;-82.6;0
IAH;29.9843998;-95.34140015;29.5656;Houston, USA;North_America;cyan;30.9;-95.3;1
ICN;37.46910095;126.4509964;7.0104;Seoul, South Korea;Asia;r;38;126.5;1
JFK;40.63980103;-73.77890015;3.9624;New York, USA;North_America;cyan;36.3;-74;1
JNB;-26.1392;28.246;1694.0784;Johannesburg, South Africa;Africa;gold;-24.6;28.7;1
KIX;34.4272995;135.2440033;7.9248;Osaka, Japan;Asia;r;30.9;128.5;1
KUL;2.745579958;101.7099991;21.0312;Kuala Lumpur, Malaysia;Asia;r;3.7;101.7;1
LAX;33.94250107;-118.4079971;38.1;Los Angeles, USA;North_America;cyan;30.6;-124.9;1
MAA;12.99000549;80.16929626;15.8496;Chennai, India;Asia;r;13.9;80.1;1
MBA;-4.034830093;39.59420013;60.96;Mombasa, Kenya;Africa;gold;-3;39.6;0
MCO;28.42939949;-81.30899811;29.2608;Orlando, USA;North_America;cyan;29.4;-81.3;1
MEX;19.4363;-99.072098;2229.9168;Mexico City, Mexico;North_America;cyan;20.4;-99;1
MLE;4.191830158;73.52909851;1.8288;Male, Maledives;Asia;r;0.1;67.1;0
MNL;14.5086;121.019997;22.86;Manila, Philippines;Asia;r;11;121;1
MUC;48.353783;11.786086;453.2;Munich, Germany;Europe;w;44.3;7.8;1
ORD;41.97859955;-87.90480042;204.8256;Chicago, USA;North_America;cyan;37.9;-90.9;1
PEK;40.08010101;116.5849991;35.3568;Beijing, China;Asia;r;41;116.5;1
PMV;10.91260338;-63.96659851;22.5552;Isla Magerita, Venezuela;South_America;lime;7.1;-63.2;1
POP;19.75790024;-70.56999969;4.572;Puerto Plata, Dominican Republic;South_America;lime;20.7;-70.5;0
PVG;31.14340019;121.8050003;3.9624;Shanghai Pudong, China;Asia;r;28.8;113.3;1
SCL;-33.39300156;-70.78579712;473.964;Santiago de Chile, Chile;South_America;lime;-32.5;-70.7;1
SFO;37.61899948;-122.375;3.9624;San Francisco, USA;North_America;cyan;38.6;-122.3;1
VRA;23.03440094;-81.43530273;64.008;Varadero, Cuba;South_America;lime;24;-87.9;0
WDH;-22.4799;17.4709;1719.072;Windhoek, Namibia;Africa;gold;-20.4;17.2;0
YVR;49.19390106;-123.1839981;4.2672;Vancouver, Canada;North_America;cyan;50.1;-123.1;1
YYZ;43.67720032;-79.63059998;173.4312;Toronto, Canada;North_America;cyan;44.6;-79.6;1
VERSION = "v0.0.11"
VERSION = "v0.0.12" # fall-back
DEBUG = true
# FRONTEND config
......@@ -21,79 +21,79 @@ ACN_PRC_NSIGMA = 3
# anything that is not in VARIABLES will not appear on the dashboard
VARIABLES = [
"lat",
"lon",
"alt",
"p",
"Ozone",
"CO",
"ACE",
"ACE_prc",
"ACN",
"ACN_prc",
"WindSpeed",
"WindDirTr",
"ToAirTmp",
"StcAirTmp",
"Tpot",
"H2Ogas",
"NO",
"NOy",
"TGM",
"GEM",
"CO",
"CO2",
"CH4",
"N4_12",
"N12",
"N18",
"PNumC",
"PSurfC",
"PVolC",
"PMassC",
"PSizeD01",
"PSizeD02",
"PSizeD03",
"PSizeD04",
"PSizeD05",
"PSizeD06",
"PSizeD07",
"PSizeD08",
"PSizeD09",
"PSizeD10",
"SC_Conc",
"BC_Mass_Conc",
"BC_Conc",
"temp__k_",
"pv__pvu_",
"pot_temp__k_",
"eq_pott_temp__k_",
"spec_hum__g_kg_",
"u__m_s_",
"v__m_s_",
"w__mubar_s_",
"wind_speed__m_s_",
"wind_dir__deg_",
"h2o__ppmv_",
"rh___",
"z__0_1_g_m_",
"eq_latitude_deg_n_",
"cc",
"clwc_g_kg_",
"clic_g_kg_",
"p_strop__hpa_",
"p_dtrop__hpa_",
"t_strop__k_",
"t_dtrop__k_",
"pt_strop__k_",
"pt_dtrop__k_",
"pv_strop__pvu_",
"z_strop__01grav_m_",
"z_dtrop__01grav_m_",
"dp_strop__hpa_",
"dp_dtrop__hpa_",
"cpc1_amb_ncm3",
"cpc2_amb_ncm3",
"opc1_amb_ncm3",
"opc12_amb_ncm3",
"lat",
"lon",
"alt",
"p",
"Ozone",
"CO",
"ACE",
"ACE_prc",
"ACN",
"ACN_prc",
"WindSpeed",
"WindDirTr",
"ToAirTmp",
"StcAirTmp",
"Tpot",
"H2Ogas",
"NO",
"NOy",
"TGM",
"GEM",
"CO",
"CO2",
"CH4",
"N4_12",
"N12",
"N18",
"PNumC",
"PSurfC",
"PVolC",
"PMassC",
"PSizeD01",
"PSizeD02",
"PSizeD03",
"PSizeD04",
"PSizeD05",
"PSizeD06",
"PSizeD07",
"PSizeD08",
"PSizeD09",
"PSizeD10",
"SC_Conc",
"BC_Mass_Conc",
"BC_Conc",
"temp__k_",
"pv__pvu_",
"pot_temp__k_",
"eq_pott_temp__k_",
"spec_hum__g_kg_",
"u__m_s_",
"v__m_s_",
"w__mubar_s_",
"wind_speed__m_s_",
"wind_dir__deg_",
"h2o__ppmv_",
"rh___",
"z__0_1_g_m_",
"eq_latitude_deg_n_",
"cc",
"clwc_g_kg_",
"clic_g_kg_",
"p_strop__hpa_",
"p_dtrop__hpa_",
"t_strop__k_",
"t_dtrop__k_",
"pt_strop__k_",
"pt_dtrop__k_",
"pv_strop__pvu_",
"z_strop__01grav_m_",
"z_dtrop__01grav_m_",
"dp_strop__hpa_",
"dp_dtrop__hpa_",
"cpc1_amb_ncm3",
"cpc2_amb_ncm3",
"opc1_amb_ncm3",
"opc12_amb_ncm3",
]
......@@ -10,13 +10,20 @@ import pandas as pd
import plotly.graph_objects as go
from dash import Dash
from dash.dependencies import Input, Output
from plotly.validators.scatter.marker import SymbolValidator
from src.config import DotDict
from src.config import DotDict, get_latest_semantic_version_tag
from src.data import loader
from src.layout import create_layout
raw_symbols = SymbolValidator().values # https://plotly.com/python/marker-style/
appconfig = DotDict.from_toml_file("./config/env.toml")
appconfig.VERSION = get_latest_semantic_version_tag() or appconfig.VERSION
varconfig = pd.read_csv("./config/parms_units.csv", sep=";")
varconfig = varconfig.set_index("VNAME")
airportsconfig = pd.read_csv("./config/CARIBIC2_Airports.csv", sep=";")
airportsconfig = airportsconfig.set_index("IATA")
# fmt: off
app = Dash(
......@@ -30,20 +37,25 @@ app.title = appconfig.TITLE
app.css.config.serve_locally = True
app.scripts.config.serve_locally = True
data = loader(Path("./testdata").glob("*.nc"), appconfig, True) # type: ignore
data, ap_info = loader(Path("./testdata").glob("*.nc"), appconfig, airportsconfig, True) # type: ignore
app.layout = create_layout(data, appconfig)
server = app.server # for deployment via gunicorn / WSGI server
@app.callback(
[Output("fig-map", "figure"), Output("fig-ts", "figure")],
[
Output("fig-map", "figure"),
Output("fig-ts", "figure"),
Output("flight-dep-dest", "children"),
Output("var-info", "children"),
],
[Input("flight-dropdown", "value"), Input("variable-dropdown", "value")],
)
def update_map(selected_flight: int, selected_variable: str):
def update_plots(selected_flight: int, selected_variable: str):
filtered_df = data[data["flight_number"] == selected_flight]
valid_indices = filtered_df[selected_variable].notna()
unit = varconfig.loc[selected_variable == varconfig.VNAME, "Unit"].values[0]
unit = varconfig.loc[selected_variable, :].Unit
fig_map = go.Figure(
go.Scattermap(
......@@ -65,17 +77,29 @@ def update_map(selected_flight: int, selected_variable: str):
name="Flight Path",
)
)
fig_map.add_trace(
fig_map.add_trace( # marker to indicate departure airport
go.Scattermap(
lat=[filtered_df.lat.iloc[0]],
lon=[filtered_df.lon.iloc[0]],
mode="markers",
marker={"size": 15, "symbol": "airport", "allowoverlap": True},
marker={"size": 15, "allowoverlap": True, "symbol": "airport", "angle": 45},
marker_symbol="airport",
text=["departure airport"],
hoverinfo="text",
name="departure",
)
)
fig_map.add_trace( # marker to indicate destination airport
go.Scattermap(
lat=[filtered_df.lat.iloc[-1]],
lon=[filtered_df.lon.iloc[-1]],
mode="markers",
marker={"size": 15, "allowoverlap": True, "symbol": "airport", "angle": 225},
text=["destination airport"],
hoverinfo="text",
name="destination",
)
)
fig_map.update_layout(
map={
......@@ -132,7 +156,12 @@ def update_map(selected_flight: int, selected_variable: str):
fig_ts.update_xaxes(title="UTC")
fig_ts.update_yaxes(title=f"{selected_variable} {unit}")
return [fig_map, fig_ts]
return [
fig_map,
fig_ts,
ap_info[selected_flight],
varconfig.loc[selected_variable, :].Long_Name,
]
if __name__ == "__main__":
......
[project]
name = "caribic-dash"
version = "0.0.11"
version = "0.0.12"
description = "IRISCC dashboard with CARIBIC data"
readme = "README.md"
requires-python = ">=3.12"
......@@ -9,14 +9,14 @@ classifiers = [
"Operating System :: OS Independent",
]
dependencies = [
"dash>=2.18.2",
"dash-bootstrap-components>=2.0.0",
"gunicorn>=23.0.0",
"netcdf4>=1.7.2", # indirect; backend for xarray
"pandas>=2.2.3",
"plotly>=6.0.1",
"python-dotenv>=1.0.1",
"xarray>=2025.3.0",
"dash>=2.18.2",
"dash-bootstrap-components>=2.0.0",
"gunicorn>=23.0.0",
"netcdf4>=1.7.2", # indirect; backend for xarray
"pandas>=2.2.3",
"plotly>=6.0.1",
"python-dotenv>=1.0.1",
"xarray>=2025.3.0",
]
[dependency-groups]
......
import re
import subprocess
import tomllib
from typing import List, Optional, Tuple
class DotDict(dict):
......@@ -84,3 +87,63 @@ class DotDict(dict):
del self[key]
else:
raise AttributeError(f"'{self.__class__.__name__}' object has no attribute '{key}'")
# -----------------------------------------------------------------------------
def parse_semantic_version(tag: str) -> Optional[Tuple[int, int, int]]:
"""
Parses a semantic version tag and returns the major, minor, and patch versions.
Args:
tag: The tag string.
Returns:
A tuple of (major, minor, patch) if the tag matches the semantic version pattern,
or None otherwise.
"""
match = re.match(r"v(\d+)\.(\d+)\.(\d+)$", tag)
if match:
return int(match.group(1)), int(match.group(2)), int(match.group(3))
return None
def get_latest_semantic_version_tag() -> Optional[str]:
"""
Retrieves the latest git tag that matches the semantic versioning pattern.
Returns:
The latest semantic version tag as a string, or None if no matching tags are found or an error occurs.
"""
try:
process = subprocess.run(["git", "tag"], capture_output=True, text=True, check=True)
tags = process.stdout.strip().splitlines()
semantic_tags: List[Tuple[Tuple[int, int, int], str]] = []
for tag in tags:
version_tuple = parse_semantic_version(tag)
if version_tuple:
semantic_tags.append((version_tuple, tag))
if not semantic_tags:
return None
# Sort the semantic tags by version in descending order.
semantic_tags.sort(key=lambda x: x[0], reverse=True)
return semantic_tags[0][1] # return the tag string
except subprocess.CalledProcessError:
return None
except FileNotFoundError:
return None
if __name__ == "__main__":
latest_tag = get_latest_semantic_version_tag()
if latest_tag:
print(f"Latest semantic version tag: {latest_tag}")
else:
print("No semantic version tags found or an error occurred.")
......@@ -13,11 +13,15 @@ import xarray as xr
from .config import DotDict
def loader(paths: Iterable[Path], config: DotDict, drop: bool = False) -> pd.DataFrame:
def loader(
paths: Iterable[Path], config: DotDict, apconfig: pd.DataFrame, drop: bool = False
) -> (pd.DataFrame, dict):
"""
Load multiple netCDF files into a single DataFrame.
"""
dataframes = []
airports = {}
for path in sorted(paths):
fullset = xr.open_dataset(path).to_dataframe()
if drop:
......@@ -29,6 +33,10 @@ def loader(paths: Iterable[Path], config: DotDict, drop: bool = False) -> pd.Dat
# extract flight number and date from filename; add as extra columns
df["flight_number"] = int(path.name.split("_")[2])
airports[int(path.name.split("_")[2])] = (
f"from: {path.name.split('_')[3]} ({apconfig.loc[path.name.split('_')[3], :].Name}), to: {path.name.split('_')[4]} ({apconfig.loc[path.name.split('_')[4], :].Name})"
)
df["date"] = datetime.strptime(path.stem.split("_")[1], "%Y%m%d").strftime("%Y-%m-%d")
# make a linear interpolation of the acetonitrile column for BB flaggin
if "ACN" in df.columns:
......@@ -42,4 +50,4 @@ def loader(paths: Iterable[Path], config: DotDict, drop: bool = False) -> pd.Dat
dataframes.append(df)
return pd.concat(dataframes)
return (pd.concat(dataframes), airports)
......@@ -30,7 +30,9 @@ def create_layout(df: pd.DataFrame, config: DotDict) -> list:
"label": c,
"value": c,
}
# exclude processing variables:
for c in df.columns
if c not in ["flight_number", "date", "BB_flag"]
]
var_sel_idx = 0
for idx, vo in enumerate(var_opts):
......@@ -65,7 +67,7 @@ def create_layout(df: pd.DataFrame, config: DotDict) -> list:
html.A(
html.Img(
src=dash.get_asset_url("iriscc_logo.svg"),
className="header-logo"
className="header-logo",
),
href=config.IRISCC_URL,
target="_blank",
......@@ -98,9 +100,14 @@ def create_layout(df: pd.DataFrame, config: DotDict) -> list:
searchable=True,
style={
"display": "inline-block",
"width": "240px",
"width": "180px",
},
),
html.Label(
"",
id="flight-dep-dest",
style={"margin-left": "29px", "font-size": "12px"},
),
],
style={
"display": "flex",
......@@ -123,10 +130,15 @@ def create_layout(df: pd.DataFrame, config: DotDict) -> list:
clearable=False,
searchable=True,
style={
"width": "40%",
"display": "inline-block",
"width": "180px",
},
),
html.Label(
"",
id="var-info",
style={"margin-left": "29px", "font-size": "12px"},
),
],
style={
"display": "flex",
......
import os
import subprocess
import sys
import tempfile
import tomllib
import unittest
from io import StringIO
from unittest.mock import Mock, patch
from .config import DotDict
from .config import DotDict, get_latest_semantic_version_tag, parse_semantic_version
class TestEnhancedDotDict(unittest.TestCase):
......@@ -223,5 +225,57 @@ port = 8080
self.assertIsInstance(dot_dict.nested_lists[0][0], DotDict)
class TestSemanticVersionTag(unittest.TestCase):
@patch("subprocess.run")
def test_get_latest_semantic_version_tag_valid(self, mock_run):
mock_process = Mock()
mock_process.stdout = "v1.0.0\nv0.9.0\nv1.0.1\nother_tag\n"
mock_run.return_value = mock_process
latest_tag = get_latest_semantic_version_tag()
self.assertEqual(latest_tag, "v1.0.1")
@patch("subprocess.run")
def test_get_latest_semantic_version_tag_no_semantic_tags(self, mock_run):
mock_process = Mock()
mock_process.stdout = "tag1\ntag2\ntag3\n"
mock_run.return_value = mock_process
latest_tag = get_latest_semantic_version_tag()
self.assertIsNone(latest_tag)
@patch("subprocess.run")
def test_get_latest_semantic_version_tag_empty_tags(self, mock_run):
mock_process = Mock()
mock_process.stdout = ""
mock_run.return_value = mock_process
latest_tag = get_latest_semantic_version_tag()
self.assertIsNone(latest_tag)
@patch("subprocess.run")
def test_get_latest_semantic_version_tag_git_error(self, mock_run):
mock_run.side_effect = subprocess.CalledProcessError(1, "git tag")
latest_tag = get_latest_semantic_version_tag()
self.assertIsNone(latest_tag)
@patch("subprocess.run")
def test_get_latest_semantic_version_tag_file_not_found(self, mock_run):
mock_run.side_effect = FileNotFoundError()
latest_tag = get_latest_semantic_version_tag()
self.assertIsNone(latest_tag)
def test_parse_semantic_version_valid(self):
self.assertEqual(parse_semantic_version("v1.2.3"), (1, 2, 3))
self.assertEqual(parse_semantic_version("v0.0.1"), (0, 0, 1))
def test_parse_semantic_version_invalid(self):
self.assertIsNone(parse_semantic_version("1.2.3"))
self.assertIsNone(parse_semantic_version("v1.2"))
self.assertIsNone(parse_semantic_version("v1.2.3-alpha"))
self.assertIsNone(parse_semantic_version("abc"))
if __name__ == "__main__":
unittest.main()
......@@ -67,7 +67,7 @@ wheels = [
[[package]]
name = "caribic-dash"
version = "0.0.11"
version = "0.0.12"
source = { virtual = "." }
dependencies = [
{ name = "dash" },
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment