Calculated Measures
When to compute rather than aggregate, how dependencies drive recalculation, and the mistakes that cause stuck values.
A calculated measure produces its values by evaluating an expression against other measures. It is how every KPI in Infohub gets built — efficiency, yield, OEE, cost per unit, specific energy consumption — and it is the retrieval type where most modelling judgement is exercised.
Aggregate or calculate?#
The two are easy to confuse because both derive values from other measures.
Aggregate when you want the same quantity over a longer period. Daily steam flow from hourly steam flow. Monthly production from daily production. One input measure, one interval step, a summary rule.
Calculate when you want a different quantity over the same period. Efficiency from output and input. Yield from good and total. Cost from consumption and price. Several input measures, arithmetic, same interval.
If you find yourself wanting to aggregate and combine in one step, you need two measures: aggregate each input to the interval you want, then calculate at that interval.
Calculate at the interval, then aggregate the calculation — carefully. A daily efficiency is not always the average of hourly efficiencies. If you need the daily figure to be "total output over total input for the day", calculate it from the daily totals, not by averaging the hourly efficiencies. Averaging ratios weights every hour equally regardless of how much ran in it, and produces a number that will not reconcile with the daily totals.
Creating one#
A calculated measure needs:
| Setting | Notes |
|---|---|
| Interval | The interval it produces values at. Its dependencies are read at this interval. |
| Dependencies | The measures the expression may reference |
| Expression | C# evaluating to a double? |
| Unit | The unit of the result, which is often not the unit of any input |
| Format | How the value is displayed |
Adding a dependency is what creates the Measure_<id> symbol the expression uses. See Calculation Expression Reference for the full API.
A minimal example — yield as a percentage, guarded:
Measure_2010.Value == 0 ? 0 : Measure_2009.Value / Measure_2010.Value * 100
Interval matching matters#
A calculated measure reads each dependency at its own interval. A daily calculation referencing an hourly measure gets that measure's daily-aligned value, not twenty-four values.
In practice this means: build the aggregation chain first, so each input exists at the interval where you need it, then calculate. A calculation reaching across intervals is usually a sign that a missing aggregation should be added instead.
How recalculation is triggered#
This is the part worth understanding properly, because it explains most surprises.
Infohub tracks dependencies. When a measure's value changes for an interval, the measures that depend on it are queued for recalculation for that interval. The chain propagates: a corrected hourly value re-aggregates to daily, which re-triggers the daily calculation, which re-triggers anything depending on that.
Two consequences:
Backfilling works. Load history into a base measure and everything above it recalculates for those periods.
Order does not need managing. You do not sequence calculations. Declaring dependencies is how you express order, and the engine derives the rest.
A dependency you do not declare is a dependency Infohub cannot see. If an expression reaches a value by a route Infohub does not know about, that route will not trigger recalculation. The measure will compute once and then look stuck, updating only when one of its declared dependencies changes. When a calculated measure holds a stale value while its inputs move, the declared dependency list is the first thing to check.
Dependencies on other periods#
Expressions can read previous periods — Measure_1043[-1].Value — and windows — Measure_1043[-7, 0].Values.Average(). This is how period comparison, growth rates, and rolling averages are built without extra measures.
Be aware of what this implies for recalculation: a rolling seven-day average depends on seven days of input, so correcting one historical day legitimately causes seven days of that average to recompute. That is correct behaviour, and it is also why very long windows over very frequent intervals get expensive.
Returning null versus zero#
double? lets a calculation say "no value here". Choose deliberately:
- Return 0 where zero is operationally true — no production means zero output.
- Return null where the value is genuinely unknown, and you would rather see a gap than a false zero averaged into a monthly figure.
The common mistake is returning 0 from a ratio that will later be averaged. Five shutdown days contributing 0% will drag a monthly efficiency well below reality. See The Zero Guard.
Units and formats are not automatic#
Infohub does not derive the result unit from the expression. If you divide litres by tonnes, the result is l/t only because you said so.
Getting this wrong is quietly damaging: a chart legend claiming % on a ratio that was never multiplied by 100, or a measure labelled kWh that actually holds MWh, will be believed by whoever reads it. Set the unit and the format when you create the measure, not later. See Format Strings and Units of Measure.
Keeping expressions maintainable#
Push repeated logic into functions. Any pattern appearing more than twice — a safe division, a percentage of target, a conversion — belongs in a custom function.
Read parameters from model attributes. A capacity, a factor, or a price hard-coded into an expression cannot be templated and has to be edited per instance. Read it from a model attribute and one expression serves every instance.
Prefer several clear measures to one clever one. An expression combining six inputs and three conditionals is hard to review and harder to debug, because you can only see its output. Two intermediate measures cost almost nothing and make the failure visible at the step that failed.
Keep expressions numeric. Formatting belongs in the measure's format string, not in the expression.
Diagnosing a calculated measure#
| Symptom | Usual cause |
|---|---|
| No values at all | Expression fails every interval — often an unguarded division or a null reference |
| Gaps on weekends, shutdowns, or month boundaries | Unguarded division meeting a zero denominator |
| Value stuck while inputs move | Missing declared dependency |
| Values wrong by a factor of 100 | Percentage not multiplied, or multiplied twice |
| Daily figure disagrees with the sum of hours | Ratio averaged instead of recalculated from daily totals |
| Correct going forward, wrong historically | Expression fixed but the affected range never recalculated |
| Wrong after an unrelated edit in the same session | A stale cached calculation assembly — reopen and recalculate the affected range |
For a systematic approach, see Diagnosing Missing Data.
A worked example: specific energy consumption#
Energy per tonne, daily, from an energy totaliser and a production counter.
Energy Meter— Retrieved, Tag, from the energy registerEnergy Used Hourly— Aggregated, Hourly, Counter (register to quantity)Energy Used Daily— Aggregated, Daily, SumProduction Daily— Aggregated, Daily, Sum, from the hourly production countSpecific Energy Daily— Calculated, Daily, unitkWh/t:
SafeDivide(Measure_<energyDailyId>.Value, Measure_<productionDailyId>.Value)
Five measures, one KPI, and every step inspectable when the number looks wrong.