Program Listing for File delay_lib.h

Return to documentation for file (src/tdm/delay_lib.h)

#pragma once
#include <algorithm>
#include <array>
#include <cctype>
#include <cmath>
#include <optional>
#include <stdexcept>
#include <string>
#include <vector>

#include "json.hpp"


enum class RatioLib {
  GIO,
  MGT
};

enum class DelayScope {
  Default,
  Board,
  InterBoard,
  InterCluster,
  InterRack
};

struct LinearModel {
  double a = 0.0, b = 0.0;
  bool valid = false;

  double eval(double x) const
  {
    return a * x + b;
  }
};

struct PWLModel {
  std::vector<double> xs;
  std::vector<double> ys;
  std::vector<double> slopes;
  bool valid = false;

  static PWLModel fromPoints(std::vector<double> x, std::vector<double> y)
  {
    PWLModel m;
    if (x.size() != y.size() || x.size() < 2) {
      m.valid = false;
      return m;
    }
    // sort & unique by x
    std::vector<std::pair<double, double>> kv;
    kv.reserve(x.size());
    for (size_t i = 0; i < x.size(); ++i) kv.push_back({x[i], y[i]});
    std::sort(kv.begin(), kv.end(),
              [](auto &a, auto &b) { return a.first < b.first; });
    // 去重相同x,保留最后一个
    std::vector<double> X, Y;
    X.reserve(kv.size());
    Y.reserve(kv.size());
    for (size_t i = 0; i < kv.size(); ++i) {
      if (!X.empty() && std::abs(kv[i].first - X.back()) < 1e-12) {
        Y.back() = kv[i].second;  // 覆盖
      } else {
        X.push_back(kv[i].first);
        Y.push_back(kv[i].second);
      }
    }
    if (X.size() < 2) {
      m.valid = false;
      return m;
    }
    m.xs = std::move(X);
    m.ys = std::move(Y);
    m.slopes.resize(m.xs.size() - 1);
    for (size_t i = 0; i < m.slopes.size(); ++i) {
      double dx   = m.xs[i + 1] - m.xs[i];
      m.slopes[i] = (m.ys[i + 1] - m.ys[i]) / dx;
    }
    m.valid = true;
    return m;
  }

  double eval(double r) const
  {
    if (!valid)
      throw std::runtime_error("PWLModel not valid");
    if (r <= xs.front()) {
      // 左端外推:用第一段斜率
      return ys.front() + (r - xs.front()) * slopes.front();
    }
    if (r >= xs.back()) {
      // 右端外推:用最后一段斜率
      return ys.back() + (r - xs.back()) * slopes.back();
    }
    // 中间插值:找到 r 所在的段
    auto it  = std::upper_bound(xs.begin(), xs.end(), r);
    size_t i = size_t(std::max<int>(0, int(it - xs.begin()) - 1));
    return ys[i] + (r - xs[i]) * slopes[i];
  }

  double slopeAt(double r) const
  {
    if (!valid)
      throw std::runtime_error("PWLModel not valid");
    if (r <= xs.front())
      return slopes.front();
    if (r >= xs.back())
      return slopes.back();
    auto it  = std::upper_bound(xs.begin(), xs.end(), r);
    size_t i = size_t(std::max<int>(0, int(it - xs.begin()) - 1));
    return slopes[i];
  }
};

struct LibModel {
  bool has_bypass     = false;
  double bypass_ratio = 2.0;
  double bypass_delay = 0.0;

  bool has_low_linear = false;
  double low_lo = 1.0, low_hi = 8.0;
  LinearModel low_linear;

  PWLModel pwl;

  LinearModel fallback;

  double delay(double r) const
  {
    // bypass 精确点
    if (has_bypass && std::abs(r - bypass_ratio) < 1e-12)
      return bypass_delay;

    // 低范围线性段
    if (has_low_linear && r >= low_lo && r <= low_hi && low_linear.valid) {
      return low_linear.eval(r);
    }
    // 主体 PWL
    if (pwl.valid)
      return pwl.eval(r);

    // 兜底 linear
    if (fallback.valid)
      return fallback.eval(r);

    throw std::runtime_error("LibModel has no valid model to evaluate delay()");
  }

