Source code for dicom_dre.profiles.default

"""Default de-identification profile (DICOM-PS3.15E-Basic).

Full PS3.15E de-identification with UID hashing, date jitter,
and PHI removal.
"""

from typing import cast

from pydicom.datadict import dictionary_VR
from pydicom.dataset import Dataset
from pydicom.tag import BaseTag
from pydicom.tag import Tag

from dicom_dre.actions import TagAction
from dicom_dre.actions import append_value
from dicom_dre.actions import cap_age
from dicom_dre.actions import dummy_for_vr
from dicom_dre.actions import empty
from dicom_dre.actions import hash_identifier_param
from dicom_dre.actions import hash_uid
from dicom_dre.actions import hash_value_identifier
from dicom_dre.actions import if_exists
from dicom_dre.actions import jitter_date
from dicom_dre.actions import keep
from dicom_dre.actions import remove
from dicom_dre.actions import round_decimal_string
from dicom_dre.actions import set_param
from dicom_dre.actions import set_value
from dicom_dre.parameters import DEFAULT_STUDY_ID
from dicom_dre.parameters import DeidParameters
from dicom_dre.profile import DeidProfile
from dicom_dre.profile import Jitter
from dicom_dre.profile import PrivateTagSpec
from dicom_dre.profiles.config import ProfileSettings
from dicom_dre.text_redactor import get_text_redactor


# Approved private elements preserved verbatim, keyed on the private-creator
# string. Global private-group removal exempts these; every other private
# element is removed. Profiles derived from this one via dataclasses.replace
# inherit the set; the strict profile imports it directly.
PRESERVED_PRIVATE_SPECS: frozenset[PrivateTagSpec] = frozenset(
    {
        PrivateTagSpec(
            group=0x0019,
            creator="GEMS_ACQU_01",
            offsets=(
                0x0F,  # DS HorizFrameOfRef
                0x11,  # SS SeriesContrast
                0x12,  # SS LastPseq
                0x17,  # SS SeriesPlane
                0x18,  # LO FirstScanRas
                0x19,  # DS FirstScanLocation
                0x1A,  # LO LastScanRas
                0x1B,  # DS LastScanLoc
                0x1E,  # DS DisplayFieldOfView
                0x5A,  # FL AcquisitionDuration
                0x7D,  # DS SecondEcho
                0x7E,  # SS NumberOfEchoes
                0x7F,  # DS TableDelta
                0x81,  # SS Contiguous
                0x84,  # DS PeakSAR
                0x87,  # DS CardiacRepetitionTime
                0x88,  # SS ImagesPerCardiacCycle
                0x8A,  # SS ActualReceiveGainAnalog
                0x8B,  # SS ActualReceiveGainDigital
                0x8D,  # DS DelayAfterTrigger
                0x8F,  # SS Swappf
                0x90,  # SS PauseInterval
                0x91,  # DS PauseTime
                0x93,  # DS AutoPrescanCenterFrequency
                0x94,  # SS AutoPrescanTransmitGain
                0x95,  # SS AutoPrescanAnalogReceiverGain
                0x96,  # SS AutoPrescanDigitalReceiverGain
                0x97,  # SL BitmapdefiningCVs
                0x9B,  # SS PulseSequenceMode
                0x9C,  # LO PulseSequenceName
                Jitter(0x9D),  # DT PulseSequenceDate
                0x9E,  # LO SequenceName, InternalPulseSequenceName
                0x9F,  # SS TransmittingCoilType
                0xA2,  # SL RawDataRunNumber
                0xA3,  # UL CalibratedFieldStrength
                0xA4,  # SS SATFatWaterBone
                0xA7,  # DS UserData0
                0xA8,  # DS UserData1
                0xA9,  # DS UserData2
                0xAA,  # DS UserData3
                0xAB,  # DS UserData4
                0xAC,  # DS UserData5
                0xAD,  # DS UserData6
                0xAE,  # DS UserData7
                0xAF,  # DS UserData8
                0xB0,  # DS UserData9
                0xB1,  # DS UserData10
                0xB2,  # DS UserData11
                0xB3,  # DS UserData12
                0xB4,  # DS UserData13
                0xB5,  # DS UserData14
                0xB6,  # DS UserData15
                0xB7,  # DS UserData16
                0xB8,  # DS UserData17
                0xB9,  # DS UserData18
                0xBA,  # DS UserData19
                0xBB,  # DS DiffusionDirectionX, UserData20
                0xBC,  # DS DiffusionDirectionY, UserData21
                0xBD,  # DS DiffusionDirectionZ, UserData22
                0xBE,  # DS ProjectionAngle
                0xC0,  # SS SaturationPlanes
                0xC2,  # SS SATlocationR
                0xC3,  # SS SATlocationL
                0xC4,  # SS SATlocationA
                0xC5,  # SS SATlocationP
                0xC6,  # SS SATlocationH
                0xC7,  # SS SATlocationF
                0xC8,  # SS SATThicknessRL
                0xC9,  # SS SATThicknessAP
                0xCA,  # SS SATThicknessHF
                0xCB,  # SS PhaseContrastFlowAxis
                0xCC,  # SS VelocityEncoding
                0xCD,  # SS ThicknessDisclaimer
                0xCE,  # SS PrescanType
                0xCF,  # SS PrescanStatus
                0xD2,  # SS ProjectionAlgorithm
                0xD5,  # SS FractionalEcho
                0xD8,  # SS VariableEchoflag
                0xD9,  # DS ConcatenatedSAT
                0xDF,  # DS UserData23
                0xE0,  # DS NumberOfDiffusionDirection, UserData24
                0xE2,  # DS VelocityEncodeScale
                0xF2,  # SS FastPhases
                0xF9,  # DS TransmitGain
            ),
        ),
        PrivateTagSpec(
            group=0x0043,
            creator="GEMS_PARM_01",
            offsets=(
                0x01,  # SS BitmapOfPrescanOptions
                0x02,  # SS GradientOffsetInX
                0x03,  # SS GradientOffsetInY
                0x04,  # SS GradientOffsetInZ
                0x06,  # SS NumberOfEPIShots
                0x07,  # SS ViewsPerSegment
                0x08,  # SS RespiratoryRate
                0x09,  # SS RespiratoryTriggerPoint
                0x0A,  # SS TypeOfReceiverUsed
                0x0B,  # DS DB_dtPeakRateOfChangeOfGradientField
                0x0C,  # DS dB_dtLimitsInUnitsOfPercent
                0x0D,  # DS PSDEstimatedLimit
                0x0E,  # DS PSDEstimatedLimitInTeslaPerSecond
                0x29,  # OB HistogramTables
                0x2C,  # SS EffectiveEchoSpacing
                0x2D,  # SH FilterMode
                0x2F,  # SS PrivateImageType
                0x30,  # SS VasCollapseFlag
                0x32,  # SS VasFlags
                0x33,  # FL Neg_scanspacing
                0x34,  # IS OffsetFrequency
                0x35,  # UL User_usage_tag
                0x36,  # UL User_fill_map_MSW
                0x37,  # UL User_fill_map_LSW
                0x38,  # FL UserData_25_48
                0x39,  # IS SlopInt_6_9
                0x60,  # IS SlopInt_10_17
                0x7D,  # US ReconModeFlagWord
                0x80,  # LO CoilIDData
                0x81,  # LO GECoilName
                0x83,  # DS AssetRFactors
                0x84,  # LO AdditionalAssetData
                0x89,  # LO GoverningBodydB_dtAndSARDefinition
                0x8A,  # CS InPlanePhaseEncodingDirection
                0x90,  # LO SARDefinition
                0x91,  # DS SARValue
                0x97,  # LO ImageFilteringParameters
                0x9A,  # IS RxStackIdentification
            ),
        ),
        PrivateTagSpec(
            group=0x0027,
            creator="GEMS_IMAG_01",
            offsets=(
                0x31,  # SS ImagingMode
                0x32,  # SS PulseSequence
                0x33,  # UL ImagingOptions
                0x35,  # SS PlaneType
                0x40,  # SH RASletterOfImageLocation
                0x41,  # FL ImageLocation
                0x60,  # FL ImageDimensionX
                0x61,  # FL ImageDimensionY
                0x62,  # FL NumberOfExcitations
            ),
        ),
    }
)

