pv_distributed_photovoltaic.py 16.6 KB
Newer Older
lcn's avatar
lcn committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
# -*- 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.es_util.es_utils import EsUtil
from pot_libs.mysql_util.mysql_util import MysqlUtil
from unify_api.constants import ENERGY_INVESTMENT_PV, \
    INLINE_15MIN_POWER_ESINDEX, INLINE_1DAY_POWER_ESINDEX
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
lcn's avatar
lcn committed
24
    
lcn's avatar
lcn committed
25 26 27 28
    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
29
        cid = inl_info['cid']
lcn's avatar
lcn committed
30
        city = await self._get_company_city(cid)
lcn's avatar
lcn committed
31
        
lcn's avatar
lcn committed
32 33 34 35 36 37 38 39 40 41
        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
lcn's avatar
lcn committed
42
        
lcn's avatar
lcn committed
43 44
        pp = await self._get_company_price_policy(cid)
        pp_info_d = PricePolicyHelper.map_price_policy(pp, inline_vc,
lcn's avatar
lcn committed
45 46 47 48
                                                       max_dt.int_timestamp)
        time_str_d = PricePolicyHelper.quarter_chars_2_time_str(
            pp_info_d['quarters'])
        
lcn's avatar
lcn committed
49 50 51 52 53 54 55 56 57 58 59 60
        # 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)
lcn's avatar
lcn committed
61
        
lcn's avatar
lcn committed
62 63 64 65 66 67
        # 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)
lcn's avatar
lcn committed
68
        
lcn's avatar
lcn committed
69 70 71
        pv_ot = PvOptimizationTool(pv_system, price, env_benifit, df_load,
                                   df_pv, inline_var)
        pv_ot.output()
lcn's avatar
lcn committed
72
        
lcn's avatar
lcn committed
73 74 75 76 77 78
        # 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
lcn's avatar
lcn committed
79
    
lcn's avatar
lcn committed
80 81 82 83 84 85 86 87
    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
lcn's avatar
lcn committed
88
    
lcn's avatar
lcn committed
89 90 91 92 93 94 95 96 97
    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"]
lcn's avatar
lcn committed
98
        
lcn's avatar
lcn committed
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
        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
        }
lcn's avatar
lcn committed
118
        
lcn's avatar
lcn committed
119 120 121 122 123 124 125 126 127 128 129
        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']
        }
lcn's avatar
lcn committed
130
        
lcn's avatar
lcn committed
131 132
        invest_evaluate = {'i_and_r': i_and_r, 'carbon_footprint': c_footprint}
        return invest_evaluate
lcn's avatar
lcn committed
133
    
lcn's avatar
lcn committed
134 135 136 137 138 139 140 141 142 143
    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
lcn's avatar
lcn committed
144
    
lcn's avatar
lcn committed
145 146 147 148 149 150 151 152 153 154
    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
155 156
            hours = await conn.fetchone(sql, (self._inlid,)) or {}
        hours = hours.get("peak_sunshine_hours") or 0
lcn's avatar
lcn committed
157
        annual_hours = hours * 365
lcn's avatar
lcn committed
158
        
lcn's avatar
lcn committed
159 160 161 162 163 164 165 166 167 168 169 170 171 172
        # 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
lcn's avatar
lcn committed
173
    
lcn's avatar
lcn committed
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 207 208
    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
lcn's avatar
lcn committed
209
    
lcn's avatar
lcn committed
210 211 212 213 214 215 216 217 218 219 220 221
    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
lcn's avatar
lcn committed
222
    
lcn's avatar
lcn committed
223 224 225 226 227 228
    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
lcn's avatar
lcn committed
229 230
    
    async def _build_kwh_charge_sum_lastest_30(self, p_char):
lcn's avatar
lcn committed
231 232 233
        """ build es query sentance for get kwh sum and charge sum
        within lastest 30 days for specified p_char.
        """
lcn's avatar
lcn committed
234 235 236 237 238
        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
239 240
        dt = pendulum.now()
        dt_1_month_ago = dt.subtract(days=30)
