Build a Digital Twin for a Potted Plant
Disclaimer: This tutorial has been generated using chitragupta. Despite some potential for hallucination, the ideas communicated in this tutorial are accurate. Please send your corrections and suggestions to prasad.talasila@gmail.com
What you'll build. A ~70-line Python script, twin.py, that simulates
a potted plant losing moisture over 24 hours and a digital twin that reads
its moisture sensor, predicts when the soil will dry out, and waters the
plant automatically -- no human in the loop. Running it prints a line per
hour and, partway through, one marked WATERED:
1 2 3 | |
What you'll learn. How to structure a digital twin as two independent pieces -- a physical-side simulation and a twin that only ever sees sensor readings -- and why closing the loop (the twin issuing a command back to the physical side) is what separates a digital twin from a dashboard.
What you need. Python 3.10 or later, and a terminal. No packages beyond the standard library.
Time. About 20 minutes.
Step 1: Create the project folder
1 2 | |
Step 2: Simulate the pot
You don't have a real plant wired up, so start by writing a stand-in for
one. Create twin.py with this content:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
tick() is one hour passing: the soil loses a random amount of moisture,
the way real soil does under evaporation. read_sensor() is what a real
capacitive soil-moisture probe gives you -- the true value, plus a little
sensor noise. random.seed(7) pins that randomness so your output matches
this tutorial's exactly.
Check that it works before moving on:
1 2 3 4 5 6 7 | |
You should see:
1 2 3 | |
Three ticks, three shrinking readings. That's the physical side done -- notice it knows nothing about digital twins. It's just a pot.
Step 3: Write the digital twin
The twin is a second, separate class. Critically, it never touches Pot
directly -- it only ever sees numbers arriving from read_sensor(), the
same way a real twin only sees whatever its sensor's network connection
delivers. Append this to twin.py:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
ingest() is the twin's half of the data connection: each new reading
gets appended, and only the last three are kept, since only the recent
trend matters for what comes next. predict_next() is the twin's model --
deliberately the simplest one that works: extend the line between the last
two readings one more hour forward. decide() is the twin's algorithm: if
that predicted value would be too dry, it's time to water. This is exactly
the three-part shape every digital twin shares -- data in, a model to
interpret it, an algorithm to act on it -- just small enough here to read
in one sitting.
Step 4: Close the loop
A twin that only predicts is a dashboard. What makes it a twin is that
its decision changes the physical side. Append the loop that wires
Pot and PlantTwin together:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | |
Each hour, the pot's real moisture drops, the sensor reports a noisy
version of it, the twin folds that reading into its prediction, and if the
twin's prediction crosses the dry threshold, it calls pot.water() --
reaching back across the loop to change the physical side, before a human
ever looks at the readings.
Step 5: Run it
1 | |
You should see 24 lines of output. Line 12 is marked WATERED, and the
next line's sensor value jumps back up above 59, because the twin's
command already changed the pot before that reading was taken:
1 2 3 | |
If your output matches, the loop is genuinely closed: the twin's decision at hour 12 is the reason hour 13's real-world reading looks completely different.
Step 6: Change the twin's behavior without touching the pot
Open twin.py and change:
1 | |
to:
1 | |
Save the file and run python3 twin.py again. Watering now happens at
hour 8 instead of hour 12, and a second time at hour 21:
1 2 3 | |
Notice what you didn't have to change: Pot is untouched. This is the
payoff of keeping the twin as a separate piece that only talks to the
physical side through readings and commands -- you can retune, replace, or
completely rewrite the twin's model without ever touching the thing it's
twinning.
What you built
You now have a working digital twin: a physical-side simulation that knows
nothing about twins, a twin that knows nothing about the physical side's
internals, and a closed loop connecting them where the twin's own
prediction changes what the physical side does next. Swap Pot for code
that reads a real capacitive soil sensor over I2C and calls a relay to run
a water pump, and the PlantTwin class doesn't need to change at all --
that's the whole reason to build the two halves this way.
Where to go next
PlantTwin's "extend the last two points" model is deliberately the
crudest thing that could work. A real deployment would replace
predict_next with a Kalman filter, which blends the model's prediction
against each new noisy reading instead of trusting the latest one alone
[1], and would read real sensor data over MQTT rather
than calling a Python method directly. A how-to guide would be the right
place to cover wiring up an actual moisture probe and pump, since that
involves choices -- which board, which protocol -- that a first tutorial
deliberately avoids.
Further reading
[1] H. Feng, C. Gomes, and P. G. Larsen, Model-Based Monitoring and State Estimation for Digital Twins: The Kalman Filter, arXiv, 2023. feng_model-based_2023