# Tags removed during de-identification because they may carry PHI.
PHI_REMOVE_TAGS: frozenset[BaseTag] = frozenset(
    {
        Tag(0x0000, 0x1000),  # AffectedSOPInstanceUID
        Tag(0x0008, 0x0080),  # InstitutionName
        Tag(0x0008, 0x0081),  # InstitutionAddress
        Tag(0x0008, 0x0082),  # InstitutionCodeSeq
        Tag(0x0008, 0x0092),  # ReferringPhysicianAddress
        Tag(0x0008, 0x0094),  # ReferringPhysicianPhoneNumbers
        Tag(0x0008, 0x0096),  # ReferringPhysiciansIDSeq
        Tag(0x0008, 0x009C),  # ConsultingPhysicianName
        Tag(0x0008, 0x009D),  # ConsultingPhysicianIdentificationSequence
        Tag(0x0008, 0x0201),  # TimezoneOffsetFromUTC
        Tag(0x0008, 0x0051),  # IssuerOfAccessionNumberSeq
        Tag(0x0008, 0x1010),  # StationName
        Tag(0x0008, 0x1040),  # InstitutionalDepartmentName
        Tag(0x0008, 0x1048),  # PhysicianOfRecord
        Tag(0x0008, 0x1049),  # PhysicianOfRecordIdSeq
        Tag(0x0008, 0x1050),  # PerformingPhysicianName
        Tag(0x0008, 0x1052),  # PerformingPhysicianIdSeq
        Tag(0x0008, 0x1060),  # NameOfPhysicianReadingStudy
        Tag(0x0008, 0x1062),  # PhysicianReadingStudyIdSeq
        Tag(0x0008, 0x1070),  # OperatorName
        Tag(0x0008, 0x1072),  # OperatorsIdentificationSeq
        Tag(0x0008, 0x1080),  # AdmittingDiagnosisDescription
        Tag(0x0008, 0x1084),  # AdmittingDiagnosisCodeSeq
        Tag(0x0008, 0x1110),  # RefStudySeq
        Tag(0x0008, 0x1111),  # RefPPSSeq
        Tag(0x0008, 0x1120),  # RefPatientSeq
        Tag(0x0008, 0x2111),  # DerivationDescription
        Tag(0x0008, 0x4000),  # IdentifyingComments
        Tag(0x0010, 0x0021),  # IssuerOfPatientID
        Tag(0x0010, 0x0032),  # PatientBirthTime
        Tag(0x0010, 0x0050),  # PatientInsurancePlanCodeSeq
        Tag(0x0010, 0x0101),  # PatientPrimaryLanguageCodeSeq
        Tag(0x0010, 0x0102),  # PatientPrimaryLanguageModifierCodeSeq
        Tag(0x0010, 0x1000),  # OtherPatientIDs
        Tag(0x0010, 0x1001),  # OtherPatientNames
        Tag(0x0010, 0x1002),  # OtherPatientIDsSeq
        Tag(0x0010, 0x1005),  # PatientBirthName
        Tag(0x0010, 0x1040),  # PatientAddress
        Tag(0x0010, 0x1050),  # InsurancePlanIdentification
        Tag(0x0010, 0x1060),  # PatientMotherBirthName
        Tag(0x0010, 0x1080),  # MilitaryRank
        Tag(0x0010, 0x1081),  # BranchOfService
        Tag(0x0010, 0x1090),  # MedicalRecordLocator
        Tag(0x0010, 0x1100),  # ReferencedPatientPhotoSequence
        Tag(0x0010, 0x2000),  # MedicalAlerts
        Tag(0x0010, 0x2110),  # ContrastAllergies
        Tag(0x0010, 0x2150),  # CountryOfResidence
        Tag(0x0010, 0x2152),  # RegionOfResidence
        Tag(0x0010, 0x2154),  # PatientPhoneNumbers
        Tag(0x0010, 0x2155),  # PatientTelecomInformation
        Tag(0x0010, 0x2160),  # EthnicGroup
        Tag(0x0010, 0x2180),  # Occupation
        Tag(0x0010, 0x21A0),  # SmokingStatus
        Tag(0x0010, 0x21B0),  # AdditionalPatientHistory
        Tag(0x0010, 0x21C0),  # PregnancyStatus
        Tag(0x0010, 0x21F0),  # PatientReligiousPreference
        Tag(0x0010, 0x2203),  # PatientSexNeutered
        Tag(0x0010, 0x2297),  # ResponsiblePerson
        Tag(0x0010, 0x2299),  # ResponsibleOrganization
        Tag(0x0010, 0x4000),  # PatientComments
        Tag(0x0018, 0x1004),  # PlateID
        Tag(0x0018, 0x1005),  # GeneratorID
        Tag(0x0018, 0x1007),  # CassetteID
        Tag(0x0018, 0x1008),  # GantryID
        Tag(0x0018, 0x1078),  # RadiopharmaceuticalStartDateTime
        Tag(0x0018, 0x1079),  # RadiopharmaceuticalStopDateTime
        Tag(0x0018, 0x1200),  # DateOfLastCalibration
        Tag(0x0018, 0x1400),  # AcquisitionDeviceProcessingDescription
        Tag(0x0018, 0x4000),  # AcquisitionComments
        Tag(0x0018, 0x700A),  # DetectorID
        Tag(0x0018, 0x700C),  # DateOfLastDetectorCalibration
        Tag(0x0018, 0x9424),  # AcquisitionProtocolDescription
        Tag(0x0018, 0x9506),  # ContributingSourcesSequence
        Tag(0x0018, 0xA003),  # ContributionDescription
        Tag(0x0020, 0x3401),  # ModifyingDeviceID
        Tag(0x0020, 0x3404),  # ModifyingDeviceManufacturer
        Tag(0x0020, 0x3406),  # ModifiedImageDescription
        Tag(0x0020, 0x4000),  # ImageComments
        Tag(0x0020, 0x9158),  # FrameComments
        Tag(0x0028, 0x4000),  # ImagePresentationComments
        Tag(0x0032, 0x0012),  # StudyIDIssuer
        Tag(0x0032, 0x0032),  # StudyVerifiedDate
        Tag(0x0032, 0x0034),  # StudyReadDate
        Tag(0x0032, 0x1020),  # ScheduledStudyLocation
        Tag(0x0032, 0x1021),  # ScheduledStudyLocationAET
        Tag(0x0032, 0x1030),  # ReasonforStudy
        Tag(0x0032, 0x1032),  # RequestingPhysician
        Tag(0x0032, 0x1033),  # RequestingService
        Tag(0x0032, 0x1060),  # RequestedProcedureDescription
        Tag(0x0032, 0x1070),  # RequestedContrastAgent
        Tag(0x0032, 0x4000),  # StudyComments
        Tag(0x0038, 0x0004),  # RefPatientAliasSeq
        Tag(0x0038, 0x0010),  # AdmissionID
        Tag(0x0038, 0x0011),  # IssuerOfAdmissionID
        Tag(0x0038, 0x001E),  # ScheduledPatientInstitutionResidence
        Tag(0x0038, 0x0040),  # DischargeDiagnosisDescription
        Tag(0x0038, 0x0050),  # SpecialNeeds
        Tag(0x0038, 0x0060),  # ServiceEpisodeID
        Tag(0x0038, 0x0061),  # IssuerOfServiceEpisodeId
        Tag(0x0038, 0x0062),  # ServiceEpisodeDescription
        Tag(0x0038, 0x0300),  # CurrentPatientLocation
        Tag(0x0038, 0x0400),  # PatientInstitutionResidence
        Tag(0x0038, 0x0500),  # PatientState
        Tag(0x0038, 0x1234),  # ReferencedPatientAliasSeq
        Tag(0x0038, 0x4000),  # VisitComments
        Tag(0x0040, 0x0001),  # ScheduledStationAET
        Tag(0x0040, 0x0006),  # ScheduledPerformingPhysicianName
        Tag(0x0040, 0x0007),  # SPSDescription
        Tag(0x0040, 0x000B),  # ScheduledPerformingPhysicianIDSeq
        Tag(0x0040, 0x0010),  # ScheduledStationName
        Tag(0x0040, 0x0011),  # SPSLocation
        Tag(0x0040, 0x0012),  # PreMedication
        Tag(0x0040, 0x0241),  # PerformedStationAET
        Tag(0x0040, 0x0242),  # PerformedStationName
        Tag(0x0040, 0x0243),  # PerformedLocation
        Tag(0x0040, 0x0248),  # PerformedStationNameCodeSeq
        Tag(0x0040, 0x0253),  # PPSID
        Tag(0x0040, 0x0254),  # PPSDescription
        Tag(0x0040, 0x0275),  # RequestAttributesSeq
        Tag(0x0040, 0x0280),  # PPSComments
        Tag(0x0040, 0x0555),  # AcquisitionContextSeq
        Tag(0x0040, 0x1001),  # RequestedProcedureID
        Tag(0x0040, 0x1004),  # PatientTransportArrangements
        Tag(0x0040, 0x1005),  # RequestedProcedureLocation
        Tag(0x0040, 0x1010),  # NamesOfIntendedRecipientsOfResults
        Tag(0x0040, 0x1011),  # IntendedRecipientsOfResultsIDSequence
        Tag(0x0040, 0x1102),  # PersonAddress
        Tag(0x0040, 0x1103),  # PersonTelephoneNumbers
        Tag(0x0040, 0x1104),  # PersonTelecomInformation
        Tag(0x0040, 0x1400),  # RequestedProcedureComments
        Tag(0x0040, 0x2001),  # ReasonForTheImagingServiceRequest
        Tag(0x0040, 0x2008),  # OrderEnteredBy
        Tag(0x0040, 0x2009),  # OrderEntererLocation
        Tag(0x0040, 0x2010),  # OrderCallbackPhoneNumber
        Tag(0x0040, 0x2011),  # OrderCallbackTelecomInformation
        Tag(0x0040, 0x2400),  # ImagingServiceRequestComments
        Tag(0x0040, 0x3001),  # ConfidentialityPatientData
        Tag(0x0040, 0x4025),  # ScheduledStationNameCodeSeq
        Tag(0x0040, 0x4027),  # ScheduledStationGeographicLocCodeSeq
        Tag(0x0040, 0x4028),  # PerformedStationNameCodeSequence
        Tag(0x0040, 0x4030),  # PerformedStationGeoLocCodeSeq
        Tag(0x0040, 0x4034),  # ScheduledHumanPerformersSeq
        Tag(0x0040, 0x4035),  # ActualHumanPerformersSequence
        Tag(0x0040, 0x4036),  # HumanPerformersOrganization
        Tag(0x0040, 0x4037),  # HumanPerformersName
        Tag(0x0040, 0xA027),  # VerifyingOrganization
        Tag(0x0040, 0xA030),  # VerificationDateTime
        Tag(0x0040, 0xA032),  # ObservationDateTime
        Tag(0x0040, 0xA075),  # VerifyingObserverName
        Tag(0x0040, 0xA078),  # AuthorObserverSequence
        Tag(0x0040, 0xA07A),  # ParticipantSequence
        Tag(0x0040, 0xA07C),  # CustodialOrganizationSeq
        Tag(0x0040, 0xA120),  # DateTime
        Tag(0x0040, 0xA121),  # Date
        Tag(0x0040, 0xA13A),  # RefDatetime
        Tag(0x0040, 0xA307),  # TrialCurrentObserver
        Tag(0x0040, 0xA352),  # TrialVerbalSource
        Tag(0x0040, 0xA353),  # TrialAddress
        Tag(0x0040, 0xA354),  # TrialTelephoneNumber
        Tag(0x0040, 0xA358),  # TrialVerbalSourceIdentifierCodeSequence
        Tag(0x0060, 0x3000),  # OverlayData
        Tag(0x0060, 0x4000),  # OverlayComments
        Tag(0x0070, 0x0086),  # ContentCreatorsIdCodeSeq
        Tag(0x0088, 0x0200),  # IconImageSequence
        Tag(0x0088, 0x0904),  # TopicTitle
        Tag(0x0088, 0x0906),  # TopicSubject
        Tag(0x0088, 0x0910),  # TopicAuthor
        Tag(0x0088, 0x0912),  # TopicKeyWords
        Tag(0x0400, 0x0100),  # DigitalSignatureUID
        Tag(0x0400, 0x0402),  # RefDigitalSignatureSeq
        Tag(0x0400, 0x0403),  # RefSOPInstanceMACSeq
        Tag(0x0400, 0x0404),  # MAC
        Tag(0x0400, 0x0550),  # ModifiedAttributesSequence
        Tag(0x0400, 0x0561),  # OriginalAttributesSequence
        Tag(0x0400, 0x0600),  # InstanceOriginStatus
        Tag(0x2030, 0x0020),  # TextString
        Tag(0x3008, 0x0105),  # SourceSerialNumber
        Tag(0x300C, 0x0113),  # ReasonForOmissionDescription
        Tag(0x300E, 0x0008),  # ReviewerName
        Tag(0x4000, 0x0010),  # Arbitrary
        Tag(0x4000, 0x4000),  # TextComments
        Tag(0x4008, 0x0042),  # ResultsIDIssuer
        Tag(0x4008, 0x0102),  # InterpretationRecorder
        Tag(0x4008, 0x010A),  # InterpretationTranscriber
        Tag(0x4008, 0x010B),  # InterpretationText
        Tag(0x4008, 0x010C),  # InterpretationAuthor
        Tag(0x4008, 0x0111),  # InterpretationApproverSequence
        Tag(0x4008, 0x0114),  # PhysicianApprovingInterpretation
        Tag(0x4008, 0x0115),  # InterpretationDiagnosisDescription
        Tag(0x4008, 0x0118),  # ResultsDistributionListSeq
        Tag(0x4008, 0x0119),  # DistributionName
        Tag(0x4008, 0x011A),  # DistributionAddress
        Tag(0x4008, 0x0202),  # InterpretationIdIssuer
        Tag(0x4008, 0x0300),  # Impressions
        Tag(0x4008, 0x4000),  # ResultComments
        Tag(0xFFFA, 0xFFFA),  # DigitalSignaturesSeq
        Tag(0xFFFC, 0xFFFC),  # DataSetTrailingPadding
        # PS3.15 Basic Profile X attributes (2024b confidentiality table).
        Tag(0x0008, 0x0054),  # Retrieve AE Title
        Tag(0x0008, 0x0055),  # Station AE Title
        Tag(0x0008, 0x1000),  # Network ID
        Tag(0x0008, 0x1030),  # Study Description
        Tag(0x0008, 0x103E),  # Series Description
        Tag(0x0008, 0x1041),  # Institutional Department Type Code Sequence
        Tag(0x0008, 0x1088),  # Pyramid Description
        Tag(0x0010, 0x1010),  # Patient's Age
        Tag(0x0012, 0x0022),  # Issuer of Clinical Trial Protocol ID
        Tag(0x0012, 0x0023),  # Other Clinical Trial Protocol IDs Sequence
        Tag(0x0012, 0x0032),  # Issuer of Clinical Trial Site ID
        Tag(0x0012, 0x0041),  # Issuer of Clinical Trial Subject ID
        Tag(0x0012, 0x0043),  # Issuer of Clinical Trial Subject Reading ID
        Tag(0x0012, 0x0051),  # Clinical Trial Time Point Description
        Tag(0x0012, 0x0055),  # Issuer of Clinical Trial Time Point ID
        Tag(0x0012, 0x0071),  # Clinical Trial Series ID
        Tag(0x0012, 0x0072),  # Clinical Trial Series Description
        Tag(0x0012, 0x0073),  # Issuer of Clinical Trial Series ID
        Tag(0x0012, 0x0082),  # Clinical Trial Protocol Ethics Committee Approval Number
        Tag(0x0016, 0x002B),  # Maker Note
        Tag(0x0016, 0x004B),  # Device Setting Description
        Tag(0x0016, 0x004D),  # Camera Owner Name
        Tag(0x0016, 0x004E),  # Lens Specification
        Tag(0x0016, 0x004F),  # Lens Make
        Tag(0x0016, 0x0050),  # Lens Model
        Tag(0x0016, 0x0051),  # Lens Serial Number
        Tag(0x0016, 0x0070),  # GPS Version ID
        Tag(0x0016, 0x0071),  # GPS Latitude Ref
        Tag(0x0016, 0x0072),  # GPS Latitude
        Tag(0x0016, 0x0073),  # GPS Longitude Ref
        Tag(0x0016, 0x0074),  # GPS Longitude
        Tag(0x0016, 0x0075),  # GPS Altitude Ref
        Tag(0x0016, 0x0076),  # GPS Altitude
        Tag(0x0016, 0x0077),  # GPS Time Stamp
        Tag(0x0016, 0x0078),  # GPS Satellites
        Tag(0x0016, 0x0079),  # GPS Status
        Tag(0x0016, 0x007A),  # GPS Measure Mode
        Tag(0x0016, 0x007B),  # GPS DOP
        Tag(0x0016, 0x007C),  # GPS Speed Ref
        Tag(0x0016, 0x007D),  # GPS Speed
        Tag(0x0016, 0x007E),  # GPS Track Ref
        Tag(0x0016, 0x007F),  # GPS Track
        Tag(0x0016, 0x0080),  # GPS Img Direction Ref
        Tag(0x0016, 0x0081),  # GPS Img Direction
        Tag(0x0016, 0x0082),  # GPS Map Datum
        Tag(0x0016, 0x0083),  # GPS Dest Latitude Ref
        Tag(0x0016, 0x0084),  # GPS Dest Latitude
        Tag(0x0016, 0x0085),  # GPS Dest Longitude Ref
        Tag(0x0016, 0x0086),  # GPS Dest Longitude
        Tag(0x0016, 0x0087),  # GPS Dest Bearing Ref
        Tag(0x0016, 0x0088),  # GPS Dest Bearing
        Tag(0x0016, 0x0089),  # GPS Dest Distance Ref
        Tag(0x0016, 0x008A),  # GPS Dest Distance
        Tag(0x0016, 0x008B),  # GPS Processing Method
        Tag(0x0016, 0x008C),  # GPS Area Information
        Tag(0x0016, 0x008E),  # GPS Differential
        Tag(0x0018, 0x1009),  # Unique Device Identifier
        Tag(0x0018, 0x100A),  # UDI Sequence
        Tag(0x0018, 0x5011),  # Transducer Identification Sequence
        Tag(0x0018, 0x9185),  # Respiratory Motion Compensation Technique Description
        Tag(0x0018, 0x9373),  # X-Ray Detector Label
        Tag(0x0018, 0x937B),  # Multi-energy Acquisition Description
        Tag(0x0018, 0x937F),  # Decomposition Description
        Tag(0x0018, 0x9937),  # Requested Series Description
        Tag(0x0020, 0x0027),  # Pyramid Label
        Tag(0x0032, 0x1066),  # Reason for Visit
        Tag(0x0032, 0x1067),  # Reason for Visit Code Sequence
        Tag(0x0038, 0x0014),  # Issuer of Admission ID Sequence
        Tag(0x0038, 0x0064),  # Issuer of Service Episode ID Sequence
        Tag(0x003A, 0x0329),  # Waveform Filter Description
        Tag(0x003A, 0x032B),  # Filter Lookup Table Description
        Tag(0x0040, 0x0009),  # Scheduled Procedure Step ID
        Tag(0x0040, 0x0310),  # Comments on Radiation Dose
        Tag(0x0040, 0x050A),  # Specimen Accession Number
        Tag(0x0040, 0x051A),  # Container Description
        Tag(0x0040, 0x0600),  # Specimen Short Description
        Tag(0x0040, 0x0602),  # Specimen Detailed Description
        Tag(0x0040, 0x06FA),  # Slide Identifier
        Tag(0x0040, 0x1002),  # Reason for the Requested Procedure
        Tag(0x0040, 0x100A),  # Reason for Requested Procedure Code Sequence
        Tag(0x0050, 0x001B),  # Container Component ID
        Tag(0x0050, 0x0020),  # Device Description
        Tag(0x0050, 0x0021),  # Long Device Description
        Tag(0x006A, 0x0006),  # Annotation Group Description
        Tag(0x0074, 0x1234),  # Receiving AE
        Tag(0x0074, 0x1236),  # Requesting AE
        Tag(0x0400, 0x0310),  # Certified Timestamp
        Tag(0x0400, 0x0551),  # Nonconforming Modified Attributes Sequence
        Tag(0x0400, 0x0552),  # Nonconforming Data Element Value
        Tag(0x2100, 0x0070),  # Originator
        Tag(0x3002, 0x0121),  # Position Acquisition Template Name
        Tag(0x3002, 0x0123),  # Position Acquisition Template Description
        Tag(0x3006, 0x0004),  # Structure Set Name
        Tag(0x3006, 0x0006),  # Structure Set Description
        Tag(0x3006, 0x0028),  # ROI Description
        Tag(0x3006, 0x0038),  # ROI Generation Description
        Tag(0x3006, 0x004D),  # ROI Creator Sequence
        Tag(0x3006, 0x004E),  # ROI Interpreter Sequence
        Tag(0x3006, 0x0085),  # ROI Observation Label
        Tag(0x3006, 0x0088),  # ROI Observation Description
        Tag(0x300A, 0x0003),  # RT Plan Name
        Tag(0x300A, 0x0004),  # RT Plan Description
        Tag(0x300A, 0x000B),  # Treatment Sites
        Tag(0x300A, 0x000E),  # Prescription Description
        Tag(0x300A, 0x0016),  # Dose Reference Description
        Tag(0x300A, 0x0072),  # Fraction Group Description
        Tag(0x300A, 0x00C3),  # Beam Description
        Tag(0x300A, 0x00DD),  # Bolus Description
        Tag(0x300A, 0x0196),  # Fixation Device Description
        Tag(0x300A, 0x01A6),  # Shielding Device Description
        Tag(0x300A, 0x01B2),  # Setup Technique Description
        Tag(0x300A, 0x0216),  # Source Manufacturer
        Tag(0x300A, 0x02EB),  # Compensator Description
        Tag(0x300A, 0x0676),  # Equipment Frame of Reference Description
        Tag(0x300A, 0x078E),  # Patient Treatment Preparation Procedure Parameter Description
        Tag(0x300A, 0x0792),  # Patient Treatment Preparation Method Description
        Tag(0x300A, 0x0794),  # Patient Setup Photo Description
        Tag(0x300A, 0x079A),  # Displacement Reference Label
        Tag(0x3010, 0x0036),  # Entity Name
        Tag(0x3010, 0x0037),  # Entity Description
        Tag(0x3010, 0x0061),  # Prior Treatment Dose Description
        Tag(0x4008, 0x0040),  # Results ID
        Tag(0x4008, 0x0200),  # Interpretation ID
        # PS3.15 2026c Basic Profile X additions (post-2024b).
        Tag(0x0008, 0x1301),  # Principal Diagnosis Code Sequence
        Tag(0x0008, 0x1302),  # Primary Diagnosis Code Sequence
        Tag(0x0008, 0x1303),  # Secondary Diagnoses Code Sequence
        Tag(0x0008, 0x1304),  # Histological Diagnoses Code Sequence
        Tag(0x0010, 0x0011),  # Person Names to Use Sequence
        Tag(0x0010, 0x0012),  # Name to Use
        Tag(0x0010, 0x0013),  # Name to Use Comment
        Tag(0x0010, 0x0014),  # Third Person Pronouns Sequence
        Tag(0x0010, 0x0015),  # Pronoun Code Sequence
        Tag(0x0010, 0x0016),  # Pronoun Comment
        Tag(0x0010, 0x0041),  # Gender Identity Sequence
        Tag(0x0010, 0x0042),  # Sex Parameters for Clinical Use Category Comment
        Tag(0x0010, 0x0043),  # Sex Parameters for Clinical Use Category Sequence
        Tag(0x0010, 0x0044),  # Gender Identity Code Sequence
        Tag(0x0010, 0x0045),  # Gender Identity Comment
        Tag(0x0010, 0x0046),  # Sex Parameters for Clinical Use Category Code Sequence
        Tag(0x0010, 0x0047),  # Sex Parameters for Clinical Use Category Reference
        Tag(0x0010, 0x2161),  # Ethnic Group Code Sequence
        Tag(0x0010, 0x2162),  # Ethnic Groups
        Tag(0x0018, 0x1010),  # Secondary Capture Device ID
        Tag(0x0018, 0x1011),  # Hardcopy Creation Device ID
        Tag(0x003A, 0x0020),  # Multiplex Group Label
        Tag(0x003A, 0x0203),  # Channel Label
        Tag(0x003A, 0x020C),  # Channel Derivation Description
        Tag(0x0040, 0x0556),  # Acquisition Context Description
        Tag(0x0040, 0xA034),  # Effective Start DateTime
        Tag(0x0040, 0xA035),  # Effective Stop DateTime
        Tag(0x0040, 0xB034),  # Annotation DateTime
        Tag(0x0040, 0xB036),  # Segment Definition DateTime
        Tag(0x0040, 0xB03B),  # Montage Name
        Tag(0x0040, 0xB03F),  # Montage Channel Label
        Tag(0x0040, 0xE012),  # Display URI
    }
)

