Skill 01 · Instrument Data To Allotrope
Subchapter 1.3
references/flattening_guide.mdMarkdown7 KBView on GitHub
Converting hierarchical ASM JSON to flat 2D tables for LIMS import, spreadsheet analysis, or data engineering pipelines.
ASM is semantically rich but hierarchical. Many systems need flat tables:
Each measurement becomes one row. Metadata is repeated per row.
The flattening intentionally omits top-level ASM metadata such as:
$asm.manifest (model version, schema URIs)This keeps the output focused on experimental data. If you need schema version tracking for compliance or audit purposes, consider storing the original ASM JSON alongside the flattened CSV, or modify the flattening script to include these fields.
ASM Hierarchy → Flat Column
─────────────────────────────────────────────────
device-system-document.
device-identifier → instrument_serial_number
model-number → instrument_model
measurement-aggregate-document.
analyst → analyst
measurement-time → measurement_datetime
measurement-document[].
sample-identifier → sample_id
viable-cell-density.value → viable_cell_density
viable-cell-density.unit → viable_cell_density_unit
viability.value → viability_percentUse snake_case with descriptive suffixes:
| ASM Field | Flat Column |
|---|---|
viable-cell-density | viable_cell_density |
.value | _value (or omit if obvious) |
.unit | _unit |
measurement-time | measurement_datetime |
{
"cell-counting-aggregate-document": {
"device-system-document": {
"device-identifier": "VCB001",
"model-number": "Vi-CELL BLU"
},
"cell-counting-document": [{
"measurement-aggregate-document": {
"analyst": "jsmith",
"measurement-time": "2024-01-15T10:30:00Z",
"measurement-document": [
{
"sample-identifier": "Sample_A",
"viable-cell-density": {"value": 2500000, "unit": "(cell/mL)"},
"viability": {"value": 95.2, "unit": "%"}
},
{
"sample-identifier": "Sample_B",
"viable-cell-density": {"value": 1800000, "unit": "(cell/mL)"},
"viability": {"value": 88.7, "unit": "%"}
}
]
}
}]
}
}sample_id,viable_cell_density,viable_cell_density_unit,viability_percent,analyst,measurement_datetime,instrument_serial_number,instrument_model
Sample_A,2500000,(cell/mL),95.2,jsmith,2024-01-15T10:30:00Z,VCB001,Vi-CELL BLU
Sample_B,1800000,(cell/mL),88.7,jsmith,2024-01-15T10:30:00Z,VCB001,Vi-CELL BLU{
"plate-reader-aggregate-document": {
"plate-reader-document": [{
"measurement-aggregate-document": {
"plate-identifier": "ELISA_001",
"measurement-document": [
{"well-location": "A1", "absorbance": {"value": 0.125, "unit": "mAU"}},
{"well-location": "A2", "absorbance": {"value": 0.892, "unit": "mAU"}},
{"well-location": "A3", "absorbance": {"value": 1.456, "unit": "mAU"}}
]
}
}]
}
}plate_id,well_position,absorbance,absorbance_unit
ELISA_001,A1,0.125,mAU
ELISA_001,A2,0.892,mAU
ELISA_001,A3,1.456,mAUData cubes (time series, spectra) need special handling:
Each point becomes a row:
sample_id,time_seconds,absorbance
Sample_A,0,0.100
Sample_A,60,0.125
Sample_A,120,0.150Measurements as columns:
sample_id,abs_0s,abs_60s,abs_120s
Sample_A,0.100,0.125,0.150Keep as array (some systems support this):
sample_id,absorbance_timeseries
Sample_A,"[0.100,0.125,0.150]"sample_id, viable_cell_density, viable_cell_density_unit, total_cell_count,
viability_percent, average_cell_diameter, average_cell_diameter_unit,
analyst, measurement_datetime, instrument_serial_numbersample_id, wavelength_nm, absorbance, pathlength_cm, concentration,
concentration_unit, a260_a280_ratio, a260_a230_ratio,
analyst, measurement_datetime, instrument_serial_numberplate_id, well_position, sample_type, sample_id, absorbance, absorbance_unit,
concentration, concentration_unit, dilution_factor, cv_percent,
analyst, measurement_datetime, instrument_serial_numbersample_id, target_name, well_position, ct_value, ct_mean, ct_sd,
quantity, quantity_unit, amplification_efficiency,
analyst, measurement_datetime, instrument_serial_numberimport json
import pandas as pd
def flatten_asm(asm_dict, technique="cell-counting"):
"""
Flatten ASM JSON to pandas DataFrame.
Args:
asm_dict: Parsed ASM JSON
technique: ASM technique type
Returns:
pandas DataFrame with one row per measurement
"""
rows = []
# Get aggregate document
agg_key = f"{technique}-aggregate-document"
agg_doc = asm_dict.get(agg_key, {})
# Extract device info
device = agg_doc.get("device-system-document", {})
device_info = {
"instrument_serial_number": device.get("device-identifier"),
"instrument_model": device.get("model-number")
}
# Get technique documents
doc_key = f"{technique}-document"
for doc in agg_doc.get(doc_key, []):
meas_agg = doc.get("measurement-aggregate-document", {})
# Extract common metadata
common = {
"analyst": meas_agg.get("analyst"),
"measurement_datetime": meas_agg.get("measurement-time"),
**device_info
}
# Extract each measurement
for meas in meas_agg.get("measurement-document", []):
row = {**common}
# Flatten measurement fields
for key, value in meas.items():
if isinstance(value, dict) and "value" in value:
# Value datum pattern
col = key.replace("-", "_")
row[col] = value["value"]
if "unit" in value:
row[f"{col}_unit"] = value["unit"]
else:
row[key.replace("-", "_")] = value
rows.append(row)
return pd.DataFrame(rows)
# Usage
with open("asm_output.json") as f:
asm = json.load(f)
df = flatten_asm(asm, "cell-counting")
df.to_csv("flattened_output.csv", index=False)When importing flattened data into a LIMS:
Source