Skip to content

Commit c476da5

Browse files
authored
Merge branch 'main' into updated_color_correction_tutorial
2 parents 565f079 + 7c7b4ec commit c476da5

File tree

4 files changed

+127
-38
lines changed

4 files changed

+127
-38
lines changed

docs/outputs.md

+15-15
Original file line numberDiff line numberDiff line change
@@ -48,32 +48,29 @@ Methods are accessed as plantcv.outputs.*method*.
4848

4949
* scale: Units of the measurement or a scale in which the observations are expressed; if possible, standard units and scales should be used and mapped to existing ontologies; in case of a non-standard scale a full explanation should be given.
5050

51-
* datatype: The type of data to be stored. In JSON, values must be one of the following data types:
52-
- a string
53-
- a number
54-
- an array
55-
- a boolean
56-
- null
57-
- a JSON object
58-
59-
They are equilvalent to python data types of the following:
60-
- 'str'
61-
- 'int' or 'float'
62-
- 'list' or 'tuple'
63-
- 'bool'
64-
- 'NoneType'
65-
- 'dict'
51+
* datatype: The type of data to be stored. See note below for supported data types.
6652

6753
* value: The data itself. Make sure the data type of value matches the data type stated in "datatype".
6854

6955
* label: The label for each value, which will be useful when the data is a frequency table (e.g. hues).
7056

57+
**add_metadata**(*term, datatype, value*): Add metadata about the image or other information
58+
59+
* term: Metadata term/name
60+
61+
* datatype: The type of data to be stored. See note below for supported data types.
62+
63+
* value: The data itself. Make sure the data type of value matches the data type stated in "datatype".
64+
7165
**save_results**(*filename, outformat="json"*): Save results to a file
7266

7367
* filename: Path and name of the output file
7468

7569
* outformat: Output file format (default = "json"). Supports "json" and "csv" formats
7670

71+
!!!note
72+
Supported data types for JSON output are: int, float, str, list, bool, tuple, dict, NoneType, numpy.float64.
73+
7774
**Example use:**
7875
- [Use In VIS/NIR Tutorial](tutorials/vis_nir_tutorial.md)
7976

@@ -119,6 +116,9 @@ pcv.outputs.add_observation(sample='default', variable='percent_diseased',
119116
method='ratio of pixels', scale='percent', datatype=float,
120117
value=percent_diseased, label='percent')
121118

119+
# Add metadata
120+
pcv.outputs.add_metadata(term="genotype", datatype=str, value="wildtype")
121+
122122
# Write custom data to results file
123123
pcv.outputs.save_results(filename=args.result, outformat="json")
124124

docs/updating.md

+5
Original file line numberDiff line numberDiff line change
@@ -731,6 +731,11 @@ pages for more details on the input and output variable types.
731731
* post v3.3: **plantcv.outputs.add_observation**(*variable, trait, method, scale, datatype, value, label*)
732732
* post v3.11: **plantcv.outputs.add_observation**(*sample, variable, trait, method, scale, datatype, value, label*)
733733

734+
#### plantcv.outputs.add_metadata
735+
736+
* pre v4.1: NA
737+
* post v4.1: **plantcv.outputs.add_metadata**(*term, datatype, value*)
738+
734739
#### plantcv.outputs.clear
735740

736741
* pre v3.2: NA

plantcv/plantcv/classes.py

+80-23
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,14 @@ def __init__(self):
7070
self.measurements = {}
7171
self.images = []
7272
self.observations = {}
73+
self.metadata = {}
7374

7475
# Add a method to clear measurements
7576
def clear(self):
7677
self.measurements = {}
7778
self.images = []
7879
self.observations = {}
80+
self.metadata = {}
7981

