Flat and Entity Interfaces
CityLearn supports two I/O contracts: flat for classic Gymnasium-style RL workflows and entity for ORL, GraphRL, Transformers and dynamic topology.
Quick Comparison
Topic |
Flat |
Entity |
|---|---|---|
Observation |
List of vectors per building or one central vector. |
Dict with entity tables and relational edges. |
Action |
List/array ordered by |
Dict with action tables or an ID-keyed map. |
Best for |
RBC, MARL, wrappers, SB3-style workflows. |
GNNs, transformers, dynamic input/output networks. |
Dynamic topology |
Not supported. |
Supported. |
Stable IDs |
Implicit through vector order. |
Explicit in |
EV/charger relation |
Expanded flat names. |
Separate tables plus |
Flat Mode
env = CityLearnEnv(schema, interface="flat")
With central_agent=False, observations are one vector per building:
observations = [
[building_1_feature_1, building_1_feature_2],
[building_2_feature_1, building_2_feature_2],
]
With central_agent=True, observations are concatenated into a single vector:
observations = [[all_active_features_for_all_buildings]]
shared_observations are included once in the central vector to avoid repeated weather, pricing and calendar features.
Flat actions follow the same convention:
actions = [
[building_1_action_1, building_1_action_2],
[building_2_action_1, building_2_action_2],
]
The exact order is available through:
env.observation_names
env.action_names
Entity Mode
env = CityLearnEnv(schema, interface="entity")
Dynamic topology requires entity mode:
env = CityLearnEnv(schema, interface="entity", topology_mode="dynamic")
Entity observations have this structure:
{
"tables": {
"district": np.ndarray,
"building": np.ndarray,
"charger": np.ndarray,
"ev": np.ndarray,
"storage": np.ndarray,
"pv": np.ndarray,
"deferrable_appliance": np.ndarray
},
"edges": {
"district_to_building": np.ndarray,
"building_to_charger": np.ndarray,
"building_to_storage": np.ndarray,
"building_to_pv": np.ndarray,
"building_to_deferrable_appliance": np.ndarray,
"charger_to_ev_connected": np.ndarray,
"charger_to_ev_connected_mask": np.ndarray,
"charger_to_ev_incoming": np.ndarray,
"charger_to_ev_incoming_mask": np.ndarray
},
"meta": {
"time_step": int,
"endogenous_time_step": int,
"spec_version": "entity_v1",
"topology_version": int,
"runtime_status": {
"version": "runtime_status_v1",
"emits_health_state": false,
"active_events": [],
"asset_connections": [],
"asset_availability": [],
"sensor_channels": [],
"actuator_channels": [],
"communication_links": [],
"value_quality": []
}
}
}
runtime_status_v1 reports simulator facts, not an RL-policy health
classification. fault_mode is preserved as the original cause. In
particular, a stuck event is not automatically labelled stale: a consumer
must derive that state from freshness, duration, semantic type and criticality.
Asset connection, asset availability, sensor-channel state, actuator-channel state and community/cloud communication state are independent parts of the contract. A normally disconnected EV is not a failed charger.
entity_specs
env.entity_specs is the machine-readable schema for tables, columns, IDs, units, bundles and edges:
specs = env.entity_specs
building_features = specs["tables"]["building"]["features"]
charger_ids = specs["tables"]["charger"]["ids"]
charger_units = specs["tables"]["charger"]["units"]
Field |
Contents |
|---|---|
|
Stable canonical row IDs. |
|
Column names. |
|
Inferred units. |
|
Unit, bundle and legacy flag per feature. |
|
Action table IDs, columns and units. |
|
Source/target table metadata. |
|
Active IDs, lifecycle and topology version. |
|
Status vocabulary, sparse defaults and explicit no-health-state boundary. |
|
Requested/post-channel/limited/applied action stages. |
Temporal Semantics
Field |
Meaning |
|---|---|
Exogenous observations |
Read at timestep |
Endogenous observations |
Read at settled |
Topology events |
Events at |
Entity Bundles
Bundle |
Default |
Tables |
Purpose |
|---|---|---|---|
|
always on |
charger, storage, deferrable |
Essential static and service features. |
|
off |
building, charger, ev, storage, pv |
Power, step energy, efficiency, derived SOC and PV. |
|
off |
district |
Aggregates, headroom, counts and topology version. |
|
off |
district |
Forecasts already present in the dataset. |
|
off |
district, building |
Short lags and rolling means. |
Entity Actions
Recommended table payload:
actions = {
"tables": {
"building": building_action_array,
"charger": charger_action_array,
"deferrable_appliance": deferrable_action_array
}
}
ID-keyed overrides are also supported:
actions = {
"map": {
"building:Building_1": {"electrical_storage": 0.2},
"charger:Building_1:AC001": {"electric_vehicle_storage": 0.5},
"deferrable_appliance:Building_1:washer_1": {"start": 1.0}
}
}
Use prefixed IDs for robust GraphRL and Transformer integrations.
After an entity-mode step, info["entity_action_execution"] contains stable-ID
entries for the requested command, the value after channel perturbations, the
equipment-limited value and the physically applied power where observable.
Unavailable quantities are None. info["topology_events_applied"] identifies
events applied before the returned observation.
Dynamic Topology Guidance
For every dynamic-topology reset(), the environment first restores the member pool and structural
asset composition loaded from the schema, then applies any events scheduled at time step 0.
Added or removed chargers, deferrable appliances, PV systems, electrical storage and runtime-cloned
members therefore do not leak into the next episode. Reusing one environment across training episodes
has the same topology event timeline as recreating the environment for each episode.
Element |
Agent guidance |
|---|---|
|
Re-read |
EV masks |
Use |
Running statistics |
Maintain stats per feature, not per fixed row. |
Removed assets |
Ignore IDs that disappear from |
Added assets |
Initialize model memory/hidden state for new IDs. |