2026年电工杯数学建模A题绿电直连型电氢氨园区优化运行求解全过程论文及程序

2026年电工杯数学建模

A题 绿电直连型电氢氨园区优化运行

原题再现:

  随着"双碳"战略的深入推进,新能源大规模并网消纳难的问题日益凸显。绿电直连项目为探索促进新能源就近消纳提供了新路径。绿电直连项目分为并网型和离网型两类,并网型项目作为整体接入公共电网,与公共电网形成清晰的物理界面与责任界面,电源应接入用户和公共电网产权分界点的用户侧。
  国家发展改革委、国家能源局文件《关于有序推动绿电直连发展有关事项的通知》对绿电直连项目的"新能源年自发自用电量占总可用发电量比例"、"总用电量绿电比例"、"新能源上网电量占总可用发电量的比例"等指标有明确要求,各指标可参考如下公式计算: 新能源自发自用电量占总可用发电量比例=(总用电量---上网电量---网购电量)/新能源发电量(要求大于60%); 总用电量绿电比例=(新能源发电量---上网电量)/总用电量(要求大于30%); 新能源上网电量比例=上网电量/新能源发电量(要求小于20%)。

  由于氨相较于氢更易于储运,"绿电---绿氢---绿氨"一体化绿电直连化工园区正在成为解决新能源消纳和化工行业深度脱碳的重要路径。 一个典型的绿电直连制氢氨项目包含电解制氢、制氨、风力发电、光伏发电、本地负荷及联网线路,结构如图1所示。 若园区的制氨初始产能为36吨/日,与此配套的碱性电解槽功率10MW(140 kg/h),质子交换膜电解槽功率10MW(160 kg/h),合成氨装置功率0.75MW(1.5吨/h),园区其他常规用电负荷峰值为6MW、标幺功率曲线如附件1所示;园区风电装机容量为40MW、光伏装机64MW,典型日风、光出力标幺功率曲线如附件2所示;考虑风光功率具有强随机波动性,给定6种不同出力水平的风电、4种不同出力水平光伏的日功率曲线分别如附件3、4所示,据此可生成24种不同风、光出力场景,园区运行分析时段为1小时。 园区制氨产能扩容过程中,配套电氢氨装置的额定功率将随产能规模呈线性同步提升。绿电直连园区的风、光发电优先供给区域内制氢、制氨负荷以及本地电负荷,不足电量从外部电网购电(网购电量),余电可上网(上网电量),需要满足绿电直连项目指标要求。如果电制氢氨的一日生产过程中,园区既有电网购电也有余电,若园区改为离网运行,仅靠风光发电难以支撑园区生产运行的全部用电需求,增配储能可能缓解或者解决这个问题。 风光发电与制氢设备技术参数如附件5所示,储能设备和合成氨装置技术参数如附件6所示,峰、平、谷分时电价参数如附件7所示,风电、光伏余电上网电价如附件8所示。
  问题一:典型风光场景下的绿电直连电氢氨园区运行指标分析 假设电解槽与合成氨装置每日满负荷连续运行,不计园区功率损耗,根据功率平衡原则:
  (1)计算园区典型日用电负荷功率、风光发电功率、购电、售电的功率并绘制曲线;
  (2)计算园区典型日用电量、新能源发电量、网购电量、上网电量,新能源自发自用电量占总可用发电量比例、总用电量绿电比例、新能源上网电量比例和吨氨成本,分析上述指标是否满足绿电直连项目的要求,如有指标不满足,请分析原因。
  问题二:基于离散制氨调节的绿电直连型电氢氨园区运行优化 若园区制氨产能增至72吨/日,制氨产量从72吨/日起,按9吨/日递减至36吨/日,电氢氨装置只有全额开机和停机两种运行方式,分析以下问题:
  (1)典型风光场景下,给出每一种产量下成本最低的生产时段安排及绿电指标,找出使吨氨成本最低的日产量,分析这个日产量对应的生产方案的绿电直连指标合格情况及制氢氨设备的利用率情况。
  (2)24种风光出力场景下,分析每种产量下最优的制氨生产时段安排、相应的绿电直连指标、吨氨成本、园区日购电、售电等指标的分布情况及特征;假设每一种风光场景代表 15天,按全满足、部分满足、全不满足三类情况统计分析全年绿电直连指标状况,绘制园区全年的吨氨成本分布曲线并计算全年总吨氨成本。
  问题三:多种场景下基于连续制氨调节的绿电直连型电氢氨园区运行分析 园区制氨产能增至72吨/日,制氨产量从72吨/日起,按9吨/日递减至36吨/日,电氢氨装置功率连续可调(下限为10%),在24种风光组合出力场景下,分析以下问题:
  (1)计算每种日发电场景下园区最优的制氨用电功率调度方案、相应的绿电直连指标和吨氨成本。按全满足、部分满足、全不满足三类情况统计分析全年绿电直连指标状况,绘制园区全年的吨氨成本分布曲线并计算全年总吨氨成本。
  (2)从园区日购电、售电、吨氨成本、绿电直连指标等角度,分析各种场景运行状况及原因。
  (3)与问题二(2)的计算结果相比,分析吨氨成本、绿电直连指标等的变化情况,并说明其原因。
  问题四:绿电直连型电氢氨园区离网运行分析及储能配置研究
