202 lines
5.5 KiB
TypeScript
202 lines
5.5 KiB
TypeScript
import { cobbStandardPerformance } from '../mockData/cobbStandard.ts';
|
|
|
|
/**
|
|
* Analyze trend of numerical data over a window
|
|
* @param data Array of numerical values
|
|
* @param windowSize Number of recent data points to analyze
|
|
* @returns Trend description in Indonesian
|
|
*/
|
|
export function analyzeTrend(data: number[], windowSize = 7): string {
|
|
if (!data || data.length < 2) {
|
|
return 'tidak cukup data';
|
|
}
|
|
|
|
// Filter out invalid values (null, undefined, NaN, 0)
|
|
const validData = data.filter((val) => val != null && !isNaN(val) && val !== 0);
|
|
|
|
if (validData.length < 2) {
|
|
return 'tidak cukup data';
|
|
}
|
|
|
|
// Take last N values
|
|
const recentData = validData.slice(-Math.min(windowSize, validData.length));
|
|
|
|
if (recentData.length < 2) {
|
|
return 'tidak cukup data';
|
|
}
|
|
|
|
// Calculate simple linear regression slope
|
|
const n = recentData.length;
|
|
const xMean = (n - 1) / 2; // Mean of indices 0, 1, 2, ..., n-1
|
|
const yMean = recentData.reduce((sum, val) => sum + val, 0) / n;
|
|
|
|
let numerator = 0;
|
|
let denominator = 0;
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
numerator += (i - xMean) * ((recentData[i] ?? 0) - yMean);
|
|
denominator += Math.pow(i - xMean, 2);
|
|
}
|
|
|
|
const slope = denominator !== 0 ? numerator / denominator : 0;
|
|
|
|
// Calculate relative slope (as percentage of mean)
|
|
const relativeSlope = yMean !== 0 ? (slope / yMean) * 100 : 0;
|
|
|
|
// Determine trend based on relative slope
|
|
// Threshold: >2% = increasing, <-2% = decreasing, else stable
|
|
if (relativeSlope > 2) {
|
|
return 'meningkat';
|
|
} else if (relativeSlope < -2) {
|
|
return 'menurun';
|
|
} else {
|
|
return 'stabil';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get cycle phase based on day of cycle
|
|
* @param day Current day of cycle (1-based)
|
|
* @returns Phase name
|
|
*/
|
|
export function getCyclePhase(day: number): string {
|
|
if (day <= 14) {
|
|
return 'Brooding';
|
|
} else if (day <= 28) {
|
|
return 'Growth';
|
|
} else {
|
|
return 'Finisher';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get Cobb standard performance metrics for a given day
|
|
* @param day Day of cycle (1-based)
|
|
* @returns Object with FCR and weight, or null if not available
|
|
*/
|
|
export function getCobbStandardForDay(day: number): { fcr: number; weight: number } | null {
|
|
const standard = cobbStandardPerformance.find((entry) => entry.day === day);
|
|
if (standard) {
|
|
return {
|
|
fcr: standard.fcr,
|
|
// Weight targets live in page builders / CP707 knowledge; FCR row has no weight column here.
|
|
weight: 0,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Calculate deviation between actual and target values
|
|
* @param actual Actual value
|
|
* @param target Target value
|
|
* @returns Object with absolute and percentage deviation
|
|
*/
|
|
export function calculateDeviation(
|
|
actual: number,
|
|
target: number
|
|
): { absolute: number; percent: number } {
|
|
const absolute = actual - target;
|
|
const percent = target !== 0 ? (absolute / target) * 100 : 0;
|
|
return { absolute, percent };
|
|
}
|
|
|
|
/**
|
|
* Determine environmental status based on readings and cycle phase
|
|
* @param temp Temperature in Celsius
|
|
* @param humidity Humidity percentage
|
|
* @param ammonia Ammonia in ppm
|
|
* @param day Current day of cycle
|
|
* @returns Status description in Indonesian
|
|
*/
|
|
export function getEnvironmentalStatus(
|
|
temp: number,
|
|
humidity: number,
|
|
ammonia: number,
|
|
day: number
|
|
): string {
|
|
const issues: string[] = [];
|
|
|
|
// Age-appropriate temperature ranges
|
|
// Younger chicks need warmer temperatures
|
|
let optimalTempMin: number;
|
|
let optimalTempMax: number;
|
|
|
|
if (day <= 7) {
|
|
optimalTempMin = 32;
|
|
optimalTempMax = 35;
|
|
} else if (day <= 14) {
|
|
optimalTempMin = 30;
|
|
optimalTempMax = 32;
|
|
} else if (day <= 21) {
|
|
optimalTempMin = 28;
|
|
optimalTempMax = 30;
|
|
} else {
|
|
optimalTempMin = 26;
|
|
optimalTempMax = 28;
|
|
}
|
|
|
|
// Check temperature
|
|
if (temp < optimalTempMin) {
|
|
issues.push('suhu terlalu rendah');
|
|
} else if (temp > optimalTempMax) {
|
|
issues.push('suhu terlalu tinggi');
|
|
}
|
|
|
|
// Check humidity (optimal: 50-70%)
|
|
if (humidity < 50) {
|
|
issues.push('kelembapan rendah');
|
|
} else if (humidity > 70) {
|
|
issues.push('kelembapan tinggi');
|
|
}
|
|
|
|
// Check ammonia (safe: <25ppm, warning: 25-50ppm, critical: >50ppm)
|
|
if (ammonia > 50) {
|
|
issues.push('amonia kritis');
|
|
} else if (ammonia > 25) {
|
|
issues.push('amonia tinggi');
|
|
}
|
|
|
|
if (issues.length === 0) {
|
|
return 'optimal';
|
|
} else if (issues.length === 1) {
|
|
return issues[0] ?? 'optimal';
|
|
} else {
|
|
return issues.join(', ');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Analyze feed consumption trend
|
|
* @param feedHistory Array of daily feed consumption (in sacks)
|
|
* @param _currentDay Current day of cycle (reserved for future use)
|
|
* @returns Trend description in Indonesian
|
|
*/
|
|
export function analyzeFeedTrend(feedHistory: number[], _currentDay: number): string {
|
|
if (!feedHistory || feedHistory.length < 3) {
|
|
return 'tidak cukup data';
|
|
}
|
|
|
|
// Get last 7 days of feed data
|
|
const recentFeed = feedHistory.slice(-7);
|
|
|
|
// Rough expectation: feed consumption should increase gradually as chickens grow
|
|
// Early days: 0.5-1 sack/1000 birds/day
|
|
// Mid cycle: 1-2 sacks/1000 birds/day
|
|
// Late cycle: 2-3 sacks/1000 birds/day
|
|
|
|
// This is a simplified heuristic - in real deployment, this would be calibrated
|
|
// based on actual farm data and population size
|
|
|
|
// For now, just analyze if consumption is increasing (expected as birds grow)
|
|
const trend = analyzeTrend(recentFeed, 7);
|
|
|
|
if (trend === 'meningkat') {
|
|
return 'sesuai pertumbuhan';
|
|
} else if (trend === 'menurun') {
|
|
return 'menurun (perlu perhatian)';
|
|
} else {
|
|
return 'stabil';
|
|
}
|
|
}
|