Skip to content

Commit 7dbc35f

Browse files
Add support for different seed profiles (#257)
1 parent b85b001 commit 7dbc35f

20 files changed

Lines changed: 1199 additions & 110 deletions

app.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ const viewsPath = [
1919

2020
const entryPoints = [
2121
'app/assets/sass/main.scss',
22-
'app/assets/javascript/*.js'
22+
'app/assets/javascript/*.js',
23+
'app/data/generated/**/*.json'
2324
]
2425

2526
async function init() {

app/assets/javascript/main.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,19 @@ document.addEventListener('DOMContentLoaded', () => {
9595
// Handle reset data in background
9696
setupResetSessionLink()
9797

98+
// Reading workflow: auto-dismiss the opinion banner after a delay
99+
const opinionBanner = document.querySelector('[data-reading-opinion-banner]')
100+
if (opinionBanner) {
101+
const delay = parseInt(opinionBanner.dataset.autoCloseDelay, 10) || 3000
102+
setTimeout(() => {
103+
opinionBanner.classList.add('app-reading-opinion-banner--fade-out')
104+
// Remove from DOM after the CSS transition (0.2s) completes
105+
opinionBanner.addEventListener('transitionend', () => {
106+
opinionBanner.remove()
107+
}, { once: true })
108+
}, delay)
109+
}
110+
98111
// Reading workflow: delay initial opinion controls to prevent premature clicks
99112
// When first arriving on a case, users should be prevented from giving an opinion for a period of time. On NBSS this is 30 seconds, but for the prototype is set to 5 seconds to avoid being annoying whilst testing.
100113
const opinionForm = document.querySelector('[data-reading-opinion-form]')

app/data/session-data-defaults.js

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ const { needsRegeneration } = require('../lib/utils/regenerate-data')
1414
const config = require('../config')
1515
const repeatReasons = require('./repeat-reasons')
1616
const symptomTypes = require('./symptom-types')
17+
const {
18+
DEFAULT_SEED_DATA_PROFILE,
19+
SEED_DATA_PROFILES,
20+
createSeedProfilesState
21+
} = require('../lib/generators/seed-profiles')
1722

1823
// Check if generated data folder exists and create if needed
1924
const generatedDataPath = path.join(__dirname, 'generated')
@@ -26,6 +31,7 @@ let clinics = []
2631
let events = []
2732
let generationInfo = {
2833
generatedAt: 'Never',
34+
seedDataProfile: DEFAULT_SEED_DATA_PROFILE,
2935
stats: { participants: 0, clinics: 0, events: 0 }
3036
}
3137

@@ -39,16 +45,33 @@ if (fs.existsSync(generationInfoPath)) {
3945
}
4046
}
4147

48+
if (!generationInfo.seedDataProfile) {
49+
generationInfo.seedDataProfile = DEFAULT_SEED_DATA_PROFILE
50+
}
51+
52+
if (!SEED_DATA_PROFILES[generationInfo.seedDataProfile]) {
53+
generationInfo.seedDataProfile = DEFAULT_SEED_DATA_PROFILE
54+
}
55+
4256
// Generate or load data
4357
if (needsRegeneration(generationInfo)) {
4458
console.log('Generating new seed data...')
45-
require('../lib/generate-seed-data.js')()
59+
require('../lib/generate-seed-data.js')({
60+
seedDataProfile: generationInfo.seedDataProfile
61+
})
4662

47-
// Save generation info
48-
fs.writeFileSync(
49-
generationInfoPath,
50-
JSON.stringify({ generatedAt: new Date().toISOString() })
51-
)
63+
// Reload generation info written by the generator
64+
if (fs.existsSync(generationInfoPath)) {
65+
try {
66+
generationInfo = JSON.parse(fs.readFileSync(generationInfoPath))
67+
} catch (err) {
68+
console.warn('Error reading generation info after regeneration:', err)
69+
}
70+
}
71+
72+
if (!generationInfo.seedDataProfile) {
73+
generationInfo.seedDataProfile = DEFAULT_SEED_DATA_PROFILE
74+
}
5275
}
5376

5477
// Load generated data
@@ -65,6 +88,10 @@ const defaultSettings = {
6588
debugMode: 'false',
6689
showEnvironmentBanner: 'true',
6790
mammogramViewOrder: 'cc-first', // 'cc-first' | 'mlo-first'
91+
seedProfiles: {
92+
...createSeedProfilesState(),
93+
selectedKey: generationInfo.seedDataProfile
94+
},
6895
screening: {
6996
confirmIdentityOnCheckIn: 'true',
7097
manualImageCollection: 'true',

app/lib/generate-seed-data.js

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const { generateClinicsForBSU } = require('./generators/clinic-generator')
1414
const { generateEvent } = require('./generators/event-generator')
1515
const { getCurrentRiskLevel } = require('./utils/participants')
1616
const { generateReadingData } = require('./generators/reading-generator')
17+
const { getSeedDataProfile } = require('./generators/seed-profiles')
1718

1819
const riskLevels = require('../data/risk-levels')
1920

@@ -105,7 +106,8 @@ const generateClinicsForDay = (
105106
usedParticipantsInSnapshot,
106107
indices,
107108
testScenarioParticipantIds = new Set(),
108-
unitEvents = []
109+
unitEvents = [],
110+
seedDataProfile
109111
) => {
110112
const clinics = []
111113
const events = []
@@ -182,7 +184,8 @@ const generateClinicsForDay = (
182184
outcomeWeights: config.screening.outcomes[firstClinic.clinicType],
183185
forceStatus: scenario.participant.config.scheduling.status,
184186
specialAppointmentOverride:
185-
scenario?.participant?.config?.specialAppointment
187+
scenario?.participant?.config?.specialAppointment,
188+
seedDataProfile
186189
})
187190

188191
events.push(event)
@@ -254,7 +257,8 @@ const generateClinicsForDay = (
254257
participant,
255258
clinic,
256259
outcomeWeights: config.screening.outcomes[clinic.clinicType],
257-
forceInProgress: shouldBeInProgress
260+
forceInProgress: shouldBeInProgress,
261+
seedDataProfile
258262
})
259263

260264
if (shouldBeInProgress) {
@@ -286,11 +290,18 @@ const generateSnapshotPeriod = (startDate, numberOfDays) => {
286290
)
287291
}
288292

289-
const generateData = async () => {
293+
const generateData = async (options = {}) => {
294+
const selectedSeedDataProfile =
295+
options.seedDataProfileObject || getSeedDataProfile(options.seedDataProfile)
296+
290297
if (!fs.existsSync(config.paths.generatedData)) {
291298
fs.mkdirSync(config.paths.generatedData, { recursive: true })
292299
}
293300

301+
console.log(
302+
`Using seed data profile: ${selectedSeedDataProfile.label.toLowerCase()}`
303+
)
304+
294305
// Create test participants first, using generateParticipant but with overrides
295306
console.log('Generating test scenario participants...')
296307
const testParticipants = testScenarios.map((scenario) => {
@@ -383,7 +394,8 @@ const generateData = async () => {
383394
usedParticipantsInSnapshot,
384395
indices,
385396
testScenarioParticipantIds,
386-
unitEvents
397+
unitEvents,
398+
selectedSeedDataProfile
387399
)
388400
)
389401

@@ -428,7 +440,8 @@ const generateData = async () => {
428440
console.log('Generating sample reading data...')
429441
const eventsWithReadingData = generateReadingData(
430442
sortedEvents,
431-
require('../data/users')
443+
require('../data/users'),
444+
selectedSeedDataProfile
432445
)
433446

434447
// breastScreeningUnits.forEach(unit => {
@@ -459,6 +472,7 @@ const generateData = async () => {
459472
writeData('events.json', { events: eventsWithReadingData })
460473
writeData('generation-info.json', {
461474
generatedAt: new Date().toISOString(),
475+
seedDataProfile: selectedSeedDataProfile.key,
462476
stats: {
463477
participants: finalParticipants.length,
464478
clinics: allClinics.length,

app/lib/generators/event-generator.js

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,8 @@ const generateEvent = ({
8989
forceStatus = null,
9090
id = null,
9191
specialAppointmentOverride = null,
92-
forceInProgress = false
92+
forceInProgress = false,
93+
seedDataProfile = null
9394
}) => {
9495
// Parse dates once
9596
const [hours, minutes] = config.clinics.simulatedTime.split(':')
@@ -103,7 +104,10 @@ const generateEvent = ({
103104

104105
// Generate special appointment requirements for this event
105106
const specialAppointment =
106-
specialAppointmentOverride || generateSpecialAppointment()
107+
specialAppointmentOverride ||
108+
generateSpecialAppointment({
109+
probability: seedDataProfile?.specialAppointment?.probability
110+
})
107111
const hasSpecialAppointment = Boolean(
108112
specialAppointment?.supportTypes?.length
109113
)
@@ -268,7 +272,12 @@ const generateEvent = ({
268272
event.mammogramData = generateMammogramImages({
269273
startTime: actualStartTime,
270274
isSeedData: true,
271-
config: participant.config
275+
config: participant.config,
276+
scenarioWeights: seedDataProfile?.mammogram?.scenarioWeights,
277+
imperfectChanceForTechnicalOrIncomplete:
278+
seedDataProfile?.mammogram?.imperfectChanceForTechnicalOrIncomplete,
279+
notesForReaderChanceWithoutImperfect:
280+
seedDataProfile?.mammogram?.notesForReaderChanceWithoutImperfect
272281
})
273282

274283
// Sync event status with mammogram completeness
@@ -295,7 +304,8 @@ const generateEvent = ({
295304
// Generate previous mammograms (reported mammograms from other facilities)
296305
const previousMammograms = generatePreviousMammograms({
297306
eventDate: event.timing.actualEndTime || event.timing.actualStartTime,
298-
addedByUserId: event.sessionDetails.startedBy
307+
addedByUserId: event.sessionDetails.startedBy,
308+
rate: seedDataProfile?.previousMammograms?.rate
299309
})
300310
if (previousMammograms) {
301311
event.previousMammograms = previousMammograms
@@ -306,6 +316,7 @@ const generateEvent = ({
306316
const medicalInformation = generateMedicalInformation({
307317
addedByUserId: event.sessionDetails.startedBy,
308318
config: participant.config,
319+
...(seedDataProfile?.medicalInformation || {}),
309320
// Allow config to override probabilities for test scenarios
310321
...(participant.config?.medicalInformation || {})
311322
})
@@ -322,6 +333,7 @@ const generateEvent = ({
322333
const medicalInformation = generateMedicalInformation({
323334
addedByUserId: event.sessionDetails.startedBy,
324335
config: participant.config,
336+
...(seedDataProfile?.medicalInformation || {}),
325337
// Allow config to override probabilities for test scenarios
326338
...(participant.config?.medicalInformation || {})
327339
})
@@ -336,7 +348,12 @@ const generateEvent = ({
336348
event.mammogramData = generateMammogramImages({
337349
startTime: dayjs(event.sessionDetails.startedAt),
338350
isSeedData: true,
339-
config: participant.config
351+
config: participant.config,
352+
scenarioWeights: seedDataProfile?.mammogram?.scenarioWeights,
353+
imperfectChanceForTechnicalOrIncomplete:
354+
seedDataProfile?.mammogram?.imperfectChanceForTechnicalOrIncomplete,
355+
notesForReaderChanceWithoutImperfect:
356+
seedDataProfile?.mammogram?.notesForReaderChanceWithoutImperfect
340357
})
341358
}
342359
}
@@ -369,7 +386,11 @@ const generateEvent = ({
369386
// Select image set for events with mammogram data
370387
// Done at the end so full event context (symptoms, implants, etc.) is available
371388
if (event.mammogramData) {
372-
const selectedSet = getImageSetForEvent(event.id, 'diagrams', { event })
389+
const selectedSet = getImageSetForEvent(event.id, 'diagrams', {
390+
event,
391+
contextualWeights:
392+
seedDataProfile?.imageSetSelection?.contextualTagWeights
393+
})
373394
if (selectedSet) {
374395
event.mammogramData.selectedSetId = selectedSet.id
375396
}

app/lib/generators/mammogram-generator.js

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,10 @@ const generateViewImages = ({
156156
const generateMammogramImages = ({
157157
startTime = new Date(),
158158
isSeedData = false,
159-
config = {}
159+
config = {},
160+
scenarioWeights = null,
161+
imperfectChanceForTechnicalOrIncomplete = 0.15,
162+
notesForReaderChanceWithoutImperfect = 0.05
160163
} = {}) => {
161164
const accessionBase = faker.number
162165
.int({ min: 100000000, max: 999999999 })
@@ -166,7 +169,9 @@ const generateMammogramImages = ({
166169
const views = {}
167170

168171
// Select scenario (use config override or random weighted selection)
169-
const scenario = config.scenario || weighted.select(IMAGE_SCENARIO_WEIGHTS)
172+
const scenario =
173+
config.scenario ||
174+
weighted.select(scenarioWeights || IMAGE_SCENARIO_WEIGHTS)
170175

171176
// Determine view configuration based on scenario
172177
let viewsToRepeat = config.repeatViews || []
@@ -325,7 +330,7 @@ const generateMammogramImages = ({
325330
imperfectData.isImperfectButBestPossible = ['yes']
326331
} else if (
327332
(scenario === 'technicalRepeat' || scenario === 'incomplete') &&
328-
Math.random() < 0.15
333+
Math.random() < imperfectChanceForTechnicalOrIncomplete
329334
) {
330335
imperfectData.isImperfectButBestPossible = ['yes']
331336
}
@@ -343,7 +348,7 @@ const generateMammogramImages = ({
343348
'Skin folds present due to weight loss',
344349
'Best possible images achieved'
345350
])
346-
} else if (Math.random() < 0.05) {
351+
} else if (Math.random() < notesForReaderChanceWithoutImperfect) {
347352
notesData.notesForReader = faker.helpers.arrayElement([
348353
'Mole on right breast',
349354
'Pacemaker present',

app/lib/generators/medical-information-generator.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const {
2727
* @param {number} [options.probabilityOfPregnancyBreastfeeding=0.05] - Chance of having pregnancy/breastfeeding data
2828
* @param {number} [options.probabilityOfOtherMedicalInfo=0.20] - Chance of having other medical information
2929
* @param {number} [options.probabilityOfBreastFeatures=0.15] - Chance of having breast features
30+
* @param {number} [options.probabilityOfMultipleBreastFeatures=0.30] - If breast features exist, chance of multiple markers
3031
* @param {number} [options.probabilityOfMedicalHistory=0.50] - Chance of having medical history
3132
* @param {Array} [options.forceMedicalHistoryTypes] - Array of medical history types to force generation (e.g. ['breastCancer', 'cysts'])
3233
* @param {object} [options.config] - Participant config for overrides and forced generation
@@ -40,6 +41,7 @@ const generateMedicalInformation = (options = {}) => {
4041
probabilityOfPregnancyBreastfeeding = 0.05,
4142
probabilityOfOtherMedicalInfo = 0.2,
4243
probabilityOfBreastFeatures = 0.15,
44+
probabilityOfMultipleBreastFeatures = 0.3,
4345
probabilityOfMedicalHistory = 0.5,
4446
forceMedicalHistoryTypes,
4547
config
@@ -99,6 +101,7 @@ const generateMedicalInformation = (options = {}) => {
99101
// Generate breast features
100102
const breastFeatures = generateBreastFeatures({
101103
probabilityOfAnyFeatures: probabilityOfBreastFeatures,
104+
probabilityOfMultipleFeatures: probabilityOfMultipleBreastFeatures,
102105
config
103106
})
104107

app/lib/generators/previous-mammogram-generator.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,11 +248,14 @@ const generatePreviousMammogram = ({
248248

249249
// Generate previous mammograms for an event
250250
// Returns an array, or null if none generated
251-
const generatePreviousMammograms = ({ eventDate, addedByUserId }) => {
252-
const rate = config.generation?.previousMammogramRate ?? 0.2
251+
const generatePreviousMammograms = ({ eventDate, addedByUserId, rate }) => {
252+
const effectiveRate =
253+
rate !== undefined
254+
? rate
255+
: (config.generation?.previousMammogramRate ?? 0.2)
253256

254257
// Decide whether this event has reported mammograms
255-
if (Math.random() > rate) {
258+
if (Math.random() > effectiveRate) {
256259
return null
257260
}
258261

0 commit comments

Comments
 (0)