Wednesday, October 29, 2025

Simulasi Dinamika Tangki Air dengan Lazarus: Cara Mudah Memahami Sistem Kontrol

 Simulasi Dinamika Tangki Air dengan Lazarus: Cara Mudah Memahami Sistem Kontrol


kita akan membaut sebuah sistem pengisian tanki air, kenapa pilih tanki air karena paling simple perhitungannya dan mudah buatnya dan analisanya. tapi meskipun system ini lambat tapi kita bisa aplikasikan nantinya PID buatan kita sehingga kita tau apakah PID aplikatif dan sekalian belajar karakteristik PID pada system lambat.

jadi mari kita mulai dengan membayangkan sebuah tangki dulu :

Volume tangki

jika dengan luas alas 100cm2 dan tinggi tangki 100cm jadi volume tangki full adalah 10.000cm3 itu yang akan mendasari simulasi kita. 

Dalam dunia kontrol sistem, memahami perilaku sistem dinamik adalah langkah penting sebelum menerapkan algoritma seperti PID.
Pada artikel ini, kita akan membangun dan mensimulasikan sistem pengisian tangki air, sebuah model klasik yang sederhana namun kaya makna.
Meski sistem ini tergolong lambat, justru di situlah tantangannya — kita bisa menguji respons dan kestabilan PID pada kondisi nyata sekaligus memahami karakteristik sistem dengan lebih intuitif.

Desain system tangki




Menetapkan FlowIn / FlowOut — kenapa 100 s untuk mengisi / menguras?

Dalam contoh teks kamu disebutkan: FlowIn dan FlowOut masing-masing 100 cm³/s. Dengan parameter tanki di kode:

  • TankArea = 100.0 cm²

  • TankHeightMax = 100.0 cm

maka volume penuh tangki = TankArea * TankHeightMax = 100 cm² * 100 cm = 10.000 cm³.

Jika laju pengisian bersih (net flow) = 100 cm³/s, waktu untuk mengisi dari kosong ke penuh adalah:

  • waktu = volume / laju = 10.000 cm³ / 100 cm³/s = 100 s.

(Itu yang kamu tulis — aritmetika di atas valid digit-by-digit: 10000 ÷ 100 = 100.)

Jadi pernyataan “mengisi dari kosong membutuhkan 100 detik dan untuk menguras dari full membutuhkan 100 detik” benar asalkan FlowIn = 100 dan FlowOut = 0 untuk kasus isi; atau untuk menguras, FlowOut = 100 dan FlowIn = 0. Jika keduanya aktif, yang relevan adalah net flow = FlowIn − FlowOut.


Hubungan langsung dengan kode (baris penting dan maknanya)

Di kode kamu ada fungsi penting:

NetFlow := FlowRate - flow_out; // Net flow (cm^3/s) Level := Level + (NetFlow / TankArea) * DeltaTime;

Penjelasan langkah-per-langkah:

  1. NetFlow dihitung dalam satuan cm³/s.

  2. NetFlow / TankArea → perubahan tinggi per detik (cm/s), karena cm³/s dibagi cm² → cm/s.

  3. Dikali DeltaTime (detik per step, di kode DeltaTime = 0.1 → 100 ms) → perubahan tinggi per step (cm).

  4. Ditambahkan ke Level untuk mendapatkan tinggi baru.

Dengan kata lain formula update tingkat (Level) di kode sama dengan persamaan kontinuitas sederhana:

Δh=QinQoutAΔt\Delta h = \dfrac{Q_{in} - Q_{out}}{A} \cdot \Delta t

di mana hh = tinggi (cm),


Q
Q
= laju aliran (cm³/s),
A
A
= luas penampang (cm²), Δt\Delta t = durasi step (s).



animasi Lazarus



Gambar diatas adalah simulasi yang sudah saya siapkan untuk menggambarkan volume tangki yang nantinya akan di hitung oleh system. untuk membuat ini saya sudah siapkan videonya pada link ini.

sekarang kita membutuhkan mendesain program di lazarusnya :

Inti desain: kenapa butuh 2 procedure + 2 timer

