update chicken weight to have chicken_area data as well

This commit is contained in:
Alberto-Audrix committed 2026-10-07 15:48:22 +07:00
1 parent a6d4ddac65
commit 6bad900cf0
13 files changed
+132 -21

No files matched your search

@@ -283,6 +283,7 @@ class Command(BaseCommand):
weight = AVG_WEIGHT[i]
prev_weight = AVG_WEIGHT[i - 1] if i > 0 else DOC_IN_WEIGHT
adg = _round(weight - prev_weight, 2) if weight else 0.0
area_cm2 = _round(4.8 * (weight ** 0.66), 1) if weight else 0.0
cw, _ = ChickenWeight.objects.update_or_create(
cycle=cycle,
date=day,
@@ -293,6 +294,7 @@ class Command(BaseCommand):
"chicken_count": SAMPLE_COUNT[i],
"uniformity": UNIFORMITY[i],
"average_daily_gain": adg,
"chicken_area": area_cm2,
},
)
cw_by_date[day] = cw
@@ -147,16 +147,18 @@ class Command(BaseCommand):
cw_by_date = {}
for row in sheets["chicken_weight"]:
avg_wt = float(as_number(row.get("average_weight")))
obj, _ = ChickenWeight.objects.update_or_create(
cycle=cycle,
date=row["date"],
defaults={
"age": int(as_number(row.get("age"))),
"doc_weight": int(as_number(row.get("doc_weight"))),
"average_weight": float(as_number(row.get("average_weight"))),
"average_weight": avg_wt,
"chicken_count": int(as_number(row.get("chicken_count"))),
"uniformity": float(as_number(row.get("uniformity"))),
"average_daily_gain": float(as_number(row.get("average_daily_gain"))),
"chicken_area": round(4.8 * (avg_wt ** 0.66), 1) if avg_wt > 0 else 0.0,
},
)
cw_by_date[row["date"]] = obj
@@ -0,0 +1,48 @@
# Generated by Django 5.2.17 on 2026-10-07 08:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('operations', '0020_drop_orphan_structured_data'),
]
operations = [
migrations.AddField(
model_name='chickenweight',
name='chicken_area',
field=models.FloatField(default=0.0),
),
migrations.AlterField(
model_name='chickencounting',
name='cc_id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='chickenweight',
name='cw_id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='feedsacks',
name='fs_id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='iotpanel',
name='panel_id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='kpi',
name='kpi_id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
migrations.AlterField(
model_name='manualinput',
name='manual_id',
field=models.BigAutoField(primary_key=True, serialize=False),
),
]
+1
View File
@@ -105,6 +105,7 @@ class ChickenWeight(models.Model):
chicken_count = models.IntegerField()
uniformity = models.FloatField()
average_daily_gain = models.FloatField()
chicken_area = models.FloatField(default=0.0)
cycle = models.ForeignKey(Cycle, on_delete=models.CASCADE, related_name="chicken_weights")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
@@ -116,10 +116,24 @@ def aggregate_weight_by_date(
weights = [float(f.get("predicted_weight_g") or 0) for f in floors]
unifs = [float(f.get("uniformity") or 0) for f in floors]
samples = [int(f.get("n_valid_samples") or 0) for f in floors]
areas = [
float(
f.get("area_cm2_median")
or f.get("area_cm2")
or f.get("predicted_area_cm2")
or f.get("bbox_area_cm2")
or f.get("area")
or 0
)
for f in floors
]
valid_areas = [a for a in areas if a > 0]
avg_area = sum(valid_areas) / len(valid_areas) if valid_areas else 0.0
by_date[day] = {
"average_weight": sum(weights) / len(weights),
"uniformity": sum(unifs) / len(unifs),
"chicken_count": max(samples) if samples else 0,
"chicken_area": round(avg_area, 2),
}
return by_date
@@ -309,6 +323,7 @@ def sync_chicken_weight_for_cycle(
uniformity = 0.0
chicken_count = int(cycle.doc_in_count)
adg = 0.0
chicken_area = 0.0
else:
metrics = weight_by_date.get(day)
if metrics is None:
@@ -317,6 +332,7 @@ def sync_chicken_weight_for_cycle(
uniformity = float(metrics["uniformity"])
chicken_count = int(metrics["chicken_count"])
adg = round(average_weight - prev_weight, 2)
chicken_area = float(metrics.get("chicken_area", 0.0))
defaults = {
"age": age,
@@ -325,6 +341,7 @@ def sync_chicken_weight_for_cycle(
"chicken_count": chicken_count,
"uniformity": uniformity,
"average_daily_gain": adg,
"chicken_area": chicken_area,
}
ChickenWeight.objects.update_or_create(
cycle=cycle,
@@ -175,12 +175,7 @@ erDiagram
int chicken_count "NOT NULL (n_weight_samples)"
float uniformity "NOT NULL (Flock weight uniformity %)"
float average_daily_gain "NOT NULL (ADG in grams/day)"
float area_cm2_median "Median body area in cm^2"
float minor_axis_cm_median "Median minor axis / width in cm"
float major_axis_cm_median "Median major axis / length in cm"
float gompertz_baseline_g "Theoretical Gompertz curve baseline"
float vision_gain_g "Allometric vision gain residual"
float rejection_rate "MAD outlier filter rejection percentage"
float chicken_area "NOT NULL DEFAULT 0.0 (Median chicken area in cm^2)"
timestamp created_at "NOT NULL"
timestamp updated_at "NOT NULL"
}
@@ -308,8 +303,7 @@ Our edge pipeline aggregates floor camera runs and pushes clean daily rollups di
- `chicken_count` $\leftarrow \sum \text{n\_weight\_samples}$ (valid non-outlier tracks).
- `uniformity` $\leftarrow \text{flock size uniformity percentage}$.
- `average_daily_gain` $\leftarrow \text{daily weight gain in grams/day}$.
- `area_cm2_median`, `minor_axis_cm_median`, `major_axis_cm_median` $\leftarrow \text{calibrated physical dimensions}$.
- `gompertz_baseline_g`, `vision_gain_g` $\leftarrow \text{audited growth model residual breakdown}$.
- `chicken_area` $\leftarrow \text{calibrated median chicken body area in cm²}$.
---
+17 -1
View File
@@ -1290,6 +1290,22 @@
"parent": "PF_pHT-a65sNfpEKlwq7-159",
"label": "average_daily_gain float NOT NULL"
},
{
"id": "PF_pHT-a65sNfpEKlwq7-cw-area",
"type": "node",
"parent": "PF_pHT-a65sNfpEKlwq7-66"
},
{
"id": "PF_pHT-a65sNfpEKlwq7-cw-area-col1",
"type": "node",
"parent": "PF_pHT-a65sNfpEKlwq7-cw-area"
},
{
"id": "PF_pHT-a65sNfpEKlwq7-cw-area-col2",
"type": "node",
"parent": "PF_pHT-a65sNfpEKlwq7-cw-area",
"label": "chicken_area float NOT NULL"
},
{
"id": "PF_pHT-a65sNfpEKlwq7-321",
"type": "node",
@@ -2537,4 +2553,4 @@
]
}
]
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

After

Width:  |  Height:  |  Size: 337 KiB

+6 -4
View File
@@ -147,8 +147,9 @@ CREATE INDEX IF NOT EXISTS idx_cycles_close_requested_by_id ON cycles (close_req
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS flock (
flock_id SERIAL PRIMARY KEY,
flock_name VARCHAR(30) NOT NULL,
kandang_id INTEGER NOT NULL REFERENCES kandang (kandang_id) ON DELETE CASCADE,
flock_name VARCHAR(30) NOT NULL,
iot_api_url VARCHAR(500) NOT NULL DEFAULT '',
kandang_id INTEGER NOT NULL REFERENCES kandang (kandang_id) ON DELETE CASCADE,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
);
@@ -259,8 +260,9 @@ CREATE TABLE IF NOT EXISTS chicken_weight (
doc_weight INTEGER NOT NULL,
average_weight FLOAT NOT NULL,
chicken_count INTEGER NOT NULL,
uniformity FLOAT NOT NULL,
average_daily_gain FLOAT NOT NULL,
uniformity FLOAT NOT NULL,
average_daily_gain FLOAT NOT NULL,
chicken_area FLOAT NOT NULL DEFAULT 0.0,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
cycle_id INTEGER NOT NULL REFERENCES cycles (cycle_id) ON DELETE CASCADE
+16 -2
View File
@@ -154,6 +154,7 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
age: fallback.age,
uniformity: fallback.uniformity,
average_daily_gain: fallback.average_daily_gain,
chicken_area: fallback.chicken_area,
cycle: fallback.cycle,
kandang: fallback.kandang,
};
@@ -322,7 +323,15 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
<table className="min-w-full text-sm">
<thead className="bg-gray-50 text-gray-600">
<tr>
{['Tanggal', 'Umur', 'DOC', 'AVG', 'Uniformity', 'ADG'].map((h) => (
{[
'Tanggal',
'Umur',
'DOC',
'AVG',
'Uniformity',
'ADG',
'Luas Ayam (IOT)',
].map((h) => (
<th key={h} className="px-4 py-3 text-left font-semibold">
{h}
</th>
@@ -332,7 +341,7 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
<tbody>
{rows.length === 0 ? (
<tr>
<td colSpan={6} className="px-4 py-6 text-center text-gray-400">
<td colSpan={7} className="px-4 py-6 text-center text-gray-400">
Belum ada data berat untuk siklus ini.
</td>
</tr>
@@ -351,6 +360,11 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
<td className="px-4 py-2">
{formatNumber(row.average_daily_gain, 2, 2, true)}
</td>
<td className="px-4 py-2">
{row.chicken_area && row.chicken_area > 0
? `${formatNumber(row.chicken_area, 2, 0, true)} cm²`
: '-'}
</td>
</tr>
))
)}
+9
View File
@@ -86,6 +86,15 @@ const DailyDetailPanel: React.FC<DailyDetailPanelProps> = ({ row, docWeight }) =
value={`${formatNumber(row.chicken_count, 0)} ekor`}
icon={<i className="fa-solid fa-users"></i>}
/>
<DetailCard
label="Luas Ayam (IOT)"
value={
row.chicken_area && row.chicken_area > 0
? `${formatNumber(row.chicken_area, 2, 0, true)} cm²`
: '-'
}
icon={<i className="fa-solid fa-vector-square"></i>}
/>
{deviation != null && (
<DetailCard
label="Deviasi vs Target"
+2
View File
@@ -161,6 +161,7 @@ export type ChickenWeight = {
chicken_count: number;
uniformity: number;
average_daily_gain: number;
chicken_area?: number;
created_at: string;
updated_at: string;
};
@@ -171,6 +172,7 @@ export type LatestAvg = {
age: number;
uniformity: number;
average_daily_gain: number;
chicken_area?: number;
cycle: number;
kandang: number;
};
+9 -5
View File
@@ -1,17 +1,21 @@
export function formatNumber(
value: number,
value: number | string | null | undefined,
maxDecimals = 2,
minDecimals = 0,
truncate = false
): string {
let display = value;
const num = typeof value === 'number' ? value : Number(value);
if (!Number.isFinite(num)) return '-';
const safeMax = Math.max(0, maxDecimals);
const safeMin = Math.min(Math.max(0, minDecimals), safeMax);
let display = num;
if (truncate) {
const factor = 10 ** maxDecimals;
const factor = 10 ** safeMax;
display = Math.trunc(display * factor) / factor;
}
return display.toLocaleString('id-ID', {
maximumFractionDigits: maxDecimals,
minimumFractionDigits: minDecimals,
maximumFractionDigits: safeMax,
minimumFractionDigits: safeMin,
});
}