Py.Cafe

jackparmer/

dash-pid-controller-simulation

PID Controller Simulation

DocsPricing
  • app.py
  • requirements.txt
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import dash
from dash import dcc, html
import dash_bootstrap_components as dbc
from dash.dependencies import Input, Output
import numpy as np
import plotly.graph_objs as go

# PID Controller class
class PID:
    def __init__(self, Kp, Ki, Kd, setpoint):
        self.Kp = Kp
        self.Ki = Ki
        self.Kd = Kd
        self.setpoint = setpoint
        self.integral = 0
        self.previous_error = 0

    def update(self, measured_value, dt):
        error = self.setpoint - measured_value
        self.integral += error * dt
        derivative = (error - self.previous_error) / dt
        output = self.Kp * error + self.Ki * self.integral + self.Kd * derivative
        self.previous_error = error
        return output

# Function to generate different setpoint shapes
def generate_setpoint(shape, time, amplitude=50):
    if shape == 'constant':
        return np.ones_like(time) * amplitude
    elif shape == 'sine wave':
        return amplitude + amplitude * np.sin(time)
    elif shape == 'sine wave with increasing frequency':
        return amplitude + amplitude * np.sin(time * time)
    elif shape == 'square wave':
        return amplitude + amplitude * np.sign(np.sin(time))
    elif shape == 'random noise':
        return amplitude + np.random.normal(0, amplitude/2, len(time))
    else:
        return np.ones_like(time) * amplitude

# Initialize the app
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])

# Layout of the app
app.layout = dbc.Container([
    dbc.Row([
        dbc.Col([
            html.H2("PID Controller Simulation"),
            html.Label("Setpoint Shape"),
            dcc.Dropdown(
                id='setpoint-shape-dropdown',
                options=[
                    {'label': 'Constant', 'value': 'constant'},
                    {'label': 'Sine Wave', 'value': 'sine wave'},
                    {'label': 'Sine Wave with Increasing Frequency', 'value': 'sine wave with increasing frequency'},
                    {'label': 'Square Wave', 'value': 'square wave'},
                    {'label': 'Random Noise', 'value': 'random noise'}
                ],
                value='constant'
            ),
            html.Label("Kp"),
            dcc.Slider(id='kp-slider', min=0, max=10, step=0.1, value=1,
                       marks={i: str(i) for i in range(0, 11)}),
            html.Label("Ki"),
            dcc.Slider(id='ki-slider', min=0, max=10, step=0.1, value=0.1,
                       marks={i: str(i) for i in range(0, 11)}),
            html.Label("Kd"),
            dcc.Slider(id='kd-slider', min=0, max=10, step=0.1, value=0.01,
                       marks={i: str(i) for i in range(0, 11)})
        ], width=3),
        dbc.Col([
            dcc.Graph(id='pid-graph')
        ], width=9)
    ])
])

# Callback to update the graph
@app.callback(
    Output('pid-graph', 'figure'),
    Input('setpoint-shape-dropdown', 'value'),
    Input('kp-slider', 'value'),
    Input('ki-slider', 'value'),
    Input('kd-slider', 'value')
)
def update_graph(setpoint_shape, kp, ki, kd):
    dt = 0.1
    time = np.arange(0, 10, dt)
    setpoint = generate_setpoint(setpoint_shape, time)
    
    pid = PID(Kp=kp, Ki=ki, Kd=kd, setpoint=setpoint[0])
    measured_value = 0
    process_variable = []

    for i, t in enumerate(time):
        control_signal = pid.update(measured_value, dt)
        measured_value += control_signal * dt
        process_variable.append(measured_value)
        if i < len(setpoint) - 1:
            pid.setpoint = setpoint[i + 1]

    figure = go.Figure()
    figure.add_trace(go.Scatter(x=time, y=process_variable, mode='lines', name='Process Variable'))
    figure.add_trace(go.Scatter(x=time, y=setpoint, mode='lines', name='Setpoint'))

    figure.update_layout(title='PID Controller Response', xaxis_title='Time', yaxis_title='Process Variable')

    return figure

# Run the app
if __name__ == '__main__':
    app.run_server(debug=True)