An educational Python and Simulink project for learning controls software engineering through a simplified chilled-water system.
Stage 1 established a lumped water-temperature model and proportional control. Stage 2 added a tested equipment operating sequence with OFF, STARTING, RUNNING, and FAULT states. Stage 3 adds separate chilled-water supply and return temperatures, mass-flow heat transfer, a defined load-step experiment, and quantitative physics validation. Stage 3B recreates that experiment in Simulink and compares every temperature sample with the Python reference. Stage 4A compares proportional and PI control against explicit setpoint-response, control-output, energy-conservation, and numerical-sensitivity criteria. Stage 4B recreates the PI case in Simulink and compares its temperature, command, and integral-state samples with Python. Stage 5A exercises controller saturation and compares PI recovery with and without conditional anti-windup. Stage 5B rebuilds the exercised anti-windup case in Simulink and compares every state and output sample with Python.
This is an independent learning simulation. It is not connected to physical equipment and does not represent any manufacturer's control software.
The Python model uses only the standard library and has been tested with Python 3.12.13. The Simulink cross-validations were developed for MATLAB R2024a.
From the project root:
python main.pyThe program calculates 30 minutes of operation using 10-second simulation steps. It does not wait for 30 minutes of real time. Results are written to results/stage2.csv; the existing Stage 1 result remains separate.
Run all tests with:
python -m unittest discover -s tests -vRun the Stage 3 load-step experiment and regenerate its plot with:
python stage3_main.py
python plot_stage3.pyThe Stage 3 runner writes results/stage3.csv. The plotting script reads that
CSV and writes docs/stage3-load-step-results.svg using only the Python
standard library.
From MATLAB, rebuild and validate the Stage 3B model with:
addpath("matlab")
build_stage3b_model
run_stage3b_validationThe validation script compares all 181 Simulink samples with the Python CSV,
checks the 1e-9 degC acceptance limit, and generates comparison evidence.
Run the Stage 4A controller comparison and regenerate its plot with:
python stage4_main.py
python plot_stage4.pyThe runner writes aligned P and PI samples to results/stage4.csv and compact
metrics to results/stage4_summary.csv. The plotting script creates
docs/stage4-p-vs-pi-results.svg using only the Python standard library.
From MATLAB, rebuild and validate the Stage 4B PI model with:
addpath("matlab")
build_stage4b_model
run_stage4b_validationThe script compares all 181 PI samples for CHWS, CHWR, cooling fraction, and
integral state. Each maximum absolute difference must be no greater than
1e-9 in the signal's native units.
Run the Stage 5A saturation and anti-windup comparison with:
python stage5_main.py
python plot_stage5.pyThe experiment applies a 120 kW overload to a plant with 100 kW maximum cooling,
then returns the load to 30 kW. It writes aligned protected and unprotected PI
samples to results/stage5.csv, summary metrics to
results/stage5_summary.csv, and the result figure to
docs/stage5-anti-windup-results.svg.
From MATLAB, rebuild and validate the Stage 5B anti-windup model with:
addpath("matlab")
build_stage5b_model
run_stage5b_validationThe script compares all 361 protected PI samples for CHWS, CHWR, cooling
fraction, and integral state. Each maximum absolute difference must be no
greater than 1e-9 in the signal's native units.
The scenario is deterministic so every state transition can be reproduced and tested.
| Simulated time | Input event | Resulting state | Cooling permission |
|---|---|---|---|
| 0 s | Initial condition | OFF | Blocked |
| 60 s | Start command | STARTING | Blocked |
| 80 s | Flow proven | RUNNING | Permitted |
| 600 s | Normal stop command | OFF | Blocked |
| 720 s | Second start command | STARTING | Blocked |
| 740 s | Flow proven | RUNNING | Permitted |
| 900 s | Flow lost while running | FAULT | Blocked |
| 950 s | Flow restored | FAULT remains latched | Blocked |
| 960 s | Reset with flow restored | OFF | Blocked |
The water begins at 14 degrees Celsius with a constant 30 kW heat load. It warms while cooling is blocked, cools while the equipment is RUNNING, and warms again after the fault and reset. The default run ends in OFF at approximately 14.862 degrees Celsius.
The upper plot shows the water-temperature response. The middle plot compares
requested cooling with the cooling permitted by the equipment state. The lower
plot records the OFF, STARTING, RUNNING, and FAULT sequence. The plotted data is
preserved in results/stage2.csv; results/stage1.csv retains the Stage 1
baseline.
Stage 3 starts from the analytical steady state for a 30 kW building load,
then increases the load to 50 kW at 10 simulated minutes. The final simulated
CHWS temperature is 8.665 degC versus an analytical value of 8.667 degC. The
final CHWR temperature is 11.055 degC versus an analytical value of 11.059
degC. The maximum energy-conservation residual is approximately
3.5e-12 kJ; changing the time step from 10 s to 5 s changes the final
temperature by approximately 0.0003 degC.
The experiment therefore passes its analytical-temperature, total-energy, and time-step-sensitivity acceptance criteria. Its assumptions and derivation are recorded in Stage 3 Engineering Basis. The Stage 3 Tool Guide explains how to run and inspect the Python program, tests, CSV data, SVG plot, and Git evidence.
The Simulink model uses Unit Delay, Step, Sum, Gain, Saturation, and
To Workspace blocks to reproduce the same discrete energy balances and
proportional controller as Python. The automated validation compares every
CHWS and CHWR sample and fails if either maximum absolute difference exceeds
1e-9 degC.
The model structure, acceptance criterion, evidence outputs, and limitations are recorded in Stage 3B Cross-Validation. The Stage 3B Tool Guide explains how to open, run, inspect, and rebuild the Simulink model.
Both controllers use the same Stage 3 plant, 7 degC CHWS setpoint, and 30 to 50 kW load step. P finishes with a 1.665 degC CHWS error. PI finishes with a -0.004 degC error, enters and remains within the +/-0.1 degC band after 800 s, and limits overshoot to 0.004 degC. Its integrated absolute error is 88.8% lower than P for the defined post-step period.
All cooling commands remain between 0% and 100%, maximum energy residual stays
below 3.6e-12 kJ, and changing the PI time step from 10 s to 5 s changes final
CHWS by about 0.0017 degC. The PI controller includes conditional anti-windup,
which is exercised by dedicated saturation tests.
The equations, initial-condition decision, metrics, criteria, results, and limitations are recorded in Stage 4A Engineering Basis. The Stage 4A Tool Guide explains how to run and inspect the Python evidence.
The generated Simulink model uses standard Sum, Gain, Unit Delay,
Saturation, Step, and To Workspace blocks. It reproduces the Stage 4A PI
controller timing and the Stage 3 two-node plant at a 10-second sample time.
The automated validation aligns 181 samples and checks CHWS, CHWR, applied
cooling fraction, and integral state. The measured maximum differences and
pass/fail result are preserved in results/stage4b_summary.csv.
The defined load step never saturates the controller, so this Stage 4B result covers normal PI operation. The later Stage 5B experiment exercises saturation and cross-validates conditional anti-windup. The evidence boundary is in Stage 4B Cross-Validation; operating steps are in Stage 4B Tool Guide.
The 120 kW overload exceeds the 100 kW cooling limit, so both PI cases reach
100% output. Without anti-windup, the integral state grows to 1827.3 degC s
and keeps the controller fully loaded for 390 s after the load returns to
30 kW. Conditional anti-windup limits the integral state to 629.8 degC s and
exits full cooling after 160 s.
The protected case limits recovery undershoot to 0.612 degC versus 2.996 degC
without protection. Recovery IAE falls from approximately 2127.9 to
879.7 degC s. All commands remain bounded, energy residual stays below
2.7e-12 kJ, and the tested 5-second versus 10-second final-error difference is
approximately 0.0003 degC.
The equations, defined overload, criteria, results, and limitations are in Stage 5A Engineering Basis. The Stage 5A Tool Guide explains how to run and inspect the evidence.
The generated Simulink model applies the same conditional-integration rule as
Python during the 120 kW overload and subsequent recovery. Relational and
logical blocks detect when the unrestricted PI command is saturated and the
current error would drive it farther outside the available range. A switch
then holds or updates the integral state stored by a Unit Delay block.
The automated validation aligns 361 samples and checks CHWS, CHWR, applied
cooling fraction, and integral state. It also verifies the 160 s saturation
release time and approximately 0.612 degC recovery undershoot. Maximum signal
differences and the pass/fail result are preserved in
results/stage5b_summary.csv.
The method, scope, and limitations are in Stage 5B Cross-Validation; operating steps are in Stage 5B Tool Guide.
temperature + setpoint
|
v
proportional_control() ------> requested cooling
|
commands + flow + timer |
| |
v v
update_equipment_state() ---> apply_cooling_permission()
| |
v v
equipment state applied cooling
|
v
update_temperature()
Only RUNNING permits the requested cooling fraction. OFF, STARTING, and FAULT apply zero cooling.
| Current state | Condition | Next state |
|---|---|---|
| OFF | Start command | STARTING |
| OFF | No start command | OFF |
| STARTING | Stop command | OFF |
| STARTING | Flow proven | RUNNING |
| STARTING | Flow not proven and timeout not reached | STARTING |
| STARTING | Startup timed out without flow | FAULT |
| RUNNING | Flow lost | FAULT |
| RUNNING | Flow proven and stop command | OFF |
| RUNNING | Flow proven and no stop command | RUNNING |
| FAULT | Reset command and flow restored | OFF |
| FAULT | Any other condition | FAULT |
Flow loss has priority over a simultaneous normal stop so the abnormal event is recorded as FAULT. A fault remains latched until its required condition is restored and a reset is requested.
| File | Function | Inputs | Output |
|---|---|---|---|
controller.py |
proportional_control() |
Water temperature and setpoint in degrees Celsius; gain in 1/degree Celsius | Requested cooling fraction from 0.0 to 1.0 |
controller.py |
proportional_integral_control() |
Temperature, setpoint, P/I gains, integral state, and scan interval | Requested cooling fraction and next integral state |
controller.py |
update_equipment_state() |
Current state and Boolean start, stop, flow, timeout, and reset signals | Next equipment state |
controller.py |
apply_cooling_permission() |
Requested cooling fraction and equipment state | Applied cooling fraction from 0.0 to 1.0 |
plant.py |
update_temperature() |
Temperature, applied cooling, heat load, capacity, water mass, and time step | Temperature after one step |
plant.py |
calculate_flow_heat_transfer_kw() |
Mass flow in kg/s and CHWS/CHWR temperatures in degrees Celsius | Heat carried from return to supply in kW |
plant.py |
update_supply_return_temperatures() |
CHWS, CHWR, mass flow, load, cooling, node masses, and time step | CHWS and CHWR after one step |
main.py |
scenario_inputs() |
Simulation time in seconds | Reproducible command and flow signals |
main.py |
main() |
Fixed experiment settings | Terminal output and results/stage2.csv |
stage3_main.py |
run_experiment() |
Time step in seconds | Reproducible Stage 3 load-step samples |
stage3_main.py |
calculate_metrics() |
Stage 3 samples | Analytical, conservation, and settling metrics |
stage4_main.py |
run_experiment() |
Controller name and time step | Reproducible P or PI load-step samples |
stage4_main.py |
evaluate_acceptance() |
P, PI, and 5-second PI samples | Named checks and controller metrics |
stage5_main.py |
run_experiment() |
Anti-windup selection and time step | Reproducible overload and recovery samples |
stage5_main.py |
evaluate_acceptance() |
Protected, unprotected, and 5-second protected samples | Saturation and recovery checks plus metrics |
The CSV records time, water temperature, setpoint, state, requested cooling, applied cooling, and the Boolean scenario inputs. This makes the reason for each response traceable.
error_c = temperature_c - setpoint_c
requested_cooling = kp * error_c
requested_cooling_fraction = clamp(requested_cooling, 0.0, 1.0)
For 9 degrees Celsius water, a 7 degree setpoint, and kp = 0.3:
error = 9 - 7 = 2 degrees Celsius
request = 0.3 * 2 = 0.6 = 60%
The request expresses temperature demand. It is not automatically the cooling applied to the water.
RUNNING -> applied cooling = requested cooling
OFF, STARTING, or FAULT -> applied cooling = 0
This separation is the main Stage 2 controls concept. Equipment readiness and protective states can override a valid temperature-control request.
The plant is one well-mixed 1000 kg water mass. Water specific heat is approximated as 4.18 kJ/(kg * degree Celsius).
cooling_kw = applied_cooling_fraction * max_cooling_kw
net_heat_kw = heat_load_kw - cooling_kw
thermal_capacity_kj_per_c = water_mass_kg * 4.18
temperature_change_c = net_heat_kw * dt_s / thermal_capacity_kj_per_c
next_temperature_c = temperature_c + temperature_change_c
With cooling blocked, the 30 kW load adds 300 kJ in one 10-second step:
temperature change = 30 * 10 / (1000 * 4.18)
= 0.07177 degrees Celsius
The test suite contains 54 test methods:
- 10 retained Stage 1 tests for proportional control, the energy balance, validation, and the original closed loop.
- 5 cooling-permission tests for supported states, boundaries, invalid inputs, and a connected thermal example.
- 6 state-transition tests for startup, timeout, stop, running flow loss, fault priority, reset, and validation.
- 3 runner tests that execute the real entry point in temporary directories, verify all 181 timestamps and key states, preserve a Stage 1 baseline file, check permission on every row, and independently recalculate every temperature step.
- 8 Stage 3 tests for flow heat transfer, zero-flow behaviour, balanced node heat rates, total-energy conservation, analytical steady state, load-step convergence, and time-step sensitivity.
- 2 Stage 3B evidence tests that independently read the preserved MATLAB CSV outputs and enforce the Python-Simulink comparison limit.
- 9 Stage 4A tests for PI calculation, input validation, conditional anti-windup, balanced initial conditions, setpoint response, integrated error, command limits, retained energy conservation, and time-step sensitivity.
- 2 Stage 4B evidence tests that read the preserved MATLAB CSV outputs and enforce sample count plus CHWS, CHWR, cooling-fraction, and integral-state difference limits.
- 7 Stage 5A tests for the anti-windup option, load-profile boundaries, balanced initial conditions, exercised saturation, recovery improvements, command and energy limits, and time-step sensitivity.
- 2 Stage 5B evidence tests that read the preserved MATLAB CSV outputs and enforce sample count, cross-tool signal limits, saturation-release time, and recovery undershoot.
Passing these scenarios verifies the stated educational model. It does not establish real-equipment performance, safety certification, or commissioning results.
- Supply and return temperatures are not modelled separately.
- Heat load, water mass, and maximum cooling capacity are fixed.
- Commands and flow changes follow a scripted educational scenario.
- Pumps, valves, refrigerant behaviour, compressor dynamics, electrical power, anti-recycle timers, and sensor noise are not modelled.
- STARTING represents flow-proof waiting only.
- FAULT represents simplified loss of chilled-water flow; manufacturer protections are not reproduced.
- The reset rule is an educational choice and not a site-specific operating sequence.
- Each side is represented by one well-mixed water mass.
- Mass flow and available cooling capacity remain constant.
- Pipe heat loss, pump heat, transport delay, sensor dynamics, valve dynamics, and refrigerant behaviour are not modelled.
- The Stage 3 experiment holds the equipment in RUNNING to isolate the thermal response; the tested Stage 2 operating sequence remains a separate scenario.
- Parameters are declared educational assumptions rather than selected design values for a real building.
The current repository is classified as an early simplified engineering model. It uses a real energy balance and tested control sequence, but it is not yet a design, equipment-selection, energy-prediction, or commissioning model.
The detailed Engineering Gap Audit records the current capability scores, Engineers Australia evidence mapping, major gaps, priorities, deferred features, and the bounded next sprint.
The Engineering Evidence Register links each completed stage from requirement and theory through implementation, test, result, interpretation, and limitations.
Stage 3 establishes the physics and validation foundation. Stage 4A adds a quantitative P-versus-PI comparison without changing the plant equations. Stage 4B reproduces the PI response in Simulink before translation to PLC Structured Text. Extra faults, multi-chiller staging, and UI work remain later stages.
A concise description of the completed stage is:
I developed and tested a two-node chilled-water model in Python, recreated the same equations as a Simulink block model, and compared every CHWS and CHWR sample under an identical load disturbance. I then compared P and PI control using final error, overshoot, settling time, integrated absolute error, command limits, energy conservation, and time-step sensitivity, and checked the PI temperature, output, and integral-state traces in Simulink.
The code, data, tests, and documentation should be presented as learning evidence. Any public post should distinguish this simulation from hardware commissioning or a manufacturer's actual control logic.