# UID tags always re-hashed to deterministic replacement UIDs (UID fall-through will catch others).
UID_TAGS: frozenset[BaseTag] = frozenset(
    {
        Tag(0x0000, 0x1001),  # RequestedSOPInstanceUID
        Tag(0x0002, 0x0003),  # MediaStorageSOPInstanceUID
        Tag(0x0004, 0x1511),  # RefSOPInstanceUIDinFile
        Tag(0x0008, 0x0014),  # InstanceCreatorUID
        Tag(0x0008, 0x0018),  # SOPInstanceUID
        Tag(0x0008, 0x0058),  # FailedSOPInstanceUIDList
        Tag(0x0008, 0x010C),  # PrivateCodingSchemeCreatorUID
        Tag(0x0008, 0x010D),  # CodeSetExtensionCreatorUID
        Tag(0x0008, 0x1155),  # ReferencedSOPInstanceUID
        Tag(0x0008, 0x1195),  # TransactionUID
        Tag(0x0008, 0x9123),  # CreatorVersionUID
        Tag(0x0018, 0x1002),  # DeviceUID
        Tag(0x0020, 0x000D),  # StudyInstanceUID
        Tag(0x0020, 0x000E),  # SeriesInstanceUID
        Tag(0x0020, 0x0052),  # FrameOfReferenceUID
        Tag(0x0020, 0x0200),  # SynchronizationFrameOfReferenceUID
        Tag(0x0020, 0x9161),  # ConcatenationUID
        Tag(0x0020, 0x9164),  # DimensionOrganizationUID
        Tag(0x0028, 0x1199),  # PaletteColorLUTUID
        Tag(0x0028, 0x1214),  # LargePaletteColorLUTUid
        Tag(0x0040, 0x4023),  # RefGenPurposeSchedProcStepTransUID
        Tag(0x0040, 0xA124),  # UID
        Tag(0x0040, 0xA171),  # TrialObservationUID
        Tag(0x0040, 0xA172),  # TrialReferencedObservationUID
        Tag(0x0040, 0xA402),  # TrialObservationSubjectUID
        Tag(0x0040, 0xDB0C),  # TemplateExtensionOrganizationUID
        Tag(0x0040, 0xDB0D),  # TemplateExtensionCreatorUID
        Tag(0x0062, 0x0021),  # TrackingUID
        Tag(0x0070, 0x031A),  # FiducialUID
        Tag(0x0070, 0x1101),  # PresentationDisplayCollectionUID
        Tag(0x0070, 0x1102),  # PresentationSequenceCollectionUID
        Tag(0x0088, 0x0140),  # StorageMediaFilesetUID
        Tag(0x3006, 0x0024),  # ReferencedFrameOfReferenceUID
        Tag(0x3006, 0x00C2),  # RelatedFrameOfReferenceUID
        Tag(0x300A, 0x0013),  # DoseReferenceUID
        Tag(0x0008, 0x3010),  # IrradiationEventUID (std U: hash and retain reference)
        Tag(0x300A, 0x0054),  # TableTopPositionAlignmentUID (2026c, std U)
    }
)

