<commit_before>part of elec.risk_system;

class PriceCurve extends TimeSeries<Map<Bucket, num>> with MarksCurve {
  static final DateFormat _isoFmt = DateFormat('yyyy-MM');

  /// A simple forward curve model for daily and monthly values extending
  /// a TimeSeries<Map<Bucket,num>>.  There are no gaps in the observations.
  PriceCurve();

  /// A simple forward curve model for daily and monthly values extending
  /// a TimeSeries<Map<Bucket,num>>.  There are no gaps in the observations.
  /// Support only daily and monthly observations.
  PriceCurve.fromIterable(Iterable<IntervalTuple<Map<Bucket, num>>> xs) {
    addAll(xs);
  }

  /// Construct a price curve if you have the data from different buckets.
  PriceCurve.fromBuckets(Map<Bucket, TimeSeries<num>> xs) {
    _buckets = xs.keys.toSet();
    // put the first bucket
    addAll(xs[_buckets!.first]!.map((e) =>
        IntervalTuple(e.interval, <Bucket, num>{_buckets!.first: e.value})));

    for (var bucket in _buckets!.skip(1)) {
      var out = merge(xs[bucket]!, joinType: JoinType.Outer, f: (x, dynamic y) {
        if (x == null) {
          return <Bucket, num>{bucket: y};
        } else if (y == null) {
          return x;
        } else {
          x[bucket] = y;
          return x;
        }
      });
      clear();
      addAll(out);
    }
  }

  /// Construct a forward curve given an input in this form.  The buckets
  /// can be different, but the covering needs to be complete (no gaps.)
  ///   [
  ///     {'term': '2020-07-17', 'value': {'5x16': 27.10, '7x8': 15.5}},
  ///     {'term': '2020-07-18', 'value': {'2x16H': 22.15, '7x8': 15.5}},
  ///     ...
  ///     {'term': '2020-08', 'value': {'5x16': 31.50, '2x16H': 25.15, '7x8': 18.75}},
  ///     ...
  ///   ]
  ///   The inputs are time-ordered with no gaps.
  PriceCurve.fromJson(List<Map<String, dynamic>> xs, Location location) {
    for (var x in xs) {
      Interval term;
      if ((x['term'] as String).length == 10) {
        term = Date.parse(x['term'], location: location);
      } else if ((x['term'] as String).length == 7) {
        term = Month.parse(x['term'], fmt: _isoFmt, location: location);
      } else {
        throw ArgumentError('Unsupported term format ${x['term']}');
      }
      var value = {
        for (var e in (x['value'] as Map).entries)
          Bucket.parse(e.key): e.value as num
      };
      add(IntervalTuple(term, value));
    }
  }

  /// Input document is of this form
  /// ```
  /// {
  ///   'terms': ['2020-01-15', ... '2020-02', ...],
  ///   'buckets': {
  ///     '5x16': [81.5, 80.17, ...],
  ///     '2x16H': [70.2, 67.32, ...],
  ///     '7x8': [45.7, 42.81, ...],
  ///   }
  /// }
  /// ```
  PriceCurve.fromMongoDocument(
      Map<String, dynamic> document, Location location) {
    var buckets = {
      for (String b in document['buckets'].keys) b: Bucket.parse(b)
    };
    final bKeys = buckets.keys.toList();
    var terms = document['terms'] as List;
    var xs = <IntervalTuple<Map<Bucket, num>>>[];
    for (var i = 0; i < terms.length; i++) {
      var value = <Bucket, num>{};
      for (var bucket in bKeys) {
        num? v = document['buckets'][bucket][i];
        if (v != null) {
          value[buckets[bucket]!] = v;
        }
      }
      Interval term;
      if (terms[i].length == 7) {
        term = Month.parse(terms[i], location: location);
      } else if (terms[i].length == 10) {
        term = Date.parse(terms[i], location: location);
      } else {
        throw ArgumentError('Unsupported term ${terms[i]}');
      }
      xs.add(IntervalTuple(term, value));
    }
    addAll(xs);
  }

  Set<Bucket>? _buckets;

  /// an hourly timeseries cache for the entire price curve
  TimeSeries<num>? _ts;

