turash/bugulma/backend/internal/matching/plugins/heat_plugin.go
Damir Mukimov 000eab4740
Major repository reorganization and missing backend endpoints implementation
Repository Structure:
- Move files from cluttered root directory into organized structure
- Create archive/ for archived data and scraper results
- Create bugulma/ for the complete application (frontend + backend)
- Create data/ for sample datasets and reference materials
- Create docs/ for comprehensive documentation structure
- Create scripts/ for utility scripts and API tools

Backend Implementation:
- Implement 3 missing backend endpoints identified in gap analysis:
  * GET /api/v1/organizations/{id}/matching/direct - Direct symbiosis matches
  * GET /api/v1/users/me/organizations - User organizations
  * POST /api/v1/proposals/{id}/status - Update proposal status
- Add complete proposal domain model, repository, and service layers
- Create database migration for proposals table
- Fix CLI server command registration issue

API Documentation:
- Add comprehensive proposals.md API documentation
- Update README.md with Users and Proposals API sections
- Document all request/response formats, error codes, and business rules

Code Quality:
- Follow existing Go backend architecture patterns
- Add proper error handling and validation
- Match frontend expected response schemas
- Maintain clean separation of concerns (handler -> service -> repository)
2025-11-25 06:01:16 +01:00

323 lines
10 KiB
Go

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)))
}