| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/chromeos/power/auto_screen_brightness/gaussian_trainer.h" |
| |
| #include "base/metrics/field_trial_params.h" |
| #include "chrome/browser/chromeos/power/auto_screen_brightness/utils.h" |
| #include "chromeos/constants/chromeos_features.h" |
| |
| #include <algorithm> |
| #include <cmath> |
| #include <limits> |
| |
| #include "base/logging.h" |
| |
| namespace chromeos { |
| namespace power { |
| namespace auto_screen_brightness { |
| |
| namespace { |
| |
| 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::max(0.0, std::min(reference_brightness / scale, |
| reference_brightness - offset)); |
| } |
| |
| // 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::min(100.0, std::max(reference_brightness * scale, |
| reference_brightness + offset)); |
| } |
| |
| // 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) { |
| const double lower_bound = brightness_old / (1.0 + brightness_step_size); |
| const double upper_bound = brightness_old * (1.0 + brightness_step_size); |
| |
| return std::min(std::max(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); |
| |
| 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); |
| |
| // If model's prediction is consistent with user's selection, then no |
| // brightness change will be necessary. |
| // TODO(jiameng): add UMA metrics. |
| if ((model_brightness >= target_brightness && bounded_user_adjustment >= 0) || |
| (model_brightness <= target_brightness && bounded_user_adjustment <= 0)) |
| 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); |
| } |
| |
| double Gaussian(double x, double sigma) { |
| double xs = x / sigma; |
| return std::exp(-xs * xs); |
| } |
| |
| } // namespace |
| |
| 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_; |
| } |
| |
| MonotoneCubicSpline 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_) |
| return *current_curve_; |
| |
| current_curve_.emplace(MonotoneCubicSpline(ambient_log_lux_, brightness_)); |
| need_to_update_curve_ = false; |
| return *current_curve_; |
| } |
| |
| 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. |
| // TODO(jiameng): add UMA metrics to record this. |
| if (IsBrightnessOutlier(data.brightness_old, brightness_global, params_)) { |
| 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::min( |
| 100.0, std::max(params_.min_brightness, brightness_[center_index])); |
| |
| // 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::max(std::min(brightness_[i - 1], max_value), min_value); |
| // TODO(jiameng): add UMA metrics. |
| 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::max(std::min(brightness_[i + 1], max_value), min_value); |
| // TODO(jiameng): add UMA metrics. |
| 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 |
| } |
| |
| } // namespace auto_screen_brightness |
| } // namespace power |
| } // namespace chromeos |