JSON Utilities¶
The hera.utils.jsonutils module provides functions for serializing configurations that contain physical units, comparing JSON structures side by side, generating parameter sweeps, and manipulating values by JSON path.
Importing¶
from hera.utils import (
ConfigurationToJSON,
JSONToConfiguration,
JSONVariations,
compareJSONS,
setJSONPath,
)
ConfigurationToJSON -- Serialize Configs with Units¶
Convert a Python dict (which may contain pint Quantity values) into a JSON-safe dict where every value is a string or plain number.
from hera.utils import ureg, ConfigurationToJSON
config = {
"solver": {
"velocity": 10.0 * ureg.m / ureg.s,
"iterations": 500,
}
}
# Basic serialization -- units become strings
json_dict = ConfigurationToJSON(config)
# {'solver': {'velocity': '10.0 meter / second', 'iterations': 500}}
Splitting Units for Queryable Storage¶
When splitUnits=True, each quantity is expanded into a sub-dict with magnitude, units, and value keys (magnitude and units describe the quantity after optional standardization; value is the original quantity string and is omitted when keepOriginalUnits=False). This is useful when you need to query by numeric magnitude (e.g., in MongoDB with $lt / $gt):
json_dict = ConfigurationToJSON(config, splitUnits=True)
# {'solver': {
# 'velocity': {
# 'magnitude': 10.0,
# 'units': '10.0 meter / second',
# 'value': '10.0 meter / second'
# },
# 'iterations': 500
# }}
Set standardize=True to convert all quantities to MKS base units before serializing. Legacy unum values are converted to pint first.
JSONToConfiguration -- Restore Units¶
Convert a serialized dict back into a configuration with live pint Quantity objects:
from hera.utils import JSONToConfiguration
restored = JSONToConfiguration(json_dict)
print(restored["solver"]["velocity"]) # 10.0 meter / second
Strings that pint can parse (such as '10.0 meter / second') become Quantity objects; other strings, numbers and booleans are returned unchanged. A {magnitude, units, value} sub-dict produced by splitUnits=True is turned back into a single Quantity.
Options:
returnStandardize=True-- for split-unit sub-dicts, rebuild the quantity fromunits(the standardized form) instead ofvalue.returnUnum=True-- return unum objects instead of pint (legacy support; needsunum).
stripConfigurationUnits -- Keep Only Magnitudes¶
stripConfigurationUnits(config, returnStandardize=False, ignoreStandardization=[]) replaces every quantity with its magnitude. With returnStandardize=True the quantities are converted to MKS first, except under keys listed in ignoreStandardization:
from hera.utils import ureg, stripConfigurationUnits
stripConfigurationUnits({"speed": 36 * ureg.km / ureg.hour}, returnStandardize=True)
# {'speed': 10.0}
JSONVariations -- Parameter Sweeps¶
Generate the Cartesian product of parameter variations applied to a base configuration. Each "variation group" is a dict mapping JSON paths to lists of values. Parameters within one group change together (all lists in a group must have the same length, otherwise ValueError is raised); the Cartesian product is taken across groups. JSONVariations is a generator that yields a new deep copy of the base for each combination.
from hera.utils import JSONVariations
base = {
"mesh": {"resolution": 100, "layers": 5},
"physics": {"viscosity": 0.01},
}
variations = [
# Group 1: resolution and layers change together
{"mesh.resolution": [100, 200], "mesh.layers": [5, 10]},
# Group 2: viscosity varies independently
{"physics.viscosity": [0.01, 0.001, 0.0001]},
]
for variant in JSONVariations(base, variations):
res = variant["mesh"]["resolution"]
visc = variant["physics"]["viscosity"]
print(f"resolution={res}, viscosity={visc}")
# resolution=100, viscosity=0.01
# resolution=100, viscosity=0.001
# resolution=100, viscosity=0.0001
# resolution=200, viscosity=0.01
# resolution=200, viscosity=0.001
# resolution=200, viscosity=0.0001
compareJSONS -- Side-by-Side Comparison¶
Compare multiple JSON configurations and see only the fields that differ:
from hera.utils import compareJSONS
config_a = {"solver": {"dt": 0.1, "maxIter": 100}}
config_b = {"solver": {"dt": 0.2, "maxIter": 100}}
diff = compareJSONS(run1=config_a, run2=config_b)
print(diff)
# Shows a DataFrame with solver.dt differing between run1 and run2
The JSONs are flattened with convertJSONtoPandas (list items become name_0, name_1, ...) and compared with compareDataframeConfigurations. Pass longFormat=True to get long-format output. Use changeDotToUnderscore=True to make the parameter names compatible with pandas.query().
diffJSONS -- JSON Patch Between Two Documents¶
For exactly two documents, diffJSONS(source, target) returns the RFC 6902 JSON Patch that turns source into target (an empty list when they are equal):
from hera.utils import diffJSONS
diffJSONS({"a": 1}, {"a": 2})
# [{'op': 'replace', 'path': '/a', 'value': 2}]
diffJSONS imports the jsonpatch package, which is not pinned in requirements.txt or setup.py (it usually arrives as a transitive dependency); if it is missing, run pip install jsonpatch.
loadJSON and convertJSONtoPandas¶
loadJSON(jsonData)accepts a file path, a JSON string, a file-like object, a dict or a list and returns the loaded object.convertJSONtoPandas(jsonData, nameColumn="parameterNameFullPath", valueColumn="value")flattens a JSON (path, string or dict) into a two-column DataFrame of JSON paths and values.
setJSONPath -- Modify Nested Values by Path¶
Set values deep inside a nested dict using dot-separated JSON paths:
from hera.utils import setJSONPath
base = {"a": {"b": 1}, "c": {"d": 2}}
updated = setJSONPath(base, {"a.b": 99})
print(updated)
# {'a': {'b': 99}, 'c': {'d': 2}}
The original dict is not modified unless you pass inPlace=True. Paths may be given with or without the leading $.. Each path must already exist in base; a missing path raises IndexError.