Asynchronous Bioreactor Optimization

Understand Asyncio Python Fundamentals for Continuous Bioreactor Optimization
Author

Sjoerd de Haan

Published

May 21, 2025

🚀 Async Python Fundamentals for Continuous Bioreactor Optimization

When optimizing continuous bioreactors you deal with streams of sensor data (pH, oxygen levels, glucose), predictive models and dynamical control of pumps or nutrient feeds.

Somehow you need to juggle these tasks. This is where Python’s asyncio comes in handy: it lets your application do many things at once without freezing or slowing down.

bioreactor

🎯 Why Does This Matter in Bioreactor Control?

  • Your app streams sensor data continuously — waiting on these shouldn’t freeze your dashboard or ML prediction pipeline.
  • Predictive models run in the background, adjusting feed rates or oxygenation without delays.
  • Your web dashboard updates live with no lag, keeping scientists informed and in control.
  • Async programming ensures your app can handle multiple tasks concurrently, without spinning up threads or processes.

How exactly does this work? Let’s draft a high-level example of a bioreactor control system.


🔍 Async Continuous Bioreactor Optimization

import asyncio

async def read_sensors(queue):
    while True:
        await queue.put(sensor_data)    # Non-blocking

def predict_model(data):
    # This is a blocking, CPU-bound operation (e.g., NumPy or PyTorch)
    result = heavy_ml_model.predict(data)
    return result

async def compute_control(data):
      result = control_model(data) # blocking
    return result

async def run_prediction(queue, control_queue):
    """Consume sensor data, run prediction, and send control updates."""
    while True:
        sensor_data = await queue.get()  # Wait for sensor data, non-blocking
        prediction  = await asyncio.to_thread(predict_model, data) # Run prediction in a separate thread, non-blocking
        await predict_queue.put(prediction)  # Pass prediciton to control system
        queue.task_done()                     # Mark sensor data processed

async def update_control(control_queue):
    """Consume predictions and adjust bioreactor control parameters."""
    while True:
        prediction = await control_queue.get()  # Wait for prediction, Non-blocking
        control_action = await asyncio.to_thread(compute_control, prediction)  # Run control update in a separate thread 
        log(f"Control updated: {control_action}")
        control_queue.task_done()

async def main():
    sensor_queue = asyncio.Queue()
    control_queue = asyncio.Queue()

    # Start concurrent tasks
    sensor_task = asyncio.create_task(read_sensors(sensor_queue))
    prediction_task = asyncio.create_task(run_prediction(sensor_queue, control_queue))
    control_task = asyncio.create_task(update_control(control_queue))

    log("Bioreactor control system running...")

    # Run forever (or until externally stopped)
    await asyncio.gather(sensor_task, prediction_task, control_task)

if __name__ == "__main__":
    asyncio.run(main())

🧠 What’s Going On? Why Blocking Matters

Async programming is all about yielding control to allow other tasks to run.

In the example above, we have three main tasks: 1. Reading sensors: This is a non-blocking operation. It continuously reads sensor data and puts it into a queue. 2. Running predictions: This is a blocking operation. It runs a heavy ML model to predict control parameters based on sensor data. 3. Updating control parameters: This is also a blocking operation. It computes control actions based on predictions.

The key is to avoid blocking the event loop with heavy computations. Instead, we use asyncio.to_thread() to run blocking functions in a separate thread, allowing the event loop to continue processing other tasks.

This table summarizes the blocking and non-blocking nature of various components in the code:

Step Blocking or Non-blocking? Why?
while True Non-blocking This creates an infinite loop that runs concurrently with other tasks without blocking.
async def Non-blocking This defines an asynchronous function that can be run concurrently with other tasks.
await Non-blocking This yields control back to the event loop, allowing other tasks to run concurrently.
asyncio Non-blocking This is the main library for asynchronous programming in Python, allowing for concurrent execution.
read_sensors Non-blocking (await queue.put(...)) Waiting for sensor data should not block other tasks.
predict_model Blocking (CPU-bound) This is a heavy computation that will block the event loop if not run in a separate thread.
compute_control Blocking (CPU-bound) Similar to predict_model, this is a heavy computation that should not block the event loop.
run_prediction Non-blocking (await queue.get(...)) Waiting for sensor data should not block other tasks.
update_control Non-blocking (await control_queue.get(...)) Waiting for predictions should not block other tasks.
asyncio.to_thread(...) Non-blocking This runs a blocking function in a separate thread, allowing the event loop to continue.
asyncio.gather(...) Non-blocking This allows multiple tasks to run concurrently without blocking each other.
asyncio.run(...) Non-blocking This starts the event loop and runs the main function, allowing for concurrent execution.
asyncio.Queue() Non-blocking This is a thread-safe queue that allows for non-blocking communication between tasks.
task_done() Non-blocking This marks a task as done without blocking the event loop.
create_task(...) Non-blocking This creates a new task that runs concurrently with other tasks without blocking.
log(...) Non-blocking Logging is typically non-blocking, but can be made blocking if necessary.

⚙️ Summary: Async Is About Scheduling, Not Just Speed

asyncio lets your bioreactor system juggle multiple I/O-bound tasks smoothly:

  • Reading sensors
  • Sending data to models
  • Updating control parameters
  • Serving dashboards

…all without blocking each other.

By carefully choosing where to block (quick operations) and where to yield control (await), your biotech software becomes more efficient and responsive.