adjust ai insight and seed data

This commit is contained in:
Alberto-Audrix committed 2026-09-18 11:34:07 +07:00
1 parent 68e0d1a0bb
commit a2b5b10977
18 files changed
+1835 -140

No files matched your search

@@ -17,28 +17,28 @@ Every daily series is hand-tuned so the numbers stay internally consistent:
57.47 t live weight, avg ~1,412 g/bird at ~33.1 days; tail harvests leave
fewer than 50 birds in the house at close-out.
* ``kpi`` / ``manual`` – FCR/EEF/lot-weight follow the same formulas as the
Kandang 2 preview:
Kandang 2 preview / ``kpi_rollups``:
FCR = feed_total_kg / (harvest_weight_total_g + lot_weight_g)
EEF = life_pct * (avg_harvest_weight/1000) / (FCR * avg_harvest_day) * 100
lot = chicken_life * average_weight
* ``iot_panel`` – derived from the real Kandang 2 sensor extract with
deterministic per-house jitter (same site, same season).
chicken_life (KPI) = DOC − mortality (excludes harvest; matches kpi_rollups)
stock_akhir = chicken_life − harvest_total (in-house remaining)
lot = stock_akhir * average_weight
iot_weight (KPI) = average_weight g/bird (not lot total; rollups multiply by stock)
* ``iot_panel`` – 10-minute readings via ``seed_iot_10min`` (real Kandang 2
Lantai 1 extract + deterministic per-house jitter).
"""
from __future__ import annotations
import json
import math
import random
from datetime import date, timedelta
from pathlib import Path
from django.core.management.base import BaseCommand, CommandError
from django.core.management import call_command
from django.core.management.base import BaseCommand
from django.db import transaction
from apps.accounts.models import User
from apps.farms.models import Cycle, Flock, Kandang, Site
from apps.jobs.sukawarna_preview import IOT_EXTRACT_JSON
from apps.operations.models import (
ChickenCounting,
ChickenWeight,
@@ -127,7 +127,7 @@ OUT_TODAY = [0] * 42 + [3, 4, 5, 3, 2, 2, 1]
# --- Harvest plan: age -> (birds, avg live weight g/bird) --------------------
# 20 partial harvests day 29-48; the tail harvests clear out the house so the
# leftover flock (chicken_life) at close-out stays under 50 birds.
# leftover flock (stock_akhir) at close-out stays under 50 birds.
HARVEST = {
29: (4_500, 1_310),
30: (5_000, 1_360),
@@ -163,18 +163,17 @@ class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument("--password", default=DEFAULT_PASSWORD)
parser.add_argument("--iot-json", default=str(IOT_EXTRACT_JSON))
parser.add_argument("--no-reset", action="store_true")
parser.add_argument("--jitter-seed", type=int, default=7)
parser.add_argument(
"--skip-iot-10min",
action="store_true",
help="Skip 10-minute IoT panel seeding (default: run seed_iot_10min)",
)
@transaction.atomic
def handle(self, *args, **options):
iot_path = Path(options["iot_json"])
if not iot_path.is_file():
raise CommandError(f"IoT extract not found: {iot_path}")
iot = json.loads(iot_path.read_text(encoding="utf-8"))
password = options["password"]
rng = random.Random(options["jitter_seed"])
user, created = User.objects.get_or_create(
user_name="staff",
@@ -329,10 +328,12 @@ class Command(BaseCommand):
mort_today = MORTALITY[i]
mort_total_after = mort_total + mort_today
life = DOC_IN_COUNT - mort_total_after - harvest_total
# Liveability for EEF: birds still accounted for (DOC minus mortality),
# not remaining in-house stock (which drops during harvest).
life_pct = (DOC_IN_COUNT - mort_total_after) / DOC_IN_COUNT * 100.0 if DOC_IN_COUNT else 0.0
# Matches kpi_rollups: chicken_life excludes harvest; stock_akhir is in-house.
chicken_life = DOC_IN_COUNT - mort_total_after
stock_akhir = max(chicken_life - harvest_total, 0)
life_pct = (
chicken_life / DOC_IN_COUNT * 100.0 if DOC_IN_COUNT else 0.0
)
# Running averages: flock weight before the first harvest, then the
# harvest-weighted running mean carried forward on idle days.
if harvest_today:
@@ -345,7 +346,7 @@ class Command(BaseCommand):
avg_hday = harvest_day_accum / harvest_total
avg_hwt = harvest_wt_accum / harvest_total
lot_weight_g = life * weight if weight else 0.0
lot_weight_g = stock_akhir * weight if weight else 0.0
denom = harvest_weight_total + lot_weight_g
fcr = (feed_total * 50_000) / denom if denom > 0 else 0.0
if avg_hday > 0 and fcr > 0:
@@ -393,9 +394,9 @@ class Command(BaseCommand):
"harvest_total": harvest_total,
"harvest_weight": float(harvest_today * harvest_wt_today),
"harvest_weight_total": harvest_weight_total,
"chicken_life": life,
"chicken_life": chicken_life,
"chicken_life_percentage": _round(life_pct, 3),
"iot_weight": _round(lot_weight_g, 2),
"iot_weight": _round(weight, 2),
"average_harvest_day": _round(avg_hday, 4),
"average_harvest_weight": _round(avg_hwt, 4),
"fcr": _round(fcr, 4),
@@ -409,30 +410,18 @@ class Command(BaseCommand):
mort_total = mort_total_after
# --- IoT panel: Kandang 2 sensors with per-house jitter -------------
panel_count = 0
for flock_block in iot["flocks"]:
flock = flocks_by_name[flock_block["name"]]
for row in flock_block["rows"]:
day = date.fromisoformat(row["date"])
avg_temp = float(row["average_temperature"]) + rng.uniform(-0.4, 0.4)
humidity = min(max(float(row["humidity"]) + rng.uniform(-3.0, 3.0), 0.0), 100.0)
wind = max(float(row["wind_speed"]) + rng.uniform(-0.1, 0.1), 0.0)
water = max(float(row["water_total"]) * 0.9 + rng.uniform(-120.0, 120.0), 0.0)
chill = float(row["chill_factor"])
exp_temp = avg_temp - (70.0 - humidity) / 5.0 - wind * chill
IotPanel.objects.update_or_create(
flock=flock,
date=day,
defaults={
"wind_speed": _round(wind, 2),
"humidity": _round(humidity, 1),
"water_total": _round(water, 1),
"average_temperature": _round(avg_temp, 2),
"experience_temperature": _round(exp_temp, 4),
},
)
panel_count += 1
if not options["skip_iot_10min"]:
call_command(
"seed_iot_10min",
cycle_start=START_DATE.isoformat(),
jitter_seed=options["jitter_seed"],
no_reset=options["no_reset"],
)
panel_count = IotPanel.objects.filter(
flock__kandang=kandang,
date__gte=cycle.start_date,
date__lte=cycle.end_date,
).count()
last_kpi = KPI.objects.filter(cycle=cycle).order_by("-date").first()
self.stdout.write(
@@ -1,9 +1,9 @@
from __future__ import annotations
import json
from datetime import date
from pathlib import Path
from django.core.management import call_command
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
@@ -11,7 +11,6 @@ from apps.accounts.models import User
from apps.farms.models import Cycle, Flock, Kandang, Site
from apps.jobs.sukawarna_preview import (
FEED_REFERENCE_XLSX,
IOT_EXTRACT_JSON,
PREVIEW_XLSX,
as_number,
load_feed_reference,
@@ -40,22 +39,22 @@ class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument("--password", default=DEFAULT_PASSWORD)
parser.add_argument("--xlsx", default=str(PREVIEW_XLSX))
parser.add_argument("--iot-json", default=str(IOT_EXTRACT_JSON))
parser.add_argument("--feed-reference", default=str(FEED_REFERENCE_XLSX))
parser.add_argument("--no-reset", action="store_true")
parser.add_argument(
"--skip-iot-10min",
action="store_true",
help="Skip 10-minute IoT panel seeding (default: run seed_iot_10min)",
)
@transaction.atomic
def handle(self, *args, **options):
xlsx = Path(options["xlsx"])
iot_path = Path(options["iot_json"])
feed_reference_path = Path(options["feed_reference"])
if not xlsx.is_file():
raise CommandError(f"Preview not found: {xlsx}")
if not iot_path.is_file():
raise CommandError(f"IoT extract not found: {iot_path}")
sheets = load_preview(xlsx)
iot = json.loads(iot_path.read_text(encoding="utf-8"))
feed_reference = {}
if feed_reference_path.is_file():
feed_reference = load_feed_reference(feed_reference_path)
@@ -257,9 +256,13 @@ class Command(BaseCommand):
"harvest_total": int(as_number(row.get("panen_total"))),
"harvest_weight": float(as_number(row.get("harvest_weight_g"))),
"harvest_weight_total": float(as_number(row.get("harvest_weight_total_g"))),
"chicken_life": int(as_number(row.get("chicken_life"))),
# Preview `chicken_life` is in-house remaining; KPI stores
# DOC − mortality (matches kpi_rollups / FCR stock_akhir math).
"chicken_life": int(as_number(row.get("chicken_life")))
+ int(as_number(row.get("panen_total"))),
"chicken_life_percentage": float(as_number(row.get("chicken_life_pct"))),
"iot_weight": float(as_number(row.get("lot_weight_g"))),
# Per-bird avg g (matches kpi_rollups); not lot_weight_g total.
"iot_weight": float(cw_by_date[day].average_weight),
"average_harvest_day": float(as_number(row.get("avg_harvest_day"))),
"average_harvest_weight": float(as_number(row.get("avg_harvest_weight_g"))),
"fcr": float(as_number(row.get("fcr"))),
@@ -275,23 +278,17 @@ class Command(BaseCommand):
if missing:
raise CommandError(f"KPI dates missing source rows: {missing}")
panel_count = 0
for flock_block in iot["flocks"]:
flock = flocks_by_name[flock_block["name"]]
for row in flock_block["rows"]:
day = date.fromisoformat(row["date"])
IotPanel.objects.update_or_create(
flock=flock,
date=day,
defaults={
"wind_speed": float(row["wind_speed"]),
"humidity": float(row["humidity"]),
"water_total": float(row["water_total"]),
"average_temperature": float(row["average_temperature"]),
"experience_temperature": float(row["experience_temperature"]),
},
)
panel_count += 1
if not options["skip_iot_10min"]:
call_command(
"seed_iot_10min",
cycle_start=start_date.isoformat(),
no_reset=options["no_reset"],
)
panel_count = IotPanel.objects.filter(
flock__kandang=kandang,
date__gte=cycle.start_date,
date__lte=cycle.end_date,
).count()
last_kpi = KPI.objects.filter(cycle=cycle).order_by("-date").first()
self.stdout.write(