园区离网运行时,园区用能受制于风光发电功率。在给定的24种风光出力场景、制氨产
能72吨/日条件下,分析以下问题:
  (1)假设电制氢氨装置用电功率连续可调(下限为10%),在尽限利用风光发电条件下,计算每种风光出力场景下园区制氨的产量和吨氨成本、全年制氨总量和氢氨年平均产能利用率,分析园区能源自给状况和风光发电利用状况,估算园区能源自治的最小风、光装机容量。
  (2)针对问题(1)中最大弃电场景,给出储能容量最优配置方案,制定24个场景下有储能参与生产运行功率最优调度方案,并计算各场景的吨氨成本、全年制氨总量和氢氨年平均产能利用率,并分析由储能带来的园区能源自给和风光发电利用改善状况。
  (3)基于上述储能配置结果,在满足相同制氨产量需求前提下,基于全年吨氨成本对比分析园区离网和联网两种运行模式的经济性以及系统支撑的成本价值。
  问题五:
  在国家政策的鼓励推动下,绿电直连园区会快速发展,请分析绿电园区容量渗透率显著提高后,对其所接入电力系统运行会产生哪些影响(至少指出三种利弊)?你有什么进一步推动绿电直连园区发展的政策建议并给出论证依据。

整体求解过程概述(摘要)

  针对子问题一(未来5分钟收益率预测),为了提取分钟OHLCV中的微观价格惯性、局部反转、波动聚集与量价耦合信息,首先实施时间戳标准化、重复样本消除、OHLC逻辑一致性校验和成交量组内插补;随后构造对数收益滞后、多尺度动量、实现波动率、Parkinson与Garman-Klass区间波动率、K线形态、成交量异常、Amihud流动性以及横截面秩等72项特征。鉴于短周期收益具有高噪声和弱信号特征,引入带RobustScaler的Ridge回归作为低方差基准,并与HistGradientBoosting、ExtraTrees及融合模型比较。仿真实验中,验证集预先选定Ridge;其测试RMSE为0.001593,较零预测基线降低6.34%,时点平均RankIC为0.335,方向准确率为61.49%。
  针对子问题二(未来60分钟收益率预测),考虑到预测窗口显著增长,短期K线噪声的相对影响下降,而慢趋势、波动状态、下行风险与市场共振的重要性上升。在问题一统一清洗和基础特征体系上进一步引入120分钟动量、波动率的波动率、下行波动率、市场平均收益和跨标的相对强弱特征,避免重复执行相同的数据处理。鉴于原始回归振幅容易因行情均值漂移而失真,引入仅基于验证集估计的横截面秩校准,并按综合损失选择Ridge与HistGB融合模型。测试集中融合模型RMSE为0.007949,时点平均RankIC为0.426,方向准确率为61.21%;同时,单一Ridge的测试RMSE为0.007808,说明长周期任务存在"排序能力较强而幅值估计仍需收缩"的典型特征。
  针对子问题三(普通行情、极端行情与多市场联合建模),为了兼顾尖峰厚尾分布下的风险识别能力,本文以训练集绝对收益95%分位数定义极端样本,并采用连续尾部加权损失提高模型对大幅波动的关注度;同时按权益类、期货类、数字资产类进行分组检验,考察联合模型的迁移能力。结果表明,5分钟Ridge在极端样本上的RMSE由基线0.006964降至0.006221,极端方向准确率达到82.08%;60分钟融合模型在三类市场中均保持正的时点平均RankIC。进一步的特征噪声扰动与极端阈值敏感性分析表明,输入噪声提高至20%时,两任务RMSE增幅仍较小,但极端阈值越高,有效尾部样本数快速下降,提示真实竞赛中应采用分层交叉验证和置信区间报告。
  综上,本文构建了"数据理解与探索---数据预处理---特征工程---模型构建---模型训练与评估---结果解释与洞察"的完整可复现流程。模型在短周期任务中突出低方差线性结构,在长周期任务中通过多尺度状态特征与横截面校准增强排序稳定性,并以净化时间划分、零预测基线、多模型对照、尾部评价、市场分组和噪声扰动完成验证。该方案能够直接替换为官方数据重新训练,但仿真结果只用于方法展示,最终成绩应以评测平台统一执行为准。