# Date/DateTime tags shifted by the jitter amount, each only when present.
DATE_TAGS: frozenset[BaseTag] = frozenset(
    {
        Tag(0x0008, 0x0012),  # InstanceCreationDate
        Tag(0x0008, 0x0015),  # InstanceCoercionDateTime
        Tag(0x0008, 0x0020),  # StudyDate
        Tag(0x0008, 0x0021),  # SeriesDate
        Tag(0x0008, 0x0022),  # AcquisitionDate
        Tag(0x0008, 0x0023),  # ContentDate
        Tag(0x0008, 0x0024),  # OverlayDate
        Tag(0x0008, 0x0025),  # CurveDate
        Tag(0x0008, 0x002A),  # AcquisitionDatetime
        Tag(0x0010, 0x0030),  # PatientBirthDate
        Tag(0x0010, 0x21D0),  # LastMenstrualDate
        Tag(0x0018, 0x1012),  # DateOfSecondaryCapture
        Tag(0x0018, 0x9516),  # StartAcquisitionDateTime
        Tag(0x0018, 0x9517),  # EndAcquisitionDateTime
        Tag(0x0018, 0x9074),  # FrameAcquisitionDatetime
        Tag(0x0018, 0x9151),  # FrameReferenceDatetime
        Tag(0x0032, 0x1000),  # ScheduledStudyStartDate
        Tag(0x0032, 0x1010),  # ScheduledStudyStopDate
        Tag(0x0032, 0x1040),  # StudyArrivalDate
        Tag(0x0032, 0x1050),  # StudyCompletionDate
        Tag(0x0038, 0x001A),  # ScheduledAdmissionDate
        Tag(0x0038, 0x001C),  # ScheduledDischargeDate
        Tag(0x0038, 0x0020),  # AdmittingDate
        Tag(0x0040, 0x0002),  # SPSStartDate
        Tag(0x0040, 0x0004),  # SPSEndDate
        Tag(0x0040, 0x0244),  # PPSStartDate
        Tag(0x0040, 0x0250),  # PPSEndDate
        Tag(0x0040, 0x2004),  # IssueDateOfImagingServiceRequest
        Tag(0x0040, 0x4005),  # ScheduledProcedureStepStartDateTime
        Tag(0x0040, 0x4008),  # ScheduledProcedureStepExpirationDateTime
        Tag(0x0040, 0x4010),  # ScheduledProcedureStepModificationDateTime
        Tag(0x0040, 0x4011),  # ExpectedCompletionDateTime
        Tag(0x0040, 0x4050),  # PerformedProcedureStepStartDateTime
        Tag(0x0040, 0x4051),  # PerformedProcedureStepEndDateTime
        Tag(0x0040, 0x4052),  # ProcedureStepCancellationDateTime
        Tag(0x0040, 0xA192),  # TrialObservationDate
        Tag(0x0070, 0x0082),  # PresentationCreationDate
        Tag(0x3006, 0x0008),  # StructureSetDate
        # PS3.15 Basic Profile date/datetime attributes shifted by the jitter.
        Tag(0x0012, 0x0086),  # Ethics Committee Approval Effectiveness Start Date
        Tag(0x0012, 0x0087),  # Ethics Committee Approval Effectiveness End Date
        Tag(0x0014, 0x407E),  # Calibration Date
        Tag(0x0016, 0x008D),  # GPS Date Stamp
        Tag(0x0018, 0x1202),  # DateTime of Last Calibration
        Tag(0x0018, 0x1203),  # Calibration DateTime
        Tag(0x0018, 0x1204),  # Date of Manufacture
        Tag(0x0018, 0x1205),  # Date of Installation
        Tag(0x0018, 0x9369),  # Source Start DateTime
        Tag(0x0018, 0x936A),  # Source End DateTime
        Tag(0x0018, 0x9623),  # Functional Sync Pulse
        Tag(0x0018, 0x9701),  # Decay Correction DateTime
        Tag(0x0018, 0x9804),  # Exclusion Start DateTime
        Tag(0x0018, 0x9919),  # Instruction Performed DateTime
        Tag(0x0018, 0xA002),  # Contribution DateTime
        Tag(0x0020, 0x3403),  # Modified Image Date
        Tag(0x0038, 0x0030),  # Discharge Date
        Tag(0x003A, 0x0314),  # Impedance Measurement DateTime
        Tag(0x0040, 0xA023),  # Findings Group Recording Date (Trial)
        Tag(0x0040, 0xA033),  # Observation Start DateTime
        Tag(0x0040, 0xA082),  # Participation DateTime
        Tag(0x0040, 0xA110),  # Date of Document or Verbal Transaction (Trial)
        Tag(0x0040, 0xDB06),  # Template Version
        Tag(0x0040, 0xDB07),  # Template Local Version
        Tag(0x0040, 0xE004),  # HL7 Document Effective Time
        Tag(0x0044, 0x0004),  # Approval Status DateTime
        Tag(0x0044, 0x000B),  # Product Expiration DateTime
        Tag(0x0044, 0x0010),  # Substance Administration DateTime
        Tag(0x0044, 0x0104),  # Assertion DateTime
        Tag(0x0044, 0x0105),  # Assertion Expiration DateTime
        Tag(0x0068, 0x6226),  # Effective DateTime
        Tag(0x0068, 0x6270),  # Information Issue DateTime
        Tag(0x0072, 0x000A),  # Hanging Protocol Creation DateTime
        Tag(0x0072, 0x0061),  # Selector DA Value
        Tag(0x0072, 0x0063),  # Selector DT Value
        Tag(0x0100, 0x0420),  # SOP Authorization DateTime
        Tag(0x0400, 0x0105),  # Digital Signature DateTime
        Tag(0x0400, 0x0562),  # Attribute Modification DateTime
        Tag(0x2100, 0x0040),  # Creation Date
        Tag(0x3006, 0x002D),  # ROI DateTime
        Tag(0x3006, 0x002E),  # ROI Observation DateTime
        Tag(0x3008, 0x0024),  # Treatment Control Point Date
        Tag(0x3008, 0x0054),  # First Treatment Date
        Tag(0x3008, 0x0056),  # Most Recent Treatment Date
        Tag(0x3008, 0x0162),  # Safe Position Exit Date
        Tag(0x3008, 0x0166),  # Safe Position Return Date
        Tag(0x3008, 0x0250),  # Treatment Date
        Tag(0x300A, 0x0006),  # RT Plan Date
        Tag(0x300A, 0x022C),  # Source Strength Reference Date
        Tag(0x300A, 0x0736),  # Treatment Tolerance Violation DateTime
        Tag(0x300A, 0x073A),  # Recorded RT Control Point DateTime
        Tag(0x300A, 0x0741),  # Interlock DateTime
        Tag(0x300A, 0x0760),  # Override DateTime
        Tag(0x300C, 0x0127),  # Beam Hold Transition DateTime
        Tag(0x300E, 0x0004),  # Review Date
        Tag(0x3010, 0x004C),  # Intended Phase Start Date
        Tag(0x3010, 0x004D),  # Intended Phase End Date
        Tag(0x4008, 0x0100),  # Interpretation Recorded Date
        Tag(0x4008, 0x0108),  # Interpretation Transcription Date
        Tag(0x4008, 0x0112),  # Interpretation Approval Date
    }
)

