Spaces:
Sleeping
Sleeping
| // 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 | |
| } |