Tabel 121 titik data tidak dibaca siapa pun; kurva S dibaca dalam dua detik. Bagian ini mengubah struct hasil menjadi tiga gambar: kurva biomassa dengan anotasi kunci, kurva laju yang menampar titik belok, dan panel perbandingan tiga skenario laju. Semua digambar oleh satu fungsi buatVisualisasi yang aman dijalankan tanpa layar — figur dibuat tersembunyi, disimpan ke PNG, lalu ditutup.
Yang kita bangun pada bagian ini:
- Gambar 1 — kurva P vs t dengan garis K, garis K/2, dan marker t50,
- Gambar 2 — laju dP/dt vs t dengan puncak sebagai titik belok,
- Gambar 3 — subplot tiga skenario r = 0.05, 0.08, 0.12,
- kesalahan umum visualisasi simulasi dan cara menghindarinya,
- persiapan data heatmap dari hasil sweep untuk Bagian 4.
Tiga Gambar dari Satu Fungsi #
buatVisualisasi(hasil, dirOutput) menerima struct hasil Bagian 2 dan folder keluaran, menyimpan tiga PNG, lalu mengonfirmasi tiap berkas lewat fprintf:
Gambar 1: kurva-pertumbuhan.png - kurva P, garis K dan K/2, t50 = 57.44 hari
Gambar 2: laju-pertumbuhan.png - laju dP/dt, puncak 1.9997 per hari di t = 57.50 hari
Gambar 3: skenario-r.png - perbandingan r = 0.05, 0.08, 0.12 per hariMengapa tiga gambar dan bukan satu? Karena tiap gambar menjawab satu pertanyaan: gambar 1 menjawab “berapa dan kapan”, gambar 2 menjawab “kapan paling cepat”, gambar 3 menjawab “bagaimana kalau lajunya berubah”.
Kurva Pertumbuhan dengan Anotasi #
Kurva utama adalah P versus t. Nilai yang membuatnya berguna justru bukan kurvanya, melainkan tiga acuan di sekelilingnya: kapasitas K, setengah kapasitas, dan waktu t50. Berikut bagian Gambar 1 dari src/buatVisualisasi.m:
% ---- Gambar 1: kurva pertumbuhan + anotasi ----
f1 = figure('visible', 'off');
plot(t, P, 'b-', 'LineWidth', 2);
hold on;
plot([t(1) t(end)], [K K], 'k--', 'LineWidth', 1);
plot([t(1) t(end)], [K/2 K/2], 'r--', 'LineWidth', 1);
plot(t50, K/2, 'ko', 'MarkerFaceColor', 'k');
hold off;
text(t50 + 4, K/2 + 3, sprintf('t50 = %.1f hari', t50));
legend('P(t)', 'K', 'K/2', 't50', 'Location', 'southeast');
xlabel('waktu t (hari)');
ylabel('biomassa P (satuan)');
title('Pertumbuhan logistik biomassa tanaman');
grid on;
berkas{1} = 'kurva-pertumbuhan.png';
saveas(f1, fullfile(dirOutput, berkas{1}));
delete(f1);Mari kita bedah baris-baris kode di atas:
- Baris 2 (
f1 = figure('visible', 'off');): figur dibuat tersembunyi sejak awal. Skrip tetap berjalan di mesin tanpa tampilan — server uji, container, CI — dansaveastetap menghasilkan PNG. - Baris 5-6 (
plot([t(1) t(end)], [K K], 'k--', ...)): garis horizontal digambar manual lewat dua titik(t awal, K)dan(t akhir, K). MATLAB modern punyaylineuntuk ini, tetapi Octave belum menyediakannya; dua titik bekerja di keduanya. - Baris 7 (
plot(t50, K/2, 'ko', 'MarkerFaceColor', 'k');): marker bulat diletakkan tepat di perpotongan garis K/2 dan kurva — momen biomassa mencapai separuh kapasitas. - Baris 9 (
text(t50 + 4, K/2 + 3, sprintf(...))): anotasi teks dibangun dengansprintfdari data, bukan diketik manual, sehingga angkanya ikut benar saat parameter berubah. Offset+4hari dan+3satuan menyimpannya dari garis putus-putus. - Baris 10 (
legend('P(t)', 'K', 'K/2', 't50', ...)): empat entri legenda cocok dengan empat panggilanplotsebelumnya — legenda menghitung panggilan plot, jadi urutan label harus mengikuti urutan penggambaran. - Baris 15-17 (
saveas ... delete(f1);): gambar disimpan denganfullfileagar pemisah folder benar di sistem operasi mana pun, lalu figur ditutup supaya memori grafik tidak menumpuk saat fungsi dipanggil berulang.
Laju Pertumbuhan dan Titik Belok #
Turunan numerik diff(P)./diff(t) dari grid harian memberi laju rata-rata per hari — dan puncaknya adalah informasi penting kurva S. Berikut bagian Gambar 2:
% ---- Gambar 2: laju pertumbuhan + titik belok ----
laju = diff(P) ./ diff(t);
tTengah = t(1:end-1) + diff(t) / 2;
[lajuMaks, iMaks] = max(laju);
f2 = figure('visible', 'off');
plot(tTengah, laju, 'b-', 'LineWidth', 2);
hold on;
plot(tTengah(iMaks), lajuMaks, 'ro', 'MarkerFaceColor', 'r');
hold off;
text(tTengah(iMaks) + 5, 0.9 * lajuMaks, ...
sprintf('puncak %.2f per hari di t = %.1f', lajuMaks, ...
tTengah(iMaks)));
legend('dP/dt', 'puncak', 'Location', 'southeast');
xlabel('waktu t (hari)');
ylabel('laju dP/dt (satuan per hari)');
title('Laju pertumbuhan: puncak di titik belok');
grid on;
berkas{2} = 'laju-pertumbuhan.png';
saveas(f2, fullfile(dirOutput, berkas{2}));
delete(f2);Mari kita bedah baris-baris kode di atas:
- Baris 2-3 (
laju = ... tTengah = ...):diffmemendekan vektor satu elemen, jadi laju antar hari diberi waktu khusus di tengah interval — (t ke-k + t ke-k+1)/2. Tanpa ini, kurva laju bergeser setengah hari ke kiri. - Baris 4 (
[lajuMaks, iMaks] = max(laju);): satu panggilan memberi nilai puncak dan posisinya — pola yang sama dipakaianalisisFase. - Baris 9-13 (marker dan anotasi puncak): lingkaran merah menandai puncak; teksnya dihitung dari
lajuMaksdantTengah(iMaks). Pada hasil nyata: 1.9997 per hari di t = 57.5 — menimpa t50 = 57.44. Bukan kebetulan: puncak laju logistik memang jatuh tepat di P = K/2, itulah titik belok.
Bentuk kurvanya sendiri sudah bercerita: naik, memuncak di tengah musim, lalu turun mendekati nol — fase eksponensial, transisi, dan jenuh yang dihitung analisisFase kini terlihat mata.
Membandingkan Skenario dengan subplot #
Pertanyaan “bagaimana kalau lajunya berubah?” dijawab dengan mengulang simulasi untuk beberapa r lalu menumpuknya sebagai subplot. Berikut bagian Gambar 3:
% ---- Gambar 3: perbandingan skenario laju r ----
daftarR = [0.05 0.08 0.12];
paramsSkenario = hasil.params;
f3 = figure('visible', 'off');
for k = 1:numel(daftarR)
paramsSkenario.r = daftarR(k);
hasilK = jalankanSimulasi(paramsSkenario);
subplot(3, 1, k);
plot(hasilK.t, hasilK.P, 'b-', 'LineWidth', 2);
hold on;
plot([hasilK.t(1) hasilK.t(end)], [K K], 'k--', 'LineWidth', 1);
hold off;
title(sprintf('r = %.2f per hari (K = %g)', daftarR(k), K));
xlabel('waktu t (hari)');
ylabel('P (satuan)');
grid on;
end
berkas{3} = 'skenario-r.png';
saveas(f3, fullfile(dirOutput, berkas{3}));
delete(f3);Mari kita bedah baris-baris kode di atas:
- Baris 3 (
paramsSkenario = hasil.params;): struct disalin per nilai. MengubahparamsSkenario.rdi dalam loop tidak menyentuh parameter simulasi dasar yang tersimpan dihasil.params. - Baris 5-7 (
for k = ... hasilK = jalankanSimulasi(...)): tiap skenario mensimulasikan ulang dari awal — simulasi 121 titik hanya butuh sekejap, jadi tidak ada alasan mendaur ulang data lama. - Baris 8 (
subplot(3, 1, k);): tiga panel bertumpuk vertikal dengan sumbu waktu yang sejajar — pembandingan kurva waktu paling mudah dibaca saat matanya bergerak lurus ke bawah.tiledlayoutadalah alternatif modern di MATLAB, tetapisubplotbekerja di kedua mesin. - Baris 11 (garis K di tiap panel): garis acuan yang sama di tiap panel membuat perbandingan adil — ketiga kurva menuju K yang sama; yang berbeda hanyalah kecepatannya. Panel r = 0.05 bahkan belum menyentuh 90% K pada hari ke-120.
Kesalahan Umum Visualisasi Simulasi #
| Kesalahan | Akibat | Perbaikan |
|---|---|---|
| Sumbu y dipotong (misalnya mulai dari 80) | kurva terlihat “sudah jenuh” padahal belum | biarkan rentang alami; bila perlu, mulai dari 0 |
| Label tanpa satuan | “P = 55” tidak berarti apa pun | xlabel/ylabel selalu menyebut satuan |
| Banyak kurva berbeda parameter dalam satu axes | legenda membludak, pola tak terbaca | pisahkan dengan subplot |
| Angka anotasi diketik manual | salah saat parameter berubah | bangun dari data lewat sprintf |
| Tanpa garis acuan K | “99.3340” tidak jelas dekat atau jauh dari kapasitas | gambar garis K dan K/2 |
Menyimpan Gambar dan Resolusi #
saveas(f, berkas) menyimpan resolusi layar — cukup untuk web dan laporan layar. Untuk cetak, print(f, 'kurva.png', '-dpng', '-r300') menaikkan resolusi ke 300 dpi; keduanya tersedia di MATLAB dan Octave. MATLAB R2020b+ juga menawarkan exportgraphics(f, 'kurva.png', 'Resolution', 300) yang hasilnya lebih rapi — tulis sebagai pilihan modern bila target Anda MATLAB murni.
Menyiapkan Data Heatmap #
Bagian 4 akan menyapu grid parameter r x K, dan hasilnya paling enak dibaca sebagai heatmap. Data-datanya sudah berbentuk matriks Pakhir (baris = r, kolom = K); yang tersisa hanya memetakan ke sumbu. Potongan berikut dari src/jalankanStudi.m menggambar heatmap dari data sweep:
% ---- 5. heatmap dari data sweep ----
[R, KK] = meshgrid(sweep.r, sweep.K);
f4 = figure('visible', 'off');
contourf(R, KK, sweep.Pakhir.');
colorbar();
xlabel('r (per hari)');
ylabel('K (satuan biomassa)');
title('P akhir hari ke-120 pada grid r x K');
namaHeatmap = 'heatmap-sweep.png';
saveas(f4, fullfile(dirOutput, namaHeatmap));
delete(f4);
fprintf('Heatmap: %s - P akhir pada grid r x K\n', namaHeatmap);Mari kita bedah baris-baris kode di atas:
- Baris 2 (
[R, KK] = meshgrid(sweep.r, sweep.K);):meshgridmembangun grid koordinat dengan baris mengikuti argumen kedua (K) dan kolom mengikuti argumen pertama (r) — ukuran 4x5. - Baris 4 (
contourf(R, KK, sweep.Pakhir.')):Pakhirberukuran 5x4 (baris r), jadi ia harus di-transpose agar sejalan dengan grid 4x5 hasilmeshgrid. Transpose yang hilang adalah bug heatmap paling umum: gambar tetap tergambar, tapi terbalik diameternya. - Baris 5 (
colorbar();): kontur terisi tanpa skala warna hanya jadi dekorasi;colorbarmemberi angkanya.
Tiga gambar dan mesin penghasilnya sudah berdiri. Bagian 4 mengisi panel datanya: menyapu 20 kombinasi parameter, membaca matriksnya, dan melipat semuanya menjadi laporan otomatis.