lcn's avatar
lcn committed
241 242 243 244 245 246 247
        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 {}
    
lcn's avatar
lcn committed
248 249
    async def _construct_inline_var(self, inline_tc):
        inline_var = {'inline_capacity': inline_tc}
lcn's avatar
lcn committed
250 251
        
        result = await self._build_kwh_charge_sum_lastest_30("s")
lcn's avatar
lcn committed
252
        # search_rlt = self._es.search(inline_15min_power_esindex, q)
lcn's avatar
lcn committed
253 254 255 256
        charge_s = result.get("charge") or 0
        kwh_s = result.get("kwh") or 0
        
        result = await self._build_kwh_charge_sum_lastest_30("p")
lcn's avatar
lcn committed
257
        # search_rlt = self._es.search(inline_15min_power_esindex, q)
lcn's avatar
lcn committed
258 259 260
        
        charge_p = result.get("charge") or 0
        kwh_p = result.get("kwh") or 0
lcn's avatar
lcn committed
261 262 263 264 265
        # 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
lcn's avatar
lcn committed
266 267
        
        result = await self._build_kwh_charge_sum_lastest_30("f")
lcn's avatar
lcn committed
268
        # search_rlt = self._es.search(inline_15min_power_esindex, q)
lcn's avatar
lcn committed
269 270
        charge_f = result.get("charge") or 0
        kwh_f = result.get("kwh") or 0
lcn's avatar
lcn committed
271 272 273
        inline_var['flat_charge'] = charge_f
        inline_var['flat_kwh'] = kwh_f
        return inline_var
lcn's avatar
lcn committed
274 275
    
    async def _build_load_curve(self, start_dt):
lcn's avatar
lcn committed
276
        end_dt = start_dt.add(days=1)
lcn's avatar
lcn committed
277 278 279 280 281 282 283 284 285 286 287 288 289
        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
        """
        
        async with MysqlUtil() as conn:
            results = await conn.fetchall(sql, args=(self._inlid, start_time,
                                                     end_time))
        return results or []
    
lcn's avatar
lcn committed
290
    async def _construct_load_curve(self, start_dt):
lcn's avatar
lcn committed
291
        hits_list = await self._build_load_curve(start_dt)
lcn's avatar
lcn committed
292 293 294
        # hits_list is already sorted by quarter_time asc
        kw_list = []
        for item in hits_list:
lcn's avatar
lcn committed
295 296 297 298
            kw_list.append({
                'quarter_time': item.get('create_time'),
                'load_curve': item.get('p')
            })
lcn's avatar
lcn committed
299 300
        df = pd.DataFrame(kw_list)
        return df
lcn's avatar
lcn committed
301
    
lcn's avatar
lcn committed
302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
    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
lcn's avatar
lcn committed
317
    
lcn's avatar
lcn committed
318
    async def _get_inline_info(self):
wang.wenrong's avatar
wang.wenrong committed
319
        """ get inline_vc, tc_runtime, cid from redis.
lcn's avatar
lcn committed
320 321
        :return: a dict
        """
wang.wenrong's avatar
wang.wenrong committed
322
        sql = "SELECT inline_vc, tc_runtime, cid cid from " \
lcn's avatar
lcn committed
323 324 325 326 327
              "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
328
               'cid': info['cid']}
lcn's avatar
lcn committed
329
        return rlt
lcn's avatar
lcn committed
330
    
lcn's avatar
lcn committed
331 332 333 334 335 336 337 338 339
    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
lcn's avatar
lcn committed
340
    
lcn's avatar
lcn committed
341 342 343 344 345 346 347
    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']
lcn's avatar
lcn committed
348
    
lcn's avatar
lcn committed
349
    async def _find_kwh_max_day(self):
lcn's avatar
lcn committed
350 351 352 353
        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
354 355
        """
        # search_rlt = self._es.search(inline_1day_power_esindex, q)
lcn's avatar
lcn committed
356 357 358 359 360 361 362 363 364 365 366 367
        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
368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393


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