  double slope(double r) const
  {
    // bypass 是一个离散点,没有“段”的斜率;可返回:
    //  - 若落在低范围线性:返回线性斜率
    //  - 否则用 PWL 的 slope或 fallback 的 a
    if (has_low_linear && r >= low_lo && r <= low_hi && low_linear.valid) {
      return low_linear.a;
    }
    if (pwl.valid)
      return pwl.slopeAt(r);
    if (fallback.valid)
      return fallback.a;
    throw std::runtime_error("LibModel has no valid model to evaluate slope()");
  }
};

struct LibMeta {
  double rmin = 1.0, rmax = 1e12;
  std::vector<int> choices;
  bool has_bounds = false;
};

struct ScopedLibModels {
  bool has_gio = false;
  bool has_mgt = false;
  LibModel gio;
  LibModel mgt;
  LibMeta gio_meta;
  LibMeta mgt_meta;
};

struct DelayLibrary {
  LibModel gio, mgt;
  LibMeta gio_meta, mgt_meta;
  static constexpr int kDelayScopeCount = 5;
  std::array<ScopedLibModels, kDelayScopeCount>
      scoped_models;
  bool has_scoped_models = false;

  static inline LinearModel fitLinear(const std::vector<double> &xs,
                                      const std::vector<double> &ys)
  {
    LinearModel lm;
    if (xs.size() < 2 || xs.size() != ys.size())
      return lm;
    double Sx = 0, Sy = 0, Sxx = 0, Sxy = 0;
    const size_t n = xs.size();
    for (size_t i = 0; i < n; ++i) {
      Sx += xs[i];
      Sy += ys[i];
      Sxx += xs[i] * xs[i];
      Sxy += xs[i] * ys[i];
    }
    double denom = n * Sxx - Sx * Sx;
    if (std::abs(denom) < 1e-12)
      return lm;
    lm.a     = (n * Sxy - Sx * Sy) / denom;
    lm.b     = (Sy - lm.a * Sx) / n;
    lm.valid = true;
    return lm;
  }

  static inline bool parsePoints(const nlohmann::json &arr,
                                 std::vector<double> &xs,
                                 std::vector<double> &ys)
  {
    if (!arr.is_array() || arr.empty())
      return false;
    xs.clear();
    ys.clear();
    xs.reserve(arr.size());
    ys.reserve(arr.size());
    for (const auto &p : arr) {
      if (p.is_array() && p.size() == 2) {
        xs.push_back(p[0].get<double>());
        ys.push_back(p[1].get<double>());
      } else if (p.is_object()) {
        if (!p.contains("ratio"))
          return false;
        double r = p["ratio"].get<double>();
        double d = 0.0;
        if (p.contains("delay"))
          d = p["delay"].get<double>();
        else if (p.contains("delay_ns"))
          d = p["delay_ns"].get<double>();
        else
          return false;
        xs.push_back(r);
        ys.push_back(d);
      } else
        return false;
    }
    return xs.size() >= 2;
  }

  static inline bool parseBypass(const nlohmann::json &b, double &r,
                                 std::optional<double> &dopt)
  {
    if (!b.is_object() || !b.contains("ratio"))
      return false;

    r = b["ratio"].get<double>();

    if (b.contains("delay")) {
      dopt = b["delay"].get<double>();
      return true;
    }
    if (b.contains("delay_ns")) {
      dopt = b["delay_ns"].get<double>();
      return true;
    }

    // 只有 ratio:允许,delay 由外部推导
    dopt = std::nullopt;
    return true;
  }

  static inline void parseMeta(const nlohmann::json &j, LibMeta &meta)
  {
    if (j.contains("min_ratio"))
      meta.rmin = j["min_ratio"].get<double>();
    if (j.contains("max_ratio"))
      meta.rmax = j["max_ratio"].get<double>();
    meta.has_bounds = j.contains("min_ratio") || j.contains("max_ratio");
    if (j.contains("choices") && j["choices"].is_array()) {
      meta.choices.clear();
      for (auto &c : j["choices"]) meta.choices.push_back(c.get<int>());
    }
  }

  static inline int scopeIndex(DelayScope scope)
  {
    return static_cast<int>(scope);
  }

