Function Reference & Data Flow
This document provides a comprehensive catalog of all functions in PVTools with complete source code and detailed explanations for developers to understand the entire optimization workflow.
Table of Contents
- Core Entry Points
- Production Modeling
- Technical Utilities
- Financial Utilities
- Data Processing
- Profile Estimation
- BESS Utilities
- Optimization Engines
- Data Flow Analysis
- Key Data Structures
Core Entry Points
main() - Consumption Profile Estimation
Location: pv_tools/optimiser.py
Purpose: Initialize optimization environment and estimate consumption profile from billing data
Source Code:
def main(
c: Dict,
log_file: Optional[str] = None,
typical_consumption: Optional[List[float]] = None,
) -> List[float]:
# delete log_file if it exists
if log_file is not None and os.path.exists(log_file):
os.remove(log_file)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S",
filename=log_file,
)
stream_handler = logging.StreamHandler()
stream_handler.setLevel(logging.INFO)
stream_formatter = logging.Formatter("%(levelname)s: %(message)s")
stream_handler.setFormatter(stream_formatter)
logging.getLogger().addHandler(stream_handler)
logger = logging.getLogger(__name__)
logger.info(f"Latitude: {c['latitude']}, Longitude: {c['longitude']}")
# assuming 30 days in a month
if typical_consumption is None:
logger.info(
"No typical consumption provided. Calculating typical consumption based on yaml info."
)
monthly_energy = solve_pre_surcharge_bill(
c,
c["parameters"]["tariff_category"],
c["parameters"]["monthly_bill"],
c["parameters"]["peak_power_demand"],
c["parameters"]["peak_fraction"],
)
logger.info(f"Monthly energy consumption: {np.round(monthly_energy,0)} kWh")
daily_energy = monthly_energy / 30
typical_consumption = estimate_load_profile(
c["parameters"]["daytime_ratio"],
daily_energy,
c["parameters"]["peak_power_demand"],
)
return typical_consumption
Detailed Explanation:
-
Logging Setup:
- Removes existing log file if provided
- Configures dual logging (file + console) with timestamps
- File logging includes full details, console shows level and message only
-
Consumption Estimation Logic:
- If no consumption profile provided, performs reverse calculation
- Uses
solve_pre_surcharge_bill()to convert monthly bill to kWh consumption - Assumes 30-day month for daily energy calculation
- Calls
estimate_load_profile()to create realistic 24-hour pattern
-
Key Dependencies:
solve_pre_surcharge_bill(): Reverse bill calculationestimate_load_profile(): 24-hour profile generation- Configuration must include: tariff_category, monthly_bill, peak_power_demand, daytime_ratio
optimise() - Main Optimization Workflow
Location: pv_tools/optimiser.py
Purpose: Execute complete optimization workflow with dynamic optimizer selection
Source Code:
def optimise(c: Dict, typical_consumption: List[float]) -> Tuple[Dict, Dict]:
calc_type = c["parameters"]["contract_type"]
financing_method = c["parameters"]["financing"]
module_path = f"pv_tools.optimizers.{financing_method}.{calc_type}"
module = __import__(module_path, fromlist=[calc_type])
method = getattr(module, calc_type)
result, c = method(c, typical_consumption)
kg2tonnes = 1e-3
months2year = 12
W2kW = 1e-3
# calculate some additional outputs
result["no_of_panels"] = result["system_size"] / (
c["standard_parameters"]["power_output_per_panel"] * W2kW
)
result["annual_tnb_tariff_increase"] = c["prices"][
"TNB_avg_annual_tariff_increase"
]["rate"]
# esg metrics
result["ESG"] = {}
result["ESG"]["annual_tonnes_of_CO2_reduced"] = (
result["monthly_energy_production"]
* months2year
* c["ESG"]["grid_emission_factor"]["value"]
* kg2tonnes
)
result["ESG"]["trees_planted_per_year"] = (
result["ESG"]["annual_tonnes_of_CO2_reduced"]
/ kg2tonnes
/ c["ESG"]["mature_tree_absorption"]["value"]
)
return result, c
Detailed Explanation:
-
Dynamic Optimizer Selection:
- Constructs module path:
pv_tools.optimizers.{financing_method}.{calc_type} - Examples:
Tokenization.PPA,TermLoan.Instalment - Uses
__import__()for runtime module loading - Gets callable function from module
- Constructs module path:
-
Result Enrichment:
- Panel Count:
system_size / (power_output_per_panel * 1e-3) - Tariff Escalation: Annual TNB tariff increase rate
- ESG Metrics: CO2 reduction and tree equivalents
- Panel Count:
-
ESG Calculations:
- CO2 Reduction:
monthly_production * 12 * emission_factor * 1e-3 - Tree Equivalents:
CO2_reduction / tree_absorption_rate
- CO2 Reduction:
Production Modeling
modelSetup() - Configuration Enhancement
Location: pv_tools/production_modeling.py
Purpose: Load external data files and resolve geographic coordinates
Source Code:
def modelSetup(c: dict) -> dict:
# load in all necessary data
with open(c["paths"]["tariffs"], "r") as f:
grid_tariffs = json.load(f)
with open(c["paths"]["prices"], "r") as f:
prices = json.load(f)
with open(c["paths"]["ESG"], "r") as f:
ESG = json.load(f)
with open(c["paths"]["inverters"], "r") as f:
inverters = json.load(f)
with open(c["paths"]["CT_rating"], "r") as f:
CT_rating_data = json.load(f)
# get longitude and latitude if not provided
if (
c["parameters"]["location"]["longitude"] is None
or c["parameters"]["location"]["latitude"] is None
):
latitude, longitude = getCoordinates(
c["parameters"]["location"]["street_address"]
)
else:
latitude = c["parameters"]["location"]["latitude"]
longitude = c["parameters"]["location"]["longitude"]
# set export rate to 0 for SELCO scheme
if c["parameters"]["scheme"] == "SELCO":
c["standard_parameters"]["SMP_export"]["rate"] = 0
c["grid_tariffs"] = grid_tariffs
c["prices"] = prices
c["ESG"] = ESG
c["inverters"] = inverters
c["latitude"] = latitude
c["longitude"] = longitude
c["CT_rating_data"] = CT_rating_data
return c
Detailed Explanation:
-
Data File Loading:
- Loads 5 critical JSON files: tariffs, prices, ESG, inverters, CT_rating
- Each file contains structured data for calculations
- Files are loaded synchronously (could be optimized for parallel loading)
-
Geographic Resolution:
- Checks if lat/lon provided in config
- If missing, calls
getCoordinates()for geocoding - Uses OpenStreetMap Nominatim service
-
Scheme-Specific Adjustments:
- SELCO scheme: Sets export rate to 0 (no grid export)
- NOVA/SARE: Uses configured export rates
Technical Utilities
calculate_bill() - TNB Bill Calculation Engine
Location: pv_tools/utilities/technical.py
Purpose: Calculate comprehensive TNB electricity bills with all surcharges
Source Code:
def calculate_bill(
c,
tariff_category,
total_consumption,
peak_power_demand=None,
peak_fraction=None,
):
# first check if total_consumption is negative
if total_consumption < 0:
# use SMP export rate if negative
total_energy_charge = (
total_consumption * c["standard_parameters"]["SMP_export"]["rate"]
)
return total_energy_charge
tariff_json = c["grid_tariffs"]
tariff_data = tariff_json.get(tariff_category)
if not tariff_data:
raise ValueError(f"Tariff category {tariff_category} not found in tariff JSON.")
charge_type = tariff_data.get("charge_type", "energy_only")
if charge_type == "energy_only":
# This covers tariffs that use a tiered pricing structure.
tiers = tariff_data.get("tiers")
if tiers is None:
# Fall back to a single energy_charge if provided.
energy_info = tariff_data.get("energy_charge")
if energy_info is None:
raise ValueError(
"No tier or energy_charge information available for tariff category."
)
effective_rate = energy_info["rate"] / 100.0 # Convert sen to RM
total_energy_charge = total_consumption * effective_rate
remaining_consumption = total_consumption
total_energy_charge = 0.0
for tier in tiers:
rate_rm = tier["rate"] / 100.0 # convert sen/kWh to RM/kWh
limit = tier.get(
"limit"
) # kWh limit for this tier; None indicates no upper bound
if limit is not None:
if remaining_consumption >= limit:
total_energy_charge += limit * rate_rm
remaining_consumption -= limit
else:
total_energy_charge += remaining_consumption * rate_rm
remaining_consumption = 0
break
else:
# Unlimited tier: all remaining consumption converts to charge
total_energy_charge += remaining_consumption * rate_rm
remaining_consumption = 0
break
elif charge_type == "energy_and_demand":
total_energy_charge = 0.0
if peak_power_demand is not None:
total_energy_charge += (
peak_power_demand * tariff_data["demand_charge"]["rate"]
)
# For tariffs that include both energy and demand charges, we assume the
# total_energy_charge provided is for energy consumption only.
if "energy_charge" in tariff_data:
# Use a single energy charge rate.
effective_rate = tariff_data["energy_charge"]["rate"] / 100.0
total_energy_charge += total_consumption * effective_rate
elif (
"energy_charge_peak" in tariff_data
and "energy_charge_offpeak" in tariff_data
):
# Use separate peak and off-peak rates.
if peak_fraction is None:
peak_fraction = 0.5 # default 50% if not provided
peak_rate = tariff_data["energy_charge_peak"]["rate"]
offpeak_rate = tariff_data["energy_charge_offpeak"]["rate"]
total_energy_charge += (
peak_fraction * total_consumption * peak_rate
+ (1 - peak_fraction) * total_consumption * offpeak_rate
) / 100.0
else:
raise ValueError("Energy charge information missing for tariff category.")
else:
raise ValueError(
f"Unknown charge_type '{charge_type}' for tariff category {tariff_category}."
)
# add in ICPT charges
ICPT_charge = total_consumption * c["standard_parameters"]["ICPT"]["rate"]
# add in KWTBB charges
KWTBB_charge = total_energy_charge * c["standard_parameters"]["KWTBB"]["rate"]
total_energy_charge += ICPT_charge
total_energy_charge += KWTBB_charge
return total_energy_charge
Detailed Explanation:
-
Export Handling:
- Negative consumption = solar export
- Uses SMP export rate for credits
- Returns negative value (credit to customer)
-
Tariff Structure Support:
- energy_only: Tiered pricing (e.g., Domestic B)
- energy_and_demand: Commercial tariffs with demand charges
- Time-of-Use: Peak/off-peak rate structures
-
Tiered Billing Logic:
- Processes tiers sequentially
- Each tier has rate and limit (None = unlimited)
- Accumulates charges until consumption exhausted
-
Surcharge Application:
- ICPT: Applied to total consumption
- KWTBB: Applied to energy charge (not demand)
Data Flow Analysis
Main Optimization Workflow
YAML Config
↓
modelSetup() - Load data files & resolve coordinates
↓
Enhanced Config (with tariffs, prices, ESG, inverters, lat/lon)
↓
main() - Estimate consumption profile
↓
Consumption Profile (24 hourly values)
↓
optimise() - Dynamic optimizer selection
↓
Financing Optimizer (Tokenization.PPA, TermLoan.Instalment, etc.)
↓
power_and_energy() - Core energy balance
↓
Optimization Loop (SLSQP)
↓
Results + ESG Metrics
Energy Balance Process Flow
daily_consumption + config
↓
Scheme Constraints (NOVA/SELCO/SARE)
↓
Load Historical Production Data
↓
Scale by System Size
↓
Calculate Power Delta (consumption - production)
↓
BESS Sizing (if enabled)
↓
SOC Simulation (hour-by-hour)
↓
Monthly Aggregation
↓
Bill Calculations (with/without solar)
↓
Export Credits & Grid Draw
↓
Return monthly_data + storage_size
Data Dependencies
External APIs:
- PVGIS (solar production data)
- OpenStreetMap (geocoding)
Local Data Files:
- tariffs.json (TNB tariff structures)
- prices.json (equipment & service costs)
- ESG.json (emission factors, tree absorption)
- inverters.json (available inverter specs)
- CT_rating.json (connection capacity limits)
Function Dependencies:
- calculate_bill() ← tariffs.json
- size_inverters() ← inverters.json
- get_fuse_limitation() ← CT_rating.json
- calculate_project_cost() ← prices.json
- ESG calculations ← ESG.json
Key Data Structures
Configuration Dictionary (c)
{
"parameters": {
"location": {
"latitude": float, # Decimal degrees
"longitude": float, # Decimal degrees
"street_address": str # For geocoding if lat/lon missing
},
"tariff_category": str, # "B", "C1", "C2", etc.
"monthly_bill": float, # Current monthly bill (RM)
"peak_power_demand": float, # Peak demand (kW)
"scheme": str, # "NOVA", "SELCO", "SARE"
"contract_type": str, # "PPA", "DirectPurchase", "Instalment"
"financing": str, # "Tokenization", "TermLoan"
"daytime_ratio": float, # Daytime consumption ratio
"efficiency_scaling": float # System efficiency factor
},
"decision_variables": {
"system_size": float, # Solar system size (kWp)
"declination": float, # Panel tilt angle
"azimuth": float # Panel orientation
},
"paths": {
"tariffs": str, # Path to tariffs.json
"prices": str, # Path to prices.json
"ESG": str, # Path to ESG.json
"inverters": str, # Path to inverters.json
"CT_rating": str # Path to CT_rating.json
},
"standard_parameters": {
"SMP_export": {"rate": float}, # Export rate (RM/kWh)
"power_output_per_panel": float, # Panel power (W)
"ICPT": {"rate": float}, # ICPT surcharge rate
"KWTBB": {"rate": float} # KWTBB surcharge rate
}
}
Monthly Data DataFrame
{
"year": int, # Year
"month": int, # Month (1-12)
"E_consumed": float, # Monthly consumption (kWh)
"E_produced": float, # Monthly production (kWh)
"E_grid_draw": float, # Grid import (kWh)
"exported_kWh": float, # Grid export (kWh)
"export_credit_RM": float, # Export earnings (RM)
"peak_power_demand": float, # Peak demand with solar (kW)
"bill_excluding_solar": float, # Grid bill minus export credits (RM)
"bill_without_solar": float, # Total bill without solar (RM)
"bill_equivalent_solar": float, # Cost of solar at grid rates (RM)
"peak_sun_hours": float # Equivalent peak sun hours
}
Optimization Results Dictionary
{
"system_size": float, # Optimal system size (kWp)
"PPA_tariff": float, # Optimal PPA rate (RM/kWh)
"monthly_energy_consumption": float, # Monthly consumption (kWh)
"monthly_energy_production": float, # Monthly production (kWh)
"total_savings": float, # Total savings percentage
"savings_on_solar_portion": float, # Solar portion savings percentage
"total_monthly_bill_with_solar": float, # Total bill with solar (RM)
"total_monthly_bill_without_solar": float, # Total bill without solar (RM)
"monthly_data": pd.DataFrame, # Complete monthly data
"daily_consumption": List[float], # 24-hour consumption profile
"no_of_panels": int, # Number of solar panels
"ESG": {
"annual_tonnes_of_CO2_reduced": float, # CO2 reduction
"trees_planted_per_year": float # Tree equivalents
}
}
This comprehensive function reference provides developers with complete source code, detailed explanations, and data flow analysis for understanding and extending PVTools' optimization capabilities.