Desain yang kamu tulis membagi tugas menjadi dua lapis waktu berbeda:

  1. RunSimulationStep (high-rate, mis. tiap 100 ms)

    • Tugas: menghitung dinamika plant secara numerik (perubahan level/volume pada tiap langkah simulasi).

    • Alasan: fisika (perubahan tinggi akibat flow) harus dihitung cukup sering agar simulasi stabil dan numerik akurat. Di kode kamu DeltaTime = 0.1 → 100 ms, dan tmr1 memanggil RunSimulationStep tiap interval itu.

  2. UpdateTank (low-rate / akumulasi, mis. tiap 1 s)

    • Tugas yang kamu maksud: menghitung/menyimpan akumulasi total volume untuk keperluan logging, tampilan yang tidak perlu update tiap-milisekon, atau penyajian nilai per detik.

    • Alasan: operasi UI, penulisan log, atau pembacaan sensor sering cukup tiap 1 s; mempercepat UI tidak memberi manfaat dan malah bisa membuat aplikasi berat. Maka tmr2 dipakai untuk tugas yang lebih lambat (UI / akumulasi) — di desainmu kamu ingin tmr2.Interval := 1000.

Kesimpulan: dua timer ini meng-decouple (memisahkan) perhitungan fisika berkecepatan tinggi dari tugas UI/akumulasi berkecepatan rendah — praktek standar pada simulasi & control realtime.

Dari kode bisa di lihat:

  • tmr1.Interval := round(DeltaTime*1000);tmr1 dipakai untuk memanggil RunSimulationStep (Δt = 0.1 s).
    procedure TForm1.tmr1Timer(Sender: TObject); begin RunSimulationStep; end;

  • RunSimulationStep mengambil nilai trackbar (flow_in := tb1.Position; flow_out := tb2.Position;) lalu memanggil UpdateTank(level_tanki, flow_in); dan mengeset tinggi := round(level_tanki);

  • tmr2Timer saat ini hanya melakukan:

    tinggi := Round(level_tanki); // ambil nilai dari sensor VirtualScreen.RedrawBitmap;

    namun di desain kamu ingin tmr2 menjalankan akumulasi / update per 1 detik (misalnya untuk menambah entri log / menampilkan total volume per detik).

Catatan: di kode UpdateTank sekarang menghitung level tiap kali dipanggil (yaitu saat tmr1 berjalan) — sehingga UpdateTank sesungguhnya adalah update fisika per step, bukan akumulasi per 1 detik.


berikut adalah video aplikasi yang sudah jadi : file project bisa didwonload di






dan berikut adalah listing lengkapnya :

unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, ExtCtrls, StdCtrls,
  ComCtrls, BGRAVirtualScreen, BGRABitmap, BGRABitmapTypes;

type

  { TForm1 }

  TForm1 = class(TForm)
    Button1: TButton;
    Button2: TButton;
    Button3: TButton;
    Label1: TLabel;
    Label2: TLabel;
    lbl4: TLabel;
    lbl2: TLabel;
    lbl1: TLabel;
    lbl3: TLabel;
    tmr1: TTimer;
    tmr2: TTimer;
    tb1: TTrackBar;
    tb2: TTrackBar;
    VirtualScreen: TBGRAVirtualScreen;
    procedure Button1Click(Sender: TObject);
    procedure Button2Click(Sender: TObject);
    procedure Button3Click(Sender: TObject);
    procedure FormCreate(Sender: TObject);
    procedure tb1Change(Sender: TObject);
    procedure tb2Change(Sender: TObject);
    procedure tmr1Timer(Sender: TObject);
    procedure tmr2Timer(Sender: TObject);
    procedure TrackBar1Change(Sender: TObject);
    procedure VirtualScreenRedraw(Sender: TObject; Bitmap: TBGRABitmap);
  private
    background: TBGRABitmap;
    x0,y0,tebal,lebar,tinggi : integer;
    garis,isian : TBGRAPixel;

    level_tanki, flow_in, flow_out : double;
    level_tanki_old,level_tanki_new : Double;

    procedure RunSimulationStep;
    procedure UpdateTank(var Level: Double; FlowRate: Double);
  public

  end;

var
  Form1: TForm1;

implementation

{$R *.lfm}

//kalau mau rubah speck simulasi, rubah disini
const
  DeltaTime = 0.1;         // 100 ms
  TankArea = 100.0;        // cm^2
  MaxFlow = 100;         // cm^3/s
  TankHeightMax = 100.0;   // cm
  DeltaT: Single = 0.5;    // 10 ms

{ TForm1 }