  static inline std::string normalizeName(std::string name)
  {
    for (char &ch : name) {
      unsigned char uch = static_cast<unsigned char>(ch);
      if (std::isalnum(uch))
        ch = static_cast<char>(std::tolower(uch));
      else
        ch = '_';
    }
    return name;
  }

  static inline std::optional<DelayScope> parseScopeName(
      const std::string &name)
  {
    const std::string n = normalizeName(name);
    if (n == "default" || n == "base")
      return DelayScope::Default;
    if (n == "board" || n == "local" || n == "board_local")
      return DelayScope::Board;
    if (n == "inter_board" || n == "ibm" || n == "cluster_local")
      return DelayScope::InterBoard;
    if (n == "inter_cluster" || n == "icm")
      return DelayScope::InterCluster;
    if (n == "inter_rack" || n == "irm")
      return DelayScope::InterRack;
    return std::nullopt;
  }

  static inline double parseOffsetNs(const nlohmann::json &j)
  {
    if (j.contains("offset_ns"))
      return j["offset_ns"].get<double>();
    if (j.contains("delay_offset_ns"))
      return j["delay_offset_ns"].get<double>();
    if (j.contains("add_delay_ns"))
      return j["add_delay_ns"].get<double>();
    return 0.0;
  }

  static inline void addDelayOffset(LibModel &model, double offset_ns)
  {
    if (std::abs(offset_ns) < 1e-12)
      return;
    if (model.has_bypass)
      model.bypass_delay += offset_ns;
    if (model.has_low_linear && model.low_linear.valid)
      model.low_linear.b += offset_ns;
    if (model.pwl.valid) {
      for (double &y : model.pwl.ys) y += offset_ns;
    }
    if (model.fallback.valid)
      model.fallback.b += offset_ns;
  }

  static inline bool modelUsable(const LibModel &model, RatioLib lib)
  {
    if (lib == RatioLib::GIO) {
      return model.pwl.valid || model.fallback.valid || model.has_low_linear ||
             model.has_bypass;
    }
    return model.pwl.valid || model.fallback.valid;
  }

  static inline void inferBoundsFromPoints(const LibModel &model, LibMeta &meta)
  {
    if (model.pwl.valid && !meta.has_bounds) {
      meta.rmin       = model.pwl.xs.front();
      meta.rmax       = model.pwl.xs.back();
      meta.has_bounds = true;
    }
  }

  static inline void buildGioBypassAndLowLinear(const nlohmann::json &j,
                                                LibModel &model)
  {
    bool bypass_specified = false;
    if (j.contains("bypass")) {
      double br = 0.0;
      std::optional<double> bdopt;
      if (parseBypass(j["bypass"], br, bdopt)) {
        bypass_specified   = true;
        model.has_bypass   = true;
        model.bypass_ratio = br;
        if (bdopt.has_value()) {
          model.bypass_delay = *bdopt;
        } else if (model.pwl.valid) {
          const double tol = 1e-9;
          auto it =
              std::lower_bound(model.pwl.xs.begin(), model.pwl.xs.end(), br);
          if (it != model.pwl.xs.end() && std::abs(*it - br) < tol) {
            size_t idx         = size_t(it - model.pwl.xs.begin());
            model.bypass_delay = model.pwl.ys[idx];
          } else {
            model.bypass_delay = model.pwl.eval(br);
          }
        }
      }
    }

    if (!bypass_specified && model.pwl.valid && !model.pwl.xs.empty()) {
      model.has_bypass   = true;
      model.bypass_ratio = model.pwl.xs.front();
      model.bypass_delay = model.pwl.ys.front();
    }

    if (model.has_bypass && model.pwl.valid) {
      double left_r = model.bypass_ratio, left_d = model.bypass_delay;
      double right_r = -1, right_d = 0;
      for (size_t i = 0; i < model.pwl.xs.size(); ++i) {
        if (model.pwl.xs[i] > left_r + 1e-12) {
          right_r = model.pwl.xs[i];
          right_d = model.pwl.ys[i];
          break;
        }
      }
      if (right_r > 0 && std::abs(right_r - left_r) > 1e-12) {
        model.low_linear.valid = true;
        model.low_linear.a     = (right_d - left_d) / (right_r - left_r);
        model.low_linear.b     = left_d - model.low_linear.a * left_r;
        model.has_low_linear   = true;
        model.low_lo           = left_r;
        model.low_hi           = right_r;
      }
    }
  }

