SmartSymbol

June 11, 2026 · View on GitHub

SmartSymbol is a derived weather parameter computed from several NWP model fields. It produces an integer code describing the dominant weather condition at a location and time. Day values are 1–77; 100 is added when solar position indicates darkness, giving night values 101–177.

Note

This document covers SmartSymbol as derived from gridded forecast data in the QEngine. SmartSymbol can also be derived from observations, but that is handled by a separate codebase and is out of scope here.

Input parameters

ParameterFMI IDNotes
Total cloud cover (%)kFmiTotalCloudCoverAlways required
Probability of thunderstorm (%)kFmiProbabilityThunderstormOptional; triggers thunder branch when ≥ 30 %
1-hour precipitation (mm)kFmiPrecipitation1hRequired when no thunder
Precipitation formkFmiPotentialPrecipitationForm / kFmiPrecipitationFormRequired when precipitation ≥ 0.025 mm
Precipitation typekFmiPotentialPrecipitationType / kFmiPrecipitationTypeRequired for liquid water branch
Fog intensitykFmiFogIntensityOptional; triggers fog symbol when > 0

Precipitation form codes

CodeMeaning
0Drizzle
1Water (rain)
2Sleet
3Snow
4Freezing drizzle
5Freezing rain
6Hail
7Snow pellets
8Ice particles

Precipitation type codes

CodeMeaning
1Large-scale (stratiform)
2Convective (shower)

Calculation logic

The algorithm evaluates conditions in strict priority order and returns as soon as a branch matches:

  1. Thunder — if POT ≥ 30 %, return a thunderstorm symbol immediately (no precipitation form considered).
  2. No precipitation — if rain < 0.025 mm/h, check fog intensity, then cloud cover.
  3. Precipitation form — select the sub-branch: drizzle, freezing drizzle, freezing rain, snow pellets/ice, hail, rain, sleet, or snow.
  4. Precipitation type — for liquid rain only: convective (showers) vs. large-scale (stratiform).
  5. Day/night — add 100 to any base symbol when solar position is dark.

Symbol value table

Night variants: add 100 to every value (e.g. day 31 → night 131).

Clear / cloudy / fog (no precipitation, rain < 0.025 mm/h)

SymbolCondition
1Clear — cloud cover < 20 %
2Mostly clear — 20–33 %
4Partly cloudy — 33–72 %
6Mostly cloudy — 72–93 %
7Overcast — ≥ 93 %
9Fog — fog intensity > 0 (any cloud cover)

Drizzle and freezing precipitation (fixed — no cloudiness or intensity variation)

SymbolPrecipitation form
11Drizzle (form = 0)
14Freezing drizzle (form = 4)
17Freezing rain (form = 5)

Showers — convective rain (form = 1, type = 2)

SymbolCloud cover
21< 72 %
2472–93 %
27≥ 93 %

Large-scale rain (form = 1, type = 1 or missing)

Intensity thresholds: light < 0.4 mm/h, moderate 0.4–4 mm/h, heavy ≥ 4 mm/h.

SymbolCloud coverPrecipitation (mm/h)
31< 72 %< 0.4
32< 72 %0.4–4
33< 72 %≥ 4
3472–93 %< 0.4
3572–93 %0.4–4
3672–93 %≥ 4
37≥ 93 %< 0.4
38≥ 93 %0.4–4
39≥ 93 %≥ 4

Sleet (form = 2)

Intensity thresholds: light < 0.4 mm/h, moderate 0.4–1.5 mm/h, heavy ≥ 1.5 mm/h.

SymbolCloud coverPrecipitation (mm/h)
41< 72 %< 0.4
42< 72 %0.4–1.5
43< 72 %≥ 1.5
4472–93 %< 0.4
4572–93 %0.4–1.5
4672–93 %≥ 1.5
47≥ 93 %< 0.4
48≥ 93 %0.4–1.5
49≥ 93 %≥ 1.5

Snow (form = 3)

Same intensity thresholds as sleet: light < 0.4 mm/h, moderate 0.4–1.5 mm/h, heavy ≥ 1.5 mm/h.

SymbolCloud coverPrecipitation (mm/h)
51< 72 %< 0.4
52< 72 %0.4–1.5
53< 72 %≥ 1.5
5472–93 %< 0.4
5572–93 %0.4–1.5
5672–93 %≥ 1.5
57≥ 93 %< 0.4
58≥ 93 %0.4–1.5
59≥ 93 %≥ 1.5

Note: symbol 57 is shared with the snow-pellets/ice-particles fallback (form 7 or 8 → 57 directly). Both resolve to the same meaning: overcast snowfall.

Hail (form = 6)

SymbolCloud cover
61< 72 %
6472–93 %
67≥ 93 %

Thunder (POT ≥ 30 %)

Checked before precipitation form; overrides all other branches.

