Controller¶
Purpose of the Controller¶
Shown above is a typical respiratory waveform (without averaging) as produced with PVP1. Blue is the recorded pressure, orange is the flow out of the system. Note that airflow (and also oxygen concentration) are only measured during expiration, so that the main control-loop during inspiration runs as fast as possible, and is not slowed down by communication delays. Pressure is recorded continuously. Empirically, the Raspberry pi allowed for the primary control loop to run at speeds of ~5ms per loop, which was considerably faster than all hardware delays (i.e. the time it takes for a mechanical, physical valve to open or close; see main manuscript).
The purpose of the controller is to produce a breath waveform, as the one shown above. More specifically, it’s job is to reach a certain target-pressure (PIP), and to hold that pressure for a certain amount of time. These numbers are provided by the user via thee UI. Exhalation is passive, and PEEP pressure is mechanically controlled with a spring-valve.
Conceptually, the controller is written as a hybrid system of state and PID control. During inspiration, it actively controls pressure with a simple PID controller. That means that during inspiration, it measures the deviation of the pressure-is-vale from the pressure-target-value, and depending an that distance (and its integral and derivative), it adjusts the opening of the inspiratory valve. Expiration was then instantiated by closing the inspiratory, and opening the expiratory valve to passively release PIP pressure as fast as possible. After reaching PEEP, the controller opens the inspiratory valve slightly to sustain a small flow during PEEP, using the aforementioned manually operated PEEP-valve. We found, empirically, that a combination of proportional and integral term worked best across different physical lung settings.
The controller was also built to allow the user to adjust flow through the system. This is done by a linear correction of the proportional-term. With this adjustment, the user can manipulate the rise-time of the pressure waveform.
In addition to this core function, the controller module continuously monitors for autonomous breaths, high airway pressure, and general system status. Autonomous breathing was detected by transient pressure drops below PEEP. A detected breath triggered a new breath cycle. High airway pressure is defined as exceeding a certain pressure for a certain time (as to not be triggered by a cough). This event triggered an alarm, and an immediate release of air to drop to a safe pressure and not to exceed PIP. Both of these functionalities are fast, and respond, at the latest, within few hundred milliseconds. The controller also assesses whether numerical values and sensor readings are reasonable, and changing over time. If this is not the case, it raises an technical alarm. All alarms are collected and maintained by an intelligent alarm manager, that provides the UI with the alarms to display in order of their importance.
The final functionality of the control module is the estimation of VTE (VTE stands for exhaled tidal volume), which is thee volume of air that made it in- and out of the lung. We estimate this number by integrating the expiratory flow during expiration, and subtracting the baseline flow used to sustain PEEP (details in the accompanying manuscript):
Architecture of the Controller¶
In terms of software components, the Controller consists of one main controller
class, that is instantiated in its own thread. This object receives sensor-data from HAL, and computes control parameters, to change the mechanical position of valves. The Controller also receives ventilation control parameters (see set_control()
). All exchanged variables are mutex’d.
The Controller also feeds the Logger
a continuous stream of SensorValues
objects so as to store high-temporal resolution data, including the control signals.
The main control loop is pvp.controller._start_mainloop()
which queries the Hardware for new variables, and performs a PID update using .pvp.controller._PID_update().
The Controller is configured by the values
module,
The Controller can be launched alone, but was not intended to be launched alone. The alarm functionality requires the UI.
Classes:
|
Abstract controller class for simulation/hardware. |
|
Uses ControlModuleBase to control the hardware. |
|
Physics simulator for inflating a balloon with an attached PEEP valve. |
|
Controlling Simulation. |
Functions:
|
Generates control module. |
- class pvp.controller.control_module.ControlModuleBase(save_logs: bool = False, flush_every: int = 10)[source]¶
Bases:
object
Abstract controller class for simulation/hardware.
1. General notes: All internal variables fall in three classes, denoted by the beginning of the variable:
COPY_varname: These are copies (for safe threading purposes) that are regularly sync’ed with internal variables.
__varname: These are variables only used in the ControlModuleBase-Class
_varname: These are variables used in derived classes.
2. Set and get values. Internal variables should only to be accessed though the set_ and get_ functions. These functions act on COPIES of internal variables (__ and _), that are sync’d every few iterations. How often this is done is adjusted by the variable self._NUMBER_CONTROLL_LOOPS_UNTIL_UPDATE. To avoid multiple threads manipulating the same variables at the same time, every manipulation of COPY_ is surrounded by a thread lock.
Public Methods:
get_sensors(): Returns a copy of the current sensor values.
get_alarms(): Returns a List of all alarms, active and logged
get_control(ControlSetting): Sets a controll-setting. Is updated at latest within self._NUMBER_CONTROLL_LOOPS_UNTIL_UPDATE
get_past_waveforms(): Returns a List of waveforms of pressure and volume during at the last N breath cycles, N<self. _RINGBUFFER_SIZE, AND clears this archive.
start(): Starts the main-loop of the controller
stop(): Stops the main-loop of the controller
set_control(): Set the control
is_running(): Returns a bool whether the main-thread is running
get_heartbeat(): Returns a heartbeat, more specifically, the continuously increasing iteration-number of the main control loop.
Initializes the ControlModuleBase class.
- Parameters
save_logs (bool, optional) – Should sensor data and controls should be saved with the
DataLogger
? Defaults to False.flush_every (int, optional) – Flush and rotate logs every n breath cycles. Defaults to 10.
- Raises
alert – [description]
Methods:
__init__
([save_logs, flush_every])Initializes the ControlModuleBase class.
Makes a copy of internal variables.
A method callable from the outside to get a copy of sensorValues
A method callable from the outside to get a copy of the alarms, that the controller checks: High airway pressure, and technical alarms.
set_control
(control_setting)A method callable from the outside to set alarms.
get_control
(control_setting_name)A method callable from the outside to get current control settings.
set_breath_detection
(breath_detection)Return current state of autonomous breath detection
Produces the INSPIRATORY control-signal that has been calculated in __calculate_control_signal_in(dt)
Produces the EXPIRATORY control-signal for the different states, i.e. open/close.
Resets the internal controller cycle to zero, i.e. restarts the breath cycle.
_PID_update
(dt)This instantiates the PID control algorithms. During the breathing cycle, it goes through the four states: 1) Rise to PIP, speed is controlled by flow (variable: __SET_PIP_GAIN) 2) Sustain PIP pressure 3) Quick fall to PEEP 4) Sustaint PEEP pressure Once the cycle is complete, it checks the cycle for any alarms, and starts a new one. A record of pressure/volume waveforms is kept and saved.
Public method to return a list of past waveforms from __cycle_waveform_archive. Note: After calling this function, archive is emptied! The format is - Returns a list of [Nx3] waveforms, of [time, pressure, volume] - Most recent entry is waveform_list[-1].
Prototype method to start main PID loop.
start
()Method to start _start_mainloop as a thread.
stop
()Method to stop the main loop thread, and close the logfile.
Public Method to assess whether the main loop thread is running.
Returns an independent heart-beat of the controller, i.e. the internal loop counter incremented in _start_mainloop.
- __init__(save_logs: bool = False, flush_every: int = 10)[source]¶
Initializes the ControlModuleBase class.
- Parameters
save_logs (bool, optional) – Should sensor data and controls should be saved with the
DataLogger
? Defaults to False.flush_every (int, optional) – Flush and rotate logs every n breath cycles. Defaults to 10.
- Raises
alert – [description]
- _initialize_set_to_COPY()[source]¶
Makes a copy of internal variables. This is used to facilitate threading
- __comptest(phase, ls, selector)¶
Helper function to identify the index the first occurence of a number in list exceeding threshold, and returns phase[idx]
- Parameters
phase (array) – a list of numbers
list (array) – array of bools with same length as phase
selector (string) – ‘first’ or ‘last’ whichever is wanted
- Returns
phase[idx] where idx is first, or last point where numbers in list exceed threshold
- Return type
- __analyze_last_waveform()¶
- This goes through the last waveform, and updates the internal variables:
VTE, PEEP, PIP, PIP_TIME, I_PHASE, FIRST_PEEP and BPM.
- get_sensors() pvp.common.message.SensorValues [source]¶
A method callable from the outside to get a copy of sensorValues
- Returns
A set of current sensorvalues, handeled by the controller.
- Return type
- get_alarms() Union[None, Tuple[pvp.alarm.alarm.Alarm]] [source]¶
A method callable from the outside to get a copy of the alarms, that the controller checks: High airway pressure, and technical alarms.
- set_control(control_setting: pvp.common.message.ControlSetting)[source]¶
A method callable from the outside to set alarms. This updates the entries of COPY with new control values.
- Parameters
control_setting (ControlSetting) – [description]
- get_control(control_setting_name: pvp.common.values.ValueName) pvp.common.message.ControlSetting [source]¶
A method callable from the outside to get current control settings. This returns values of COPY to the outside world.
- Parameters
control_setting_name (ValueName) – The specific control asked for
- Returns
ControlSettings-Object that contains relevant data
- Return type
- get_breath_detection() bool [source]¶
Return current state of autonomous breath detection
- Returns
bool
- __get_PID_error(ytarget, yis, dt, RC)¶
Calculates the three terms for PID control. Also takes a timestep “dt” on which the integral-term is smoothed.
- __calculate_control_signal_in(dt)¶
- Calculates the PID control signal by:
Combining the the three gain parameters.
And smoothing the control signal with a moving window of three frames (~10ms)
- Parameters
dt (float) – timestep
- _get_control_signal_in()[source]¶
Produces the INSPIRATORY control-signal that has been calculated in __calculate_control_signal_in(dt)
- Returns
the numerical control signal for the inspiratory prop valve
- Return type
- _get_control_signal_out()[source]¶
Produces the EXPIRATORY control-signal for the different states, i.e. open/close
- Returns
numerical control signal for expiratory side: open (1) close (0)
- Return type
- _control_reset()[source]¶
Resets the internal controller cycle to zero, i.e. restarts the breath cycle. Used for autonomous breath detection.
- __test_for_alarms()¶
- Implements tests that are to be executed in the main control loop:
Test for HAPA
Test for Technical Alert, making sure sensor values are plausible
Test for Technical Alert, make sure continuous in contact
Currently: Alarms are time.time() of first occurance.
- __start_new_breathcycle()¶
- Some housekeeping. This has to be executed when the next breath cycles starts:
starts new breathcycle
initializes newe __cycle_waveform
analyzes last breath waveform for PIP, PEEP etc. with __analyze_last_waveform()
flushes the logfile
- _PID_update(dt)[source]¶
This instantiates the PID control algorithms. During the breathing cycle, it goes through the four states:
Rise to PIP, speed is controlled by flow (variable: __SET_PIP_GAIN)
Sustain PIP pressure
Quick fall to PEEP
Sustaint PEEP pressure
Once the cycle is complete, it checks the cycle for any alarms, and starts a new one. A record of pressure/volume waveforms is kept and saved
- Parameters
dt (float) – timesstep since last update
- __save_values()¶
Helper function to reorganize key parameters in the main PID control loop, into a SensorValues object, that can be stored in the logfile, using a method from the DataLogger.
- get_past_waveforms()[source]¶
Public method to return a list of past waveforms from __cycle_waveform_archive. Note: After calling this function, archive is emptied! The format is
Returns a list of [Nx3] waveforms, of [time, pressure, volume]
Most recent entry is waveform_list[-1]
- Returns
[Nx3] waveforms, of [time, pressure, volume]
- Return type
- _start_mainloop()[source]¶
Prototype method to start main PID loop. Will depend on simulation or device, specified below.
- class pvp.controller.control_module.ControlModuleDevice(save_logs=True, flush_every=10, config_file=None)[source]¶
Bases:
pvp.controller.control_module.ControlModuleBase
Uses ControlModuleBase to control the hardware.
Initializes the ControlModule for the physical system. Inherits methods from ControlModuleBase
- Parameters
save_logs (bool, optional) – Should logs be kept? Defaults to True.
flush_every (int, optional) – How often are log-files to be flushed, in units of main-loop-itertions? Defaults to 10.
config_file (str, optional) – Path to device config file, e.g. ‘pvp/io/config/dinky-devices.ini’. Defaults to None.
Methods:
__init__
([save_logs, flush_every, config_file])Initializes the ControlModule for the physical system.
Copies the current measurements to`COPY_sensor_values`, so that it can be queried from the outside.
_set_HAL
(valve_open_in, valve_open_out)Set Controls with HAL, decorated with a timeout.
_get_HAL
()Get sensor values from HAL, decorated with timeout.
This returns valves back to normal setting (in: closed, out: open)
This is the main loop.
- __init__(save_logs=True, flush_every=10, config_file=None)[source]¶
Initializes the ControlModule for the physical system. Inherits methods from ControlModuleBase
- Parameters
save_logs (bool, optional) – Should logs be kept? Defaults to True.
flush_every (int, optional) – How often are log-files to be flushed, in units of main-loop-itertions? Defaults to 10.
config_file (str, optional) – Path to device config file, e.g. ‘pvp/io/config/dinky-devices.ini’. Defaults to None.
- __get_hal(**kwargs)¶
- _sensor_to_COPY()[source]¶
Copies the current measurements to`COPY_sensor_values`, so that it can be queried from the outside.
- _set_HAL(valve_open_in, valve_open_out)[source]¶
Set Controls with HAL, decorated with a timeout.
As hardware communication is the speed bottleneck. this code is slightly optimized in so far as only changes are sent to hardware.
- _get_HAL()[source]¶
Get sensor values from HAL, decorated with timeout. As hardware communication is the speed bottleneck. this code is slightly optimized in so far as some sensors are queried only in certain phases of the breatch cycle. This is done to run the primary PID loop as fast as possible:
pressure is always queried
Flow is queried only outside of inspiration
In addition, oxygen is only read every 5 seconds.
- class pvp.controller.control_module.Balloon_Simulator(peep_valve)[source]¶
Bases:
object
Physics simulator for inflating a balloon with an attached PEEP valve. For math, see https://en.wikipedia.org/wiki/Two-balloon_experiment
Methods:
set_flow_in
(Qin, dt)set_flow_out
(Qout, dt)update
(dt)OUupdate
(variable, dt, mu, sigma, tau)This is a simple function to produce an OU process on variable.
_reset
()Resets Balloon to default settings.
- class pvp.controller.control_module.ControlModuleSimulator(save_logs: bool = False, simulator_dt=None, peep_valve_setting=5)[source]¶
Bases:
pvp.controller.control_module.ControlModuleBase
Controlling Simulation.
Initializes the ControlModuleBase with the simple simulation (for testing/dev).
- Parameters
Methods:
__init__
([save_logs, simulator_dt, ...])Initializes the ControlModuleBase with the simple simulation (for testing/dev).
Make the sensor value object from current (simulated) measurements
This is the main loop.
- __init__(save_logs: bool = False, simulator_dt=None, peep_valve_setting=5)[source]¶
Initializes the ControlModuleBase with the simple simulation (for testing/dev).
- __SimulatedPropValve(x)¶
This simulates the action of a proportional valve. Flow-current-curve eye-balled from generic prop vale with logistic activation.
- __SimulatedSolenoid(x)¶
This simulates the action of a two-state Solenoid valve.