// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/power/auto_screen_brightness/gaussian_trainer.h"
#include <algorithm>
#include <cmath>
#include <limits>
#include "ash/constants/ash_features.h"
#include "base/logging.h"
#include "base/metrics/field_trial_params.h"
#include "base/metrics/histogram_functions.h"
#include "base/metrics/histogram_macros.h"
#include "chrome/browser/ash/power/auto_screen_brightness/utils.h"
namespace ash {
namespace power {
namespace auto_screen_brightness {
namespace {
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused.
// Logs whether a new brightness exceeded the reasonable distance from the old
// brightness. A reasonable distance is defined by the params
// |brightness_step_size| and |model_brightness_step_size|.
enum class BoundedBrightnessChange {
// User's chosen new brightness is within their [lower_bound, upper_bound].
kUserWithinBounds = 0,
// Target brightness has a reasonable distance model's predicted brightness.
kModelWithinBounds = 1,
// User's chosen new brightness is below their lower bound.
kUserLower = 2,
// User's chosen new brightness is above their upper bound.
kUserUpper = 3,
// Target brightness is below model's predicted brightness and exceeded the
// bound.
kModelLower = 4,
// Target brightness is above model's predicted brightness and exceeded the
// bound.
kModelUpper = 5,
kMaxValue = kModelUpper
};
// Returns a |BoundedBrightnessChange| to be logged to UMA.
// |is_lower_bound_exceeded| is nullopt if the new brightness is within the
// bounds.
BoundedBrightnessChange GetBoundedBrightnessChange(
std::optional<bool> is_lower_bound_exceeded,
bool is_user) {
if (!is_lower_bound_exceeded.has_value()) {
if (is_user) {
return BoundedBrightnessChange::kUserWithinBounds;
}
return BoundedBrightnessChange::kModelWithinBounds;
}
if (*is_lower_bound_exceeded) {
if (is_user) {
return BoundedBrightnessChange::kUserLower;
}
return BoundedBrightnessChange::kModelLower;
}
if (is_user) {
return BoundedBrightnessChange::kUserUpper;
}
return BoundedBrightnessChange::kModelUpper;
}
constexpr double kTol = 1e-10;
// Calculates lower bound from |reference_brightness| using the min of
// 1. Division by a scaling factor and
// 2. Subtraction of an offset.
double BrightnessLowerBound(double reference_brightness,
double scale,
double offset) {
DCHECK_GT(scale, 0.0);
DCHECK_GE(offset, 0.0);
return std::clamp(reference_brightness / scale, 0.0,
std::max(reference_brightness - offset, 0.0));
}
// Calculates upper bound from |reference_brightness| using the max of
// 1. Multiplication by a scaling factor and
// 2. Addition of an offset.
// The upper bound is also capped at 100.0.
double BrightnessUpperBound(double reference_brightness,
double scale,
double offset) {
DCHECK_GT(scale, 0.0);
DCHECK_GE(offset, 0.0);
return std::clamp(reference_brightness * scale,
std::min(reference_brightness + offset, 100.0), 100.0);
}
// Returns whether |brightness| is an outlier from a |reference_brightness|.
bool IsBrightnessOutlier(double brightness,
double reference_brightness,
const GaussianTrainer::Params& params) {
DCHECK_GE(reference_brightness, 0.0);
DCHECK_LE(reference_brightness, 100.0);
return brightness < BrightnessLowerBound(reference_brightness,
params.brightness_bound_scale,
params.brightness_bound_offset) ||
brightness > BrightnessUpperBound(reference_brightness,
params.brightness_bound_scale,
params.brightness_bound_offset);
}
// User's selected |brightness_new| may not be the value that the user needs for
// various reasons, e.g. they could overshoot. Hence this function calculates
// the bounded brightness change based on a heuristic magnitude. The new
// brightness is bounded within a factor of 1+|brightness_step_size| from
// |brightness_old|.
double BoundedBrightnessAdjustment(double brightness_old,
double brightness_new,
double brightness_step_size,
bool is_user) {
const double lower_bound = brightness_old / (1.0 + brightness_step_size);
const double upper_bound = brightness_old * (1.0 + brightness_step_size);
const bool exceeded_upper = brightness_new > upper_bound;
const bool exceeded_lower = brightness_new < lower_bound;
const BoundedBrightnessChange change = GetBoundedBrightnessChange(
exceeded_lower || exceeded_upper ? std::optional<bool>(exceeded_lower)
: std::nullopt,
is_user);
UMA_HISTOGRAM_ENUMERATION(
"AutoScreenBrightness.ModelTraining.BrightnessChange", change);
return std::clamp(brightness_new, lower_bound, upper_bound) - brightness_old;
}
// Calculates recommended brightness change, given old brightness, user's
// selected new brghtness and model's predicted brightness.
double ModelPredictionAdjustment(double brightness_old,
double brightness_new,
double model_brightness,
const GaussianTrainer::Params& params) {
DCHECK_GE(brightness_old, 0.0);
DCHECK_LE(brightness_old, 100.0);
DCHECK_GE(brightness_new, 0.0);
DCHECK_LE(brightness_new, 100.0);
DCHECK_GE(model_brightness, 0.0);
DCHECK_LE(model_brightness, 100.0);
const double bounded_user_adjustment = BoundedBrightnessAdjustment(
brightness_old, brightness_new, params.brightness_step_size,
true /* is_user */);
DCHECK_GE(bounded_user_adjustment, -100.0);
DCHECK_LE(bounded_user_adjustment, 100.0);
const double target_brightness = brightness_old + bounded_user_adjustment;
DCHECK_GE(target_brightness, 0.0);
DCHECK_LE(target_brightness, 100.0);
// Check if model prediction and user adjustment are consistent.
const bool is_consistent =
(model_brightness >= target_brightness && bounded_user_adjustment >= 0) ||
(model_brightness <= target_brightness && bounded_user_adjustment <= 0);
UMA_HISTOGRAM_BOOLEAN(
"AutoScreenBrightness.ModelTraining.ModelUserConsistent", is_consistent);
// If model's prediction is consistent with user's selection, then no
// brightness change will be necessary.
if (is_consistent) {
return 0.0;
}
// Model prediction is incorrect, calculate the change we need to make by
// treating |model_brightness| as the old brightness and |target_brightness|
// as the new brightness.
return BoundedBrightnessAdjustment(model_brightness, target_brightness,
params.model_brightness_step_size,
false /* is_user */);
}
double Gaussian(double x, double sigma) {
double xs = x / sigma;
return std::exp(-xs * xs);
}
void LogModelCurveError(double error, bool model_updated) {
DCHECK_GE(error, 0.0);
const std::string histogram_name =
std::string("AutoScreenBrightness.ModelTraining.Inaccuracy.") +
(model_updated ? "Update" : "NoUpdate");
base::UmaHistogramPercentageObsoleteDoNotUse(histogram_name,
std::round(error));
}
} // namespace
TrainingResult::TrainingResult() = default;
TrainingResult::TrainingResult(
const std::optional<MonotoneCubicSpline>& new_curve,
double error)
: new_curve(new_curve), error(error) {}
TrainingResult::TrainingResult(const TrainingResult& result) = default;
TrainingResult::~TrainingResult() = default;
GaussianTrainer::Params::Params() = default;
GaussianTrainer::GaussianTrainer() {
params_.brightness_bound_scale = GetFieldTrialParamByFeatureAsDouble(
features::kAutoScreenBrightness, "brightness_bound_scale",
params_.brightness_bound_scale);
if (params_.brightness_bound_scale <= 0.0) {
valid_params_ = false;
LogParameterError(ParameterError::kModelError);
return;
}
params_.brightness_bound_offset = GetFieldTrialParamByFeatureAsDouble(
features::kAutoScreenBrightness, "brightness_bound_offset",
params_.brightness_bound_offset);
if (params_.brightness_bound_offset < 0.0) {
valid_params_ = false;
LogParameterError(ParameterError::kModelError);
return;
}
params_.brightness_step_size = GetFieldTrialParamByFeatureAsDouble(
features::kAutoScreenBrightness, "brightness_step_size",
params_.brightness_step_size);
if (params_.brightness_step_size <= 0.0) {
valid_params_ = false;
LogParameterError(ParameterError::kModelError);
return;
}
params_.model_brightness_step_size = GetFieldTrialParamByFeatureAsDouble(
features::kAutoScreenBrightness, "model_brightness_step_size",
params_.model_brightness_step_size);
if (params_.model_brightness_step_size <= 0.0) {
valid_params_ = false;
LogParameterError(ParameterError::kModelError);
return;
}
params_.sigma = GetFieldTrialParamByFeatureAsDouble(
features::kAutoScreenBrightness, "sigma", params_.sigma);
if (params_.sigma <= 0.0) {
valid_params_ = false;
LogParameterError(ParameterError::kModelError);
return;
}
params_.low_log_lux_threshold = GetFieldTrialParamByFeatureAsDouble(
features::kAutoScreenBrightness, "low_log_lux_threshold",
params_.low_log_lux_threshold);
params_.high_log_lux_threshold = GetFieldTrialParamByFeatureAsDouble(
features::kAutoScreenBrightness, "high_log_lux_threshold",
params_.high_log_lux_threshold);
if (params_.low_log_lux_threshold >= params_.high_log_lux_threshold) {
valid_params_ = false;
LogParameterError(ParameterError::kModelError);
return;
}
params_.min_grad_low_lux = GetFieldTrialParamByFeatureAsDouble(
features::kAutoScreenBrightness, "min_grad_low_lux",
params_.min_grad_low_lux);
params_.min_grad_high_lux = GetFieldTrialParamByFeatureAsDouble(
features::kAutoScreenBrightness, "min_grad_high_lux",
params_.min_grad_high_lux);
params_.min_grad = GetFieldTrialParamByFeatureAsDouble(
features::kAutoScreenBrightness, "min_grad", params_.min_grad);
params_.max_grad = GetFieldTrialParamByFeatureAsDouble(
features::kAutoScreenBrightness, "max_grad", params_.max_grad);
if (params_.min_grad_low_lux < 0.0 || params_.min_grad_low_lux >= 1.0) {
valid_params_ = false;
LogParameterError(ParameterError::kModelError);
return;
}
if (params_.min_grad_high_lux < 0.0 || params_.min_grad_high_lux >= 1.0) {
valid_params_ = false;
LogParameterError(ParameterError::kModelError);
return;
}
if (params_.min_grad < 0.0 || params_.min_grad >= 1.0) {
valid_params_ = false;
LogParameterError(ParameterError::kModelError);
return;
}
if (params_.min_grad < params_.min_grad_low_lux) {
valid_params_ = false;
LogParameterError(ParameterError::kModelError);
return;
}
if (params_.min_grad < params_.min_grad_high_lux) {
valid_params_ = false;
LogParameterError(ParameterError::kModelError);
return;
}
if (params_.max_grad < 1.0) {
valid_params_ = false;
LogParameterError(ParameterError::kModelError);
return;
}
params_.min_brightness = GetFieldTrialParamByFeatureAsDouble(
features::kAutoScreenBrightness, "min_brightness",
params_.min_brightness);
if (params_.min_brightness < 0.0) {
valid_params_ = false;
LogParameterError(ParameterError::kModelError);
return;
}
}
GaussianTrainer::~GaussianTrainer() = default;
bool GaussianTrainer::HasValidConfiguration() const {
return valid_params_;
}
bool GaussianTrainer::SetInitialCurves(
const MonotoneCubicSpline& global_curve,
const MonotoneCubicSpline& current_curve) {
DCHECK(valid_params_);
// This function could be called again if the caller wants to reset the
// curves.
global_curve_.emplace(global_curve);
current_curve_.emplace(current_curve);
ambient_log_lux_ = current_curve_->GetControlPointsX();
brightness_ = current_curve_->GetControlPointsY();
const size_t num_points = ambient_log_lux_.size();
// Global curve and personal curve should have the same ambient log lux.
const std::vector<double> global_log_lux = global_curve_->GetControlPointsX();
DCHECK_EQ(global_log_lux.size(), num_points);
for (size_t i = 0; i < num_points; ++i) {
DCHECK_LE(std::abs(global_log_lux[i] - ambient_log_lux_[i]), kTol);
}
// Calculate |min_ratios_| and |max_ratios_| from global curve.
min_ratios_.resize(num_points - 1);
max_ratios_.resize(num_points - 1);
const std::vector<double> global_brightness =
global_curve_->GetControlPointsY();
// TODO(jiameng): may revise to allow 0 as a control point.
DCHECK_GT(global_brightness[0], 0);
for (size_t i = 0; i < num_points - 1; ++i) {
double min_grad = params_.min_grad;
if (global_log_lux[i] < params_.low_log_lux_threshold) {
min_grad = params_.min_grad_low_lux;
} else if (global_log_lux[i] > params_.high_log_lux_threshold) {
min_grad = params_.min_grad_high_lux;
}
const double ratio = global_brightness[i + 1] / global_brightness[i];
DCHECK_GE(ratio, 1);
min_ratios_[i] = std::pow(ratio, min_grad);
max_ratios_[i] = std::pow(ratio, params_.max_grad);
}
if (!IsInitialPersonalCurveValid()) {
// Use global curve instead if personal curve isn't valid.
current_curve_.emplace(global_curve);
brightness_ = current_curve_->GetControlPointsY();
return false;
}
return true;
}
MonotoneCubicSpline GaussianTrainer::GetGlobalCurve() const {
DCHECK(valid_params_);
DCHECK(global_curve_);
return *global_curve_;
}
MonotoneCubicSpline GaussianTrainer::GetCurrentCurve() const {
DCHECK(valid_params_);
DCHECK(current_curve_);
return *current_curve_;
}
TrainingResult GaussianTrainer::Train(
const std::vector<TrainingDataPoint>& data) {
DCHECK(global_curve_);
DCHECK(current_curve_);
DCHECK(!data.empty());
for (const auto& data_point : data) {
AdjustCurveWithSingleDataPoint(data_point);
}
if (!need_to_update_curve_) {
const double error = CalculateCurveError(data);
LogModelCurveError(error, false /* model_updated */);
return TrainingResult(std::nullopt, error);
}
need_to_update_curve_ = false;
const auto new_curve = MonotoneCubicSpline::CreateMonotoneCubicSpline(
ambient_log_lux_, brightness_);
if (!new_curve) {
return TrainingResult(std::nullopt, 0 /* error */);
}
current_curve_ = new_curve;
const double error = CalculateCurveError(data);
LogModelCurveError(error, true /* model_updated */);
return TrainingResult(current_curve_, error);
}
bool GaussianTrainer::IsInitialPersonalCurveValid() const {
// |global_curve_| is valid by construction.
if (*global_curve_ == *current_curve_)
return true;
for (size_t i = 0; i < brightness_.size() - 1; ++i) {
const double ratio = brightness_[i + 1] / brightness_[i];
if (ratio < min_ratios_[i] || ratio > max_ratios_[i])
return false;
}
return true;
}
void GaussianTrainer::AdjustCurveWithSingleDataPoint(
const TrainingDataPoint& data) {
const double brightness_global =
global_curve_->Interpolate(data.ambient_log_lux);
// Check if this |data| is an outlier and should be ignored. It's an outlier
// if its original/old brightness is too far off from the brightness as
// predicted by the global curve. This assumes the global curve is reasonably
// accurate.
const bool is_brightness_outlier =
IsBrightnessOutlier(data.brightness_old, brightness_global, params_);
UMA_HISTOGRAM_BOOLEAN("AutoScreenBrightness.ModelTraining.BrightnessOutlier",
is_brightness_outlier);
if (is_brightness_outlier) {
return;
}
// Calculate how much adjustment we need to make to the current personal
// curve at |data.ambient_log_lux|.
const double model_brightness =
current_curve_->Interpolate(data.ambient_log_lux);
const double brightness_adjustment = ModelPredictionAdjustment(
data.brightness_old, data.brightness_new, model_brightness, params_);
if (std::abs(brightness_adjustment) <= kTol)
return;
need_to_update_curve_ = true;
// Index of the log-lux in |ambient_log_lux_| that's closest to
// |data.ambient_log_lux|.
size_t center_index = 0;
double min_dist = std::numeric_limits<double>::max();
for (size_t i = 0; i < ambient_log_lux_.size(); ++i) {
// Adjust brightness of each control point in the current brightness curve.
const double dist = std::abs(data.ambient_log_lux - ambient_log_lux_[i]);
brightness_[i] += brightness_adjustment * Gaussian(dist, params_.sigma);
if (dist < min_dist) {
center_index = i;
min_dist = dist;
}
}
EnforceMonotonicity(center_index);
}
void GaussianTrainer::EnforceMonotonicity(size_t center_index) {
DCHECK_LT(center_index, ambient_log_lux_.size());
brightness_[center_index] =
std::clamp(brightness_[center_index], params_.min_brightness, 100.0);
// Updates control points to the left of |center_index| so that brightness
// values satisfy min/max ratio requirement.
for (size_t i = center_index; i > 0; --i) {
const double min_value = brightness_[i] / max_ratios_[i - 1];
const double max_value = brightness_[i] / min_ratios_[i - 1];
brightness_[i - 1] = std::clamp(brightness_[i - 1], min_value, max_value);
if (brightness_[i - 1] > 100.0) {
brightness_[i - 1] = 100.0;
}
}
// Updates control points to the right of |center_index| so that brightness
// values satisfy min/max ratio requirement.
for (size_t i = center_index; i < ambient_log_lux_.size() - 1; ++i) {
const double min_value = brightness_[i] * min_ratios_[i];
const double max_value = brightness_[i] * max_ratios_[i];
brightness_[i + 1] = std::clamp(brightness_[i + 1], min_value, max_value);
if (brightness_[i + 1] > 100.0) {
brightness_[i + 1] = 100.0;
}
}
#ifndef NDEBUG
// Check that final |brightness_| array is monotonic across whole range and
// each value is in [0, 100].
for (size_t i = 0; i < ambient_log_lux_.size() - 1; ++i) {
DCHECK_GE(brightness_[i], 0);
DCHECK_LE(brightness_[i], 100);
DCHECK_LE(brightness_[i], brightness_[i + 1]);
}
DCHECK_GE(brightness_.back(), 0);
DCHECK_LE(brightness_.back(), 100);
#endif
}
double GaussianTrainer::CalculateCurveError(
const std::vector<TrainingDataPoint>& data) const {
DCHECK(current_curve_);
double error = 0.0;
for (const auto& data_point : data) {
error += std::abs(data_point.brightness_new -
current_curve_->Interpolate(data_point.ambient_log_lux));
}
return error / data.size();
}
} // namespace auto_screen_brightness
} // namespace power
} // namespace ash