Spaces:
Running
Running
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
} |