
Instrument Data To Allotrope
- 1.8k installs
- 23.3k repo stars
- Updated August 5, 2026
- anthropics/knowledge-work-plugins
Convert laboratory instrument output files (PDF, CSV, Excel, TXT) to Allotrope Simple Model (ASM) JSON format or flattened 2D CSV. Use this skill when scientists need to standardize instrument data fo
About
The instrument data to allotrope skill Convert laboratory instrument output files (PDF, CSV, Excel, TXT) to Allotrope Simple Model (ASM) JSON format or flattened 2D CSV. Use this skill when scientists need to standardize instrument data for LIMS systems, data lakes, or downstream analysis. Supports auto-detection of instrument types. Outputs include full ASM JSON, flattened CSV for easy import, and exportable Python code for data engineers. Common triggers include converting instrument files, standardizing lab data, preparing data for upload to LIMS/ELN systems, or generating parser code for production pipelines. Documentation covers workflows, commands, and guardrails agents should follow when users invoke this capability. Key documented areas include **Detect instrument type** from file contents (auto-detect or user-specified); **Parse file** using allotropy library (native) or flexible fallback parser; **Generate outputs**:; ASM JSON (full semantic structure). Reference commands include pip install allotropy pandas openpyxl pdfplumber --break-system-packages; from allotropy.parser_factory import Vendor. Use when developers or agents need structured guidance for instrument data.
- **Detect instrument type** from file contents (auto-detect or user-specified)
- **Parse file** using allotropy library (native) or flexible fallback parser
- **Generate outputs**:
- ASM JSON (full semantic structure)
- Flattened CSV (2D tabular format)
Instrument Data To Allotrope by the numbers
- 1,780 all-time installs (skills.sh)
- +78 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #101 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
instrument-data-to-allotrope capabilities & compatibility
- Capabilities
- **detect instrument type** from file contents (a · **parse file** using allotropy library (native) · **generate outputs**: · asm json (full semantic structure) · flattened csv (2d tabular format)
- Use cases
- planning
What instrument-data-to-allotrope says it does
**Detect instrument type** from file contents (auto-detect or user-specified)
npx skills add https://github.com/anthropics/knowledge-work-plugins --skill instrument-data-to-allotropeAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.8k |
|---|---|
| repo stars | ★ 23.3k |
| Security audit | 2 / 3 scanners passed |
| Last updated | August 5, 2026 |
| Repository | anthropics/knowledge-work-plugins ↗ |
How do I handle instrument data to allotrope tasks with agent guidance?
Convert laboratory instrument output files (PDF, CSV, Excel, TXT) to Allotrope Simple Model (ASM) JSON format or flattened 2D CSV. Use this skill when scientists need to standardize instrument data fo
Who is it for?
Teams needing documented instrument data to allotrope workflows.
Skip if: Generic advice without reading bundled docs.
When should I use this skill?
Convert laboratory instrument output files (PDF, CSV, Excel, TXT) to Allotrope Simple Model (ASM) JSON format or flattened 2D CSV. Use this skill when scientists need to standardize instrument data fo
What you get
Structured workflow from instrument data to allotrope documentation applied to the user request.
- Allotrope semantic data records
- Instrument-to-ontology mapping
Files
Instrument Data to Allotrope Converter
Convert instrument files into standardized Allotrope Simple Model (ASM) format for LIMS upload, data lakes, or handoff to data engineering teams.
Note: This is an Example Skill
>
This skill demonstrates how skills can support your data engineering tasks—automating schema transformations, parsing instrument outputs, and generating production-ready code.
>
To customize for your organization:
- Modify the references/ files to include your company's specific schemas or ontology mappings- Use an MCP server to connect to systems that define your schemas (e.g., your LIMS, data catalog, or schema registry)
- Extend the scripts/ to handle proprietary instrument formats or internal data standards>
This pattern can be adapted for any data transformation workflow where you need to convert between formats or validate against organizational standards.
Workflow Overview
1. Detect instrument type from file contents (auto-detect or user-specified) 2. Parse file using allotropy library (native) or flexible fallback parser 3. Generate outputs:
- ASM JSON (full semantic structure)
- Flattened CSV (2D tabular format)
- Python parser code (for data engineer handoff)
4. Deliver files with summary and usage instructions
When Uncertain: If you're unsure how to map a field to ASM (e.g., is this raw data or calculated? device setting or environmental condition?), ask the user for clarification. Refer to references/field_classification_guide.md for guidance, but when ambiguity remains, confirm with the user rather than guessing.Quick Start
# Install requirements first
pip install allotropy pandas openpyxl pdfplumber --break-system-packages
# Core conversion
from allotropy.parser_factory import Vendor
from allotropy.to_allotrope import allotrope_from_file
# Convert with allotropy
asm = allotrope_from_file("instrument_data.csv", Vendor.BECKMAN_VI_CELL_BLU)Output Format Selection
ASM JSON (default) - Full semantic structure with ontology URIs
- Best for: LIMS systems expecting ASM, data lakes, long-term archival
- Validates against Allotrope schemas
Flattened CSV - 2D tabular representation
- Best for: Quick analysis, Excel users, systems without JSON support
- Each measurement becomes one row with metadata repeated
Both - Generate both formats for maximum flexibility
Calculated Data Handling
IMPORTANT: Separate raw measurements from calculated/derived values.
- Raw data →
measurement-document(direct instrument readings) - Calculated data →
calculated-data-aggregate-document(derived values)
Calculated values MUST include traceability via data-source-aggregate-document:
"calculated-data-aggregate-document": {
"calculated-data-document": [{
"calculated-data-identifier": "SAMPLE_B1_DIN_001",
"calculated-data-name": "DNA integrity number",
"calculated-result": {"value": 9.5, "unit": "(unitless)"},
"data-source-aggregate-document": {
"data-source-document": [{
"data-source-identifier": "SAMPLE_B1_MEASUREMENT",
"data-source-feature": "electrophoresis trace"
}]
}
}]
}Common calculated fields by instrument type:
| Instrument | Calculated Fields |
|---|---|
| Cell counter | Viability %, cell density dilution-adjusted values |
| Spectrophotometer | Concentration (from absorbance), 260/280 ratio |
| Plate reader | Concentrations from standard curve, %CV |
| Electrophoresis | DIN/RIN, region concentrations, average sizes |
| qPCR | Relative quantities, fold change |
See references/field_classification_guide.md for detailed guidance on raw vs. calculated classification.
Validation
Always validate ASM output before delivering to the user:
python scripts/validate_asm.py output.json
python scripts/validate_asm.py output.json --reference known_good.json # Compare to reference
python scripts/validate_asm.py output.json --strict # Treat warnings as errorsValidation Rules:
- Based on Allotrope ASM specification (December 2024)
- Last updated: 2026-01-07
- Source: https://gitlab.com/allotrope-public/asm
Soft Validation Approach: Unknown techniques, units, or sample roles generate warnings (not errors) to allow for forward compatibility. If Allotrope adds new values after December 2024, the validator won't block them—it will flag them for manual verification. Use --strict mode to treat warnings as errors if you need stricter validation.
What it checks:
- Correct technique selection (e.g., multi-analyte profiling vs plate reader)
- Field naming conventions (space-separated, not hyphenated)
- Calculated data has traceability (
data-source-aggregate-document) - Unique identifiers exist for measurements and calculated values
- Required metadata present
- Valid units and sample roles (with soft validation for unknown values)
Supported Instruments
See references/supported_instruments.md for complete list. Key instruments:
| Category | Instruments |
|---|---|
| Cell Counting | Vi-CELL BLU, Vi-CELL XR, NucleoCounter |
| Spectrophotometry | NanoDrop One/Eight/8000, Lunatic |
| Plate Readers | SoftMax Pro, EnVision, Gen5, CLARIOstar |
| ELISA | SoftMax Pro, BMG MARS, MSD Workbench |
| qPCR | QuantStudio, Bio-Rad CFX |
| Chromatography | Empower, Chromeleon |
Detection & Parsing Strategy
Tier 1: Native allotropy parsing (PREFERRED)
Always try allotropy first. Check available vendors directly:
from allotropy.parser_factory import Vendor
# List all supported vendors
for v in Vendor:
print(f"{v.name}")
# Common vendors:
# AGILENT_TAPESTATION_ANALYSIS (for TapeStation XML)
# BECKMAN_VI_CELL_BLU
# THERMO_FISHER_NANODROP_EIGHT
# MOLDEV_SOFTMAX_PRO
# APPBIO_QUANTSTUDIO
# ... many moreWhen the user provides a file, check if allotropy supports it before falling back to manual parsing. The scripts/convert_to_asm.py auto-detection only covers a subset of allotropy vendors.
Tier 2: Flexible fallback parsing
Only use if allotropy doesn't support the instrument. This fallback:
- Does NOT generate
calculated-data-aggregate-document - Does NOT include full traceability
- Produces simplified ASM structure
Use flexible parser with:
- Column name fuzzy matching
- Unit extraction from headers
- Metadata extraction from file structure
Tier 3: PDF extraction
For PDF-only files, extract tables using pdfplumber, then apply Tier 2 parsing.
Pre-Parsing Checklist
Before writing a custom parser, ALWAYS:
1. Check if allotropy supports it - Use native parser if available 2. Find a reference ASM file - Check references/examples/ or ask user 3. Review instrument-specific guide - Check references/instrument_guides/ 4. Validate against reference - Run validate_asm.py --reference <file>
Common Mistakes to Avoid
| Mistake | Correct Approach |
|---|---|
| Manifest as object | Use URL string |
| Lowercase detection types | Use "Absorbance" not "absorbance" |
| "emission wavelength setting" | Use "detector wavelength setting" for emission |
| All measurements in one document | Group by well/sample location |
| Missing procedure metadata | Extract ALL device settings per measurement |
Code Export for Data Engineers
Generate standalone Python scripts that scientists can hand off:
# Export parser code
python scripts/export_parser.py --input "data.csv" --vendor "VI_CELL_BLU" --output "parser_script.py"The exported script:
- Has no external dependencies beyond pandas/allotropy
- Includes inline documentation
- Can run in Jupyter notebooks
- Is production-ready for data pipelines
File Structure
instrument-data-to-allotrope/
├── SKILL.md # This file
├── scripts/
│ ├── convert_to_asm.py # Main conversion script
│ ├── flatten_asm.py # ASM → 2D CSV conversion
│ ├── export_parser.py # Generate standalone parser code
│ └── validate_asm.py # Validate ASM output quality
└── references/
├── supported_instruments.md # Full instrument list with Vendor enums
├── asm_schema_overview.md # ASM structure reference
├── field_classification_guide.md # Where to put different field types
└── flattening_guide.md # How flattening worksUsage Examples
Example 1: Vi-CELL BLU file
User: "Convert this cell counting data to Allotrope format"
[uploads viCell_Results.xlsx]
Claude:
1. Detects Vi-CELL BLU (95% confidence)
2. Converts using allotropy native parser
3. Outputs:
- viCell_Results_asm.json (full ASM)
- viCell_Results_flat.csv (2D format)
- viCell_parser.py (exportable code)Example 2: Request for code handoff
User: "I need to give our data engineer code to parse NanoDrop files"
Claude:
1. Generates self-contained Python script
2. Includes sample input/output
3. Documents all assumptions
4. Provides Jupyter notebook versionExample 3: LIMS-ready flattened output
User: "Convert this ELISA data to a CSV I can upload to our LIMS"
Claude:
1. Parses plate reader data
2. Generates flattened CSV with columns:
- sample_identifier, well_position, measurement_value, measurement_unit
- instrument_serial_number, analysis_datetime, assay_type
3. Validates against common LIMS import requirementsImplementation Notes
Installing allotropy
pip install allotropy --break-system-packagesHandling parse failures
If allotropy native parsing fails: 1. Log the error for debugging 2. Fall back to flexible parser 3. Report reduced metadata completeness to user 4. Suggest exporting different format from instrument
ASM Schema Validation
Validate output against Allotrope schemas when available:
import jsonschema
# Schema URLs in references/asm_schema_overview.mdApache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
ASM Schema Overview
The Allotrope Simple Model (ASM) is a JSON-based standard for representing laboratory instrument data with semantic consistency.
Core Concepts
Structure
ASM uses a hierarchical document structure:
- Manifest - Links to ontologies and schemas
- Data - The actual measurement data organized by technique
Key Components
{
"$asm.manifest": {
"vocabulary": ["http://purl.allotrope.org/voc/afo/REC/2023/09/"],
"contexts": ["http://purl.allotrope.org/json-ld/afo-context-REC-2023-09.jsonld"]
},
"<technique>-aggregate-document": {
"device-system-document": { ... },
"<technique>-document": [
{
"measurement-aggregate-document": {
"measurement-document": [ ... ]
}
}
]
}
}Required Metadata Documents
data system document
Every ASM output MUST include this document with:
ASM file identifier: Output filenamedata system instance identifier: System ID or "N/A"file name: Source input filenameUNC path: Path to source fileASM converter name: Parser identifier (e.g., "allotropy_beckman_coulter_biomek")ASM converter version: Version stringsoftware name: Instrument software that generated the source file
device system document
Every ASM output MUST include this document with:
equipment serial number: Main instrument serialproduct manufacturer: Vendor namedevice document: Array of sub-components (probes, pods, etc.)device type: Standardized type (e.g., "liquid handler probe head")device identifier: Logical name (e.g., "Pod1", not serial number)equipment serial number: Component serialproduct manufacturer: Component vendor
Available ASM Techniques
The official ASM repository includes 65 technique schemas:
absorbance, automated-reactors, balance, bga, binding-affinity, bulk-density,
cell-counting, cell-culture-analyzer, chromatography, code-reader, conductance,
conductivity, disintegration, dsc, dvs, electronic-lab-notebook,
electronic-spectrometry, electrophoresis, flow-cytometry, fluorescence,
foam-height, foam-qualification, fplc, ftir, gas-chromatography, gc-ms, gloss,
hot-tack, impedance, lc-ms, light-obscuration, liquid-chromatography,
loss-on-drying, luminescence, mass-spectrometry, metabolite-analyzer,
multi-analyte-profiling, nephelometry, nmr, optical-imaging, optical-microscopy,
osmolality, oven-kf, pcr, ph, plate-reader, pressure-monitoring, psd, pumping,
raman, rheometry, sem, solution-analyzer, specific-rotation, spectrophotometry,
stirring, surface-area-analysis, tablet-hardness, temperature-monitoring,
tensile-test, thermogravimetric-analysis, titration, ultraviolet-absorbance,
x-ray-powder-diffractionSee: https://gitlab.com/allotrope-public/asm/-/tree/main/json-schemas/adm
Common ASM Schemas by Technique
Below are details for frequently-used techniques:
Cell Counting
Schema: cell-counting/REC/2024/09/cell-counting.schema.json
Key fields:
viable-cell-density(cells/mL)viability(percentage)total-cell-countdead-cell-countcell-diameter-distribution-datum
Spectrophotometry (UV-Vis)
Schema: spectrophotometry/REC/2024/06/spectrophotometry.schema.json
Key fields:
absorbance(dimensionless)wavelength(nm)transmittance(percentage)pathlength(cm)concentrationwith units
Plate Reader
Schema: plate-reader/REC/2024/06/plate-reader.schema.json
Key fields:
absorbancefluorescenceluminescencewell-location(A1-H12)plate-identifier
qPCR
Schema: pcr/REC/2024/06/pcr.schema.json
Key fields:
cycle-threshold-resultamplification-efficiencymelt-curve-datumtarget-DNA-description
Chromatography
Schema: liquid-chromatography/REC/2023/09/liquid-chromatography.schema.json
Key fields:
retention-time(minutes)peak-areapeak-heightpeak-widthchromatogram-data-cube
Data Patterns
Value Datum
Simple value with unit:
{
"value": 1.5,
"unit": "mL"
}Aggregate Datum
Collection of related values:
{
"measurement-aggregate-document": {
"measurement-document": [
{ "viable-cell-density": {"value": 2.5e6, "unit": "(cell/mL)"} },
{ "viability": {"value": 95.2, "unit": "%"} }
]
}
}Data Cube
Multi-dimensional array data:
{
"cube-structure": {
"dimensions": [{"@componentDatatype": "double", "concept": "elapsed time"}],
"measures": [{"@componentDatatype": "double", "concept": "absorbance"}]
},
"data": {
"dimensions": [[0, 1, 2, 3, 4]],
"measures": [[0.1, 0.2, 0.3, 0.4, 0.5]]
}
}Validation
Validate ASM output against official schemas:
import json
import jsonschema
from urllib.request import urlopen
# Load ASM output
with open("output.json") as f:
asm = json.load(f)
# Get schema URL from manifest
schema_url = asm.get("$asm.manifest", {}).get("$ref")
# Validate (simplified - real validation more complex)
# Note: Full validation requires resolving $ref referencesSchema Repository
Official schemas: https://gitlab.com/allotrope-public/asm/-/tree/main/json-schemas/adm
Schema structure:
json-schemas/adm/
├── cell-counting/
│ └── REC/2024/09/
│ └── cell-counting.schema.json
├── spectrophotometry/
│ └── REC/2024/06/
│ └── spectrophotometry.schema.json
├── plate-reader/
│ └── REC/2024/06/
│ └── plate-reader.schema.json
└── ...Common Issues
Missing Fields
Not all instrument exports contain all ASM fields. Report completeness:
def report_completeness(asm, expected_fields):
found = set(extract_all_fields(asm))
missing = expected_fields - found
return len(found) / len(expected_fields) * 100Unit Variations
Instruments may use different unit formats. The allotropy library normalizes these:
- "cells/mL" → "(cell/mL)"
- "%" → "%"
- "nm" → "nm"
Date Formats
ASM uses ISO 8601: 2024-01-15T10:30:00Z
Field Classification Guide
This guide helps classify instrument data fields into the correct ASM document locations. Use this when mapping raw instrument output to Allotrope Simple Model structure.
ASM Document Hierarchy
<technique>-aggregate-document
├── device-system-document # Instrument hardware info
├── data-system-document # Software/conversion info
├── <technique>-document[] # Per-run/sequence data
│ ├── analyst # Who performed the analysis
│ ├── measurement-aggregate-document
│ │ ├── measurement-time
│ │ ├── measurement-document[] # Individual measurements
│ │ │ ├── sample-document
│ │ │ ├── device-control-aggregate-document
│ │ │ └── [measurement fields]
│ │ └── [aggregate-level metadata]
│ ├── processed-data-aggregate-document
│ │ └── processed-data-document[]
│ │ ├── data-processing-document
│ │ └── [processed results]
│ └── calculated-data-aggregate-document
│ └── calculated-data-document[]Field Classification Categories
1. Device/Instrument Information → device-system-document
Hardware and firmware details about the physical instrument.
| Field Type | ASM Field | Examples |
|---|---|---|
| Instrument name | model-number | "Vi-CELL BLU", "NanoDrop One" |
| Serial number | equipment-serial-number | "VCB-12345", "SN001234" |
| Manufacturer | product-manufacturer | "Beckman Coulter", "Thermo Fisher" |
| Firmware version | firmware-version | "v2.1.3" |
| Device ID | device-identifier | "Instrument_01" |
| Brand | brand-name | "Beckman Coulter" |
Rule: If the value describes the physical instrument and doesn't change between runs, it goes in device-system-document.
---
2. Software/Data System Information → data-system-document
Information about software used for acquisition, analysis, or conversion.
| Field Type | ASM Field | Examples |
|---|---|---|
| Software name | software-name | "Chromeleon", "Gen5" |
| Software version | software-version | "7.3.2" |
| File name | file-name | "experiment_001.xlsx" |
| File path | file-identifier | "/data/runs/2024-01-15/" |
| Database ID | ASM-converter-name | "allotropy v0.1.55" |
Rule: If the value describes software, file metadata, or data provenance, it goes in data-system-document.
---
3. Sample Information → sample-document
Metadata about the biological/chemical sample being analyzed.
| Field Type | ASM Field | Examples |
|---|---|---|
| Sample ID | sample-identifier | "Sample_A", "LIMS-001234" |
| Sample name | written-name | "CHO Cell Culture Day 5" |
| Sample type/role | sample-role-type | "unknown sample role", "control sample role" |
| Batch ID | batch-identifier | "Batch-2024-001" |
| Description | description | "Protein expression sample" |
| Well position | location-identifier | "A1", "B3" |
Rule: If the value identifies or describes what was measured (not how), it goes in sample-document.
---
4. Device Control Settings → device-control-aggregate-document
Instrument settings and parameters used during measurement.
| Field Type | ASM Field | Examples |
|---|---|---|
| Injection volume | sample-volume-setting | 10 µL |
| Wavelength | detector-wavelength-setting | 254 nm |
| Temperature | compartment-temperature | 37°C |
| Flow rate | flow-rate | 1.0 mL/min |
| Exposure time | exposure-duration-setting | 500 ms |
| Detector gain | detector-gain-setting | 1.5 |
| Illumination | illumination-setting | 80% |
Rule: If the value is a configurable instrument parameter that affects measurement, it goes in device-control-aggregate-document.
---
5. Environmental Conditions → device-control-document or technique-specific
Ambient or controlled environmental parameters during measurement.
| Field Type | ASM Field | Examples |
|---|---|---|
| Ambient temperature | ambient-temperature | 22.5°C |
| Humidity | ambient-relative-humidity | 45% |
| Column temperature | compartment-temperature | 30°C |
| Sample temperature | sample-temperature | 4°C |
| Electrophoresis temp | (technique-specific) | 26.4°C |
Rule: Environmental conditions that affect measurement quality go with device control or in technique-specific locations.
---
6. Raw Measurement Data → measurement-document
Direct instrument readings - the "ground truth" data.
| Field Type | ASM Field | Examples |
|---|---|---|
| Absorbance | absorbance | 0.523 AU |
| Fluorescence | fluorescence | 12500 RFU |
| Cell count | total-cell-count | 2.5e6 cells |
| Peak area | peak-area | 1234.5 mAU·min |
| Retention time | retention-time | 5.67 min |
| Ct value | cycle-threshold-result | 24.5 |
| Concentration (measured) | mass-concentration | 1.5 mg/mL |
Rule: If the value is a direct instrument reading that wasn't computed from other values in this analysis, it goes in measurement-document.
---
7. Calculated/Derived Data → calculated-data-aggregate-document
Values computed from raw measurements.
| Field Type | ASM Field | Examples |
|---|---|---|
| Viability % | calculated-result | 95.2% |
| Concentration (from std curve) | calculated-result | 125 ng/µL |
| Ratio (260/280) | calculated-result | 1.89 |
| Relative quantity | calculated-result | 2.5x |
| % Recovery | calculated-result | 98.7% |
| CV% | calculated-result | 2.3% |
Calculated data document structure:
{
"calculated-data-name": "viability",
"calculated-result": {"value": 95.2, "unit": "%"},
"calculation-description": "viable cells / total cells * 100"
}Rule: If the value was computed from other measurements in this analysis, it goes in calculated-data-aggregate-document. Include calculation-description when possible.
---
8. Processed/Analyzed Data → processed-data-aggregate-document
Results from data processing algorithms (peak integration, cell classification, etc.).
| Field Type | ASM Field | Examples |
|---|---|---|
| Peak list | peak-list | Integrated peak results |
| Cell size distribution | cell-diameter-distribution | Histogram data |
| Baseline-corrected data | (in processed-data-document) | Corrected spectra |
| Fitted curve | (in processed-data-document) | Standard curve fit |
Associated `data-processing-document`:
{
"cell-type-processing-method": "trypan blue exclusion",
"cell-density-dilution-factor": {"value": 2, "unit": "(unitless)"},
"minimum-cell-diameter-setting": {"value": 5, "unit": "µm"},
"maximum-cell-diameter-setting": {"value": 50, "unit": "µm"}
}Rule: If the value results from an algorithm or processing method applied to raw data, it goes in processed-data-aggregate-document with its processing parameters in data-processing-document.
---
9. Timing/Timestamps → Various locations
| Timestamp Type | Location | ASM Field |
|---|---|---|
| Measurement time | measurement-document | measurement-time |
| Run start time | analysis-sequence-document | analysis-sequence-start-time |
| Run end time | analysis-sequence-document | analysis-sequence-end-time |
| Data export time | data-system-document | (custom) |
Rule: Use ISO 8601 format: 2024-01-15T10:30:00Z
---
10. Analyst/Operator Information → <technique>-document
| Field Type | ASM Field | Examples |
|---|---|---|
| Operator name | analyst | "jsmith" |
| Reviewer | (custom or extension) | "Pending" |
Rule: Analyst goes at the technique-document level, not in individual measurements.
---
Decision Tree
Is this field about...
THE INSTRUMENT ITSELF?
├── Hardware specs → device-system-document
└── Software/files → data-system-document
THE SAMPLE?
└── Sample ID, name, type, batch → sample-document
INSTRUMENT SETTINGS?
└── Configurable parameters → device-control-aggregate-document
ENVIRONMENTAL CONDITIONS?
└── Temp, humidity, etc. → device-control-document
A DIRECT READING?
└── Raw instrument output → measurement-document
A COMPUTED VALUE?
├── From other measurements → calculated-data-document
└── From processing algorithm → processed-data-document
TIMING?
├── When measured → measurement-document.measurement-time
└── When run started/ended → analysis-sequence-document
WHO DID IT?
└── Operator/analyst → <technique>-document.analystCommon Instrument-to-ASM Mappings
Note: These mappings are derived from the Benchling allotropy library. For authoritative mappings, consult the parser source code for your specific instrument.
Cell Counter (Vi-CELL BLU)
Source: `allotropy/parsers/beckman_vi_cell_blu/vi_cell_blu_structure.py`
| Instrument Field | ASM Field |
|---|---|
| Sample ID | sample_identifier |
| Analysis date/time | measurement_time |
| Analysis by | analyst |
| Viability (%) | viability |
| Viable (x10^6) cells/mL | viable_cell_density |
| Total (x10^6) cells/mL | total_cell_density |
| Cell count | total_cell_count |
| Viable cells | viable_cell_count |
| Average diameter (μm) | average_total_cell_diameter |
| Average viable diameter (μm) | average_live_cell_diameter |
| Average circularity | average_total_cell_circularity |
| Cell type | cell_type_processing_method (data-processing) |
| Dilution | cell_density_dilution_factor (data-processing) |
| Min/Max Diameter | minimum/maximum_cell_diameter_setting (data-processing) |
Spectrophotometer (NanoDrop)
| Instrument Field | ASM Field |
|---|---|
| Sample Name | sample_identifier |
| A260, A280 | absorbance (with wavelength) |
| Concentration | mass_concentration |
| 260/280 ratio | a260_a280_ratio |
| Pathlength | pathlength |
Plate Reader
| Instrument Field | ASM Field |
|---|---|
| Well | location_identifier |
| Sample Type | sample_role_type |
| Absorbance/OD | absorbance |
| Fluorescence | fluorescence |
| Plate ID | container_identifier |
Chromatography (HPLC)
| Instrument Field | ASM Field |
|---|---|
| Sample ID | sample_identifier |
| Injection Volume | injection_volume |
| Retention Time | retention_time |
| Peak Area | peak_area |
| Peak Height | peak_height |
| Column Temp | column_oven_temperature |
| Flow Rate | flow_rate |
Unit Handling
Only use units explicitly present in source data. If a value has no unit specified:
- Use
(unitless)as the unit value - Do NOT infer units based on domain knowledge
Calculated Data Traceability
When creating calculated values, always link them to their source data using data-source-aggregate-document:
{
"calculated-data-name": "DIN",
"calculated-result": {"value": 5.8, "unit": "(unitless)"},
"calculated-data-identifier": "TEST_ID_147",
"data-source-aggregate-document": {
"data-source-document": [{
"data-source-identifier": "TEST_ID_145",
"data-source-feature": "sample"
}]
}
}This declares: "DIN 5.8 was calculated from the sample at TEST_ID_145."
Why this matters:
- Audits: Prove a value came from specific raw data
- Debugging: Trace unexpected results back to their source
- Reprocessing: Know which inputs to re-analyze if algorithms change
Assign unique IDs to:
- Measurements, peaks, regions, and calculated values
- Use a consistent naming pattern (e.g.,
INSTRUMENT_TYPE_TEST_ID_N)
This enables bidirectional traversal: trace from calculated → raw, or raw → all derived values.
---
Nested Document Structure (Critical)
A common mistake is "flattening" fields directly onto measurement documents when they should be wrapped in nested structures. This breaks schema compliance and loses semantic context.
Why Nesting Matters
ASM uses nested documents for semantic grouping:
| Document | Purpose | Contains |
|---|---|---|
sample document | What was measured | Sample ID, locations, plate identifiers |
device control aggregate document | How instrument operated | Settings, parameters, techniques |
custom information document | Vendor-specific fields | Non-standard fields that don't map to ASM |
Sample Document Fields
These fields MUST be inside sample document, not flattened on measurement:
// ❌ WRONG - Fields flattened on measurement
{
"measurement identifier": "TEST_001",
"sample identifier": "Sample_A",
"location identifier": "A1",
"absorbance": {"value": 0.5, "unit": "(unitless)"}
}
// ✅ CORRECT - Fields nested in sample document
{
"measurement identifier": "TEST_001",
"sample document": {
"sample identifier": "Sample_A",
"location identifier": "A1",
"well plate identifier": "96WP001"
},
"absorbance": {"value": 0.5, "unit": "(unitless)"}
}Fields belonging in sample document:
sample identifier- Sample ID/namewritten name- Descriptive sample namebatch identifier- Batch/lot numbersample role type- Standard, blank, control, unknownlocation identifier- Well position (A1, B3, etc.)well plate identifier- Plate barcodedescription- Sample description
Device Control Document Fields
Instrument settings MUST be inside device control aggregate document:
// ❌ WRONG - Device settings flattened
{
"measurement identifier": "TEST_001",
"device identifier": "Pod1",
"technique": "Custom",
"volume": {"value": 26, "unit": "μL"}
}
// ✅ CORRECT - Settings nested in device control
{
"measurement identifier": "TEST_001",
"device control aggregate document": {
"device control document": [{
"device type": "liquid handler",
"device identifier": "Pod1"
}]
},
"aspiration volume": {"value": 26, "unit": "μL"}
}Fields belonging in device control:
device type- Type of devicedevice identifier- Device IDdetector wavelength setting- Wavelength for detectioncompartment temperature- Temperature settingsample volume setting- Volume settingflow rate- Flow rate setting
Custom Information Document
Vendor-specific fields that don't map to standard ASM terms go in custom information document:
"device control document": [{
"device type": "liquid handler",
"custom information document": {
"probe": "2",
"pod": "Pod1",
"source labware name": "Inducer",
"destination labware name": "GRP1"
}
}]Liquid Handler: Transfer Pairing
For liquid handlers, a measurement represents a complete transfer (aspirate + dispense), not separate operations:
// ❌ WRONG - Separate records for aspirate and dispense
[
{"measurement identifier": "OP_001", "transfer type": "Aspirate", "volume": {"value": 26, "unit": "μL"}},
{"measurement identifier": "OP_002", "transfer type": "Dispense", "volume": {"value": 26, "unit": "μL"}}
]
// ✅ CORRECT - Single record with source and destination
{
"measurement identifier": "TRANSFER_001",
"sample document": {
"source well location identifier": "1",
"destination well location identifier": "2",
"source well plate identifier": "96WP001",
"destination well plate identifier": "96WP002"
},
"aspiration volume": {"value": 26, "unit": "μL"},
"transfer volume": {"value": 26, "unit": "μL"}
}Pairing logic: 1. Match aspirate and dispense operations by probe number 2. Create one measurement per matched pair 3. Use source_* fields for aspirate location 4. Use destination_* fields for dispense location 5. Include both aspiration volume and transfer volume
Quick Reference: Nesting Decision
Is this field about...
THE SAMPLE BEING MEASURED?
├── Sample ID, name, batch → sample document
├── Well position → sample document.location identifier
├── Plate barcode → sample document.well plate identifier
└── Source/destination locations → sample document (with prefixes)
INSTRUMENT SETTINGS?
├── Standard settings → device control aggregate document
└── Vendor-specific → custom information document
A MEASUREMENT VALUE?
└── Direct on measurement document (e.g., absorbance, volume)
TRANSFER OPERATION TYPE?
└── DON'T use "transfer type" - pair into single measurement
with source/destination fields insteadValidation
Use validate_asm.py to check for nesting issues:
python scripts/validate_asm.py output.json --reference known_good.jsonThe validator checks for:
- Fields incorrectly flattened on measurements
- Missing
sample documentwrapper - Missing
device control aggregate documentwrapper - Missing
custom information documentfor vendor fields - Liquid handler: separate transfer types instead of paired records
Sources
Flattening ASM to 2D CSV
Converting hierarchical ASM JSON to flat 2D tables for LIMS import, spreadsheet analysis, or data engineering pipelines.
Why Flatten?
ASM is semantically rich but hierarchical. Many systems need flat tables:
- LIMS import (Benchling, STARLIMS, LabWare)
- Excel/CSV analysis
- Database loading
- Quick visual inspection
Flattening Strategy
Core Principle
Each measurement becomes one row. Metadata is repeated per row.
What's Excluded
The flattening intentionally omits top-level ASM metadata such as:
$asm.manifest(model version, schema URIs)- Root-level fields outside the technique aggregate document
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.
Hierarchy to Columns
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_percentColumn Naming Convention
Use 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 |
Example: Cell Counting
ASM Input (simplified)
{
"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": "%"}
}
]
}
}]
}
}Flattened Output
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 BLUExample: Plate Reader
ASM Input (simplified)
{
"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"}}
]
}
}]
}
}Flattened Output
plate_id,well_position,absorbance,absorbance_unit
ELISA_001,A1,0.125,mAU
ELISA_001,A2,0.892,mAU
ELISA_001,A3,1.456,mAUHandling Data Cubes
Data cubes (time series, spectra) need special handling:
Option 1: Expand to rows
Each point becomes a row:
sample_id,time_seconds,absorbance
Sample_A,0,0.100
Sample_A,60,0.125
Sample_A,120,0.150Option 2: Wide format
Measurements as columns:
sample_id,abs_0s,abs_60s,abs_120s
Sample_A,0.100,0.125,0.150Option 3: JSON array in cell
Keep as array (some systems support this):
sample_id,absorbance_timeseries
Sample_A,"[0.100,0.125,0.150]"Standard Column Sets by Technique
Cell Counting
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_numberSpectrophotometry
sample_id, wavelength_nm, absorbance, pathlength_cm, concentration,
concentration_unit, a260_a280_ratio, a260_a230_ratio,
analyst, measurement_datetime, instrument_serial_numberPlate Reader / ELISA
plate_id, well_position, sample_type, sample_id, absorbance, absorbance_unit,
concentration, concentration_unit, dilution_factor, cv_percent,
analyst, measurement_datetime, instrument_serial_numberqPCR
sample_id, target_name, well_position, ct_value, ct_mean, ct_sd,
quantity, quantity_unit, amplification_efficiency,
analyst, measurement_datetime, instrument_serial_numberPython Implementation
import 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)LIMS Import Considerations
When importing flattened data into a LIMS:
- Match column names to your LIMS schema field names
- Use ISO 8601 date format for timestamps
- Ensure sample IDs match existing LIMS sample identifiers
- Check if your LIMS expects units in separate columns or embedded in values
Supported Instruments
What Can This Skill Convert?
Any instrument data that maps to an Allotrope schema can be converted. The skill uses a tiered parsing approach:
1. Native allotropy parsers (listed below) - Highest fidelity, validated against vendor-specific formats 2. Flexible fallback parser - Handles any tabular data (CSV, Excel, TXT) by mapping columns to ASM fields 3. PDF extraction - Extracts tables from PDFs, then applies flexible parsing
If your instrument isn't listed below, the skill can still convert it as long as your data contains recognizable measurement fields (sample IDs, values, units, timestamps, etc.) that map to an ASM technique schema.
---
Instruments with Native Allotropy Parsers
The following instruments have optimized parsers in the allotropy library with their Vendor enum values.
Cell Counting
| Instrument | Vendor Enum | File Types |
|---|---|---|
| Beckman Coulter Vi-CELL BLU | BECKMAN_VI_CELL_BLU | .csv |
| Beckman Coulter Vi-CELL XR | BECKMAN_VI_CELL_XR | .txt, .xls, .xlsx |
| ChemoMetec NucleoView NC-200 | CHEMOMETEC_NUCLEOVIEW | .xlsx |
| ChemoMetec NC-View | CHEMOMETEC_NC_VIEW | .xlsx |
| Revvity Matrix | REVVITY_MATRIX | .csv |
Spectrophotometry (UV-Vis)
| Instrument | Vendor Enum | File Types |
|---|---|---|
| Thermo Fisher NanoDrop One | THERMO_FISHER_NANODROP_ONE | .csv, .xlsx |
| Thermo Fisher NanoDrop Eight | THERMO_FISHER_NANODROP_EIGHT | .tsv, .txt |
| Thermo Fisher NanoDrop 8000 | THERMO_FISHER_NANODROP_8000 | .csv |
| Unchained Labs Lunatic | UNCHAINED_LABS_LUNATIC | .csv, .xlsx |
| Thermo Fisher Genesys 30 | THERMO_FISHER_GENESYS30 | .csv |
Plate Readers (Multi-mode, Absorbance, Fluorescence)
| Instrument | Vendor Enum | File Types |
|---|---|---|
| Molecular Devices SoftMax Pro | MOLDEV_SOFTMAX_PRO | .txt |
| PerkinElmer EnVision | PERKIN_ELMER_ENVISION | .csv |
| Agilent Gen5 (BioTek) | AGILENT_GEN5 | .xlsx |
| Agilent Gen5 Image | AGILENT_GEN5_IMAGE | .xlsx |
| BMG MARS (CLARIOstar) | BMG_MARS | .csv, .txt |
| BMG LabTech Smart Control | BMG_LABTECH_SMART_CONTROL | .csv |
| Thermo SkanIt | THERMO_SKANIT | .xlsx |
| Revvity Kaleido | REVVITY_KALEIDO | .csv |
| Tecan Magellan | TECAN_MAGELLAN | .xlsx |
ELISA / Immunoassay
| Instrument | Vendor Enum | File Types |
|---|---|---|
| Molecular Devices SoftMax Pro | MOLDEV_SOFTMAX_PRO | .txt |
| MSD Discovery Workbench | MSD_WORKBENCH | .txt |
| MSD Methodical Mind | METHODICAL_MIND | .xlsx |
| BMG MARS | BMG_MARS | .csv, .txt |
qPCR / PCR
| Instrument | Vendor Enum | File Types |
|---|---|---|
| Applied Biosystems QuantStudio | APPBIO_QUANTSTUDIO | .xlsx |
| Applied Biosystems QuantStudio Design & Analysis | APPBIO_QUANTSTUDIO_DESIGNANALYSIS | .xlsx, .csv |
| Bio-Rad CFX Maestro | BIORAD_CFX_MAESTRO | .csv, .xlsx |
| Roche LightCycler | ROCHE_LIGHTCYCLER | .txt |
Chromatography (HPLC, LC)
| Instrument | Vendor Enum | File Types |
|---|---|---|
| Waters Empower | WATERS_EMPOWER | .xml |
| Thermo Fisher Chromeleon | THERMO_FISHER_CHROMELEON | .xml |
| Agilent ChemStation | AGILENT_CHEMSTATION | .csv |
Electrophoresis
| Instrument | Vendor Enum | File Types |
|---|---|---|
| Agilent TapeStation | AGILENT_TAPESTATION | .csv |
| PerkinElmer LabChip | PERKIN_ELMER_LABCHIP | .csv |
Flow Cytometry
| Instrument | Vendor Enum | File Types |
|---|---|---|
| BD Biosciences FACSDiva | BD_BIOSCIENCES_FACSDIVA | .xml |
| FlowJo | FLOWJO | .wsp |
Solution Analysis
| Instrument | Vendor Enum | File Types |
|---|---|---|
| Roche Cedex BioHT | ROCHE_CEDEX_BIOHT | .xlsx |
| Beckman Coulter Biomek | BECKMAN_COULTER_BIOMEK | .csv |
Auto-Detection Patterns
The skill attempts to identify instrument type from file contents using these patterns:
Vi-CELL BLU
- Column headers: "Sample ID", "Viable cells (x10^6 cells/mL)", "Viability (%)"
- File structure: CSV with specific column order
Vi-CELL XR
- Column headers: "Sample", "Total cells/ml", "Viable cells/ml"
- Multiple export formats supported
NanoDrop
- Column headers: "Sample Name", "Nucleic Acid Conc.", "A260", "A280"
- 260/280 and 260/230 ratio columns
Plate Readers (General)
- Well identifiers (A1-H12 pattern)
- "Plate", "Well", "Sample" columns
- Block-based structure with metadata headers
ELISA
- Standard curve data with concentrations
- OD/absorbance readings
- Sample/blank/standard classification
Using Vendor Enums
from allotropy.parser_factory import Vendor
from allotropy.to_allotrope import allotrope_from_file
# List all supported vendors
for v in Vendor:
print(f"{v.name}: {v.value}")
# Convert file
asm = allotrope_from_file("data.csv", Vendor.BECKMAN_VI_CELL_BLU)Checking Supported Status
from allotropy.parser_factory import get_parser
# Check if a vendor/file combo is supported
try:
parser = get_parser(Vendor.BECKMAN_VI_CELL_BLU)
print("Supported!")
except Exception as e:
print(f"Not supported: {e}")# Instrument Data to Allotrope Skill - Pinned Dependencies
#
# These versions are pinned for reproducibility and determinism.
# All scientists using this skill should install these exact versions
# to ensure identical ASM output from the same input files.
#
# Installation:
# pip install -r requirements.txt --break-system-packages
#
# Note: Versions pinned as of 2025-01-05
# Core parsing library - provides native instrument parsers
allotropy==0.1.55
# Data manipulation and file reading
pandas==2.0.3
# Excel file support (required by pandas for .xlsx files)
openpyxl==3.1.2
# PDF parsing support (for instruments that export PDFs)
pdfplumber==0.9.0
# Scientific computing (optional, but recommended for advanced analysis)
# numpy==1.24.3 # Uncomment if needed
# scipy==1.11.1 # Uncomment if needed
#!/usr/bin/env python3
"""
Instrument Data to ASM Converter
Converts laboratory instrument output files to Allotrope Simple Model (ASM) JSON format.
Supports auto-detection of instrument types and fallback parsing for unsupported formats.
Usage:
python convert_to_asm.py <input_file> [--vendor VENDOR] [--output OUTPUT]
"""
import json
import sys
import re
import hashlib
import importlib.metadata
from pathlib import Path
from typing import Optional, Tuple, Dict, Any
from datetime import datetime
# Lazy imports to avoid errors if not installed
def get_allotropy():
try:
from allotropy.parser_factory import Vendor
from allotropy.to_allotrope import allotrope_from_file, allotrope_from_io
return Vendor, allotrope_from_file, allotrope_from_io
except ImportError:
return None, None, None
def get_pandas():
try:
import pandas as pd
return pd
except ImportError:
return None
# Detection patterns for instrument identification
DETECTION_PATTERNS = {
"BECKMAN_VI_CELL_BLU": {
"columns": [
"Sample ID",
"Viable cells",
"Viability",
"Total cells",
"Average diameter",
],
"keywords": ["Vi-CELL BLU", "Beckman Coulter"],
"file_patterns": [r".*\.csv$"],
"confidence_boost": 20,
},
"BECKMAN_VI_CELL_XR": {
"columns": ["Sample", "Total cells/ml", "Viable cells/ml", "Viability (%)"],
"keywords": ["Vi-CELL XR", "Cell Viability Analyzer"],
"file_patterns": [r".*\.(txt|xls|xlsx)$"],
"confidence_boost": 20,
},
"THERMO_FISHER_NANODROP_EIGHT": {
"columns": ["Sample Name", "Nucleic Acid Conc.", "A260", "A280", "260/280"],
"keywords": ["NanoDrop Eight", "NanoDrop 8"],
"file_patterns": [r".*\.(tsv|txt)$"],
"confidence_boost": 15,
},
"THERMO_FISHER_NANODROP_ONE": {
"columns": ["Sample Name", "Nucleic Acid(ng/uL)", "A260", "A280"],
"keywords": ["NanoDrop One", "NanoDrop"],
"file_patterns": [r".*\.(csv|xlsx)$"],
"confidence_boost": 15,
},
"MOLDEV_SOFTMAX_PRO": {
"columns": ["Well", "Sample", "Values", "Mean", "SD"],
"keywords": ["SoftMax Pro", "SpectraMax", "Molecular Devices"],
"file_patterns": [r".*\.txt$"],
"confidence_boost": 15,
},
"BMG_MARS": {
"columns": ["Well", "Content", "Conc.", "Mean", "SD", "CV"],
"keywords": ["BMG LABTECH", "MARS", "CLARIOstar", "PHERAstar"],
"file_patterns": [r".*\.(csv|txt)$"],
"confidence_boost": 15,
},
"AGILENT_GEN5": {
"columns": ["Well", "Read", "Time", "Temperature"],
"keywords": ["Gen5", "BioTek", "Synergy"],
"file_patterns": [r".*\.xlsx$"],
"confidence_boost": 15,
},
"APPBIO_QUANTSTUDIO": {
"columns": ["Well", "Sample Name", "Target Name", "CT", "Ct Mean"],
"keywords": ["QuantStudio", "Applied Biosystems", "qPCR"],
"file_patterns": [r".*\.xlsx$"],
"confidence_boost": 15,
},
}
def detect_instrument_type(
filepath: str, file_content: Optional[str] = None
) -> Tuple[str, float]:
"""
Auto-detect instrument type from file contents.
Returns:
Tuple of (vendor_name, confidence_score)
confidence_score is 0-100
"""
path = Path(filepath)
filename = path.name.lower()
extension = path.suffix.lower()
# Read file content if not provided
if file_content is None:
try:
if extension in [".xlsx", ".xls"]:
pd = get_pandas()
if pd:
df = pd.read_excel(filepath, nrows=50)
file_content = df.to_string() + "\n" + "\n".join(df.columns)
else:
file_content = ""
else:
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
file_content = f.read(10000) # First 10KB
except Exception as e:
print(f"Warning: Could not read file for detection: {e}")
file_content = ""
content_lower = file_content.lower()
scores = {}
for vendor, patterns in DETECTION_PATTERNS.items():
score = 0
# Check file extension patterns
for pattern in patterns.get("file_patterns", []):
if re.match(pattern, filename, re.IGNORECASE):
score += 10
break
# Check column headers
columns_found = 0
for col in patterns.get("columns", []):
if col.lower() in content_lower:
columns_found += 1
if columns_found > 0:
score += min(50, columns_found * 15)
# Check keywords
for keyword in patterns.get("keywords", []):
if keyword.lower() in content_lower:
score += patterns.get("confidence_boost", 10)
scores[vendor] = min(100, score)
# Return best match
if scores:
best = max(scores.items(), key=lambda x: x[1])
return best[0], best[1]
return "UNKNOWN", 0
def convert_with_allotropy(filepath: str, vendor_name: str) -> Optional[Dict[str, Any]]:
"""
Convert file using allotropy library.
Returns:
ASM dictionary or None if conversion fails
"""
Vendor, allotrope_from_file, _ = get_allotropy()
if Vendor is None:
print(
"Warning: allotropy not installed. Run: pip install allotropy --break-system-packages"
)
return None
try:
vendor = getattr(Vendor, vendor_name, None)
if vendor is None:
print(f"Warning: Vendor {vendor_name} not found in allotropy")
return None
asm = allotrope_from_file(filepath, vendor)
return asm
except Exception as e:
print(f"Allotropy conversion failed: {e}")
return None
def get_deterministic_timestamp(filepath: str) -> str:
"""
Get deterministic timestamp for file.
Uses file modification time for reproducibility.
Returns:
ISO format timestamp string
"""
try:
path = Path(filepath)
mtime = path.stat().st_mtime
return datetime.fromtimestamp(mtime).isoformat()
except Exception:
return "TIMESTAMP_NOT_AVAILABLE"
def calculate_file_hash(filepath: str) -> str:
"""Calculate SHA256 hash of file for provenance tracking."""
try:
with open(filepath, "rb") as f:
return hashlib.sha256(f.read()).hexdigest()
except Exception:
return "HASH_NOT_AVAILABLE"
def get_library_version(library: str) -> str:
"""Get version of installed library."""
try:
return importlib.metadata.version(library)
except Exception:
return "VERSION_NOT_AVAILABLE"
def add_provenance_metadata(
asm: Dict[str, Any],
filepath: str,
vendor: str,
confidence: float,
used_fallback: bool,
warnings: list = None,
) -> Dict[str, Any]:
"""
Add provenance metadata to ASM for reproducibility and audit trail.
This metadata enables:
- Reproducing conversions months later
- Determining which version generated data
- Auditing data lineage for regulatory compliance
"""
pd = get_pandas()
asm["$conversion_metadata"] = {
"skill_version": "1.0.0",
"allotropy_version": get_library_version("allotropy"),
"pandas_version": pd.__version__ if pd else "NOT_INSTALLED",
"conversion_timestamp_utc": datetime.utcnow().isoformat(),
"input_file_sha256": calculate_file_hash(filepath),
"input_file_size_bytes": Path(filepath).stat().st_size,
"input_file_name": Path(filepath).name,
"parser_used": "fallback" if used_fallback else "allotropy",
"detection_confidence": confidence,
"vendor_detected": vendor,
"warnings": warnings or [],
}
return asm
def flexible_parse(filepath: str, detected_type: str) -> Optional[Dict[str, Any]]:
"""
Flexible fallback parser when allotropy fails.
Creates ASM-like structure from parsed data.
**WARNING:** This parser creates simplified ASM that:
- Does NOT distinguish raw vs. calculated data
- LACKS instrument control parameters (temperature, wavelengths, etc.)
- MAY NOT be compatible with regulatory requirements (GxP)
- Should be used for exploratory analysis only, not production LIMS import
"""
pd = get_pandas()
if pd is None:
print("Warning: pandas not installed for flexible parsing")
return None
path = Path(filepath)
extension = path.suffix.lower()
try:
# Read file based on extension
if extension in [".xlsx", ".xls"]:
df = pd.read_excel(filepath, engine="openpyxl")
elif extension == ".tsv":
df = pd.read_csv(filepath, sep="\t")
elif extension == ".csv":
df = pd.read_csv(filepath)
else:
df = pd.read_csv(filepath, sep=None, engine="python")
# Build ASM-like structure
asm = build_flexible_asm(df, detected_type, filepath)
return asm
except Exception as e:
print(f"Flexible parsing failed: {e}")
return None
def build_flexible_asm(df, detected_type: str, filepath: str) -> Dict[str, Any]:
"""
Build ASM-like JSON structure from parsed DataFrame.
"""
timestamp = get_deterministic_timestamp(filepath)
# Determine technique from detected type
technique = "generic"
if "VI_CELL" in detected_type:
technique = "cell-counting"
elif "NANODROP" in detected_type:
technique = "spectrophotometry"
elif detected_type in ["MOLDEV_SOFTMAX_PRO", "BMG_MARS", "AGILENT_GEN5"]:
technique = "plate-reader"
elif "QUANTSTUDIO" in detected_type:
technique = "pcr"
# Build base structure
asm = {
"$asm.manifest": {
"vocabulary": ["http://purl.allotrope.org/voc/afo/REC/2023/09/"],
"contexts": [
"http://purl.allotrope.org/json-ld/afo-context-REC-2023-09.jsonld"
],
},
f"{technique}-aggregate-document": {
"device-system-document": {
"device-identifier": "FLEXIBLE_PARSER",
"product-manufacturer": (
detected_type.split("_")[0] if "_" in detected_type else "Unknown"
),
},
f"{technique}-document": [
{
"measurement-aggregate-document": {
"measurement-time": timestamp,
"measurement-document": [],
}
}
],
},
}
# Add measurements from DataFrame
measurements = asm[f"{technique}-aggregate-document"][f"{technique}-document"][0][
"measurement-aggregate-document"
]["measurement-document"]
for _, row in df.iterrows():
meas = {}
for col in df.columns:
value = row[col]
if pd.notna(value):
# Clean column name
clean_col = str(col).lower().replace(" ", "-").replace("_", "-")
clean_col = re.sub(r"[^a-z0-9-]", "", clean_col)
# Handle numeric values
if isinstance(value, (int, float)):
meas[clean_col] = {"value": value, "unit": "(unitless)"}
else:
meas[clean_col] = str(value)
if meas:
measurements.append(meas)
return asm
def main():
"""Main entry point."""
import argparse
parser = argparse.ArgumentParser(
description="Convert instrument data to ASM format"
)
parser.add_argument("input", help="Input file path")
parser.add_argument(
"--vendor", help="Vendor enum name (auto-detected if not provided)"
)
parser.add_argument(
"--output", "-o", help="Output file path (default: input_asm.json)"
)
parser.add_argument(
"--flatten", action="store_true", help="Also generate flattened CSV"
)
parser.add_argument(
"--allow-fallback",
action="store_true",
help="Allow fallback to simplified parser (reduced metadata)",
)
parser.add_argument(
"--skip-validation",
action="store_true",
help="Skip automatic validation (not recommended)",
)
parser.add_argument(
"--force",
action="store_true",
help="Force conversion even with low confidence detection",
)
args = parser.parse_args()
input_path = Path(args.input)
if not input_path.exists():
print(f"Error: File not found: {args.input}")
sys.exit(1)
warnings = []
# Detect or use provided vendor
if args.vendor:
vendor = args.vendor.upper()
confidence = 100
print(f"Using specified vendor: {vendor}")
else:
vendor, confidence = detect_instrument_type(str(input_path))
print(f"Detected instrument: {vendor} (confidence: {confidence}%)")
# Enforce confidence thresholds
if confidence < 30:
print(
f"ERROR: Detection confidence too low ({confidence}%). Cannot proceed."
)
print("Please specify --vendor explicitly.")
sys.exit(1)
elif confidence < 60:
warning_msg = f"WARNING: Low confidence detection ({confidence}%)."
print(warning_msg)
warnings.append(warning_msg)
if not args.force:
print("Use --force to proceed anyway (not recommended).")
sys.exit(1)
# Try allotropy first
asm = convert_with_allotropy(str(input_path), vendor)
used_fallback = False
# Fall back to flexible parser
if asm is None:
print("\n" + "=" * 60)
print("ALLOTROPY PARSING FAILED - USING REDUCED METADATA PARSER")
print("=" * 60)
print("Output will lack:")
print(" - Calculated data traceability")
print(" - Device control settings")
print(" - Data processing metadata")
print("\nNot suitable for:")
print(" - Regulatory submissions")
print(" - LIMS import with validation")
print("=" * 60 + "\n")
if not args.allow_fallback:
print(
"ERROR: Allotropy parsing failed. Use --allow-fallback to continue with"
)
print("simplified parser, but note that output will lack required metadata")
print("for GxP compliance.")
sys.exit(1)
asm = flexible_parse(str(input_path), vendor)
used_fallback = True
warnings.append("Used fallback parser - reduced metadata")
if asm is None:
print("Error: Could not convert file")
sys.exit(1)
# Add provenance metadata
asm = add_provenance_metadata(
asm, str(input_path), vendor, confidence, used_fallback, warnings
)
# Determine output path
if args.output:
output_path = Path(args.output)
else:
output_path = input_path.with_suffix(".asm.json")
# Write to temporary file first
temp_path = output_path.with_suffix(".tmp")
try:
with open(temp_path, "w") as f:
json.dump(asm, f, indent=2, default=str)
# Validate unless skipped
if not args.skip_validation:
print("Running validation...")
try:
from validate_asm import validate_asm
result = validate_asm(str(temp_path))
if not result.is_valid():
print("\n" + "=" * 60)
print("VALIDATION FAILED")
print("=" * 60)
for error in result.errors:
print(f"ERROR: {error}")
for warning in result.warnings:
print(f"WARNING: {warning}")
print("=" * 60)
# Remove temp file
temp_path.unlink()
print("\nValidation failed. Output file not created.")
sys.exit(1)
else:
if result.warnings:
print("\nValidation warnings:")
for warning in result.warnings:
print(f" WARNING: {warning}")
print("Validation passed.")
except ImportError:
print(
"Warning: validate_asm.py not found. Skipping validation. "
"Consider adding validation script."
)
# Move temp file to final location
temp_path.replace(output_path)
print(f"ASM output written to: {output_path}")
except Exception as e:
# Clean up temp file on error
if temp_path.exists():
temp_path.unlink()
raise e
# Optionally flatten
if args.flatten:
from flatten_asm import flatten_asm_to_csv
flat_path = input_path.with_suffix(".flat.csv")
flatten_asm_to_csv(asm, str(flat_path))
print(f"Flattened CSV written to: {flat_path}")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Export Parser Code
Generates standalone Python scripts that can be handed off to data engineers
or run in Jupyter notebooks. The exported code is self-contained and
production-ready.
Usage:
python export_parser.py --vendor VI_CELL_BLU --output vicell_parser.py
python export_parser.py --vendor NANODROP_EIGHT --format notebook --output nanodrop_parser.ipynb
"""
import sys
from pathlib import Path
from datetime import datetime
from typing import Optional
# Template for standalone Python script
SCRIPT_TEMPLATE = '''#!/usr/bin/env python3
"""
{instrument_name} to Allotrope Simple Model (ASM) Parser
Auto-generated by Claude instrument-data-to-allotrope skill
Generated: {timestamp}
Vendor: {vendor}
This script converts {instrument_name} output files to Allotrope Simple Model (ASM)
JSON format for LIMS import, data lakes, or downstream analysis.
Requirements:
pip install allotropy pandas openpyxl
Usage:
python {script_name} input_file.csv --output output_asm.json
python {script_name} input_file.csv --flatten # Also generate CSV
Input file format:
{file_format_description}
"""
import json
import argparse
from pathlib import Path
from typing import Dict, Any, Optional
try:
from allotropy.parser_factory import Vendor
from allotropy.to_allotrope import allotrope_from_file
ALLOTROPY_AVAILABLE = True
except ImportError:
ALLOTROPY_AVAILABLE = False
print("Warning: allotropy not installed. Install with: pip install allotropy")
try:
import pandas as pd
PANDAS_AVAILABLE = True
except ImportError:
PANDAS_AVAILABLE = False
def convert_to_asm(filepath: str) -> Optional[Dict[str, Any]]:
"""
Convert {instrument_name} file to ASM format.
Args:
filepath: Path to input file
Returns:
ASM dictionary or None if conversion fails
"""
if not ALLOTROPY_AVAILABLE:
raise ImportError("allotropy library required. Install with: pip install allotropy")
try:
asm = allotrope_from_file(filepath, Vendor.{vendor})
return asm
except Exception as e:
print(f"Conversion error: {{e}}")
return None
def flatten_asm(asm: Dict[str, Any]) -> list:
"""
Flatten ASM to list of row dictionaries for CSV export.
Args:
asm: ASM dictionary
Returns:
List of flattened row dictionaries
"""
technique = "{technique}"
rows = []
agg_key = f"{{technique}}-aggregate-document"
agg_doc = asm.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"),
}}
doc_key = f"{{technique}}-document"
for doc in agg_doc.get(doc_key, []):
meas_agg = doc.get("measurement-aggregate-document", {{}})
common = {{
"analyst": meas_agg.get("analyst"),
"measurement_time": meas_agg.get("measurement-time"),
**device_info
}}
for meas in meas_agg.get("measurement-document", []):
row = {{**common}}
for key, value in meas.items():
clean_key = key.replace("-", "_")
if isinstance(value, dict) and "value" in value:
row[clean_key] = value["value"]
if "unit" in value:
row[f"{{clean_key}}_unit"] = value["unit"]
else:
row[clean_key] = value
rows.append(row)
return rows
def main():
parser = argparse.ArgumentParser(description="Convert {instrument_name} to ASM")
parser.add_argument("input", help="Input file path")
parser.add_argument("--output", "-o", help="Output JSON path")
parser.add_argument("--flatten", action="store_true", help="Also generate CSV")
args = parser.parse_args()
input_path = Path(args.input)
if not input_path.exists():
print(f"Error: File not found: {{args.input}}")
return 1
# Convert to ASM
print(f"Converting {{args.input}}...")
asm = convert_to_asm(str(input_path))
if asm is None:
print("Conversion failed")
return 1
# Write ASM JSON
output_path = args.output or str(input_path.with_suffix('.asm.json'))
with open(output_path, 'w') as f:
json.dump(asm, f, indent=2, default=str)
print(f"ASM written to: {{output_path}}")
# Optionally flatten
if args.flatten and PANDAS_AVAILABLE:
rows = flatten_asm(asm)
df = pd.DataFrame(rows)
flat_path = str(input_path.with_suffix('.flat.csv'))
df.to_csv(flat_path, index=False)
print(f"CSV written to: {{flat_path}}")
return 0
if __name__ == "__main__":
sys.exit(main())
'''
# Template for Jupyter notebook
NOTEBOOK_TEMPLATE = """{{
"cells": [
{{
"cell_type": "markdown",
"metadata": {{}},
"source": [
"# {instrument_name} to Allotrope Simple Model (ASM) Parser\\n",
"\\n",
"Auto-generated by Claude instrument-data-to-allotrope skill\\n",
"Generated: {timestamp}\\n",
"Vendor: {vendor}\\n",
"\\n",
"This notebook converts {instrument_name} output files to Allotrope Simple Model (ASM) JSON format."
]
}},
{{
"cell_type": "code",
"execution_count": null,
"metadata": {{}},
"source": [
"# Install requirements (uncomment if needed)\\n",
"# !pip install allotropy pandas openpyxl"
]
}},
{{
"cell_type": "code",
"execution_count": null,
"metadata": {{}},
"source": [
"import json\\n",
"from pathlib import Path\\n",
"import pandas as pd\\n",
"\\n",
"from allotropy.parser_factory import Vendor\\n",
"from allotropy.to_allotrope import allotrope_from_file"
]
}},
{{
"cell_type": "markdown",
"metadata": {{}},
"source": [
"## Configuration\\n",
"\\n",
"Set your input file path here:"
]
}},
{{
"cell_type": "code",
"execution_count": null,
"metadata": {{}},
"source": [
"# Configure input/output paths\\n",
"INPUT_FILE = \\"your_data_file.csv\\" # <-- Change this\\n",
"OUTPUT_ASM = \\"output_asm.json\\"\\n",
"OUTPUT_CSV = \\"output_flat.csv\\""
]
}},
{{
"cell_type": "markdown",
"metadata": {{}},
"source": [
"## Convert to ASM"
]
}},
{{
"cell_type": "code",
"execution_count": null,
"metadata": {{}},
"source": [
"# Convert file to ASM\\n",
"asm = allotrope_from_file(INPUT_FILE, Vendor.{vendor})\\n",
"\\n",
"# Save ASM JSON\\n",
"with open(OUTPUT_ASM, 'w') as f:\\n",
" json.dump(asm, f, indent=2, default=str)\\n",
"\\n",
"print(f\\"ASM saved to: {{OUTPUT_ASM}}\\")"
]
}},
{{
"cell_type": "markdown",
"metadata": {{}},
"source": [
"## Preview ASM Structure"
]
}},
{{
"cell_type": "code",
"execution_count": null,
"metadata": {{}},
"source": [
"# Show ASM structure\\n",
"print(json.dumps(asm, indent=2, default=str)[:2000])"
]
}},
{{
"cell_type": "markdown",
"metadata": {{}},
"source": [
"## Flatten to CSV"
]
}},
{{
"cell_type": "code",
"execution_count": null,
"metadata": {{}},
"source": [
"def flatten_asm(asm, technique=\\"{technique}\\"):\\n",
" rows = []\\n",
" agg_key = f\\"{{technique}}-aggregate-document\\"\\n",
" agg_doc = asm.get(agg_key, {{}})\\n",
" \\n",
" device = agg_doc.get(\\"device-system-document\\", {{}})\\n",
" device_info = {{\\n",
" \\"instrument_serial_number\\": device.get(\\"device-identifier\\"),\\n",
" \\"instrument_model\\": device.get(\\"model-number\\"),\\n",
" }}\\n",
" \\n",
" doc_key = f\\"{{technique}}-document\\"\\n",
" for doc in agg_doc.get(doc_key, []):\\n",
" meas_agg = doc.get(\\"measurement-aggregate-document\\", {{}})\\n",
" common = {{\\n",
" \\"analyst\\": meas_agg.get(\\"analyst\\"),\\n",
" \\"measurement_time\\": meas_agg.get(\\"measurement-time\\"),\\n",
" **device_info\\n",
" }}\\n",
" \\n",
" for meas in meas_agg.get(\\"measurement-document\\", []):\\n",
" row = {{**common}}\\n",
" for key, value in meas.items():\\n",
" clean_key = key.replace(\\"-\\", \\"_\\")\\n",
" if isinstance(value, dict) and \\"value\\" in value:\\n",
" row[clean_key] = value[\\"value\\"]\\n",
" if \\"unit\\" in value:\\n",
" row[f\\"{{clean_key}}_unit\\"] = value[\\"unit\\"]\\n",
" else:\\n",
" row[clean_key] = value\\n",
" rows.append(row)\\n",
" return rows\\n",
"\\n",
"# Flatten and save\\n",
"rows = flatten_asm(asm)\\n",
"df = pd.DataFrame(rows)\\n",
"df.to_csv(OUTPUT_CSV, index=False)\\n",
"print(f\\"CSV saved to: {{OUTPUT_CSV}}\\")"
]
}},
{{
"cell_type": "code",
"execution_count": null,
"metadata": {{}},
"source": [
"# Preview flattened data\\n",
"df.head()"
]
}}
],
"metadata": {{
"kernelspec": {{
"display_name": "Python 3",
"language": "python",
"name": "python3"
}},
"language_info": {{
"name": "python",
"version": "3.10.0"
}}
}},
"nbformat": 4,
"nbformat_minor": 4
}}"""
# Instrument metadata for templates
INSTRUMENT_INFO = {
"BECKMAN_VI_CELL_BLU": {
"name": "Beckman Coulter Vi-CELL BLU",
"technique": "cell-counting",
"file_format": "CSV export from Vi-CELL BLU software with columns: Sample ID, Viable cells, Viability, Total cells, etc.",
},
"BECKMAN_VI_CELL_XR": {
"name": "Beckman Coulter Vi-CELL XR",
"technique": "cell-counting",
"file_format": "TXT or XLS/XLSX export from Vi-CELL XR with sample and measurement data",
},
"THERMO_FISHER_NANODROP_EIGHT": {
"name": "Thermo Fisher NanoDrop Eight",
"technique": "spectrophotometry",
"file_format": "TSV or TXT export with Sample Name, Nucleic Acid Conc., A260, A280, 260/280 ratio",
},
"THERMO_FISHER_NANODROP_ONE": {
"name": "Thermo Fisher NanoDrop One",
"technique": "spectrophotometry",
"file_format": "CSV or XLSX export with spectrophotometry measurements",
},
"MOLDEV_SOFTMAX_PRO": {
"name": "Molecular Devices SoftMax Pro",
"technique": "plate-reader",
"file_format": "TXT export from SoftMax Pro with plate reader data",
},
"BMG_MARS": {
"name": "BMG MARS (CLARIOstar)",
"technique": "plate-reader",
"file_format": "CSV or TXT export from BMG MARS with Well, Content, Conc., Mean, SD, CV columns",
},
"AGILENT_GEN5": {
"name": "Agilent Gen5 (BioTek)",
"technique": "plate-reader",
"file_format": "XLSX export from Gen5 software",
},
"APPBIO_QUANTSTUDIO": {
"name": "Applied Biosystems QuantStudio",
"technique": "pcr",
"file_format": "XLSX export with qPCR data including Well, Sample Name, Target Name, CT values",
},
}
def generate_script(vendor: str, output_path: str) -> None:
"""Generate standalone Python script for given vendor."""
info = INSTRUMENT_INFO.get(
vendor,
{
"name": vendor.replace("_", " ").title(),
"technique": "generic",
"file_format": "Instrument output file",
},
)
script = SCRIPT_TEMPLATE.format(
instrument_name=info["name"],
timestamp=datetime.now().isoformat(),
vendor=vendor,
script_name=Path(output_path).name,
file_format_description=info["file_format"],
technique=info["technique"],
)
with open(output_path, "w") as f:
f.write(script)
def generate_notebook(vendor: str, output_path: str) -> None:
"""Generate Jupyter notebook for given vendor."""
info = INSTRUMENT_INFO.get(
vendor,
{
"name": vendor.replace("_", " ").title(),
"technique": "generic",
"file_format": "Instrument output file",
},
)
notebook = NOTEBOOK_TEMPLATE.format(
instrument_name=info["name"],
timestamp=datetime.now().isoformat(),
vendor=vendor,
technique=info["technique"],
)
with open(output_path, "w") as f:
f.write(notebook)
def main():
import argparse
parser = argparse.ArgumentParser(
description="Export parser code for data engineers"
)
parser.add_argument("--vendor", help="Vendor enum name (e.g., VI_CELL_BLU)")
parser.add_argument("--output", "-o", help="Output file path")
parser.add_argument(
"--format",
choices=["script", "notebook"],
default="script",
help="Output format (default: script)",
)
parser.add_argument(
"--list-vendors", action="store_true", help="List supported vendors"
)
args = parser.parse_args()
if args.list_vendors:
print("Supported vendors:")
for vendor in INSTRUMENT_INFO.keys():
print(f" {vendor}")
return 0
if not args.vendor or not args.output:
parser.error("--vendor and --output are required when not using --list-vendors")
vendor = args.vendor.upper()
if args.format == "notebook":
generate_notebook(vendor, args.output)
else:
generate_script(vendor, args.output)
print(f"Parser code exported to: {args.output}")
return 0
if __name__ == "__main__":
sys.exit(main())
#!/usr/bin/env python3
"""
Flatten ASM JSON to 2D CSV
Converts hierarchical Allotrope Simple Model (ASM) JSON to flat tabular format
suitable for LIMS import, spreadsheet analysis, or database loading.
Usage:
python flatten_asm.py <input_asm.json> [--output OUTPUT.csv]
"""
import json
import sys
import re
from pathlib import Path
from typing import Dict, Any, List, Optional
from datetime import datetime
try:
import pandas as pd
PANDAS_AVAILABLE = True
except ImportError:
PANDAS_AVAILABLE = False
def detect_technique(asm: Dict[str, Any]) -> str:
"""Detect the ASM technique type from document structure."""
for key in asm.keys():
if key.endswith("-aggregate-document"):
return key.replace("-aggregate-document", "")
return "generic"
def flatten_value(value: Any, prefix: str = "") -> Dict[str, Any]:
"""
Flatten a single ASM value, handling value datum patterns.
Returns dict of {column_name: value}
"""
result = {}
if isinstance(value, dict):
if "value" in value:
# Value datum pattern
result[prefix] = value["value"]
if "unit" in value:
result[f"{prefix}_unit"] = value["unit"]
else:
# Nested dict - recurse
for k, v in value.items():
clean_key = k.replace("-", "_")
nested_prefix = f"{prefix}_{clean_key}" if prefix else clean_key
result.update(flatten_value(v, nested_prefix))
elif isinstance(value, list):
# Array - could be data cube or list of items
if len(value) > 0 and isinstance(value[0], dict):
# List of objects - this shouldn't happen at leaf level
result[prefix] = json.dumps(value)
else:
# Simple array - store as JSON string
result[prefix] = json.dumps(value)
else:
# Scalar value
result[prefix] = value
return result
def extract_device_info(asm: Dict[str, Any], technique: str) -> Dict[str, Any]:
"""Extract device/instrument information from ASM."""
agg_key = f"{technique}-aggregate-document"
agg_doc = asm.get(agg_key, {})
device = agg_doc.get("device-system-document", {})
return {
"instrument_serial_number": device.get("device-identifier"),
"instrument_model": device.get("model-number"),
"instrument_manufacturer": device.get("product-manufacturer"),
"software_name": device.get("software-name"),
"software_version": device.get("software-version"),
}
def flatten_asm(asm: Dict[str, Any]) -> List[Dict[str, Any]]:
"""
Flatten ASM JSON to list of row dictionaries.
Each measurement becomes one row with metadata repeated.
"""
technique = detect_technique(asm)
rows = []
# Extract device info (shared across all rows)
device_info = extract_device_info(asm, technique)
device_info = {k: v for k, v in device_info.items() if v is not None}
# Navigate to measurements
agg_key = f"{technique}-aggregate-document"
agg_doc = asm.get(agg_key, {})
doc_key = f"{technique}-document"
technique_docs = agg_doc.get(doc_key, [])
for doc in technique_docs:
# Get measurement aggregate
meas_agg = doc.get("measurement-aggregate-document", {})
# Extract common measurement metadata
common_meta = {}
for key, value in meas_agg.items():
if key == "measurement-document":
continue
clean_key = key.replace("-", "_")
if isinstance(value, (str, int, float, bool)):
common_meta[clean_key] = value
elif isinstance(value, dict) and "value" in value:
common_meta[clean_key] = value["value"]
if "unit" in value:
common_meta[f"{clean_key}_unit"] = value["unit"]
# Extract each measurement as a row
measurements = meas_agg.get("measurement-document", [])
for meas in measurements:
row = {**device_info, **common_meta}
for key, value in meas.items():
clean_key = key.replace("-", "_")
flattened = flatten_value(value, clean_key)
row.update(flattened)
rows.append(row)
return rows
def flatten_asm_to_csv(asm: Dict[str, Any], output_path: str) -> None:
"""
Flatten ASM and write to CSV file.
Args:
asm: Parsed ASM JSON dictionary
output_path: Path for output CSV
"""
if not PANDAS_AVAILABLE:
raise ImportError(
"pandas is required for CSV output. Install with: pip install pandas"
)
rows = flatten_asm(asm)
if not rows:
print("Warning: No measurements found to flatten")
# Create empty CSV with header
with open(output_path, "w") as f:
f.write("# No measurements found in ASM\n")
return
df = pd.DataFrame(rows)
# Reorder columns for readability
priority_cols = [
"sample_identifier",
"sample_id",
"well_location",
"well_position",
"measurement_time",
"measurement_datetime",
"analyst",
]
ordered_cols = []
for col in priority_cols:
if col in df.columns:
ordered_cols.append(col)
remaining = [c for c in df.columns if c not in ordered_cols]
df = df[ordered_cols + remaining]
df.to_csv(output_path, index=False)
def flatten_asm_to_dict(asm: Dict[str, Any]) -> Dict[str, Any]:
"""
Flatten ASM and return as dictionary with rows and columns.
Useful for non-CSV outputs or further processing.
"""
rows = flatten_asm(asm)
if not rows:
return {"columns": [], "rows": []}
columns = list(rows[0].keys())
return {
"columns": columns,
"rows": [[row.get(col) for col in columns] for row in rows],
}
def main():
"""Main entry point."""
import argparse
parser = argparse.ArgumentParser(description="Flatten ASM JSON to CSV")
parser.add_argument("input", help="Input ASM JSON file")
parser.add_argument(
"--output", "-o", help="Output CSV path (default: input_flat.csv)"
)
parser.add_argument(
"--format",
choices=["csv", "json"],
default="csv",
help="Output format (default: csv)",
)
args = parser.parse_args()
input_path = Path(args.input)
if not input_path.exists():
print(f"Error: File not found: {args.input}")
sys.exit(1)
# Load ASM
with open(input_path) as f:
asm = json.load(f)
# Determine output path
if args.output:
output_path = args.output
else:
suffix = ".flat.csv" if args.format == "csv" else ".flat.json"
output_path = str(input_path.with_suffix("")) + suffix
# Flatten and write
if args.format == "csv":
flatten_asm_to_csv(asm, output_path)
else:
result = flatten_asm_to_dict(asm)
with open(output_path, "w") as f:
json.dump(result, f, indent=2)
print(f"Flattened output written to: {output_path}")
# Report stats
rows = flatten_asm(asm)
print(f" Rows: {len(rows)}")
if rows:
print(f" Columns: {len(rows[0])}")
if __name__ == "__main__":
main()
Related skills
How it compares
Pick this over generic JSON normalization skills when the target interchange standard is the Allotrope scientific ontology.
FAQ
What does instrument data to allotrope do?
Convert laboratory instrument output files (PDF, CSV, Excel, TXT) to Allotrope Simple Model (ASM) JSON format or flattened 2D CSV. Use this skill when scientists need to standardize instrument data fo
When should I invoke instrument data to allotrope?
Convert laboratory instrument output files (PDF, CSV, Excel, TXT) to Allotrope Simple Model (ASM) JSON format or flattened 2D CSV. Use this skill when scientists need to standardize instrument data fo
What are key capabilities?
**Detect instrument type** from file contents (auto-detect or user-specified)
Is Instrument Data To Allotrope safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.