adjust ai insight and seed data
This commit is contained in:
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(
|
||||
|
||||
Reference in new issue
Block a user