# Membership in a profile's rules exempts these from global removal, which matters for
# profiles that set remove_unspecified=True.
KEEP_TAGS: frozenset[BaseTag] = frozenset(
    {
        Tag(0x0008, 0x0005),  # SpecificCharacterSet
        Tag(0x0008, 0x0013),  # InstanceCreationTime
        Tag(0x0008, 0x0016),  # SOPClassUID
        Tag(0x0008, 0x0030),  # StudyTime
        Tag(0x0008, 0x0031),  # SeriesTime
        Tag(0x0008, 0x0032),  # AcquisitionTime
        Tag(0x0008, 0x0033),  # ContentTime
        Tag(0x0008, 0x0034),  # OverlayTime
        Tag(0x0008, 0x0035),  # CurveTime
        Tag(0x0008, 0x0060),  # Modality
        Tag(0x0008, 0x0070),  # Manufacturer
        Tag(0x0008, 0x1150),  # RefSOPClassUID
        Tag(0x0008, 0x2130),  # EventElapsedTime
        Tag(0x0010, 0x0040),  # PatientSex
        Tag(0x0018, 0x0010),  # ContrastBolusAgent
        Tag(0x0018, 0x0015),  # BodyPartExamined
        Tag(0x0018, 0x0027),  # InterventionDrugStopTime
        Tag(0x0018, 0x0035),  # InterventionDrugStartTime
        Tag(0x0018, 0x0080),  # RepetitionTime
        Tag(0x0018, 0x0081),  # EchoTime
        Tag(0x0018, 0x0082),  # InversionTime
        Tag(0x0018, 0x1042),  # ContrastBolusStartTime
        Tag(0x0018, 0x1043),  # ContrastBolusStopTime
        Tag(0x0018, 0x1060),  # TriggerTime
        Tag(0x0018, 0x1063),  # FrameTime
        Tag(0x0018, 0x1065),  # FrameTimeVector
        Tag(0x0018, 0x1072),  # RadiopharmaceuticalStartTime
        Tag(0x0018, 0x1073),  # RadiopharmaceuticalStopTime
        Tag(0x0018, 0x1150),  # ExposureTime
        Tag(0x0018, 0x1201),  # TimeOfLastCalibration
        Tag(0x0018, 0x1480),  # TomoTime
        Tag(0x0018, 0x7014),  # DetectorActiveTime
        Tag(0x0018, 0x9079),  # InversionTimes
        Tag(0x0018, 0x9082),  # EffectiveEchoTime
        Tag(0x0020, 0x9153),  # TriggerDelayTime
        Tag(0x0032, 0x0033),  # StudyVerifiedTime
        Tag(0x0032, 0x0035),  # StudyReadTime
        Tag(0x0032, 0x1001),  # ScheduledStudyStartTime
        Tag(0x0032, 0x1011),  # ScheduledStudyStopTime
        Tag(0x0032, 0x1041),  # StudyArrivalTime
        Tag(0x0032, 0x1051),  # StudyCompletionTime
        Tag(0x0038, 0x001B),  # ScheduledAdmissionTime
        Tag(0x0038, 0x001D),  # ScheduledDischargeTime
        Tag(0x0038, 0x0021),  # AdmittingTime
        Tag(0x0040, 0x0003),  # SPSStartTime
        Tag(0x0040, 0x0005),  # SPSEndTime
        Tag(0x0040, 0x0245),  # PPSStartTime
        Tag(0x0040, 0x0251),  # PPSEndTime
        Tag(0x0040, 0xA122),  # Time
        Tag(0x0040, 0xA193),  # TrialObservationTime
        Tag(0x0054, 0x0070),  # TimeSlotVector
        Tag(0x0054, 0x0100),  # TimeSliceVector
        Tag(0x0054, 0x1300),  # FrameReferenceTime
        Tag(0x0070, 0x0083),  # PresentationCreationTime
        # PS3.15 Basic Profile time-of-day attributes retained (day granularity).
        Tag(0x0014, 0x407C),  # Calibration Time
        Tag(0x0018, 0x1014),  # Time of Secondary Capture
        Tag(0x0018, 0x700E),  # Time of Last Detector Calibration
        Tag(0x0020, 0x3405),  # Modified Image Time
        Tag(0x0038, 0x0032),  # Discharge Time
        Tag(0x0040, 0x2005),  # Issue Time of Imaging Service Request
        Tag(0x0040, 0xA024),  # Findings Group Recording Time (Trial)
        Tag(0x0040, 0xA112),  # Time of Document Creation or Verbal Transaction (Trial)
        Tag(0x0072, 0x006B),  # Selector TM Value
        Tag(0x2100, 0x0050),  # Creation Time
        Tag(0x3006, 0x0009),  # Structure Set Time
        Tag(0x3008, 0x0025),  # Treatment Control Point Time
        Tag(0x3008, 0x0164),  # Safe Position Exit Time
        Tag(0x3008, 0x0168),  # Safe Position Return Time
        Tag(0x3008, 0x0251),  # Treatment Time
        Tag(0x300A, 0x0007),  # RT Plan Time
        Tag(0x300A, 0x022E),  # Source Strength Reference Time
        Tag(0x300E, 0x0005),  # Review Time
        Tag(0x3010, 0x0085),  # Intended Fraction Start Time
        Tag(0x4008, 0x0101),  # Interpretation Recorded Time
        Tag(0x4008, 0x0109),  # Interpretation Transcription Time
        Tag(0x4008, 0x0113),  # Interpretation Approval Time
    }
)

