Skip to content

Commit

Permalink
Improve outputs
Browse files Browse the repository at this point in the history
  • Loading branch information
astewartau committed Nov 15, 2024
1 parent 08c2f9f commit 0b1ff09
Show file tree
Hide file tree
Showing 8 changed files with 76 additions and 50 deletions.
8 changes: 7 additions & 1 deletion dcm_check/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,16 @@ def main():
sys.exit(1)

in_dicom_values = load_dicom(args.in_file)
results = get_compliance_summary(reference_model, in_dicom_values)
results = get_compliance_summary(reference_model, in_dicom_values, args.scan, args.group)

df = pd.DataFrame(results)

# remove "Acquisition" and/or "Group" columns if they are empty
if "Acquisition" in df.columns and df["Acquisition"].isnull().all():
df.drop(columns=["Acquisition"], inplace=True)
if "Group" in df.columns and df["Group"].isnull().all():
df.drop(columns=["Group"], inplace=True)

if len(df) == 0:
print("DICOM file is compliant with the reference model.")
else:
Expand Down
12 changes: 7 additions & 5 deletions dcm_check/dcm_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ def load_ref_pydantic(module_path: str, scan_type: str) -> BaseModel:

return reference_model

def get_compliance_summary(reference_model: BaseModel, dicom_values: Dict[str, Any], model_name: str = "Reference Model", raise_errors: bool = False) -> List[Dict[str, Any]]:
def get_compliance_summary(reference_model: BaseModel, dicom_values: Dict[str, Any], acquisition: str = None, group: str = None, raise_errors: bool = False) -> List[Dict[str, Any]]:
"""Validate a DICOM file against the reference model."""
compliance_summary = []

