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] weight = AVG_WEIGHT[i]
prev_weight = AVG_WEIGHT[i - 1] if i > 0 else DOC_IN_WEIGHT prev_weight = AVG_WEIGHT[i - 1] if i > 0 else DOC_IN_WEIGHT
adg = _round(weight - prev_weight, 2) if weight else 0.0 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( cw, _ = ChickenWeight.objects.update_or_create(
cycle=cycle, cycle=cycle,
date=day, date=day,
@@ -293,6 +294,7 @@ class Command(BaseCommand):
"chicken_count": SAMPLE_COUNT[i], "chicken_count": SAMPLE_COUNT[i],
"uniformity": UNIFORMITY[i], "uniformity": UNIFORMITY[i],
"average_daily_gain": adg, "average_daily_gain": adg,
"chicken_area": area_cm2,
}, },
) )
cw_by_date[day] = cw cw_by_date[day] = cw
@@ -147,16 +147,18 @@ class Command(BaseCommand):
cw_by_date = {} cw_by_date = {}
for row in sheets["chicken_weight"]: for row in sheets["chicken_weight"]:
avg_wt = float(as_number(row.get("average_weight")))
obj, _ = ChickenWeight.objects.update_or_create( obj, _ = ChickenWeight.objects.update_or_create(
cycle=cycle, cycle=cycle,
date=row["date"], date=row["date"],
defaults={ defaults={
"age": int(as_number(row.get("age"))), "age": int(as_number(row.get("age"))),
"doc_weight": int(as_number(row.get("doc_weight"))), "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"))), "chicken_count": int(as_number(row.get("chicken_count"))),
"uniformity": float(as_number(row.get("uniformity"))), "uniformity": float(as_number(row.get("uniformity"))),
"average_daily_gain": float(as_number(row.get("average_daily_gain"))), "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 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() chicken_count = models.IntegerField()
uniformity = models.FloatField() uniformity = models.FloatField()
average_daily_gain = 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") cycle = models.ForeignKey(Cycle, on_delete=models.CASCADE, related_name="chicken_weights")
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=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] weights = [float(f.get("predicted_weight_g") or 0) for f in floors]
unifs = [float(f.get("uniformity") 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] 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] = { by_date[day] = {
"average_weight": sum(weights) / len(weights), "average_weight": sum(weights) / len(weights),
"uniformity": sum(unifs) / len(unifs), "uniformity": sum(unifs) / len(unifs),
"chicken_count": max(samples) if samples else 0, "chicken_count": max(samples) if samples else 0,
"chicken_area": round(avg_area, 2),
} }
return by_date return by_date
@@ -309,6 +323,7 @@ def sync_chicken_weight_for_cycle(
uniformity = 0.0 uniformity = 0.0
chicken_count = int(cycle.doc_in_count) chicken_count = int(cycle.doc_in_count)
adg = 0.0 adg = 0.0
chicken_area = 0.0
else: else:
metrics = weight_by_date.get(day) metrics = weight_by_date.get(day)
if metrics is None: if metrics is None:
@@ -317,6 +332,7 @@ def sync_chicken_weight_for_cycle(
uniformity = float(metrics["uniformity"]) uniformity = float(metrics["uniformity"])
chicken_count = int(metrics["chicken_count"]) chicken_count = int(metrics["chicken_count"])
adg = round(average_weight - prev_weight, 2) adg = round(average_weight - prev_weight, 2)
chicken_area = float(metrics.get("chicken_area", 0.0))
defaults = { defaults = {
"age": age, "age": age,
@@ -325,6 +341,7 @@ def sync_chicken_weight_for_cycle(
"chicken_count": chicken_count, "chicken_count": chicken_count,
"uniformity": uniformity, "uniformity": uniformity,
"average_daily_gain": adg, "average_daily_gain": adg,
"chicken_area": chicken_area,
} }
ChickenWeight.objects.update_or_create( ChickenWeight.objects.update_or_create(
cycle=cycle, cycle=cycle,
@@ -175,12 +175,7 @@ erDiagram
int chicken_count "NOT NULL (n_weight_samples)" int chicken_count "NOT NULL (n_weight_samples)"
float uniformity "NOT NULL (Flock weight uniformity %)" float uniformity "NOT NULL (Flock weight uniformity %)"
float average_daily_gain "NOT NULL (ADG in grams/day)" float average_daily_gain "NOT NULL (ADG in grams/day)"
float area_cm2_median "Median body area in cm^2" float chicken_area "NOT NULL DEFAULT 0.0 (Median chicken 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"
timestamp created_at "NOT NULL" timestamp created_at "NOT NULL"
timestamp updated_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). - `chicken_count` $\leftarrow \sum \text{n\_weight\_samples}$ (valid non-outlier tracks).
- `uniformity` $\leftarrow \text{flock size uniformity percentage}$. - `uniformity` $\leftarrow \text{flock size uniformity percentage}$.
- `average_daily_gain` $\leftarrow \text{daily weight gain in grams/day}$. - `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}$. - `chicken_area` $\leftarrow \text{calibrated median chicken body area in cm²}$.
- `gompertz_baseline_g`, `vision_gain_g` $\leftarrow \text{audited growth model residual breakdown}$.
--- ---
+17 -1
View File
@@ -1290,6 +1290,22 @@
"parent": "PF_pHT-a65sNfpEKlwq7-159", "parent": "PF_pHT-a65sNfpEKlwq7-159",
"label": "average_daily_gain float NOT NULL" "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", "id": "PF_pHT-a65sNfpEKlwq7-321",
"type": "node", "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 ( CREATE TABLE IF NOT EXISTS flock (
flock_id SERIAL PRIMARY KEY, flock_id SERIAL PRIMARY KEY,
flock_name VARCHAR(30) NOT NULL, flock_name VARCHAR(30) NOT NULL,
kandang_id INTEGER NOT NULL REFERENCES kandang (kandang_id) ON DELETE CASCADE, 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, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_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, doc_weight INTEGER NOT NULL,
average_weight FLOAT NOT NULL, average_weight FLOAT NOT NULL,
chicken_count INTEGER NOT NULL, chicken_count INTEGER NOT NULL,
uniformity FLOAT NOT NULL, uniformity FLOAT NOT NULL,
average_daily_gain 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, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_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 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, age: fallback.age,
uniformity: fallback.uniformity, uniformity: fallback.uniformity,
average_daily_gain: fallback.average_daily_gain, average_daily_gain: fallback.average_daily_gain,
chicken_area: fallback.chicken_area,
cycle: fallback.cycle, cycle: fallback.cycle,
kandang: fallback.kandang, kandang: fallback.kandang,
}; };
@@ -322,7 +323,15 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
<table className="min-w-full text-sm"> <table className="min-w-full text-sm">
<thead className="bg-gray-50 text-gray-600"> <thead className="bg-gray-50 text-gray-600">
<tr> <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"> <th key={h} className="px-4 py-3 text-left font-semibold">
{h} {h}
</th> </th>
@@ -332,7 +341,7 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
<tbody> <tbody>
{rows.length === 0 ? ( {rows.length === 0 ? (
<tr> <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. Belum ada data berat untuk siklus ini.
</td> </td>
</tr> </tr>
@@ -351,6 +360,11 @@ const WeightMonitoringPage: React.FC<{ onSettings: () => void }> = ({ onSettings
<td className="px-4 py-2"> <td className="px-4 py-2">
{formatNumber(row.average_daily_gain, 2, 2, true)} {formatNumber(row.average_daily_gain, 2, 2, true)}
</td> </td>
<td className="px-4 py-2">
{row.chicken_area && row.chicken_area > 0
? `${formatNumber(row.chicken_area, 2, 0, true)} cm²`
: '-'}
</td>
</tr> </tr>
)) ))
)} )}
+9
View File
@@ -86,6 +86,15 @@ const DailyDetailPanel: React.FC<DailyDetailPanelProps> = ({ row, docWeight }) =
value={`${formatNumber(row.chicken_count, 0)} ekor`} value={`${formatNumber(row.chicken_count, 0)} ekor`}
icon={<i className="fa-solid fa-users"></i>} 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 && ( {deviation != null && (
<DetailCard <DetailCard
label="Deviasi vs Target" label="Deviasi vs Target"
+2
View File
@@ -161,6 +161,7 @@ export type ChickenWeight = {
chicken_count: number; chicken_count: number;
uniformity: number; uniformity: number;
average_daily_gain: number; average_daily_gain: number;
chicken_area?: number;
created_at: string; created_at: string;
updated_at: string; updated_at: string;
}; };
@@ -171,6 +172,7 @@ export type LatestAvg = {
age: number; age: number;
uniformity: number; uniformity: number;
average_daily_gain: number; average_daily_gain: number;
chicken_area?: number;
cycle: number; cycle: number;
kandang: number; kandang: number;
}; };
+9 -5
View File
@@ -1,17 +1,21 @@
export function formatNumber( export function formatNumber(
value: number, value: number | string | null | undefined,
maxDecimals = 2, maxDecimals = 2,
minDecimals = 0, minDecimals = 0,
truncate = false truncate = false
): string { ): 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) { if (truncate) {
const factor = 10 ** maxDecimals; const factor = 10 ** safeMax;
display = Math.trunc(display * factor) / factor; display = Math.trunc(display * factor) / factor;
} }
return display.toLocaleString('id-ID', { return display.toLocaleString('id-ID', {
maximumFractionDigits: maxDecimals, maximumFractionDigits: safeMax,
minimumFractionDigits: minDecimals, minimumFractionDigits: safeMin,
}); });
} }