Skip to content

Commit bf7a436

Browse files
feat: JSON-to-CSV - improve list parsing to better support datacubes (#1124)
Currently, when two elements are not in the same root, we always cross the parsed datasets. ASM datacubes store the x/y dimensions in lists in separate paths, which means they get crossed, so that for: ``` data : { dimensions: [[1,2]], measures: [[0.25, 0.5] } ``` you get ``` 1,0.25 1,0.5 2,0.25 2,0.5 ``` Now, we detect if all parsed datasets being combined have the same length, and if so, just concat them. This gets us the expected result: ``` 1,0.25 2,0.5 ``` for datacube extraction, while still crossing with downstream nested values
1 parent af284e7 commit bf7a436

3 files changed

Lines changed: 171 additions & 5 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ download-schema = "scripts/download_schema.py {args:}"
151151
create-parser = "scripts/create_parser.py {args:}"
152152
update-instrument-table = "scripts/update_supported_instruments_table.py {args:}"
153153
update-version = "scripts/update_version.py {args:}"
154+
json-to-csv = "scripts/json_to_csv_convert.py {args:}"
154155

155156
[tool.hatch.envs.win-scripts]
156157
platforms = ["windows"]
@@ -161,6 +162,7 @@ download-schema = "python scripts\\download_schema.py {args:}"
161162
create-parser = "python scripts\\create_parser.py {args:}"
162163
update-instrument-table = "python scripts\\update_supported_instruments_table.py {args:}"
163164
update-version = "python scripts\\update_version.py {args:}"
165+
json-to-csv = "python scripts\\json_to_csv_convert.py {args:}"
164166

165167
[tool.hatch.envs.viz.scripts]
166168
run = "scripts/visualization.py {args:}"

src/allotropy/json_to_csv/json_to_csv.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,22 @@ def _map_dataset(
141141
msg = f"Invalid transform type in dataset config: {transform.type_}"
142142
raise ValueError(msg)
143143

144-
df = (
145-
df
146-
if path_df.empty
147-
else (path_df if df.empty else df.merge(path_df, how="cross"))
148-
)
144+
# Combine the current dataframe with the path dataframe
145+
if path_df.empty:
146+
# No new data to add
147+
pass
148+
elif df.empty:
149+
# First data, use it as the base
150+
df = path_df
151+
elif len(df) == len(path_df):
152+
# Same length: concatenate side-by-side (zip parallel arrays)
153+
# This handles datacube dimensions/measures that should be paired element-wise
154+
df = pd.concat(
155+
[df.reset_index(drop=True), path_df.reset_index(drop=True)], axis=1
156+
)
157+
else:
158+
# Different lengths: cross product (Cartesian product)
159+
df = df.merge(path_df, how="cross")
149160

150161
return df
151162

tests/json_to_csv/json_to_csv_test.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -629,3 +629,156 @@ def test_map_dataset_metadata_to_json() -> None:
629629
"Dict Key": "dict_value",
630630
}
631631
assert expected == actual
632+
633+
634+
def test_map_dataset_parallel_arrays_same_length_are_zipped() -> None:
635+
"""Test that parallel arrays of same length are zipped together, not crossed."""
636+
data = {
637+
"data": {
638+
"dimensions": [[0.0, 0.01, 0.02]],
639+
"measures": [[1.0, 2.0, 3.0]],
640+
},
641+
"label": "test_label",
642+
}
643+
644+
dataset_config = DatasetConfig.create(
645+
config_json={
646+
"name": "Dataset",
647+
"columns": [
648+
{"name": "time", "path": "data/dimensions/[0]"},
649+
{"name": "value", "path": "data/measures/[0]"},
650+
{"name": "label", "path": "label"},
651+
],
652+
},
653+
transforms=[],
654+
)
655+
656+
actual = map_dataset(data, dataset_config)
657+
# Should have 3 rows (zipped), not 9 rows (crossed)
658+
expected = pd.DataFrame(
659+
{
660+
"time": [0.0, 0.01, 0.02],
661+
"value": [1.0, 2.0, 3.0],
662+
"label": ["test_label", "test_label", "test_label"],
663+
}
664+
)
665+
assert expected.equals(actual)
666+
667+
668+
def test_map_dataset_different_length_arrays_are_crossed() -> None:
669+
"""Test that arrays of different lengths are still crossed (Cartesian product)."""
670+
data = {
671+
"list1": [{"key1": "A"}, {"key1": "B"}],
672+
"list2": [{"key2": "X"}, {"key2": "Y"}, {"key2": "Z"}],
673+
}
674+
675+
dataset_config = DatasetConfig.create(
676+
config_json={
677+
"name": "Dataset",
678+
"columns": [
679+
{"name": "Key1", "path": "list1/key1"},
680+
{"name": "Key2", "path": "list2/key2"},
681+
],
682+
},
683+
transforms=[],
684+
)
685+
686+
actual = map_dataset(data, dataset_config)
687+
# Should have 6 rows (2 x 3 crossed)
688+
expected = pd.DataFrame(
689+
{
690+
"Key1": ["A", "A", "A", "B", "B", "B"],
691+
"Key2": ["X", "Y", "Z", "X", "Y", "Z"],
692+
}
693+
)
694+
assert expected.equals(actual)
695+
696+
697+
def test_map_dataset_datacube_structure() -> None:
698+
"""Test datacube-like structure with multiple parallel array pairs."""
699+
data = {
700+
"measurements": [
701+
{
702+
"location": "A1",
703+
"datacube": {
704+
"label": "UV_VIS",
705+
"data": {
706+
"dimensions": [[0.0, 0.1, 0.2]],
707+
"measures": [[1.5, 2.5, 3.5]],
708+
},
709+
},
710+
},
711+
{
712+
"location": "A2",
713+
"datacube": {
714+
"label": "UV_VIS",
715+
"data": {
716+
"dimensions": [[0.0, 0.1]],
717+
"measures": [[4.0, 5.0]],
718+
},
719+
},
720+
},
721+
]
722+
}
723+
724+
dataset_config = DatasetConfig.create(
725+
config_json={
726+
"name": "Dataset",
727+
"columns": [
728+
{"name": "location", "path": "measurements/location"},
729+
{"name": "label", "path": "measurements/datacube/label"},
730+
{"name": "time", "path": "measurements/datacube/data/dimensions/[0]"},
731+
{
732+
"name": "absorbance",
733+
"path": "measurements/datacube/data/measures/[0]",
734+
},
735+
],
736+
},
737+
transforms=[],
738+
)
739+
740+
actual = map_dataset(data, dataset_config)
741+
# Should have 5 rows total (3 for A1 + 2 for A2), not 15 rows (3X3 + 2X2 crossed)
742+
expected = pd.DataFrame(
743+
{
744+
"location": ["A1", "A1", "A1", "A2", "A2"],
745+
"label": ["UV_VIS", "UV_VIS", "UV_VIS", "UV_VIS", "UV_VIS"],
746+
"time": [0.0, 0.1, 0.2, 0.0, 0.1],
747+
"absorbance": [1.5, 2.5, 3.5, 4.0, 5.0],
748+
}
749+
)
750+
assert expected.equals(actual)
751+
752+
753+
def test_map_dataset_mixed_single_and_parallel_arrays() -> None:
754+
"""Test that single values are crossed with parallel arrays correctly."""
755+
data = {
756+
"constant": "fixed_value",
757+
"arrays": {
758+
"x": [1, 2, 3],
759+
"y": [10, 20, 30],
760+
},
761+
}
762+
763+
dataset_config = DatasetConfig.create(
764+
config_json={
765+
"name": "Dataset",
766+
"columns": [
767+
{"name": "constant", "path": "constant"},
768+
{"name": "x", "path": "arrays/x"},
769+
{"name": "y", "path": "arrays/y"},
770+
],
771+
},
772+
transforms=[],
773+
)
774+
775+
actual = map_dataset(data, dataset_config)
776+
# x and y should be zipped (3 rows), then crossed with constant
777+
expected = pd.DataFrame(
778+
{
779+
"constant": ["fixed_value", "fixed_value", "fixed_value"],
780+
"x": [1, 2, 3],
781+
"y": [10, 20, 30],
782+
}
783+
)
784+
assert expected.equals(actual)

0 commit comments

Comments
 (0)