Expand All @@ -204,13 +204,15 @@ def get_compliance_summary(reference_model: BaseModel, dicom_values: Dict[str, A
for error in e.errors():
param = error['loc'][0] if error['loc'] else "Model-Level Error"
expected = (error['ctx'].get('expected') if 'ctx' in error else None) or error['msg']
if isinstance(expected, str) and expected.startswith("'") and expected.endswith("'"):
expected = expected[1:-1]
actual = dicom_values.get(param, "N/A") if param != "Model-Level Error" else "N/A"
compliance_summary.append({
"Model_Name": model_name,
"Acquisition": acquisition,
"Group": group,
"Parameter": param,
"Expected": expected,
"Actual": actual,
"Pass": False
"Value": actual,
"Expected": expected
})

return compliance_summary
Expand Down
4 changes: 2 additions & 2 deletions dcm_check/generate_json_ref.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def generate_json_ref(in_session_dir, out_json_ref, acquisition_fields, referenc
# Remove constant fields from the groups and only include changing fields
groups = []
group_number = 1
for group, example_path in unique_groups.items():
for group, ref_path in unique_groups.items():
group_fields = [
{"field": field, "value": value}
for field, value in group if field not in constant_reference_fields
Expand All @@ -92,7 +92,7 @@ def generate_json_ref(in_session_dir, out_json_ref, acquisition_fields, referenc
groups.append({
"name": f"Group {group_number}", # Assign default name
"fields": group_fields,
"example": example_path
"ref": ref_path
})
group_number += 1

Expand Down
48 changes: 40 additions & 8 deletions dcm_check/session_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import argparse
import pandas as pd
from tabulate import tabulate

from dcm_check import load_ref_json, load_dicom, get_compliance_summary, read_session

def get_compliance_summaries_json(json_ref: str, in_session: str, output_json: str = "compliance_report.json") -> pd.DataFrame:
Expand All @@ -21,7 +20,7 @@ def get_compliance_summaries_json(json_ref: str, in_session: str, output_json: s
"""
# Step 1: Identify matched acquisitions and groups in the session
session_df = read_session(json_ref, in_session)
compliance_results = []
grouped_compliance = {}

# Step 2: Iterate over each matched acquisition-group pair
for _, row in session_df.dropna(subset=["Acquisition"]).iterrows():
Expand All @@ -37,17 +36,51 @@ def get_compliance_summaries_json(json_ref: str, in_session: str, output_json: s
dicom_values = load_dicom(first_dicom_path)

# Step 5: Run compliance check and gather results
compliance_summary = get_compliance_summary(reference_model, dicom_values, model_name=f"{acquisition}-{group}")
compliance_results.extend(compliance_summary)
compliance_summary = get_compliance_summary(reference_model, dicom_values, acquisition, group)
print(compliance_summary)

# Organize results in nested format without "Model_Name"
if acquisition not in grouped_compliance:
grouped_compliance[acquisition] = {"Acquisition": acquisition, "Groups": []}

if group:
group_entry = next((g for g in grouped_compliance[acquisition]["Groups"] if g["Name"] == group), None)
if not group_entry:
group_entry = {"Name": group, "Parameters": []}
grouped_compliance[acquisition]["Groups"].append(group_entry)
for entry in compliance_summary:
entry.pop("Acquisition", None)
entry.pop("Group", None)
group_entry["Parameters"].extend(compliance_summary)
else:
# If no group, add parameters directly under acquisition
for entry in compliance_summary:
entry.pop("Acquisition", None)
entry.pop("Group", None)
grouped_compliance[acquisition]["Parameters"] = compliance_summary

except Exception as e:
print(f"Error processing acquisition '{acquisition}' and group '{group}': {e}")

# Step 6: Save compliance summary to JSON and return as DataFrame
# Convert the grouped data to a list for JSON serialization
grouped_compliance_list = list(grouped_compliance.values())

# Save grouped compliance summary to JSON
with open(output_json, "w") as json_file:
json.dump(compliance_results, json_file, indent=4)
json.dump(grouped_compliance_list, json_file, indent=4)

# Convert the compliance summary to a DataFrame for tabulated output
compliance_df = pd.json_normalize(
grouped_compliance_list,
record_path=["Groups", "Parameters"],
meta=["Acquisition", ["Groups", "Name"]],
errors="ignore"
)

# Rename "Groups.name" to "Group" and reorder columns
compliance_df.rename(columns={"Groups.Name": "Group"}, inplace=True)
compliance_df = compliance_df[["Acquisition", "Group", "Parameter", "Value", "Expected"]]

compliance_df = pd.DataFrame(compliance_results)
return compliance_df

def main():
Expand All @@ -65,4 +98,3 @@ def main():

if __name__ == "__main__":
main()

30 changes: 12 additions & 18 deletions dcm_check/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,12 @@ def test_cli_output_file_not_compliant_with_group():
result = subprocess.run(command, capture_output=True, text=True)

expected_output = tabulate(pd.DataFrame({ # as above
"Model_Name": ["Reference Model"],
"Acquisition": ["T1"],
"Group": ["Group 1"],
"Parameter": ["ImageType"],
"Expected": ["Value error, ImageType must contain 'M'"],
"Actual": [['ORIGINAL', 'PRIMARY', 'P', 'N']],
"Pass": [False]
"Value": [['ORIGINAL', 'PRIMARY', 'P', 'N']],
"Expected": ["Value error, ImageType must contain 'M'"]
}), headers="keys", tablefmt="simple")

print(expected_output)

assert result.returncode == 0
assert expected_output in result.stdout
Expand Down Expand Up @@ -162,11 +160,9 @@ def test_cli_dicom_reference_non_compliant():
result = subprocess.run(command, capture_output=True, text=True)

expected_output = tabulate(pd.DataFrame({
"Model_Name": ["Reference Model"],
"Parameter": ["FlipAngle"],
"Expected": [15],
"Actual": [45],
"Pass": [False]
"Value": [45],
"Expected": [15]
}), headers="keys", tablefmt="simple")

# delete the non-compliant DICOM file
Expand All @@ -181,11 +177,10 @@ def test_cli_pydantic_reference():
result = subprocess.run(command, capture_output=True, text=True)

expected_output = tabulate(pd.DataFrame({
"Model_Name": ["Reference Model", "Reference Model", "Reference Model", "Reference Model"],
"Acquisition": ["T1_MPR", "T1_MPR", "T1_MPR", "T1_MPR"],
"Parameter": ["MagneticFieldStrength", "RepetitionTime", "PixelSpacing", "SliceThickness"],
"Expected": ["Field required", "Input should be greater than or equal to 2300", "Value error, Each value in PixelSpacing must be between 0.75 and 0.85", "Input should be less than or equal to 0.85"],
"Actual": ["N/A", 8.0, ['0.5', '0.5'], 1.0],
"Pass": [False, False, False, False]
"Value": ["N/A", 8.0, ['0.5', '0.5'], 1.0],
"Expected": ["Field required", "Input should be greater than or equal to 2300", "Value error, Each value in PixelSpacing must be between 0.75 and 0.85", "Input should be less than or equal to 0.85"]
}), headers="keys", tablefmt="simple")

assert result.returncode == 0
Expand All @@ -197,11 +192,10 @@ def test_cli_pydantic_reference_inferred_type():
result = subprocess.run(command, capture_output=True, text=True)

expected_output = tabulate(pd.DataFrame({
"Model_Name": ["Reference Model", "Reference Model", "Reference Model", "Reference Model"],
"Acquisition": ["T1_MPR", "T1_MPR", "T1_MPR", "T1_MPR"],
"Parameter": ["MagneticFieldStrength", "RepetitionTime", "PixelSpacing", "SliceThickness"],
"Expected": ["Field required", "Input should be greater than or equal to 2300", "Value error, Each value in PixelSpacing must be between 0.75 and 0.85", "Input should be less than or equal to 0.85"],
"Actual": ["N/A", 8.0, ['0.5', '0.5'], 1.0],
"Pass": [False, False, False, False]
"Value": ["N/A", 8.0, ['0.5', '0.5'], 1.0],
"Expected": ["Field required", "Input should be greater than or equal to 2300", "Value error, Each value in PixelSpacing must be between 0.75 and 0.85", "Input should be less than or equal to 0.85"]
}), headers="keys", tablefmt="simple")

assert result.returncode == 0
Expand Down
18 changes: 6 additions & 12 deletions dcm_check/tests/test_ref_dicom.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,7 @@ def test_dicom_compliance_specific_fields_non_compliant(t1):
assert len(compliance_summary) == 1
assert compliance_summary[0]["Parameter"] == "RepetitionTime"
assert compliance_summary[0]["Expected"] == "8.0"
assert compliance_summary[0]["Actual"] == 8.1
assert not compliance_summary[0]["Pass"]
assert compliance_summary[0]["Value"] == 8.1

def test_dicom_compliance_small_change(t1):
t1_values = dcm_check.get_dicom_values(t1)
Expand All @@ -102,8 +101,7 @@ def test_dicom_compliance_small_change(t1):
assert len(compliance_summary) == 1
assert compliance_summary[0]["Parameter"] == "RepetitionTime"
assert compliance_summary[0]["Expected"] == "8.0"
assert compliance_summary[0]["Actual"] == 8.1
assert not compliance_summary[0]["Pass"]
assert compliance_summary[0]["Value"] == 8.1

def test_dicom_compliance_num_errors(t1):
t1_values = dcm_check.get_dicom_values(t1)
Expand Down Expand Up @@ -131,12 +129,9 @@ def test_dicom_compliance_error_message(t1):
assert errors[0]["Expected"] == "8.0"
assert errors[1]["Expected"] == "3.0"
assert errors[2]["Expected"] == "400.0"
assert errors[0]["Actual"] == 8.1
assert errors[1]["Actual"] == 3.1
assert errors[2]["Actual"] == 400.1
assert not errors[0]["Pass"]
assert not errors[1]["Pass"]
assert not errors[2]["Pass"]
assert errors[0]["Value"] == 8.1
assert errors[1]["Value"] == 3.1
assert errors[2]["Value"] == 400.1

def test_dicom_compliance_error_message_missing_field(t1):
t1_values = dcm_check.get_dicom_values(t1)
Expand All @@ -151,8 +146,7 @@ def test_dicom_compliance_error_message_missing_field(t1):
assert len(errors) == 1
assert errors[0]["Parameter"] == "RepetitionTime"
assert errors[0]["Expected"] == "Field required"
assert errors[0]["Actual"] == "N/A"
assert not errors[0]["Pass"]
assert errors[0]["Value"] == "N/A"

def test_dicom_compliance_error_raise(t1):
t1_values = dcm_check.get_dicom_values(t1)
Expand Down
3 changes: 1 addition & 2 deletions dcm_check/tests/test_ref_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,7 @@ def test_json_compliance_outside_tolerance_with_dcm(json_ref_with_dcm, dicom_tes
assert len(compliance_summary) == 1
assert compliance_summary[0]["Parameter"] == "EchoTime"
assert compliance_summary[0]["Expected"] == "Input should be less than or equal to 3.1"
assert compliance_summary[0]["Actual"] == 3.2
assert not compliance_summary[0]["Pass"]
assert compliance_summary[0]["Value"] == 3.2

def test_json_compliance_pattern_match(json_ref_no_dcm, dicom_test_file):
"""Test compliance with a pattern match for SeriesDescription within group."""
Expand Down
3 changes: 1 addition & 2 deletions dcm_check/tests/test_ref_pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,7 @@ def test_t1_mpr_repetition_vs_echo_rule(t1_mpr_dicom_values):
assert len(compliance_summary) > 0
assert compliance_summary[0]["Parameter"] == "Model-Level Error"
assert compliance_summary[0]["Expected"] == "RepetitionTime must be at least 2x EchoTime"
assert compliance_summary[0]["Pass"] == False
assert compliance_summary[0]["Actual"] == "N/A"
assert compliance_summary[0]["Value"] == "N/A"

def test_diffusion_config_compliance():
"""Test DiffusionConfig compliance for a sample diffusion scan."""
Expand Down

0 comments on commit 0b1ff09

Please sign in to comment.