模型假设:

  假设1:信息可得性。在时刻t预测未来收益时,Open_t、High_t、Low_t、Close_t、Volume_t以及t之前的历史记录均已完整可得;任何t之后的价格、成交量和标签均不可进入特征。
  假设2:匿名标的一致性。asset_id在训练、验证和测试文件中保持稳定,但其真实身份未知且不需要识别;市场类别若由数据提供,可作为内部特征使用。
  假设3:价格基准化不改变收益结构。题目已经对价格字段进行统一基准化,乘法尺度变换不改变对数收益、OHLC相对区间和未来收益率,因此本文主要使用无量纲量价特征。
  假设4:时间戳可排序。所有记录的时间戳能够转换为统一时间类型;同一标的同一时间戳若重复,保留最后一条记录作为数据更新后的最终值。
  假设5:极端状态由训练集定义。极端行情阈值只能由训练集绝对标签分布估计,本文采用95%分位数;验证与测试阶段沿用该阈值,不重新查看其标签分布。
  假设6:交易成本不属于评分目标。题目要求预测收益率而非构造可交易策略,因此多空累计曲线仅用于验证预测排序,不作为实际投资收益;不引入手续费、冲击成本或持仓约束。
  假设7:仿真数据只用于流程复现。当前数值实验使用研究型仿真数据,仿真机制包含慢趋势、波动聚集、跳跃延续和量价耦合,但其参数不代表任何真实市场。

