배터리 잔여 시간 예측 정밀도 개선
This commit is contained in:
+213
@@ -737,6 +737,67 @@ function clamp_float(float $value, float $min, float $max): float
|
||||
return max($min, min($max, $value));
|
||||
}
|
||||
|
||||
function weighted_linear_regression(array $points): ?array
|
||||
{
|
||||
$weightSum = 0.0;
|
||||
$xSum = 0.0;
|
||||
$ySum = 0.0;
|
||||
|
||||
foreach ($points as $point) {
|
||||
$weight = max(0.0001, (float)($point['weight'] ?? 1.0));
|
||||
$x = (float)$point['x'];
|
||||
$y = (float)$point['y'];
|
||||
$weightSum += $weight;
|
||||
$xSum += $x * $weight;
|
||||
$ySum += $y * $weight;
|
||||
}
|
||||
|
||||
if ($weightSum <= 0 || count($points) < 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$xMean = $xSum / $weightSum;
|
||||
$yMean = $ySum / $weightSum;
|
||||
$cov = 0.0;
|
||||
$var = 0.0;
|
||||
|
||||
foreach ($points as $point) {
|
||||
$weight = max(0.0001, (float)($point['weight'] ?? 1.0));
|
||||
$dx = (float)$point['x'] - $xMean;
|
||||
$dy = (float)$point['y'] - $yMean;
|
||||
$cov += $weight * $dx * $dy;
|
||||
$var += $weight * $dx * $dx;
|
||||
}
|
||||
|
||||
if ($var <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$slope = $cov / $var;
|
||||
return [
|
||||
'slope' => $slope,
|
||||
'intercept' => $yMean - ($slope * $xMean),
|
||||
'weight_sum' => $weightSum,
|
||||
];
|
||||
}
|
||||
|
||||
function numeric_mad(array $values): ?float
|
||||
{
|
||||
$median = numeric_median($values);
|
||||
if ($median === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$deviations = [];
|
||||
foreach ($values as $value) {
|
||||
if ($value !== null && $value !== '' && is_numeric($value)) {
|
||||
$deviations[] = abs((float)$value - $median);
|
||||
}
|
||||
}
|
||||
|
||||
return numeric_median($deviations);
|
||||
}
|
||||
|
||||
function battery_trend_history(int $hours = 24): array
|
||||
{
|
||||
$hours = max(1, min(48, $hours));
|
||||
@@ -951,6 +1012,112 @@ function battery_trend_candidate(array $trendRows, float $currentPercent, int $w
|
||||
];
|
||||
}
|
||||
|
||||
function battery_regression_trend_candidate(array $trendRows, float $currentPercent, int $windowSeconds, ?float $recentWatts): ?array
|
||||
{
|
||||
if (count($trendRows) < 10) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$lastTime = (int)$trendRows[count($trendRows) - 1]['time'];
|
||||
$rows = array_values(array_filter($trendRows, static function (array $row) use ($lastTime, $windowSeconds): bool {
|
||||
return (int)$row['time'] >= ($lastTime - $windowSeconds)
|
||||
&& is_numeric($row['battery_percent'] ?? null);
|
||||
}));
|
||||
|
||||
$count = count($rows);
|
||||
if ($count < 10) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$firstTime = (int)$rows[0]['time'];
|
||||
$lastRowTime = (int)$rows[$count - 1]['time'];
|
||||
$elapsed = max(1, $lastRowTime - $firstTime);
|
||||
if ($elapsed < 900) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$points = [];
|
||||
$lastIndex = max(1, $count - 1);
|
||||
foreach ($rows as $index => $row) {
|
||||
$soc = (float)$row['battery_percent'];
|
||||
$samples = max(1, (int)($row['samples'] ?? 1));
|
||||
$recencyWeight = 0.65 + (0.35 * ($index / $lastIndex));
|
||||
$sampleWeight = min(1.0, sqrt($samples) / 4);
|
||||
$points[] = [
|
||||
'x' => max(0, (int)$row['time'] - $firstTime),
|
||||
'y' => $soc,
|
||||
'weight' => $recencyWeight * max(0.35, $sampleWeight),
|
||||
];
|
||||
}
|
||||
|
||||
$fit = weighted_linear_regression($points);
|
||||
if ($fit === null || (float)$fit['slope'] >= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$residuals = [];
|
||||
foreach ($points as $point) {
|
||||
$residuals[] = (float)$point['y'] - ((float)$fit['intercept'] + ((float)$fit['slope'] * (float)$point['x']));
|
||||
}
|
||||
|
||||
$mad = numeric_mad($residuals);
|
||||
$residualLimit = max(0.08, ($mad ?? 0.0) * 3.5);
|
||||
$filtered = [];
|
||||
foreach ($points as $index => $point) {
|
||||
if (abs($residuals[$index]) <= $residualLimit) {
|
||||
$filtered[] = $point;
|
||||
}
|
||||
}
|
||||
|
||||
if (count($filtered) >= max(8, (int)floor($count * 0.62))) {
|
||||
$refit = weighted_linear_regression($filtered);
|
||||
if ($refit !== null && (float)$refit['slope'] < 0) {
|
||||
$fit = $refit;
|
||||
$points = $filtered;
|
||||
}
|
||||
}
|
||||
|
||||
$rate = -(float)$fit['slope'];
|
||||
$drop = $rate * $elapsed;
|
||||
$minDrop = $windowSeconds <= 3600 ? 0.10 : 0.18;
|
||||
if ($rate <= 0 || $drop < $minDrop) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$windowWatts = numeric_trimmed_average(array_column($rows, 'cpu_watts'), 0.1);
|
||||
$loadFactor = 1.0;
|
||||
if ($recentWatts !== null && $recentWatts > 0 && $windowWatts !== null && $windowWatts > 0) {
|
||||
$loadFactor = 1 + ((clamp_float($recentWatts / $windowWatts, 0.7, 1.65) - 1) * 0.38);
|
||||
$rate *= $loadFactor;
|
||||
}
|
||||
|
||||
$seconds = (int)round($currentPercent / $rate);
|
||||
if ($seconds <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$keptRatio = count($points) / $count;
|
||||
$confidence = min(1.0, $elapsed / $windowSeconds)
|
||||
* min(1.0, $drop / 1.0)
|
||||
* min(1.0, $count / 120)
|
||||
* clamp_float($keptRatio, 0.45, 1.0);
|
||||
|
||||
return [
|
||||
'seconds' => $seconds,
|
||||
'rate_per_second' => $rate,
|
||||
'drop_percent' => round($drop, 3),
|
||||
'elapsed_seconds' => $elapsed,
|
||||
'window_seconds' => $windowSeconds,
|
||||
'sample_count' => $count,
|
||||
'confidence' => round($confidence, 4),
|
||||
'weight' => max(0.05, $confidence * 1.18),
|
||||
'avg_watts' => $windowWatts === null ? null : round($windowWatts, 3),
|
||||
'load_factor' => round($loadFactor, 3),
|
||||
'source' => 'robust_regression',
|
||||
'kept_ratio' => round($keptRatio, 3),
|
||||
];
|
||||
}
|
||||
|
||||
function battery_learned_profile_candidate(array $profileRows, float $currentPercent, ?float $recentWatts): ?array
|
||||
{
|
||||
if (count($profileRows) < 24) {
|
||||
@@ -1054,6 +1221,43 @@ function battery_learned_profile_candidate(array $profileRows, float $currentPer
|
||||
];
|
||||
}
|
||||
|
||||
function refine_battery_candidates(array $candidates): array
|
||||
{
|
||||
if (count($candidates) < 3) {
|
||||
return $candidates;
|
||||
}
|
||||
|
||||
$rates = array_values(array_filter(
|
||||
array_map(static fn(array $candidate): float => (float)($candidate['rate_per_second'] ?? 0), $candidates),
|
||||
static fn(float $rate): bool => $rate > 0
|
||||
));
|
||||
$medianRate = numeric_median($rates);
|
||||
if ($medianRate === null || $medianRate <= 0) {
|
||||
return $candidates;
|
||||
}
|
||||
|
||||
$refined = [];
|
||||
foreach ($candidates as $candidate) {
|
||||
$rate = (float)($candidate['rate_per_second'] ?? 0);
|
||||
if ($rate <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$ratio = $rate / $medianRate;
|
||||
if ($ratio < 0.38 || $ratio > 2.65) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$distance = abs(log(max(0.001, $ratio)));
|
||||
$stability = 1 / (1 + ($distance * 1.85));
|
||||
$candidate['weight'] = max(0.03, (float)$candidate['weight'] * $stability);
|
||||
$candidate['stability'] = round($stability, 3);
|
||||
$refined[] = $candidate;
|
||||
}
|
||||
|
||||
return $refined !== [] ? $refined : $candidates;
|
||||
}
|
||||
|
||||
function battery_power_fallback_estimate(float $percent, array $history): array
|
||||
{
|
||||
$wattValues = [];
|
||||
@@ -1110,11 +1314,18 @@ function battery_remaining_estimate(array $battery, array $history, array $trend
|
||||
$candidates[] = $candidate;
|
||||
}
|
||||
}
|
||||
foreach ([2700, 7200, 14400, 28800, 57600, 86400] as $windowSeconds) {
|
||||
$candidate = battery_regression_trend_candidate($trendRows, $percent, $windowSeconds, $recentWatts);
|
||||
if ($candidate !== null) {
|
||||
$candidates[] = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
$learned = battery_learned_profile_candidate($profileRows, $percent, $recentWatts);
|
||||
if ($learned !== null) {
|
||||
$candidates[] = $learned;
|
||||
}
|
||||
$candidates = refine_battery_candidates($candidates);
|
||||
|
||||
if ($candidates !== []) {
|
||||
$weightedRate = 0.0;
|
||||
@@ -1148,6 +1359,8 @@ function battery_remaining_estimate(array $battery, array $history, array $trend
|
||||
'load_factor' => $candidate['load_factor'],
|
||||
'intervals' => $candidate['intervals'] ?? null,
|
||||
'soc_buckets' => $candidate['soc_buckets'] ?? null,
|
||||
'kept_ratio' => $candidate['kept_ratio'] ?? null,
|
||||
'stability' => $candidate['stability'] ?? null,
|
||||
];
|
||||
}, $candidates),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user