# 40 tags emptied (set to a zero-length value) during de-identification.
# Present elements are set to a zero-length value; absent elements are not
# created.
EMPTY_TAGS: frozenset[BaseTag] = frozenset(
    {
        Tag(0x0008, 0x0090),  # ReferringPhysicianName
        Tag(0x0020, 0x0010),  # StudyID
        Tag(0x0040, 0x0550),  # SpecimenSeq
        Tag(0x0040, 0x0551),  # SpecimenIdentifier
        Tag(0x0040, 0x1003),  # RequestedProcedurePriority
        Tag(0x0040, 0x1101),  # PersonIdentificationCodeSequence
        Tag(0x0040, 0x2016),  # PlacerOrderNumber
        Tag(0x0040, 0x2017),  # FillerOrderNumber
        Tag(0x0040, 0xA073),  # VerifyingObserverSeq
        Tag(0x0040, 0xA088),  # VerifyingObserverIdentificationCodeSeq
        Tag(0x0040, 0xA123),  # PersonName
        Tag(0x0070, 0x0084),  # ContentCreatorsName
        # PS3.15 Basic Profile Z attributes emptied when present. AccessionNumber
        # (0008,0050), Patient's Name (0010,0010), and Patient's Birth Date
        # (0010,0030) are also Z, but the default profile de-identifies them by
        # hashing/jitter, so they are not listed here.
        Tag(0x0012, 0x0021),  # Clinical Trial Protocol Name
        Tag(0x0012, 0x0030),  # Clinical Trial Site ID
        Tag(0x0012, 0x0031),  # Clinical Trial Site Name
        Tag(0x0012, 0x0050),  # Clinical Trial Time Point ID
        Tag(0x0012, 0x0060),  # Clinical Trial Coordinating Center Name
        Tag(0x0040, 0x0513),  # Issuer of the Container Identifier Sequence
        Tag(0x0040, 0x0562),  # Issuer of the Specimen Identifier Sequence
        Tag(0x0040, 0x0610),  # Specimen Preparation Sequence
        Tag(0x0400, 0x0564),  # Source of Previous Values
        Tag(0x2200, 0x0002),  # Label Text
        Tag(0x2200, 0x0005),  # Barcode Value
        Tag(0x3006, 0x0026),  # ROI Name
        Tag(0x3006, 0x00A6),  # ROI Interpreter
        Tag(0x300A, 0x00B2),  # Treatment Machine Name
        Tag(0x300A, 0x0611),  # RT Accessory Holder Slot ID
        Tag(0x300A, 0x0615),  # RT Accessory Device Slot ID
        Tag(0x300A, 0x067D),  # Radiation Generation Mode Description
        Tag(0x3010, 0x000F),  # Conceptual Volume Combination Description
        Tag(0x3010, 0x0017),  # Conceptual Volume Description
        Tag(0x3010, 0x001B),  # Device Alternate Identifier
        Tag(0x3010, 0x0043),  # Manufacturer's Device Identifier
        Tag(0x3010, 0x005A),  # RT Physician Intent Narrative
        Tag(0x3010, 0x005C),  # Reason for Superseding
        Tag(0x3010, 0x007A),  # Treatment Technique Notes
        Tag(0x3010, 0x007B),  # Prescription Notes
        Tag(0x3010, 0x007F),  # Fractionation Notes
        Tag(0x3010, 0x0081),  # Prescription Notes Sequence
    }
)