  static inline bool buildStandaloneModel(const nlohmann::json &j, RatioLib lib,
                                          LibModel &model, LibMeta &meta)
  {
    if (!j.is_object() || !j.contains("points"))
      return false;

    model = LibModel{};
    meta  = LibMeta{};

    std::vector<double> xs, ys;
    if (!parsePoints(j["points"], xs, ys))
      return false;
    model.pwl = PWLModel::fromPoints(xs, ys);
    parseMeta(j, meta);
    inferBoundsFromPoints(model, meta);

    if (lib == RatioLib::GIO) {
      buildGioBypassAndLowLinear(j, model);
      if (!model.fallback.valid && model.pwl.valid) {
        std::vector<double> fx, fy;
        for (size_t i = 0; i < model.pwl.xs.size(); ++i) {
          double r = model.pwl.xs[i], d = model.pwl.ys[i];
          if (!model.has_bypass || r > model.bypass_ratio + 1e-12) {
            fx.push_back(r);
            fy.push_back(d);
          }
        }
        if (fx.size() >= 2)
          model.fallback = fitLinear(fx, fy);
      }
    } else if (!model.fallback.valid && model.pwl.valid) {
      model.fallback = fitLinear(model.pwl.xs, model.pwl.ys);
    }

    addDelayOffset(model, parseOffsetNs(j));
    return modelUsable(model, lib);
  }

  static inline bool buildScopedModel(const nlohmann::json &j, RatioLib lib,
                                      const LibModel &base_model,
                                      const LibMeta &base_meta, LibModel &model,
                                      LibMeta &meta)
  {
    if (!j.is_object())
      return false;

    if (j.contains("points"))
      return buildStandaloneModel(j, lib, model, meta);

    if (j.contains("inherit") || j.contains("offset_ns") ||
        j.contains("delay_offset_ns") || j.contains("add_delay_ns")) {
      model = base_model;
      meta  = base_meta;
      addDelayOffset(model, parseOffsetNs(j));
      parseMeta(j, meta);
      return modelUsable(model, lib);
    }

    return false;
  }

  inline void initDefaultScopeModels()
  {
    auto &base    = scoped_models[scopeIndex(DelayScope::Default)];
    base.has_gio  = true;
    base.has_mgt  = true;
    base.gio      = gio;
    base.mgt      = mgt;
    base.gio_meta = gio_meta;
    base.mgt_meta = mgt_meta;
  }

  inline bool parseScopeModels(const nlohmann::json &j)
  {
    initDefaultScopeModels();
    if (!j.contains("scopes"))
      return true;
    if (!j["scopes"].is_object())
      return false;

    for (auto it = j["scopes"].begin(); it != j["scopes"].end(); ++it) {
      auto scope_opt = parseScopeName(it.key());
      if (!scope_opt.has_value())
        return false;

      const int idx  = scopeIndex(*scope_opt);
      const auto &sj = it.value();
      if (!sj.is_object())
        return false;

      ScopedLibModels scoped;
      if (sj.contains("gio")) {
        if (!buildScopedModel(sj["gio"], RatioLib::GIO, gio, gio_meta,
                              scoped.gio, scoped.gio_meta))
          return false;
        scoped.has_gio = true;
      }
      if (sj.contains("mgt")) {
        if (!buildScopedModel(sj["mgt"], RatioLib::MGT, mgt, mgt_meta,
                              scoped.mgt, scoped.mgt_meta))
          return false;
        scoped.has_mgt = true;
      }

      if (!scoped.has_gio && !scoped.has_mgt) {
        scoped.gio      = gio;
        scoped.mgt      = mgt;
        scoped.gio_meta = gio_meta;
        scoped.mgt_meta = mgt_meta;
        addDelayOffset(scoped.gio, parseOffsetNs(sj));
        addDelayOffset(scoped.mgt, parseOffsetNs(sj));
        scoped.has_gio = true;
        scoped.has_mgt = true;
      }

      scoped_models[idx] = std::move(scoped);
      has_scoped_models  = true;
    }
    return true;
  }

