From 98b83d5f57ee8a6a1a913dbcf66a2f9dab776709 Mon Sep 17 00:00:00 2001 From: Go Komura Date: Sun, 23 Nov 2025 16:26:25 +0900 Subject: [PATCH 1/4] Add water tank level control example to documentation - Introduced a new application example demonstrating fuzzy control for a water tank. - Updated the documentation to include a link to the new example in the applications section. --- docs/make.jl | 1 + .../applications/water_tank_control.jl | 174 ++++++++++++++++++ 2 files changed, 175 insertions(+) create mode 100644 docs/src/literate/applications/water_tank_control.jl diff --git a/docs/make.jl b/docs/make.jl index e960dd0..192763c 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -161,6 +161,7 @@ makedocs(; ], "Applications" => [ "Edge detection" => "applications/edge_detector.md", + "Water tank level control" => "applications/water_tank_control.md", ], "API" => [ "Inference system API" => [ diff --git a/docs/src/literate/applications/water_tank_control.jl b/docs/src/literate/applications/water_tank_control.jl new file mode 100644 index 0000000..413b660 --- /dev/null +++ b/docs/src/literate/applications/water_tank_control.jl @@ -0,0 +1,174 @@ +# # Water tank level control +# +# This example shows how to use `FuzzyLogic.jl` to design a fuzzy +# controller for a single water tank. The goal is to keep the water level +# close to a desired reference by adjusting the inlet flow using a fuzzy +# rule base. +# +# !!! tip "Try it yourself!" +# Read this as Jupyter notebook [here](https://nbviewer.org/github/lucaferranti/FuzzyLogic.jl/blob/gh-pages/dev/notebooks/water_tank_control.ipynb) +# +# ## Plant model +# +# We consider a simple first-order model. The state of the system is the +# water level `h`. The inlet flow `q_in` is our control input, while the +# outlet flow is proportional to the current water level: +# +# ```math +# \dot h(t) = \frac{1}{A} \bigl(q_{\text{in}}(t) - k_\text{out} h(t)\bigr) +# ``` +# +# where `A` is the tank cross-section and `k_out` is an outflow +# coefficient. We simulate the model in discrete time using forward Euler +# integration. +# +# We define a small helper function to simulate one time step of the +# plant. + +using FuzzyLogic +using Plots + +# Tank parameters +const A_tank = 1.0 # [m^2] cross-section +const k_out = 0.5 # [1/s] outflow coefficient +const q_in_max = 1.0 # [m^3/s] maximum inlet flow + +""" + step_tank(h, u; dt) -> h_next + +Simulate one time step of the water tank. + +- `h` : current water level +- `u` : control signal in [0, 1] (valve opening) +- `dt` : time step + +Returns the new water level `h_next`. +""" +function step_tank(h::Float64, u::Float64; dt::Float64) + q_in = q_in_max * clamp(u, 0.0, 1.0) + q_out = k_out * max(h, 0.0) + dh = (q_in - q_out) / A_tank + h_new = h + dt * dh + return max(h_new, 0.0) +end + +# ## Fuzzy controller design +# +# We design a Mamdani-type fuzzy controller with: +# +# - Inputs +# * `e` : level error, `e = h_ref - h` +# * `de` : change of error, `de = e - e_prev` +# - Output +# * `u` : valve opening (normalized in [0, 1]) +# +# The intuition is: +# +# - If the level is much lower than the reference (`e` large positive), +# the valve should be opened almost fully. +# - If the level is higher than the reference (`e` negative), the valve +# should be almost closed. +# - If the level is close to the reference, the action depends on whether +# we are approaching the setpoint or moving away from it (`de`). +# +# We implement the controller using the `@mamfis` macro. +fis = @mamfis function water_tank_controller(e, de)::u + e := begin + domain = -1.0:1.0 + + NL = TrapezoidalMF(-1.0, -1.0, -0.7, -0.3) # negative large + NS = TriangularMF(-0.7, -0.35, 0.0) # negative small + ZE = TriangularMF(-0.1, 0.0, 0.1) # around zero + PS = TriangularMF(0.0, 0.35, 0.7) # positive small + PL = TrapezoidalMF(0.3, 0.7, 1.0, 1.0) # positive large + end + + de := begin + domain = -0.5:0.5 + + DN = TrapezoidalMF(-0.5, -0.5, -0.25, 0.0) # decreasing + DZ = TriangularMF(-0.1, 0.0, 0.1) # roughly constant + DP = TrapezoidalMF(0.0, 0.25, 0.5, 0.5) # increasing + end + + u := begin + domain = 0.0:1.0 + + Close = TrapezoidalMF(0.0, 0.0, 0.1, 0.2) + Small = TriangularMF(0.1, 0.25, 0.4) + Medium = TriangularMF(0.3, 0.5, 0.7) + Large = TriangularMF(0.6, 0.75, 0.9) + Full = TrapezoidalMF(0.8, 0.9, 1.0, 1.0) + end + + e == PL && de == DN --> u == Full + e == PL && de == DZ --> u == Full + e == PL && de == DP --> u == Large + + e == PS && de == DN --> u == Full + e == PS && de == DZ --> u == Large + e == PS && de == DP --> u == Medium + + e == ZE && de == DN --> u == Large + e == ZE && de == DZ --> u == Medium + e == ZE && de == DP --> u == Small + + e == NS && de == DN --> u == Small + e == NS && de == DZ --> u == Close + e == NS && de == DP --> u == Close + + e == NL --> u == Close +end + +# Plot the fuzzy system and its membership functions. +plot(fis, size = (800, 400)) + +plot(plot(fis, :e, size = (400, 300)), + plot(fis, :u, size = (400, 300)), + layout = (1, 2)) + +# ## Closed-loop simulation +# +# We now simulate the closed-loop system composed of the water tank model +# and the fuzzy controller. We consider a unit step in the reference +# level: the level should rise from 0 to 1 and stay close to 1 without +# excessive overshoot. +let dt = 0.1, # [s] time step + t_final = 60.0, # [s] total simulation time + h_ref = 1.0 + + n_steps = Int(round(t_final / dt)) + + time = collect(0:n_steps) .* dt + h_log = similar(time) + u_log = similar(time) + + h = 0.0 + e_prev = h_ref - h + + for k in eachindex(time) + e = h_ref - h + de = e - e_prev + + u_val = fis(e = e, de = de)[:u] + h_next = step_tank(h, u_val; dt = dt) + + h_log[k] = h + u_log[k] = u_val + + e_prev = e + h = h_next + end + + p1 = plot(time, h_log, + xlabel = "time [s]", ylabel = "water level", + label = "h(t)", legend = :bottomright) + plot!(p1, time, fill(h_ref, length(time)), linestyle = :dash, + label = "reference") + + p2 = plot(time, u_log, + xlabel = "time [s]", ylabel = "valve opening u", + label = "u(t)", legend = :bottomright) + + plot(p1, p2, layout = (2, 1), size = (800, 600)) +end From 3d1cf8bf8e116892dc92ec0a3c36c7a397ae093c Mon Sep 17 00:00:00 2001 From: Go Komura Date: Sun, 23 Nov 2025 17:16:08 +0900 Subject: [PATCH 2/4] Refactor water tank control example for improved readability - Standardized spacing and formatting in the water tank control example code. - Updated membership function definitions for clarity and consistency. - Enhanced plot layout for better visualization of results. --- .../applications/water_tank_control.jl | 74 +++++++++---------- 1 file changed, 37 insertions(+), 37 deletions(-) diff --git a/docs/src/literate/applications/water_tank_control.jl b/docs/src/literate/applications/water_tank_control.jl index 413b660..4cb0c9c 100644 --- a/docs/src/literate/applications/water_tank_control.jl +++ b/docs/src/literate/applications/water_tank_control.jl @@ -29,8 +29,8 @@ using FuzzyLogic using Plots # Tank parameters -const A_tank = 1.0 # [m^2] cross-section -const k_out = 0.5 # [1/s] outflow coefficient +const A_tank = 1.0 # [m^2] cross-section +const k_out = 0.5 # [1/s] outflow coefficient const q_in_max = 1.0 # [m^3/s] maximum inlet flow """ @@ -45,9 +45,9 @@ Simulate one time step of the water tank. Returns the new water level `h_next`. """ function step_tank(h::Float64, u::Float64; dt::Float64) - q_in = q_in_max * clamp(u, 0.0, 1.0) + q_in = q_in_max * clamp(u, 0.0, 1.0) q_out = k_out * max(h, 0.0) - dh = (q_in - q_out) / A_tank + dh = (q_in - q_out) / A_tank h_new = h + dt * dh return max(h_new, 0.0) end @@ -74,58 +74,58 @@ end # We implement the controller using the `@mamfis` macro. fis = @mamfis function water_tank_controller(e, de)::u e := begin - domain = -1.0:1.0 + domain = -1:1 - NL = TrapezoidalMF(-1.0, -1.0, -0.7, -0.3) # negative large + NL = ZShapeMF(-0.9, -0.2) # negative large NS = TriangularMF(-0.7, -0.35, 0.0) # negative small ZE = TriangularMF(-0.1, 0.0, 0.1) # around zero PS = TriangularMF(0.0, 0.35, 0.7) # positive small - PL = TrapezoidalMF(0.3, 0.7, 1.0, 1.0) # positive large + PL = SShapeMF(0.6, 0.9) # positive large end de := begin - domain = -0.5:0.5 + domain = -1:1 - DN = TrapezoidalMF(-0.5, -0.5, -0.25, 0.0) # decreasing + DN = ZShapeMF(-0.4, -0.05) # decreasing DZ = TriangularMF(-0.1, 0.0, 0.1) # roughly constant - DP = TrapezoidalMF(0.0, 0.25, 0.5, 0.5) # increasing + DP = SShapeMF(0.05, 0.4) # increasing end u := begin - domain = 0.0:1.0 + domain = 0:1 - Close = TrapezoidalMF(0.0, 0.0, 0.1, 0.2) - Small = TriangularMF(0.1, 0.25, 0.4) + Close = ZShapeMF(0.05, 0.2) + Small = TriangularMF(0.1, 0.25, 0.4) Medium = TriangularMF(0.3, 0.5, 0.7) - Large = TriangularMF(0.6, 0.75, 0.9) - Full = TrapezoidalMF(0.8, 0.9, 1.0, 1.0) + Large = TriangularMF(0.6, 0.75, 0.9) + Full = SShapeMF(0.8, 1.05) end - e == PL && de == DN --> u == Full - e == PL && de == DZ --> u == Full - e == PL && de == DP --> u == Large + e == PL && de == DN --> u == Full + e == PL && de == DZ --> u == Full + e == PL && de == DP --> u == Large - e == PS && de == DN --> u == Full - e == PS && de == DZ --> u == Large - e == PS && de == DP --> u == Medium + e == PS && de == DN --> u == Full + e == PS && de == DZ --> u == Large + e == PS && de == DP --> u == Medium - e == ZE && de == DN --> u == Large - e == ZE && de == DZ --> u == Medium - e == ZE && de == DP --> u == Small + e == ZE && de == DN --> u == Large + e == ZE && de == DZ --> u == Medium + e == ZE && de == DP --> u == Small - e == NS && de == DN --> u == Small - e == NS && de == DZ --> u == Close - e == NS && de == DP --> u == Close + e == NS && de == DN --> u == Small + e == NS && de == DZ --> u == Close + e == NS && de == DP --> u == Close - e == NL --> u == Close + e == NL --> u == Close end # Plot the fuzzy system and its membership functions. plot(fis, size = (800, 400)) plot(plot(fis, :e, size = (400, 300)), - plot(fis, :u, size = (400, 300)), - layout = (1, 2)) + plot(fis, :u, size = (400, 300)), + layout = (1, 2)) # ## Closed-loop simulation # @@ -147,7 +147,7 @@ let dt = 0.1, # [s] time step e_prev = h_ref - h for k in eachindex(time) - e = h_ref - h + e = h_ref - h de = e - e_prev u_val = fis(e = e, de = de)[:u] @@ -157,18 +157,18 @@ let dt = 0.1, # [s] time step u_log[k] = u_val e_prev = e - h = h_next + h = h_next end p1 = plot(time, h_log, - xlabel = "time [s]", ylabel = "water level", - label = "h(t)", legend = :bottomright) + xlabel = "time [s]", ylabel = "water level", + label = "h(t)", legend = :bottomright) plot!(p1, time, fill(h_ref, length(time)), linestyle = :dash, - label = "reference") + label = "reference") p2 = plot(time, u_log, - xlabel = "time [s]", ylabel = "valve opening u", - label = "u(t)", legend = :bottomright) + xlabel = "time [s]", ylabel = "valve opening u", + label = "u(t)", legend = :bottomright) plot(p1, p2, layout = (2, 1), size = (800, 600)) end From 60bf2790400bd3d88332115aede881b78153cc7b Mon Sep 17 00:00:00 2001 From: Go Komura Date: Sun, 23 Nov 2025 18:17:23 +0900 Subject: [PATCH 3/4] Enhance water tank control example with refined membership functions - Updated membership function definitions for input and output variables to improve accuracy and clarity. - Adjusted fuzzy rules to accommodate new membership functions. - Reduced time step in simulation for better resolution of results. --- .../applications/water_tank_control.jl | 70 +++++++++++-------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/docs/src/literate/applications/water_tank_control.jl b/docs/src/literate/applications/water_tank_control.jl index 4cb0c9c..eb4d574 100644 --- a/docs/src/literate/applications/water_tank_control.jl +++ b/docs/src/literate/applications/water_tank_control.jl @@ -76,48 +76,58 @@ fis = @mamfis function water_tank_controller(e, de)::u e := begin domain = -1:1 - NL = ZShapeMF(-0.9, -0.2) # negative large - NS = TriangularMF(-0.7, -0.35, 0.0) # negative small - ZE = TriangularMF(-0.1, 0.0, 0.1) # around zero - PS = TriangularMF(0.0, 0.35, 0.7) # positive small - PL = SShapeMF(0.6, 0.9) # positive large + PVS = ZShapeMF(-1.0, -0.666) # very small positive + PS = GaussianMF(-0.5, 0.333) # small + PM = GaussianMF(0.0, 0.333) # medium + PL = GaussianMF(0.5, 0.333) # large + PVL = SShapeMF(0.666, 1.0) # very large end de := begin domain = -1:1 - DN = ZShapeMF(-0.4, -0.05) # decreasing - DZ = TriangularMF(-0.1, 0.0, 0.1) # roughly constant - DP = SShapeMF(0.05, 0.4) # increasing + VDN = ZShapeMF(-1.0, -0.666) # very decreasing + DN = GaussianMF(-0.5, 0.333) # slightly decreasing + DZ = GaussianMF(0.0, 0.333) # roughly constant + DP = GaussianMF(0.5, 0.333) # slightly increasing + VDP = SShapeMF(0.666, 1.0) # very increasing end u := begin domain = 0:1 - Close = ZShapeMF(0.05, 0.2) - Small = TriangularMF(0.1, 0.25, 0.4) - Medium = TriangularMF(0.3, 0.5, 0.7) - Large = TriangularMF(0.6, 0.75, 0.9) - Full = SShapeMF(0.8, 1.05) + Close = ZShapeMF(0.00, 0.166) + Small = GaussianMF(0.25, 0.166) + Medium = GaussianMF(0.5, 0.166) + Large = GaussianMF(0.75, 0.166) + Full = SShapeMF(0.833, 1.00) end - e == PL && de == DN --> u == Full - e == PL && de == DZ --> u == Full - e == PL && de == DP --> u == Large - - e == PS && de == DN --> u == Full - e == PS && de == DZ --> u == Large - e == PS && de == DP --> u == Medium - - e == ZE && de == DN --> u == Large - e == ZE && de == DZ --> u == Medium - e == ZE && de == DP --> u == Small + e == PVL && de == VDN --> u == Full + e == PVL && de == DN --> u == Full + e == PVL && de == DZ --> u == Full + e == PVL && de == DP --> u == Large + e == PVL && de == VDP --> u == Large - e == NS && de == DN --> u == Small - e == NS && de == DZ --> u == Close - e == NS && de == DP --> u == Close - - e == NL --> u == Close + e == PL && de == VDN --> u == Full + e == PL && de == DN --> u == Full + e == PL && de == DZ --> u == Large + e == PL && de == DP --> u == Medium + e == PL && de == VDP --> u == Medium + + e == PM && de == VDN --> u == Large + e == PM && de == DN --> u == Large + e == PM && de == DZ --> u == Medium + e == PM && de == DP --> u == Small + e == PM && de == VDP --> u == Small + + e == PS && de == VDN --> u == Medium + e == PS && de == DN --> u == Small + e == PS && de == DZ --> u == Small + e == PS && de == DP --> u == Close + e == PS && de == VDP --> u == Close + + e == PVS --> u == Close end # Plot the fuzzy system and its membership functions. @@ -133,7 +143,7 @@ plot(plot(fis, :e, size = (400, 300)), # and the fuzzy controller. We consider a unit step in the reference # level: the level should rise from 0 to 1 and stay close to 1 without # excessive overshoot. -let dt = 0.1, # [s] time step +let dt = 0.01, # [s] time step t_final = 60.0, # [s] total simulation time h_ref = 1.0 From 25f3376e249fe560ac3573ff0f8bb5578633bce7 Mon Sep 17 00:00:00 2001 From: Go Komura Date: Sun, 23 Nov 2025 18:40:56 +0900 Subject: [PATCH 4/4] Implement PID control in water tank example - Added PID control logic to the water tank control example for improved performance. - Introduced new variables for PID gains and logging of PID outputs. - Updated plots to differentiate between fuzzy and PID control outputs for better visualization. --- .../applications/water_tank_control.jl | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/src/literate/applications/water_tank_control.jl b/docs/src/literate/applications/water_tank_control.jl index eb4d574..f737aa2 100644 --- a/docs/src/literate/applications/water_tank_control.jl +++ b/docs/src/literate/applications/water_tank_control.jl @@ -152,9 +152,17 @@ let dt = 0.01, # [s] time step time = collect(0:n_steps) .* dt h_log = similar(time) u_log = similar(time) + h_log_p = similar(time) + u_log_p = similar(time) h = 0.0 e_prev = h_ref - h + h_p = 0.0 + e_prev_p = h_ref - h_p + Kp = 0.8 # proportional gain + Ki = 0.1 # integral gain + Kd = 0.05 # derivative gain + int_e = 0.0 for k in eachindex(time) e = h_ref - h @@ -168,17 +176,31 @@ let dt = 0.01, # [s] time step e_prev = e h = h_next + + e_p = h_ref - h_p + de_p = e_p - e_prev_p + int_e += e_p * dt + u_p = clamp(Kp * e_p + Ki * int_e + Kd * de_p / dt, 0.0, 1.0) + h_next_p = step_tank(h_p, u_p; dt = dt) + + h_log_p[k] = h_p + u_log_p[k] = u_p + + e_prev_p = e_p + h_p = h_next_p end p1 = plot(time, h_log, xlabel = "time [s]", ylabel = "water level", - label = "h(t)", legend = :bottomright) + label = "fuzzy h(t)", legend = :bottomright) + plot!(p1, time, h_log_p, label = "PID h(t)") plot!(p1, time, fill(h_ref, length(time)), linestyle = :dash, label = "reference") p2 = plot(time, u_log, xlabel = "time [s]", ylabel = "valve opening u", - label = "u(t)", legend = :bottomright) + label = "fuzzy u(t)", legend = :bottomright) + plot!(p2, time, u_log_p, label = "PID u(t)") plot(p1, p2, layout = (2, 1), size = (800, 600)) end