问题分析:

  问题一分析
  本题属于确定性逐时功率平衡与多维度绿电指标核算建模问题,是整套园区调度体系的数据基准层。核心矛盾为风光日内出力与连续电氢氨负荷时序严重错配,仅依靠日电量总和无法真实反映园区购售电交互规律;同时题目存在两套口径绿电指标,易出现核算偏差。建模思路:统一标幺风光、负荷序列转换为实际功率,建立小时级节点功率守恒方程,天然实现 "新能源自用优先、缺额购电、余量上网、同时段不双向交易" 物理约束;拆分新能源固定成本、分时购电成本、设备运维、余电上网收益四大收支项构建吨氨成本核算公式;严格遵循赛题指定三套绿电比例计算公式同步核算,对比工程常规自发自用率做口径差异校验;输出典型日逐时功率曲线、电量结构、成本构成与指标达标情况,为后续离散、连续优化提供统一数据、成本、指标计算底层工具,规避多问题间核算口径不统一问题。
  问题二分析
  本题属于 0-1 整数规划机组组合优化问题,完全复用问题一功率平衡、成本核算全套底层逻辑,新增设备全开全停离散约束与多风光场景泛化需求。核心难点:电氢氨装置仅能整小时启停,无小时内功率调节能力,日产量固定对应总开机时长,经济最优时段选择存在组合枚举爆炸;低成本风光时段与高峰电价时段天然冲突,低日产量经济性最优但绿电指标普遍不达标,形成成本与政策指标二元权衡。建模思路:将单时段制氨增量成本作为排序依据,通过交换论证证明选取增量成本最低时段即为全局最优,跳过全量枚举大幅降低计算量;构建 6 风电 ×4 光伏共 24 组风光组合场景,按单场景 15 天折算 360 代表年批量测算;统计不同日产量下全年加权吨氨成本、绿电指标合格场景数量,绘制成本分布热力与分类堆叠图,量化离散启停模式下 "低成本低自用、高负荷高指标" 固有矛盾,为柔性连续调节方案提供对照基线。
  问题三分析
  本题属于带互斥约束混合整数线性规划 (MILP) 优化问题,在问题二 72t/d 扩容设备框架基础上,放开设备 0/1 启停限制,设置 10%~100% 连续负荷调节区间。核心难点:分时购电价低于上网电价,无约束优化会产生无意义套利购售;连续可行域包含全部离散方案,理论上成本必然优于离散模型,但调节下限会压缩优化空间;柔性负荷可同步削减购、售双向电量,同步改善经济性与绿电指标。建模思路:引入 0-1 二元状态变量搭配大 M 约束实现同时段购电、售电互斥,消除套利漏洞;以日总产量等式约束绑定总制氨规模,建立小时连续负荷率优化变量;对 24 场景、5 档日产量批量求解,对比离散模型吨氨成本降幅、指标合格场景增量;绘制负荷率热力、购售电散点图解释柔性负荷削峰填谷、减少双向能量交换的内在机理,量化设备连续可调的经济与政策双重收益。
  问题四分析
  本题属于离网源荷储时序优化与储能容量二分寻优复合问题,脱离公共电网能量支撑,新增储能充放电、SOC 跨时段耦合状态约束。核心矛盾:无电网缓冲时夜间风光出力不足会强制降载停机,白天光伏过剩产生大量弃电;储能可实现日内电量时空平移,但新增摊销成本,自治性与经济性存在权衡;仅靠风光扩容实现满产装机规模极不经济。建模思路:先搭建无储能解析调度模型,逐小时匹配风光与负荷得到全年制氨量、弃电量;引入充放电效率、1C 功率、日内 SOC 循环边界储能约束,构建以近零弃电为目标的两阶段 MILP;采用二分搜索最小储能容量,刻画容量与弃电量边际衰减关系;分别测算无储能、配套储能两套离方案产能、利用率、吨氨成本,并构建同等制氨产量的联网对照算例,量化公共电网提供能量平衡、备用容量的经济价值,明确储能在提升新能源消纳、能源自治层面的定位。
  问题五分析
  本题属于高渗透率直连园区系统机理与配套政策定性定量综合分析问题,依托前四问调度量化结论展开电网层面拓展研究。核心难点:单园区运行最优目标与区域电网安全、成本分摊目标不一致,高比例电力电子设备带来潮流、惯量、保护多重系统风险,现有电价机制无法公平分摊电网备用成本。建模分析思路:从就地消纳、柔性负荷、绿色溯源三大正面价值,反向潮流、功率爬坡、低惯量、保护整定、交叉补贴五大系统风险分层梳理机理;结合前序场景调度数据佐证柔性负荷、储能对双向功率波动的削减作用;从接入规划、设备柔性强制标准、电价辅助服务机制、电能质量管控、绿电溯源、多元试点六大维度给出可落地政策方案,实现园区微观调度优化到电力系统宏观治理的完整逻辑闭环。

模型的建立与求解整体论文缩略图

全部论文请见下方" 只会建模 QQ名片" 点击QQ名片即可

程序代码:

部分程序如下:
python 复制代码
from __future__ import annotations

import csv
import json
import math
import os
import re
import zipfile
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Dict, List, Sequence, Tuple

import matplotlib.pyplot as plt
from matplotlib import font_manager
import numpy as np
from scipy.optimize import Bounds, LinearConstraint, linprog, milp


# ============================== 0. 全局设置 ==============================
BASE_DIR = Path(__file__).resolve().parent
OUT_DIR = BASE_DIR / "模型输出"
FIG_DIR = OUT_DIR / "图表"
TABLE_DIR = OUT_DIR / "结果表"

FILES = {
    "load": BASE_DIR / "附件1:园区典型日常规电负荷标幺功率曲线.xlsx",
    "typical": BASE_DIR / "附件2:典型日风电、光伏标幺功率表.xlsx",
    "wind_scen": BASE_DIR / "附件3:园区6种场景的风电标幺功率表.xlsx",
    "pv_scen": BASE_DIR / "附件4:园区4种场景的光伏标幺功率表.xlsx",
    "device1": BASE_DIR / "附件5:风光发电与制氢设备技术参数.xlsx",
    "device2": BASE_DIR / "附件6:储能设备和合成氨装置技术参数.xlsx",
    "tou": BASE_DIR / "附件7:分时电价表.xlsx",
    "feedin": BASE_DIR / "附件8:风电、光伏余电上网电价.xlsx",
}

# 设备与容量常数(题面及附件)
WIND_CAP = 40.0              # MW
PV_CAP = 64.0                # MW
LOCAL_LOAD_PEAK = 6.0        # MW
INITIAL_PROCESS_POWER = 20.75  # MW,36 t/d:10 MW ALK + 10 MW PEM + 0.75 MW 合成氨
EXPANDED_PROCESS_POWER = 41.5  # MW,72 t/d 线性扩容后
INITIAL_NH3_RATE = 1.5       # t/h
EXPANDED_NH3_RATE = 3.0      # t/h
WIND_LCOE = 0.15             # 元/kWh
PV_LCOE = 0.12               # 元/kWh
FEEDIN_PRICE = 0.3779        # 元/kWh
ALK_OM = 0.10                # 元/kWh
PEM_OM = 0.15                # 元/kWh
NH3_OM = 0.002               # 元/kWh
PROCESS_OM_FULL_INITIAL = 10_000 * ALK_OM + 10_000 * PEM_OM + 750 * NH3_OM
PROCESS_OM_FULL_EXPANDED = 20_000 * ALK_OM + 20_000 * PEM_OM + 1_500 * NH3_OM
STORAGE_INVEST = 1000.0      # 元/kWh
STORAGE_LIFE = 15            # 年
REP_DAYS = 360               # 24 场景×15 天
STORAGE_OM = 0.01            # 元/kWh 吞吐量
ETA_C = 0.90
ETA_D = 0.90
SELF_LOSS_DAILY = 0.002      # 0.2%/d,换算到小时
MIN_PROCESS_FRAC = 0.10
PRODUCTIONS = [36, 45, 54, 63, 72]


def setup_matplotlib() -> None:
    """配置中文字体和论文级绘图参数。"""
    font_path = "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc"
    if os.path.exists(font_path):
        font_manager.fontManager.addfont(font_path)
        plt.rcParams["font.sans-serif"] = [font_manager.FontProperties(fname=font_path).get_name()]
    else:
        plt.rcParams["font.sans-serif"] = ["DejaVu Sans"]
    plt.rcParams["axes.unicode_minus"] = False
    plt.rcParams["figure.dpi"] = 150
    plt.rcParams["savefig.dpi"] = 220
    plt.rcParams["axes.grid"] = True
    plt.rcParams["grid.alpha"] = 0.25
    plt.rcParams["axes.titleweight"] = "bold"


# ============================== 1. XLSX 读取 ==============================
def _col_to_idx(cell_ref: str) -> int:
    letters = re.match(r"([A-Z]+)", cell_ref)
    if letters is None:
        raise ValueError(f"非法单元格引用:{cell_ref}")
    idx = 0
    for ch in letters.group(1):
        idx = idx * 26 + ord(ch) - 64
    return idx - 1


def read_xlsx_first_sheet(path: Path) -> List[List[object]]:
    """用 zipfile + XML 读取 xlsx 第一张工作表的值。"""
    if not path.exists():
        raise FileNotFoundError(f"未找到附件:{path}")
    ns = {
        "a": "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
        "r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
    }
    with zipfile.ZipFile(path) as zf:
        shared: List[str] = []
        if "xl/sharedStrings.xml" in zf.namelist():
            root = ET.fromstring(zf.read("xl/sharedStrings.xml"))
            for si in root.findall("a:si", ns):
                shared.append("".join((t.text or "") for t in si.findall(".//a:t", ns)))

        workbook = ET.fromstring(zf.read("xl/workbook.xml"))
        sheet_node = workbook.find("a:sheets/a:sheet", ns)
        if sheet_node is None:
            raise ValueError(f"工作簿中未发现工作表:{path}")
        rid = sheet_node.attrib[f"{{{ns['r']}}}id"]

        rel_root = ET.fromstring(zf.read("xl/_rels/workbook.xml.rels"))
        target = None
        for rel in rel_root:
            if rel.attrib.get("Id") == rid:
                target = rel.attrib.get("Target")
                break
        if target is None:
            raise ValueError(f"无法定位工作表 XML:{path}")
        sheet_path = target.lstrip("/") if target.startswith("/") else "xl/" + target.replace("../", "")

        sheet_root = ET.fromstring(zf.read(sheet_path))
        data: Dict[Tuple[int, int], object] = {}
        max_row = max_col = -1
        for cell in sheet_root.findall(".//a:sheetData/a:row/a:c", ns):
            ref = cell.attrib["r"]
            row = int(re.search(r"(\d+)", ref).group(1)) - 1
            col = _col_to_idx(ref)
            typ = cell.attrib.get("t")
            value_node = cell.find("a:v", ns)
            value: object = None
            if typ == "inlineStr":
                value = "".join((t.text or "") for t in cell.findall(".//a:t", ns))
            elif value_node is not None:
                raw = value_node.text or ""
                if typ == "s":
                    value = shared[int(raw)]
                elif typ == "b":
                    value = bool(int(raw))
                elif typ == "str":
                    value = raw
                else:
                    try:
                        number = float(raw)
                        value = int(number) if number.is_integer() else number
                    except ValueError:
                        value = raw
            data[(row, col)] = value
            max_row = max(max_row, row)
            max_col = max(max_col, col)
        return [[data.get((r, c)) for c in range(max_col + 1)] for r in range(max_row + 1)]


def load_data() -> Dict[str, np.ndarray]:
    load_table = read_xlsx_first_sheet(FILES["load"])
    typical_table = read_xlsx_first_sheet(FILES["typical"])
    wind_table = read_xlsx_first_sheet(FILES["wind_scen"])
    pv_table = read_xlsx_first_sheet(FILES["pv_scen"])

    load_pu = np.array([r[1] for r in load_table[1:]], dtype=float)
    wind_typ = np.array([r[1] for r in typical_table[1:]], dtype=float)
    pv_typ = np.array([r[2] for r in typical_table[1:]], dtype=float)
    wind_scen = np.array([[r[j] for r in wind_table[1:]] for j in range(1, 7)], dtype=float)
    pv_scen = np.array([[r[j] for r in pv_table[1:]] for j in range(1, 5)], dtype=float)
    labels = [str(r[0]) for r in load_table[1:]]

    if any(len(x) != 24 for x in [load_pu, wind_typ, pv_typ]):
        raise ValueError("小时序列长度应为 24。")
    if np.any((load_pu < 0) | (load_pu > 1.2)) or np.any(wind_scen < 0) or np.any(pv_scen < 0):
        raise ValueError("标幺数据存在异常范围。")

    return {
        "labels": np.array(labels),
        "load_pu": load_pu,
        "load": LOCAL_LOAD_PEAK * load_pu,
        "wind_typ": wind_typ,
        "pv_typ": pv_typ,
        "wind_scen": wind_scen,
        "pv_scen": pv_scen,
    }


def build_grid_price() -> np.ndarray:
    p = np.zeros(24)
    for h in range(24):
        if 10 <= h < 15 or 18 <= h < 21:
            p[h] = 0.8024
        elif 7 <= h < 10 or 15 <= h < 18 or 21 <= h < 23:
            p[h] = 0.6074
        else:
            p[h] = 0.3424
    return p


