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)
417 lines
12 KiB
Go
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
|
|
}
|
|
}
|