Skip to content

Commit 202728a

Browse files
let test participants hardcode images
1 parent 5cb84fa commit 202728a

File tree

7 files changed

+89
-62
lines changed

7 files changed

+89
-62
lines changed

app/data/test-scenarios.js

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
/**
44
* Test scenarios define specific participants and events that should always exist
55
* in the generated data. This ensures we have consistent test cases.
6-
*
6+
*
77
* Only specify what needs to be consistent - any unspecified fields will be randomly generated.
88
* This allows natural variation while maintaining key test conditions.
99
*/
@@ -20,12 +20,15 @@ module.exports = [
2020
ethnicBackground: null,
2121
},
2222
extraNeeds: ['Wheelchair user'],
23-
defaultRiskLevel: 'routine',
24-
},
25-
scheduling: {
26-
whenRelativeToToday: 0,
27-
status: 'event_scheduled',
28-
approximateTime: '10:30',
23+
config: {
24+
defaultRiskLevel: 'routine',
25+
repeatView: 'RMLO',
26+
scheduling: {
27+
whenRelativeToToday: 0,
28+
status: 'event_scheduled',
29+
approximateTime: '10:30',
30+
},
31+
},
2932
},
3033
},
3134
{
@@ -40,13 +43,15 @@ module.exports = [
4043
ethnicBackground: null,
4144
},
4245
extraNeeds: null,
43-
defaultRiskLevel: 'routine',
44-
},
45-
scheduling: {
46-
whenRelativeToToday: 0,
47-
status: 'event_checked_in',
48-
approximateTime: '11:30',
49-
// slotIndex: 20,
46+
config: {
47+
defaultRiskLevel: 'routine',
48+
scheduling: {
49+
whenRelativeToToday: 0,
50+
status: 'event_checked_in',
51+
approximateTime: '11:30',
52+
// slotIndex: 20,
53+
},
54+
},
5055
},
5156
},
5257
]

