From 2b3f77675aaf80cd2c58b260c70a039b77f90384 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Thu, 12 Dec 2024 17:37:26 +0000 Subject: [PATCH 1/3] Add dead-time time-block Needed to change all time-blocks to use TimeInterval as opposed to only giving the start time. Fixes #75 --- cpp/petsird_analysis.cpp | 2 +- cpp/petsird_generator.cpp | 3 +- model/Protocol.yml | 56 +++++++++++++++++++++++++++++-------- python/petsird_analysis.py | 4 +-- python/petsird_generator.py | 6 ++-- 5 files changed, 54 insertions(+), 17 deletions(-) diff --git a/cpp/petsird_analysis.cpp b/cpp/petsird_analysis.cpp index 5f34d64..0ee669d 100644 --- a/cpp/petsird_analysis.cpp +++ b/cpp/petsird_analysis.cpp @@ -122,7 +122,7 @@ main(int argc, char const* argv[]) if (std::holds_alternative(time_block)) { auto& event_time_block = std::get(time_block); - last_time = event_time_block.start; + last_time = event_time_block.time_interval.stop; num_prompts += event_time_block.prompt_events.size(); if (event_time_block.delayed_events) num_delayeds += event_time_block.delayed_events->size(); diff --git a/cpp/petsird_generator.cpp b/cpp/petsird_generator.cpp index c47fbcf..87ed6d4 100644 --- a/cpp/petsird_generator.cpp +++ b/cpp/petsird_generator.cpp @@ -310,7 +310,8 @@ main(int argc, char* argv[]) const auto num_prompts_this_block = poisson(gen); const auto prompts_this_block = get_events(header, num_prompts_this_block); petsird::EventTimeBlock time_block; - time_block.start = t * header.scanner.event_time_block_duration; + time_block.time_interval.start = t * header.scanner.event_time_block_duration; + time_block.time_interval.stop = (t + 1) * header.scanner.event_time_block_duration; time_block.prompt_events = prompts_this_block; writer.WriteTimeBlocks(time_block); } diff --git a/model/Protocol.yml b/model/Protocol.yml index c042f2e..5453eac 100644 --- a/model/Protocol.yml +++ b/model/Protocol.yml @@ -14,14 +14,12 @@ Header: !record # types of timeBlocks # TODO more types could be needed -TimeBlock: [EventTimeBlock, ExternalSignalTimeBlock, BedMovementTimeBlock, GantryMovementTimeBlock] +TimeBlock: [EventTimeBlock, ExternalSignalTimeBlock, BedMovementTimeBlock, GantryMovementTimeBlock, DeadTimeTimeBlock] EventTimeBlock: !record fields: - # start time since ExamInformation.startOfAcquisition in ms - # Note: duration is given by ScannerInformation.eventTimeBlockDuration - start: uint - # TODO encode end time? + # time interval for this time block + timeInterval: TimeInterval # list of prompts in this time block # TODO might be better to use !array promptEvents: CoincidenceEvent* @@ -50,8 +48,8 @@ ExternalSignalType: !record ExternalSignalTimeBlock: !record fields: - # start time since ExamInformation.startOfAcquisition in ms - start: uint + # time interval for this time block + timeInterval: TimeInterval # refer to ExternalSignalType.id signalID: uint # Note for triggers, this field is to be ignored @@ -59,12 +57,48 @@ ExternalSignalTimeBlock: !record BedMovementTimeBlock: !record fields: - # start time since ExamInformation.startOfAcquisition in ms - start: uint + # time interval for this time block + timeInterval: TimeInterval transform: RigidTransformation GantryMovementTimeBlock: !record fields: - # start time since ExamInformation.startOfAcquisition in ms - start: uint + # time interval for this time block + timeInterval: TimeInterval transform: RigidTransformation + +# A time-block that stores the dead-time information for the given interval +# TODO require that the complete scanning interval is "covered"? +# Dead-time is encoded as "alive_time_fraction", i.e. 1 - dead_time_fraction +# A component-based model is used, where the total alive-time for an pair of DetectorIds +# is computed as the product of the (singles) alive-time of each detector times the +# coincidence-alive-time of the corresponding module-pair: +# aliveTimeFraction(detectorId0, energyIdx0, detectorId1, energyIdx1) = +# singlesAliveTimeFraction[detectorId0, energyIdx0] * +# singlesAliveTimeFraction[detectorId1, energyIdx1] * +# moduleCoincidenceAliveTimeFraction[module(detectorId0], module(energyIdx0)] +DeadTimeTimeBlock: !record + fields: + # time interval for this time block + timeInterval: TimeInterval + # Singles dead-time array for the time interval, stored as fractions of "alive time" + # (1 means no dead-time, 0 means no detected counts) + # If sizeOfEnergyIdxDimension == 1, the fraction is assumed to be the same + # for all energy windows. + # Constraint: sizeOfDetectorIdDimension == number of all possible unique DetectorIds + # Constraint: (sizeOfEnergyIdxDimension == 1) or (sizeOfEnergyIdxDimension == ScannerInformation.numberOfEnergyBins) + singlesAliveTimeFraction: !array + items: float + dimensions: [detectorId, energyIdx] + # coincidence dead-time array for 2 modules in coincidence, stored as fractions of "alive time" + # (1 means no dead-time, 0 means no detected coincidences) + # If the size of this 2D array is (1,1), it is assumed that the corresponding alive-fraction is the same for all modules. + # Constraint: size(moduleCoincidenceAliveTimeFraction, 0) == 1 or total number of modules of all types + # Constraint: size(moduleCoincidenceAliveTimeFraction, 1) == 1 or total number of modules of all types + moduleCoincidenceAliveTimeFraction: !array + items: float + dimensions: [moduleId0, moduleId1] + # TODO currently commented out due to yardl proble=m (?) + # computedFields: + # sizeOfDetectorIdDimension: size(singlesAliveTimeFraction, 'detectorId') + # sizeOfEnergyIdxDimension: size(singlesAliveTimeFraction, 'energyIdx') diff --git a/python/petsird_analysis.py b/python/petsird_analysis.py index 27c3075..da4d721 100644 --- a/python/petsird_analysis.py +++ b/python/petsird_analysis.py @@ -72,11 +72,11 @@ def parserCreator(): last_time = 0 for time_block in reader.read_time_blocks(): if isinstance(time_block, petsird.TimeBlock.EventTimeBlock): - last_time = time_block.value.start + last_time = time_block.value.time_interval.stop num_prompts += len(time_block.value.prompt_events) if time_block.value.delayed_events is not None: num_delayeds += len(time_block.value.delayed_events) - print("===================== Events in time block from ", + print("===================== Events in time block until ", last_time, " ==============") for event in time_block.value.prompt_events: energy_1 += energy_mid_points[event.energy_indices[0]] diff --git a/python/petsird_generator.py b/python/petsird_generator.py index 9c89876..5ba4629 100644 --- a/python/petsird_generator.py +++ b/python/petsird_generator.py @@ -257,12 +257,14 @@ def get_events(header: petsird.Header, header = get_header() writer.write_header(header) for t in range(NUMBER_OF_TIME_BLOCKS): - start = t * header.scanner.event_time_block_duration + time_interval = petsird.TimeInterval( + start=t * header.scanner.event_time_block_duration, + stop=(t + 1) * header.scanner.event_time_block_duration) num_prompts_this_block = rng.poisson(COUNT_RATE) prompts_this_block = list( get_events(header, num_prompts_this_block)) # Normally we'd write multiple blocks, but here we have just one, # so let's write a tuple with just one element writer.write_time_blocks((petsird.TimeBlock.EventTimeBlock( - petsird.EventTimeBlock(start=start, + petsird.EventTimeBlock(time_interval=time_interval, prompt_events=prompts_this_block)), )) From b117de6bef47569b5a43f26e708b6333ecb31d20 Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Thu, 12 Dec 2024 18:15:51 +0000 Subject: [PATCH 2/3] fix indentation for computed fields --- model/Protocol.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/model/Protocol.yml b/model/Protocol.yml index 5453eac..60466d2 100644 --- a/model/Protocol.yml +++ b/model/Protocol.yml @@ -98,7 +98,7 @@ DeadTimeTimeBlock: !record moduleCoincidenceAliveTimeFraction: !array items: float dimensions: [moduleId0, moduleId1] - # TODO currently commented out due to yardl proble=m (?) - # computedFields: - # sizeOfDetectorIdDimension: size(singlesAliveTimeFraction, 'detectorId') - # sizeOfEnergyIdxDimension: size(singlesAliveTimeFraction, 'energyIdx') + + computedFields: + sizeOfDetectorIdDimension: size(singlesAliveTimeFraction, 'detectorId') + sizeOfEnergyIdxDimension: size(singlesAliveTimeFraction, 'energyIdx') From ff78514d1447ae3d0559157194b7bd07c6fb9c9b Mon Sep 17 00:00:00 2001 From: Kris Thielemans Date: Fri, 13 Dec 2024 15:52:16 +0000 Subject: [PATCH 3/3] changes to comments --- model/Protocol.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/model/Protocol.yml b/model/Protocol.yml index 60466d2..cf55ba2 100644 --- a/model/Protocol.yml +++ b/model/Protocol.yml @@ -19,6 +19,7 @@ TimeBlock: [EventTimeBlock, ExternalSignalTimeBlock, BedMovementTimeBlock, Gantr EventTimeBlock: !record fields: # time interval for this time block + # Constraint: all event-time-blocks need to have the same duration, see ScannerInformation.even_time_block_duration timeInterval: TimeInterval # list of prompts in this time block # TODO might be better to use !array @@ -76,7 +77,7 @@ GantryMovementTimeBlock: !record # aliveTimeFraction(detectorId0, energyIdx0, detectorId1, energyIdx1) = # singlesAliveTimeFraction[detectorId0, energyIdx0] * # singlesAliveTimeFraction[detectorId1, energyIdx1] * -# moduleCoincidenceAliveTimeFraction[module(detectorId0], module(energyIdx0)] +# moduleCoincidenceAliveTimeFraction[module(detectorId0), module(energyIdx0)] DeadTimeTimeBlock: !record fields: # time interval for this time block @@ -95,6 +96,7 @@ DeadTimeTimeBlock: !record # If the size of this 2D array is (1,1), it is assumed that the corresponding alive-fraction is the same for all modules. # Constraint: size(moduleCoincidenceAliveTimeFraction, 0) == 1 or total number of modules of all types # Constraint: size(moduleCoincidenceAliveTimeFraction, 1) == 1 or total number of modules of all types + # Constraint: this matrix has to be symmetric moduleCoincidenceAliveTimeFraction: !array items: float dimensions: [moduleId0, moduleId1]