  @override
  Set<Bucket> get buckets {
    _buckets ??= <Bucket>{...expand((e) => e.value.keys)};
    return _buckets!;
  }

  /// Get the curve as an hourly timeseries.
  /// If [interval] is specified, only return the curve for that interval.
  /// Since this is an expensive operation, the complete hourly curve can be
  /// cached by triggering a call with a null [interval].  However, if you
  /// only want to calculate a two month strip price, it is an overkill to
  /// calculate the hourly curve for the entire domain which may be 10 years.
  /// In that case pass in the [interval] you are interested directly.
  ///
  /// If the forward curve contains only one bucket, say 2x16H, only the hours
  /// associated with that bucket will be returned in the interval.  Not having
  /// a complete covering of the interval is DISCOURAGED because the value
  /// calculation becomes suspect.  What is the value of the Offpeak bucket if
  /// the curve only has the 2x16H hours defined?  We could assume that
  /// the 7x8 hours have a value zero but it needs to be specified in the
  /// constructor.  This has not been assumed, so the Offpeak bucket price
  /// == 2x16H price in this situation!
  TimeSeries<num> toHourly({Interval? interval}) {
    if (_ts != null) {
      // Since the cache exists, window and exit
      if (interval == null) {
        return _ts!;
      } else {
        return TimeSeries<num>.fromIterable(_ts!.window(interval));
      }
    } else {
      var ts1 = TimeSeries<num>();
      if (buckets == {Bucket.b7x8, Bucket.b2x16H, Bucket.b5x16}) {
        // this is fastest
        buckets = {Bucket.b5x16, Bucket.b7x8, Bucket.b2x16H};
      }
      var aux = (interval == null) ? this : window(interval);
      for (var x in aux) {
        var hours = x.interval.splitLeft((dt) => Hour.beginning(dt));
        for (var hour in hours) {
          for (var bucket in buckets) {
            if (bucket.containsHour(hour)) {
              ts1.add(IntervalTuple(hour, x.value[bucket]!));
              break;
            }
          }
        }
      }
      if (interval == null) {
        _ts = ts1; // cache the result
      }
      return ts1;
    }
  }

