From e57255ee51d50c796dcc30eff4c2ca01d6cd0c37 Mon Sep 17 00:00:00 2001 From: Tamara Slosarek Date: Tue, 9 Jan 2024 16:38:10 +0100 Subject: [PATCH] feat(#684): change map of gene names to lists (wip) --- app/integration_test/drugs_test.dart | 12 +- app/integration_test/main_page_test.dart | 2 +- app/lib/common/models/drug/drug.dart | 6 +- .../common/models/drug/drug_inhibitors.dart | 16 +- app/lib/common/models/userdata/genotype.dart | 2 +- app/lib/common/models/userdata/userdata.dart | 209 ++++++++++++------ app/lib/common/utilities/genome_data.dart | 17 +- app/lib/common/utilities/pdf_utils.dart | 4 +- .../widgets/annotation_cards/guideline.dart | 2 +- app/lib/report/pages/gene.dart | 4 +- 10 files changed, 181 insertions(+), 93 deletions(-) diff --git a/app/integration_test/drugs_test.dart b/app/integration_test/drugs_test.dart index 9a25b506..e17fc880 100644 --- a/app/integration_test/drugs_test.dart +++ b/app/integration_test/drugs_test.dart @@ -48,20 +48,20 @@ void main() { implication: 'nothing', warningLevel: WarningLevel.green)) ]); - UserData.instance.lookups = { - 'CYP2C9': CpicLookup( + UserData.instance.lookups = [ + CpicLookup( gene: 'CYP2C9', phenotype: 'Normal Metabolizer', variant: '*1/*1', lookupkey: '2') - }; - UserData.instance.geneResults = { - 'CYP2C9': GeneResult( + ]; + UserData.instance.geneResults = [ + GeneResult( gene: 'CYP2C9', phenotype: 'Normal Metabolizer', variant: '*1/*1', allelesTested: '1') - }; + ]; final testDrugWithoutGuidelines = Drug( id: '2', version: 1, diff --git a/app/integration_test/main_page_test.dart b/app/integration_test/main_page_test.dart index be977bd1..657e323c 100644 --- a/app/integration_test/main_page_test.dart +++ b/app/integration_test/main_page_test.dart @@ -9,7 +9,7 @@ void main() { binding.framePolicy = LiveTestWidgetsFlutterBindingFramePolicy.onlyPumps; - UserData.instance.lookups = {}; + UserData.instance.lookups = []; CachedDrugs.instance.version = 1; CachedDrugs.instance.drugs = List.empty(); diff --git a/app/lib/common/models/drug/drug.dart b/app/lib/common/models/drug/drug.dart index b5a4cb7e..84b5a8b8 100644 --- a/app/lib/common/models/drug/drug.dart +++ b/app/lib/common/models/drug/drug.dart @@ -80,9 +80,9 @@ extension DrugExtension on Drug { Guideline? get userGuideline => guidelines.firstOrNullWhere( (guideline) => guideline.lookupkey.all( - (gene, geneResults) => - geneResults.contains(UserData.lookupFor(gene, drug: name)) - ), + (gene, lookupkeys) => + lookupkeys.any((lookupkey) => UserData.lookupkeysFor(gene)!.contains(lookupkey)) + ) ); Guideline? get userOrFirstGuideline => userGuideline ?? diff --git a/app/lib/common/models/drug/drug_inhibitors.dart b/app/lib/common/models/drug/drug_inhibitors.dart index 4f0bedd2..6071d396 100644 --- a/app/lib/common/models/drug/drug_inhibitors.dart +++ b/app/lib/common/models/drug/drug_inhibitors.dart @@ -63,9 +63,19 @@ bool isModerateInhibitor(String drugName) { bool isInhibitor(String drugName) { var drugIsInhibitor = false; for (final gene in _drugInhibitorsPerGene.keys) { - final influencingDrugs = _drugInhibitorsPerGene[gene]; - final originalLookup = UserData.lookupFor(gene, drug: drugName, useOverwrite: false); - if (influencingDrugs!.contains(drugName) && originalLookup != '0.0') { + final influencingDrugs = _drugInhibitorsPerGene[gene]!; + // If the drug is not in the current gene list, test the next gene + if (!influencingDrugs.contains(drugName)) continue; + // If the drug is listed as inhibitor, test if the user is already a + // poor metabolizer for the gene; if yes, the drug cannot inhibit the + // activity further and is not regarded as an inhibitor + final originalLookups = UserData.lookupkeysFor(gene, useOverwrite: false)!; + if (originalLookups.length != 1) { + debugPrint('[WARNING] Inhibited genes are (currently) only expected to ' + 'have one matching user lookup but found ${originalLookups.length} for ' + '$drugName; ignoring all but the first one'); + } + if (originalLookups.first != '0.0') { drugIsInhibitor = true; break; } diff --git a/app/lib/common/models/userdata/genotype.dart b/app/lib/common/models/userdata/genotype.dart index da4226d7..e8207e13 100644 --- a/app/lib/common/models/userdata/genotype.dart +++ b/app/lib/common/models/userdata/genotype.dart @@ -1,4 +1,4 @@ -abstract class Genotype{ +class Genotype{ Genotype({ required this.gene, required this.variant, diff --git a/app/lib/common/models/userdata/userdata.dart b/app/lib/common/models/userdata/userdata.dart index 5ad8efe9..2d58c3ac 100644 --- a/app/lib/common/models/userdata/userdata.dart +++ b/app/lib/common/models/userdata/userdata.dart @@ -46,10 +46,144 @@ class UserData { } @HiveField(0) - Map? geneResults; + List? geneResults; - static PhenotypeInformation phenotypeInformationFor( + @HiveField(1) + List? lookups; + + // hive can't deal with sets so we have to use a list :( + @HiveField(2) + List? activeDrugNames; + + static List? _genotypesFrom( + List? genotypes, + String gene, + [String? variant] + ) { + if (genotypes == null) return null; + final matchingGenotypes = genotypes.where( + (geneResult) => geneResult.gene == gene && + (variant == null || geneResult.variant == variant) + ).toList(); + if (matchingGenotypes.isEmpty) { + throw Exception( + 'Could not find Genotype for $gene, $variant' + ); + } + return matchingGenotypes; + } + + static Genotype? _genotypeFrom( + List? genotypes, + Genotype genotype + ) { + final matchingGenotypes = + _genotypesFrom(genotypes, genotype.gene, genotype.variant); + if (matchingGenotypes != null && matchingGenotypes.length != 1) { + throw Exception( + 'Found more than one Genotype for ${genotype.toString()} but should ' + 'only find one' + ); + } + return matchingGenotypes?.first; + } + + static GeneResult? _geneResultFor(Genotype genotype) => + _genotypeFrom(UserData.instance.geneResults, genotype) as GeneResult?; + + static List? _geneResultsFor(String gene) => + _genotypesFrom(UserData.instance.geneResults, gene) as List?; + + static List? _lookupkeysFor(String gene) => + _genotypesFrom(UserData.instance.lookups, gene) as List?; + + static String? variantFor(Genotype genotype) => + _geneResultFor(genotype)?.variant; + + static String? allelesTestedFor(Genotype genotype) => + _geneResultFor(genotype)?.allelesTested; + + static Genotype? genotypeFor( + String gene, + Drug drug, + { required bool useOverwrite } + ) { + final overwrite = useOverwrite + ? UserData.overwrittenLookup(gene, drug: drug.name) + : null; + if (overwrite != null) { + return Genotype(gene: gene, variant: overwrite.value); + } + final matchingGeneResults = _geneResultsFor(gene); + if (matchingGeneResults == null) return null; + if (matchingGeneResults.length == 1) { + return Genotype( + gene: gene, + variant: matchingGeneResults.first.variant, + ); + } + // When multiple lookups were found it means that the gene has positive/ + // negative results for multiple alleles; return the lookup that matches + // the allele + final guidelineAlleles = drug.userGuideline?.lookupkey[gene]?.map( + (lookupkey) => lookupkey.split(' ').first + ).toSet(); + if (guidelineAlleles == null || guidelineAlleles.length != 1) return null; + final variant = matchingGeneResults.firstWhere( + (geneResult) => geneResult.variant.startsWith(guidelineAlleles.first) + ).variant; + return Genotype(gene: gene, variant: variant); + } + + static List? lookupkeysFor( String gene, + { + String? drug, + bool useOverwrite = true, + } + ) { + final overwrittenLookup = UserData.overwrittenLookup(gene, drug: drug); + final matchingLookups = _lookupkeysFor(gene); + return matchingLookups?.map( + (lookup) => (useOverwrite && overwrittenLookup != null) + ? overwrittenLookup.value + : lookup.lookupkey + ).toList(); + } + + static MapEntry? overwrittenLookup( + String gene, + { String? drug } + ) { + final inhibitors = strongDrugInhibitors[gene]; + if (inhibitors == null) return null; + final lookup = inhibitors.entries.firstWhereOrNull((entry) { + final isActiveInhitor = + UserData.instance.activeDrugNames?.contains(entry.key) ?? false; + final wouldInhibitItself = drug == entry.key; + return isActiveInhitor && !wouldInhibitItself; + }); + if (lookup == null) return null; + return lookup; + } + + static List activeInhibitorsFor(String gene, { String? drug }) { + return UserData.instance.activeDrugNames == null + ? [] + : UserData.instance.activeDrugNames!.filter( + (activeDrug) => + inhibitorsFor(gene).contains(activeDrug) && + activeDrug != drug + ).toList(); + } + + // TODO: revisit all the data types and their usage again; can we make this + // less redundant? Can we safely assume that binary gene results cannot be + // inhibited and what would change then? + // TODO(me): should probably receive geneResult already (otherwise not clear) + // if should use overwrite + static PhenotypeInformation phenotypeInformationFor( + Genotype? genotype, BuildContext context, { String? drug, @@ -57,19 +191,20 @@ class UserData { bool useLongPrefix = false, } ) { + final missingResult = PhenotypeInformation( + phenotype: context.l10n.general_not_tested, + ); + if (genotype == null) return missingResult; + final originalPhenotype = _geneResultFor(genotype)?.phenotype; + if (originalPhenotype == null) return missingResult; final userSalutation = thirdPerson ? context.l10n.drugs_page_inhibitor_third_person_salutation : context.l10n.drugs_page_inhibitor_direct_salutation; final strongInhibitorTextPrefix = useLongPrefix ? context.l10n.strong_inhibitor_long_prefix : context.l10n.gene_page_phenotype.toLowerCase(); - final originalPhenotype = UserData.instance.geneResults?[gene]?.phenotype; - if (originalPhenotype == null) { - return PhenotypeInformation( - phenotype: context.l10n.general_not_tested, - ); - } - final activeInhibitors = UserData.activeInhibitorsFor(gene, drug: drug); + final activeInhibitors = + UserData.activeInhibitorsFor(genotype.gene, drug: drug); if (activeInhibitors.isEmpty) { return PhenotypeInformation(phenotype: originalPhenotype); } @@ -81,7 +216,8 @@ class UserData { phenotype: originalPhenotype, ); } - final overwrittenLookup = UserData.overwrittenLookup(gene, drug: drug); + final overwrittenLookup = + UserData.overwrittenLookup(genotype.gene, drug: drug); if (overwrittenLookup == null) { return PhenotypeInformation( phenotype: originalPhenotype, @@ -110,59 +246,6 @@ class UserData { overwrittenPhenotypeText: originalPhenotypeText, ); } - - static String? variantFor(String gene) => - UserData.instance.geneResults?[gene]?.variant; - - static String? allelesTestedFor(String gene) => - UserData.instance.geneResults?[gene]?.allelesTested; - - @HiveField(1) - Map? lookups; - - static MapEntry? overwrittenLookup( - String gene, - { String? drug } - ) { - final inhibitors = strongDrugInhibitors[gene]; - if (inhibitors == null) return null; - final lookup = inhibitors.entries.firstWhereOrNull((entry) { - final isActiveInhitor = - UserData.instance.activeDrugNames?.contains(entry.key) ?? false; - final wouldInhibitItself = drug == entry.key; - return isActiveInhitor && !wouldInhibitItself; - }); - if (lookup == null) return null; - return lookup; - } - - static String? lookupFor( - String gene, - { - String? drug, - bool useOverwrite = true, - } - ) { - final overwrittenLookup = UserData.overwrittenLookup(gene, drug: drug); - if (useOverwrite && overwrittenLookup != null) { - return overwrittenLookup.value; - } - return UserData.instance.lookups?[gene]?.lookupkey; - } - - // hive can't deal with sets so we have to use a list :( - @HiveField(2) - List? activeDrugNames; - - static List activeInhibitorsFor(String gene, { String? drug }) { - return UserData.instance.activeDrugNames == null - ? [] - : UserData.instance.activeDrugNames!.filter( - (activeDrug) => - inhibitorsFor(gene).contains(activeDrug) && - activeDrug != drug - ).toList(); - } } // Wrapper of UserData.instance.activeDrugNames that manages changes; used to diff --git a/app/lib/common/utilities/genome_data.dart b/app/lib/common/utilities/genome_data.dart index e69830cb..5fe684ee 100644 --- a/app/lib/common/utilities/genome_data.dart +++ b/app/lib/common/utilities/genome_data.dart @@ -25,15 +25,10 @@ Future _saveDiplotypeAndActiveDrugsResponse( Response response, ActiveDrugs activeDrugs, ) async { - // parse response to list of user's diplotypes - final diplotypes = - geneResultsFromHTTPResponse(response); - final activeDrugList = activeDrugsFromHTTPResponse(response); - - UserData.instance.geneResults = { - for (final diplotype in diplotypes) diplotype.gene: diplotype - }; + // parse response to list of user's diplotypes and active drugs + UserData.instance.geneResults = geneResultsFromHTTPResponse(response); await UserData.save(); + final activeDrugList = activeDrugsFromHTTPResponse(response); await activeDrugs.setList(activeDrugList); // invalidate cached drugs because lookups may have changed and we need to // refilter the matching guidelines @@ -60,9 +55,9 @@ Future fetchAndSaveLookups() async { value: (lookup) => lookup, ); // ignore: omit_local_variable_types - final Map matchingLookups = {}; + final List matchingLookups = []; // extract the matching lookups - for (final diplotype in usersDiplotypes.values) { + for (final diplotype in usersDiplotypes) { // the gene and the genotype build the key for the hashmap final key = '${diplotype.gene}__${diplotype.variant}'; final lookup = lookupsHashMap[key]; @@ -72,7 +67,7 @@ Future fetchAndSaveLookups() async { // print( // 'Lab phenotype ${diplotype.phenotype} for ${diplotype.gene} (${diplotype.genotype}) is "${lookup.phenotype}" for CPIC'); // } - matchingLookups[diplotype.gene] = lookup; + matchingLookups.add(lookup); } // uncomment to make user have CYP2D6 lookupkey 0.0 diff --git a/app/lib/common/utilities/pdf_utils.dart b/app/lib/common/utilities/pdf_utils.dart index 38e7c3a2..e37b2227 100644 --- a/app/lib/common/utilities/pdf_utils.dart +++ b/app/lib/common/utilities/pdf_utils.dart @@ -146,12 +146,12 @@ String? _getPhenotypeInfo(String gene, Drug drug, BuildContext context) { String? _getActivityScoreInfo(String gene, Drug drug, BuildContext context) { final originalLookup = UserData.lookupFor( - gene, + UserData.genotypeFor(gene, drug), drug: drug.name, useOverwrite: false, ); final overwrittenLookup = UserData.lookupFor( - gene, + UserData.genotypeFor(gene, drug), drug: drug.name, useOverwrite: true, ); diff --git a/app/lib/drug/widgets/annotation_cards/guideline.dart b/app/lib/drug/widgets/annotation_cards/guideline.dart index f3ff93e1..e34a5051 100644 --- a/app/lib/drug/widgets/annotation_cards/guideline.dart +++ b/app/lib/drug/widgets/annotation_cards/guideline.dart @@ -164,7 +164,7 @@ class GuidelineAnnotationCard extends StatelessWidget { } else { final geneDescriptions = drug.guidelineGenes.map((gene) { final phenotypeInformation = UserData.phenotypeInformationFor( - gene, + UserData.genotypeFor(gene, drug, useOverwrite: false), context, drug: drug.name, ); diff --git a/app/lib/report/pages/gene.dart b/app/lib/report/pages/gene.dart index dce53273..c56d5a11 100644 --- a/app/lib/report/pages/gene.dart +++ b/app/lib/report/pages/gene.dart @@ -80,7 +80,7 @@ class GenePage extends HookWidget { TableRow _buildPhenotypeRow(BuildContext context) { final phenotypeInformation = UserData.phenotypeInformationFor( - lookup.gene, + lookup, context, ); final phenotypeText = phenotypeInformation.adaptionText.isNotNullOrBlank @@ -117,7 +117,7 @@ class GenePage extends HookWidget { List buildDrugInteractionInfo(BuildContext context, String gene) { final phenotypeInformation = UserData.phenotypeInformationFor( - gene, + lookup, context, useLongPrefix: true, );