Skip to content

docs: Additional examples and use-cases#247

Open
toby-coleman wants to merge 17 commits intomainfrom
docs/more-examples
Open

docs: Additional examples and use-cases#247
toby-coleman wants to merge 17 commits intomainfrom
docs/more-examples

Conversation

@toby-coleman
Copy link
Copy Markdown
Contributor

Summary

Add more examples and use cases for typical process control/industrial simulation problems.

Changes

  • Quadruple tank process.
  • Wastewater BSM1 process.
  • Event-driven transport models.

These examples were primarily researched and implemented by Copilot.

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 8, 2026

Benchmark comparison for 809ad8d2 (base) vs d7fc4732 (PR)


------------------------------------------------------------------------------------------------------------------ benchmark: 2 tests ------------------------------------------------------------------------------------------------------------------
Name (time in ms)                                                                         Min                 Max                Mean            StdDev              Median                IQR            Outliers     OPS            Rounds  Iterations
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
test_benchmark_process_run (main/.benchmarks/Linux-CPython-3.12-64bit/0001_base)     447.9087 (1.0)      452.1571 (1.0)      450.3487 (1.0)      1.9692 (1.0)      451.3735 (1.0)       3.4715 (1.0)           1;0  2.2205 (1.0)           5           1
test_benchmark_process_run (pr/.benchmarks/Linux-CPython-3.12-64bit/0001_pr)         449.8085 (1.00)     462.6486 (1.02)     455.4079 (1.01)     6.0737 (3.08)     453.1143 (1.00)     11.3090 (3.26)          1;0  2.1958 (0.99)          5           1
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Legend:
  Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.
  OPS: Operations Per Second, computed as 1 / Mean

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces several new demonstration notebooks for the Plugboard framework, covering wastewater treatment (BSM1-inspired model) and transport simulations (traffic intersection and bus bunching). The review identified several critical issues where components were missing required initial values for inputs, which would cause the simulations to crash on the first tick. Additionally, feedback was provided on improving the anti-windup logic in the PI controller and addressing a memoryless traffic signal delay implementation in the bus simulation.

"\n",
" comps = [\n",
" SetpointGenerator(name=\"setpoints\"),\n",
" PIController(name=\"controller\", kp=kp, ki=ki, initial_values={\"h1\": [h1_init], \"h2\": [h2_init]}),\n",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The PIController component requires initial values for its sp_h1 and sp_h2 inputs to function correctly in the first tick of the simulation. Without these, self.sp_h1 will be None during the first step() call, leading to a calculation error.

Suggested change
" PIController(name=\"controller\", kp=kp, ki=ki, initial_values={\"h1\": [h1_init], \"h2\": [h2_init]}),\n",
PIController(name="controller", kp=kp, ki=ki, initial_values={"h1": [h1_init], "h2": [h2_init], "sp_h1": [h1_init], "sp_h2": [h2_init]}),

" upper_outlet_area=a3,\n",
" pump_gain=k1,\n",
" gamma=gamma1,\n",
" initial_values={\"prev_h\": [h1_init]},\n",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The LowerTank component requires initial values for upper_h and pump_voltage in addition to prev_h. In Plugboard's LocalProcess, inputs for tick 0 are taken from initial_values. If omitted, they default to None, which will cause a crash in the step() method when calculating the square root or pump inflow.

            initial_values={"prev_h": [h1_init], "upper_h": [h3_init], "pump_voltage": [V1_0]},

" outlet_area=a3,\n",
" pump_gain=k2,\n",
" gamma_complement=1.0 - gamma2,\n",
" initial_values={\"prev_h\": [h3_init]},\n",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The UpperTank component requires an initial value for pump_voltage to avoid a None type error during the first simulation tick.

            initial_values={"prev_h": [h3_init], "pump_voltage": [V2_0]},

Comment on lines +784 to +787
" initial_values={\n",
" \"internal_recycle\": [initial_internal],\n",
" \"return_sludge\": [initial_return],\n",
" },\n",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The AnoxicReactor component is missing an initial value for the influent input. Since step() immediately uses self.influent to calculate the mixed liquor state, the simulation will crash on the first tick if this is not provided.

        initial_values={
            "internal_recycle": [initial_internal],
            "return_sludge": [initial_return],
            "influent": [_base_influent(params)],
        },

Comment on lines +763 to +764
" TrafficLight(name=\"light-a\", junction=\"A\", green_duration=GREEN, yellow_duration=YELLOW, red_duration=RED),\n",
" TrafficLight(name=\"light-b\", junction=\"B\", green_duration=GREEN, yellow_duration=YELLOW, red_duration=RED, offset=light_b_offset),\n",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The TrafficLight components require an initial value for the time input. In the first tick, self.time will be None, and the call to int(self.time) in the step() method will raise a TypeError.

Suggested change
" TrafficLight(name=\"light-a\", junction=\"A\", green_duration=GREEN, yellow_duration=YELLOW, red_duration=RED),\n",
" TrafficLight(name=\"light-b\", junction=\"B\", green_duration=GREEN, yellow_duration=YELLOW, red_duration=RED, offset=light_b_offset),\n",
TrafficLight(name="light-a", junction="A", green_duration=GREEN, yellow_duration=YELLOW, red_duration=RED, initial_values={"time": [0]}),
TrafficLight(name="light-b", junction="B", green_duration=GREEN, yellow_duration=YELLOW, red_duration=RED, offset=light_b_offset, initial_values={"time": [0]}),