  /// Calculate the value for this curve for any term and any bucket.
  ///
  /// If the forward curve contains only one bucket, say 2x16H, only the hours
  /// associated with that bucket will be counted in the value calculation.
  /// Not having a complete bucket covering of the interval is DISCOURAGED
  /// because the value calculation becomes suspect.  What is the value of the
  /// Offpeak bucket if the curve only has the 2x16H hours defined?  We could
  /// assume that the 7x8 hours have a value zero but it's better if this gets
  /// specified in the constructor.  This assumption has not been made,
  /// so the Offpeak bucket price == 2x16H price for this curve!
  ///
  /// See also the method [points] which returns the individual points for
  /// a given bucket before [interval] aggregation.
  num value(Interval interval, Bucket bucket) {
    var _domain = Interval(first.interval.start, last.interval.end);
    if (!_domain.containsInterval(interval)) {
      throw ArgumentError('Forward curve not defined for the entire $interval');
    }

    /// Take a few shortcuts if possible.  No speed improvements found if
    /// trying to calculate the ATC bucket or the Offpeak bucket separately.
    if (buckets.contains(bucket)) {
      return _calcValue(interval, bucket);
    } else {
      /// treat atc and offpeak separately since they are so common
      final _trio = {Bucket.b5x16, Bucket.b2x16H, Bucket.b7x8};
      if (bucket == Bucket.atc && buckets.containsAll(_trio)) {
        return _calcValueAtc(interval);
      }
      if (bucket == Bucket.offpeak &&
          buckets.containsAll({Bucket.b2x16H, Bucket.b7x8})) {
        return _calcValueOffpeak(interval);
      }
    }
<commit_msg>add check for location match in PriceCurve value method.<commit_after>part of elec.risk_system;

class PriceCurve extends TimeSeries<Map<Bucket, num>> with MarksCurve {
  static final DateFormat _isoFmt = DateFormat('yyyy-MM');

  /// A simple forward curve model for daily and monthly values extending
  /// a TimeSeries<Map<Bucket,num>>.  There are no gaps in the observations.
  PriceCurve();

  /// A simple forward curve model for daily and monthly values extending
  /// a TimeSeries<Map<Bucket,num>>.  There are no gaps in the observations.
  /// Support only daily and monthly observations.
  PriceCurve.fromIterable(Iterable<IntervalTuple<Map<Bucket, num>>> xs) {
    addAll(xs);
  }

  /// Construct a price curve if you have the data from different buckets.
  PriceCurve.fromBuckets(Map<Bucket, TimeSeries<num>> xs) {
    _buckets = xs.keys.toSet();
    // put the first bucket
    addAll(xs[_buckets!.first]!.map((e) =>
        IntervalTuple(e.interval, <Bucket, num>{_buckets!.first: e.value})));

    for (var bucket in _buckets!.skip(1)) {
      var out = merge(xs[bucket]!, joinType: JoinType.Outer, f: (x, dynamic y) {
        if (x == null) {
          return <Bucket, num>{bucket: y};
        } else if (y == null) {
          return x;
        } else {
          x[bucket] = y;
          return x;
        }
      });
      clear();
      addAll(out);
    }
  }

  /// Construct a forward curve given an input in this form.  The buckets
  /// can be different, but the covering needs to be complete (no gaps.)
  ///   [
  ///     {'term': '2020-07-17', 'value': {'5x16': 27.10, '7x8': 15.5}},
  ///     {'term': '2020-07-18', 'value': {'2x16H': 22.15, '7x8': 15.5}},
  ///     ...
  ///     {'term': '2020-08', 'value': {'5x16': 31.50, '2x16H': 25.15, '7x8': 18.75}},
  ///     ...
  ///   ]
  ///   The inputs are time-ordered with no gaps.
  PriceCurve.fromJson(List<Map<String, dynamic>> xs, Location location) {
    for (var x in xs) {
      Interval term;
      if ((x['term'] as String).length == 10) {
        term = Date.parse(x['term'], location: location);
      } else if ((x['term'] as String).length == 7) {
        term = Month.parse(x['term'], fmt: _isoFmt, location: location);
      } else {
        throw ArgumentError('Unsupported term format ${x['term']}');
      }
      var value = {
        for (var e in (x['value'] as Map).entries)
          Bucket.parse(e.key): e.value as num
      };
      add(IntervalTuple(term, value));
    }
  }

  /// Input document is of this form
  /// ```
  /// {
  ///   'terms': ['2020-01-15', ... '2020-02', ...],
  ///   'buckets': {
  ///     '5x16': [81.5, 80.17, ...],
  ///     '2x16H': [70.2, 67.32, ...],
  ///     '7x8': [45.7, 42.81, ...],
  ///   }
  /// }
  /// ```
  PriceCurve.fromMongoDocument(
      Map<String, dynamic> document, Location location) {
    var buckets = {
      for (String b in document['buckets'].keys) b: Bucket.parse(b)
    };
    final bKeys = buckets.keys.toList();
    var terms = document['terms'] as List;
    var xs = <IntervalTuple<Map<Bucket, num>>>[];
    for (var i = 0; i < terms.length; i++) {
      var value = <Bucket, num>{};
      for (var bucket in bKeys) {
        num? v = document['buckets'][bucket][i];
        if (v != null) {
          value[buckets[bucket]!] = v;
        }
      }
      Interval term;
      if (terms[i].length == 7) {
        term = Month.parse(terms[i], location: location);
      } else if (terms[i].length == 10) {
        term = Date.parse(terms[i], location: location);
      } else {
        throw ArgumentError('Unsupported term ${terms[i]}');
      }
      xs.add(IntervalTuple(term, value));
    }
    addAll(xs);
  }

  Set<Bucket>? _buckets;

  /// an hourly timeseries cache for the entire price curve
  TimeSeries<num>? _ts;

  @override
  Set<Bucket> get buckets {
    _buckets ??= <Bucket>{...expand((e) => e.value.keys)};
    return _buckets!;
  }

  /// Get the curve as an hourly timeseries.
  /// If [interval] is specified, only return the curve for that interval.
  /// Since this is an expensive operation, the complete hourly curve can be
  /// cached by triggering a call with a null [interval].  However, if you
  /// only want to calculate a two month strip price, it is an overkill to
  /// calculate the hourly curve for the entire domain which may be 10 years.
  /// In that case pass in the [interval] you are interested directly.
  ///
  /// If the forward curve contains only one bucket, say 2x16H, only the hours
  /// associated with that bucket will be returned in the interval.  Not having
  /// a complete covering of the interval is DISCOURAGED because the value
  /// calculation becomes suspect.  What is the value of the Offpeak bucket if
  /// the curve only has the 2x16H hours defined?  We could assume that
  /// the 7x8 hours have a value zero but it needs to be specified in the
  /// constructor.  This has not been assumed, so the Offpeak bucket price
  /// == 2x16H price in this situation!
  TimeSeries<num> toHourly({Interval? interval}) {
    if (_ts != null) {
      // Since the cache exists, window and exit
      if (interval == null) {
        return _ts!;
      } else {
        return TimeSeries<num>.fromIterable(_ts!.window(interval));
      }
    } else {
      var ts1 = TimeSeries<num>();
      if (buckets == {Bucket.b7x8, Bucket.b2x16H, Bucket.b5x16}) {
        // this is fastest
        buckets = {Bucket.b5x16, Bucket.b7x8, Bucket.b2x16H};
      }
      var aux = (interval == null) ? this : window(interval);
      for (var x in aux) {
        var hours = x.interval.splitLeft((dt) => Hour.beginning(dt));
        for (var hour in hours) {
          for (var bucket in buckets) {
            if (bucket.containsHour(hour)) {
              ts1.add(IntervalTuple(hour, x.value[bucket]!));
              break;
            }
          }
        }
      }
      if (interval == null) {
        _ts = ts1; // cache the result
      }
      return ts1;
    }
  }

  /// Calculate the value for this curve for any term and any bucket.
  ///
  /// If the forward curve contains only one bucket, say 2x16H, only the hours
  /// associated with that bucket will be counted in the value calculation.
  /// Not having a complete bucket covering of the interval is DISCOURAGED
  /// because the value calculation becomes suspect.  What is the value of the
  /// Offpeak bucket if the curve only has the 2x16H hours defined?  We could
  /// assume that the 7x8 hours have a value zero but it's better if this gets
  /// specified in the constructor.  This assumption has not been made,
  /// so the Offpeak bucket price == 2x16H price for this curve!
  ///
  /// See also the method [points] which returns the individual points for
  /// a given bucket **before** [interval] aggregation.
  num value(Interval interval, Bucket bucket) {
    var domain = Interval(first.interval.start, last.interval.end);
    if (interval.start.location != first.interval.start.location) {
      throw StateError('Timezone location of the PriceCurve does not match the interval timezone location used for the value calculation');
    }
    if (!domain.containsInterval(interval)) {
      throw ArgumentError('Forward curve not defined for the entire $interval');
    }

    /// Take a few shortcuts if possible.  No speed improvements found if
    /// trying to calculate the ATC bucket or the Offpeak bucket separately.
    if (buckets.contains(bucket)) {
      return _calcValue(interval, bucket);
    } else {
      /// treat atc and offpeak separately since they are so common
      final trio = {Bucket.b5x16, Bucket.b2x16H, Bucket.b7x8};
      if (bucket == Bucket.atc && buckets.containsAll(trio)) {
        return _calcValueAtc(interval);
      }
      if (bucket == Bucket.offpeak &&
          buckets.containsAll({Bucket.b2x16H, Bucket.b7x8})) {
        return _calcValueOffpeak(interval);
      }
    }

{'fraction_non_alphanumeric': 0.1372372565404641, 'fraction_numerical': 0.022369351417368386, 'mean_word_length': 2.390281106994988, 'pattern_counts': {'":': 0, '<': 59, '<?xml version=': 0, '>': 63, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6122466', 'n_tokens_mistral': 5046, 'n_tokens_neox': 4742, 'n_words': 1627}