Spaces:
Running
Running
File size: 7,854 Bytes
4847e7d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 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 | import math
# ---------------------------------------------------------------------------
# Indian Electricity Tariff Slabs (monthly, residential)
# Rates are in βΉ per unit (kWh).
# Add or adjust slabs here without touching any other code.
# ---------------------------------------------------------------------------
DEFAULT_TARIFF_SLABS = [
{"min": 0, "max": 50, "rate": 3.0},
{"min": 51, "max": 100, "rate": 3.5},
{"min": 101, "max": 200, "rate": 5.0},
{"min": 201, "max": None, "rate": 7.0}, # None β unbounded
]
# Solar generation assumptions (India average)
UNITS_PER_KW_PER_MONTH: float = 120.0 # 1 kW produces ~120 units/month
DEFAULT_PANEL_WATT: float = 540.0 # Standard panel size in watts
class BillOptimizationService:
"""
Pure-calculation service for solar bill optimisation using Indian
slab-based electricity tariffs.
No machine learning. No external I/O. Fully stateless β every call to
``optimize()`` is independent.
Design principles
-----------------
* Forward calculation : ``calculate_bill_from_units`` β bill amount given units.
* Reverse calculation : ``estimate_units_from_bill`` β units given bill amount.
* Solar sizing : derives required kW and panel count from unit delta.
* Safety guards : clamps negative solar values; validates all inputs.
"""
# ------------------------------------------------------------------
# Public entry point
# ------------------------------------------------------------------
def optimize(self, validated_data: dict) -> tuple[dict, int]:
"""
Main method called by the view layer.
Parameters
----------
validated_data : dict
Already-validated data from ``BillOptimizationRequestSerializer``.
All fields are guaranteed to be present with correct Python types.
Returns
-------
(response_dict, http_status_code)
"""
try:
# ββ 1. EXTRACT FIELDS (types already guaranteed by serializer) ββ
current_bill: float = validated_data["current_bill"]
target_bill: float = validated_data["target_bill"]
has_solar: bool = validated_data.get("has_solar", False)
solar_capacity_kw: float = validated_data.get("solar_capacity_kw") or 0.0
slabs = DEFAULT_TARIFF_SLABS
# ββ 2. SLAB-BASED REVERSE CALCULATIONS ββββββββββββββββββββ
current_units: float = self.estimate_units_from_bill(current_bill, slabs)
target_units: float = self.estimate_units_from_bill(target_bill, slabs)
units_to_offset: float = max(0.0, current_units - target_units)
# ββ 3. SOLAR SIZING βββββββββββββββββββββββββββββββββββββββ
if has_solar:
existing_generation = solar_capacity_kw * UNITS_PER_KW_PER_MONTH
required_kw = (
current_units - existing_generation - target_units
) / UNITS_PER_KW_PER_MONTH
else:
required_kw = units_to_offset / UNITS_PER_KW_PER_MONTH
# Safety clamp β never return negative solar capacity
required_kw = max(0.0, required_kw)
# Panel count β round UP so the target is always met
panel_kw = DEFAULT_PANEL_WATT / 1000.0 # 0.54 kW per panel
num_panels = math.ceil(required_kw / panel_kw) if required_kw > 0 else 0
estimated_monthly_generation = round(required_kw * UNITS_PER_KW_PER_MONTH, 2)
# ββ 4. RESPONSE βββββββββββββββββββββββββββββββββββββββββββ
return {
"current_units": round(current_units, 2),
"target_units": round(target_units, 2),
"units_to_offset": round(units_to_offset, 2),
"recommended_solar_kw": round(required_kw, 3),
"recommended_panels": num_panels,
"estimated_monthly_generation": estimated_monthly_generation,
}, 200
except Exception as exc:
return {"error": "Internal server error", "details": str(exc)}, 500
# ------------------------------------------------------------------
# Core calculation helpers
# ------------------------------------------------------------------
@staticmethod
def calculate_bill_from_units(units: float, slabs: list[dict]) -> float:
"""
Forward calculation: compute the electricity bill (βΉ) for a given
number of consumed units using the provided tariff slabs.
Parameters
----------
units : float
Total electricity consumed in kWh.
slabs : list[dict]
Ordered list of slab dicts with keys ``min``, ``max``, ``rate``.
``max`` of ``None`` means the slab is unbounded.
Returns
-------
float
Total bill amount in βΉ.
"""
bill = 0.0
remaining = units
for slab in slabs:
if remaining <= 0:
break
slab_min: int = slab["min"]
slab_max = slab["max"] # None for last slab
rate: float = slab["rate"]
# Effective width of this slab
if slab_max is None:
slab_units = remaining # consume all that's left
else:
slab_capacity = slab_max - slab_min + 1
slab_units = min(remaining, slab_capacity)
bill += slab_units * rate
remaining -= slab_units
return round(bill, 2)
@staticmethod
def estimate_units_from_bill(bill: float, slabs: list[dict]) -> float:
"""
Reverse calculation: estimate total kWh consumed to produce a given
monthly bill amount using progressive slab accumulation.
Parameters
----------
bill : float
Monthly electricity bill in βΉ.
slabs : list[dict]
Same slab structure as ``calculate_bill_from_units``.
Returns
-------
float
Estimated units consumed in kWh.
"""
units = 0.0
remaining = bill
for slab in slabs:
if remaining <= 0:
break
slab_min: int = slab["min"]
slab_max = slab["max"]
rate: float = slab["rate"]
if slab_max is None:
# Last slab β consume all remaining bill at this rate
units += remaining / rate
remaining = 0.0
else:
slab_capacity = slab_max - slab_min + 1 # units in slab
slab_full_cost = slab_capacity * rate # βΉ to exhaust slab
if remaining >= slab_full_cost:
# Entire slab consumed
units += slab_capacity
remaining -= slab_full_cost
else:
# Partial slab
units += remaining / rate
remaining = 0.0
return round(units, 4)
# Validation is fully delegated to BillOptimizationRequestSerializer.
# The service trusts that validated_data already contains correct types.
|