procedure TForm1.RunSimulationStep();
begin

  flow_in :=tb1.Position;
  flow_out := tb2.Position;

  // Update plant
  UpdateTank(level_tanki, flow_in);

  //tampilan ke UI
  tinggi := round(level_tanki);

end;

procedure TForm1.UpdateTank(var Level: Double; FlowRate: Double);
var
  NetFlow: Double;
begin

  NetFlow := FlowRate - flow_out;  // Sekarang ada buangan

  Level := Level + (NetFlow / TankArea) * DeltaTime;

  //pengaman simulasi
  if Level > TankHeightMax then
    Level := TankHeightMax
  else if Level < 0 then
    Level := 0;
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
  //buat kotak pada gambar
  x0 := 111; y0 := 320; tebal := 2; lebar := 148; tinggi := 217;
  garis := BGRA(0,0,255,128); isian := BGRA(0,0,255,128);
  // load background sekali saja
  background := TBGRABitmap.Create;
  background.LoadFromFile('test.bmp');

  //kondisi awal simulasi
  level_tanki := 0.0;
  level_tanki_old := 0.0;
  level_tanki_new := 0.0;
  tinggi := 0;
  flow_out := tb2.Position;
  tmr1.Interval := round(DeltaTime*1000);
  tb1.Max:= MaxFlow;
  tb2.Max := MaxFlow;

  VirtualScreen.RedrawBitmap;
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
  tmr1.Enabled:= true;
  tmr2.Enabled:= true;
end;

procedure TForm1.Button2Click(Sender: TObject);
begin
  tmr1.Enabled:= false;
  tmr2.Enabled:= false;
  tb1.Position:= 0;
  tb2.Position:= 0;

  //kondisi awal simulasi
  level_tanki := 0.0;
  level_tanki_old := 0.0;
  level_tanki_new := 0.0;
  tinggi := 0;
  flow_out := tb2.Position;
  tmr1.Interval := round(DeltaTime*1000);
  tb1.Max:= MaxFlow;
  tb2.Max := MaxFlow;

  VirtualScreen.RedrawBitmap;
end;

procedure TForm1.Button3Click(Sender: TObject);
begin
  tmr1.Enabled:= false;
  tmr2.Enabled:= false;
end;

procedure TForm1.tb1Change(Sender: TObject);
begin
  lbl1.Caption:= inttostr(tb1.Position);
  lbl3.Caption:= inttostr(tb1.Position);
end;

procedure TForm1.tb2Change(Sender: TObject);
begin
  lbl2.Caption:= inttostr(tb2.Position);
  lbl4.Caption:= inttostr(tb2.Position);
end;

procedure TForm1.tmr1Timer(Sender: TObject);
begin
  RunSimulationStep;
end;

procedure TForm1.tmr2Timer(Sender: TObject);
begin
  tinggi := Round(level_tanki); // ambil nilai dari sensor
  VirtualScreen.RedrawBitmap;
end;

procedure TForm1.TrackBar1Change(Sender: TObject);
begin
end;

procedure TForm1.VirtualScreenRedraw(Sender: TObject; Bitmap: TBGRABitmap);
var
  imgRatio, scrRatio: Double;
  drawWidth, drawHeight, xOff, yOff: Integer;
begin
  // hitung aspect ratio
  imgRatio := background.Width / background.Height;
  scrRatio := Bitmap.Width / Bitmap.Height;

  if imgRatio > scrRatio then
  begin
    // gambar lebih lebar daripada layar → sesuaikan lebar penuh
    drawWidth := Bitmap.Width;
    drawHeight := Round(drawWidth / imgRatio);
    xOff := 0;
    yOff := (Bitmap.Height - drawHeight) div 2;
  end
  else
  begin
    // gambar lebih tinggi daripada layar → sesuaikan tinggi penuh
    drawHeight := Bitmap.Height;
    drawWidth := Round(drawHeight * imgRatio);
    yOff := 0;
    xOff := (Bitmap.Width - drawWidth) div 2;
  end;

  // gambar background dengan stretch proporsional-- jangan lupa urutan menggambarnya
  Bitmap.StretchPutImage(Rect(xOff, yOff, xOff+drawWidth, yOff+drawHeight), background, dmSet);

  //gambar object kotaknya-- jangan lupa urutan menggambarnya
  Bitmap.RectangleAntialias(x0,y0,x0+lebar,y0-tinggi,garis,tebal,isian);
end;

end.


No comments:

Post a Comment