diff --git a/Changelog.md b/Changelog.md index ff610ad..5c43580 100644 --- a/Changelog.md +++ b/Changelog.md @@ -6,6 +6,7 @@ Small and insignificant changes may not be included in this log. ## Unversioned Changes +* Added the Height trait to the Create Genetic Models utility. - *Simon Sarasova* * Added LocusIsPhased information to the local user profile creation process. - *Simon Sarasova* * Added the Height trait the traits package. Migrated locus metadata from json encoding to gob encoding. - *Simon Sarasova* * Upgraded Fyne to version 2.5.0. - *Simon Sarasova* diff --git a/Contributors.md b/Contributors.md index 6fbd618..66c1b93 100644 --- a/Contributors.md +++ b/Contributors.md @@ -9,4 +9,4 @@ Many other people have written code for modules which are imported by Seekia. Th Name | Date Of First Commit | Number Of Commits --- | --- | --- -Simon Sarasova | June 13, 2023 | 270 \ No newline at end of file +Simon Sarasova | June 13, 2023 | 271 \ No newline at end of file diff --git a/imported/goeffects/goeffects.go b/imported/goeffects/goeffects.go index 817c0c9..a05c865 100644 --- a/imported/goeffects/goeffects.go +++ b/imported/goeffects/goeffects.go @@ -26,19 +26,19 @@ func ApplyCartoonEffect(inputImage image.Image, effectStrength int)(image.Image, } - blurKernelSize, err := helpers.ScaleNumberProportionally(true, effectStrength, 0, 100, 1, 3) + blurKernelSize, err := helpers.ScaleIntProportionally(true, effectStrength, 0, 100, 1, 3) if (err != nil) { return nil, err } if (blurKernelSize % 2 == 0){ blurKernelSize += 1 } - edgeThreshold, err := helpers.ScaleNumberProportionally(false, effectStrength, 0, 100, 5, 200) + edgeThreshold, err := helpers.ScaleIntProportionally(false, effectStrength, 0, 100, 5, 200) if (err != nil) { return nil, err } - oilFilterSize, err := helpers.ScaleNumberProportionally(true, effectStrength, 0, 100, 5, 20) + oilFilterSize, err := helpers.ScaleIntProportionally(true, effectStrength, 0, 100, 5, 20) if (err != nil) { return nil, err } - oilLevels, err := helpers.ScaleNumberProportionally(true, effectStrength, 0, 100, 1, 3) + oilLevels, err := helpers.ScaleIntProportionally(true, effectStrength, 0, 100, 1, 3) if (err != nil) { return nil, err } options := CTOpts{ @@ -78,7 +78,7 @@ func ApplyPencilEffect(inputImage image.Image, effectStrength int)(image.Image, goeffectsImageObject, err := convertGolangImageObjectToGoeffectsImageObject(inputImage) if (err != nil) { return nil, err } - blurAmount, err := helpers.ScaleNumberProportionally(true, effectStrength, 0, 100, 1, 20) + blurAmount, err := helpers.ScaleIntProportionally(true, effectStrength, 0, 100, 1, 20) if (err != nil) { return nil, err } if (blurAmount % 2 == 0) { @@ -119,7 +119,7 @@ func ApplyWireframeEffect(inputImage image.Image, effectStrength int, lightMode grayscaleGoeffectsImage, err := grayscaleEffectObject.Apply(&goeffectsImageObject, 5) if (err != nil) { return nil, err } - threshold, err := helpers.ScaleNumberProportionally(false, effectStrength, 0, 100, 10, 100) + threshold, err := helpers.ScaleIntProportionally(false, effectStrength, 0, 100, 10, 100) if (err != nil) { return nil, err } sobelEffectObject := NewSobel(threshold, lightMode) @@ -146,10 +146,10 @@ func ApplyOilPaintingEffect(inputImage image.Image, effectStrength int)(image.Im return inputImage, nil } - filterSize, err := helpers.ScaleNumberProportionally(true, effectStrength, 0, 100, 10, 30) + filterSize, err := helpers.ScaleIntProportionally(true, effectStrength, 0, 100, 10, 30) if (err != nil) { return nil, err } - levels, err := helpers.ScaleNumberProportionally(true, effectStrength, 0, 100, 10, 70) + levels, err := helpers.ScaleIntProportionally(true, effectStrength, 0, 100, 10, 70) if (err != nil) { return nil, err } oilPaintingEffectObject := NewOilPainting(filterSize, levels) diff --git a/internal/genetics/createCoupleGeneticAnalysis/createCoupleGeneticAnalysis.go b/internal/genetics/createCoupleGeneticAnalysis/createCoupleGeneticAnalysis.go index f179d89..77aa75d 100644 --- a/internal/genetics/createCoupleGeneticAnalysis/createCoupleGeneticAnalysis.go +++ b/internal/genetics/createCoupleGeneticAnalysis/createCoupleGeneticAnalysis.go @@ -40,7 +40,7 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom person1PrepareRawGenomesUpdatePercentageCompleteFunction := func(newPercentage int)error{ - newPercentageCompletion, err := helpers.ScaleNumberProportionally(true, newPercentage, 0, 100, 0, 25) + newPercentageCompletion, err := helpers.ScaleIntProportionally(true, newPercentage, 0, 100, 0, 25) if (err != nil){ return err } err = updatePercentageCompleteFunction(newPercentageCompletion) @@ -49,8 +49,18 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom return nil } - person1GenomesWithMetadataList, allPerson1RawGenomeIdentifiersList, person1HasMultipleGenomes, person1OnlyExcludeConflictsGenomeIdentifier, person1OnlyIncludeSharedGenomeIdentifier, err := prepareRawGenomes.GetGenomesWithMetadataListFromRawGenomesList(person1GenomesList, person1PrepareRawGenomesUpdatePercentageCompleteFunction) + anyUsefulLocationsExist, person1GenomesWithMetadataList, allPerson1RawGenomeIdentifiersList, person1HasMultipleGenomes, person1OnlyExcludeConflictsGenomeIdentifier, person1OnlyIncludeSharedGenomeIdentifier, err := prepareRawGenomes.GetGenomesWithMetadataListFromRawGenomesList(person1GenomesList, person1PrepareRawGenomesUpdatePercentageCompleteFunction) if (err != nil) { return false, "", err } + if (anyUsefulLocationsExist == false){ + // We should have checked for this when genomes were first imported. + return false, "", errors.New("CreateCoupleGeneticAnalysis called with person1GenomesList that does not contain any useful genomes") + } + if (len(person1GenomesList) > 1 && (len(person1GenomesList) != (len(person1GenomesWithMetadataList)-2)) ){ + // If there is more than 1 genome, 2 combined genomes are created + // We are checking to make sure that none of the input genomes were dropped due to not having any locations + // We should have checked to make sure each input genome has useful locations when each genome was first imported. + return false, "", errors.New("CreateCoupleGeneticAnalysis called with person1GenomesList containing at least 1 genome without useful locations.") + } processIsStopped := checkIfProcessIsStopped() if (processIsStopped == true){ @@ -59,7 +69,7 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom person2PrepareRawGenomesUpdatePercentageCompleteFunction := func(newPercentage int)error{ - newPercentageCompletion, err := helpers.ScaleNumberProportionally(true, newPercentage, 0, 100, 25, 50) + newPercentageCompletion, err := helpers.ScaleIntProportionally(true, newPercentage, 0, 100, 25, 50) if (err != nil){ return err } err = updatePercentageCompleteFunction(newPercentageCompletion) @@ -68,8 +78,18 @@ func CreateCoupleGeneticAnalysis(person1GenomesList []prepareRawGenomes.RawGenom return nil } - person2GenomesWithMetadataList, allPerson2RawGenomeIdentifiersList, person2HasMultipleGenomes, person2OnlyExcludeConflictsGenomeIdentifier, person2OnlyIncludeSharedGenomeIdentifier, err := prepareRawGenomes.GetGenomesWithMetadataListFromRawGenomesList(person2GenomesList, person2PrepareRawGenomesUpdatePercentageCompleteFunction) + anyUsefulLocationsExist, person2GenomesWithMetadataList, allPerson2RawGenomeIdentifiersList, person2HasMultipleGenomes, person2OnlyExcludeConflictsGenomeIdentifier, person2OnlyIncludeSharedGenomeIdentifier, err := prepareRawGenomes.GetGenomesWithMetadataListFromRawGenomesList(person2GenomesList, person2PrepareRawGenomesUpdatePercentageCompleteFunction) if (err != nil) { return false, "", err } + if (anyUsefulLocationsExist == false){ + // We should have checked for this when genomes were first imported. + return false, "", errors.New("CreateCoupleGeneticAnalysis called with person2GenomesList that does not contain any useful genomes") + } + if (len(person2GenomesList) > 1 && (len(person2GenomesList) != (len(person2GenomesWithMetadataList)-2)) ){ + // If there is more than 1 genome, 2 combined genomes are created + // We are checking to make sure that none of the input genomes were dropped due to not having any locations + // We should have checked to make sure each input genome has useful locations when each genome was first imported. + return false, "", errors.New("CreateCoupleGeneticAnalysis called with person2GenomesList containing at least 1 genome without useful locations.") + } processIsStopped = checkIfProcessIsStopped() if (processIsStopped == true){ @@ -936,7 +956,7 @@ func GetOffspringPolygenicDiseaseInfo_Fast(diseaseLociList []polygenicDiseases.D offspringMaximumPossibleRiskWeightSum += locusMaximumWeight } - offspringAverageDiseaseRiskScore, err := helpers.ScaleNumberProportionally(true, offspringSummedRiskWeights, offspringMinimumPossibleRiskWeightSum, offspringMaximumPossibleRiskWeightSum, 0, 10) + offspringAverageDiseaseRiskScore, err := helpers.ScaleIntProportionally(true, offspringSummedRiskWeights, offspringMinimumPossibleRiskWeightSum, offspringMaximumPossibleRiskWeightSum, 0, 10) if (err != nil) { return false, 0, 0, err } if (numberOfLociTested == 0){ @@ -1061,7 +1081,7 @@ func GetOffspringPolygenicDiseaseInfo(diseaseLociList []polygenicDiseases.Diseas offspringMaximumPossibleRiskWeightSum += locusMaximumWeight } - offspringAverageDiseaseRiskScore, err := helpers.ScaleNumberProportionally(true, offspringSummedRiskWeights, offspringMinimumPossibleRiskWeightSum, offspringMaximumPossibleRiskWeightSum, 0, 10) + offspringAverageDiseaseRiskScore, err := helpers.ScaleIntProportionally(true, offspringSummedRiskWeights, offspringMinimumPossibleRiskWeightSum, offspringMaximumPossibleRiskWeightSum, 0, 10) if (err != nil) { return false, 0, 0, nil, nil, err } sampleOffspringRiskScoresList = append(sampleOffspringRiskScoresList, offspringAverageDiseaseRiskScore) diff --git a/internal/genetics/createPersonGeneticAnalysis/createPersonGeneticAnalysis.go b/internal/genetics/createPersonGeneticAnalysis/createPersonGeneticAnalysis.go index 5fa05f5..e56f753 100644 --- a/internal/genetics/createPersonGeneticAnalysis/createPersonGeneticAnalysis.go +++ b/internal/genetics/createPersonGeneticAnalysis/createPersonGeneticAnalysis.go @@ -40,7 +40,7 @@ func CreatePersonGeneticAnalysis(genomesList []prepareRawGenomes.RawGenomeWithMe prepareRawGenomesUpdatePercentageCompleteFunction := func(newPercentage int)error{ - newPercentageCompletion, err := helpers.ScaleNumberProportionally(true, newPercentage, 0, 100, 0, 50) + newPercentageCompletion, err := helpers.ScaleIntProportionally(true, newPercentage, 0, 100, 0, 50) if (err != nil){ return err } err = updatePercentageCompleteFunction(newPercentageCompletion) @@ -49,8 +49,18 @@ func CreatePersonGeneticAnalysis(genomesList []prepareRawGenomes.RawGenomeWithMe return nil } - genomesWithMetadataList, allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := prepareRawGenomes.GetGenomesWithMetadataListFromRawGenomesList(genomesList, prepareRawGenomesUpdatePercentageCompleteFunction) + anyUsefulLocationsExist, genomesWithMetadataList, allRawGenomeIdentifiersList, multipleGenomesExist, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, err := prepareRawGenomes.GetGenomesWithMetadataListFromRawGenomesList(genomesList, prepareRawGenomesUpdatePercentageCompleteFunction) if (err != nil) { return false, "", err } + if (anyUsefulLocationsExist == false){ + // We should have checked for this when genomes were first imported. + return false, "", errors.New("CreatePersonGeneticAnalysis called with genomeList containing no genomes with useful locations.") + } + if (len(genomesList) > 1 && (len(genomesList) != (len(genomesWithMetadataList)-2)) ){ + // If there is more than 1 genome, 2 combined genomes are created + // We are checking to make sure that none of the input genomes were dropped due to not having any locations + // We should have checked to make sure each input genome has useful locations when each genome was first imported. + return false, "", errors.New("CreatePersonGeneticAnalysis called with genomeList containing at least 1 genome without useful locations.") + } // This map stores each genome's locus values // Map Structure: Genome Identifier -> Genome locus values map (rsID -> Locus Value) @@ -716,7 +726,7 @@ func GetPersonGenomePolygenicDiseaseInfo(diseaseLociList []polygenicDiseases.Dis return false, 0, 0, nil, nil } - diseaseRiskScore, err := helpers.ScaleNumberProportionally(true, summedDiseaseRiskWeight, minimumPossibleRiskWeightSum, maximumPossibleRiskWeightSum, 0, 10) + diseaseRiskScore, err := helpers.ScaleIntProportionally(true, summedDiseaseRiskWeight, minimumPossibleRiskWeightSum, maximumPossibleRiskWeightSum, 0, 10) if (err != nil) { return false, 0, 0, nil, err } return true, diseaseRiskScore, numberOfLociTested, genomeLociInfoMap, nil diff --git a/internal/genetics/geneticPrediction/geneticPrediction.go b/internal/genetics/geneticPrediction/geneticPrediction.go index cb594aa..94a4941 100644 --- a/internal/genetics/geneticPrediction/geneticPrediction.go +++ b/internal/genetics/geneticPrediction/geneticPrediction.go @@ -218,7 +218,7 @@ type DiscreteTraitPredictionAccuracyInfoMap map[DiscreteTraitOutcomeInfo]Discret type DiscreteTraitOutcomeInfo struct{ - // This is the outcome which was found + // This is the outcome which was predicted // Example: "Blue" OutcomeName string @@ -283,6 +283,65 @@ func DecodeBytesToDiscreteTraitPredictionAccuracyInfoMap(inputBytes []byte)(Disc return newDiscreteTraitPredictionAccuracyInfoMap, nil } +type NumericTraitPredictionAccuracyInfoMap map[NumericTraitOutcomeInfo]NumericTraitPredictionAccuracyRangesMap + +type NumericTraitOutcomeInfo struct{ + + // This is the outcome which was predicted + // Example: 150 centimeters + OutcomeValue float64 + + // This is a value between 0-100 which describes the percentage of the loci which were tested for the input for the prediction + PercentageOfLociTested int + + // This is a value between 0-100 which describes the percentage of the tested loci which were phased for the input for the prediction + PercentageOfPhasedLoci int +} + +// Map Structure: Accuracy Percentage (AP) -> Amount needed to deviate from prediction for the value to be accurate (AP)% of the time +// For example, if the model predicted that someone was 150 centimeters tall, how many centimeters would we have to deviate in both directions +// in order for the true outcome to fall into the range 10% of the time, 20% of the time, 30% of the time, etc... +// Example: +// -90%+: 50 centimeters +// If you travel 50 centimeters in both directions from the prediction, +// the true height value will fall into this range 90% of the time. +// -50%+: 20 centimeters +// -10%+: 10 centimeters +type NumericTraitPredictionAccuracyRangesMap map[int]float64 + + +func EncodeNumericTraitPredictionAccuracyInfoMapToBytes(inputMap NumericTraitPredictionAccuracyInfoMap)([]byte, error){ + + buffer := new(bytes.Buffer) + + encoder := gob.NewEncoder(buffer) + + err := encoder.Encode(inputMap) + if (err != nil) { return nil, err } + + inputMapBytes := buffer.Bytes() + + return inputMapBytes, nil +} + +func DecodeBytesToNumericTraitPredictionAccuracyInfoMap(inputBytes []byte)(NumericTraitPredictionAccuracyInfoMap, error){ + + if (inputBytes == nil){ + return nil, errors.New("DecodeBytesToNumericTraitPredictionAccuracyInfoMap called with nil inputBytes.") + } + + buffer := bytes.NewBuffer(inputBytes) + + decoder := gob.NewDecoder(buffer) + + var newNumericTraitPredictionAccuracyInfoMap NumericTraitPredictionAccuracyInfoMap + + err := decoder.Decode(&newNumericTraitPredictionAccuracyInfoMap) + if (err != nil){ return nil, err } + + return newNumericTraitPredictionAccuracyInfoMap, nil +} + //Outputs: // -bool: Neural network model exists for this trait (trait prediction is possible for this trait) // -bool: Trait prediction is possible for this user (User has at least 1 known trait locus value) @@ -378,7 +437,7 @@ func GetNeuralNetworkDiscreteTraitPredictionFromGenomeMap(traitName string, geno outputLayer, err := GetNeuralNetworkRawPrediction(&neuralNetworkObject, false, neuralNetworkInput) if (err != nil) { return false, false, "", 0, 0, 0, err } - predictedOutcomeName, err := GetOutcomeNameFromOutputLayer(traitName, false, outputLayer) + predictedOutcomeName, err := GetDiscreteOutcomeNameFromOutputLayer(traitName, false, outputLayer) if (err != nil) { return false, false, "", 0, 0, 0, err } modelTraitAccuracyInfoFile, err := geneticPredictionModels.GetPredictionModelDiscreteTraitAccuracyInfoBytes(traitName) @@ -521,7 +580,7 @@ func GetLociInfoFromNetworkInputLayer(inputLayer []float32)(int, int, int, error // Outputs: // -string: Output Name (Example: "Blue") // -error -func GetOutcomeNameFromOutputLayer(traitName string, verifyOutputLayer bool, outputLayer []float32)(string, error){ +func GetDiscreteOutcomeNameFromOutputLayer(traitName string, verifyOutputLayer bool, outputLayer []float32)(string, error){ if (verifyOutputLayer == true){ @@ -534,7 +593,7 @@ func GetOutcomeNameFromOutputLayer(traitName string, verifyOutputLayer bool, out } // We allow a small amount of inaccuracy due to the imprecise nature of floats. - if (summedNeurons > 1.1 || summedNeurons < .99){ + if (summedNeurons > 1.01 || summedNeurons < .99){ summedNeuronsString := helpers.ConvertFloat32ToString(summedNeurons) return "", errors.New("GetOutcomeNameFromOutputLayer called with layer containing neuron values which don't sum to 1: " + summedNeuronsString) } @@ -607,6 +666,45 @@ func GetOutcomeNameFromOutputLayer(traitName string, verifyOutputLayer bool, out } +// This function returns which outcome is being described from a neural network's final output layer +// This is only used for discrete traits +// Outputs: +// -float64: Output Value (example: 150 centimeters) +// -error +func GetNumericOutcomeValueFromOutputLayer(traitName string, outputLayer []float32)(float64, error){ + + if (len(outputLayer) != 1){ + return 0, errors.New("GetNumericOutcomeValueFromOutputLayer called with output layer which is not length of 1") + } + + outputNeuron := outputLayer[0] + + if (outputNeuron < 0 || outputNeuron > 1){ + return 0, errors.New("GetNumericOutcomeValueFromOutputLayer called with output layer contains out-of-bounds neuron") + } + + getOutcomeMinAndMax := func()(float64, float64, error){ + + switch traitName{ + case "Height":{ + // Shortest person of all time: 54 cm + // Tallest person of all time: 272 cm + return 54, 272, nil + } + } + + return 0, 0, errors.New("GetNumericOutcomeValueFromOutputLayer called with unknown traitName: " + traitName) + } + + outcomeMin, outcomeMax, err := getOutcomeMinAndMax() + if (err != nil) { return 0, err } + + outcomeValue, err := helpers.ScaleFloat64Proportionally(true, float64(outputNeuron), 0, 1, outcomeMin, outcomeMax) + if (err != nil) { return 0, err } + + return outcomeValue, nil +} + //Outputs: // -int: Layer 1 neuron count (input layer) // -int: Layer 2 neuron count @@ -631,6 +729,11 @@ func getNeuralNetworkLayerSizes(traitName string)(int, int, int, int, error){ // There are 2 output neurons, each representing a tolerance: Tolerant, Intolerant return 6, 4, 3, 2, nil } + case "Height":{ + // There are 3000 input neurons + // There is 1 output neuron, representing a height value + return 3000, 2, 2, 1, nil + } } return 0, 0, 0, 0, errors.New("getNeuralNetworkLayerSizes called with unknown traitName: " + traitName) @@ -682,7 +785,7 @@ func CreateGeneticPredictionTrainingData_OpenSNP( userPhenotypeDataObject readBiobankData.PhenotypeData_OpenSNP, userLocusValuesMap map[int64]locusValue.LocusValue)(bool, []TrainingData, error){ - if (traitName != "Eye Color" && traitName != "Lactose Tolerance"){ + if (traitName != "Eye Color" && traitName != "Lactose Tolerance" && traitName != "Height"){ return false, nil, errors.New("CreateGeneticPredictionTrainingData_OpenSNP called with unknown traitName: " + traitName) } @@ -800,6 +903,27 @@ func CreateGeneticPredictionTrainingData_OpenSNP( return true, []float32{0, 1}, nil } + case "Height":{ + + userHeightIsKnown := userPhenotypeDataObject.HeightIsKnown + if (userHeightIsKnown == false){ + return false, nil, nil + } + + userHeight := userPhenotypeDataObject.Height + + // Shortest person of all time: 54 cm + // Tallest person of all time: 272 cm + + outputValue, err := helpers.ScaleFloat64Proportionally(true, userHeight, 54, 272, 0, 1) + if (err != nil) { return false, nil, err } + + outputValueFloat32 := float32(outputValue) + + outputLayer := []float32{outputValueFloat32} + + return true, outputLayer, nil + } } return false, nil, errors.New("Unknown traitName: " + traitName) @@ -911,55 +1035,49 @@ func CreateGeneticPredictionTrainingData_OpenSNP( anyLocusExists = true - getLocusAlleles := func()(string, string){ + //Outputs: + // -float32: Final neuron value + // -0 == Value is unknown + // -0.5 == Value is known, phase is unknown + // -1 == Value is known, phase is known + // -string: Allele 1 value + // -string: Allele 2 value + getFirstNeuronAndLocusAlleles := func()(float32, string, string){ locusAllele1 := userLocusValue.Base1Value locusAllele2 := userLocusValue.Base2Value - if (randomizePhaseBool == false){ - return locusAllele1, locusAllele2 + if (locusAllele1 == locusAllele2){ + // Locus phase is unimportant + return 1, locusAllele1, locusAllele2 + } + + locusIsPhased := userLocusValue.LocusIsPhased + + if (randomizePhaseBool == false && locusIsPhased == true){ + return 1, locusAllele1, locusAllele2 } // We randomize the phase of the locus + // We always do this if the locus is not phased, because the genome data might actually be partially/fully phased, + // even if it is not advertised as being phased randomNumber := pseudorandomNumberGenerator.IntN(2) if (randomNumber == 1){ // This has a 50% chance of being true. - return locusAllele1, locusAllele2 + return 0.5, locusAllele1, locusAllele2 } - return locusAllele2, locusAllele1 + return 0.5, locusAllele2, locusAllele1 } - locusAllele1, locusAllele2 := getLocusAlleles() + locusIsKnownAndPhasedNeuronValue, locusAllele1, locusAllele2 := getFirstNeuronAndLocusAlleles() locusAllele1NeuronValue, err := convertAlleleToNeuron(locusAllele1) if (err != nil){ return false, nil, err } locusAllele2NeuronValue, err := convertAlleleToNeuron(locusAllele2) if (err != nil) { return false, nil, err } - getLocusIsKnownAndPhasedNeuronValue := func()float32{ - - if (locusAllele1 == locusAllele2){ - // Phase of locus must be known. - // Swapping the loci would change nothing. - return 1 - } - - if (randomizePhaseBool == true){ - return 0.5 - } - - locusIsPhased := userLocusValue.LocusIsPhased - if (locusIsPhased == true){ - return 1 - } - - return 0.5 - } - - locusIsKnownAndPhasedNeuronValue := getLocusIsKnownAndPhasedNeuronValue() - inputLayer = append(inputLayer, locusIsKnownAndPhasedNeuronValue, locusAllele1NeuronValue, locusAllele2NeuronValue) } diff --git a/internal/genetics/myAnalyses/myAnalyses.go b/internal/genetics/myAnalyses/myAnalyses.go index 8750a8c..4d4feb2 100644 --- a/internal/genetics/myAnalyses/myAnalyses.go +++ b/internal/genetics/myAnalyses/myAnalyses.go @@ -627,7 +627,7 @@ func StartCreateNewPersonGeneticAnalysis(personIdentifier string)(string, error) for index, genomeMap := range personGenomesMapList{ - newPercentageProgress, err := helpers.ScaleNumberProportionally(true, index, 0, finalIndex, 0, 10) + newPercentageProgress, err := helpers.ScaleIntProportionally(true, index, 0, finalIndex, 0, 10) if (err != nil) { return err } err = updatePercentageCompleteFunction(newPercentageProgress) @@ -665,7 +665,7 @@ func StartCreateNewPersonGeneticAnalysis(personIdentifier string)(string, error) analysisUpdatePercentageCompleteFunction := func(inputProgress int)error{ - newPercentageProgress, err := helpers.ScaleNumberProportionally(true, inputProgress, 0, 100, 10, 10) + newPercentageProgress, err := helpers.ScaleIntProportionally(true, inputProgress, 0, 100, 10, 10) if (err != nil) { return err } err = updatePercentageCompleteFunction(newPercentageProgress) @@ -899,7 +899,7 @@ func StartCreateNewCoupleGeneticAnalysis(inputPerson1Identifier string, inputPer break } - personPercentageComplete, err := helpers.ScaleNumberProportionally(true, processPercentageComplete, 0, 100, personPercentageRangeStart, personPercentageRangeEnd) + personPercentageComplete, err := helpers.ScaleIntProportionally(true, processPercentageComplete, 0, 100, personPercentageRangeStart, personPercentageRangeEnd) if (err != nil) { return err } err = updatePercentageCompleteFunction(personPercentageComplete) @@ -978,7 +978,7 @@ func StartCreateNewCoupleGeneticAnalysis(inputPerson1Identifier string, inputPer updateCoupleAnalysisPercentageCompleteFunction := func(newPercentage int)error{ - personPercentageComplete, err := helpers.ScaleNumberProportionally(true, newPercentage, 0, 100, 74, 100) + personPercentageComplete, err := helpers.ScaleIntProportionally(true, newPercentage, 0, 100, 74, 100) if (err != nil) { return err } err = updatePercentageCompleteFunction(personPercentageComplete) diff --git a/internal/genetics/prepareRawGenomes/prepareRawGenomes.go b/internal/genetics/prepareRawGenomes/prepareRawGenomes.go index 7ec0a32..c1e1ccb 100644 --- a/internal/genetics/prepareRawGenomes/prepareRawGenomes.go +++ b/internal/genetics/prepareRawGenomes/prepareRawGenomes.go @@ -68,16 +68,17 @@ func CreateRawGenomeWithMetadataObject(genomeIdentifier [16]byte, rawGenomeStrin // -[]RawGenomeWithMetadata // -func(int)error: Update Percentage Complete Function //Outputs: +// -bool: Any useful locations exist in any of the provided genomes // -[]GenomeWithMetadata: Genomes with metadata list // -[][16]byte: All raw genome identifiers list (not including combined genomes) // -bool: Combined genomes exist // -[16]byte: Only exclude conflicts genome identifier // -[16]byte: Only include shared genome identifier // -error -func GetGenomesWithMetadataListFromRawGenomesList(inputGenomesList []RawGenomeWithMetadata, updatePercentageCompleteFunction func(int)error)([]GenomeWithMetadata, [][16]byte, bool, [16]byte, [16]byte, error){ +func GetGenomesWithMetadataListFromRawGenomesList(inputGenomesList []RawGenomeWithMetadata, updatePercentageCompleteFunction func(int)error)(bool, []GenomeWithMetadata, [][16]byte, bool, [16]byte, [16]byte, error){ if (len(inputGenomesList) == 0){ - return nil, nil, false, [16]byte{}, [16]byte{}, errors.New("GetGenomesWithMetadataListFromRawGenomesList called with empty inputGenomesList") + return false, nil, nil, false, [16]byte{}, [16]byte{}, errors.New("GetGenomesWithMetadataListFromRawGenomesList called with empty inputGenomesList") } // The reading of genomes will take up the first 20% of the percentage range @@ -87,18 +88,17 @@ func GetGenomesWithMetadataListFromRawGenomesList(inputGenomesList []RawGenomeWi // Each map stores a genome from a company or a combined genome. genomesWithMetadataList := make([]GenomeWithMetadata, 0) - numberOfGenomesRead := 0 - totalNumberOfGenomesToRead := len(inputGenomesList) + finalIndex := len(inputGenomesList) - 1 allRawGenomeIdentifiersList := make([][16]byte, 0) - for _, rawGenomeWithMetadataObject := range inputGenomesList{ + for index, rawGenomeWithMetadataObject := range inputGenomesList{ - newPercentageCompletion, err := helpers.ScaleNumberProportionally(true, numberOfGenomesRead, 0, totalNumberOfGenomesToRead, 0, 20) - if (err != nil) { return nil, nil, false, [16]byte{}, [16]byte{}, err } + newPercentageCompletion, err := helpers.ScaleIntProportionally(true, index, 0, finalIndex, 0, 20) + if (err != nil) { return false, nil, nil, false, [16]byte{}, [16]byte{}, err } err = updatePercentageCompleteFunction(newPercentageCompletion) - if (err != nil) { return nil, nil, false, [16]byte{}, [16]byte{}, err } + if (err != nil) { return false, nil, nil, false, [16]byte{}, [16]byte{}, err } genomeIdentifier := rawGenomeWithMetadataObject.GenomeIdentifier genomeIsPhased := rawGenomeWithMetadataObject.GenomeIsPhased @@ -107,12 +107,11 @@ func GetGenomesWithMetadataListFromRawGenomesList(inputGenomesList []RawGenomeWi // Now we convert rawGenomeMap to a genomeMap anyValuesExist, genomeMap, err := ConvertRawGenomeToGenomeMap(rawGenomeMap, genomeIsPhased) - if (err != nil) { return nil, nil, false, [16]byte{}, [16]byte{}, err } + if (err != nil) { return false, nil, nil, false, [16]byte{}, [16]byte{}, err } if (anyValuesExist == false){ - // We have to make sure this never happens so the user isn't confused as to why genomes - // that were imported were not included in the analysis - // We make sure this doesn't happen by verifying the genome at the time of importing - return nil, nil, false, [16]byte{}, [16]byte{}, errors.New("Genome supplied to GetGenomesWithMetadataListFromRawGenomesList has no valid locations.") + // This genome is not useful + // No useful locations exist + continue } genomeWithMetadataObject := GenomeWithMetadata{ @@ -123,17 +122,20 @@ func GetGenomesWithMetadataListFromRawGenomesList(inputGenomesList []RawGenomeWi genomesWithMetadataList = append(genomesWithMetadataList, genomeWithMetadataObject) allRawGenomeIdentifiersList = append(allRawGenomeIdentifiersList, genomeIdentifier) + } - numberOfGenomesRead += 1 + if (len(genomesWithMetadataList) == 0){ + // None of the provided genomes contained any useful locations + return false, nil, nil, false, [16]byte{}, [16]byte{}, nil } containsDuplicates, _ := helpers.CheckIfListContainsDuplicates(allRawGenomeIdentifiersList) if (containsDuplicates == true){ - return nil, nil, false, [16]byte{}, [16]byte{}, errors.New("GetGenomesWithMetadataListFromRawGenomesList called with inputGenomesList containing duplicate genomeIdentifiers.") + return false, nil, nil, false, [16]byte{}, [16]byte{}, errors.New("GetGenomesWithMetadataListFromRawGenomesList called with inputGenomesList containing duplicate genomeIdentifiers.") } err := updatePercentageCompleteFunction(20) - if (err != nil){ return nil, nil, false, [16]byte{}, [16]byte{}, err } + if (err != nil){ return false, nil, nil, false, [16]byte{}, [16]byte{}, err } if (len(genomesWithMetadataList) <= 1){ @@ -141,9 +143,9 @@ func GetGenomesWithMetadataListFromRawGenomesList(inputGenomesList []RawGenomeWi // No genome combining is needed. err = updatePercentageCompleteFunction(100) - if (err != nil){ return nil, nil, false, [16]byte{}, [16]byte{}, err } + if (err != nil){ return false, nil, nil, false, [16]byte{}, [16]byte{}, err } - return genomesWithMetadataList, allRawGenomeIdentifiersList, false, [16]byte{}, [16]byte{}, nil + return true, genomesWithMetadataList, allRawGenomeIdentifiersList, false, [16]byte{}, [16]byte{}, nil } // Now we create the shared genomes @@ -156,15 +158,15 @@ func GetGenomesWithMetadataListFromRawGenomesList(inputGenomesList []RawGenomeWi // This map stores all RSIDs across all genomes allRSIDsMap := make(map[int64]struct{}) - finalIndex := len(genomesWithMetadataList) - 1 + finalIndex = len(genomesWithMetadataList) - 1 for index, genomeWithMetadataObject := range genomesWithMetadataList{ - newPercentageCompletion, err := helpers.ScaleNumberProportionally(true, index, 0, finalIndex, 20, 50) - if (err != nil){ return nil, nil, false, [16]byte{}, [16]byte{}, err } + newPercentageCompletion, err := helpers.ScaleIntProportionally(true, index, 0, finalIndex, 20, 50) + if (err != nil){ return false, nil, nil, false, [16]byte{}, [16]byte{}, err } err = updatePercentageCompleteFunction(newPercentageCompletion) - if (err != nil){ return nil, nil, false, [16]byte{}, [16]byte{}, err } + if (err != nil){ return false, nil, nil, false, [16]byte{}, [16]byte{}, err } genomeMap := genomeWithMetadataObject.GenomeMap @@ -185,11 +187,11 @@ func GetGenomesWithMetadataListFromRawGenomesList(inputGenomesList []RawGenomeWi for rsID, _ := range allRSIDsMap{ - newPercentageCompletion, err := helpers.ScaleNumberProportionally(true, index, 0, finalIndex, 50, 100) - if (err != nil){ return nil, nil, false, [16]byte{}, [16]byte{}, err } + newPercentageCompletion, err := helpers.ScaleIntProportionally(true, index, 0, finalIndex, 50, 100) + if (err != nil){ return false, nil, nil, false, [16]byte{}, [16]byte{}, err } err = updatePercentageCompleteFunction(newPercentageCompletion) - if (err != nil){ return nil, nil, false, [16]byte{}, [16]byte{}, err } + if (err != nil){ return false, nil, nil, false, [16]byte{}, [16]byte{}, err } index += 1 @@ -200,7 +202,7 @@ func GetGenomesWithMetadataListFromRawGenomesList(inputGenomesList []RawGenomeWi } anyAliasesExist, rsidAliasesList, err := locusMetadata.GetRSIDAliases(rsID) - if (err != nil){ return nil, nil, false, [16]byte{}, [16]byte{}, err } + if (err != nil){ return false, nil, nil, false, [16]byte{}, [16]byte{}, err } if (anyAliasesExist == true){ for _, rsidAlias := range rsidAliasesList{ @@ -386,7 +388,7 @@ func GetGenomesWithMetadataListFromRawGenomesList(inputGenomesList []RawGenomeWi } locusBase1, locusBase2, phaseIsKnown_OnlyExcludeConflicts, phaseIsKnown_OnlyIncludeShared, err := getLocusBasePair() - if (err != nil){ return nil, nil, false, [16]byte{}, [16]byte{}, err } + if (err != nil){ return false, nil, nil, false, [16]byte{}, [16]byte{}, err } // Now we add to the combined genome maps // The OnlyExcludeConflicts will only omit when there is a tie @@ -436,7 +438,7 @@ func GetGenomesWithMetadataListFromRawGenomesList(inputGenomesList []RawGenomeWi } onlyExcludeConflictsGenomeIdentifier, err := helpers.GetNewRandom16ByteArray() - if (err != nil) { return nil, nil, false, [16]byte{}, [16]byte{}, err } + if (err != nil) { return false, nil, nil, false, [16]byte{}, [16]byte{}, err } onlyExcludeConflictsGenomeWithMetadataObject := GenomeWithMetadata{ GenomeType: "OnlyExcludeConflicts", @@ -445,7 +447,7 @@ func GetGenomesWithMetadataListFromRawGenomesList(inputGenomesList []RawGenomeWi } onlyIncludeSharedGenomeIdentifier, err := helpers.GetNewRandom16ByteArray() - if (err != nil) { return nil, nil, false, [16]byte{}, [16]byte{}, err } + if (err != nil) { return false, nil, nil, false, [16]byte{}, [16]byte{}, err } onlyIncludeSharedGenomeWithMetadataObject := GenomeWithMetadata{ GenomeType: "OnlyIncludeShared", @@ -456,9 +458,9 @@ func GetGenomesWithMetadataListFromRawGenomesList(inputGenomesList []RawGenomeWi genomesWithMetadataList = append(genomesWithMetadataList, onlyExcludeConflictsGenomeWithMetadataObject, onlyIncludeSharedGenomeWithMetadataObject) err = updatePercentageCompleteFunction(100) - if (err != nil){ return nil, nil, false, [16]byte{}, [16]byte{}, err } + if (err != nil){ return false, nil, nil, false, [16]byte{}, [16]byte{}, err } - return genomesWithMetadataList, allRawGenomeIdentifiersList, true, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, nil + return true, genomesWithMetadataList, allRawGenomeIdentifiersList, true, onlyExcludeConflictsGenomeIdentifier, onlyIncludeSharedGenomeIdentifier, nil } //Outputs: diff --git a/internal/genetics/readBiobankData/openSNP.go b/internal/genetics/readBiobankData/openSNP.go index 0f8f3ee..2492d44 100644 --- a/internal/genetics/readBiobankData/openSNP.go +++ b/internal/genetics/readBiobankData/openSNP.go @@ -7,6 +7,7 @@ import "seekia/internal/helpers" import "encoding/csv" import "os" import "io" +import "strings" type PhenotypeData_OpenSNP struct{ @@ -237,7 +238,7 @@ func ReadOpenSNPPhenotypesFile(fileObject *os.File)(bool, []PhenotypeData_OpenSN // -int: User height (in centimeters) getUserHeight := func()(bool, float64){ - userHeightRaw := userDataLineSlice[13] + userHeightRaw := userDataLineSlice[13] switch userHeightRaw{ case "-":{ @@ -273,64 +274,88 @@ func ReadOpenSNPPhenotypesFile(fileObject *os.File)(bool, []PhenotypeData_OpenSN case `4'9"`:{ return true, 144.78 } - case `4'10"`:{ + case `4'10"`, `4'10`:{ return true, 147.32 } - case `4'11"`:{ + case `4'11"`, `4' 11"`:{ return true, 149.86 } - case `5'`:{ + case `5'`, `5' 0"`, `5' 0'`, `5'0"`, `5'0`:{ return true, 152.4 } - case `5'1"`:{ + case `5'1"`, `5'1" or 155cm`:{ return true, 154.94 } + case `5'1.5"`:{ + return true, 156.21 + } case `5'2"`:{ return true, 157.48 } - case `5'3"`, `5'3''`, `160 cm`:{ + case `5' 2 1/2"`:{ + return true, 158.75 + } + case `5'2.75" at max`:{ + return true, 159.385 + } + case `5'3"`, `5'3''`:{ return true, 160 } - case `5'4"`:{ + case `5' 3.5" `:{ + return true, 161.29 + } + case `5'4"`, `5'4`, `5'4''`, "162.56", `64"`:{ return true, 162.56 } - case `5'5"`:{ + case `5'4 3/4, taller than all females in my family`:{ + return true, 164.465 + } + case `5'5"`, `5' 5"`, `5'5" `, `5’5”`:{ return true, 165.1 } - case `5'6"`:{ + case "167":{ + return true, 167 + } + case `5'6"`, `5’6`:{ return true, 167.64 } - case `168 cm`:{ - return true, 168 + case `5' 6.5" `:{ + return true, 168.91 + } + case `169.316`:{ + return true, 169.316 } case `5'7"`:{ return true, 170.18 } + case `Average ( 165cm < x < 180cm )`:{ + return true, 172.5 + } case `5'8"`:{ return true, 172.72 } - case `5'9"`:{ + case `Average 173cm`:{ + return true, 173 + } + case `5'8.5"`:{ + return true, 173.99 + } + case `5'9"`, `5'9"/176cm`:{ return true, 175.26 } + case `5" 9 1/2"`:{ + return true, 176.53 + } case `5'10"`, `5'10''`:{ return true, 177.8 } - case `179 cm`:{ - return true, 179 - } - case `180cm`:{ - return true, 180 - } case `5'11"`:{ return true, 180.34 } - case `6'`:{ + case `6'`, `6 ft 0 in`, `6'0" - 183cm`:{ return true, 182.88 } - case `183 cm`:{ - return true, 183 - } - case `6'1"`:{ + case `6'1"`, `6' 1" - 185 cm`:{ return true, 185.42 } case `6'2"`:{ @@ -339,6 +364,9 @@ func ReadOpenSNPPhenotypesFile(fileObject *os.File)(bool, []PhenotypeData_OpenSN case `6'3"`:{ return true, 190.5 } + case "192":{ + return true, 192 + } case `6'4"`:{ return true, 193.04 } @@ -348,6 +376,9 @@ func ReadOpenSNPPhenotypesFile(fileObject *os.File)(bool, []PhenotypeData_OpenSN case `6'6"`:{ return true, 198.12 } + case `>200cm`:{ + return true, 200 + } case `6'7"`:{ return true, 200.66 } @@ -366,8 +397,28 @@ func ReadOpenSNPPhenotypesFile(fileObject *os.File)(bool, []PhenotypeData_OpenSN case `7'`:{ return true, 213.36 } + } - //TODO: Add more responses + trimmedHeight, suffixExists := strings.CutSuffix(userHeightRaw, "cm") + if (suffixExists == true){ + heightFloat64, err := helpers.ConvertStringToFloat64(trimmedHeight) + if (err == nil){ + return true, heightFloat64 + } + } + + trimmedHeight, suffixExists = strings.CutSuffix(userHeightRaw, " cm") + if (suffixExists == true){ + heightFloat64, err := helpers.ConvertStringToFloat64(trimmedHeight) + if (err == nil){ + return true, heightFloat64 + } + } + + // This is an outcome with backticks and quotes + result := "6`" + `2"` + if (userHeightRaw == result){ + return true, 187.96 } return false, 0 diff --git a/internal/helpers/helpers.go b/internal/helpers/helpers.go index 8f9ef12..29bde7c 100644 --- a/internal/helpers/helpers.go +++ b/internal/helpers/helpers.go @@ -1948,29 +1948,29 @@ func ConvertFloat64ToRoundedStringWithTranslatedUnits(inputFloat float64)(string } -// This function takes a number and the min and max range of that number -// It returns a number scaled between a new min and max -func ScaleNumberProportionally(ascending bool, input int, inputMin int, inputMax int, newMin int, newMax int)(int, error){ +// This function takes an int and the min and max range of that int +// It returns an int scaled between a new min and max +func ScaleIntProportionally(ascending bool, input int, inputMin int, inputMax int, newMin int, newMax int)(int, error){ if (inputMin == inputMax) { return inputMin, nil } if (inputMin > inputMax) { - return 0, errors.New("ScaleNumberProportionally error: InputMin is greater than inputMax") + return 0, errors.New("ScaleIntProportionally error: InputMin is greater than inputMax") } if (input < inputMin) { - return 0, errors.New("ScaleNumberProportionally error: Input is less than inputMin") + return 0, errors.New("ScaleIntProportionally error: Input is less than inputMin") } if (input > inputMax) { - return 0, errors.New("ScaleNumberProportionally error: Input is greater than inputMax") + return 0, errors.New("ScaleIntProportionally error: Input is greater than inputMax") } if (newMin == newMax) { return newMin, nil } if (newMin > newMin){ - return 0, errors.New("ScaleNumberProportionally error: newMin is greater than newMin.") + return 0, errors.New("ScaleIntProportionally error: newMin is greater than newMin.") } inputRangePortionLength := input - inputMin @@ -2002,6 +2002,58 @@ func ScaleNumberProportionally(ascending bool, input int, inputMin int, inputMax return result, nil } +// This function takes an float64 and the min and max range of that float64 +// It returns a float64 scaled between a new min and max +func ScaleFloat64Proportionally(ascending bool, input float64, inputMin float64, inputMax float64, newMin float64, newMax float64)(float64, error){ + + if (inputMin == inputMax) { + return inputMin, nil + } + if (inputMin > inputMax) { + return 0, errors.New("ScaleFloat64Proportionally error: InputMin is greater than inputMax") + } + + if (input < inputMin) { + return 0, errors.New("ScaleFloat64Proportionally error: Input is less than inputMin") + } + if (input > inputMax) { + return 0, errors.New("ScaleFloat64Proportionally error: Input is greater than inputMax") + } + + if (newMin == newMax) { + return newMin, nil + } + if (newMin > newMin){ + return 0, errors.New("ScaleFloat64Proportionally error: newMin is greater than newMin.") + } + + inputRangePortionLength := input - inputMin + + inputRangeDistance := inputMax - inputMin + + inputRangePortion := inputRangePortionLength/inputRangeDistance + + // This represents the portion of our output range that we want to travel across + getOutputRangePortion := func()float64{ + if (ascending == true){ + return inputRangePortion + } + + outputRangePortion := 1 - inputRangePortion + return outputRangePortion + } + + outputRangePortion := getOutputRangePortion() + + outputRangeDistance := newMax - newMin + + outputRangePortionLength := outputRangeDistance * outputRangePortion + + result := newMin + outputRangePortionLength + + return result, nil +} + func XORTwo32ByteArrays(array1 [32]byte, array2 [32]byte)[32]byte{ var newArray [32]byte diff --git a/internal/helpers/helpers_test.go b/internal/helpers/helpers_test.go index c95aaef..6a16027 100644 --- a/internal/helpers/helpers_test.go +++ b/internal/helpers/helpers_test.go @@ -321,86 +321,86 @@ func TestSliceFunctions(t *testing.T){ func TestNumberProportionalScaling(t *testing.T){ - result, err := helpers.ScaleNumberProportionally(true, 50, 0, 100, 0, 50) + result, err := helpers.ScaleIntProportionally(true, 50, 0, 100, 0, 50) if (err != nil){ - t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + t.Fatalf("ScaleIntProportionally failed: " + err.Error()) } if (result != 25){ - t.Fatalf("ScaleNumberProportionally failed test 1.") + t.Fatalf("ScaleIntProportionally failed test 1.") } - result, err = helpers.ScaleNumberProportionally(true, 25, 0, 100, 0, 200) + result, err = helpers.ScaleIntProportionally(true, 25, 0, 100, 0, 200) if (err != nil){ - t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + t.Fatalf("ScaleIntProportionally failed: " + err.Error()) } if (result != 50){ - t.Fatalf("ScaleNumberProportionally failed test 2.") + t.Fatalf("ScaleIntProportionally failed test 2.") } - result, err = helpers.ScaleNumberProportionally(false, 25, 0, 100, 0, 200) + result, err = helpers.ScaleIntProportionally(false, 25, 0, 100, 0, 200) if (err != nil){ - t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + t.Fatalf("ScaleIntProportionally failed: " + err.Error()) } if (result != 150){ - t.Fatalf("ScaleNumberProportionally failed test 3.") + t.Fatalf("ScaleIntProportionally failed test 3.") } - result, err = helpers.ScaleNumberProportionally(true, 1, 0, 10, 0, 200) + result, err = helpers.ScaleIntProportionally(true, 1, 0, 10, 0, 200) if (err != nil){ - t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + t.Fatalf("ScaleIntProportionally failed: " + err.Error()) } if (result != 20){ - t.Fatalf("ScaleNumberProportionally failed test 4.") + t.Fatalf("ScaleIntProportionally failed test 4.") } - result, err = helpers.ScaleNumberProportionally(true, -50, -100, 0, 0, 200) + result, err = helpers.ScaleIntProportionally(true, -50, -100, 0, 0, 200) if (err != nil){ - t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + t.Fatalf("ScaleIntProportionally failed: " + err.Error()) } if (result != 100){ - t.Fatalf("ScaleNumberProportionally failed test 5.") + t.Fatalf("ScaleIntProportionally failed test 5.") } - result, err = helpers.ScaleNumberProportionally(true, -25, -100, 0, 0, 200) + result, err = helpers.ScaleIntProportionally(true, -25, -100, 0, 0, 200) if (err != nil){ - t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + t.Fatalf("ScaleIntProportionally failed: " + err.Error()) } if (result != 150){ - t.Fatalf("ScaleNumberProportionally failed test 6.") + t.Fatalf("ScaleIntProportionally failed test 6.") } - result, err = helpers.ScaleNumberProportionally(true, 50, 0, 100, 0, 2) + result, err = helpers.ScaleIntProportionally(true, 50, 0, 100, 0, 2) if (err != nil){ - t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + t.Fatalf("ScaleIntProportionally failed: " + err.Error()) } if (result != 1){ - t.Fatalf("ScaleNumberProportionally failed test 7.") + t.Fatalf("ScaleIntProportionally failed test 7.") } - result, err = helpers.ScaleNumberProportionally(true, 10, 0, 100, 5, 25) + result, err = helpers.ScaleIntProportionally(true, 10, 0, 100, 5, 25) if (err != nil){ - t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + t.Fatalf("ScaleIntProportionally failed: " + err.Error()) } if (result != 7){ - t.Fatalf("ScaleNumberProportionally failed test 8.") + t.Fatalf("ScaleIntProportionally failed test 8.") } - result, err = helpers.ScaleNumberProportionally(true, 100, 0, 100, 2, 22) + result, err = helpers.ScaleIntProportionally(true, 100, 0, 100, 2, 22) if (err != nil){ - t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + t.Fatalf("ScaleIntProportionally failed: " + err.Error()) } if (result != 22){ - t.Fatalf("ScaleNumberProportionally failed test 9.") + t.Fatalf("ScaleIntProportionally failed test 9.") } - result, err = helpers.ScaleNumberProportionally(false, 0, 0, 100, 2, 22) + result, err = helpers.ScaleIntProportionally(false, 0, 0, 100, 2, 22) if (err != nil){ - t.Fatalf("ScaleNumberProportionally failed: " + err.Error()) + t.Fatalf("ScaleIntProportionally failed: " + err.Error()) } if (result != 22){ - t.Fatalf("ScaleNumberProportionally failed test 10.") + t.Fatalf("ScaleIntProportionally failed test 10.") } } diff --git a/internal/imageEffects/imageEffects.go b/internal/imageEffects/imageEffects.go index 06a6882..b5218e6 100644 --- a/internal/imageEffects/imageEffects.go +++ b/internal/imageEffects/imageEffects.go @@ -121,7 +121,7 @@ func GetImageWithEmojiOverlay(inputImage image.Image, emojiImage image.Image, em imageLongerSideLength := max(imageWidth, imageHeight) // This is the length of the longest side of the emoji we will draw - emojiLongerSideMaximumLength, err := helpers.ScaleNumberProportionally(true, emojiScale, 0, 100, 1, imageLongerSideLength) + emojiLongerSideMaximumLength, err := helpers.ScaleIntProportionally(true, emojiScale, 0, 100, 1, imageLongerSideLength) if (err != nil) { return nil, err } newEmoji, err := imagery.ResizeGolangImage(emojiImage, emojiLongerSideMaximumLength) @@ -130,10 +130,10 @@ func GetImageWithEmojiOverlay(inputImage image.Image, emojiImage image.Image, em // Now we get the X and Y Coordinate point for where we will draw the emoji // We first find the center coordinate of the emoji we are drawing - emojiCenterXCoordinate, err := helpers.ScaleNumberProportionally(true, xAxisPercentage, 0, 100, 0, imageWidth) + emojiCenterXCoordinate, err := helpers.ScaleIntProportionally(true, xAxisPercentage, 0, 100, 0, imageWidth) if (err != nil) { return nil, err } - emojiCenterYCoordinate, err := helpers.ScaleNumberProportionally(false, yAxisPercentage, 0, 100, 0, imageHeight) + emojiCenterYCoordinate, err := helpers.ScaleIntProportionally(false, yAxisPercentage, 0, 100, 0, imageHeight) if (err != nil) { return nil, err } emojiWidth, emojiHeight, err := imagery.GetImageWidthAndHeightPixels(newEmoji) diff --git a/internal/imagery/imagery.go b/internal/imagery/imagery.go index 099c615..cdc76bd 100644 --- a/internal/imagery/imagery.go +++ b/internal/imagery/imagery.go @@ -497,7 +497,7 @@ func PixelateGolangImage(inputImage image.Image, amount0to100 int)(image.Image, longerSideLength := max(widthPixels, heightPixels) - pixelationAmountInt, err := helpers.ScaleNumberProportionally(true, amount0to100, 0, 100, 0, longerSideLength/5) + pixelationAmountInt, err := helpers.ScaleIntProportionally(true, amount0to100, 0, 100, 0, longerSideLength/5) if (err != nil) { return nil, err } rectangle := image.Rect(0, 0, widthPixels, heightPixels) diff --git a/internal/messaging/myChatConversations/myChatConversations.go b/internal/messaging/myChatConversations/myChatConversations.go index 9ccd97a..fa21b97 100644 --- a/internal/messaging/myChatConversations/myChatConversations.go +++ b/internal/messaging/myChatConversations/myChatConversations.go @@ -469,7 +469,7 @@ func StartUpdatingMyConversations(identityType string, networkType byte) error{ // Input is a value between 0-100 // We reduce it down to a value between 0-50 - newProgressInt, err := helpers.ScaleNumberProportionally(true, input, 0, 100, 0, 50) + newProgressInt, err := helpers.ScaleIntProportionally(true, input, 0, 100, 0, 50) if (err != nil) { return err } newPercentageProgressFloat := float64(newProgressInt)/100 @@ -715,7 +715,7 @@ func StartUpdatingMyConversations(identityType string, networkType byte) error{ return nil } - newScaledPercentageInt, err := helpers.ScaleNumberProportionally(true, index, 0, maximumIndex, 70, 85) + newScaledPercentageInt, err := helpers.ScaleIntProportionally(true, index, 0, maximumIndex, 70, 85) if (err != nil) { return err } newProgressFloat := float64(newScaledPercentageInt)/100 diff --git a/internal/messaging/myChatMessages/myChatMessages.go b/internal/messaging/myChatMessages/myChatMessages.go index 5142d85..3d4cb9c 100644 --- a/internal/messaging/myChatMessages/myChatMessages.go +++ b/internal/messaging/myChatMessages/myChatMessages.go @@ -268,7 +268,7 @@ func GetUpdatedMyChatMessagesMapList(myIdentityType string, networkType byte, up for messageHash, messageInbox := range myRawInboxMessageHashesMap{ - newPercentageProgress, err := helpers.ScaleNumberProportionally(true, index, 0, maximumIndex, 20, 100) + newPercentageProgress, err := helpers.ScaleIntProportionally(true, index, 0, maximumIndex, 20, 100) if (err != nil){ return err } err = updateProgressFunction(newPercentageProgress) diff --git a/internal/moderation/viewedContent/viewedContent.go b/internal/moderation/viewedContent/viewedContent.go index a76abef..c8baa39 100644 --- a/internal/moderation/viewedContent/viewedContent.go +++ b/internal/moderation/viewedContent/viewedContent.go @@ -480,7 +480,7 @@ func StartUpdatingViewedContent(networkType byte)error{ return nil } - newScaledPercentageInt, err := helpers.ScaleNumberProportionally(true, index, 0, maximumIndex, 50, 80) + newScaledPercentageInt, err := helpers.ScaleIntProportionally(true, index, 0, maximumIndex, 50, 80) if (err != nil) { return err } newProgressFloat := float64(newScaledPercentageInt)/100 diff --git a/internal/moderation/viewedModerators/viewedModerators.go b/internal/moderation/viewedModerators/viewedModerators.go index 24864ff..c279702 100644 --- a/internal/moderation/viewedModerators/viewedModerators.go +++ b/internal/moderation/viewedModerators/viewedModerators.go @@ -365,7 +365,7 @@ func StartUpdatingViewedModerators(networkType byte)error{ return nil } - progressPercentage, err := helpers.ScaleNumberProportionally(true, index, 0, numberOfModerators-1, 20, 50) + progressPercentage, err := helpers.ScaleIntProportionally(true, index, 0, numberOfModerators-1, 20, 50) if (err != nil) { return err } progressFloat := float64(progressPercentage)/100 @@ -452,7 +452,7 @@ func StartUpdatingViewedModerators(networkType byte)error{ return nil } - newScaledPercentageInt, err := helpers.ScaleNumberProportionally(true, index, 0, maximumIndex, 50, 80) + newScaledPercentageInt, err := helpers.ScaleIntProportionally(true, index, 0, maximumIndex, 50, 80) if (err != nil) { return err } newProgressFloat := float64(newScaledPercentageInt)/100 diff --git a/internal/myMatches/myMatches.go b/internal/myMatches/myMatches.go index 801d1a1..10f7cef 100644 --- a/internal/myMatches/myMatches.go +++ b/internal/myMatches/myMatches.go @@ -323,7 +323,7 @@ func StartUpdatingMyMatches(networkType byte)error{ return nil } - progressPercentage, err := helpers.ScaleNumberProportionally(true, index, 0, maximumIndex, 0, 50) + progressPercentage, err := helpers.ScaleIntProportionally(true, index, 0, maximumIndex, 0, 50) if (err != nil) { return err } progressFloat := float64(progressPercentage)/100 @@ -449,7 +449,7 @@ func StartUpdatingMyMatches(networkType byte)error{ return nil } - newScaledPercentageInt, err := helpers.ScaleNumberProportionally(true, index, 0, maximumIndex, 50, 80) + newScaledPercentageInt, err := helpers.ScaleIntProportionally(true, index, 0, maximumIndex, 50, 80) if (err != nil) { return err } newProgressFloat := float64(newScaledPercentageInt)/100 diff --git a/internal/network/viewedHosts/viewedHosts.go b/internal/network/viewedHosts/viewedHosts.go index 5c597c9..aaaa146 100644 --- a/internal/network/viewedHosts/viewedHosts.go +++ b/internal/network/viewedHosts/viewedHosts.go @@ -335,7 +335,7 @@ func StartUpdatingViewedHosts(networkType byte)error{ return nil } - progressPercentage, err := helpers.ScaleNumberProportionally(true, index, 0, numberOfEnabledHosts-1, 0, 50) + progressPercentage, err := helpers.ScaleIntProportionally(true, index, 0, numberOfEnabledHosts-1, 0, 50) if (err != nil) { return err } progressFloat := float64(progressPercentage)/100 @@ -413,13 +413,13 @@ func StartUpdatingViewedHosts(networkType byte)error{ hostAttributeValuesMap[hostIdentityHashString] = attributeValueFloat } - + isStopped := CheckIfBuildViewedHostsIsStopped() if (isStopped == true){ return nil } - newScaledPercentageInt, err := helpers.ScaleNumberProportionally(true, index, 0, maximumIndex, 50, 80) + newScaledPercentageInt, err := helpers.ScaleIntProportionally(true, index, 0, maximumIndex, 50, 80) if (err != nil) { return err } newProgressFloat := float64(newScaledPercentageInt)/100 @@ -429,47 +429,47 @@ func StartUpdatingViewedHosts(networkType byte)error{ appMemory.SetMemoryEntry("ViewedHostsReadyProgressStatus", newProgressString) } - compareHostsFunction := func(identityHashA string, identityHashB string)int{ + compareHostsFunction := func(identityHash1 string, identityHash2 string)int{ - if (identityHashA == identityHashB){ + if (identityHash1 == identityHash2){ panic("compareHostsFunction called with duplicate hosts.") } - attributeValueA, attributeValueAExists := hostAttributeValuesMap[identityHashA] + attributeValue1, attributeValue1Exists := hostAttributeValuesMap[identityHash1] - attributeValueB, attributeValueBExists := hostAttributeValuesMap[identityHashB] + attributeValue2, attributeValue2Exists := hostAttributeValuesMap[identityHash2] - if (attributeValueAExists == false && attributeValueBExists == false){ + if (attributeValue1Exists == false && attributeValue2Exists == false){ // We don't know the attribute value for either host // We sort hosts in unicode order - if (identityHashA < identityHashB){ + if (identityHash1 < identityHash2){ return -1 } return 1 - } else if (attributeValueAExists == true && attributeValueBExists == false){ + } else if (attributeValue1Exists == true && attributeValue2Exists == false){ // We sort unknown attribute hosts to the back of the list return -1 - } else if (attributeValueAExists == false && attributeValueBExists == true){ + } else if (attributeValue1Exists == false && attributeValue2Exists == true){ return 1 } // Both attribute values exist - if (attributeValueA == attributeValueB){ + if (attributeValue1 == attributeValue2){ // We sort identity hashes in unicode order - if (identityHashA < identityHashB){ + if (identityHash1 < identityHash2){ return -1 } return 1 } - if (attributeValueA < attributeValueB){ + if (attributeValue1 < attributeValue2){ if (currentSortDirection == "Ascending"){ return -1 diff --git a/internal/profiles/myProfileExports/myProfileExports.go b/internal/profiles/myProfileExports/myProfileExports.go index e571261..0569c50 100644 --- a/internal/profiles/myProfileExports/myProfileExports.go +++ b/internal/profiles/myProfileExports/myProfileExports.go @@ -153,11 +153,6 @@ func UpdateMyExportedProfile(myProfileType string, networkType byte)error{ _, _, _, _, myGenomesMap, err := readGeneticAnalysis.GetMetadataFromPersonGeneticAnalysis(myGeneticAnalysisObject) if (err != nil) { return err } - myGenomeLocusValuesMap, exists := myGenomesMap[genomeIdentifierToShare] - if (exists == false){ - return errors.New("GetMyChosenMateGeneticAnalysis returning genetic analysis which has GenomesMap which is missing my genome identifier.") - } - monogenicDiseaseNamesList, err := monogenicDiseases.GetMonogenicDiseaseNamesList() if (err != nil) { return err } @@ -253,6 +248,11 @@ func UpdateMyExportedProfile(myProfileType string, networkType byte)error{ } } + myGenomeLocusValuesMap, exists := myGenomesMap[genomeIdentifierToShare] + if (exists == false){ + return errors.New("GetMyChosenMateGeneticAnalysis returning genetic analysis which has GenomesMap which is missing my genome identifier.") + } + for rsID, _ := range myLociToShareMap{ locusValueObject, exists := myGenomeLocusValuesMap[rsID] diff --git a/utilities/createGeneticModels/createGeneticModels.go b/utilities/createGeneticModels/createGeneticModels.go index faa1b61..202fc80 100644 --- a/utilities/createGeneticModels/createGeneticModels.go +++ b/utilities/createGeneticModels/createGeneticModels.go @@ -38,6 +38,7 @@ import "os" import "strings" import "sync" import "slices" +import "math" import mathRand "math/rand/v2" import goFilepath "path/filepath" import "time" @@ -722,7 +723,7 @@ func setStartAndMonitorCreateTrainingDataPage(window fyne.Window, previousPage f if (err != nil) { return false, false, err } //TODO: Add more traits - traitNamesList := []string{"Eye Color", "Lactose Tolerance"} + traitNamesList := []string{"Eye Color", "Lactose Tolerance", "Height"} // We create the folders for each trait's training data @@ -765,7 +766,7 @@ func setStartAndMonitorCreateTrainingDataPage(window fyne.Window, previousPage f err = progressDetailsBinding.Set(progressDetailsStatus) if (err != nil) { return false, false, err } - trainingProgressPercentage, err := helpers.ScaleNumberProportionally(true, index, 0, maximumIndex, 0, 100) + trainingProgressPercentage, err := helpers.ScaleIntProportionally(true, index, 0, maximumIndex, 0, 100) if (err != nil) { return false, false, err } trainingProgressFloat64 := float64(trainingProgressPercentage)/100 @@ -837,18 +838,26 @@ func setStartAndMonitorCreateTrainingDataPage(window fyne.Window, previousPage f continue } - getUserLociValuesMap := func()(map[int64]locusValue.LocusValue, error){ + //Outputs: + // -bool: Any useful locations exist in any of the user's genomes + // -map[int64]locusValue.LocusValue + // -error + getUserLociValuesMap := func()(bool, map[int64]locusValue.LocusValue, error){ updatePercentageCompleteFunction := func(_ int)error{ return nil } - genomesWithMetadataList, _, combinedGenomesExist, onlyExcludeConflictsGenomeIdentifier, _, err := prepareRawGenomes.GetGenomesWithMetadataListFromRawGenomesList(userRawGenomesWithMetadataList, updatePercentageCompleteFunction) - if (err != nil) { return nil, err } + anyUsefulLocationsExist, genomesWithMetadataList, _, combinedGenomesExist, onlyExcludeConflictsGenomeIdentifier, _, err := prepareRawGenomes.GetGenomesWithMetadataListFromRawGenomesList(userRawGenomesWithMetadataList, updatePercentageCompleteFunction) + if (err != nil) { return false, nil, err } + if (anyUsefulLocationsExist == false){ + // None of the user's genomes have any useful locations + return false, nil, nil + } if (combinedGenomesExist == false){ if (len(genomesWithMetadataList) != 1){ - return nil, errors.New("GetGenomesWithMetadataListFromRawGenomesList returning non-1 length genomesWithMetadataList when combinedGenomesExist == false") + return false, nil, errors.New("GetGenomesWithMetadataListFromRawGenomesList returning non-1 length genomesWithMetadataList when combinedGenomesExist == false") } // Only 1 genome exists @@ -857,7 +866,7 @@ func setStartAndMonitorCreateTrainingDataPage(window fyne.Window, previousPage f genomeMap := genomeWithMetadataObject.GenomeMap - return genomeMap, nil + return true, genomeMap, nil } for _, genomeWithMetadataObject := range genomesWithMetadataList{ @@ -868,15 +877,19 @@ func setStartAndMonitorCreateTrainingDataPage(window fyne.Window, previousPage f genomeMap := genomeWithMetadataObject.GenomeMap - return genomeMap, nil + return true, genomeMap, nil } } - return nil, errors.New("OnlyExcludeConflicts genome not found from GetGenomesWithMetadataListFromRawGenomesList's returned list.") + return false, nil, errors.New("OnlyExcludeConflicts genome not found from GetGenomesWithMetadataListFromRawGenomesList's returned list.") } - userLociValuesMap, err := getUserLociValuesMap() + anyUsefulLocationsExist, userLociValuesMap, err := getUserLociValuesMap() if (err != nil) { return false, false, err } + if (anyUsefulLocationsExist == false){ + // None of the user's genome files contain any useful locations + continue + } for _, traitName := range traitNamesList{ @@ -980,7 +993,7 @@ func setTrainModelsPage(window fyne.Window, previousPage func()){ description3 := getLabelCentered("This will take a while.") description4 := getLabelCentered("You must select a trait model to train.") - traitNamesList := []string{"Eye Color", "Lactose Tolerance"} + traitNamesList := []string{"Eye Color", "Lactose Tolerance", "Height"} traitNameSelector := widget.NewSelect(traitNamesList, nil) @@ -1305,7 +1318,7 @@ func setTestModelsPage(window fyne.Window, previousPage func()){ description5 := getLabelCentered("The results will also be saved in the ModelAccuracies folder.") description6 := getLabelCentered("You must select a trait model to test.") - traitNamesList := []string{"Eye Color", "Lactose Tolerance"} + traitNamesList := []string{"Eye Color", "Lactose Tolerance", "Height"} traitNameSelector := widget.NewSelect(traitNamesList, nil) @@ -1383,7 +1396,7 @@ func setStartAndMonitorTestModelPage(window fyne.Window, traitName string, previ //Outputs: // -bool: Process completed (true == was not stopped mid-way) - // -geneticPrediction.TraitPredictionAccuracyInfoMap + // -geneticPrediction.DiscreteTraitPredictionAccuracyInfoMap // -error testModel := func()(bool, geneticPrediction.DiscreteTraitPredictionAccuracyInfoMap, error){ @@ -1464,10 +1477,10 @@ func setStartAndMonitorTestModelPage(window fyne.Window, traitName string, previ return false, nil, errors.New("Neural network prediction output length does not match expected output length.") } - correctOutcomeName, err := geneticPrediction.GetOutcomeNameFromOutputLayer(traitName, true, trainingDataExpectedOutputLayer) + correctOutcomeName, err := geneticPrediction.GetDiscreteOutcomeNameFromOutputLayer(traitName, true, trainingDataExpectedOutputLayer) if (err != nil) { return false, nil, err } - predictedOutcomeName, err := geneticPrediction.GetOutcomeNameFromOutputLayer(traitName, true, predictionLayer) + predictedOutcomeName, err := geneticPrediction.GetDiscreteOutcomeNameFromOutputLayer(traitName, true, predictionLayer) if (err != nil) { return false, nil, err } getPredictionIsCorrectBool := func()bool{ @@ -1565,7 +1578,7 @@ func setStartAndMonitorTestModelPage(window fyne.Window, traitName string, previ // This map stores the accuracy for each outcome traitPredictionAccuracyInfoMap := make(map[geneticPrediction.DiscreteTraitOutcomeInfo]geneticPrediction.DiscreteTraitPredictionAccuracyInfo) - for traitAccuracyData, value := range traitPredictionInfoMap{ + for traitPredictionInfo, value := range traitPredictionInfoMap{ quantityOfExamples := value.QuantityOfExamples quantityOfPredictions := value.QuantityOfPredictions @@ -1601,7 +1614,7 @@ func setStartAndMonitorTestModelPage(window fyne.Window, traitName string, previ newTraitPredictionAccuracyInfo.ProbabilityOfCorrectOutcomePrediction = percentageOfCorrectOutcomePredictions } - traitPredictionAccuracyInfoMap[traitAccuracyData] = newTraitPredictionAccuracyInfo + traitPredictionAccuracyInfoMap[traitPredictionInfo] = newTraitPredictionAccuracyInfo } // Testing is complete. @@ -1635,6 +1648,224 @@ func setStartAndMonitorTestModelPage(window fyne.Window, traitName string, previ } setViewModelTestingDiscreteTraitResultsPage(window, traitName, traitPredictionAccuracyInfoMap, previousPage) + + } else { + + // traitIsDiscreteOrNumeric == "Numeric" + + //Outputs: + // -bool: Process completed (true == was not stopped mid-way) + // -geneticPrediction.NumericTraitPredictionAccuracyInfoMap + // -error + testModel := func()(bool, geneticPrediction.NumericTraitPredictionAccuracyInfoMap, error){ + + // We use this map to count up the information about predictions + // We use information from this map to construct the final accuracy information map + // Map Structure: NumericTraitOutcomeInfo -> List of true outcomes + traitPredictionInfoMap := make(map[geneticPrediction.NumericTraitOutcomeInfo][]float64) + + + _, testingSetFilepathsList, err := getTrainingAndTestingDataFilepathLists(traitName) + if (err != nil) { return false, nil, err } + + traitNameWithoutWhitespaces := strings.ReplaceAll(traitName, " ", "") + + // We read the trained model for this trait + modelFilename := traitNameWithoutWhitespaces + "Model.gob" + + trainedModelFilepath := goFilepath.Join("./TrainedModels/", modelFilename) + + fileExists, fileContents, err := localFilesystem.GetFileContents(trainedModelFilepath) + if (err != nil) { return false, nil, err } + if (fileExists == false){ + return false, nil, errors.New("TrainedModel not found: " + trainedModelFilepath) + } + + neuralNetworkObject, err := geneticPrediction.DecodeBytesToNeuralNetworkObject(fileContents) + if (err != nil) { return false, nil, err } + + numberOfTrainingDatas := len(testingSetFilepathsList) + numberOfTrainingDatasString := helpers.ConvertIntToString(numberOfTrainingDatas) + + finalIndex := numberOfTrainingDatas - 1 + + for index, filePath := range testingSetFilepathsList{ + + testModelIsStoppedBoolMutex.RLock() + testModelIsStopped := testModelIsStoppedBool + testModelIsStoppedBoolMutex.RUnlock() + + if (testModelIsStopped == true){ + // User exited the process + return false, nil, nil + } + + fileExists, fileContents, err := localFilesystem.GetFileContents(filePath) + if (err != nil) { return false, nil, err } + if (fileExists == false){ + return false, nil, errors.New("TrainingData file not found: " + filePath) + } + + trainingDataObject, err := geneticPrediction.DecodeBytesToTrainingDataObject(fileContents) + if (err != nil) { return false, nil, err } + + trainingDataInputLayer := trainingDataObject.InputLayer + trainingDataExpectedOutputLayer := trainingDataObject.OutputLayer + + predictionLayer, err := geneticPrediction.GetNeuralNetworkRawPrediction(&neuralNetworkObject, false, trainingDataInputLayer) + if (err != nil) { return false, nil, err } + + if (len(predictionLayer) != 1){ + return false, nil, errors.New("Neural network numeric prediction output layer length is not 1.") + } + + if (len(trainingDataExpectedOutputLayer) != 1){ + return false, nil, errors.New("Neural network training data prediction output layer length is not 1.") + } + + correctOutcomeValue, err := geneticPrediction.GetNumericOutcomeValueFromOutputLayer(traitName, trainingDataExpectedOutputLayer) + if (err != nil) { return false, nil, err } + + predictedOutcomeValue, err := geneticPrediction.GetNumericOutcomeValueFromOutputLayer(traitName, predictionLayer) + if (err != nil) { return false, nil, err } + + numberOfKnownLoci, numberOfKnownAndPhasedLoci, numberOfLoci, err := geneticPrediction.GetLociInfoFromNetworkInputLayer(trainingDataInputLayer) + if (err != nil) { return false, nil, err } + + proportionOfLociTested := float64(numberOfKnownLoci)/float64(numberOfLoci) + percentageOfLociTested := int(100*proportionOfLociTested) + + proportionOfPhasedLoci := float64(numberOfKnownAndPhasedLoci)/float64(numberOfKnownLoci) + percentageOfPhasedLoci := int(100*proportionOfPhasedLoci) + + newNumericTraitOutcomeInfo := geneticPrediction.NumericTraitOutcomeInfo{ + OutcomeValue: predictedOutcomeValue, + PercentageOfLociTested: percentageOfLociTested, + PercentageOfPhasedLoci: percentageOfPhasedLoci, + } + + existingList, exists := traitPredictionInfoMap[newNumericTraitOutcomeInfo] + if (exists == false){ + traitPredictionInfoMap[newNumericTraitOutcomeInfo] = []float64{correctOutcomeValue} + } else { + existingList = append(existingList, correctOutcomeValue) + traitPredictionInfoMap[newNumericTraitOutcomeInfo] = existingList + } + + exampleIndexString := helpers.ConvertIntToString(index+1) + numberOfExamplesProgress := "Tested " + exampleIndexString + "/" + numberOfTrainingDatasString + " Examples" + + progressDetailsBinding.Set(numberOfExamplesProgress) + + newProgressFloat64 := float64(index)/float64(finalIndex) + + progressPercentageBinding.Set(newProgressFloat64) + } + + // Now we construct the TraitAccuracyInfoMap + + // This map stores the accuracy for each outcome + traitPredictionAccuracyInfoMap := make(map[geneticPrediction.NumericTraitOutcomeInfo]geneticPrediction.NumericTraitPredictionAccuracyRangesMap) + + for traitPredictionInfo, realOutcomesList := range traitPredictionInfoMap{ + + if (len(realOutcomesList) == 0){ + return false, nil, errors.New("traitPredictionInfoMap contains empty realOutcomesList.") + } + + // This is the predicted height value for this set of real outcomes + predictionValue := traitPredictionInfo.OutcomeValue + + // Map Structure: Accuracy Percentage (AP) -> Amount needed to deviate from prediction + // for the value to be accurate (AP)% of the time + newNumericTraitPredictionAccuracyRangesMap := make(map[int]float64) + + rangeDistance := float64(0) + + for { + + rangeMin := predictionValue - rangeDistance + rangeMax := predictionValue + rangeDistance + + valuesInRangeList := make([]float64, 0) + valuesOutOfRangeList := make([]float64, 0) + + for _, outcomeValue := range realOutcomesList{ + + if (outcomeValue <= rangeMax && outcomeValue >= rangeMin){ + valuesInRangeList = append(valuesInRangeList, outcomeValue) + } else { + valuesOutOfRangeList = append(valuesOutOfRangeList, outcomeValue) + } + } + + quantityOfValuesInRange := len(valuesInRangeList) + totalQuantityOfValues := len(realOutcomesList) + + proportionOfValuesInRange := float64(quantityOfValuesInRange)/float64(totalQuantityOfValues) + percentageOfValuesInRange := proportionOfValuesInRange * 100 + + if (percentageOfValuesInRange >= 1){ + percentageOfValuesInRangeInt := int(percentageOfValuesInRange) + + newNumericTraitPredictionAccuracyRangesMap[percentageOfValuesInRangeInt] = rangeDistance + } + if (quantityOfValuesInRange == totalQuantityOfValues){ + newNumericTraitPredictionAccuracyRangesMap[100] = rangeDistance + break + } + + // Now we increase rangeDistance + // We find the distance to the next closest item in the list that isn't already in our range + + nearestValueDistance := float64(0) + + for index, outcomeValue := range valuesOutOfRangeList{ + + distance := math.Abs(predictionValue - outcomeValue) + + if (index == 0 || distance < nearestValueDistance){ + nearestValueDistance = distance + } + } + + rangeDistance += nearestValueDistance + } + + traitPredictionAccuracyInfoMap[traitPredictionInfo] = newNumericTraitPredictionAccuracyRangesMap + } + + // Testing is complete. + + // We save the info map as a file in the ModelAccuracies folder + + fileBytes, err := geneticPrediction.EncodeNumericTraitPredictionAccuracyInfoMapToBytes(traitPredictionAccuracyInfoMap) + if (err != nil) { return false, nil, err } + + _, err = localFilesystem.CreateFolder("./ModelAccuracies") + if (err != nil) { return false, nil, err } + + modelAccuracyFilename := traitNameWithoutWhitespaces + "ModelAccuracy.gob" + + err = localFilesystem.CreateOrOverwriteFile(fileBytes, "./ModelAccuracies/", modelAccuracyFilename) + if (err != nil) { return false, nil, err } + + progressPercentageBinding.Set(1) + + return true, traitPredictionAccuracyInfoMap, nil + } + + processIsComplete, traitPredictionAccuracyInfoMap, err := testModel() + if (err != nil){ + setErrorEncounteredPage(window, err, previousPage) + return + } + if (processIsComplete == false){ + // User exited the page + return + } + + setViewModelTestingNumericTraitResultsPage(window, traitName, traitPredictionAccuracyInfoMap, previousPage) } } @@ -1680,6 +1911,11 @@ func setViewModelTestingDiscreteTraitResultsPage(window fyne.Window, traitName s traitObject, err := traits.GetTraitObject(traitName) if (err != nil) { return nil, err } + traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric + if (traitIsDiscreteOrNumeric != "Discrete"){ + return nil, errors.New("setViewModelTestingDiscreteTraitResultsPage called with non-discrete trait: " + traitName) + } + outcomeNamesList := traitObject.OutcomesList for _, outcomeName := range outcomeNamesList{ @@ -1801,6 +2037,175 @@ func setViewModelTestingDiscreteTraitResultsPage(window fyne.Window, traitName s } +// This is a page to view the details of testing for a specific trait's model +func setViewModelTestingNumericTraitResultsPage(window fyne.Window, traitName string, traitAccuracyInfoMap geneticPrediction.NumericTraitPredictionAccuracyInfoMap, exitPage func()){ + + title := getBoldLabelCentered("Numeric Trait Prediction Accuracy Details") + + exitButton := getWidgetCentered(widget.NewButtonWithIcon("Exit", theme.CancelIcon(), exitPage)) + + description1 := getLabelCentered("The results of the prediction accuracy for this trait are below.") + + traitNameTitle := widget.NewLabel("Trait Name:") + traitNameLabel := getBoldLabel(traitName) + traitNameRow := container.NewHBox(layout.NewSpacer(), traitNameTitle, traitNameLabel, layout.NewSpacer()) + + description2 := getLabelCentered("Each value is a range that the prediction must be widened by to be accurate X% of the time.") + description3 := getLabelCentered("For example, for a height prediction to be accurate 90% of the time, allow a +/-10 cm range.") + + getResultsGrid := func()(*fyne.Container, error){ + + probabilityOfTitle := getItalicLabelCentered("Probability Of") + correctPredictionTitle := getItalicLabelCentered("Correct Prediction") + + accuracyRangeTitle1 := getItalicLabelCentered("Accuracy Range") + knownLociLabel_0to33 := getItalicLabelCentered("0-33% Known Loci") + + accuracyRangeTitle2 := getItalicLabelCentered("Accuracy Range") + knownLociLabel_34to66 := getItalicLabelCentered("34-66% Known Loci") + + accuracyRangeTitle3 := getItalicLabelCentered("Accuracy Range") + knownLociLabel_67to100 := getItalicLabelCentered("67-100% Known Loci") + + probabilityOfCorrectPredictionColumn := container.NewVBox(probabilityOfTitle, correctPredictionTitle, widget.NewSeparator()) + accuracyRangeColumn_0to33 := container.NewVBox(accuracyRangeTitle1, knownLociLabel_0to33, widget.NewSeparator()) + accuracyRangeColumn_34to66 := container.NewVBox(accuracyRangeTitle2, knownLociLabel_34to66, widget.NewSeparator()) + accuracyRangeColumn_67to100 := container.NewVBox(accuracyRangeTitle3, knownLociLabel_67to100, widget.NewSeparator()) + + traitObject, err := traits.GetTraitObject(traitName) + if (err != nil) { return nil, err } + + traitIsDiscreteOrNumeric := traitObject.DiscreteOrNumeric + if (traitIsDiscreteOrNumeric != "Numeric"){ + return nil, errors.New("setViewModelTestingNumericTraitResultsPage called with non-discrete trait: " + traitName) + } + + probabilityMinimumRange := 1 + + for { + + if (probabilityMinimumRange == 100){ + break + } + + getProbabilityMaximumRange := func()int{ + + if (probabilityMinimumRange == 90){ + return 100 + } + + probabilityMaximumRange := probabilityMinimumRange + 9 + + return probabilityMaximumRange + } + + probabilityMaximumRange := getProbabilityMaximumRange() + + probabilityMinimumRangeString := helpers.ConvertIntToString(probabilityMinimumRange) + probabilityMaximumRangeString := helpers.ConvertIntToString(probabilityMaximumRange) + + probabilityOfCorrectPredictionRangeFormatted := probabilityMinimumRangeString + "% - " + probabilityMaximumRangeString + "%" + + probabilityOfCorrectPredictionRangeLabel := getBoldLabelCentered(probabilityOfCorrectPredictionRangeFormatted) + + // We use the below variables to sum up the accuracy distances so we can average them + + predictionAccuracyDistancesSum_0to33 := float64(0) + distancesCount_0to33 := 0 + + predictionAccuracyDistancesSum_34to66 := float64(0) + distancesCount_34to66 := 0 + + predictionAccuracyDistancesSum_67to100 := float64(0) + distancesCount_67to100 := 0 + + for traitOutcomeInfo, traitPredictionAccuracyRangesMap := range traitAccuracyInfoMap{ + + percentageOfLociTested := traitOutcomeInfo.PercentageOfLociTested + + for percentageCorrect, distance := range traitPredictionAccuracyRangesMap{ + + if (percentageCorrect < probabilityMinimumRange || percentageCorrect > probabilityMaximumRange){ + continue + } + + if (percentageOfLociTested <= 33){ + + predictionAccuracyDistancesSum_0to33 += distance + distancesCount_0to33 += 1 + + } else if (percentageOfLociTested > 33 && percentageOfLociTested <= 66){ + + predictionAccuracyDistancesSum_34to66 += distance + distancesCount_34to66 += 1 + + } else { + + predictionAccuracyDistancesSum_67to100 += distance + distancesCount_67to100 += 1 + } + } + } + + getAverageAccuracyText := func(distancesSum float64, distancesCount int)string{ + if (distancesCount == 0){ + return "Unknown" + } + + averageDistance := distancesSum/float64(distancesCount) + + averageDistanceString := helpers.ConvertFloat64ToStringRounded(averageDistance, 1) + + //TODO: Retrieve units from traits package? + result := "+/- " + averageDistanceString + " centimeters" + + return result + } + + averageDistanceText_0to33 := getAverageAccuracyText(predictionAccuracyDistancesSum_0to33, distancesCount_0to33) + averageDistanceText_34to66 := getAverageAccuracyText(predictionAccuracyDistancesSum_34to66, distancesCount_34to66) + averageDistanceText_67to100 := getAverageAccuracyText(predictionAccuracyDistancesSum_67to100, distancesCount_67to100) + + averageDistanceLabel_0to33 := getBoldLabelCentered(averageDistanceText_0to33) + averageDistanceLabel_34to66 := getBoldLabelCentered(averageDistanceText_34to66) + averageDistanceLabel_67to100 := getBoldLabelCentered(averageDistanceText_67to100) + + probabilityOfCorrectPredictionColumn.Add(probabilityOfCorrectPredictionRangeLabel) + accuracyRangeColumn_0to33.Add(averageDistanceLabel_0to33) + accuracyRangeColumn_34to66.Add(averageDistanceLabel_34to66) + accuracyRangeColumn_67to100.Add(averageDistanceLabel_67to100) + + probabilityOfCorrectPredictionColumn.Add(widget.NewSeparator()) + accuracyRangeColumn_0to33.Add(widget.NewSeparator()) + accuracyRangeColumn_34to66.Add(widget.NewSeparator()) + accuracyRangeColumn_67to100.Add(widget.NewSeparator()) + + if (probabilityMinimumRange == 1){ + probabilityMinimumRange = 10 + } else { + probabilityMinimumRange += 10 + } + } + + resultsGrid := container.NewHBox(layout.NewSpacer(), probabilityOfCorrectPredictionColumn, accuracyRangeColumn_0to33, accuracyRangeColumn_34to66, accuracyRangeColumn_67to100, layout.NewSpacer()) + + return resultsGrid, nil + } + + resultsGrid, err := getResultsGrid() + if (err != nil){ + setErrorEncounteredPage(window, err, func(){setHomePage(window)}) + return + } + + page := container.NewVBox(title, exitButton, widget.NewSeparator(), description1, widget.NewSeparator(), traitNameRow, widget.NewSeparator(), description2, description3, widget.NewSeparator(), resultsGrid) + + pageScrollable := container.NewVScroll(page) + + window.SetContent(pageScrollable) +} + + // This function returns a list of training data and testing data filepaths for a trait. //Outputs: // -[]string: Sorted list of training data filepaths @@ -1808,7 +2213,7 @@ func setViewModelTestingDiscreteTraitResultsPage(window fyne.Window, traitName s // -error func getTrainingAndTestingDataFilepathLists(traitName string)([]string, []string, error){ - if (traitName != "Eye Color" && traitName != "Lactose Tolerance"){ + if (traitName != "Eye Color" && traitName != "Lactose Tolerance" && traitName != "Height"){ return nil, nil, errors.New("getTrainingAndTestingDataFilepathLists called with invalid traitName: " + traitName) } @@ -1845,11 +2250,14 @@ func getTrainingAndTestingDataFilepathLists(traitName string)([]string, []string if (traitName == "Eye Color"){ - return 112953, nil + return 113648, nil } else if (traitName == "Lactose Tolerance"){ - return 24808, nil + return 24872, nil + } else if (traitName == "Height"){ + + return 92281, nil } return 0, errors.New("Unknown traitName: " + traitName) @@ -1862,7 +2270,7 @@ func getTrainingAndTestingDataFilepathLists(traitName string)([]string, []string numberOfTrainingDataFilesString := helpers.ConvertIntToString(numberOfTrainingDataFiles) - return nil, nil, errors.New(traitName + " number of training datas is unexpected: " + numberOfTrainingDataFilesString) + return nil, nil, errors.New(traitName + " quantity of training datas is unexpected: " + numberOfTrainingDataFilesString) } // We sort the training data to be in a deterministically random order