simulator / src /simulator.py
mathiassandnes's picture
Claude Opus 4.5
Initial commit: LiiS Circular Economy Simulator
651ab81
Raw History Blame Contribute Delete
6.59 kB
"""
LiiS Circular Economy Simulator
Core calculations for CTI metrics and P&L projections.
"""
from dataclasses import dataclass
from pathlib import Path
import yaml
@dataclass
class CTIMetrics:
"""Circular Transition Indicator metrics."""
eolrr: float # End-of-Life Recovery Rate (0-1)
pri: float # Primary Resource Input (0-1, lower is better)
lifetime_extension: float # Ratio of actual to design lifetime
units_recovered: int # Absolute units recovered per cycle
blended_cost: float # EUR per unit (new + refurb weighted)
@dataclass
class PLProjection:
"""Profit & Loss projection."""
revenue_per_unit: float
cost_per_unit: float
margin_per_unit: float
margin_percent: float
break_even_price: float
def load_product(name: str) -> dict:
"""Load product configuration from YAML."""
config_path = Path(__file__).parent.parent / "products" / name / "config.yaml"
if not config_path.exists():
raise FileNotFoundError(f"Product config not found: {config_path}")
return yaml.safe_load(config_path.read_text())
def calculate_cti(config: dict, distributed_units: int = 200) -> CTIMetrics:
"""
Calculate CTI metrics for a product configuration.
Args:
config: Product configuration dict (from YAML)
distributed_units: Number of units distributed in pilot
Returns:
CTIMetrics with calculated values
"""
returns = config["return_assumptions"]
design = config["design"]
costs = config["costs"]
# EoLRR = Return Rate × (Grade A + Grade B percentage)
return_rate = returns["return_rate"]
grade_a = returns["grade_distribution"]["a"]
grade_b = returns["grade_distribution"]["b"]
eolrr = return_rate * (grade_a + grade_b)
# Units recovered
units_recovered = int(distributed_units * eolrr)
# PRI for Year 2 steady state (simplified)
# Assumes monthly demand of distributed_units / 12
monthly_demand = distributed_units / 12
months_until_return = returns["months_until_return"]
refurb_available = units_recovered / (months_until_return / 12)
# New units needed = demand - refurb available
new_units_monthly = max(0, monthly_demand - refurb_available)
total_monthly = monthly_demand
pri = new_units_monthly / total_monthly if total_monthly > 0 else 1.0
# Lifetime Extension
cycles = design["max_cycles"]
use_per_cycle = design["use_cycle_years"]
design_lifetime = design["lifetime_years"]
lifetime_extension = (cycles * use_per_cycle) / design_lifetime
# Blended cost
mfg_cost = costs["manufacturing_unit_cost"]
avg_refurb_cost = (
costs["refurbishment"]["grade_a"] * grade_a
+ costs["refurbishment"]["grade_b"] * grade_b
) / (grade_a + grade_b)
blended_cost = (
new_units_monthly * mfg_cost + refurb_available * avg_refurb_cost
) / total_monthly
return CTIMetrics(
eolrr=round(eolrr, 3),
pri=round(pri, 3),
lifetime_extension=round(lifetime_extension, 2),
units_recovered=units_recovered,
blended_cost=round(blended_cost, 2),
)
def calculate_pl(config: dict, cti: CTIMetrics) -> PLProjection:
"""
Calculate P&L projection based on CTI metrics.
Args:
config: Product configuration dict
cti: Calculated CTI metrics
Returns:
PLProjection with financial metrics
"""
pricing = config["pricing"]
costs = config["costs"]
# Revenue per unit over lease term
monthly_price = pricing["lease_monthly"]
lease_months = pricing["lease_term_months"]
revenue_per_unit = monthly_price * lease_months
# Cost per unit (blended manufacturing + logistics)
logistics_cost = costs["logistics"]["delivery"] + costs["logistics"]["return"]
cost_per_unit = cti.blended_cost + logistics_cost
# Margin
margin_per_unit = revenue_per_unit - cost_per_unit
margin_percent = (margin_per_unit / revenue_per_unit) * 100 if revenue_per_unit > 0 else 0
# Break-even monthly price
break_even_price = cost_per_unit / lease_months
return PLProjection(
revenue_per_unit=round(revenue_per_unit, 2),
cost_per_unit=round(cost_per_unit, 2),
margin_per_unit=round(margin_per_unit, 2),
margin_percent=round(margin_percent, 1),
break_even_price=round(break_even_price, 2),
)
def format_report(config: dict, cti: CTIMetrics, pl: PLProjection) -> str:
"""Generate a markdown report for a product."""
name = config["product"]["name"]
report = f"""# CTI Analysis: {name}
## Summary
| Metric | Value | Target |
|--------|-------|--------|
| EoLRR | {cti.eolrr:.1%} | {config['cti_parameters']['target_eolrr']:.0%} |
| PRI (Year 2) | {cti.pri:.1%} | {config['cti_parameters']['target_pri_year2']:.0%} |
| Lifetime Extension | {cti.lifetime_extension:.1f}x | 1.0x |
## P&L Projection
| Metric | Value |
|--------|-------|
| Revenue per unit | EUR {pl.revenue_per_unit:.2f} |
| Cost per unit (blended) | EUR {pl.cost_per_unit:.2f} |
| Margin per unit | EUR {pl.margin_per_unit:.2f} |
| Margin % | {pl.margin_percent:.1f}% |
| Break-even price | EUR {pl.break_even_price:.2f}/month |
## Configuration
- **Lease price**: EUR {config['pricing']['lease_monthly']}/month
- **Lease term**: {config['pricing']['lease_term_months']} months
- **Manufacturing cost**: EUR {config['costs']['manufacturing_unit_cost']}
- **Return rate**: {config['return_assumptions']['return_rate']:.0%}
## Grade Distribution
| Grade | % | Refurb Cost |
|-------|---|-------------|
| A (reusable) | {config['return_assumptions']['grade_distribution']['a']:.0%} | EUR {config['costs']['refurbishment']['grade_a']} |
| B (refurbish) | {config['return_assumptions']['grade_distribution']['b']:.0%} | EUR {config['costs']['refurbishment']['grade_b']} |
| C (scrap) | {config['return_assumptions']['grade_distribution']['c']:.0%} | EUR {config['costs']['refurbishment']['grade_c']} |
"""
return report
if __name__ == "__main__":
# Example usage
for product_name in ["besafe", "hamax"]:
print(f"\n{'='*50}")
print(f"Analyzing: {product_name}")
print("=" * 50)
config = load_product(product_name)
cti = calculate_cti(config)
pl = calculate_pl(config, cti)
print(f"EoLRR: {cti.eolrr:.1%}")
print(f"PRI: {cti.pri:.1%}")
print(f"Lifetime Extension: {cti.lifetime_extension:.1f}x")
print(f"Blended Cost: EUR {cti.blended_cost:.2f}")
print(f"Margin: {pl.margin_percent:.1f}%")