  bool buildFromJson(const nlohmann::json &j)
  {
    // 1) GIO
    if (!j.contains("gio") || !j["gio"].is_object())
      return false;
    {
      const auto &gj = j["gio"];
      std::vector<double> xs, ys;
      if (gj.contains("points")) {
        if (!parsePoints(gj["points"], xs, ys))
          return false;
        gio.pwl = PWLModel::fromPoints(xs, ys);
      }
      parseMeta(gj, gio_meta);
      // 若未显式给 rmin/rmax,则用 points 自动推断
      if (gio.pwl.valid && !gio_meta.has_bounds) {
        gio_meta.rmin       = gio.pwl.xs.front();
        gio_meta.rmax       = gio.pwl.xs.back();
        gio_meta.has_bounds = true;
      }
      // bypass 单独挂在 root 或 gio 下都支持
      // 同时支持:只给 ratio,不给 delay(delay 从 points/PWL 推导)
      auto apply_bypass = [&](const nlohmann::json &bj) -> bool {
        double br = 0.0;
        std::optional<double> bdopt;
        if (!parseBypass(bj, br, bdopt))
          return false;

        gio.has_bypass   = true;
        gio.bypass_ratio = br;

        if (bdopt.has_value()) {
          // JSON 显式给了 bypass delay:原行为
          gio.bypass_delay = *bdopt;
          return true;
        }

        // JSON 只给 ratio:delay 从 points/PWL 推导,避免重复定义
        // 注意:这里不要调用 discreteDelay(),因为 has_bypass 已经置 true,
        // discreteDelay 会优先返回 bypass_delay(尚未赋值,可能是 0)。
        if (gio.pwl.valid) {
          const double tol = 1e-9;
          auto it = std::lower_bound(gio.pwl.xs.begin(), gio.pwl.xs.end(), br);
          if (it != gio.pwl.xs.end() && std::abs(*it - br) < tol) {
            size_t idx       = size_t(it - gio.pwl.xs.begin());
            gio.bypass_delay = gio.pwl.ys[idx];
            return true;
          }
          // 找不到离散点就用 PWL 估值
          gio.bypass_delay = gio.pwl.eval(br);
          return true;
        }

        // 3) 再否则:没法推导
        return false;
      };

      bool bypass_specified = false;
      if (j.contains("bypass")) {
        bypass_specified = true;
        if (!apply_bypass(j["bypass"]))
          return false;
      } else if (gj.contains("bypass")) {
        bypass_specified = true;
        if (!apply_bypass(gj["bypass"]))
          return false;
      }

      // 如果 JSON 没写 bypass:默认把 points 的最小点当 bypass
      if (!bypass_specified) {
        if (gio.pwl.valid && gio.pwl.xs.size() >= 1) {
          gio.has_bypass   = true;
          gio.bypass_ratio = gio.pwl.xs.front();
          gio.bypass_delay = gio.pwl.ys.front();
        }
      }

      // 自动构造 GIO 低段线性(bypass -> 第一段 ≥bypass 的点)
      if (gio.has_bypass && gio.pwl.valid) {
        // 找到第一个 >= bypass_ratio 的 pwl 点
        double left_r = gio.bypass_ratio, left_d = gio.bypass_delay;
        double right_r = -1, right_d = 0;
        for (size_t i = 0; i < gio.pwl.xs.size(); ++i) {
          if (gio.pwl.xs[i] > left_r + 1e-12) {
            right_r = gio.pwl.xs[i];
            right_d = gio.pwl.ys[i];
            break;
          }
        }
        if (right_r > 0 && std::abs(right_r - left_r) > 1e-12) {
          gio.low_linear.valid = true;
          gio.low_linear.a     = (right_d - left_d) / (right_r - left_r);
          gio.low_linear.b     = left_d - gio.low_linear.a * left_r;
          gio.has_low_linear   = true;
          gio.low_lo           = left_r;
          gio.low_hi           = right_r;  // 例如 [1,8]
        }
      }

      // 线性回归 fallback(建议:GIO 用 r>=8 的点做回归,避免把直连混进去)
      if (!gio.fallback.valid && gio.pwl.valid) {
        std::vector<double> fx, fy;
        for (size_t i = 0; i < gio.pwl.xs.size(); ++i) {
          double r = gio.pwl.xs[i], d = gio.pwl.ys[i];
          if (!gio.has_bypass || r > gio.bypass_ratio + 1e-12) {
            fx.push_back(r);
            fy.push_back(d);
          }
        }
        if (fx.size() >= 2)
          gio.fallback = fitLinear(fx, fy);
      }
    }

    // 2) MGT
    if (!j.contains("mgt") || !j["mgt"].is_object())
      return false;
    {
      const auto &mj = j["mgt"];
      std::vector<double> xs, ys;
      if (mj.contains("points")) {
        if (!parsePoints(mj["points"], xs, ys))
          return false;
        mgt.pwl = PWLModel::fromPoints(xs, ys);
      }
      parseMeta(mj, mgt_meta);
      // 若未显式给 rmin/rmax,则用 points 自动推断
      if (mgt.pwl.valid && !mgt_meta.has_bounds) {
        mgt_meta.rmin       = mgt.pwl.xs.front();
        mgt_meta.rmax       = mgt.pwl.xs.back();
        mgt_meta.has_bounds = true;
      }
      if (!mgt.fallback.valid && mgt.pwl.valid) {
        mgt.fallback = fitLinear(mgt.pwl.xs, mgt.pwl.ys);
      }
    }

    // 最少保证能评估
    bool gio_ok = gio.pwl.valid || gio.fallback.valid || gio.has_low_linear ||
                  gio.has_bypass;
    bool mgt_ok = mgt.pwl.valid || mgt.fallback.valid;
    if (!(gio_ok && mgt_ok))
      return false;
    return parseScopeModels(j);
  }

