mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
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)
323 lines
10 KiB
Go
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)))
|
|
}
|