Commit 51816949 authored by Jason Rhinelander's avatar Jason Rhinelander

QMarket optimization overhaul; Stepper oscillation

- pricing retries now has a "first" tries and a regular retries.  The
  first one typically needs more iterations because the initial value
  is arbitrary, while later ones have a decent first guess in the
  previous period's value.
- Removed obsolete 'optimizer' variable (made obsolete when
  optimization moved into QMarket itself in the eris optimizer design
  overhaul).
- The Stepper used for adjusting the price is now public, so that its
  properties can be changed by the QMarket creator.
- The default Stepper now has a minimum step size of 1/65536.  The
  previous minimum of machine epsilon was rediculously small (and
  required a lot of steps to get away from).
- Stepper now has a variable for checking whether the steps are
  oscillating back and forth at the minimum step size.
- QMarket uses oscillation detecting to detect that the price is close
  enough to the optimum and thus more price adjustments aren't needed.
- Various previously-const public attributes of Stepper are now
  non-const so that Stepper users can change the Stepper behaviour.
  This makes Stepper-using classes (like QMarket) easier: they can just
  expose the Stepper instead of needing to pass through a bunch of
  construction-time Stepper parameters.
parent 2b536871
......@@ -165,8 +165,9 @@ class Stepper final {
*
* If relative_steps is true, this returns the relative step value, where 1 indicates no
* change, 1.2 indicates a 20% increase, etc. The relative step 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.
* strictly positive value not equal to 1. 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.
*
* If relative_steps is false, this returns the absolute change in the current value, which
* could be any positive or negative value; the returned value is the amount by which the
......@@ -176,17 +177,21 @@ class Stepper final {
* `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.
*
* After this is called, you may optionally consider the `oscillating_min' value, which will
* tell you how many of the previous steps have simply oscillated around the minimum step
* size (and thus aren't doing anything useful).
*/
double step(bool up);
/// The number of steps in the same direction required to double the step size
const int increase;
int increase;
/// The minimum (relative) step size allowed, specified in the constructor.
const double min_step;
/// The minimum (relative) step size allowed, as specified in the constructor.
double min_step;
/// The maximum (relative) step size allowed, specified in the constructor.
const double max_step;
/// The maximum (relative) step size allowed, as specified in the constructor.
double max_step;
/** If true, steps are relative; if false, steps are absolute.
*
......@@ -202,14 +207,21 @@ class Stepper final {
* when this value is false, the value passed to take_step will be the absolute change,
* *not* a multiple of the current value.
*/
const bool relative_steps;
bool relative_steps;
/// The most recent step size
/// The most recent step size. If no step has been taken yet, this is the initial step.
double step_size;
/// The most recent step direction: true if the last step was positive, false if negative.
bool prev_up = true;
/** Will be set to the number of times the step direction has oscillated back and forth
* while at the minimum step size, and thus the normal action of reducing the step size
* can't be taken. As soon as two steps occur in the same directory, this will be reset to
* 0.
*/
unsigned int oscillating_min = 0;
/** 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).
......
......@@ -16,12 +16,17 @@ namespace eris { namespace market {
* in the intraOptimize() of other optimizers to try to determine the price that just exactly sells
* out the market.
*/
class QMarket : public Market, public virtual intraopt::Initialize, public virtual intraopt::Reoptimize {
class QMarket : public Market,
public virtual intraopt::Initialize,
public virtual intraopt::Reoptimize,
public virtual intraopt::Finish {
public:
/// Default initial price, if not given in constructor
static constexpr double default_initial_price = 1.0;
/// Default WalrasianPricer tries, if not given in constructor
static constexpr int default_qmpricer_tries = 5;
/// Default repricing tries, if not given in constructor
static constexpr unsigned int default_pricing_tries = 5;
/// Default initial round repricing tries, if not given in constructor
static constexpr unsigned int default_pricing_tries_first = 25;
/** Constructs a new quantity market, with a specified unit of output and unit of input
* (price) per unit of output.
......@@ -33,20 +38,31 @@ class QMarket : public Market, public virtual intraopt::Initialize, public virtu
* calculated and exchanged in the market are multiples of this bundle.
*
* \param initial_price the initial per-unit price (as a multiple of price_unit) of goods in
* this market. Defaults to 1; must be > 0. This is typically adjusted up or down by QMStepper (or a
* similar inter-period optimizer) between periods.
* this market. Defaults to 1; must be > 0. This will be adjusted during periods to get
* close to having no excess supply/demand for the firms' quantities, depending on the
* following settings.
*
* \param qmpricer_tries if greater than 0, this specifies the number of tries to try a
* price during intra-period optimization. This generally causes an reset of the entire
* intraopt stage, and can thus be quite expensive, but is needed for the market to act as
* if governed by a sort of Walrasian pricer. If 0, the market will not attempt to adjust
* its price, in which case some external intraopt object must be created to manage the
* market's price.
* \param pricing_tries if greater than 0, this specifies the number of tries to try a
* price during intra-period optimization. Values of this parameter greater than 1 mean
* multiple prices will be tried in a period. Each retry causes an reset of the entire
* intraopt stage, and can thus be computationally expensive, but are needed for the market
* to act as if governed by a sort of (imperfect) Walrasian pricer. If 0, the market will
* not attempt to adjust its price, in which case some external mechanism should be created
* to manage the market's price. Higher values give "better" prices (in the sense of being
* closer to equilibrium), at the cost of increased computational time (since every retry
* requires reoptimization of *every* agent).
*
* \param pricing_tries_first just like pricing_tries, but applies to the first optimization
* after the market is added to a simulation. This should typically be much higher than
* pricing_tries to deal with the fact than an initial price is often arbitrary, while
* prices in later periods at least have the current price (i.e. from the previous period)
* as a useful reference point.
*/
QMarket(Bundle output_unit,
Bundle price_unit,
double initial_price = default_initial_price,
unsigned int qmpricer_tries = default_qmpricer_tries);
unsigned int pricing_tries = default_pricing_tries,
unsigned int pricing_tries_first = default_pricing_tries_first);
/// Returns the pricing information for purchasing q units in this market.
virtual price_info price(double q) const override;
......@@ -54,8 +70,7 @@ class QMarket : public Market, public virtual intraopt::Initialize, public virtu
/** Returns the current price of 1 output unit in this market. Note that this does not take
* into account whether any quantity is actually available, it simply returns the going
* price (as a multiple of price_unit) for 1 unit of the output_unit Bundle. This typically
* changes from one period to the next, but is constant within a period for this market
* type.
* changes within periods as the market tries to get close to a market clearing price.
*/
virtual double price() const;
......@@ -81,15 +96,9 @@ class QMarket : public Market, public virtual intraopt::Initialize, public virtu
*/
double firmQuantities(double max = std::numeric_limits<double>::infinity()) const;
/** The ID of the automatically-created WalrasianPricer intra-period optimizer attached to this
* market. Will be 0 if no such optimizer has been created.
*/
eris_id_t optimizer = 0;
/// Reserves q units, paying at most p_max for them.
virtual Reservation reserve(SharedMember<AssetAgent> agent, double q, double p_max = std::numeric_limits<double>::infinity()) override;
/// Adds a firm to this market. The Firm must be a QFirm object (or subclass)
virtual void addFirm(SharedMember<Firm> f) override;
......@@ -118,18 +127,33 @@ class QMarket : public Market, public virtual intraopt::Initialize, public virtu
*/
virtual bool intraReoptimize() override;
/** Resets the first-period state once a period is successfully finished.
*/
virtual void intraFinish() override;
/** The Stepper object used for this market. This is public so that the stepper parameters
* can be changed; the default stepper object uses Stepper default values, except for the
* minimum step size which is overridden to 1/65536 (thus the minimum step size is about
* 0.0015% of the current price)
*/
Stepper stepper {Stepper::default_initial_step, Stepper::default_increase_count, 1.0/65536.0};
protected:
/// The current price of the good as a multiple of price_unit
double price_;
/// The Stepper object used for calculating price steps
Stepper stepper_;
/// The number of times we adjust price each period
int tries_ = 0;
unsigned int tries_, tries_first_;
/// The number of times we have already tried to adjust in the current period
int tried_ = 0;
unsigned int tried_ = 0;
/// The excess capacity we found in the previous iteration
double last_excess_ = 0.0;
/// Tracks whether this is the first period or not
bool first_period_ = true;
/// Resets first_period_ to true when added to a simulation.
virtual void added() override;
};
} }
......@@ -11,10 +11,20 @@ double Stepper::step(bool up) {
else
same = 1;
bool around_min = false;
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;
if (step_size == min_step) {
// If we're already at the minimum, and we're changing direction, we're just oscillating
// around the optimal value, so set that flag.
around_min = true;
}
else {
step_size /= 2;
if (step_size < min_step) step_size = min_step;
}
}
else if (same >= increase and step_size < max_step) {
// We've taken several steps in the same direction, so double the step size
......@@ -29,6 +39,11 @@ double Stepper::step(bool up) {
same /= 2;
}
if (around_min)
oscillating_min++;
else
oscillating_min = 0;
prev_up = up;
if (not relative_steps) return up ? step_size : -step_size;
......
......@@ -3,8 +3,8 @@
namespace eris { namespace market {
QMarket::QMarket(Bundle output_unit, Bundle price_unit, double initial_price, unsigned int qmpricer_tries) :
Market(output_unit, price_unit), tries_(qmpricer_tries) {
QMarket::QMarket(Bundle output_unit, Bundle price_unit, double initial_price, unsigned int pricing_tries, unsigned int pricing_tries_first) :
Market(output_unit, price_unit), tries_(pricing_tries), tries_first_(pricing_tries_first) {
price_ = initial_price <= 0 ? 1 : initial_price;
}
......@@ -65,7 +65,9 @@ Market::Reservation QMarket::reserve(SharedMember<AssetAgent> agent, double q, d
std::unordered_map<eris_id_t, BundleNegative> firm_transfers;
while (q > 0) {
const double threshold = q * std::numeric_limits<double>::epsilon();
while (q > threshold) {
DEBUGVAR(q);
qfirm.clear();
double qmin = 0; // Will store the maximum quantity that all firms can supply
......@@ -117,28 +119,42 @@ void QMarket::intraInitialize() {
}
bool QMarket::intraReoptimize() {
// If there are no firms, there's nothing to do
if (firms().empty()) return false;
bool first_try = tried_ == 0;
unsigned int max_tries = first_period_ ? tries_first_ : tries_;
DEBUG("QMarket trying some optimization...");
DEBUGVAR(firmQuantities());
if (tried_ >= max_tries)
DEBUG(tried_ << " > " << max_tries << ", stopping");
// If we're all out of adjustments, don't change the price
if (++tried_ > tries_) return false;
if (++tried_ > max_tries) return false;
auto qlock = writeLock();
double excess_capacity = firmQuantities();
bool last_was_decrease = not stepper_.prev_up;
DEBUGVAR(excess_capacity);
bool last_was_decrease = not stepper.prev_up;
bool increase_price = excess_capacity <= 0;
if (not first_try and last_was_decrease and not increase_price and excess_capacity >= last_excess_) {
// Decreasing the price last time didn't help anything--perhaps noise from other market
// adjustments, but it could also mean that we've hit market satiation, in which case
// decreasing price further won't help.
increase_price = true;
}
last_excess_ = excess_capacity;
double new_price = stepper_.step(increase_price);
double new_price = stepper.step(increase_price);
// If we're just stepping back and forth by the minimum step size, we're basically done; prefer
// the slightly lower price (by not taking a minimum positive step). The ending price won't be
// lower by much: the minimum step size is very small, but assume firms would rather have no
// excess inventory than a tiny bit of excess inventory.
if (stepper.oscillating_min > 0 and new_price > 1) {
DEBUG("Detected oscillation: ending optimization.");
return false;
}
DEBUG("Changing price from " << price() << " to " << new_price << "*" << price() << "=" << new_price*price());
......@@ -149,4 +165,15 @@ bool QMarket::intraReoptimize() {
return false;
}
void QMarket::added() {
Market::added();
first_period_ = true;
}
void QMarket::intraFinish() {
if (not firms().empty())
first_period_ = false;
}
} }
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