  inline double delayGio(double r) const
  {
    return gio.delay(r);
  }
  inline double delayMgt(double r) const
  {
    return mgt.delay(r);
  }
  inline double slopeGio(double r) const
  {
    return gio.slope(r);
  }
  inline double slopeMgt(double r) const
  {
    return mgt.slope(r);
  }

  inline double clampGio(double r) const
  {
    return std::max(gio_meta.rmin, std::min(r, gio_meta.rmax));
  }
  inline double clampMgt(double r) const
  {
    return std::max(mgt_meta.rmin, std::min(r, mgt_meta.rmax));
  }
  inline double delay(RatioLib lib, double r) const
  {
    return (lib == RatioLib::GIO) ? delayGio(r) : delayMgt(r);
  }
  inline double slope(RatioLib lib, double r) const
  {
    return (lib == RatioLib::GIO) ? slopeGio(r) : slopeMgt(r);
  }

  inline const LibModel &modelFor(DelayScope scope, RatioLib lib) const
  {
    const int idx = scopeIndex(scope);
    if (idx >= 0 && idx < kDelayScopeCount) {
      const auto &scoped = scoped_models[idx];
      if (lib == RatioLib::GIO && scoped.has_gio)
        return scoped.gio;
      if (lib == RatioLib::MGT && scoped.has_mgt)
        return scoped.mgt;
    }
    return (lib == RatioLib::GIO) ? gio : mgt;
  }

  inline const LibMeta &metaFor(DelayScope scope, RatioLib lib) const
  {
    const int idx = scopeIndex(scope);
    if (idx >= 0 && idx < kDelayScopeCount) {
      const auto &scoped = scoped_models[idx];
      if (lib == RatioLib::GIO && scoped.has_gio)
        return scoped.gio_meta;
      if (lib == RatioLib::MGT && scoped.has_mgt)
        return scoped.mgt_meta;
    }
    return (lib == RatioLib::GIO) ? gio_meta : mgt_meta;
  }

  inline double delay(DelayScope scope, RatioLib lib, double r) const
  {
    return modelFor(scope, lib).delay(r);
  }

  inline double slope(DelayScope scope, RatioLib lib, double r) const
  {
    return modelFor(scope, lib).slope(r);
  }

  inline double delayGio(DelayScope scope, double r) const
  {
    return delay(scope, RatioLib::GIO, r);
  }

