Commit 2086f589 authored by Jason Rhinelander's avatar Jason Rhinelander

Moved interopt::Stepper core to algorithms.hpp

algorithms.hpp now has eris::Stepper which *only* handles stepping
logic; eris::interopt::Stepper now uses a eris::Stepper object to
calculate steps, providing a base InterOpt implementatio.
(interopt::Stepper's functionality should be unchanged by this).
parent 1c6afc15
......@@ -4,6 +4,7 @@
#include <stack>
#include <type_traits>
#include <vector>
#include <limits>
namespace eris {
......@@ -102,4 +103,68 @@ range_<Iter> range(std::pair<Iter, Iter> const &pair) {
return range_<Iter>(pair);
}
/** Generic class for a stepping a value up or down by a certain amount, increasing or decreasing
* the step size based on the previous steps. This is often used in optimizers to find an optimal
* output/price level.
*/
class Stepper final {
public:
/** Constructs a new Stepper object.
*
* \param step the initial size of a step, relative to the current value. Defaults to 1/32
* (about 3.1%). Over time the step size will change based on the following options. When
* increasing, the new value is \f$(1 + step)\f$ times the current value; when decreasing
* the value is \f$\frac{1}{1 + step}\f$ times the current value. This ensures that an
* increase followed by a decrease will result in the same value (at least within numerical
* precision).
*
* \param increase_count if the value moves consistently in one direction this many times,
* the step size will be doubled. Defaults to 4 (i.e. the 4th step will be doubled). Must
* be at least 2, though values less than 4 aren't recommended in practice. When
* increasing, the previous increase count is halved; thus, with the default value of 4, it
* takes only two additional steps in the same direction before increasing the step size
* again. With a value of 6, it would take 6 changes for the first increase, then 3 for
* subsequent increases, etc.
*
* \param min_step is the minimum step size that can be taken. The default is equal to
* machine epsilon (i.e. the smallest value v such that 1 + v is a value distinct from 1),
* which is the smallest value possible for a step.
*/
Stepper(double step = 1.0/32.0, int increase_count = 4, double min_step = std::numeric_limits<double>::epsilon());
/** Called to signal that a step increase (`up=true`) or decrease (`up=false`) should be
* taken. Returns the relative step value, where 1 indicates no change, 1.2 indicates a 20%
* increase, etc.
*
* The returned multiple will always be a strictly positive value. If \f$s\f$ is the
* current step size, the returned value will be either \f$1+s\f$ for an upward step or
* \f$\frac{1}{1+s}\f$ for a downward step.
*
* When called, this first checks whether the step size should be changed: if at least
* `increase` steps in the same direction have occured, the step size is doubled (and the
* count of previous steps in this direction is halved); if the last step was in the
* opposite direction, the step size is halved.
*/
double step(bool up);
/// The number of steps in the same direction required to double the step size
const int increase;
/// The minimum (relative) step size allowed, specified in the constructor.
const double min_step;
/// The most recent relative step size
double step_size;
/// The most recent step direction: true if the last step was positive, false if negative.
bool prev_up = true;
/** The number of steps that have occurred in the same direction. When a step size doubling
* occurs, this value is halved (since the previous steps are only half the size of current
* steps).
*/
int same = 0;
};
}
#pragma once
#include <eris/Optimizer.hpp>
#include <eris/algorithms.hpp>
namespace eris { namespace interopt {
......@@ -11,8 +12,8 @@ namespace eris { namespace interopt {
* This is an abstract base class and requires an implementation of the should_increase() and
* take_step() methods.
*
* \sa eris::Stepper the class handling the step adjustment logic
* \sa PriceStepper a Stepper implementation adapted to PriceFirms
* \sa QMOpt a Stepper implementation adapted to the price of Quantity markets
*/
class Stepper : public InterOptimizer {
public:
......@@ -35,57 +36,32 @@ class Stepper : public InterOptimizer {
*/
Stepper(double step = 1.0/32.0, int increase_count = 4);
/** Determines whether the price should go up or down, and by how much. Calls
/** Determines whether the value should go up or down, and by how much. Calls
* (unimplemented) method should_increase() to determine the direction of change.
*/
virtual void optimize() const override;
/** Applies the price change calculated in optimize(). This will call take_step() with the
* relative price change (e.g. 1.25 to increase price by 25%).
/** Applies the value change calculated in optimize(). This will call take_step() with the
* relative value change (e.g. 1.25 to increase value by 25%).
*/
virtual void apply() override;
protected:
/** Called to determine whether the price next period should go up or down. A true return
* value indicates that a price increase should be tried, false indicates a decrease. This
/// The eris::Stepper object used to handle step adjustment logic
eris::Stepper stepper_;
/** Called to determine whether the value next period should go up or down. A true return
* value indicates that a value increase should be tried, false indicates a decrease. This
* typically makes use of the prev_up and same members as required.
*/
virtual bool should_increase() const = 0;
/** Called to change the price to the given multiple of the current price. This is called
/** Called to change the value to the given multiple of the current value. This is called
* by apply(). The relative value will always be a positive value not equal to 1.
*/
virtual void take_step(double relative) = 0;
/** Called to check whether the step needs to be updated, and updates it (and related
* variables) if so. Note that the step will never be smaller than min_step.
*
* The step size is cut in half when the step direction is reversed; it is doubled if there
* have been
*/
virtual void update_step();
/** The minimum (relative) step size allowed: this is equal to machine epsilon (i.e. the
* smallest value v such that 1 + v is a value distinct from 1.
*/
const double min_step = std::numeric_limits<double>::epsilon();
protected:
/** Will be true when calling should_increase() if the previous step was an increase, false
* otherwise. Should not be used when step equals 0 (in other words, for the first step).
*/
bool prev_up = true;
/** When should_increase() is called, this contains the number of times of consecutive times
* that the quantity has moved taken a step in the same direction. The value is
* approximate, however, as it is divided by 2 (rounding down) each time the step size is
* doubled. This will always be equal to at least 1 except for the very first optimization,
* when it will equal 0.
*/
int same = 0;
private:
double step_;
const int increase_;
/// Whether we are going to step up. Calculated in optimize(), given to stepper_ in apply()
mutable bool curr_up_ = false;
};
......
#include <eris/algorithms.hpp>
namespace eris {
Stepper::Stepper(double step, int increase_count, double min_step)
: increase(increase_count), min_step(min_step), step_size(step) {}
double Stepper::step(bool up) {
bool first_time = same == 0;
if (up == prev_up)
++same;
else
same = 1;
if (up != prev_up and not first_time) {
// Changing directions, cut the step size in half
step_size /= 2;
if (step_size < min_step) step_size = min_step;
}
else if (same >= increase) {
// We've taken several steps in the same direction, so double the step size
step_size *= 2;
// We just doubled the step size, so, in terms of the new step size, only half of the
// previous steps count:
same /= 2;
}
double mult = up ? (1 + step_size) : 1.0 / (1 + step_size);
prev_up = up;
return mult;
}
}
......@@ -10,12 +10,12 @@ bool PriceStepper::should_increase() const {
SharedMember<PriceFirm> firm = simulation()->agent(firm_);
curr_profit_ = firm->assets() / price_;
if (same == 0) // First time, no history to give us an informed decision; might as well increase.
if (stepper_.same == 0) // First time, no history to give us an informed decision; might as well increase.
return true;
else if (curr_profit_ > prev_profit_) // The last changed increased profit; keep going
return prev_up;
return stepper_.prev_up;
else if (curr_profit_ < prev_profit_) // The last change decreased profit; turn around
return !prev_up;
return !stepper_.prev_up;
else // The last change did not change profit at all; lower price (this might help with some
// perfect Bertrand cases)
return false;
......
......@@ -2,37 +2,15 @@
namespace eris { namespace interopt {
Stepper::Stepper(double step, int increase_count) : step_(step), increase_(increase_count) {}
Stepper::Stepper(double step, int increase_count)
: stepper_(eris::Stepper(step, increase_count)) {}
void Stepper::optimize() const {
curr_up_ = should_increase();
}
void Stepper::apply() {
if (curr_up_ == prev_up)
++same;
else
same = 1;
update_step();
double mult = curr_up_ ? (1 + step_) : 1.0 / (1 + step_);
take_step(mult);
prev_up = curr_up_;
}
void Stepper::update_step() {
if (curr_up_ != prev_up) {
// Changing directions, cut the step size
step_ /= 2;
if (step_ < min_step) step_ = min_step;
}
else if (same >= increase_) {
step_ *= 2;
same /= 2;
}
take_step(stepper_.step(curr_up_));
}
} }
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment