pv_distributed_photovoltaic.py 16.3 KB
Newer Older
lcn's avatar
lcn committed
1 2 3 4 5 6 7 8 9
# -*- coding: utf-8 -*-
"""
calc distributed photovoltaic optimation for companies.
This is a tornado process and  responds request from web back server.
"""
import pendulum
import datetime
import pandas as pd
from pot_libs.mysql_util.mysql_util import MysqlUtil
ZZH's avatar
ZZH committed
10
from unify_api.constants import ENERGY_INVESTMENT_PV
lcn's avatar
lcn committed
11 12 13 14 15 16 17 18 19 20 21
from unify_api.modules.energy_optimize.service.energy_store_optimize import \
    AutoDic
from unify_api.modules.energy_optimize.service.ess_utils import \
    PricePolicyHelper
from unify_api.modules.energy_optimize.service.pv_optimation_tool import \
    PvOptimizationTool


class PhotovoltaicOptimize(object):
    def __init__(self, inlid):
        self._inlid = inlid
ZZH's avatar
ZZH committed
22

lcn's avatar
lcn committed
23 24 25 26
    async def calc_inline(self, pv_params):
        rlt = {'rlt_flag': True}
        inl_info = await self._get_inline_info()
        inline_vc = inl_info['inline_vc']
wang.wenrong's avatar
wang.wenrong committed
27
        cid = inl_info['cid']
lcn's avatar
lcn committed
28
        city = await self._get_company_city(cid)
ZZH's avatar
ZZH committed
29

lcn's avatar
lcn committed
30 31 32 33 34 35 36 37 38 39
        df_pv = await self._construct_pv_curve(city)  # construct df_pv
        if len(df_pv) == 0:
            rlt['rlt_flag'] = False
            rlt['message'] = "无光伏典型出力曲线,请联系工作人员!"
            return rlt
        max_dt = await self._find_kwh_max_day()  # 00:00:00 of the max kwh day
        if not max_dt:
            rlt['rlt_flag'] = False
            rlt['message'] = '暂无'
            return rlt
ZZH's avatar
ZZH committed
40

lcn's avatar
lcn committed
41 42
        pp = await self._get_company_price_policy(cid)
        pp_info_d = PricePolicyHelper.map_price_policy(pp, inline_vc,
lcn's avatar
lcn committed
43 44 45
                                                       max_dt.int_timestamp)
        time_str_d = PricePolicyHelper.quarter_chars_2_time_str(
            pp_info_d['quarters'])
ZZH's avatar
ZZH committed
46

lcn's avatar
lcn committed
47 48 49 50 51 52 53 54 55 56 57 58
        # construct pv_system
        price_type = pp_info_d['price_type']
        pv_system = await self._construct_pv_system(pv_params, price_type)
        # construct price
        price = self._construct_price(pv_params, pp_info_d, time_str_d)
        # construct env_benifit
        env_benifit = self._construct_env_benifit()
        # construct inline_var
        tc_runtime = inl_info['tc_runtime']
        inline_var = await self._construct_inline_var(tc_runtime)
        # construct df_load
        df_load = await self._construct_load_curve(max_dt)
ZZH's avatar
ZZH committed
59

lcn's avatar
lcn committed
60 61 62 63 64 65
        # logger.info('pv_system: %s', pv_system)
        # logger.info('price: %s', price)
        # logger.info('env_benifit: %s', env_benifit)
        # logger.info('inline_var: %s', inline_var)
        # logger.info('df_load: %s', df_load)
        # logger.info('df_pv: %s', df_pv)
ZZH's avatar
ZZH committed
66

lcn's avatar
lcn committed
67 68 69
        pv_ot = PvOptimizationTool(pv_system, price, env_benifit, df_load,
                                   df_pv, inline_var)
        pv_ot.output()
ZZH's avatar
ZZH committed
70

lcn's avatar
lcn committed
71 72 73 74 75 76
        # assemble return value
        rlt['install_cap'] = self._assemble_install_cap(pv_ot)
        rlt['invest_evaluate'] = self._assemble_invest_evaluate(pv_ot)
        rlt['opt_analysis'] = pv_ot.opt_analysis
        rlt['opt_curve'] = self._assemble_opt_curve(pv_ot)
        return rlt
ZZH's avatar
ZZH committed
77

lcn's avatar
lcn committed
78 79 80 81 82 83 84 85
    def _assemble_install_cap(self, pv_ot):
        install_cap = {
            'capacity': pv_ot.invest_capacity["capacity"],
            'install_space': pv_ot.pv_system["install_space"],
            'ttl_invest': pv_ot.invest_capacity["ttl_invest"],
            'first_year_ttl_kwh': pv_ot.invest_capacity["first_year_ttl_kwh"]
        }
        return install_cap
ZZH's avatar
ZZH committed
86

lcn's avatar
lcn committed
87 88 89 90 91 92 93 94 95
    def _assemble_invest_evaluate(self, pv_ot):
        cost_per_kwh = (pv_ot.price["rmb_per_wp"] -
                        pv_ot.price["first_install_subsidy"])
        invest_income = pv_ot.invest_evaluate["invest_income"]
        first_year_ttl_kwh = pv_ot.invest_capacity["first_year_ttl_kwh"]
        first_year_month_income = invest_income["first_year_month_income"]
        first_year_income = invest_income["first_year_income"]
        first_year_income_rate = invest_income["first_year_income_rate"]
        invest_income_year = invest_income["invest_income_year"]
ZZH's avatar
ZZH committed
96

lcn's avatar
lcn committed
97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
        i_and_r = {
            'user_type': pv_ot.pv_system["user_type"],
            'ttl_invest': pv_ot.invest_capacity["ttl_invest"],
            'capacity': pv_ot.invest_capacity["capacity"],
            'cost_per_kwh': cost_per_kwh,
            'install_space': pv_ot.pv_system["install_space"],
            'peak_sunshine_hours': pv_ot.pv_system["peak_sunshine_hours"],
            'self_use_ratio': pv_ot.pv_system["self_use_ratio"],
            'self_use_price_discout': pv_ot.price["self_use_price_discout"],
            'sel_use_per_kwh': pv_ot.price["sel_use_per_kwh"],
            'local_subsidy': pv_ot.price["local_subsidy"],
            'first_install_subsidy': pv_ot.price['first_install_subsidy'],
            'state_subsidy': pv_ot.price["state_subsidy"],
            'first_year_ttl_kwh': first_year_ttl_kwh,
            'first_year_month_income': first_year_month_income,
            'first_year_income': first_year_income,
            'first_year_income_rate': first_year_income_rate,
            'invest_income_year': invest_income_year
        }
ZZH's avatar
ZZH committed
116

lcn's avatar
lcn committed
117 118 119 120 121 122 123 124 125 126 127
        env_benifit = pv_ot.invest_evaluate["env_benifit_per_year"]
        c_footprint = {
            'families': env_benifit['one_family_kwh'],
            'tree': env_benifit['tree'],
            'coal': env_benifit['Coal'],
            'CO2': env_benifit['CO2'],
            'SO2': env_benifit['SO2'],
            'NOx': env_benifit['NOx'],
            'smoke': env_benifit['Smoke'],
            'H2O': env_benifit['H2O']
        }
ZZH's avatar
ZZH committed
128

lcn's avatar
lcn committed
129 130
        invest_evaluate = {'i_and_r': i_and_r, 'carbon_footprint': c_footprint}
        return invest_evaluate
ZZH's avatar
ZZH committed
131

lcn's avatar
lcn committed
132 133 134 135 136 137 138 139 140 141
    def _assemble_opt_curve(self, pv_ot):
        rlt = []
        for idx, row in pv_ot.opt_curve.iterrows():
            tmpd = {}
            tmpd['quarter_time'] = idx
            tmpd['load_curve'] = row['load_curve']
            tmpd['load_pv_curve'] = row['load_pv_curve']
            tmpd['pv_curve'] = row['pv_curve']
            rlt.append(tmpd)
        return rlt
ZZH's avatar
ZZH committed
142

lcn's avatar
lcn committed
143 144 145 146 147 148 149 150 151 152
    async def _construct_pv_system(self, pv_params, price_type):
        area = pv_params['install_space']
        # ratio fixed, convert to decimal, web backend just pass us a
        # percent, we need to divide 100.
        ratio = pv_params['self_use_ratio'] / 100
        sql = ("select pv.peak_sunshine_hours from inline i, company c, "
               "algo_distributed_pv_quick_check_list pv "
               "where i.inlid=%s and i.cid=c.cid and "
               "c.city=pv.city;")
        async with MysqlUtil() as conn:
lcn's avatar
lcn committed
153 154
            hours = await conn.fetchone(sql, (self._inlid,)) or {}
        hours = hours.get("peak_sunshine_hours") or 0
lcn's avatar
lcn committed
155
        annual_hours = hours * 365
ZZH's avatar
ZZH committed
156

lcn's avatar
lcn committed
157 158 159 160 161 162 163 164 165 166 167 168 169 170
        # peak_sunshine_hours means annual_peak_sunshine_hours, the name
        # in algorithm is misleading
        pv_system = {
            "user_type": price_type,  # 工商业
            "install_space": area,  # 安装面积m2
            "efficiency": 0.8,  # 光伏发电效率
            "first_year_decay_rate": 2.5,  # 首年衰减率
            "first10_year_decay_rate": 0.8,  # 第2-10年衰减率
            "other_year_decay_rate": 0.7,  # 第11-25年衰减率
            "evaluate_year": 25,  # 评估年限
            "self_use_ratio": ratio,  # 自发自用比例
            "peak_sunshine_hours": annual_hours  # 年峰值日照小时数
        }
        return pv_system
ZZH's avatar
ZZH committed
171

lcn's avatar
lcn committed
172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
    def _construct_price(self, pv_params, pp_info_d, time_str_d):
        cons_p = pv_params['rmb_per_wp']
        user_p = pv_params['sel_use_per_kwh']
        user_p_dis = pv_params['self_use_price_discout']
        first_sbsy = pv_params['first_install_subsidy']
        local_sbsy = pv_params['local_subsidy']
        local_year = pv_params['local_subsidy_year']
        price_md = pp_info_d['price_md']
        price_d = {
            "rmb_per_wp": cons_p,  # 建设单价
            "self_use_price_discout": user_p_dis,  # 自发自用电价折扣
            "sel_use_per_kwh": user_p,  # 自发自用电价
            # "state_subsidy": 0.42,  # 国家补贴
            "state_subsidy": 0.0,  # 国家补贴
            "local_subsidy": local_sbsy,  # 地方补贴
            "local_subsidy_year": local_year,  # 地方补贴年限
            "first_install_subsidy": first_sbsy,  # 初始安装补贴
            "coal_in_grid": 0.43,  # 脱硫电价
            "max_demand": price_md,
            "spfv_price": {}
        }
        sfpv_price = price_d['spfv_price']
        if pp_info_d['price_s']:
            sct = self._construct_section('s', pp_info_d, time_str_d)
            sfpv_price['section_s'] = sct
        if pp_info_d['price_p']:
            sct = self._construct_section('p', pp_info_d, time_str_d)
            sfpv_price['section_p'] = sct
        if pp_info_d['price_f']:
            sct = self._construct_section('f', pp_info_d, time_str_d)
            sfpv_price['section_f'] = sct
        if pp_info_d['price_v']:
            sct = self._construct_section('v', pp_info_d, time_str_d)
            sfpv_price['section_v'] = sct
        return price_d
ZZH's avatar
ZZH committed
207

lcn's avatar
lcn committed
208 209 210 211 212 213 214 215 216 217 218 219
    def _construct_env_benifit(self):
        env_benifit_param = {
            "one_family_kwh": 3600,  # 一户家庭一年用3600度电
            "CO2": 0.592,  # 二氧化碳排放kg/kWh
            "SO2": 0.0002,  # 二氧化硫排放kg/kWh
            "NOx": 0.00019,  # 氮氧化合物kg/kWh
            "Smoke": 0.00004,  # 烟尘kg/kWh
            "Coal": 0.3076,  # 煤耗kg/kWh
            "H2O": 0.06,  # 纯净水m3/kWh
            "tree": 18.3  # 1棵树1年可吸收18.3千克CO2
        }
        return env_benifit_param
ZZH's avatar
ZZH committed
220

lcn's avatar
lcn committed
221 222 223 224 225 226
    def _construct_section(self, p_char, pp_info_d, time_str_d):
        """ contruct section_x for price_d."""
        section = {'price': pp_info_d['price_' + p_char]}
        time_range_str = ';'.join(time_str_d[p_char])
        section['time_range'] = time_range_str
        return section
ZZH's avatar
ZZH committed
227

lcn's avatar
lcn committed
228
    async def _build_kwh_charge_sum_lastest_30(self, p_char):
lcn's avatar
lcn committed
229 230 231
        """ build es query sentance for get kwh sum and charge sum
        within lastest 30 days for specified p_char.
        """
lcn's avatar
lcn committed
232 233 234 235 236
        sql = f"""
         select sum(kwh) kwh,sum(charge) charge from inline_15min_power
         where inlid = %s and spfv = %s and create_time >= % s and create_time
         < %s
        """
lcn's avatar
lcn committed
237 238
        dt = pendulum.now()
        dt_1_month_ago = dt.subtract(days=30)
lcn's avatar
lcn committed
239 240 241 242 243 244
        start_time = dt_1_month_ago.format("YYYY-MM-DD HH:mm:ss")
        end_time = dt.format("YYYY-MM-DD HH:mm:ss")
        async with MysqlUtil() as conn:
            result = await conn.fetchone(sql, args=(self._inlid, p_char,
                                                    start_time, end_time))
        return result or {}
ZZH's avatar
ZZH committed
245

lcn's avatar
lcn committed
246 247
    async def _construct_inline_var(self, inline_tc):
        inline_var = {'inline_capacity': inline_tc}
ZZH's avatar
ZZH committed
248

lcn's avatar
lcn committed
249
        result = await self._build_kwh_charge_sum_lastest_30("s")
lcn's avatar
lcn committed
250
        # search_rlt = self._es.search(inline_15min_power_esindex, q)
lcn's avatar
lcn committed
251 252
        charge_s = result.get("charge") or 0
        kwh_s = result.get("kwh") or 0
ZZH's avatar
ZZH committed
253

lcn's avatar
lcn committed
254
        result = await self._build_kwh_charge_sum_lastest_30("p")
lcn's avatar
lcn committed
255
        # search_rlt = self._es.search(inline_15min_power_esindex, q)
ZZH's avatar
ZZH committed
256

lcn's avatar
lcn committed
257 258
        charge_p = result.get("charge") or 0
        kwh_p = result.get("kwh") or 0
lcn's avatar
lcn committed
259 260 261 262 263
        # add 's' and 'p', because algorithm needs these
        charge_sp = charge_s + charge_p
        kwh_sp = kwh_s + kwh_p
        inline_var['peak_charge'] = charge_sp
        inline_var['peak_kwh'] = kwh_sp
ZZH's avatar
ZZH committed
264

lcn's avatar
lcn committed
265
        result = await self._build_kwh_charge_sum_lastest_30("f")
lcn's avatar
lcn committed
266
        # search_rlt = self._es.search(inline_15min_power_esindex, q)
lcn's avatar
lcn committed
267 268
        charge_f = result.get("charge") or 0
        kwh_f = result.get("kwh") or 0
lcn's avatar
lcn committed
269 270 271
        inline_var['flat_charge'] = charge_f
        inline_var['flat_kwh'] = kwh_f
        return inline_var
ZZH's avatar
ZZH committed
272

lcn's avatar
lcn committed
273
    async def _build_load_curve(self, start_dt):
lcn's avatar
lcn committed
274
        end_dt = start_dt.add(days=1)
lcn's avatar
lcn committed
275 276 277 278 279 280 281
        start_time = start_dt.format("YYYY-MM-DD HH:mm:ss")
        end_time = end_dt.format("YYYY-MM-DD HH:mm:ss")
        sql = f"""
            select create_time,p from inline_15min_power
            where inlid = %s and create_time >= %s and create_time < %s
            order by create_time asc limit 100
        """
ZZH's avatar
ZZH committed
282

lcn's avatar
lcn committed
283 284 285 286
        async with MysqlUtil() as conn:
            results = await conn.fetchall(sql, args=(self._inlid, start_time,
                                                     end_time))
        return results or []
ZZH's avatar
ZZH committed
287

lcn's avatar
lcn committed
288
    async def _construct_load_curve(self, start_dt):
lcn's avatar
lcn committed
289
        hits_list = await self._build_load_curve(start_dt)
lcn's avatar
lcn committed
290 291 292
        # hits_list is already sorted by quarter_time asc
        kw_list = []
        for item in hits_list:
lcn's avatar
lcn committed
293 294 295 296
            kw_list.append({
                'quarter_time': item.get('create_time'),
                'load_curve': item.get('p')
            })
lcn's avatar
lcn committed
297 298
        df = pd.DataFrame(kw_list)
        return df
ZZH's avatar
ZZH committed
299

lcn's avatar
lcn committed
300 301 302 303 304 305 306 307 308 309 310 311 312 313 314
    async def _construct_pv_curve(self, city):
        sql = "select hour, p from algo_distributed_pv where city=%s " \
              "order by hour asc"
        async with MysqlUtil() as conn:
            sql_rlt = await conn.fetchall(sql, args=(city,))
        pv_list = []
        for item in sql_rlt:
            dt = pendulum.datetime(2019, 1, 1, tz='Asia/Shanghai')
            dt = dt + item['hour']  # item['hour'] is a timedelta object
            qrt_dt = datetime.datetime(year=dt.year, month=dt.month,
                                       day=dt.day, hour=dt.hour,
                                       minute=dt.minute, second=dt.second)
            pv_list.append({'quarter_time': qrt_dt, 'pv_curve': item['p']})
        df = pd.DataFrame(pv_list)
        return df
ZZH's avatar
ZZH committed
315

lcn's avatar
lcn committed
316
    async def _get_inline_info(self):
wang.wenrong's avatar
wang.wenrong committed
317
        """ get inline_vc, tc_runtime, cid from redis.
lcn's avatar
lcn committed
318 319
        :return: a dict
        """
wang.wenrong's avatar
wang.wenrong committed
320
        sql = "SELECT inline_vc, tc_runtime, cid cid from " \
lcn's avatar
lcn committed
321 322 323 324 325
              "inline where inlid = %s"
        async with MysqlUtil() as conn:
            info = await conn.fetchone(sql, args=(self._inlid,))
        rlt = {'inline_vc': info['inline_vc'],
               'tc_runtime': info['tc_runtime'],
wang.wenrong's avatar
wang.wenrong committed
326
               'cid': info['cid']}
lcn's avatar
lcn committed
327
        return rlt
ZZH's avatar
ZZH committed
328

lcn's avatar
lcn committed
329 330 331 332 333 334 335 336 337
    async def _get_company_price_policy(self, cid):
        result = AutoDic()
        sql = 'SELECT * FROM price_policy where cid = %s'
        async with MysqlUtil() as conn:
            policies = await conn.fetchall(sql, (cid,))
        for policy in policies:
            result[str(policy['inline_vc'])][str(policy['start_month'])][
                policy['time_range']] = policy
        return result
ZZH's avatar
ZZH committed
338

lcn's avatar
lcn committed
339 340 341 342 343 344 345
    async def _get_company_city(self, cid):
        sql = "SELECT city from company where cid = %s"
        async with MysqlUtil() as conn:
            c_info = await conn.fetchone(sql, (cid,))
        # company_j = self._r_cache.hget(company_hashname, str(cid))
        # c_info = json.loads(company_j)
        return c_info['city']
ZZH's avatar
ZZH committed
346

lcn's avatar
lcn committed
347
    async def _find_kwh_max_day(self):
lcn's avatar
lcn committed
348 349 350 351
        sql = f"""
            select create_time from inline_1day_power
            where inlid = %s and create_time >=%s and create_time < %s
            order by kwh desc limit 1;
lcn's avatar
lcn committed
352 353
        """
        # search_rlt = self._es.search(inline_1day_power_esindex, q)
lcn's avatar
lcn committed
354 355 356 357 358 359 360 361 362 363 364 365
        dt = pendulum.now()
        dt_half_year_ago = dt.subtract(months=6)
        start_time = dt_half_year_ago.format("YYYY-MM-DD HH:mm:ss")
        end_time = dt.format("YYYY-MM-DD HH:mm:ss")
        async with MysqlUtil() as conn:
            result = await conn.fetchone(sql, args=(self._inlid,
                                                    start_time,
                                                    end_time))
            if not result:
                return None
        max_dt = result.get("create_time").strftime("%Y-%m-%d %H:%M:%S")
        return pendulum.parse(max_dt)
lcn's avatar
lcn committed
366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391


async def pv_out_result(inlid, params):
    """结果输出函数"""
    # get cid
    sql = "select cid from inline where inlid = %s"
    async with MysqlUtil() as conn:
        cid_info = await conn.fetchone(sql=sql, args=(inlid,))
    cid = cid_info.get("cid")
    # get proxy_id
    sql = "select cpm.proxy from company c inner join company_proxy_map cpm " \
          "on cpm.cid=c.cid where c.cid = %s"
    async with MysqlUtil() as conn:
        proxy_res = await conn.fetchone(sql, args=(cid,))
    proxy_id = proxy_res["proxy"]
    sql = "insert into energy_investment_analysis_record " \
          "(cid, analysis_type, inlid, proxy, time) " \
          "values (%s, %s, %s, %s, %s)"
    ts = pendulum.now().int_timestamp
    async with MysqlUtil() as conn:
        await conn.execute(sql, args=(
            cid, ENERGY_INVESTMENT_PV, inlid, proxy_id, ts))
    # handle request
    pv_optimize = PhotovoltaicOptimize(inlid)
    algo_rlt = await pv_optimize.calc_inline(params)
    return algo_rlt