{"schema_version":"1.0","audience":"automated agents and LLMs accessing this Zarr store","domain":"atmosphere","access":{"open":"xarray.open_zarr(store, group='<SITE><HEIGHT>', consolidated=True)   # e.g. 'CBW207' — one station, one intake height","combined_view":"co2 — the cross-station panel, every station at once; read panel_warning before opening it","available_views":["co2","ch4","n2o","co","rn","14co2"],"layout":"station × time_<species> panel — dims (station, time_<species>); latitude/longitude are 1-D coords on station","station_dim":"station","time_coord":"time_<species> — time_co2, time_ch4, time_co, time_n2o, time_rn, time_14co2, time_mto. There is NO plain 'time' array in any group or panel: ds.sel(time=...) raises. Slice with ds.sel(time_co2=slice('2023-01-01','2023-02-01')). In a /query CSV the column is time_co2 too.","lat_coord":"latitude","lon_coord":"longitude","bbox_filter":"select station whose latitude/longitude fall in range, then slice time","entity_groups":"top-level groups not starting with '_' are individual stations/sites/cruises; '_combined' holds cross-entity views","list_entities":"listing groups is a ZARR call, not an xarray one: zarr.open_group(store, mode='r').group_keys() returns the top-level group names. An xarray Dataset has no .group_keys() and no .groups — reaching for either raises AttributeError. List first, then read one with xarray.open_zarr(store, group=<id>).","entity_ids":"<SITE_CODE><INTAKE_HEIGHT_IN_M> — e.g. CBW207 (Cabauw, 207 m), HUN115, IZO29. One group per station AND height; tall towers have several","entity_views":["<SITE><HEIGHT>","meteo/<SITE><HEIGHT>"],"open_entity":"xarray.open_zarr(store, group='CBW207', consolidated=True)","list_entities_example":"import zarr; keys = list(zarr.open_group(store, mode='r').group_keys()); heights = [g for g in keys if g.startswith('CBW')]   # every intake height at one site. bind group_keys() with list(): it is a generator","meteorology":"ATC meteorology (ap, at, rh, ws, wd on a 'time_mto' axis, hourly, stamped at the START of the hour) lives under 'meteo/<SITE><HEIGHT>' and in the cross-station 'mto' panel — NOT in the gas station groups. Its sampling heights differ from the gas intake heights (Cabauw: gas 27/67/127/207 m, meteo 1.5/20/40/80/140/200 m), so a gas group and its meteo twin exist only where the heights coincide; they cross-link via the 'meteo_group' / 'gas_group' attrs","panel_warning":"the cross-station species panels ('co2', 'ch4', ...) hold every station at once and will exhaust memory in a small worker — open the station group unless you genuinely need all stations","entity_vs_combined":"for a single named entity open its own group; use the combined view only for cross-entity work. Do NOT locate a named entity by bounding box — match it by id","citation_location":"a station group carries citation, source_doi, station_landing_page and license in .attrs. The species panels (co2, ch4, ...) carry NO citation attr — there it is a per-station array: ds.citation.values (162 long, alongside source_doi and station_url)."},"conventions":{"metadata":"CF-1.12, ACDD-1.3 with a canonical cross-store attribute vocabulary","identity_attrs":["latitude","longitude","elevation","station_id","station_name","network","country","source_doi","citation","collection_doi","license","license_url"],"quality_control":"<gas>_qc_flag is a ONE-BYTE flag (dtype |S1 — bytes: decode, never str()). Keep b'U' (correct before manual QC, the NRT tail) or b'O' (correct after manual QC). b'N' and b'K' are the ATC's rejections and DO occur — 3.0 % of the co2 panel between them. The fill is the EMPTY byte string b'' (78 % of panel cells): that station simply has no observation at that timestamp, a GAP, not a rejection — its value is NaN. A non-empty flag does not guarantee a finite value. From /query the same flag arrives as a plain str. Server-side apply_qc=true applies exactly this rule.","reading_string_arrays":"take .values before iterating: for a coord like 'cruise' or a byte-flag array, iterating the DataArray yields 0-d DataArrays, so .startswith()/.decode() fail and a boolean mask can come out silently all-False. Use arr.values (or arr[:].values) first."},"discovery":{"variable_catalog":"variable_catalog.json (sibling of the store directory)","api":"ICOS zarr proxy — GET /variables, /variables/{id}, /query?id=..&station=..&cruise=..&lat_min=..&lat_max=..&lon_min=..&lon_max=..&start=..&end=..&apply_qc=true ; station/cruise ids are CASE-SENSITIVE, end is EXCLUSIVE, apply_qc DROPS rows the store flags bad; for WOCE stores it keeps flag 2 AND the -1 'no source flag' rows, whose values may be NaN — filter *_QC == 2 yourself (unknown/unusable filters answer 400 naming the problem); machine-readable contract at /openapi.json","croissant":"icos-obspack.croissant.jsonld (sibling of the store directory)","croissant_url":"ICOS zarr proxy: GET /icos-obspack.zarr/croissant — same Croissant, also linked (with @id) from the proxy-root schema.org DataCatalog when fetched with Accept: application/ld+json","collection_doi":""},"docs":["CONVENTIONS.md","CONSUMER_MIGRATION.md"]}