8082
# Method to add observation to outputs
8183
def add_observation(self, sample, variable, trait, method, scale, datatype, value, label):
@@ -108,16 +110,8 @@ def add_observation(self, sample, variable, trait, method, scale, datatype, valu
108110
if sample not in self.observations:
109111
self.observations[sample] = {}
110112

111-
# Supported data types
112-
supported_dtype = ["int", "float", "str", "list", "bool", "tuple", "dict", "NoneType", "numpy.float64"]
113-
# Supported class types
114-
class_list = [f"<class '{cls}'>" for cls in supported_dtype]
115-
116-
# Send an error message if datatype is not supported by json
117-
if str(type(value)) not in class_list:
118-
# String list of supported types
119-
type_list = ', '.join(map(str, supported_dtype))
120-
fatal_error(f"The Data type {type(value)} is not compatible with JSON! Please use only these: {type_list}!")
113+
# Validate that the data type is supported by JSON
114+
_ = _validate_data_type(value)
121115

122116
# Save the observation for the sample and variable
123117
self.observations[sample][variable] = {
@@ -129,6 +123,32 @@ def add_observation(self, sample, variable, trait, method, scale, datatype, valu
129123
"label": label
130124
}
131125

126+
# Method to add metadata instance to outputs
127+
def add_metadata(self, term, datatype, value):
128+
"""Add a metadata term and value to outputs.
129+
130+
Parameters
131+
----------
132+
term : str
133+
Metadata term/name.
134+
datatype : type
135+
The type of data to be stored, e.g. 'int', 'float', 'str', 'list', 'bool', etc.
136+
value : any
137+
The data itself.
138+
"""
139+
# Create an empty dictionary for the sample if it does not exist
140+
if term not in self.metadata:
141+
self.metadata[term] = {}
142+
143+
# Validate that the data type is supported by JSON
144+
_ = _validate_data_type(value)
145+
146+
# Save the observation for the sample and variable
147+
self.metadata[term] = {
148+
"datatype": str(datatype),
149+
"value": value
150+
}
151+
132152
# Method to save observations to a file
133153
def save_results(self, filename, outformat="json"):
134154
"""Save results to a file.
@@ -145,16 +165,26 @@ def save_results(self, filename, outformat="json"):
145165
with open(filename, 'r') as f:
146166
hierarchical_data = json.load(f)
147167
hierarchical_data["observations"] = self.observations
168+
existing_metadata = hierarchical_data["metadata"]
169+
for term in self.metadata:
170+
save_term = term
171+
if term in existing_metadata:
172+
save_term = f"{term}_1"
173+
hierarchical_data["metadata"][save_term] = self.metadata[term]
148174
else:
149-
hierarchical_data = {"metadata": {}, "observations": self.observations}
150-
175+
hierarchical_data = {"metadata": self.metadata, "observations": self.observations}
151176
with open(filename, mode='w') as f:
152177
json.dump(hierarchical_data, f)
178+
153179
elif outformat.upper() == "CSV":
154180
# Open output CSV file
155181
csv_table = open(filename, "w")
182+
# Gather any additional metadata
183+
metadata_key_list = list(self.metadata.keys())
184+
metadata_val_list = [val["value"] for val in self.metadata.values()]
156185
# Write the header
157-
csv_table.write(",".join(map(str, ["sample", "trait", "value", "label"])) + "\n")
186+
header = metadata_key_list + ["sample", "trait", "value", "label"]
187+
csv_table.write(",".join(map(str, header)) + "\n")
158188
# Iterate over data samples
159189
for sample in self.observations:
160190
# Iterate over traits for each sample
@@ -168,23 +198,18 @@ def save_results(self, filename, outformat="json"):
168198
# Skip list of tuple data types
169199
if not isinstance(value, tuple):
170200
# Save one row per value-label
171-
row = [sample, var, value, label]
201+
row = metadata_val_list + [sample, var, value, label]
172202
csv_table.write(",".join(map(str, row)) + "\n")
173203
# If the data type is Boolean, store as a numeric 1/0 instead of True/False
174204
elif isinstance(val, bool):
175-
row = [sample,
176-
var,
177-
int(self.observations[sample][var]["value"]),
178-
self.observations[sample][var]["label"]]
205+
row = metadata_val_list + [sample, var, int(self.observations[sample][var]["value"]),
206+
self.observations[sample][var]["label"]]
179207
csv_table.write(",".join(map(str, row)) + "\n")
180208
# For all other supported data types, save one row per trait
181209
# Assumes no unusual data types are present (possibly a bad assumption)
182210
else:
183-
row = [sample,
184-
var,
185-
self.observations[sample][var]["value"],
186-
self.observations[sample][var]["label"]
187-
]
211+
row = metadata_val_list + [sample, var, self.observations[sample][var]["value"],
212+
self.observations[sample][var]["label"]]
188213
csv_table.write(",".join(map(str, row)) + "\n")
189214

190215
def plot_dists(self, variable):
@@ -233,6 +258,38 @@ def plot_dists(self, variable):
233258
return chart
234259

235260

261+
def _validate_data_type(data):
262+
"""Validate that the data type is supported by JSON.
263+
264+
Parameters
265+
----------
266+
data : any
267+
Data to be validated.
268+
269+
Returns
270+
-------
271+
bool
272+
True if the data type is supported by JSON.
273+
274+
Raises
275+
------
276+
ValueError
277+
If the data type is not supported by JSON.
278+
"""
279+
# Supported data types
280+
supported_dtype = ["int", "float", "str", "list", "bool", "tuple", "dict", "NoneType", "numpy.float64"]
281+
# Supported class types
282+
class_list = [f"<class '{cls}'>" for cls in supported_dtype]
283+
284+
# Send an error message if datatype is not supported by json
285+
if str(type(data)) not in class_list:
286+
# String list of supported types
287+
type_list = ', '.join(map(str, supported_dtype))
288+
fatal_error(f"The Data type {type(data)} is not compatible with JSON! Please use only these: {type_list}!")
289+
290+
return True
291+
292+
236293
class Spectral_data:
237294
"""PlantCV Hyperspectral data class"""
238295

tests/plantcv/test_outputs.py

+27
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ def test_save_results_json_newfile(tmpdir):
3535
outputs = Outputs()
3636
outputs.add_observation(sample='default', variable='test', trait='test variable', method='test', scale='none',
3737
datatype=str, value="test", label="none")
38+
outputs.add_metadata(term="add_date", datatype="str", value="Nov-14-2023")
3839
outputs.save_results(filename=outfile, outformat="json")
3940
with open(outfile, "r") as fp:
4041
results = json.load(fp)
@@ -51,6 +52,8 @@ def test_save_results_json_existing_file(test_data, tmpdir):
5152
outputs = Outputs()
5253
outputs.add_observation(sample='default', variable='test', trait='test variable', method='test', scale='none',
5354
datatype=str, value="test", label="none")
55+
outputs.add_metadata(term="add_date", datatype="str", value="Nov-14-2023")
56+
outputs.add_metadata(term="camera", datatype="str", value="TV")
5457
outputs.save_results(filename=outfile, outformat="json")
5558
with open(outfile, "r") as fp:
5659
results = json.load(fp)
@@ -81,6 +84,30 @@ def test_save_results_csv(test_data, tmpdir):
8184
assert results == test_results
8285

8386

87+
def test_save_results_csv_add_metadata(tmpdir):
88+
"""Test for PlantCV."""
89+
# Create a test tmp directory
90+
outfile = tmpdir.mkdir("cache").join("results.csv")
91+
# Create output instance
92+
outputs = Outputs()
93+
outputs.add_observation(sample='default', variable='string', trait='string variable', method='string', scale='none',
94+
datatype=str, value="string", label="none")
95+
outputs.add_metadata(term="add_date", datatype="str", value="Nov-14-2023")
96+
outputs.save_results(filename=outfile, outformat="csv")
97+
with open(outfile, "r") as fp:
98+
results = fp.read()
99+
x = slice(0, 33)
100+
assert results[x] == "add_date,sample,trait,value,label"
101+
102+
103+
def test_add_metadata_invalid_type():
104+
"""Test for PlantCV."""
105+
# Create output instance
106+
outputs = Outputs()
107+
with pytest.raises(RuntimeError):
108+
outputs.add_metadata(term="bad_dtype", datatype="str", value=np.array([2]))
109+
110+
84111
def test_clear_outputs():
85112
"""Test for PlantCV."""
86113
# Create output instance

0 commit comments

Comments
 (0)