T04: Your First Calculation
Build a KPI, break it deliberately by omitting the zero guard, then fix it and learn why recalculation matters.
What you will build: a specific water consumption KPI, an unguarded division that fails, and the fix.
Time: about 30 minutes.
You need: T02 and T03 completed.
Step 1 — Add the energy chain#
One more chain, so you have something to divide. Under Production, in the Filler 1 metric:
| Name | Retrieval type | Interval | Aggregation | Source / Tag | Unit |
|---|---|---|---|---|---|
Energy Meter |
Retrieved | Tag | — | 110-JQ-001.PV |
kWh |
Energy Hourly |
Aggregated | Hour(s) | Counter | Energy Meter |
kWh |
Energy Daily |
Aggregated | Day(s) | Sum | Energy Hourly |
kWh |
Another totaliser, so Counter then Sum — the pattern from T02.
Step 2 — Build the KPI#
Create a metric Efficiency under Production, with:
| Field | Value |
|---|---|
| Name | Energy per 1000 Bottles |
| Retrieval type | Calculated |
| Interval | Day(s) |
| Unit | kWh/1000btl |
| Format | 0.00 |
| Dependencies | Energy Daily, Bottles Daily |
Now the expression. Write it without a guard, deliberately:
Measure_<energyDailyId>.Value / Measure_<bottlesDailyId>.Value * 1000
Adding the dependencies is what creates the Measure_<id> symbols — you do not type the dependency list into the expression.
Let it run a day. It works, and produces a plausible number.
Step 3 — Break it#
Now make the denominator zero, the way a real plant does.
The simplest way to see it: wait for a period with no production, or temporarily point Bottle Counter at a tag that does not move. In a real plant it happens by itself — a maintenance day, a public holiday, a shutdown.
When bottles produced for the day is zero, the calculation divides by zero. It does not produce a wrong number — it fails, and the measure has no value for that day.
And the failure travels. Anything aggregating from this measure now has a hole. A monthly figure built from thirty days where two failed is computed from twenty-eight, and it is quietly too low.
There is no red banner. Nothing announces this. The dashboard shows a gap that looks like a data outage, and teams have chased "the historian dropped out" for weeks when the real cause was one unguarded ratio meeting one zero.
Step 4 — Fix it#
Change the expression to:
Measure_<bottlesDailyId>.Value == 0 ? 0
: Measure_<energyDailyId>.Value / Measure_<bottlesDailyId>.Value * 1000
Read it as: "if bottles is zero, the answer is zero; otherwise divide."
That is the zero guard, and every division in every Infohub calculation should have one. It costs one line and it means the calculation cannot fail on that division.
Step 5 — Recalculate#
Here is the part people miss.
Fixing the expression changes how future periods are calculated. The days that already failed still have no value. Infohub does not go back and retry them because the configuration changed.
Recalculate the affected range. Then check that the previously-failed days now have values.
Every fix needs a recalculation. This applies to almost every correction you will ever make — a wrong aggregation type, a missing dependency, a bad expression. Fix the configuration, then recalculate the range that was wrong. A model where half the history uses the old logic and half the new is worse than either, because nobody can tell which figures to trust.
Step 6 — Zero or null?#
You returned 0. Consider whether that is honest.
On a day with no production, is energy per 1000 bottles genuinely zero? Arguably not — it is undefined. And if this measure gets averaged into a monthly figure, those zeros will drag the average down and understate the plant's real consumption rate.
The alternative:
Measure_<bottlesDailyId>.Value == 0 ? (double?)null
: Measure_<energyDailyId>.Value / Measure_<bottlesDailyId>.Value * 1000
Now the day has no value, the gap is visible, and the monthly average is computed only from days that actually ran.
The rule of thumb: return 0 where zero is the operational truth; return null where the value is genuinely unknown and will be averaged. For a ratio that gets averaged upward, null is usually more truthful.
Change it to null and recalculate. Compare the monthly average before and after — the difference is the lesson.
Step 7 — Guard against nulls too#
.Value is a nullable double, and a null divisor is a different problem from a zero one. == 0 does not catch it.
The safest single form covers both:
(Measure_<bottlesDailyId>.Value ?? 0) == 0 ? (double?)null
: Measure_<energyDailyId>.Value / Measure_<bottlesDailyId>.Value * 1000
Use this shape when you are not certain the divisor always exists.
Step 8 — Add a target comparison#
One more calculation, using the manual target from T03. In Efficiency:
| Field | Value |
|---|---|
| Name | Production vs Target Daily |
| Retrieval type | Calculated |
| Interval | Day(s) |
| Unit | % |
| Format | 0.0 '%' |
| Dependencies | Bottles Daily, Production Target Daily |
(Measure_<targetDailyId>.Value ?? 0) == 0 ? (double?)null
: Measure_<bottlesDailyId>.Value / Measure_<targetDailyId>.Value * 100
Note the format: 0.0 '%'. The measure holds 94.2 meaning 94.2%, so the % is a quoted literal. Using 0.0% instead would display 9420.0%, because % in a .NET format string multiplies by 100.
What you have learned#
Calculations are C#. Ternaries, Math, LINQ, null-coalescing — all available. The expression must evaluate to a double?.
Dependencies create the symbols. Declare them; do not type them into the expression.
Every division needs a guard. Zero divisors are routine, not exceptional: shutdowns, unfilled manual entries, sensor faults, new measures with no history.
Failures propagate silently. One unguarded division leaves holes several layers up.
Choose zero or null deliberately. Zero into an average understates. Null keeps the gap visible.
Fix, then recalculate. Configuration changes are not retrospective.
The unit and format are yours to set. Infohub derives neither. % in a format string multiplies by 100.
Try this before moving on#
Write the guard as a reusable function instead. Create a custom function:
public static double? SafeDivide(double? numerator, double? denominator)
{
if (!denominator.HasValue || denominator.Value == 0) return null;
return (numerator ?? 0) / denominator.Value;
}
Then the expression becomes:
SafeDivide(Measure_<energyDailyId>.Value, Measure_<bottlesDailyId>.Value) * 1000
Readable, impossible to forget, and if you later change your mind about null versus zero you change one function rather than two hundred expressions. T11 covers functions properly.
Next: T05: Charts and Dashboards — making all of this visible.