Comment on lines +765 to +766
" Junction(name=\"junction-a\", junction_name=\"A\", congestion_threshold=15),\n",
" Junction(name=\"junction-b\", junction_name=\"B\", congestion_threshold=15),\n",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The Junction components require initial values for time and signal. While signal might default to a safe check, float(self.time) in step() will crash if time is None.

Suggested change
" Junction(name=\"junction-a\", junction_name=\"A\", congestion_threshold=15),\n",
" Junction(name=\"junction-b\", junction_name=\"B\", congestion_threshold=15),\n",
Junction(name="junction-a", junction_name="A", congestion_threshold=15, initial_values={"time": [0], "signal": ["red"]}),
Junction(name="junction-b", junction_name="B", congestion_threshold=15, initial_values={"time": [0], "signal": ["red"]}),

Comment on lines +654 to +661
" Bus(\n",
" name=bus_ids[i], bus_id=bus_ids[i],\n",
" departure_tick=i * DEPARTURE_HEADWAY_TICKS,\n",
" signal_positions=signal_positions,\n",
" perturbation_stop=PERTURBATION_STOP if (apply_perturbation and bus_ids[i] == PERTURBATION_BUS) else None,\n",
" perturbation_ticks=PERTURBATION_TICKS if (apply_perturbation and bus_ids[i] == PERTURBATION_BUS) else 0,\n",
" rng_seed=rng_seed + i * 100,\n",
" )\n",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The Bus component requires an initial value for the tick input. The step() method performs int(self.tick), which will fail with a TypeError on the first tick if initial_values are not provided.

        Bus(
            name=bus_ids[i], bus_id=bus_ids[i],
            departure_tick=i * DEPARTURE_HEADWAY_TICKS,
            signal_positions=signal_positions,
            perturbation_stop=PERTURBATION_STOP if (apply_perturbation and bus_ids[i] == PERTURBATION_BUS) else None,
            perturbation_ticks=PERTURBATION_TICKS if (apply_perturbation and bus_ids[i] == PERTURBATION_BUS) else 0,
            rng_seed=rng_seed + i * 100,
            initial_values={"tick": [0]},
        )

Comment on lines +298 to +300
" # Anti-windup: only integrate when the output is not saturated\n",
" if u_raw == u_clamped:\n",
" self._integral[i] += e * self._dt\n",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The anti-windup logic here is a bit too restrictive. It only allows integration when the output is exactly equal to the clamped value. A more robust approach is to allow integration if the output is not saturated, OR if the output is saturated but the error has a sign that would move the output away from the limit. This prevents the integral term from getting 'stuck' at the limit when the error changes sign.

            # Anti-windup: only integrate if not saturated, or if integration moves output away from limit
            if (self._v_min < u_raw < self._v_max) or (u_raw <= self._v_min and e > 0) or (u_raw >= self._v_max and e < 0):
                self._integral[i] += e * self._dt

Comment on lines +442 to +444
" if phase_in_cycle > SIGNAL_GREEN_FRAC * 90.0:\n",
" red_wait = 90.0 - phase_in_cycle\n",
" distance = max(0.0, distance - BUS_SPEED * red_wait)\n",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The traffic signal delay logic implemented here is memoryless. If a bus hits a 'red' light (i.e., distance is reduced to 0), it stays at its current position. However, in the next tick, it re-rolls the probability of passing. This means the wait time follows a geometric distribution based on SIGNAL_GREEN_FRAC rather than a deterministic signal cycle. This contradicts the 'realistic variance' claim in the docstring and can lead to buses being stuck for unrealistic durations or passing immediately despite a 'red' state.

@codecov
Copy link
Copy Markdown

codecov bot commented Apr 8, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 9, 2026

Benchmark comparison for 809ad8d2 (base) vs 1ba92541 (PR)


------------------------------------------------------------------------------------------------------------------ benchmark: 2 tests -----------------------------------------------------------------------------------------------------------------
Name (time in ms)                                                                         Min                 Max                Mean            StdDev              Median               IQR            Outliers     OPS            Rounds  Iterations
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
test_benchmark_process_run (pr/.benchmarks/Linux-CPython-3.12-64bit/0001_pr)         459.3402 (1.0)      474.7201 (1.0)      464.1131 (1.0)      6.3328 (1.0)      460.7296 (1.0)      7.3110 (1.11)          1;0  2.1546 (1.0)           5           1
test_benchmark_process_run (main/.benchmarks/Linux-CPython-3.12-64bit/0001_base)     461.3899 (1.00)     478.2677 (1.01)     466.4273 (1.00)     6.8485 (1.08)     465.1872 (1.01)     6.5977 (1.0)           1;0  2.1440 (1.00)          5           1
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Legend:
  Outliers: 1 Standard Deviation from Mean; 1.5 IQR (InterQuartile Range) from 1st Quartile and 3rd Quartile.
  OPS: Operations Per Second, computed as 1 / Mean

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant