| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138 |
- # -*- coding: utf-8 -*-
- """price.py — 柱2「智能排程」电价层: 参数化假定价形状 + 接真电价 (provenance→verdict 联动).
- B1 排程机会成本 = Σ(功率×电价)。此前价形状写死在 runner(maint_schedule_gjd 280×五段式);
- 本模块把价形状抽成 config 驱动(value_assumptions.price) + 留真电价接口:
- - 真价源在 (spot.source 文件 / spot.hourly_real dict) → 用真价, provenance=real → 【结算】
- - 否则 benchmark × tou_factor × season 展开假定形状 → provenance=assumed → 【参考·参数假定·待业主结算价】
- verdict 自动联动: ¥ 用假定价则交付降【参考·假定】; 真结算价到货 → 切【结算】 (不改一行排程逻辑)。
- 纯函数 (build_price_by_hour): 除 spot.source 显式文件路径外不碰 IO/时钟; 展开逻辑无副作用。
- 配套: templates/value_layer_assumptions.example.yaml (价形状模板) / schedule.py (消费 price_by_hour)。
- """
- import json
- from pathlib import Path
- ASSUMED_TAG = '【参考·参数假定·待业主结算价】'
- SETTLEMENT_TAG = '【结算】'
- def _parse_hours(spec):
- """'00-08' | '08-17,22-24' | '23-2'(跨零点) → set(hours) ⊂ [0,24)。半开 [a,b)。"""
- out = set()
- for part in str(spec or '').split(','):
- part = part.strip()
- if not part:
- continue
- if '-' in part:
- a, b = (int(x) for x in part.split('-', 1))
- rng = range(a, b) if b > a else list(range(a, 24)) + list(range(0, b)) # 跨零点
- out.update(h % 24 for h in rng)
- else:
- out.add(int(part) % 24)
- return out
- def _parse_months(spec):
- """'7-8' | '12-1'(跨年) | '3' → set(months) ⊂ [1,12]。闭区间。"""
- out = set()
- for part in str(spec or '').split(','):
- part = part.strip()
- if not part:
- continue
- if '-' in part:
- a, b = (int(x) for x in part.split('-', 1))
- out.update(range(a, b + 1) if b >= a else list(range(a, 13)) + list(range(1, b + 1)))
- else:
- out.add(int(part))
- return out
- def _season_factor(season_cfg, month):
- """month → (factor, name)。命中具名季则用其 factor, 否则 normal(缺省1.0)。"""
- if not isinstance(season_cfg, dict) or month is None:
- return 1.0, 'normal'
- for name, band in season_cfg.items():
- if name == 'normal' or not isinstance(band, dict):
- continue
- if band.get('months') and month in _parse_months(band['months']):
- return float(band.get('factor', 1.0)), name
- return float((season_cfg.get('normal') or {}).get('factor', 1.0)), 'normal'
- def _load_source(source):
- """真电价源 → {hour: 元/MWh}。支持: dict 直传 / .json({hour:price}) / .csv(hour,price 两列)。
- 不存在/null/不支持 → None (回落假定形状)。"""
- if not source:
- return None
- if isinstance(source, dict):
- return {int(k): float(v) for k, v in source.items()}
- p = Path(str(source))
- if not p.is_file():
- return None
- try:
- if p.suffix == '.json':
- d = json.loads(p.read_text(encoding='utf-8'))
- return {int(k): float(v) for k, v in d.items()}
- if p.suffix == '.csv':
- out = {}
- for ln in p.read_text(encoding='utf-8').splitlines()[1:]: # 跳表头
- cells = ln.split(',')
- if len(cells) >= 2:
- out[int(float(cells[0]))] = float(cells[1])
- return out or None
- except (ValueError, OSError, json.JSONDecodeError):
- return None
- return None
- def build_price_by_hour(price_cfg, month=None):
- """价 config (value_assumptions.price) → ({hour: 元/MWh}, provenance)。
- provenance: {mode, tariff_anchor, assumed(bool), verdict_tag, season, note}。
- ★接真电价: spot.hourly_real(dict) 或 spot.source(文件) 在 → 用真价, assumed=False, 【结算】。
- ★假定形状: benchmark × tou_factor(时段) × season_factor(月) 展开 24 时; 任一 assumed 标 → 整体假定。
- """
- if not isinstance(price_cfg, dict):
- return {}, {'error': 'price_cfg 非 dict', 'verdict_tag': 'INSUFFICIENT'}
- spot = price_cfg.get('spot') or {}
- real = spot.get('hourly_real') or _load_source(spot.get('source'))
- if real:
- price = {int(h) % 24: float(p) for h, p in real.items()}
- return price, {'mode': 'real', 'tariff_anchor': None, 'assumed': False,
- 'verdict_tag': SETTLEMENT_TAG, 'season': None,
- 'note': f'真电价 (源={spot.get("source") or "hourly_real"}, {len(price)}时段)'}
- bench = price_cfg.get('benchmark_cny_per_mwh')
- if bench is None:
- return {}, {'error': '缺 benchmark_cny_per_mwh 且无真价源', 'verdict_tag': 'INSUFFICIENT'}
- tou = spot.get('tou_factor') or {}
- hourly_factor = {h: 1.0 for h in range(24)}
- for band in tou.values():
- if isinstance(band, dict):
- for h in _parse_hours(band.get('hours', '')):
- hourly_factor[h] = float(band.get('factor', 1.0))
- sf, sname = _season_factor(spot.get('season_factor'), month)
- price = {h: round(float(bench) * hourly_factor[h] * sf, 2) for h in range(24)}
- assumed = bool(price_cfg.get('benchmark_assumed', True) or spot.get('tou_assumed', True)
- or spot.get('season_assumed', True))
- return price, {
- 'mode': 'assumed_shape', 'tariff_anchor': float(bench), 'assumed': assumed,
- 'season': sname, 'season_factor': sf,
- 'verdict_tag': ASSUMED_TAG if assumed else SETTLEMENT_TAG,
- 'note': f'benchmark {bench}元/MWh × tou × season({sname}×{sf}) 展开; '
- + ('参数假定→待业主结算价' if assumed else '标定为真'),
- }
- def stamp_schedule_verdict(sched_result, provenance):
- """schedule.optimize_schedule 结果 + 价 provenance → 打交付 verdict (¥ 用假定价则降级)。
- 排程逻辑不变(数字照算), 只在交付层据价来源标 verdict — 真价到货切【结算】不改一行。"""
- if not isinstance(sched_result, dict):
- return sched_result
- out = dict(sched_result)
- out['price_provenance'] = provenance
- out['delivery_verdict'] = provenance.get('verdict_tag', ASSUMED_TAG)
- base = out.get('_note', '')
- out['_note'] = f'{base} | 价格: {provenance.get("note", "")} → {out["delivery_verdict"]}'
- return out
|