배터리 잔여 시간 예측 정밀도 개선

This commit is contained in:
seo
2026-06-26 02:46:35 +09:00
parent b0714f9ff2
commit 80502d753d
3 changed files with 234 additions and 8 deletions
+213
View File
@@ -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),
];