turash/bugulma/backend/internal/matching/engine/engine.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

417 lines
12 KiB
Go

package engine
import (
"encoding/json"
"math"
"bugulma/backend/internal/analysis/regulatory"
"bugulma/backend/internal/analysis/risk"
"bugulma/backend/internal/analysis/transport"
"bugulma/backend/internal/domain"
)
// Criteria defines the criteria for finding matches
type Criteria struct {
ResourceType domain.ResourceType
Direction domain.ResourceDirection
OrganizationID string
SiteID string
MaxDistanceKm float64
MinCompatibility float64
MinEconomicScore float64
MinTemporalScore float64
MinQualityScore float64
ExcludeSameOrg bool
Limit int
}
// Candidate represents a potential match before it's created
type Candidate struct {
SourceFlow *domain.ResourceFlow
TargetFlow *domain.ResourceFlow
DistanceKm float64
CompatibilityScore float64
EconomicScore float64
TemporalScore float64
QualityScore float64
OverallScore float64
Priority int
EstimatedAnnualSavings float64
RiskLevel string
}
// ScoringResult contains scoring components
type ScoringResult struct {
Compatibility float64
Economic float64
Temporal float64
Quality float64
Overall float64
}
// Engine handles the core matching algorithm and evaluation
type Engine struct {
riskService *risk.Service
transportService *transport.Service
regulatoryService *regulatory.Service
}
// NewEngine creates a new matching engine
func NewEngine(
riskSvc *risk.Service,
transportSvc *transport.Service,
regulatorySvc *regulatory.Service,
) *Engine {
return &Engine{
riskService: riskSvc,
transportService: transportSvc,
regulatoryService: regulatorySvc,
}
}
// FindMatches finds potential matches based on criteria
func (e *Engine) FindMatches(
criteria Criteria,
sourceFlows []*domain.ResourceFlow,
targetFlows []*domain.ResourceFlow,
) ([]*Candidate, error) {
var candidates []*Candidate
for _, source := range sourceFlows {
for _, target := range targetFlows {
// Check if flows are compatible (same type, opposite direction)
if !e.areFlowsCompatible(source, target, criteria) {
continue
}
// Calculate distance (simplified - would use actual geographic calculation)
distance := 10.0 // Placeholder
// Evaluate candidate
candidate := e.evaluateCandidate(source, target, distance, criteria)
// Check if candidate meets minimum criteria
if e.meetsCriteria(candidate, criteria) {
candidates = append(candidates, candidate)
}
}
}
// Rank and limit results
candidates = e.rankCandidates(candidates)
if criteria.Limit > 0 && len(candidates) > criteria.Limit {
candidates = candidates[:criteria.Limit]
}
return candidates, nil
}
// evaluateCandidate performs complete evaluation of a match candidate
func (e *Engine) evaluateCandidate(
source, target *domain.ResourceFlow,
distance float64,
criteria Criteria,
) *Candidate {
candidate := &Candidate{
SourceFlow: source,
TargetFlow: target,
DistanceKm: distance,
}
// Calculate all scoring components
candidate.CompatibilityScore = e.calculateCompatibilityScore(source, target)
candidate.TemporalScore = e.calculateTemporalScore(source, target)
candidate.QualityScore = e.calculateQualityScore(source, target)
candidate.EconomicScore = e.calculateEconomicScore(source, target, distance)
// Calculate overall score
scoring := &ScoringResult{
Compatibility: candidate.CompatibilityScore,
Economic: candidate.EconomicScore,
Temporal: candidate.TemporalScore,
Quality: candidate.QualityScore,
}
candidate.OverallScore = e.calculateOverallScore(scoring, distance)
// Calculate priority
candidate.Priority = e.calculatePriority(candidate.OverallScore, candidate.EstimatedAnnualSavings)
// Assess risk level
riskAssessment := e.riskService.AssessResourceFlowRisk(distance, source.Type, candidate.EconomicScore)
candidate.RiskLevel = riskAssessment.RiskLevel
return candidate
}
// calculateCompatibilityScore assesses technical compatibility
func (e *Engine) calculateCompatibilityScore(output, input *domain.ResourceFlow) float64 {
if output.Type != input.Type {
return 0.0 // Must be same resource type
}
compatibility := 1.0
// Parse quality data
var outputQuality, inputQuality domain.Quality
json.Unmarshal(output.Quality, &outputQuality)
json.Unmarshal(input.Quality, &inputQuality)
// Temperature compatibility (critical for heat/steam)
if outputQuality.TemperatureCelsius != nil && inputQuality.TemperatureCelsius != nil {
tempDiff := math.Abs(*outputQuality.TemperatureCelsius - *inputQuality.TemperatureCelsius)
maxTempDiff := 10.0 // 10°C tolerance
tempCompat := math.Max(0, 1.0-(tempDiff/maxTempDiff))
compatibility *= tempCompat
}
// Pressure compatibility
if outputQuality.PressureBar != nil && inputQuality.PressureBar != nil {
pressureDiff := math.Abs(*outputQuality.PressureBar - *inputQuality.PressureBar)
maxPressureDiff := *inputQuality.PressureBar * 0.2 // 20% tolerance
if maxPressureDiff > 0 {
pressureCompat := math.Max(0, 1.0-(pressureDiff/maxPressureDiff))
compatibility *= pressureCompat
}
}
// Purity compatibility
if outputQuality.PurityPct != nil && inputQuality.PurityPct != nil {
purityDiff := math.Abs(*outputQuality.PurityPct - *inputQuality.PurityPct)
maxPurityDiff := 5.0 // 5% tolerance
purityCompat := math.Max(0, 1.0-(purityDiff/maxPurityDiff))
compatibility *= purityCompat
}
// Physical state compatibility
if outputQuality.PhysicalState != "" && inputQuality.PhysicalState != "" {
if outputQuality.PhysicalState != inputQuality.PhysicalState {
compatibility *= 0.5 // Penalty for state mismatch
}
}
// Enhanced quality compatibility checks
compatibility *= e.calculateEnhancedQualityCompatibility(&outputQuality, &inputQuality)
return compatibility
}
// calculateEnhancedQualityCompatibility checks advanced quality parameters
func (e *Engine) calculateEnhancedQualityCompatibility(output, input *domain.Quality) float64 {
compatibility := 1.0
// Viscosity compatibility
if output.ViscosityCp != nil && input.ViscosityCp != nil {
viscosityDiff := math.Abs(*output.ViscosityCp - *input.ViscosityCp)
maxViscosityDiff := *input.ViscosityCp * 0.3 // 30% tolerance
if maxViscosityDiff > 0 {
viscosityCompat := math.Max(0, 1.0-(viscosityDiff/maxViscosityDiff))
compatibility *= viscosityCompat
}
}
// Certification compatibility
if len(output.Certifications) > 0 && len(input.Certifications) > 0 {
matchingCerts := 0
for _, outputCert := range output.Certifications {
for _, inputCert := range input.Certifications {
if outputCert == inputCert {
matchingCerts++
break
}
}
}
if len(input.Certifications) > 0 {
certCompat := float64(matchingCerts) / float64(len(input.Certifications))
compatibility *= certCompat
}
}
return compatibility
}
// calculateTemporalScore assesses temporal compatibility
func (e *Engine) calculateTemporalScore(output, input *domain.ResourceFlow) float64 {
var outputTime, inputTime domain.TimeProfile
json.Unmarshal(output.TimeProfile, &outputTime)
json.Unmarshal(input.TimeProfile, &inputTime)
score := 1.0
// Pattern match
patternMatch := 0.5
if outputTime.SupplyPattern == inputTime.SupplyPattern {
patternMatch = 1.0
}
score *= patternMatch
// Seasonality match
seasonMatch := 1.0
if len(outputTime.Seasonality) > 0 && len(inputTime.Seasonality) > 0 {
overlap := 0
for _, os := range outputTime.Seasonality {
for _, is := range inputTime.Seasonality {
if os == is {
overlap++
}
}
}
seasonMatch = float64(overlap) / float64(len(inputTime.Seasonality))
}
score *= seasonMatch
// Predictability match
predictabilityCompat := 1.0
if outputTime.PredictabilityScore != nil && inputTime.PredictabilityScore != nil {
predictabilityDiff := math.Abs(*outputTime.PredictabilityScore - *inputTime.PredictabilityScore)
maxPredictabilityDiff := 0.3 // 30% difference tolerance
predictabilityCompat = math.Max(0.5, 1.0-(predictabilityDiff/maxPredictabilityDiff))
}
score *= predictabilityCompat
return score
}
// calculateQualityScore assesses data quality
func (e *Engine) calculateQualityScore(output, input *domain.ResourceFlow) float64 {
precisionScores := map[domain.PrecisionLevel]float64{
domain.PrecisionMeasured: 1.0,
domain.PrecisionEstimated: 0.75,
domain.PrecisionRough: 0.5,
}
sourceScores := map[domain.SourceType]float64{
domain.SourceDevice: 1.0,
domain.SourceCalculated: 0.7,
domain.SourceDeclared: 0.6,
}
outputScore := precisionScores[output.PrecisionLevel] * sourceScores[output.SourceType]
inputScore := precisionScores[input.PrecisionLevel] * sourceScores[input.SourceType]
return (outputScore + inputScore) / 2
}
// calculateEconomicScore assesses economic viability
func (e *Engine) calculateEconomicScore(output, input *domain.ResourceFlow, distance float64) float64 {
var outputEcon, inputEcon domain.EconomicData
json.Unmarshal(output.EconomicData, &outputEcon)
json.Unmarshal(input.EconomicData, &inputEcon)
// Simplified economic calculation
// cost_in - cost_out - transport_cost
transportCost := e.transportService.AnalyzeTransport(distance, output.Type).CostPerUnit
// Get quantity
var outputQty domain.Quantity
json.Unmarshal(output.Quantity, &outputQty)
savings := inputEcon.CostIn - outputEcon.CostOut - transportCost
if savings > 0 {
return math.Min(savings*outputQty.Amount*0.01, 1.0) // Scale to 0-1
}
return 0.1 // Minimum score for negative economics
}
// calculateOverallScore computes weighted average
func (e *Engine) calculateOverallScore(scoring *ScoringResult, distance float64) float64 {
weights := map[string]float64{
"quality": 0.25,
"temporal": 0.20,
"economic": 0.15,
"trust": 0.15,
"transport": 0.10,
"regulatory": 0.10,
"distance": 0.05,
}
// Transport penalty
transportPenalty := math.Min(1.0, distance/50.0)
// Regulatory penalty
regulatoryPenalty := 0.1 // Simplified
overall := scoring.Compatibility*weights["quality"] +
scoring.Temporal*weights["temporal"] +
scoring.Economic*weights["economic"] +
scoring.Quality*weights["trust"] -
transportPenalty*weights["transport"] -
regulatoryPenalty*weights["regulatory"] +
(1.0-transportPenalty)*weights["distance"]
return math.Max(0, math.Min(1, overall))
}
// areFlowsCompatible checks if two flows can potentially match
func (e *Engine) areFlowsCompatible(source, target *domain.ResourceFlow, criteria Criteria) bool {
// Must be same resource type
if source.Type != target.Type {
return false
}
// Must be opposite directions
if source.Direction == target.Direction {
return false
}
// Must be different organizations (unless explicitly allowed)
if !criteria.ExcludeSameOrg && source.OrganizationID == target.OrganizationID {
return false
}
return true
}
// meetsCriteria checks if a candidate meets the specified criteria
func (e *Engine) meetsCriteria(candidate *Candidate, criteria Criteria) bool {
if candidate.CompatibilityScore < criteria.MinCompatibility {
return false
}
if candidate.EconomicScore < criteria.MinEconomicScore {
return false
}
if candidate.TemporalScore < criteria.MinTemporalScore {
return false
}
if candidate.QualityScore < criteria.MinQualityScore {
return false
}
if criteria.MaxDistanceKm > 0 && candidate.DistanceKm > criteria.MaxDistanceKm {
return false
}
return true
}
// rankCandidates sorts candidates by overall score and priority
func (e *Engine) rankCandidates(candidates []*Candidate) []*Candidate {
// Simple sort by overall score (descending)
for i := 0; i < len(candidates); i++ {
for j := i + 1; j < len(candidates); j++ {
if candidates[i].OverallScore < candidates[j].OverallScore {
candidates[i], candidates[j] = candidates[j], candidates[i]
}
}
}
return candidates
}
// calculatePriority calculates match priority
func (e *Engine) calculatePriority(overallScore, annualSavings float64) int {
score := overallScore * 0.7
economic := math.Min(annualSavings/10000, 1.0) * 0.3 // Cap at €10k
combined := (score + economic) * 100
switch {
case combined >= 90:
return 1 // High priority
case combined >= 70:
return 2 // Medium priority
case combined >= 50:
return 3 // Low priority
default:
return 4 // Very low priority
}
}