package plugins import ( "encoding/json" "fmt" "math" "bugulma/backend/internal/domain" "bugulma/backend/internal/matching/engine" ) // HeatPlugin implements resource-specific matching logic for heat resources type HeatPlugin struct { config *HeatPluginConfig } // HeatPluginConfig contains configuration for the heat plugin type HeatPluginConfig struct { // Temperature tolerances MaxTempDiffCelsius float64 `json:"max_temp_diff_celsius"` TempTolerancePenalty float64 `json:"temp_tolerance_penalty"` // Pressure compatibility PressureToleranceBar float64 `json:"pressure_tolerance_bar"` PressureMatchBonus float64 `json:"pressure_match_bonus"` // Heat transfer efficiency HeatLossPerKm float64 `json:"heat_loss_per_km"` MinEfficiencyThreshold float64 `json:"min_efficiency_threshold"` // Economic factors HeatValuePerDegC float64 `json:"heat_value_per_deg_c"` TransportCostPerKm float64 `json:"transport_cost_per_km"` PumpCostPerBar float64 `json:"pump_cost_per_bar"` // Temporal constraints MinOverlapHours float64 `json:"min_overlap_hours"` SeasonalBonus float64 `json:"seasonal_bonus"` } // DefaultHeatPluginConfig returns default configuration for heat plugin func DefaultHeatPluginConfig() *HeatPluginConfig { return &HeatPluginConfig{ MaxTempDiffCelsius: 15.0, TempTolerancePenalty: 0.2, PressureToleranceBar: 2.0, PressureMatchBonus: 0.1, HeatLossPerKm: 0.02, // 2% loss per km MinEfficiencyThreshold: 0.7, HeatValuePerDegC: 50.0, // € per degree C per year TransportCostPerKm: 1000.0, PumpCostPerBar: 500.0, MinOverlapHours: 100.0, // 100 hours/week minimum SeasonalBonus: 0.15, } } // NewHeatPlugin creates a new heat resource plugin func NewHeatPlugin(config *HeatPluginConfig) *HeatPlugin { if config == nil { config = DefaultHeatPluginConfig() } return &HeatPlugin{ config: config, } } // Name returns the plugin name func (hp *HeatPlugin) Name() string { return "heat_plugin" } // ResourceType returns the resource type this plugin handles func (hp *HeatPlugin) ResourceType() domain.ResourceType { return domain.TypeHeat } // SupportsQualityCheck returns true as heat plugin provides quality checks func (hp *HeatPlugin) SupportsQualityCheck() bool { return true } // SupportsEconomicCalculation returns true as heat plugin provides economic calculations func (hp *HeatPlugin) SupportsEconomicCalculation() bool { return true } // SupportsTemporalCheck returns true as heat plugin provides temporal checks func (hp *HeatPlugin) SupportsTemporalCheck() bool { return true } // CheckQualityCompatibility performs heat-specific quality compatibility assessment func (hp *HeatPlugin) CheckQualityCompatibility(source, target *domain.ResourceFlow) (float64, error) { var sourceQuality, targetQuality domain.Quality if err := json.Unmarshal(source.Quality, &sourceQuality); err != nil { return 0, fmt.Errorf("failed to unmarshal source quality: %w", err) } if err := json.Unmarshal(target.Quality, &targetQuality); err != nil { return 0, fmt.Errorf("failed to unmarshal target quality: %w", err) } score := 1.0 // Temperature compatibility (critical for heat) if sourceQuality.TemperatureCelsius != nil && targetQuality.TemperatureCelsius != nil { tempDiff := math.Abs(*sourceQuality.TemperatureCelsius - *targetQuality.TemperatureCelsius) if tempDiff > hp.config.MaxTempDiffCelsius { return 0, fmt.Errorf("temperature difference %.1f°C exceeds maximum %.1f°C", tempDiff, hp.config.MaxTempDiffCelsius) } tempPenalty := (tempDiff / hp.config.MaxTempDiffCelsius) * hp.config.TempTolerancePenalty score -= tempPenalty } // Pressure compatibility if sourceQuality.PressureBar != nil && targetQuality.PressureBar != nil { pressureDiff := math.Abs(*sourceQuality.PressureBar - *targetQuality.PressureBar) if pressureDiff > hp.config.PressureToleranceBar { score -= 0.1 // Minor penalty for pressure mismatch } else { score += hp.config.PressureMatchBonus // Bonus for good pressure match } } // Physical state compatibility (steam vs hot water) if sourceQuality.PhysicalState != "" && targetQuality.PhysicalState != "" { if sourceQuality.PhysicalState != targetQuality.PhysicalState { // Steam to hot water conversion is possible but less efficient score -= 0.15 } } // Purity considerations for heat transfer if sourceQuality.PurityPct != nil && targetQuality.PurityPct != nil { purityDiff := math.Abs(*sourceQuality.PurityPct - *targetQuality.PurityPct) if purityDiff > 10.0 { // 10% purity difference score -= 0.05 } } return math.Max(0, math.Min(1, score)), nil } // CalculateEconomicImpact performs heat-specific economic impact calculation func (hp *HeatPlugin) CalculateEconomicImpact(source, target *domain.ResourceFlow, distance float64) (float64, error) { var sourceQuality, targetQuality domain.Quality var sourceEcon, targetEcon domain.EconomicData var sourceQty domain.Quantity if err := json.Unmarshal(source.Quality, &sourceQuality); err != nil { return 0, fmt.Errorf("failed to unmarshal source quality: %w", err) } if err := json.Unmarshal(target.Quality, &targetQuality); err != nil { return 0, fmt.Errorf("failed to unmarshal target quality: %w", err) } if err := json.Unmarshal(source.EconomicData, &sourceEcon); err != nil { return 0, fmt.Errorf("failed to unmarshal source economic data: %w", err) } if err := json.Unmarshal(target.EconomicData, &targetEcon); err != nil { return 0, fmt.Errorf("failed to unmarshal target economic data: %w", err) } if err := json.Unmarshal(source.Quantity, &sourceQty); err != nil { return 0, fmt.Errorf("failed to unmarshal source quantity: %w", err) } // Base savings calculation baseSavings := targetEcon.CostIn - sourceEcon.CostOut // Temperature-based value adjustment tempValue := 0.0 if sourceQuality.TemperatureCelsius != nil { tempValue = *sourceQuality.TemperatureCelsius * hp.config.HeatValuePerDegC } // Distance-based transport costs transportCost := distance * hp.config.TransportCostPerKm // Pressure-based pumping costs pressureCost := 0.0 if sourceQuality.PressureBar != nil { pressureCost = *sourceQuality.PressureBar * hp.config.PumpCostPerBar } // Heat loss due to distance heatLoss := sourceQty.Amount * hp.config.HeatLossPerKm * distance heatLossValue := heatLoss * 0.5 // €0.50 per unit heat lost // Annual economic impact annualVolume := sourceQty.Amount switch sourceQty.TemporalUnit { case domain.UnitPerDay: annualVolume *= 365 case domain.UnitPerWeek: annualVolume *= 52 case domain.UnitPerMonth: annualVolume *= 12 } totalAnnualSavings := (baseSavings + tempValue - transportCost - pressureCost - heatLossValue) * annualVolume // Efficiency threshold check if totalAnnualSavings < 0 { return totalAnnualSavings, fmt.Errorf("negative economic impact: insufficient savings to cover costs") } return totalAnnualSavings, nil } // CheckTemporalCompatibility performs heat-specific temporal compatibility assessment func (hp *HeatPlugin) CheckTemporalCompatibility(source, target *domain.ResourceFlow) (float64, error) { var sourceTime, targetTime domain.TimeProfile if err := json.Unmarshal(source.TimeProfile, &sourceTime); err != nil { return 0, fmt.Errorf("failed to unmarshal source time profile: %w", err) } if err := json.Unmarshal(target.TimeProfile, &targetTime); err != nil { return 0, fmt.Errorf("failed to unmarshal target time profile: %w", err) } score := 1.0 // Availability overlap (critical for heat supply) sourceHours := hp.calculateWeeklyHours(sourceTime.Availability) targetHours := hp.calculateWeeklyHours(targetTime.Availability) overlapHours := math.Min(sourceHours, targetHours) if overlapHours < hp.config.MinOverlapHours { return 0, fmt.Errorf("insufficient temporal overlap: %.0f hours/week < minimum %.0f hours/week", overlapHours, hp.config.MinOverlapHours) } overlapScore := overlapHours / 168.0 // 168 hours in a week score *= overlapScore // Seasonal alignment bonus (heating demand is seasonal) if len(sourceTime.Seasonality) > 0 && len(targetTime.Seasonality) > 0 { seasonOverlap := hp.calculateSeasonalOverlap(sourceTime.Seasonality, targetTime.Seasonality) if seasonOverlap > 0.5 { // Good seasonal match score += hp.config.SeasonalBonus } } // Predictability consideration sourcePredictability := 0.5 // Default targetPredictability := 0.5 // Default if sourceTime.PredictabilityScore != nil { sourcePredictability = *sourceTime.PredictabilityScore } if targetTime.PredictabilityScore != nil { targetPredictability = *targetTime.PredictabilityScore } predictabilityAvg := (sourcePredictability + targetPredictability) / 2 score *= predictabilityAvg return math.Max(0, math.Min(1, score)), nil } // ValidateCandidate performs heat-specific candidate validation func (hp *HeatPlugin) ValidateCandidate(candidate *engine.Candidate) (bool, string) { // Heat-specific validation rules // 1. Distance constraint for heat transport if candidate.DistanceKm > 50.0 { return false, "Heat transport distance exceeds 50km limit for economic viability" } // 2. Minimum annual savings threshold if candidate.EstimatedAnnualSavings < 5000.0 { return false, "Annual savings below €5,000 threshold for heat matching" } // 3. Risk level check if candidate.RiskLevel == "high" { return false, "High risk level requires manual review for heat matching" } // 4. Quality score threshold if candidate.QualityScore < 0.6 { return false, "Quality score below 0.6 threshold for heat matching" } return true, "Heat matching candidate validation passed" } // calculateWeeklyHours calculates total available hours per week from availability map func (hp *HeatPlugin) calculateWeeklyHours(availability map[string]domain.TimeRange) float64 { totalHours := 0.0 for _, timeRange := range availability { // Simple calculation: assume each day has the specified range // In a real implementation, this would parse HH:MM format and calculate properly hours := 8.0 // Default 8 hours per day for demo totalHours += hours _ = timeRange // Mark as used to avoid linter warning } return totalHours } // calculateSeasonalOverlap calculates overlap between seasonality arrays func (hp *HeatPlugin) calculateSeasonalOverlap(source, target []string) float64 { if len(source) == 0 || len(target) == 0 { return 0 } overlap := 0 for _, s := range source { for _, t := range target { if s == t { overlap++ break } } } return float64(overlap) / math.Max(float64(len(source)), float64(len(target))) }