turash/models/match/match.go
Damir Mukimov 4a2fda96cd
Initial commit: Repository setup with .gitignore, golangci-lint v2.6.0, and code quality checks
- Initialize git repository
- Add comprehensive .gitignore for Go projects
- Install golangci-lint v2.6.0 (latest v2) globally
- Configure .golangci.yml with appropriate linters and formatters
- Fix all formatting issues (gofmt)
- Fix all errcheck issues (unchecked errors)
- Adjust complexity threshold for validation functions
- All checks passing: build, test, vet, lint
2025-11-01 07:36:22 +01:00

379 lines
13 KiB
Go

package match
import (
"fmt"
"math"
"github.com/damirmukimov/city_resource_graph/models/transport"
)
// EconomicCalculation represents the economic viability calculations for a match
// Based on economic_calculation.json schema
type EconomicCalculation struct {
MatchID string `json:"match_id"`
SourceResource ResourceFlowSummary `json:"source_resource"`
TargetResource ResourceFlowSummary `json:"target_resource"`
Calculations MatchCalculations `json:"calculations"`
Assumptions CalculationAssumptions `json:"assumptions"`
}
// ResourceFlowSummary represents a simplified resource flow for matching
type ResourceFlowSummary struct {
ID string `json:"id"`
Type string `json:"type"`
Direction string `json:"direction"`
Quantity float64 `json:"quantity"`
Unit string `json:"unit"`
CostPerUnit float64 `json:"cost_per_unit"`
}
// MatchCalculations contains all economic calculations for a match
type MatchCalculations struct {
AnnualSavings float64 `json:"annual_savings"`
PaybackPeriodYears float64 `json:"payback_period_years"`
NPV10Years float64 `json:"npv_10_years"`
IRRPercent float64 `json:"irr_percent"`
TransportationCosts TransportationEstimate `json:"transportation_costs"`
CO2ReductionTonnes float64 `json:"co2_reduction_tonnes"`
ImplementationComplexity string `json:"implementation_complexity"`
RegulatoryRequirements []string `json:"regulatory_requirements"`
}
// TransportationEstimate represents transportation cost estimates for a match
type TransportationEstimate struct {
AnnualCost float64 `json:"annual_cost"`
DistanceKm float64 `json:"distance_km"`
Method string `json:"method"`
Feasibility float64 `json:"feasibility_score"` // 0-1 scale
}
// CalculationAssumptions contains the assumptions used in calculations
type CalculationAssumptions struct {
DiscountRate float64 `json:"discount_rate"`
OperatingHoursYear int `json:"operating_hours_year"`
MaintenanceCostFactor float64 `json:"maintenance_cost_factor"`
EnergyCostInflation float64 `json:"energy_cost_inflation"`
}
// MatchEconomicsParams contains parameters for match economic calculations
type MatchEconomicsParams struct {
SourceResource ResourceFlowSummary
TargetResource ResourceFlowSummary
DistanceKm float64
InitialInvestment float64 // One-time setup costs
SymbiosisType transport.SymbiosisType
Complexity string
RiskLevel string
AnnualQuantity float64 // Units exchanged per year
UnitValue float64 // € per unit exchanged
CO2ReductionFactor float64 // tonnes CO2 per unit exchanged
}
// DefaultCalculationAssumptions returns standard assumptions for calculations
func DefaultCalculationAssumptions() CalculationAssumptions {
return CalculationAssumptions{
DiscountRate: 0.08, // 8% discount rate
OperatingHoursYear: 8000, // 8000 hours/year
MaintenanceCostFactor: 0.05, // 5% of capital annually
EnergyCostInflation: 0.02, // 2% annual inflation
}
}
// CalculateMatchEconomics computes all economic metrics for a potential match
func CalculateMatchEconomics(params MatchEconomicsParams, assumptions CalculationAssumptions) (*EconomicCalculation, error) {
if params.SourceResource.ID == "" || params.TargetResource.ID == "" {
return nil, fmt.Errorf("source and target resource IDs are required")
}
// Calculate annual savings (cost reduction for source, value creation for target)
annualSavings := calculateAnnualSavings(params)
// Calculate transportation costs using the exchange cost calculator
transportCost, err := calculateTransportationCost(params)
if err != nil {
return nil, fmt.Errorf("failed to calculate transportation cost: %w", err)
}
// Calculate CO2 reduction
co2Reduction := params.AnnualQuantity * params.CO2ReductionFactor
// Calculate implementation complexity
complexity := assessImplementationComplexity(params)
// Identify regulatory requirements
regulatoryReqs := identifyRegulatoryRequirements(params)
// Calculate NPV over 10 years
npv10Years := calculateMatchNPV(params, assumptions, annualSavings, transportCost.AnnualCost)
// Calculate IRR
irrPercent := calculateMatchIRR(params, assumptions, annualSavings, transportCost.AnnualCost)
// Calculate payback period
paybackYears := calculatePaybackPeriod(params.InitialInvestment, annualSavings-transportCost.AnnualCost)
calculations := MatchCalculations{
AnnualSavings: annualSavings,
PaybackPeriodYears: paybackYears,
NPV10Years: npv10Years,
IRRPercent: irrPercent * 100, // Convert to percentage
TransportationCosts: *transportCost,
CO2ReductionTonnes: co2Reduction,
ImplementationComplexity: complexity,
RegulatoryRequirements: regulatoryReqs,
}
result := &EconomicCalculation{
MatchID: generateMatchID(params.SourceResource.ID, params.TargetResource.ID),
SourceResource: params.SourceResource,
TargetResource: params.TargetResource,
Calculations: calculations,
Assumptions: assumptions,
}
return result, nil
}
// calculateAnnualSavings calculates the annual cost savings or value creation from the match
func calculateAnnualSavings(params MatchEconomicsParams) float64 {
// For waste-to-resource matches, savings = source disposal cost reduction + target value creation
sourceSavings := params.SourceResource.CostPerUnit * params.AnnualQuantity // Avoid disposal costs
targetValue := params.TargetResource.CostPerUnit * params.AnnualQuantity // Value of resource to target
return sourceSavings + targetValue
}
// calculateTransportationCost uses the exchange cost calculator to determine transport costs
func calculateTransportationCost(params MatchEconomicsParams) (*TransportationEstimate, error) {
exchangeParams := transport.ExchangeParams{
DistanceKm: params.DistanceKm,
Value: params.UnitValue * params.AnnualQuantity, // Annual exchange value
Volume: params.AnnualQuantity,
SymbiosisType: params.SymbiosisType,
Complexity: params.Complexity,
RiskLevel: params.RiskLevel,
}
cost, err := transport.CalculateExchangeCost(exchangeParams)
if err != nil {
return nil, err
}
estimate := &TransportationEstimate{
AnnualCost: cost.TotalAnnualCost,
DistanceKm: params.DistanceKm,
Method: determineTransportMethod(params.SymbiosisType),
Feasibility: feasibilityToScore(cost.Feasibility),
}
return estimate, nil
}
// calculateMatchNPV calculates Net Present Value for the match over 10 years
func calculateMatchNPV(params MatchEconomicsParams, assumptions CalculationAssumptions,
annualSavings, annualTransportCost float64) float64 {
npv := -params.InitialInvestment // Initial investment (negative cash flow)
annualNetBenefit := annualSavings - annualTransportCost
maintenanceCost := params.InitialInvestment * assumptions.MaintenanceCostFactor
for year := 1; year <= 10; year++ {
// Apply energy cost inflation to benefits
inflatedBenefit := annualNetBenefit * math.Pow(1+assumptions.EnergyCostInflation, float64(year-1))
annualMaintenance := maintenanceCost * math.Pow(1+assumptions.EnergyCostInflation, float64(year-1))
cashFlow := inflatedBenefit - annualMaintenance
npv += cashFlow / math.Pow(1+assumptions.DiscountRate, float64(year))
}
return npv
}
// calculateMatchIRR calculates Internal Rate of Return for the match
func calculateMatchIRR(params MatchEconomicsParams, assumptions CalculationAssumptions,
annualSavings, annualTransportCost float64) float64 {
// Create cash flow array
cashFlows := make([]float64, 11) // Year 0 to 10
cashFlows[0] = -params.InitialInvestment
annualNetBenefit := annualSavings - annualTransportCost
maintenanceCost := params.InitialInvestment * assumptions.MaintenanceCostFactor
for year := 1; year <= 10; year++ {
inflatedBenefit := annualNetBenefit * math.Pow(1+assumptions.EnergyCostInflation, float64(year-1))
annualMaintenance := maintenanceCost * math.Pow(1+assumptions.EnergyCostInflation, float64(year-1))
cashFlows[year] = inflatedBenefit - annualMaintenance
}
// Use Newton's method to find IRR
return calculateIRR(cashFlows)
}
// calculateIRR implements Newton's method to find IRR
func calculateIRR(cashFlows []float64) float64 {
irr := 0.1 // Initial guess: 10%
maxIterations := 1000
tolerance := 0.00001
for i := 0; i < maxIterations; i++ {
npv := 0.0
derivative := 0.0
for t, cf := range cashFlows {
if t == 0 {
npv += cf
derivative -= float64(t+1) * cf / math.Pow(1+irr, float64(t+2))
} else {
npv += cf / math.Pow(1+irr, float64(t))
derivative -= float64(t) * cf / math.Pow(1+irr, float64(t+1))
}
}
if math.Abs(npv) < tolerance {
return irr
}
if derivative == 0 {
return 0.0 // Cannot converge
}
irr = irr - npv/derivative
}
return irr // Return best approximation
}
// calculatePaybackPeriod calculates the payback period in years
func calculatePaybackPeriod(initialInvestment, annualNetCashFlow float64) float64 {
if annualNetCashFlow <= 0 {
return 999 // Never pays back
}
years := initialInvestment / annualNetCashFlow
// Cap at 10 years for practicality
if years > 10 {
return 999
}
return years
}
// assessImplementationComplexity determines implementation complexity
func assessImplementationComplexity(params MatchEconomicsParams) string {
score := 0.0
// Distance factor
if params.DistanceKm > 25 {
score += 2.0
} else if params.DistanceKm > 5 {
score += 1.0
}
// Resource type complexity
switch params.SymbiosisType {
case transport.SymbiosisWasteToResource, transport.SymbiosisMaterialRecycling:
score += 1.5 // Complex regulatory requirements
case transport.SymbiosisEnergyCascading, transport.SymbiosisUtilitySharing:
score += 1.0 // Moderate technical complexity
}
// Investment size
if params.InitialInvestment > 50000 {
score += 1.0
}
if score >= 3.0 {
return "high"
} else if score >= 1.5 {
return "medium"
}
return "low"
}
// identifyRegulatoryRequirements determines required permits and approvals
func identifyRegulatoryRequirements(params MatchEconomicsParams) []string {
requirements := []string{}
// Waste and material handling always requires permits
if params.SymbiosisType == transport.SymbiosisWasteToResource ||
params.SymbiosisType == transport.SymbiosisMaterialRecycling {
requirements = append(requirements, "waste_disposal_permit")
requirements = append(requirements, "environmental_impact_assessment")
}
// Energy transfers may require grid connection permits
if params.SymbiosisType == transport.SymbiosisEnergyCascading ||
params.SymbiosisType == transport.SymbiosisUtilitySharing {
requirements = append(requirements, "energy_distribution_license")
}
// Long distance transport may require additional permits
if params.DistanceKm > 50 {
requirements = append(requirements, "transport_license")
}
// High value exchanges may require insurance
if params.UnitValue*params.AnnualQuantity > 100000 {
requirements = append(requirements, "liability_insurance")
}
return requirements
}
// determineTransportMethod suggests the appropriate transport method
func determineTransportMethod(symbiosisType transport.SymbiosisType) string {
switch symbiosisType {
case transport.SymbiosisWasteToResource, transport.SymbiosisMaterialRecycling:
return "truck"
case transport.SymbiosisEnergyCascading, transport.SymbiosisUtilitySharing:
if symbiosisType == transport.SymbiosisEnergyCascading {
return "heat_pipe"
}
return "pipeline"
case transport.SymbiosisDataSharing, transport.SymbiosisIoTNetwork, transport.SymbiosisSoftwareLicenses:
return "network"
default:
return "truck"
}
}
// feasibilityToScore converts feasibility string to numeric score
func feasibilityToScore(feasibility string) float64 {
switch feasibility {
case "high":
return 0.9
case "medium":
return 0.6
case "low":
return 0.3
default:
return 0.5
}
}
// generateMatchID creates a unique match identifier
func generateMatchID(sourceID, targetID string) string {
return fmt.Sprintf("match_%s_%s", sourceID, targetID)
}
// ValidateEconomicCalculation validates the calculation results
func ValidateEconomicCalculation(calc *EconomicCalculation) error {
if calc.MatchID == "" {
return fmt.Errorf("match ID is required")
}
if calc.Calculations.AnnualSavings < 0 {
return fmt.Errorf("annual savings cannot be negative")
}
if calc.Calculations.PaybackPeriodYears < 0 {
return fmt.Errorf("payback period cannot be negative")
}
if calc.Calculations.IRRPercent < -100 || calc.Calculations.IRRPercent > 1000 {
return fmt.Errorf("IRR percentage out of reasonable range")
}
return nil
}