app/lib/generate-seed-data.js

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const testScenarios = require('../data/test-scenarios')
2828
// Create an index of participants by risk level for efficient lookup
2929
const createParticipantIndices = (participants, clinicDate, events = []) => {
3030
// console.time('Creating participant indices')
31-
31+
3232
const riskLevelIndex = {}
3333
const screeningHistoryIndex = new Map()
3434

@@ -43,13 +43,13 @@ const createParticipantIndices = (participants, clinicDate, events = []) => {
4343
if (age >= ageRange.lower && age <= ageRange.upper) {
4444
// Initialize array for this risk level if needed
4545
riskLevelIndex[riskLevel] = riskLevelIndex[riskLevel] || []
46-
46+
4747
// Add participant to their risk level index
4848
riskLevelIndex[riskLevel].push(participant)
4949
}
5050

5151
// Track all screening events for this participant
52-
const participantEvents = events.filter(event =>
52+
const participantEvents = events.filter(event =>
5353
event.participantId === participant.id
5454
)
5555
screeningHistoryIndex.set(participant.id, participantEvents)
@@ -83,12 +83,12 @@ const findNearestSlot = (slots, targetTime) => {
8383
return eligibleSlots.reduce((nearest, slot) => {
8484
const slotTime = dayjs(slot.dateTime)
8585
const slotMinutes = slotTime.hour() * 60 + slotTime.minute()
86-
86+
8787
if (!nearest) return slot
8888

8989
const currentDiff = Math.abs(targetMinutes - slotMinutes)
9090
const nearestDiff = Math.abs(
91-
targetMinutes -
91+
targetMinutes -
9292
(dayjs(nearest.dateTime).hour() * 60 + dayjs(nearest.dateTime).minute())
9393
)
9494

@@ -105,9 +105,9 @@ const generateClinicsForDay = (date, allParticipants, unit, usedParticipantsInSn
105105
const isRecentSnapshot = dayjs(date).isAfter(dayjs().subtract(1, 'month'))
106106

107107
// Only look for test scenarios in recent snapshots
108-
const testScenariosForDay = isRecentSnapshot
108+
const testScenariosForDay = isRecentSnapshot
109109
? testScenarios.filter(scenario => {
110-
const targetDate = dayjs().startOf('day').add(scenario.scheduling.whenRelativeToToday, 'day')
110+
const targetDate = dayjs().startOf('day').add(scenario.participant.config.scheduling.whenRelativeToToday, 'day')
111111
return targetDate.isSame(dayjs(date).startOf('day'), 'day')
112112
})
113113
: []
@@ -124,14 +124,14 @@ const generateClinicsForDay = (date, allParticipants, unit, usedParticipantsInSn
124124
// For test scenarios, only use first clinic of the day
125125
if (testScenariosForDay.length > 0 && newClinics.length > 0) {
126126
const firstClinic = newClinics[0]
127-
127+
128128
testScenariosForDay.forEach(scenario => {
129129
const participant = participants.find(p => p.id === scenario.participant.id)
130130
if (!participant) return
131131

132-
const slot = scenario.scheduling.slotIndex !== undefined
133-
? firstClinic.slots[scenario.scheduling.slotIndex]
134-
: findNearestSlot(firstClinic.slots, scenario.scheduling.approximateTime)
132+
const slot = scenario.participant.config.scheduling.slotIndex !== undefined
133+
? firstClinic.slots[scenario.participant.config.scheduling.slotIndex]
134+
: findNearestSlot(firstClinic.slots, scenario.participant.config.scheduling.approximateTime)
135135

136136
if (!slot) {
137137
console.log(`Warning: Could not find suitable slot for test participant ${participant.id}`)
@@ -143,7 +143,7 @@ const generateClinicsForDay = (date, allParticipants, unit, usedParticipantsInSn
143143
participant,
144144
clinic: firstClinic,
145145
outcomeWeights: config.screening.outcomes[firstClinic.clinicType],
146-
forceStatus: scenario.scheduling.status,
146+
forceStatus: scenario.participant.config.scheduling.status,
147147
})
148148

149149
events.push(event)
@@ -164,7 +164,7 @@ const generateClinicsForDay = (date, allParticipants, unit, usedParticipantsInSn
164164
const selectedRiskLevel = weighted.select(
165165
Object.fromEntries(
166166
availableRiskLevels.map(level => [
167-
level,
167+
level,
168168
riskLevels[level].weight
169169
])
170170
)
@@ -175,7 +175,7 @@ const generateClinicsForDay = (date, allParticipants, unit, usedParticipantsInSn
175175
.filter(p => !usedParticipantsInSnapshot.has(p.id))
176176

177177
if (availableParticipants.length === 0) {
178-
178+
179179
const newParticipant = generateParticipant({
180180
ethnicities,
181181
breastScreeningUnits: [unit],
@@ -278,7 +278,7 @@ const generateData = async () => {
278278
console.log(`Generating data for ${unit.name}...`)
279279

280280
let unitEvents = [] // Track events for this unit across snapshots
281-
281+
282282
// Process each snapshot
283283
const unitData = snapshots.map(dates => {
284284
// Create a set to track used participants for this entire snapshot
@@ -291,7 +291,7 @@ const generateData = async () => {
291291
// console.log(`- ${indices.screeningHistoryIndex.size} participants with history`)
292292

293293
// Process each day in the snapshot
294-
const snapshotData = dates.map(date =>
294+
const snapshotData = dates.map(date =>
295295
generateClinicsForDay(date, participants, unit, usedParticipantsInSnapshot, indices)
296296
)
297297

app/lib/generators/event-generator.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ const generateEvent = ({ slot, participant, clinic, outcomeWeights, forceStatus
141141
// Add mammogram images for completed events
142142
event.mammogramData = generateMammogramImages({
143143
startTime: actualStartTime,
144-
isSeedData: true
144+
isSeedData: true,
145+
config: participant.config
145146
})
146147
}
147148

app/lib/generators/mammogram-generator.js

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,10 @@ const REPEAT_REASONS = [
2020
'technical fault'
2121
]
2222

23-
// Probability settings for repeats
24-
const REPEAT_PROBABILITIES = {
25-
needsRepeat: 0.35, // 15% chance of needing any repeat
26-
multipleRepeats: 0.2 // 20% chance of needing more than one repeat if already repeating
23+
// Default probability settings
24+
const DEFAULT_PROBABILITIES = {
25+
viewMissing: 0.05, // 5% chance of a view being missing
26+
needsRepeat: 0.15 // 15% chance of one view needing a repeat
2727
}
2828

2929
const generateViewKey = (side, view) => {
@@ -46,14 +46,14 @@ const generateImageUrl = (side, view, accessionNumber) => {
4646
* @param {string} params.accessionBase - Base accession number
4747
* @param {number} params.startIndex - Starting index for image numbering
4848
* @param {string} params.startTime - Start timestamp
49-
* @param {boolean} params.isSeedData - Whether generating seed data or live data
49+
* @param {boolean} params.isSeedData - Whether generating seed data
50+
* @param {boolean} [params.needsRepeat] - Force this view to be repeated
5051
* @returns {Object} View data with images
5152
*/
52-
const generateViewImages = ({ side, view, accessionBase, startIndex, startTime, isSeedData }) => {
53+
const generateViewImages = ({ side, view, accessionBase, startIndex, startTime, isSeedData, needsRepeat = false }) => {
5354
let currentIndex = startIndex
5455
let currentTime = dayjs(startTime)
5556
const images = []
56-
const needsRepeat = Math.random() < REPEAT_PROBABILITIES.needsRepeat
5757

5858
// Generate initial image
5959
images.push({
@@ -62,20 +62,16 @@ const generateViewImages = ({ side, view, accessionBase, startIndex, startTime,
6262
url: generateImageUrl(side, view, `${accessionBase}/${currentIndex}`)
6363
})
6464

65-
// Generate repeats if needed
65+
// Generate repeat if needed
6666
if (needsRepeat) {
67-
const repeatCount = Math.random() < REPEAT_PROBABILITIES.multipleRepeats ? 2 : 1
67+
currentIndex++
68+
currentTime = currentTime.add(faker.number.int({ min: 25, max: 50 }), 'seconds')
6869

69-
for (let i = 0; i < repeatCount; i++) {
70-
currentIndex++
71-
currentTime = currentTime.add(faker.number.int({ min: 25, max: 50 }), 'seconds')
72-
73-
images.push({
74-
timestamp: currentTime.toISOString(),
75-
accessionNumber: `${accessionBase}/${currentIndex}`,
76-
url: generateImageUrl(side, view, `${accessionBase}/${currentIndex}`)
77-
})
78-
}
70+
images.push({
71+
timestamp: currentTime.toISOString(),
72+
accessionNumber: `${accessionBase}/${currentIndex}`,
73+
url: generateImageUrl(side, view, `${accessionBase}/${currentIndex}`)
74+
})
7975
}
8076

8177
return {
@@ -94,24 +90,50 @@ const generateViewImages = ({ side, view, accessionBase, startIndex, startTime,
9490
* @param {Object} options - Generation options
9591
* @param {Date|string} [options.startTime] - Starting timestamp (defaults to now)
9692
* @param {boolean} [options.isSeedData=false] - Whether generating seed data
93+
* @param {Object} [options.config] - Optional configuration for specific scenarios
94+
* @param {string} [options.config.repeatView] - Force a specific view to be repeated (e.g. 'RMLO')
95+
* @param {string[]} [options.config.missingViews] - Array of views to omit (e.g. ['RMLO'])
96+
* @param {Object} [options.probabilities] - Override default probabilities
9797
* @returns {Object} Complete mammogram data
9898
*/
99-
const generateMammogramImages = ({ startTime = new Date(), isSeedData = false } = {}) => {
99+
const generateMammogramImages = ({
100+
startTime = new Date(),
101+
isSeedData = false,
102+
config = {},
103+
probabilities = DEFAULT_PROBABILITIES
104+
} = {}) => {
100105
const accessionBase = faker.number.int({ min: 100000000, max: 999999999 }).toString()
101106
let currentIndex = 1
102107
let currentTime = dayjs(startTime)
103108
const views = {}
104109

110+
// Determine which view gets repeated (if any)
111+
let viewToRepeat = null
112+
if (config.repeatView) {
113+
viewToRepeat = config.repeatView
114+
} else if (Math.random() < probabilities.needsRepeat) {
115+
viewToRepeat = faker.helpers.arrayElement(['RMLO', 'RCC', 'LCC', 'LMLO'])
116+
}
117+
105118
// Generate each standard view
106119
STANDARD_VIEWS.forEach(({ side, view }) => {
107120
const viewKey = generateViewKey(side, view)
121+
const viewShortWithSide = `${side === 'right' ? 'R' : 'L'}${view === 'mediolateral oblique' ? 'MLO' : 'CC'}`
122+
123+
// Skip if this view is in missingViews config
124+
if (config.missingViews?.includes(viewShortWithSide) ||
125+
(!config.missingViews && Math.random() < probabilities.viewMissing)) {
126+
return
127+
}
128+
108129
const viewData = generateViewImages({
109130
side,
110131
view,
111132
accessionBase,
112133
startIndex: currentIndex,
113134
startTime: currentTime.toISOString(),
114-
isSeedData
135+
isSeedData,
136+
needsRepeat: viewToRepeat === viewShortWithSide
115137
})
116138

117139
views[viewKey] = viewData
@@ -132,7 +154,7 @@ const generateMammogramImages = ({ startTime = new Date(), isSeedData = false }
132154
views,
133155
metadata: {
134156
totalImages,
135-
standardViewsCompleted: true,
157+
standardViewsCompleted: Object.keys(views).length === 4,
136158
startTime: allTimestamps[0],
137159
endTime: allTimestamps[allTimestamps.length - 1]
138160
}

app/lib/generators/participant-generator.js

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const generateEthnicity = (ethnicities) => {
4747
}
4848

4949
const ethnicGroup = weighted.select(Object.keys(ethnicities), [0.85, 0.08, 0.03, 0.02, 0.02])
50-
50+
5151
// 20% chance of having background set to "Not provided"
5252
if (Math.random() < 0.2) {
5353
return {
@@ -262,18 +262,17 @@ const generateParticipant = ({
262262
const id = generateId()
263263

264264
// Determine risk level first as it affects age generation
265-
const participantRiskLevel = overrides?.defaultRiskLevel || riskLevel || pickRiskLevel()
265+
const participantRiskLevel = overrides?.config?.defaultRiskLevel || riskLevel || pickRiskLevel()
266266

267267
// const participantRiskLevel = 'moderate'
268268

269269
// First get or generate BSU
270-
const assignedBSU = overrides?.assignedBSU
270+
const assignedBSU = overrides?.assignedBSU
271271
? breastScreeningUnits.find(bsu => bsu.id === overrides.assignedBSU)
272272
: faker.helpers.arrayElement(breastScreeningUnits)
273273

274274
// Generate ethnicity data using new function
275275
const ethnicityData = generateEthnicity(ethnicities)
276-
console.log(ethnicityData)
277276

278277
// Generate base random participant first
279278
const baseParticipant = {

app/routes/events.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,6 @@ module.exports = router => {
9595
const canBeginScreening = data.beginScreening
9696
delete data.beginScreening
9797

98-
// console.log({data})
99-
10098
const eventIndex = req.session.data.events.findIndex(e => e.id === eventId)
10199

102100
if (!canBeginScreening) {
@@ -126,8 +124,9 @@ module.exports = router => {
126124

127125
// Specific route for imaging view
128126
router.get('/clinics/:clinicId/events/:eventId/imaging', (req, res) => {
129-
const { eventId } = req.params
127+
const { clinicId, eventId } = req.params
130128
const event = req.session.data.events.find(e => e.id === eventId)
129+
const eventData = getEventData(req.session.data, clinicId, eventId)
131130

132131
// If no mammogram data exists, generate it
133132
if (!event.mammogramData) {
@@ -136,7 +135,8 @@ module.exports = router => {
136135
const startTime = dayjs().subtract(3, 'minutes').toDate()
137136
const mammogramData = generateMammogramImages({
138137
startTime,
139-
isSeedData: false
138+
isSeedData: false,
139+
config: eventData?.participant?.config,
140140
})
141141

142142
// Update both session data and locals

app/views/events/mammography/imaging.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
{% block pageContent %}
1212

13-
{{ participant | log }}
13+
{{ participant | log("Participant") }}
1414

1515
{% set unit = data.breastScreeningUnits | findById(clinic.breastScreeningUnitId) %}
1616

0 commit comments

Comments
 (0)