Source code for el1xr_opt.Modules.oM_Community

# Developed by: Erik F. Alvarez
#
# Electric Power System Unit
# RISE
# erik.alvarez@ri.se
#
# Energy-community / virtual-sharing layer for el1xr_opt (Phase 6a).
#
# Lets retailers (members) in the same zone share locally generated electricity
# before importing from / exporting to the grid. "Virtual" sharing: the allocation
# is financial/metering, the existing grid carries the power. This is a linear
# layer added on top of the retail model and is OFF by default, enabled with the
# option flag ``IndBinCommunity`` so existing cases are unchanged.
#
# How it works:
#   * Each member er gets two non-negative variables per period/scenario/load level:
#       vEleShareOut[p,sc,n,er]  energy contributed to the community pool
#       vEleShareIn [p,sc,n,er]  energy received from the community pool
#   * The retail balance (in oM_ModelFormulation.eEleRetNodeBalance) gains
#       + vEleShareIn - vEleShareOut
#     so received energy helps meet demand and contributed energy uses local
#     surplus, exactly like buy/sell but internal to the community.
#   * A per-zone pool-conservation constraint makes sharing lossless and local:
#       sum_{er in zone} vEleShareOut == sum_{er in zone} vEleShareIn   (per p,sc,n)
#
# The benefit is automatic: a kWh shared internally avoids both the retail buy
# mark-up (for the receiver) and the low sell price (for the giver), so whenever
# the buy price exceeds the sell price the optimiser shares surplus instead of
# round-tripping it through the grid, lowering total community cost. The split of
# the savings between members (community pricing) is a settlement question handled
# in Phase 6b; for 6a the total-cost objective already captures the saving.

import time

from pyomo.environ import Var, Constraint, NonNegativeReals
from .utils.oM_Utils import log_time


[docs] def community_active(model) -> bool: """True if the energy-community layer is enabled for this case.""" return bool(model.Par.get('pOptIndBinCommunity', 0))
[docs] def create_community_variables(model, optmodel, indlog): """Create the per-member share variables. No-op (and no vars) when the community layer is off, so existing cases are untouched. Must run before ``create_constraints`` because the retail balance references these variables. """ if not community_active(model): return model StartTime = time.time() setattr(optmodel, 'vEleShareIn', Var(optmodel.psner, within=NonNegativeReals, doc='community energy received by a member [kWh]')) setattr(optmodel, 'vEleShareOut', Var(optmodel.psner, within=NonNegativeReals, doc='community energy contributed by a member [kWh]')) log_time('--- Declaring the energy-community variables:', StartTime, ind_log=indlog) return model
[docs] def create_community_constraints(model, optmodel, indlog): """Per-zone pool conservation: contributed energy equals received energy within each community (zone), at every period/scenario/load level. Lossless, local virtual sharing. No-op when the community layer is off. """ if not community_active(model): return model StartTime = time.time() # zone -> members (retailers), from the existing z2er mapping. members = {zn: [er for (z, er) in model.z2er if z == zn] for zn in model.zn} def ePool(optmodel, p, sc, n, zn): ers = members.get(zn, []) if len(ers) < 2: # a pool needs at least two members return Constraint.Skip return (sum(optmodel.vEleShareOut[p, sc, n, er] for er in ers) == sum(optmodel.vEleShareIn[p, sc, n, er] for er in ers)) psnzn = [(p, sc, n, zn) for (p, sc, n) in model.psn for zn in model.zn] optmodel.__setattr__('eEleCommunityPool', Constraint(psnzn, rule=ePool, doc='community pool conservation per zone [kWh]')) log_time('--- Declaring the energy-community constraints:', StartTime, ind_log=indlog) return model