# PS3.15 Basic Profile D (dummy) attributes replaced with a VR-valid non-empty
# value when present, via dummy_for_vr(). Graphic Annotation Sequence (0070,0001)
# is intentionally excluded: the Clean Graphics handling recurses into it and
# redacts nested text rather than clearing the sequence.
DUMMY_TAGS: frozenset[BaseTag] = frozenset(
    {
        Tag(0x0018, 0x11BB),  # Acquisition Field Of View Label
        Tag(0x0018, 0x9367),  # X-Ray Source ID
        Tag(0x0018, 0x9371),  # X-Ray Detector ID
        Tag(0x0034, 0x0001),  # Flow Identifier Sequence
        Tag(0x0034, 0x0002),  # Flow Identifier
        Tag(0x0034, 0x0005),  # Source Identifier
        Tag(0x0034, 0x0007),  # Frame Origin Timestamp
        Tag(0x0040, 0xB020),  # Waveform Annotation Sequence (2026c)
        Tag(0x0042, 0x0011),  # Encapsulated Document
        Tag(0x006A, 0x0005),  # Annotation Group Label
        Tag(0x0072, 0x005E),  # Selector AE Value
        Tag(0x0072, 0x005F),  # Selector AS Value
        Tag(0x0072, 0x0065),  # Selector OB Value
        Tag(0x0072, 0x0066),  # Selector LO Value
        Tag(0x0072, 0x0068),  # Selector LT Value
        Tag(0x0072, 0x006A),  # Selector PN Value
        Tag(0x0072, 0x006C),  # Selector SH Value
        Tag(0x0072, 0x006D),  # Selector UN Value
        Tag(0x0072, 0x006E),  # Selector ST Value
        Tag(0x0072, 0x0070),  # Selector UT Value
        Tag(0x0072, 0x0071),  # Selector UR Value
        Tag(0x0400, 0x0115),  # Certificate of Signer
        Tag(0x0400, 0x0563),  # Modifying System
        Tag(0x0400, 0x0565),  # Reason for the Attribute Modification
        Tag(0x2100, 0x0140),  # Destination AE
        Tag(0x3006, 0x0002),  # Structure Set Label
        Tag(0x300A, 0x0002),  # RT Plan Label
        Tag(0x300A, 0x0608),  # Treatment Position Group Label
        Tag(0x300A, 0x0619),  # Radiation Dose Identification Label
        Tag(0x300A, 0x0623),  # Radiation Dose In-Vivo Measurement Label
        Tag(0x300A, 0x062A),  # RT Tolerance Set Label
        Tag(0x300A, 0x067C),  # Radiation Generation Mode Label
        Tag(0x300A, 0x0734),  # Treatment Tolerance Violation Description
        Tag(0x300A, 0x0742),  # Interlock Description
        Tag(0x300A, 0x0783),  # Interlock Origin Description
        Tag(0x3010, 0x002D),  # Device Label
        Tag(0x3010, 0x0033),  # User Content Label
        Tag(0x3010, 0x0034),  # User Content Long Label
        Tag(0x3010, 0x0035),  # Entity Label
        Tag(0x3010, 0x0038),  # Entity Long Label
        Tag(0x3010, 0x0054),  # RT Prescription Label
        Tag(0x3010, 0x0056),  # RT Treatment Approach Label
        Tag(0x3010, 0x0077),  # Treatment Site
    }
)

# Non-per-patient identifiers hashed on their own element value (PS3.15
# Table E.1-1 action code D) via hash_value_identifier(). Hashing rather than a
# fixed dummy keeps distinct subjects and containers distinguishable while
# removing the original value. These recur across a subject's or container's
# instances, so a value-derived hash preserves those groupings.
HASH_TAGS: frozenset[BaseTag] = frozenset(
    {
        Tag(0x0012, 0x0040),  # ClinicalTrialSubjectID (LO)
        Tag(0x0012, 0x0042),  # ClinicalTrialSubjectReadingID (LO)
        Tag(0x0040, 0x0512),  # ContainerIdentifier (LO)
        Tag(0x0062, 0x0020),  # TrackingID (UT)
        Tag(0x0018, 0x1000),  # DeviceSerialNumber (LO)
        Tag(0x0018, 0x1003),  # DeviceID (LO)
    }
)


