MRD Upgrade tool#66
Conversation
There was a problem hiding this comment.
Pull request overview
Introduces a Python tool (mrd-upgrade) plus library API to upgrade existing MRD binary files from older schema versions (initially v2.2.0) to the current schema (v2.2.1). Also refactors how the MRD version string is propagated through builds (reading the top-level VERSION file directly instead of relying on the MRD_VERSION_STRING env var) and includes an unrelated refactor of mrd_to_ismrmrd.py.
Changes:
- New upgrade tool:
python/mrd/tools/{upgrade.py,_schema_registry.py,_v220_reader.py}with CLI entry point, version detection, v2.2.0 read-only serializers, and a chained upgrade dispatcher. - End-to-end test harness under
test/upgrade/(shell driver + v2.2.0 generator + verifier) and ajust upgrade-testrecipe. - Version-string plumbing reworked:
setup.py, conda recipes, CMake, Matlab, justfile, Dockerfile, devcontainer and CI now derive the version from theVERSIONfile /_version.py; theMRD_VERSION_STRINGenv var is removed.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| UPGRADE.md | Design/rationale document for the upgrade tool. |
| docs/python/upgrade.md | User-facing docs for mrd-upgrade. |
| docs/.vitepress/config.mts | Adds upgrade page to docs sidebar. |
| python/pyproject.toml | Adds mrd-upgrade script entry; bumps ismrmrd dep. |
| python/setup.py | Reads version from mrd/_version.py instead of env var. |
| python/mrd/_version.py | Generated version literal (__version__ = "2.2.1"). |
| python/mrd/tools/upgrade.py | CLI + upgrade_mrd_file public API and chained-step dispatcher. |
| python/mrd/tools/_schema_registry.py | Embedded v2.2.0 schema literal and identify_file_version. |
| python/mrd/tools/_v220_reader.py | v2.2.0 read-only serializers producing v2.2.1 objects. |
| python/mrd/tools/mrd_to_ismrmrd.py | Refactor to use constructor kwargs; unrelated to upgrade tool. |
| python/conda/meta.yaml, cpp/conda/meta.yaml | Read version from VERSION via load_file_regex. |
| cpp/CMakeLists.txt | Prefer PKG_VERSION then VERSION file; remove env-var path. |
| matlab/buildToolbox.m | Read version from VERSION file. |
| docker/Dockerfile, docker/build-images.sh | Drop MRD_VERSION_STRING build-arg; copy VERSION into image. |
| justfile | Add upgrade-test; generate _version.py; drop env var. |
| environment.yml | Add ismrmrd-python>=1.15.0; drop pip ismrmrd==1.14.2. |
| .readthedocs.yaml, .devcontainer/devcontainer.bashrc, .github/workflows/mrd_build.yml | Remove env-var setup; fetch tags in CI for git archive v2.2.0. |
| test/upgrade/test-upgrade.sh | End-to-end test driver. |
| test/upgrade/generate_v220.py | Produces a v2.2.0 stream under the v2.2.0 codecs. |
| test/upgrade/verify_upgrade.py | Validates the upgraded v2.2.1 file. |
Comments suppressed due to low confidence (11)
python/mrd/tools/mrd_to_ismrmrd.py:107
- Behavior change: previously the code only set
meas.patientPositionifmeas_info.patient_position in pos_map. Nowpos_map[meas_info.patient_position]is accessed unconditionally, which will raiseKeyErrorif aPatientPositionenum value is ever added to MRD but not yet mirrored inpos_map. Consider usingpos_map.get(...)with a sensible default (as you did for trajectory at line 266) or restore the membership guard.
meas = ismrmrd.xsd.measurementInformationType(
patientPosition=pos_map[meas_info.patient_position]
)
python/mrd/tools/mrd_to_ismrmrd.py:290
- The previous code guarded all four attribute accesses (
if par_img.acceleration_factor, plus per-fieldis not Nonechecks). The new code unconditionally accessespar_img.acceleration_factor.kspace_encoding_step_1and.kspace_encoding_step_2. Ifpar_img.acceleration_factorisNone(which can occur if the MRD producer omitted it even though the schema marks it required), this will raiseAttributeError. Also, previously aNonekspace_encoding_step_*was skipped; nowNoneis forwarded intoaccelerationFactorType, which may not accept it.
acc = ismrmrd.xsd.accelerationFactorType(
kspace_encoding_step_1=par_img.acceleration_factor.kspace_encoding_step_1,
kspace_encoding_step_2=par_img.acceleration_factor.kspace_encoding_step_2
)
parallel = ismrmrd.xsd.parallelImagingType(accelerationFactor=acc)
python/mrd/tools/mrd_to_ismrmrd.py:205
limitsis now constructed and assigned toenc.encodingLimitsunconditionally, even whenenc_mrd.encoding_limitsis falsy. Previously the field was only set when limits were actually present. This is likely benign but is a behavior change: the resulting ISMRMRD XML will now always contain an (empty)encodingLimitselement. Confirm this is intended.
limits = ismrmrd.xsd.encodingLimitsType()
if enc_mrd.encoding_limits:
python/mrd/tools/mrd_to_ismrmrd.py:156
- Behavior change: previously
coilNumber/coilNamewere only assigned when non-None. Now they're passed in directly. Iflabel.coil_numberisNone(the MRD type may permit it depending on producer behavior), thecoilLabelTypeconstructor may rejectNonefor an integer field. Consider preserving the previous guarded assignment.
coil = ismrmrd.xsd.coilLabelType(
coilNumber=label.coil_number,
coilName=label.coil_name
)
python/mrd/tools/_schema_registry.py:57
identify_file_versionreads the 4-byte binary format version withstream.read_view(4)but never validates it.BinaryProtocolReader.__init__does checkversion != CURRENT_BINARY_FORMAT_VERSION. As a result, a file written with a future incompatible Yardl binary format could still be misidentified by schema string match without raising a clear error. Consider validating the format version here too (or document why it's intentionally skipped).
_fmt_version = stream.read_view(4)
schema = string_serializer.read(stream)
python/mrd/tools/upgrade.py:158
- When
--in-placeis used, the upgraded file is created viatempfile.mkstemp(which creates the file with mode 0600) and thenos.replaced over the source. This means an in-place upgrade will silently strip the source file's original permissions (e.g. world-readable scans become user-only). Consider copying the source'sst_mode(and possiblyst_uid/st_gidif running as root) to the temp file before the rename, or document this side effect.
# Write to a temp file next to the source, then atomically replace.
src_dir = os.path.dirname(os.path.abspath(src))
fd, tmp_path = tempfile.mkstemp(dir=src_dir, suffix=".tmp")
os.close(fd)
try:
upgrade_mrd_file(src, tmp_path)
os.replace(tmp_path, src)
python/mrd/tools/upgrade.py:169
- When
args.outputis not provided and--in-placeis not used, the destination is computed assrc + ".upgraded". If that file already exists it is silently overwritten. Consider checkingos.path.exists(dst)and refusing (or requiring--force) to avoid clobbering an existing file.
dst = args.output if args.output else src + ".upgraded"
upgrade_mrd_file(src, dst)
print(f"Upgraded {src!r} → {dst!r}")
test/upgrade/test-upgrade.sh:24
git archive v2.2.0 python/mrd/requires thev2.2.0tag to be present in the local clone. CI fetches withfetch-depth: 0andfetch-tags: true(added in this PR), but developers runningjust upgrade-teston a shallow clone, orbash test/upgrade/test-upgrade.shdirectly, will get a confusingfatal: not a valid object nameerror. Consider an upfront check (git rev-parse v2.2.0^{tag}) with a clearer message, or auto-fetching the tag if missing.
# Extract all v2.2.0 files from git.
git -C "$WORKSPACE" archive v2.2.0 python/mrd/ | tar -x --strip-components=2 -C "$V220_MOD"
test/upgrade/test-upgrade.sh:40
test-upgrade.shrelies on the side effect thatgit archive ... | tar -x --strip-components=2 -C "$V220_MOD"extracts the v2.2.0 tree into$V220_MOD(which is.../mrd_v220/mrd). Because the v2.2.0 tag predates this PR, the extracted tree won't containmrd/tools/— sofrom mrd.tools._schema_registry import identify_file_version(used in the inline Python snippet on lines 32-40) only works because thepython3interpreter that runs that snippet usesPYTHONPATH=${WORKSPACE}/python:...(line 14), wheretools/exists. This is subtle and easy to break later ifPYTHONPATHordering or the inline snippet's working dir changes. A short comment explaining which PYTHONPATH "wins" for each invocation would make this more robust.
PYTHONPATH="$WORKDIR/mrd_v220:${PYTHONPATH:-}" python3 "$TESTDIR/generate_v220.py" "$V220"
echo " Verifying source file is detected as v2.2.0 ..."
detected=$(python3 - "$V220" <<'EOF'
import sys
from mrd.tools._schema_registry import identify_file_version
v = identify_file_version(sys.argv[1])
if v != "2.2.0":
raise RuntimeError(f"Expected 2.2.0, got {v!r}")
print(v)
EOF
)
python/mrd/tools/_schema_registry.py:56
- Linting/static-analysis nit:
_fmt_versionis read and stored but never used. Either drop the assignment (juststream.read_view(4)for side-effect of advancing the stream) or validate it againstCURRENT_BINARY_FORMAT_VERSIONfrom_binary.py. As written, some lint configs will flag this as an unused variable.
_fmt_version = stream.read_view(4)
test/upgrade/test-upgrade.sh:64
- The error-case test only covers "already-current file". It doesn't cover the other two
ValueErrorpaths inupgrade_mrd_file: (a) unrecognised schema (file with magic bytes but unknown schema string) and (b) a supported-version-but-broken-chain failure mode. Given how easy these are to exercise (a single malformed temp file), adding even one assertion for the "unrecognised" branch would catch a meaningful regression.
echo " Testing error cases ..."
python3 - "$V221" <<'EOF'
import sys
from mrd.tools.upgrade import upgrade_mrd_file
try:
upgrade_mrd_file(sys.argv[1], sys.argv[1] + ".should_not_exist")
raise RuntimeError("Expected ValueError for already-current file")
except ValueError:
pass # expected
EOF
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| exp = ismrmrd.xsd.experimentalConditionsType(H1resonanceFrequency_Hz=mrd_header.experimental_conditions.h1resonance_frequency_hz) | ||
| header = ismrmrd.xsd.ismrmrdHeader(experimentalConditions=exp) |
| _PAST_SCHEMA_TO_VERSION: dict[str, str] = {v: k for k, v in _PAST_SCHEMAS.items()} | ||
|
|
||
|
|
||
| def identify_file_version(path: str) -> str | None: |
| echo " Building mrd v2.2.0 module from git tag ..." | ||
| V220_MOD="$WORKDIR/mrd_v220/mrd" | ||
| mkdir -p "$V220_MOD" | ||
|
|
||
| # Extract all v2.2.0 files from git. | ||
| git -C "$WORKSPACE" archive v2.2.0 python/mrd/ | tar -x --strip-components=2 -C "$V220_MOD" | ||
|
|
||
| echo " Generating v2.2.0 stream ..." | ||
| V220="$WORKDIR/test_v220.mrd" | ||
| # Prepend the v2.2.0 module dir so it shadows the current mrd-python install. | ||
| PYTHONPATH="$WORKDIR/mrd_v220:${PYTHONPATH:-}" python3 "$TESTDIR/generate_v220.py" "$V220" |
| # Frozen schema literals for past versions | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| _V220_SCHEMA = r"""{"protocol":{"name":"Mrd","sequence":[{"name":"header","type":[null,"Mrd.Header"]},{"name":"data","type":{"stream":{"items":"Mrd.StreamItem"}}}]},"types":[{"name":"AccelerationFactorType","fields":[{"name":"kspaceEncodingStep1","type":"uint32"},{"name":"kspaceEncodingStep2","type":"uint32"}]},{"name":"Acquisition","fields":[{"name":"head","type":"Mrd.AcquisitionHeader"},{"name":"data","type":"Mrd.AcquisitionData"},{"name":"trajectory","type":"Mrd.TrajectoryData"}]},{"name":"Acquisition","fields":[{"name":"head","type":"Mrd.AcquisitionHeader"},{"name":"data","type":"Mrd.AcquisitionData"},{"name":"trajectory","type":"Mrd.TrajectoryData"}]},{"name":"AcquisitionBucket","fields":[{"name":"data","type":{"vector":{"items":"Mrd.Acquisition"}}},{"name":"ref","type":{"vector":{"items":"Mrd.Acquisition"}}},{"name":"datastats","type":{"vector":{"items":"Mrd.EncodingLimitsType"}}},{"name":"refstats","type":{"vector":{"items":"Mrd.EncodingLimitsType"}}},{"name":"waveforms","type":{"vector":{"items":"Mrd.WaveformUint32"}}}]},{"name":"AcquisitionData","type":{"array":{"items":"complexfloat32","dimensions":[{"name":"coils"},{"name":"samples"}]}}},{"name":"AcquisitionFlags","base":"uint64","values":[{"symbol":"firstInEncodeStep1","value":1},{"symbol":"lastInEncodeStep1","value":2},{"symbol":"firstInEncodeStep2","value":4},{"symbol":"lastInEncodeStep2","value":8},{"symbol":"firstInAverage","value":16},{"symbol":"lastInAverage","value":32},{"symbol":"firstInSlice","value":64},{"symbol":"lastInSlice","value":128},{"symbol":"firstInContrast","value":256},{"symbol":"lastInContrast","value":512},{"symbol":"firstInPhase","value":1024},{"symbol":"lastInPhase","value":2048},{"symbol":"firstInRepetition","value":4096},{"symbol":"lastInRepetition","value":8192},{"symbol":"firstInSet","value":16384},{"symbol":"lastInSet","value":32768},{"symbol":"firstInSegment","value":65536},{"symbol":"lastInSegment","value":131072},{"symbol":"isNoiseMeasurement","value":262144},{"symbol":"isParallelCalibration","value":524288},{"symbol":"isParallelCalibrationAndImaging","value":1048576},{"symbol":"isReverse","value":2097152},{"symbol":"isNavigationData","value":4194304},{"symbol":"isPhasecorrData","value":8388608},{"symbol":"lastInMeasurement","value":16777216},{"symbol":"isHpfeedbackData","value":33554432},{"symbol":"isDummyscanData","value":67108864},{"symbol":"isRtfeedbackData","value":134217728},{"symbol":"isSurfacecoilcorrectionscanData","value":268435456},{"symbol":"isPhaseStabilizationReference","value":536870912},{"symbol":"isPhaseStabilization","value":1073741824}]},{"name":"AcquisitionHeader","fields":[{"name":"flags","type":"Mrd.AcquisitionFlags"},{"name":"idx","type":"Mrd.EncodingCounters"},{"name":"measurementUid","type":"uint32"},{"name":"scanCounter","type":[null,"uint32"]},{"name":"acquisitionTimeStampNs","type":[null,"uint64"]},{"name":"physiologyTimeStampNs","type":{"vector":{"items":"uint64"}}},{"name":"channelOrder","type":{"vector":{"items":"uint32"}}},{"name":"discardPre","type":[null,"uint32"]},{"name":"discardPost","type":[null,"uint32"]},{"name":"centerSample","type":[null,"uint32"]},{"name":"encodingSpaceRef","type":[null,"uint32"]},{"name":"sampleTimeNs","type":[null,"uint64"]},{"name":"position","type":{"array":{"items":"float32","dimensions":[{"length":3}]}}},{"name":"readDir","type":{"array":{"items":"float32","dimensions":[{"length":3}]}}},{"name":"phaseDir","type":{"array":{"items":"float32","dimensions":[{"length":3}]}}},{"name":"sliceDir","type":{"array":{"items":"float32","dimensions":[{"length":3}]}}},{"name":"patientTablePosition","type":{"array":{"items":"float32","dimensions":[{"length":3}]}}},{"name":"userInt","type":{"vector":{"items":"int32"}}},{"name":"userFloat","type":{"vector":{"items":"float32"}}}]},{"name":"AcquisitionSystemInformationType","fields":[{"name":"systemVendor","type":[null,"string"]},{"name":"systemModel","type":[null,"string"]},{"name":"systemFieldStrengthT","type":[null,"float32"]},{"name":"relativeReceiverNoiseBandwidth","type":[null,"float32"]},{"name":"receiverChannels","type":[null,"uint32"]},{"name":"coilLabel","type":{"vector":{"items":"Mrd.CoilLabelType"}}},{"name":"institutionName","type":[null,"string"]},{"name":"stationName","type":[null,"string"]},{"name":"deviceID","type":[null,"string"]},{"name":"deviceSerialNumber","type":[null,"string"]}]},{"name":"Array","typeParameters":["T"],"type":{"array":{"items":"T"}}},{"name":"ArrayComplexFloat","type":{"name":"Mrd.Array","typeArguments":["complexfloat32"]}},{"name":"Calibration","values":[{"symbol":"separable2D","value":0},{"symbol":"full3D","value":1},{"symbol":"other","value":2}]},{"name":"CalibrationMode","values":[{"symbol":"noacceleration","value":0},{"symbol":"embedded","value":1},{"symbol":"interleaved","value":2},{"symbol":"separate","value":3},{"symbol":"external","value":4},{"symbol":"other","value":5}]},{"name":"CoilLabelType","fields":[{"name":"coilNumber","type":"uint32"},{"name":"coilName","type":"string"}]},{"name":"DiffusionDimension","values":[{"symbol":"average","value":0},{"symbol":"contrast","value":1},{"symbol":"phase","value":2},{"symbol":"repetition","value":3},{"symbol":"set","value":4},{"symbol":"segment","value":5},{"symbol":"user0","value":6},{"symbol":"user1","value":7},{"symbol":"user2","value":8},{"symbol":"user3","value":9},{"symbol":"user4","value":10},{"symbol":"user5","value":11},{"symbol":"user6","value":12},{"symbol":"user7","value":13}]},{"name":"DiffusionType","fields":[{"name":"gradientDirection","type":"Mrd.GradientDirectionType"},{"name":"bvalue","type":"float32"}]},{"name":"EncodingCounters","fields":[{"name":"kspaceEncodeStep1","type":[null,"uint32"]},{"name":"kspaceEncodeStep2","type":[null,"uint32"]},{"name":"average","type":[null,"uint32"]},{"name":"slice","type":[null,"uint32"]},{"name":"contrast","type":[null,"uint32"]},{"name":"phase","type":[null,"uint32"]},{"name":"repetition","type":[null,"uint32"]},{"name":"set","type":[null,"uint32"]},{"name":"segment","type":[null,"uint32"]},{"name":"user","type":{"vector":{"items":"uint32"}}}]},{"name":"EncodingLimitsType","fields":[{"name":"kspaceEncodingStep0","type":[null,"Mrd.LimitType"]},{"name":"kspaceEncodingStep1","type":[null,"Mrd.LimitType"]},{"name":"kspaceEncodingStep2","type":[null,"Mrd.LimitType"]},{"name":"average","type":[null,"Mrd.LimitType"]},{"name":"slice","type":[null,"Mrd.LimitType"]},{"name":"contrast","type":[null,"Mrd.LimitType"]},{"name":"phase","type":[null,"Mrd.LimitType"]},{"name":"repetition","type":[null,"Mrd.LimitType"]},{"name":"set","type":[null,"Mrd.LimitType"]},{"name":"segment","type":[null,"Mrd.LimitType"]},{"name":"user0","type":[null,"Mrd.LimitType"]},{"name":"user1","type":[null,"Mrd.LimitType"]},{"name":"user2","type":[null,"Mrd.LimitType"]},{"name":"user3","type":[null,"Mrd.LimitType"]},{"name":"user4","type":[null,"Mrd.LimitType"]},{"name":"user5","type":[null,"Mrd.LimitType"]},{"name":"user6","type":[null,"Mrd.LimitType"]},{"name":"user7","type":[null,"Mrd.LimitType"]}]},{"name":"EncodingSpaceType","fields":[{"name":"matrixSize","type":"Mrd.MatrixSizeType"},{"name":"fieldOfViewMm","type":"Mrd.FieldOfViewMm"}]},{"name":"EncodingType","fields":[{"name":"encodedSpace","type":"Mrd.EncodingSpaceType"},{"name":"reconSpace","type":"Mrd.EncodingSpaceType"},{"name":"encodingLimits","type":"Mrd.EncodingLimitsType"},{"name":"trajectory","type":"Mrd.Trajectory"},{"name":"trajectoryDescription","type":[null,"Mrd.TrajectoryDescriptionType"]},{"name":"parallelImaging","type":[null,"Mrd.ParallelImagingType"]},{"name":"echoTrainLength","type":[null,"int64"]}]},{"name":"ExperimentalConditionsType","fields":[{"name":"h1resonanceFrequencyHz","type":"int64"}]},{"name":"FieldOfViewMm","fields":[{"name":"x","type":"float32"},{"name":"y","type":"float32"},{"name":"z","type":"float32"}]},{"name":"GradientDirectionType","fields":[{"name":"rl","type":"float32"},{"name":"ap","type":"float32"},{"name":"fh","type":"float32"}]},{"name":"Header","fields":[{"name":"version","type":[null,"int64"]},{"name":"subjectInformation","type":[null,"Mrd.SubjectInformationType"]},{"name":"studyInformation","type":[null,"Mrd.StudyInformationType"]},{"name":"measurementInformation","type":[null,"Mrd.MeasurementInformationType"]},{"name":"acquisitionSystemInformation","type":[null,"Mrd.AcquisitionSystemInformationType"]},{"name":"experimentalConditions","type":"Mrd.ExperimentalConditionsType"},{"name":"encoding","type":{"vector":{"items":"Mrd.EncodingType"}}},{"name":"sequenceParameters","type":[null,"Mrd.SequenceParametersType"]},{"name":"userParameters","type":[null,"Mrd.UserParametersType"]},{"name":"waveformInformation","type":{"vector":{"items":"Mrd.WaveformInformationType"}}}]},{"name":"Image","typeParameters":["T"],"fields":[{"name":"head","type":"Mrd.ImageHeader"},{"name":"data","type":{"name":"Mrd.ImageData","typeArguments":["T"]}},{"name":"meta","type":"Mrd.ImageMeta"}]},{"name":"ImageArray","fields":[{"name":"data","type":{"array":{"items":"complexfloat32","dimensions":[{"name":"loc"},{"name":"s"},{"name":"n"},{"name":"channel"},{"name":"z"},{"name":"y"},{"name":"x"}]}}},{"name":"headers","type":{"array":{"items":"Mrd.ImageHeader","dimensions":[{"name":"loc"},{"name":"s"},{"name":"n"}]}}},{"name":"meta","type":{"array":{"items":"Mrd.ImageMeta","dimensions":[{"name":"loc"},{"name":"s"},{"name":"n"}]}}},{"name":"waveforms","type":{"vector":{"items":"Mrd.WaveformUint32"}}}]},{"name":"ImageComplexDouble","type":{"name":"Mrd.Image","typeArguments":["complexfloat64"]}},{"name":"ImageComplexFloat","type":{"name":"Mrd.Image","typeArguments":["complexfloat32"]}},{"name":"ImageData","typeParameters":["Y"],"type":{"array":{"items":"Y","dimensions":[{"name":"channel"},{"name":"z"},{"name":"y"},{"name":"x"}]}}},{"name":"ImageDouble","type":{"name":"Mrd.Image","typeArguments":["float64"]}},{"name":"ImageFlags","base":"uint64","values":[{"symbol":"isNavigationData","value":1},{"symbol":"firstInAverage","value":16},{"symbol":"lastInAverage","value":32},{"symbol":"firstInSlice","value":64},{"symbol":"lastInSlice","value":128},{"symbol":"firstInContrast","value":256},{"symbol":"lastInContrast","value":512},{"symbol":"firstInPhase","value":1024},{"symbol":"lastInPhase","value":2048},{"symbol":"firstInRepetition","value":4096},{"symbol":"lastInRepetition","value":8192},{"symbol":"firstInSet","value":16384},{"symbol":"lastInSet","value":32768}]},{"name":"ImageFloat","type":{"name":"Mrd.Image","typeArguments":["float32"]}},{"name":"ImageHeader","fields":[{"name":"flags","type":"Mrd.ImageFlags"},{"name":"measurementUid","type":"uint32"},{"name":"fieldOfView","type":{"array":{"items":"float32","dimensions":[{"length":3}]}}},{"name":"position","type":{"array":{"items":"float32","dimensions":[{"length":3}]}}},{"name":"colDir","type":{"array":{"items":"float32","dimensions":[{"length":3}]}}},{"name":"lineDir","type":{"array":{"items":"float32","dimensions":[{"length":3}]}}},{"name":"sliceDir","type":{"array":{"items":"float32","dimensions":[{"length":3}]}}},{"name":"patientTablePosition","type":{"array":{"items":"float32","dimensions":[{"length":3}]}}},{"name":"average","type":[null,"uint32"]},{"name":"slice","type":[null,"uint32"]},{"name":"contrast","type":[null,"uint32"]},{"name":"phase","type":[null,"uint32"]},{"name":"repetition","type":[null,"uint32"]},{"name":"set","type":[null,"uint32"]},{"name":"acquisitionTimeStampNs","type":[null,"uint64"]},{"name":"physiologyTimeStampNs","type":{"vector":{"items":"uint64"}}},{"name":"imageType","type":"Mrd.ImageType"},{"name":"imageIndex","type":[null,"uint32"]},{"name":"imageSeriesIndex","type":[null,"uint32"]},{"name":"userInt","type":{"vector":{"items":"int32"}}},{"name":"userFloat","type":{"vector":{"items":"float32"}}}]},{"name":"ImageInt16","type":{"name":"Mrd.Image","typeArguments":["int16"]}},{"name":"ImageInt32","type":{"name":"Mrd.Image","typeArguments":["int32"]}},{"name":"ImageMeta","type":{"map":{"keys":"string","values":{"vector":{"items":"Mrd.ImageMetaValue"}}}}},{"name":"ImageMetaValue","type":[{"tag":"string","type":"string"},{"tag":"int64","type":"int64"},{"tag":"float64","type":"float64"}]},{"name":"ImageType","values":[{"symbol":"magnitude","value":1},{"symbol":"phase","value":2},{"symbol":"real","value":3},{"symbol":"imag","value":4},{"symbol":"complex","value":5}]},{"name":"ImageUint16","type":{"name":"Mrd.Image","typeArguments":["uint16"]}},{"name":"ImageUint32","type":{"name":"Mrd.Image","typeArguments":["uint32"]}},{"name":"InterleavingDimension","values":[{"symbol":"phase","value":0},{"symbol":"repetition","value":1},{"symbol":"contrast","value":2},{"symbol":"average","value":3},{"symbol":"other","value":4}]},{"name":"LimitType","fields":[{"name":"minimum","type":"uint32"},{"name":"maximum","type":"uint32"},{"name":"center","type":"uint32"}]},{"name":"MatrixSizeType","fields":[{"name":"x","type":"uint32"},{"name":"y","type":"uint32"},{"name":"z","type":"uint32"}]},{"name":"MeasurementDependencyType","fields":[{"name":"dependencyType","type":"string"},{"name":"measurementID","type":"string"}]},{"name":"MeasurementInformationType","fields":[{"name":"measurementID","type":[null,"string"]},{"name":"seriesDate","type":[null,"date"]},{"name":"seriesTime","type":[null,"time"]},{"name":"patientPosition","type":"Mrd.PatientPosition"},{"name":"relativeTablePosition","type":[null,"Mrd.ThreeDimensionalFloat"]},{"name":"initialSeriesNumber","type":[null,"int64"]},{"name":"protocolName","type":[null,"string"]},{"name":"sequenceName","type":[null,"string"]},{"name":"seriesDescription","type":[null,"string"]},{"name":"measurementDependency","type":{"vector":{"items":"Mrd.MeasurementDependencyType"}}},{"name":"seriesInstanceUIDRoot","type":[null,"string"]},{"name":"frameOfReferenceUID","type":[null,"string"]},{"name":"referencedImageSequence","type":[null,"Mrd.ReferencedImageSequenceType"]}]},{"name":"MultibandSpacingType","fields":[{"name":"dZ","type":{"vector":{"items":"float32"}}}]},{"name":"MultibandType","fields":[{"name":"spacing","type":{"vector":{"items":"Mrd.MultibandSpacingType"}}},{"name":"deltaKz","type":"float32"},{"name":"multibandFactor","type":"uint32"},{"name":"calibration","type":"Mrd.Calibration"},{"name":"calibrationEncoding","type":"uint64"}]},{"name":"ParallelImagingType","fields":[{"name":"accelerationFactor","type":"Mrd.AccelerationFactorType"},{"name":"calibrationMode","type":[null,"Mrd.CalibrationMode"]},{"name":"interleavingDimension","type":[null,"Mrd.InterleavingDimension"]},{"name":"multiband","type":[null,"Mrd.MultibandType"]}]},{"name":"PatientGender","values":[{"symbol":"m","value":0},{"symbol":"f","value":1},{"symbol":"o","value":2}]},{"name":"PatientPosition","values":[{"symbol":"hFP","value":0},{"symbol":"hFS","value":1},{"symbol":"hFDR","value":2},{"symbol":"hFDL","value":3},{"symbol":"fFP","value":4},{"symbol":"fFS","value":5},{"symbol":"fFDR","value":6},{"symbol":"fFDL","value":7}]},{"name":"ReconAssembly","fields":[{"name":"data","type":"Mrd.ReconBuffer"},{"name":"ref","type":[null,"Mrd.ReconBuffer"]}]},{"name":"ReconBuffer","fields":[{"name":"data","type":{"array":{"items":"complexfloat32","dimensions":[{"name":"loc"},{"name":"s"},{"name":"n"},{"name":"chan"},{"name":"e2"},{"name":"e1"},{"name":"e0"}]}}},{"name":"trajectory","type":{"array":{"items":"float32","dimensions":[{"name":"loc"},{"name":"s"},{"name":"n"},{"name":"e2"},{"name":"e1"},{"name":"basis"},{"name":"samples"}]}}},{"name":"density","type":[null,{"array":{"items":"float32","dimensions":[{"name":"loc"},{"name":"s"},{"name":"n"},{"name":"e2"},{"name":"e1"},{"name":"e0"}]}}]},{"name":"headers","type":{"array":{"items":"Mrd.AcquisitionHeader","dimensions":[{"name":"loc"},{"name":"s"},{"name":"n"},{"name":"e2"},{"name":"e1"}]}}},{"name":"sampling","type":"Mrd.SamplingDescription"}]},{"name":"ReconData","fields":[{"name":"buffers","type":{"vector":{"items":"Mrd.ReconAssembly"}}}]},{"name":"ReferencedImageSequenceType","fields":[{"name":"referencedSOPInstanceUID","type":{"vector":{"items":"string"}}}]},{"name":"SamplingDescription","fields":[{"name":"encodedFOV","type":"Mrd.FieldOfViewMm"},{"name":"reconFOV","type":"Mrd.FieldOfViewMm"},{"name":"encodedMatrix","type":"Mrd.MatrixSizeType"},{"name":"reconMatrix","type":"Mrd.MatrixSizeType"},{"name":"samplingLimits","type":"Mrd.SamplingLimits"}]},{"name":"SamplingLimits","fields":[{"name":"kspaceEncodingStep0","type":"Mrd.LimitType"},{"name":"kspaceEncodingStep1","type":"Mrd.LimitType"},{"name":"kspaceEncodingStep2","type":"Mrd.LimitType"}]},{"name":"SequenceParametersType","fields":[{"name":"tR","type":{"vector":{"items":"float32"}}},{"name":"tE","type":{"vector":{"items":"float32"}}},{"name":"tI","type":{"vector":{"items":"float32"}}},{"name":"flipAngleDeg","type":{"vector":{"items":"float32"}}},{"name":"sequenceType","type":[null,"string"]},{"name":"echoSpacing","type":{"vector":{"items":"float32"}}},{"name":"diffusionDimension","type":[null,"Mrd.DiffusionDimension"]},{"name":"diffusion","type":{"vector":{"items":"Mrd.DiffusionType"}}},{"name":"diffusionScheme","type":[null,"string"]}]},{"name":"StreamItem","type":[{"tag":"Acquisition","type":"Mrd.Acquisition"},{"tag":"WaveformUint32","type":"Mrd.WaveformUint32"},{"tag":"ImageUint16","type":"Mrd.ImageUint16"},{"tag":"ImageInt16","type":"Mrd.ImageInt16"},{"tag":"ImageUint32","type":"Mrd.ImageUint32"},{"tag":"ImageInt32","type":"Mrd.ImageInt32"},{"tag":"ImageFloat","type":"Mrd.ImageFloat"},{"tag":"ImageDouble","type":"Mrd.ImageDouble"},{"tag":"ImageComplexFloat","type":"Mrd.ImageComplexFloat"},{"tag":"ImageComplexDouble","type":"Mrd.ImageComplexDouble"},{"tag":"AcquisitionBucket","type":"Mrd.AcquisitionBucket"},{"tag":"ReconData","type":"Mrd.ReconData"},{"tag":"ArrayComplexFloat","type":"Mrd.ArrayComplexFloat"},{"tag":"ImageArray","type":"Mrd.ImageArray"}]},{"name":"StudyInformationType","fields":[{"name":"studyDate","type":[null,"date"]},{"name":"studyTime","type":[null,"time"]},{"name":"studyID","type":[null,"string"]},{"name":"accessionNumber","type":[null,"int64"]},{"name":"referringPhysicianName","type":[null,"string"]},{"name":"studyDescription","type":[null,"string"]},{"name":"studyInstanceUID","type":[null,"string"]},{"name":"bodyPartExamined","type":[null,"string"]}]},{"name":"SubjectInformationType","fields":[{"name":"patientName","type":[null,"string"]},{"name":"patientWeightKg","type":[null,"float32"]},{"name":"patientHeightM","type":[null,"float32"]},{"name":"patientID","type":[null,"string"]},{"name":"patientBirthdate","type":[null,"date"]},{"name":"patientGender","type":[null,"Mrd.PatientGender"]}]},{"name":"ThreeDimensionalFloat","fields":[{"name":"x","type":"float32"},{"name":"y","type":"float32"},{"name":"z","type":"float32"}]},{"name":"Trajectory","values":[{"symbol":"cartesian","value":0},{"symbol":"epi","value":1},{"symbol":"radial","value":2},{"symbol":"goldenangle","value":3},{"symbol":"spiral","value":4},{"symbol":"other","value":5}]},{"name":"TrajectoryData","type":{"array":{"items":"float32","dimensions":[{"name":"basis"},{"name":"samples"}]}}},{"name":"TrajectoryDescriptionType","fields":[{"name":"identifier","type":"string"},{"name":"userParameterLong","type":{"vector":{"items":"Mrd.UserParameterLongType"}}},{"name":"userParameterDouble","type":{"vector":{"items":"Mrd.UserParameterDoubleType"}}},{"name":"userParameterString","type":{"vector":{"items":"Mrd.UserParameterStringType"}}},{"name":"comment","type":[null,"string"]}]},{"name":"UserParameterBase64Type","fields":[{"name":"name","type":"string"},{"name":"value","type":"string"}]},{"name":"UserParameterDoubleType","fields":[{"name":"name","type":"string"},{"name":"value","type":"float64"}]},{"name":"UserParameterLongType","fields":[{"name":"name","type":"string"},{"name":"value","type":"int64"}]},{"name":"UserParameterStringType","fields":[{"name":"name","type":"string"},{"name":"value","type":"string"}]},{"name":"UserParametersType","fields":[{"name":"userParameterLong","type":{"vector":{"items":"Mrd.UserParameterLongType"}}},{"name":"userParameterDouble","type":{"vector":{"items":"Mrd.UserParameterDoubleType"}}},{"name":"userParameterString","type":{"vector":{"items":"Mrd.UserParameterStringType"}}},{"name":"userParameterBase64","type":{"vector":{"items":"Mrd.UserParameterBase64Type"}}}]},{"name":"Waveform","typeParameters":["T"],"fields":[{"name":"flags","type":"uint64"},{"name":"measurementUid","type":"uint32"},{"name":"scanCounter","type":"uint32"},{"name":"timeStampNs","type":"uint64"},{"name":"sampleTimeNs","type":"uint64"},{"name":"waveformId","type":"uint32"},{"name":"data","type":{"name":"Mrd.WaveformSamples","typeArguments":["T"]}}]},{"name":"WaveformInformationType","fields":[{"name":"waveformName","type":"string"},{"name":"waveformType","type":"Mrd.WaveformType"},{"name":"userParameters","type":"Mrd.UserParametersType"}]},{"name":"WaveformSamples","typeParameters":["T"],"type":{"array":{"items":"T","dimensions":[{"name":"channels"},{"name":"samples"}]}}},{"name":"WaveformType","values":[{"symbol":"ecg","value":0},{"symbol":"pulse","value":1},{"symbol":"respiratory","value":2},{"symbol":"trigger","value":3},{"symbol":"gradientwaveform","value":4},{"symbol":"other","value":5}]},{"name":"WaveformUint32","type":{"name":"Mrd.Waveform","typeArguments":["uint32"]}},{"name":"WaveformUint32","type":{"name":"Mrd.Waveform","typeArguments":["uint32"]}}]}""" |
| with V220MrdReader(src) as reader: | ||
| header = reader.read_header() | ||
| with BinaryMrdWriter(dst) as writer: | ||
| writer.write_header(header) | ||
| writer.write_data(reader.read_data()) |
Introduces a new Python tool to upgrade MRD files to the current version of MRD.
Initially, it supports the upgrade path from v2.2.0 (previous) to v2.2.1 (current).