ellalgo_rs/oracles/
profit_oracle.rs

1use crate::cutting_plane::{OracleOptim, OracleOptimQ};
2use ndarray::prelude::*;
3
4type Arr = Array1<f64>;
5type Cut = (Arr, f64);
6
7/// The ProfitOracle struct represents an oracle for a profit maximization problem with specific
8/// parameters.
9///
10///  This example is taken from [Aliabadi and Salahi, 2013]:
11///
12///    max   p(A x1**alpha x2**beta) - v1 * x1 - v2 * x2
13///    s.t.  x1 <= k
14///
15///  where:
16///
17///    p(scale x1**alpha x2**beta): Cobb-Douglas production function
18///    p: the market price per unit
19///    scale: the scale of production
20///    alpha, beta: the output elasticities
21///    x: input quantity
22///    v: output price
23///    k: a given constant that restricts the quantity of x1
24///
25/// Reference:
26///
27/// * Aliabadi, Hossein, and Maziar Salahi. "Robust Geometric Programming Approach to Profit Maximization
28///     with Interval Uncertainty." Computer Science Journal Of Moldova 61.1 (2013): 86-96.
29///
30/// Properties:
31///
32/// * `log_p_scale`: The natural logarithm of the scale parameter of the Cobb-Douglas production
33///         function. It represents the overall scale of production.
34/// * `log_k`: The natural logarithm of the constant k that restricts the quantity of x1.
35/// * `price_out`: The `price_out` property represents the output prices `v1` and `v2` in the profit
36///         maximization problem. It is of type `Arr`, which is likely a shorthand for an array or vector data
37///         structure.
38/// * `elasticities`: An array representing the output elasticities (alpha and beta) in the profit
39///         maximization problem.
40#[derive(Debug)]
41pub struct ProfitOracle {
42    idx: i32,
43    log_p_scale: f64,
44    log_k: f64,
45    price_out: Arr,
46    pub elasticities: Arr,
47    log_cobb: f64,
48    vx: f64,
49    q: Arr,
50}
51
52impl ProfitOracle {
53    /// The function `new` constructs a new ProfitOracle object with given parameters.
54    ///
55    /// Arguments:
56    ///
57    /// * `p`: The parameter `p` represents the market price per unit. It is a floating-point number (f64)
58    ///         that indicates the price at which the product is sold in the market.
59    /// * `scale`: The scale parameter represents the scale of production. It determines the quantity of
60    ///         output produced.
61    /// * `k`: The parameter `k` is a given constant that restricts the quantity of `x1`. It is used in the
62    ///         calculation of the profit oracle object.
63    /// * `elasticities`: The parameter "elasticities" represents the output elasticities, which are
64    ///         coefficients that measure the responsiveness of the quantity of output to changes in the inputs. It
65    ///         is expected to be an array or list of values.
66    /// * `price_out`: The `price_out` parameter represents the output price. It is of type `Arr`, which
67    ///         suggests that it is an array or collection of values. The specific type of `Arr` is not specified in
68    ///         the code snippet, so it could be an array, a vector, or any other collection type
69    ///
70    /// Returns:
71    ///
72    /// The `new` function is returning an instance of the `ProfitOracle` struct.
73    pub fn new(params: (f64, f64, f64), elasticities: Arr, price_out: Arr) -> Self {
74        let (unit_price, scale, limit) = params;
75        let log_p_scale = (unit_price * scale).ln();
76        let log_k = limit.ln();
77        ProfitOracle {
78            idx: -1,
79            log_p_scale,
80            log_k,
81            price_out,
82            elasticities,
83            log_cobb: 0.0,
84            vx: 0.0,
85            q: Arr::zeros(2),
86        }
87    }
88
89    /// The function assess_feas calculates the gradient and objective function value for an optimization
90    /// problem in Rust.
91    ///
92    /// Arguments:
93    ///
94    /// * `y`: A reference to an array of f64 values.
95    /// * `gamma`: The parameter `gamma` is a mutable reference to a `f64` variable.
96    fn assess_feas(&mut self, y: &Arr, gamma: &mut f64) -> Option<(Arr, f64)> {
97        let num_constraints = 2;
98        for _ in 0..num_constraints {
99            self.idx += 1;
100            if self.idx == num_constraints {
101                self.idx = 0; // round robin
102            }
103            let fj = match self.idx {
104                0 => y[0] - self.log_k, // y0 <= log k
105                1 => {
106                    self.log_cobb = self.log_p_scale + self.elasticities.dot(y);
107                    self.q = &self.price_out * y.mapv(f64::exp);
108                    self.vx = self.q[0] + self.q[1];
109                    (*gamma + self.vx).ln() - self.log_cobb
110                }
111                _ => unreachable!(),
112            };
113            if fj > 0.0 {
114                return Some((
115                    match self.idx {
116                        0 => array![1.0, 0.0],
117                        1 => &self.q / (*gamma + self.vx) - &self.elasticities,
118                        _ => unreachable!(),
119                    },
120                    fj,
121                ));
122            }
123        }
124
125        None
126    }
127}
128
129impl OracleOptim<Arr> for ProfitOracle {
130    type CutChoice = f64; // single cut
131
132    /// The function assess_optim calculates the gradient and objective function value for an optimization
133    /// problem in Rust.
134    ///
135    /// Arguments:
136    ///
137    /// * `y`: A reference to an array of f64 values.
138    /// * `gamma`: The parameter `gamma` is a mutable reference to a `f64` variable.
139    fn assess_optim(&mut self, y: &Arr, gamma: &mut f64) -> ((Arr, f64), bool) {
140        if let Some(cut) = self.assess_feas(y, gamma) {
141            return (cut, false);
142        }
143
144        let te = self.log_cobb.exp();
145        *gamma = te - self.vx;
146        let grad = (&self.q / te) - &self.elasticities;
147        ((grad, 0.0), true)
148    }
149}
150
151/// The `ProfitRbOracle` struct is an implementation of an oracle for a profit maximization problem with
152/// robustness. It is used to solve a profit maximization problem where the parameters have interval
153/// uncertainty. The implementation is based on the approach described in the paper "Robust Geometric
154/// Programming Approach to Profit Maximization with Interval Uncertainty" by Aliabadi and Salahi.
155/// The `ProfitRbOracle` struct is an implementation of an oracle for a profit maximization problem with
156/// robustness.
157///
158/// Reference:
159///
160/// * Aliabadi, Hossein, and Maziar Salahi. "Robust Geometric Programming Approach to Profit Maximization
161///         with Interval Uncertainty." Computer Science Journal Of Moldova 61.1 (2013): 86-96.
162#[derive(Debug)]
163pub struct ProfitRbOracle {
164    uie: [f64; 2],
165    omega: ProfitOracle,
166    elasticities: Arr,
167}
168
169impl ProfitRbOracle {
170    /// The function `new` constructs a new ProfitRbOracle object with given parameters.
171    ///
172    /// Arguments:
173    ///
174    /// * `p`: The market price per unit.
175    /// * `scale`: The `scale` parameter represents the scale of production. It determines the level of
176    ///         output or production for a given set of inputs. It can be thought of as the size or capacity of the
177    ///         production process.
178    /// * `k`: A given constant that restricts the quantity of x1.
179    /// * `aa`: The parameter `aa` represents the output elasticities. It is an array that contains the
180    ///         elasticities of each output variable.
181    /// * `price_out`: The parameter `price_out` represents the output price. It is of type `Arr`, which
182    ///         suggests that it is an array or vector of values.
183    /// * `e`: Parameters for uncertainty.
184    /// * `e3`: The parameter `e3` represents the uncertainty in the market price per unit. It is used to
185    ///         adjust the market price in the calculation of the `omega` variable.
186    ///
187    /// Returns:
188    ///
189    /// The `new` function is returning an instance of the `ProfitRbOracle` struct.
190    pub fn new(
191        params: (f64, f64, f64),
192        aa: Arr,
193        price_out: Arr,
194        vparams: (f64, f64, f64, f64, f64),
195    ) -> Self {
196        let (e1, e2, e3, e4, e5) = vparams;
197        let uie = [e1, e2];
198        let params_rb = (params.0 - e3, params.1, params.2 - e4);
199        let omega = ProfitOracle::new(
200            params_rb,
201            aa.clone(),
202            price_out + Arr::from_vec(vec![e5, e5]),
203        );
204        ProfitRbOracle {
205            uie,
206            omega,
207            elasticities: aa,
208        }
209    }
210}
211
212impl OracleOptim<Arr> for ProfitRbOracle {
213    type CutChoice = f64; // single cut
214
215    /// The `assess_optim` function takes an input quantity `y` and updates the best-so-far optimal value
216    /// `gamma` based on the elasticities and returns a cut and the updated best-so-far value.
217    ///
218    /// Arguments:
219    ///
220    /// * `y`: The parameter `y` is an input quantity represented as an array (`Arr`) in log scale.
221    /// * `gamma`: The parameter `gamma` is the best-so-far optimal value. It is passed as a mutable reference
222    ///            (`&mut f64`) so that its value can be updated within the function.
223    fn assess_optim(&mut self, y: &Arr, gamma: &mut f64) -> (Cut, bool) {
224        let mut a_rb = self.elasticities.clone();
225        for i in 0..2 {
226            a_rb[i] += if y[i] > 0.0 {
227                -self.uie[i]
228            } else {
229                self.uie[i]
230            };
231        }
232        self.omega.elasticities = a_rb;
233        self.omega.assess_optim(y, gamma)
234    }
235}
236
237/// The ProfitOracleQ struct is an oracle for the profit maximization problem in a discrete version.
238///
239/// Properties:
240///
241/// * `omega`: The `omega` property is an instance of the `ProfitOracle` struct. It is used to calculate
242///            the profit for a given input.
243/// * `yd`: The variable `yd` is an array that represents the discrete version of y values
244pub struct ProfitOracleQ {
245    omega: ProfitOracle,
246    yd: Arr,
247}
248
249impl ProfitOracleQ {
250    /// The function `new` constructs a new `ProfitOracleQ` object with given parameters.
251    ///
252    /// Arguments:
253    ///
254    /// * `p`: The parameter `p` represents the market price per unit. It is a floating-point number (f64)
255    ///         that indicates the price at which a unit of the product is sold in the market.
256    /// * `scale`: The "scale" parameter represents the scale of production, which refers to the level of
257    ///         output or production of a particular good or service. It indicates the quantity of goods or services
258    ///         produced within a given time period.
259    /// * `k`: A given constant that restricts the quantity of x1. It is used to limit the quantity of a
260    ///         particular input (x1) in the production process.
261    /// * `elasticities`: The parameter `elasticities` represents the output elasticities. Output elasticity measures the
262    ///         responsiveness of the quantity of output to a change in the input quantity. It indicates how much
263    ///         the quantity of output changes in response to a change in the quantity of input.
264    /// * `price_out`: The parameter `price_out` represents the output price. It is an array that contains
265    ///         the prices of the outputs.
266    ///
267    /// Returns:
268    ///
269    /// The `new` function is returning an instance of the `ProfitOracleQ` struct.
270    pub fn new(params: (f64, f64, f64), elasticities: Arr, price_out: Arr) -> Self {
271        ProfitOracleQ {
272            yd: array![0.0, 0.0],
273            omega: ProfitOracle::new(params, elasticities, price_out),
274        }
275    }
276}
277
278impl OracleOptimQ<Arr> for ProfitOracleQ {
279    type CutChoice = f64; // single cut
280
281    /// The `assess_optim_q` function takes in an input quantity `y` in log scale, updates the best-so-far
282    /// optimal value `gamma`, and returns a cut and the updated best-so-far value.
283    ///
284    /// Arguments:
285    ///
286    /// * `y`: The parameter `y` is an input quantity in log scale. It is of type `Arr`, which is likely an
287    ///         array or vector type.
288    /// * `gamma`: The parameter `gamma` represents the best-so-far optimal value. It is a mutable reference to
289    ///         a `f64` value, which means it can be modified within the function.
290    /// * `retry`: A boolean value indicating whether it is a retry or not.
291    fn assess_optim_q(&mut self, y: &Arr, gamma: &mut f64, retry: bool) -> (Cut, bool, Arr, bool) {
292        if !retry {
293            if let Some(cut) = self.omega.assess_feas(y, gamma) {
294                return (cut, false, y.clone(), true);
295            }
296
297            // let mut xd = y.mapv(f64::exp).mapv(f64::round);
298            let mut xd = y.map(|x| x.exp().round());
299            if xd[0] == 0.0 {
300                xd[0] = 1.0; // nearest integer than 0
301            }
302            if xd[1] == 0.0 {
303                xd[1] = 1.0;
304            }
305            self.yd = xd.mapv(f64::ln);
306        }
307        let ((grad, beta), shrunk) = self.omega.assess_optim(&self.yd, gamma);
308        let beta = beta + grad.dot(&(&self.yd - y));
309        ((grad, beta), shrunk, self.yd.clone(), !retry)
310    }
311}
312
313#[cfg(test)]
314mod tests {
315    use super::{ProfitOracle, ProfitOracleQ, ProfitRbOracle};
316    // use super::{ProfitOracle, ProfitOracleQ, ProfitRbOracle};
317    use crate::cutting_plane::{cutting_plane_optim, cutting_plane_optim_q, Options};
318    use crate::ell::Ell;
319    use ndarray::array;
320
321    #[test]
322    pub fn test_profit_oracle() {
323        let unit_price = 20.0;
324        let scale = 40.0;
325        let limit = 30.5;
326        let params = (unit_price, scale, limit);
327        let elasticities = array![0.1, 0.4];
328        let price_out = array![10.0, 35.0];
329
330        let mut ellip = Ell::new(array![100.0, 100.0], array![0.0, 0.0]);
331        let mut omega = ProfitOracle::new(params, elasticities, price_out);
332        let mut gamma = 0.0;
333        let options = Options::default();
334        let (y_opt, niter) = cutting_plane_optim(&mut omega, &mut ellip, &mut gamma, &options);
335        assert!(y_opt.is_some());
336        if let Some(y) = y_opt {
337            assert!(y[0] <= limit.ln());
338        }
339        assert_eq!(niter, 83, "regression test");
340    }
341
342    #[test]
343    pub fn test_profit_oracle_rb() {
344        let unit_price = 20.0;
345        let scale = 40.0;
346        let limit = 30.5;
347        let params = (unit_price, scale, limit);
348        let elasticities = array![0.1, 0.4];
349        let price_out = array![10.0, 35.0];
350        let e1 = 0.003;
351        let e2 = 0.007;
352        let e3 = 1.0;
353        let e4 = 1.0;
354        let e5 = 1.0;
355        let vparams = (e1, e2, e3, e4, e5);
356
357        let mut ellip = Ell::new(array![100.0, 100.0], array![0.0, 0.0]);
358        let mut omega = ProfitRbOracle::new(params, elasticities, price_out, vparams);
359        let mut gamma = 0.0;
360        let options = Options::default();
361        let (y_opt, niter) = cutting_plane_optim(&mut omega, &mut ellip, &mut gamma, &options);
362        assert!(y_opt.is_some());
363        if let Some(y) = y_opt {
364            assert!(y[0] <= limit.ln());
365        }
366        assert_eq!(niter, 90, "regression test");
367    }
368
369    #[test]
370    pub fn test_profit_oracle_q() {
371        let unit_price = 20.0;
372        let scale = 40.0;
373        let limit = 30.5;
374        let params = (unit_price, scale, limit);
375        let elasticities = array![0.1, 0.4];
376        let price_out = array![10.0, 35.0];
377
378        let mut ellip = Ell::new(array![100.0, 100.0], array![0.0, 0.0]);
379        let mut omega = ProfitOracleQ::new(params, elasticities, price_out);
380        let mut gamma = 0.0;
381        let options = Options::default();
382        let (y_opt, niter) = cutting_plane_optim_q(&mut omega, &mut ellip, &mut gamma, &options);
383        assert!(y_opt.is_some());
384        if let Some(y) = y_opt {
385            assert!(y[0] <= limit.ln());
386        }
387        assert_eq!(niter, 29, "regression test");
388    }
389}