  inline double delayMgt(DelayScope scope, double r) const
  {
    return delay(scope, RatioLib::MGT, r);
  }

  inline double slopeGio(DelayScope scope, double r) const
  {
    return slope(scope, RatioLib::GIO, r);
  }

  inline double slopeMgt(DelayScope scope, double r) const
  {
    return slope(scope, RatioLib::MGT, r);
  }

  inline double minRatio(RatioLib lib) const
  {
    return (lib == RatioLib::GIO) ? gio_meta.rmin : mgt_meta.rmin;
  }
  inline double maxRatio(RatioLib lib) const
  {
    return (lib == RatioLib::GIO) ? gio_meta.rmax : mgt_meta.rmax;
  }

  inline double minRatio(DelayScope scope, RatioLib lib) const
  {
    return metaFor(scope, lib).rmin;
  }

  inline double maxRatio(DelayScope scope, RatioLib lib) const
  {
    return metaFor(scope, lib).rmax;
  }

  inline std::vector<int> choices(RatioLib lib) const
  {
    const LibMeta &meta = (lib == RatioLib::GIO) ? gio_meta : mgt_meta;
    const LibModel &mod = (lib == RatioLib::GIO) ? gio : mgt;

    if (!meta.choices.empty())
      return meta.choices;

    // 没给 choices,则根据 PWL 的 xs 自动生成(并加入 bypass 点)
    std::vector<int> out;
    if (mod.pwl.valid) {
      out.reserve(mod.pwl.xs.size() + 1);
      for (double r : mod.pwl.xs) out.push_back((int)std::llround(r));
    }
    if (mod.has_bypass)
      out.push_back((int)std::llround(mod.bypass_ratio));

    // 去重、排序、移除非正
    std::sort(out.begin(), out.end());
    out.erase(std::unique(out.begin(), out.end()), out.end());
    out.erase(
        std::remove_if(out.begin(), out.end(), [](int x) { return x <= 0; }),
        out.end());
    return out;
  }

  inline std::vector<int> choices(DelayScope scope, RatioLib lib) const
  {
    const LibMeta &meta = metaFor(scope, lib);
    const LibModel &mod = modelFor(scope, lib);

    if (!meta.choices.empty())
      return meta.choices;

    std::vector<int> out;
    if (mod.pwl.valid) {
      out.reserve(mod.pwl.xs.size() + 1);
      for (double r : mod.pwl.xs) out.push_back((int)std::llround(r));
    }
    if (mod.has_bypass)
      out.push_back((int)std::llround(mod.bypass_ratio));

    std::sort(out.begin(), out.end());
    out.erase(std::unique(out.begin(), out.end()), out.end());
    out.erase(
        std::remove_if(out.begin(), out.end(), [](int x) { return x <= 0; }),
        out.end());
    return out;
  }

  inline bool hasChoice(RatioLib lib, int r) const
  {
    auto cs = choices(lib);
    return std::find(cs.begin(), cs.end(), r) != cs.end();
  }

  inline bool hasChoice(DelayScope scope, RatioLib lib, int r) const
  {
    auto cs = choices(scope, lib);
    return std::find(cs.begin(), cs.end(), r) != cs.end();
  }

  inline std::optional<double> discreteDelay(RatioLib lib, double r) const
  {
    return discreteDelay(DelayScope::Default, lib, r);
  }

  inline std::optional<double> discreteDelay(DelayScope scope, RatioLib lib,
                                             double r) const
  {
    const LibModel &mod = modelFor(scope, lib);
    const double tol    = 1e-9;

    // 先查 points(更客观)
    if (mod.pwl.valid) {
      auto it = std::lower_bound(mod.pwl.xs.begin(), mod.pwl.xs.end(), r);
      if (it != mod.pwl.xs.end() && std::abs(*it - r) < tol) {
        size_t idx = size_t(it - mod.pwl.xs.begin());
        return mod.pwl.ys[idx];
      }
    }

    // 再查 bypass(避免覆盖 points 的同 ratio)
    if (mod.has_bypass && std::abs(r - mod.bypass_ratio) < tol)
      return mod.bypass_delay;

    return std::nullopt;
  }
};