File size: 5,416 Bytes
61fd4d1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
// Package env defines fault events and injection logic for GridMind-RL.
// Faults are rare but high-impact events that force agents to adapt their strategies.
// They target the Wild Card + World Modeling judging themes.
package env

import "math/rand"

// FaultType identifies the kind of fault event.
type FaultType string

const (
	FaultChillerFailure FaultType = "chiller_failure" // HVAC efficiency drops
	FaultGridOutage     FaultType = "grid_outage"      // Price spike + max grid stress
	FaultSensorFault    FaultType = "sensor_fault"     // Observation noise on temperature
	FaultTariffSpike    FaultType = "tariff_spike"     // Flash electricity price surge
)

// FaultEvent describes a single active fault during an episode.
type FaultEvent struct {
	Type        FaultType `json:"type"`
	StartStep   int       `json:"start_step"`
	EndStep     int       `json:"end_step"` // exclusive
	Severity    float64   `json:"severity"` // 0.0–1.0
	Description string    `json:"description"`
}

// IsActive returns true if the fault is active at the given step.
func (f *FaultEvent) IsActive(step int) bool {
	return step >= f.StartStep && step < f.EndStep
}

// FaultSchedule holds all faults scheduled for an episode.
type FaultSchedule struct {
	Events []FaultEvent `json:"events"`
}

// ActiveAt returns all fault events active at the given step.
func (fs *FaultSchedule) ActiveAt(step int) []FaultEvent {
	var active []FaultEvent
	for _, e := range fs.Events {
		if e.IsActive(step) {
			active = append(active, e)
		}
	}
	return active
}

// GenerateFaultSchedule creates a randomised schedule of fault events for an episode.
// Probability and severity are scaled by difficulty level.
// Guarantees at least one fault fires in hard mode.
func GenerateFaultSchedule(rng *rand.Rand, difficulty string) *FaultSchedule {
	schedule := &FaultSchedule{}

	// Base probabilities per fault type - increased for hard mode
	type faultSpec struct {
		fType    FaultType
		probEasy float64
		probMed  float64
		probHard float64
		minDur   int // steps
		maxDur   int
	}

	specs := []faultSpec{
		{FaultChillerFailure, 0.05, 0.15, 0.45, 8, 24},
		{FaultGridOutage, 0.05, 0.10, 0.45, 4, 12},
		{FaultSensorFault, 0.08, 0.15, 0.45, 6, 20},
		{FaultTariffSpike, 0.10, 0.20, 0.50, 1, 4},
	}

	for _, spec := range specs {
		prob := spec.probEasy
		switch difficulty {
		case "medium":
			prob = spec.probMed
		case "hard":
			prob = spec.probHard
		}

		if rng.Float64() > prob {
			continue // no fault of this type this episode
		}

		// Random start time (avoid very first and last 10 steps)
		maxStart := EpisodeSteps - spec.maxDur - 10
		if maxStart < 10 {
			maxStart = 10
		}
		start := 10 + rng.Intn(maxStart)
		dur := spec.minDur + rng.Intn(spec.maxDur-spec.minDur+1)
		end := start + dur
		if end > EpisodeSteps {
			end = EpisodeSteps
		}
		severity := 0.4 + rng.Float64()*0.6 // 0.4–1.0

		event := FaultEvent{
			Type:      spec.fType,
			StartStep: start,
			EndStep:   end,
			Severity:  severity,
		}

		switch spec.fType {
		case FaultChillerFailure:
			event.Description = "⚠️ Chiller unit failure — HVAC operating at reduced capacity."
		case FaultGridOutage:
			event.Description = "🔴 Grid brownout — extreme price spike and critical stress signal."
		case FaultSensorFault:
			event.Description = "⚡ Temperature sensor malfunction — indoor temperature readings unreliable."
		case FaultTariffSpike:
			event.Description = "💸 Emergency tariff spike — electricity price has surged. Minimize consumption immediately."
		}

		schedule.Events = append(schedule.Events, event)
	}

	// Force at least one fault in hard mode if schedule is empty
	if len(schedule.Events) == 0 && difficulty == "hard" {
		schedule.Events = append(schedule.Events, FaultEvent{
			Type:        FaultTariffSpike,
			StartStep:   20,
			EndStep:     23,
			Severity:    0.6,
			Description: "Unexpected tariff spike — immediate load response required",
		})
	}

	return schedule
}

// ApplyFaults modifies environment signals based on active faults for the current step.
// It returns a list of active fault descriptions for the observation.
func ApplyFaults(b *BuildingState, schedule *FaultSchedule, step int, rng *rand.Rand) []string {
	if schedule == nil {
		return nil
	}
	active := schedule.ActiveAt(step)
	if len(active) == 0 {
		// Reset noise when no fault active
		b.TempObservationNoise = 0.0
		return nil
	}

	descriptions := make([]string, 0, len(active))
	for _, fault := range active {
		switch fault.Type {
		case FaultChillerFailure:
			// Reduce effective HVAC power — the building state's max power is scaled down
			// The physics engine uses MaxHVACPower; we reduce it proportionally to severity.
			b.MaxHVACPower = MaxHVACPowerKW * (1.0 - fault.Severity*0.8)

		case FaultGridOutage:
			// Force maximum grid stress and multiply the price to simulate outage conditions
			b.GridStressSignal = 1.0
			b.CurrentPrice = b.CurrentPrice * (1.0 + fault.Severity*3.0)

		case FaultSensorFault:
			// Add noise to the indoor temperature reading (observation only, not physics)
			// This affects what the agent sees but not the actual physics
			b.TempObservationNoise = (rng.Float64()*2 - 1) * 5.0 * fault.Severity

		case FaultTariffSpike:
			b.CurrentPrice = b.CurrentPrice * (1.0 + fault.Severity*4.0)
		}

		descriptions = append(descriptions, fault.Description)
	}
	return descriptions
}