# ============================== 2. 通用计算 ==============================
def grid_dispatch(total_load: np.ndarray, renewable: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
    net = total_load - renewable
    return np.maximum(net, 0.0), np.maximum(-net, 0.0)


def green_metrics(total_load: np.ndarray, renewable: np.ndarray,
                  buy: np.ndarray, sell: np.ndarray) -> Dict[str, float]:
    e_load = float(np.sum(total_load))
    e_ren = float(np.sum(renewable))
    e_buy = float(np.sum(buy))
    e_sell = float(np.sum(sell))
    # 题面口径
    ratio_self_formula = (e_load - e_sell - e_buy) / e_ren if e_ren > 0 else math.nan
    green_ratio = (e_ren - e_sell) / e_load if e_load > 0 else math.nan
    export_ratio = e_sell / e_ren if e_ren > 0 else math.nan
    # 常见物理口径,用于交叉验证
    self_consumption_standard = (e_ren - e_sell) / e_ren if e_ren > 0 else math.nan
    return {
        "日用电量_MWh": e_load,
        "新能源发电量_MWh": e_ren,
        "网购电量_MWh": e_buy,
        "上网电量_MWh": e_sell,
        "题面自发自用比例": ratio_self_formula,
        "总用电量绿电比例": green_ratio,
        "新能源上网比例": export_ratio,
        "常规自发自用率_校验": self_consumption_standard,
    }


def classify_metrics(m: Dict[str, float]) -> Tuple[str, List[bool]]:
    flags = [
        m["题面自发自用比例"] >= 0.60,
        m["总用电量绿电比例"] >= 0.30,
        m["新能源上网比例"] <= 0.20,
    ]
    if all(flags):
        return "全满足", flags
    if not any(flags):
        return "全不满足", flags
    return "部分满足", flags


def generation_cost(wind_pu: np.ndarray, pv_pu: np.ndarray) -> float:
    return float(np.sum(WIND_CAP * wind_pu * 1000 * WIND_LCOE
                        + PV_CAP * pv_pu * 1000 * PV_LCOE))


def connected_cost(wind_pu: np.ndarray, pv_pu: np.ndarray, load: np.ndarray,
                   process_fraction: np.ndarray, scale: float,
                   grid_price: np.ndarray) -> Dict[str, object]:
    process_power = INITIAL_PROCESS_POWER * scale * process_fraction
    total_load = load + process_power
    renewable = WIND_CAP * wind_pu + PV_CAP * pv_pu
    buy, sell = grid_dispatch(total_load, renewable)
    gen_cost = generation_cost(wind_pu, pv_pu)
    process_om_full = PROCESS_OM_FULL_INITIAL * scale
    process_om = float(np.sum(process_om_full * process_fraction))
    grid_cost = float(np.sum(buy * 1000 * grid_price))
    sale_revenue = float(np.sum(sell * 1000 * FEEDIN_PRICE))
    total_cost = gen_cost + process_om + grid_cost - sale_revenue
    ammonia = INITIAL_NH3_RATE * scale * float(np.sum(process_fraction))
    m = green_metrics(total_load, renewable, buy, sell)
    cls, flags = classify_metrics(m)
    return {
        "process_fraction": process_fraction,
        "process_power": process_power,
        "total_load": total_load,
        "renewable": renewable,
        "buy": buy,
        "sell": sell,
        "ammonia_t": ammonia,
        "total_cost_yuan": total_cost,
        "cost_per_ton": total_cost / ammonia if ammonia > 0 else math.inf,
        "cost_parts": {
            "新能源发电成本": gen_cost,
            "网购电成本": grid_cost,
            "电氢氨设备运维": process_om,
            "余电上网收益": sale_revenue,
        },
        "metrics": m,
        "classification": cls,
        "metric_flags": flags,
    }


# ============================== 3. 问题一 ==============================
def solve_q1(data: Dict[str, np.ndarray], grid_price: np.ndarray) -> Dict[str, object]:
    y = np.ones(24)
    return connected_cost(data["wind_typ"], data["pv_typ"], data["load"], y, 1.0, grid_price)


# ============================== 4. 问题二 ==============================
def solve_discrete(wind: np.ndarray, pv: np.ndarray, load: np.ndarray,
                   q: int, grid_price: np.ndarray) -> Dict[str, object]:
    """0-1 时段选择模型。由于小时之间无耦合,按增量成本排序可得到精确最优解。"""
    run_hours = int(round(q / EXPANDED_NH3_RATE))
    renewable = WIND_CAP * wind + PV_CAP * pv
    incremental = np.zeros(24)
    for t in range(24):
        base = load[t]
        buy0 = max(base - renewable[t], 0.0)
        sell0 = max(renewable[t] - base, 0.0)
        cost0 = buy0 * 1000 * grid_price[t] - sell0 * 1000 * FEEDIN_PRICE

        on_load = base + EXPANDED_PROCESS_POWER
        buy1 = max(on_load - renewable[t], 0.0)
        sell1 = max(renewable[t] - on_load, 0.0)
        cost1 = (buy1 * 1000 * grid_price[t] - sell1 * 1000 * FEEDIN_PRICE
                 + PROCESS_OM_FULL_EXPANDED)
        incremental[t] = cost1 - cost0

    selected = np.argsort(incremental)[:run_hours]
    y = np.zeros(24)
    y[selected] = 1.0
    result = connected_cost(wind, pv, load, y, 2.0, grid_price)
    result["incremental_cost"] = incremental
    result["selected_hours"] = [int(i) for i in np.where(y > 0.5)[0]]
    result["utilization"] = float(np.mean(y))
    return result


# ============================== 5. 问题三 ==============================
def solve_continuous(wind: np.ndarray, pv: np.ndarray, load: np.ndarray,
                     q: float, grid_price: np.ndarray,
                     min_frac: float = MIN_PROCESS_FRAC) -> Dict[str, object]:
    """连续调节 MILP:功率连续,但用二进制变量阻止同一时段同时购售电。"""
    renewable = WIND_CAP * wind + PV_CAP * pv
    n = 24
    # 变量:[y, buy, sell, z];z=1 表示购电状态
    c = np.r_[
        np.full(n, PROCESS_OM_FULL_EXPANDED),
        grid_price * 1000,
        np.full(n, -FEEDIN_PRICE * 1000),
        np.zeros(n),
    ]
    integrality = np.r_[np.zeros(3 * n), np.ones(n)]
    lower = np.r_[np.full(n, min_frac), np.zeros(2 * n), np.zeros(n)]
    upper = np.r_[np.ones(n), np.full(2 * n, 100.0), np.ones(n)]

    a_eq, b_eq = [], []
    for t in range(n):
        row = np.zeros(4 * n)
        row[t] = EXPANDED_PROCESS_POWER
        row[n + t] = -1.0
        row[2 * n + t] = 1.0
        a_eq.append(row)
        b_eq.append(renewable[t] - load[t])
    row = np.zeros(4 * n)
    row[:n] = 1.0
    a_eq.append(row)
    b_eq.append(q / EXPANDED_NH3_RATE)
    constraints = [LinearConstraint(np.array(a_eq), np.array(b_eq), np.array(b_eq))]

    big_m = 100.0
    a_ub, lb_ub, ub_ub = [], [], []
    for t in range(n):
        # buy_t <= M z_t
        row = np.zeros(4 * n)
        row[n + t] = 1.0
        row[3 * n + t] = -big_m
        a_ub.append(row)
        lb_ub.append(-np.inf)
        ub_ub.append(0.0)
        # sell_t <= M(1-z_t)
        row = np.zeros(4 * n)
        row[2 * n + t] = 1.0
        row[3 * n + t] = big_m
        a_ub.append(row)
        lb_ub.append(-np.inf)
        ub_ub.append(big_m)
    constraints.append(LinearConstraint(np.array(a_ub), np.array(lb_ub), np.array(ub_ub)))

    sol = milp(c, integrality=integrality, bounds=Bounds(lower, upper),
               constraints=constraints, options={"time_limit": 60})
    if not sol.success:
        raise RuntimeError(f"连续调节模型求解失败:{sol.message}")
    y = sol.x[:n]
    result = connected_cost(wind, pv, load, y, 2.0, grid_price)
    result["selected_hours"] = [int(i) for i in np.where(y > min_frac + 1e-6)[0]]
    result["utilization"] = float(np.mean(y))
    return result
全部论文请见下方" 只会建模 QQ名片" 点击QQ名片即可
相关推荐
疯狂打码的少年1 小时前
【数据结构】串的模式匹配:KMP算法(重点)
数据结构·笔记·算法
晚风醉蝶2 小时前
第零篇-算法全景图
算法
疯狂打码的少年2 小时前
【数据结构】树的基本概念与二叉树定义
java·数据结构·笔记·算法
webmote333 小时前
用 NVIDIA Nemotron 3 Super + .NET 构建有记忆的多轮对话
后端·算法
Keven_113 小时前
算法札记:区分稀疏图与稠密图在经验上的数学计算差异
算法·稠密图·稀疏图
Sagittarius_A*3 小时前
后量子密码|前置基础 03|数字签名运行逻辑:签名签发、核验原理与实际业务应用
算法·信息安全·密码学·数字签名·后量子密码
DBA小马哥3 小时前
向量数据库入门到进阶:Embedding、ANN算法与RAG落地的关键术语
数据库·算法·embedding
Luhui Dev4 小时前
如何在 WorkBuddy 中使用大角几何:从 MCP 接入到 AI 几何作图
人工智能·数学·算法·agent·luhuidev
Smilecoc4 小时前
决策树(五):决策树回归
算法·决策树·回归