SymbolCloud cover
71< 72 %
7472–93 %
77≥ 93 %

Troubleshooting

SmartSymbol is missing (no value returned)

The function returns missing if any required input is absent or its interpolated value is kFloatMissing. Check in order:

  1. kFmiTotalCloudCover is always required — missing or missing-valued → None.
  2. kFmiPrecipitation1h is required unless POT ≥ 30 % — missing or missing-valued → None.
  3. When rain ≥ 0.025 mm/h, both kFmiPotentialPrecipitationForm and kFmiPrecipitationForm are absent → None.

Symbol is always overcast (7, 37, 38, 39, 57–59, 74, 77, …)

If cloud cover is unexpectedly treated as overcast everywhere, the interpolated kFmiTotalCloudCover value may be IEEE NaN rather than kFloatMissing. NaN passes the == kFloatMissing guard and then fails every < threshold comparison, making the code behave as though cloud cover is above all limits. See the edge cases section for details.

Getting drizzle (11) unexpectedly when precipitation form should be something else

When kFmiPotentialPrecipitationForm / kFmiPrecipitationForm returns IEEE NaN, casting it to int is undefined behaviour. On most x86 platforms this produces 0, which is the drizzle code, so symbol 11 is returned instead of None.

Thunder symbols (71–77) with no apparent storm

The POT threshold is only 30 %. A relatively low forecast probability of thunderstorms is enough to trigger the thunder branch, which overrides precipitation form entirely. Inspect the kFmiProbabilityThunderstorm value at the location and time in question.

Correct precipitation type but wrong intensity level

Check the actual kFmiPrecipitation1h value against the thresholds for the form in question:

  • Rain (form = 1): light < 0.4 mm/h, moderate 0.4–4 mm/h, heavy ≥ 4 mm/h
  • Sleet (form = 2) and snow (form = 3): light < 0.4 mm/h, moderate 0.4–1.5 mm/h, heavy ≥ 1.5 mm/h

Showers (21–27) when large-scale rain (31–39) is expected, or vice versa

Check kFmiPotentialPrecipitationType / kFmiPrecipitationType: 2 = convective (showers), 1 = large-scale. If the parameter is absent the code defaults to large-scale.

Symbol value above 77 during daytime

If a daytime symbol exceeds 77 but is not in the expected night range (101–177), a precipitation form code outside 0–8 has reached the catch-all formula 10×form+21+10 \times \text{form} + 21 + … and produced an out-of-range value. See the edge cases section.

Edge cases

SmartSymbol cannot be IEEE NaN

calc_smart_symbol() returns std::optional<int>. Every code path either returns {} (which becomes TS::None() / missing in SmartSymbolNumber()) or an integer produced by arithmetic on small constant values. The day/night addition 100 + *symbol is also integer-only. The output is therefore always either a concrete integer or missing — never a floating-point NaN.

Only kFloatMissing is guarded, not IEEE NaN

All missing-value checks use == kFloatMissing. If q.interpolate() returns an IEEE NaN (possible when underlying grid data is corrupt or improperly initialized), the NaN passes through every == kFloatMissing guard silently:

VariableWhat happens with NaN
n (cloud cover)All n < limit comparisons return false → treated as overcast (≥ 93 %) in every branch
rainrain < 0.025 returns false → precipitation branch entered even though no valid rain value exists
rformstatic_cast<int>(NaN) is undefined behaviour; on most platforms yields 0 (drizzle) or INT_MIN, silently producing a wrong symbol instead of None
rtypeSame UB issue; if the cast accidentally produces 2 the convective branch fires incorrectly
fogNaN > 0 returns false → fog branch safely skipped
thunderNaN >= 30 returns false → thunder branch safely skipped

kFloatMissing for rtype silently falls through to large-scale

When the precipitation type parameter exists but its interpolated value is kFloatMissing (≈ 32700), there is no explicit guard. static_cast<int>(kFloatMissing) ≈ 32700, which is not equal to 2 (convective), so the code falls through to the large-scale rain branch. This happens to be the correct default but relies on kFloatMissing not coincidentally equalling 2.

Unrecognized rform values produce out-of-range symbols

Form codes 0–8 are all handled explicitly. Any other value reaches the catch-all formula 10 * rform + 21 + …. For rform ≥ 9 this produces values ≥ 112, which overlap the night range (100+) and would be misinterpreted as nighttime variants of existing symbols. There is no guard that returns missing for unknown form codes.

thunder condition order

The thunder check is written as thunder >= thunder_limit1 && thunder != kFloatMissing. Because kFloatMissing ≈ 32700 satisfies >= 30, the first sub-expression is true for missing values. The second sub-expression saves it. Swapping the order would be safer and more idiomatic, but the current order is not a bug in practice.

Source

querydata/Q.cpp, function calc_smart_symbol() (lines 767–883) and SmartSymbolNumber() (lines 1014–1037).