def redact_description(field_name: str, allowlist_csv: str, preserve_dates: bool) -> TagAction:
    """Return the tag action for a free-text description element.

    A per-patient value supplied on ``params.<field_name>`` takes precedence and
    is written verbatim. When it is ``None`` the element is redacted using the
    allowlist redactor, so PHI in SeriesDescription, StudyDescription, and
    ProtocolName is removed while allowlisted tokens are preserved. The action is
    present-only: a missing element is left absent.
    """
    redactor = get_text_redactor(allowlist_csv, preserve_dates)

    def action(ds: Dataset, tag: BaseTag, params: DeidParameters) -> None:
        override = getattr(params, field_name)
        if override is not None:
            if tag in ds:
                ds[tag].value = override
            return
        if tag not in ds:
            return
        value = ds[tag].value
        if value:
            ds[tag].value = cast(str, redactor.redact_text(text=str(value), track_redacted=False))
        else:
            ds[tag].value = ""

    return action


def redact_free_text(allowlist_csv: str, preserve_dates: bool) -> TagAction:
    """Return an action that redacts a free-text element against the allowlist.

    Present-only: a missing element is left absent. Raw bytes from an implicit-VR
    (UN/OB) element are decoded and the element VR is set to its dictionary VR so
    Phase 5 does not re-decode it, making redaction independent of encoding.
    """
    redactor = get_text_redactor(allowlist_csv, preserve_dates)

    def action(ds: Dataset, tag: BaseTag, params: DeidParameters) -> None:
        if tag not in ds:
            return
        elem = ds[tag]
        value = elem.value
        if isinstance(value, bytes):
            value = value.decode("ascii", errors="replace").strip().rstrip("\x00")
            try:
                elem.VR = dictionary_VR(tag)
            except KeyError:
                pass
        if not value:
            return
        elem.value = cast(str, redactor.redact_text(text=str(value), track_redacted=False))

    return action


def _build_constant_rules() -> dict[BaseTag, TagAction]:
    """Build the patient-invariant rule mapping shared by every profile.

    The action objects are stateless closures, so one instance is shared across
    all tags in each category: ``dict.fromkeys`` assigns a single value to every
    key.
    """
    rules: dict[BaseTag, TagAction] = {}
    rules.update(dict.fromkeys(PHI_REMOVE_TAGS, remove()))
    rules.update(dict.fromkeys(KEEP_TAGS, keep()))
    rules.update(dict.fromkeys(EMPTY_TAGS, empty()))

    # Mandatory evidence attributes with literal values
    rules[Tag(0x0012, 0x0062)] = set_value("YES", create_if_missing=True)  # PatientIdentityRemoved
    rules[Tag(0x0028, 0x0303)] = set_value(  # LongitudinalTemporalInformationModified
        "MODIFIED", create_if_missing=True
    )
    rules[Tag(0x0010, 0x1010)] = cap_age(89, "090Y")  # PatientAge
    rules[Tag(0x0010, 0x1020)] = round_decimal_string("0.05")  # PatientSize, nearest 5 cm
    rules[Tag(0x0010, 0x1030)] = round_decimal_string("5")  # PatientWeight, nearest 5 kg

    # Dummy-value attributes (PS3.15 Table E.1-1 action code D): replaced with a
    # fixed, VR-valid non-empty value when present. create_if_missing is left at
    # its default (False), since D applies only to existing elements.
    rules[Tag(0x0012, 0x0010)] = set_value("REMOVED")  # ClinicalTrialSponsorName (LO)
    rules[Tag(0x0012, 0x0081)] = set_value("REMOVED")  # ClinicalTrialProtocolEthicsCommitteeName (LO)
    # ContextGroupVersion/LocalVersion are code-system version stamps, not patient
    # events, so a fixed dummy DT is used rather than the patient-scoped jitter.
    rules[Tag(0x0008, 0x0106)] = set_value("20000101000000")  # ContextGroupVersion (DT)
    rules[Tag(0x0008, 0x0107)] = set_value("20000101000000")  # ContextGroupLocalVersion (DT)

    # Sequences absent from the removal set above (Radiopharmaceutical Information
    # (0054,0016), Referenced Series/Image/Instance) are retained. The engine
    # recurses into every sequence and hashes nested UIDs (RefSOPInstanceUID)
    # while keeping the class UID, preserving cross-object references with hashed
    # identifiers.
    return rules


[docs] def default_profile( settings: ProfileSettings | None = None, *, deid_method: str = "DICOM-PS3.15E-Basic", preserve_dates: bool = False, ) -> DeidProfile: """Construct a full PS3.15E de-identification profile. The returned profile is a patient-invariant policy object. Per-patient identity values (PatientID, AccessionNumber, StudyID, PatientName, the free-text description overrides, and the date jitter) are supplied at apply time via :class:`DeidParameters`. When a caller supplies no PatientID, PatientName, or AccessionNumber, the original element value is hashed with ``settings.hash_salt`` and the study identifier; PatientName reuses the PatientID hash. A fail-safe placeholder is written only when there is no value to hash. The free-text description fields accept a per-patient value that takes precedence; when it is ``None`` the engine redacts the corresponding element from the dataset at apply time using ``settings.allowlist_csv``. ``preserve_dates`` selects a date-preserving redactor for the free-text fields and does not alter the profile's date-jitter behavior. """ settings = settings or ProfileSettings() uid_action = hash_uid(settings.uid_root, use_study_salt=True) shift = if_exists(jitter_date()) rules = _build_constant_rules() rules.update(dict.fromkeys(UID_TAGS, uid_action)) rules.update(dict.fromkeys(DATE_TAGS, shift)) rules.update(dict.fromkeys(DUMMY_TAGS, dummy_for_vr(settings.uid_root, use_study_salt=True))) rules.update(dict.fromkeys(HASH_TAGS, hash_value_identifier(salt=settings.hash_salt))) # PatientName runs before the PatientID rule so it hashes the original # PatientID element, yielding the same hash the PatientID rule then writes. rules[Tag(0x0010, 0x0010)] = hash_identifier_param( # PatientName "patient_name", salt=settings.hash_salt, fallback_field="patient_id", source_tag=Tag(0x0010, 0x0020) ) rules[Tag(0x0010, 0x0020)] = hash_identifier_param("patient_id", salt=settings.hash_salt) # PatientID rules[Tag(0x0008, 0x0050)] = hash_identifier_param("accession_number", salt=settings.hash_salt) # AccessionNumber rules[Tag(0x0008, 0x103E)] = redact_description( # SeriesDescription "series_description", settings.allowlist_csv, preserve_dates ) rules[Tag(0x0008, 0x1030)] = redact_description( # StudyDescription "study_description", settings.allowlist_csv, preserve_dates ) rules[Tag(0x0018, 0x1030)] = redact_description( # ProtocolName "protocol_name", settings.allowlist_csv, preserve_dates ) # GSPS 2D annotation text redact = redact_free_text(settings.allowlist_csv, preserve_dates) rules[Tag(0x0070, 0x0006)] = redact # UnformattedTextValue (ST) rules[Tag(0x0070, 0x0289)] = redact # TickLabel (SH) rules[Tag(0x0040, 0xA160)] = redact # TextValue (UT), KO/SR content free text # Presentation State content identification (may embed dates and operator identifiers). # The text redactor preserves numeric tokens (measurements), so it cannot strip an # operator number from these fields; the standard actions are applied instead. rules[Tag(0x0070, 0x0080)] = set_value("DEIDENTIFIED") # ContentLabel (CS), Type 1 -- dummy value rules[Tag(0x0070, 0x0081)] = empty() # ContentDescription (LO), Type 2 # Mandatory evidence attributes -- created if missing and then set here rules[Tag(0x0012, 0x0020)] = set_param( # ClinicalTrialProtocolID "study_id", default=DEFAULT_STUDY_ID, create_if_missing=True ) rules[Tag(0x0012, 0x0063)] = append_value(deid_method, create_if_missing=True) # DeIdentificationMethod return DeidProfile( name="DICOM-PS3.15E-Basic", rules=rules, keep_groups=frozenset(), remove_private=True, remove_curves=True, remove_overlays=True, modifies_dates=True, allowlist_csv=settings.allowlist_csv, hash_salt=settings.hash_salt, uid_root=settings.uid_root, uid_use_study_salt=True, preserved_private_specs=PRESERVED_PRIVATE_SPECS, deid_options=frozenset({"113104", "113105", "113108"}), )