├── docs ├── _static │ └── loinc2hpo.css ├── requirements.txt ├── images │ ├── mission.png │ ├── simulate.png │ ├── FHIR_intro.png │ ├── FHIR_resource1.png │ ├── FHIR_resource2.png │ ├── FHIR_resource3.png │ ├── loinc_examples.png │ ├── annotation_scheme.png │ ├── annotation_example1.png │ ├── annotation_example2.png │ ├── queryPatientFromServer.png │ ├── convertObservationsToHPO.png │ └── downloadObservationsFromServer.png ├── Makefile ├── getting_started.rst ├── index.rst ├── intro_to_LOINC.rst ├── conf.py └── FHIR_mapping.rst ├── loinc2hpo-fhir ├── src │ ├── test │ │ ├── resources │ │ │ ├── json │ │ │ │ ├── malformedObservation.fhir │ │ │ │ ├── glucoseNoInterpretationNoReference.fhir │ │ │ │ ├── ecoliNoInterpretation.fhir │ │ │ │ ├── neisseriaNoInterpretation.fhir │ │ │ │ ├── staphylococcusNoInterpretation.fhir │ │ │ │ ├── ecoli.fhir │ │ │ │ ├── neisseria.fhir │ │ │ │ ├── staphylococcus.fhir │ │ │ │ ├── glucoseHighNoInterpretation.fhir │ │ │ │ ├── glucoseHigh.fhir │ │ │ │ ├── glucoseLow.fhir │ │ │ │ ├── glucoseNormal.fhir │ │ │ │ ├── glucoseAbnormal.fhir │ │ │ │ ├── hemoglobin.fhir │ │ │ │ ├── glucoseConflictingInterpretation.fhir │ │ │ │ └── erythrocyte.fhir │ │ │ └── LoincTableCoreTiny.csv │ │ └── java │ │ │ └── fhir │ │ │ ├── ObservationWithInterpretationTest.java │ │ │ ├── ObservationWithCodedValueTest.java │ │ │ ├── ObservationWithValueRangeTest.java │ │ │ ├── Dstu3ObservationTest.java │ │ │ ├── FhirOutcomeCodeTest.java │ │ │ └── TestBase.java │ └── main │ │ └── java │ │ └── org │ │ └── monarchinitiative │ │ └── loinc2hpofhir │ │ ├── fhir2hpo │ │ ├── Uberobservation.java │ │ ├── FhirOutcomeCode.java │ │ ├── ObservationDtu3.java │ │ ├── ObservationR5.java │ │ └── ObservationR4.java │ │ └── Loinc2HpoFhir.java └── pom.xml ├── loinc2hpo-core ├── src │ ├── test │ │ ├── resources │ │ │ ├── log4j2.xml │ │ │ ├── loinc2hpoAnnotationTest.tsv │ │ │ └── LoincTableCoreTiny.csv │ │ └── java │ │ │ └── org │ │ │ └── monarchinitiative │ │ │ └── loinc2hpocore │ │ │ ├── annotation │ │ │ ├── LoincScaleTest.java │ │ │ └── Loinc2HpoAnnotationTest.java │ │ │ ├── io │ │ │ └── LoincTableCoreParserTest.java │ │ │ └── loinc │ │ │ ├── LoincIdTest.java │ │ │ └── LoincEntryTest.java │ └── main │ │ ├── java │ │ └── org │ │ │ └── monarchinitiative │ │ │ └── loinc2hpocore │ │ │ ├── exception │ │ │ ├── Loinc2HpoException.java │ │ │ └── Loinc2HpoRuntimeException.java │ │ │ ├── annotation │ │ │ ├── LoincAnnotation.java │ │ │ ├── Hpo2Outcome.java │ │ │ ├── LoincScale.java │ │ │ ├── NominalLoincAnnotation.java │ │ │ ├── OrdinalHpoAnnotation.java │ │ │ ├── QuantitativeLoincAnnotation.java │ │ │ └── Loinc2HpoAnnotation.java │ │ │ ├── Loinc2Hpo.java │ │ │ ├── codesystems │ │ │ ├── ShortCode.java │ │ │ └── Outcome.java │ │ │ ├── loinc │ │ │ ├── LoincId.java │ │ │ └── LoincEntry.java │ │ │ └── io │ │ │ ├── LoincTableCoreParser.java │ │ │ └── Loinc2HpoAnnotationParser.java │ │ └── resources │ │ └── log4j2.xml └── pom.xml ├── .github └── workflows │ └── maven-master-deploy.yaml ├── .readthedocs.yaml ├── loinc2hpo-cli ├── src │ └── main │ │ └── java │ │ └── org │ │ └── monarchinitiative │ │ └── loinc2hpocli │ │ ├── Main.java │ │ └── command │ │ ├── LoincTableCoreStatsCommand.java │ │ └── AnnotationQcCommand.java └── pom.xml ├── README.md ├── .gitignore ├── LICENSE └── CHANGELOG.md /docs/_static/loinc2hpo.css: -------------------------------------------------------------------------------- 1 | .wy-nav-content { 2 | max-width: 75% !important; 3 | } -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==4.2.0 2 | sphinx_rtd_theme==1.0.0 3 | readthedocs-sphinx-search==0.1.1 -------------------------------------------------------------------------------- /docs/images/mission.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monarch-initiative/loinc2hpo/HEAD/docs/images/mission.png -------------------------------------------------------------------------------- /docs/images/simulate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monarch-initiative/loinc2hpo/HEAD/docs/images/simulate.png -------------------------------------------------------------------------------- /docs/images/FHIR_intro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monarch-initiative/loinc2hpo/HEAD/docs/images/FHIR_intro.png -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/resources/json/malformedObservation.fhir: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Crazy", 3 | "id": "f004" 4 | } -------------------------------------------------------------------------------- /docs/images/FHIR_resource1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monarch-initiative/loinc2hpo/HEAD/docs/images/FHIR_resource1.png -------------------------------------------------------------------------------- /docs/images/FHIR_resource2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monarch-initiative/loinc2hpo/HEAD/docs/images/FHIR_resource2.png -------------------------------------------------------------------------------- /docs/images/FHIR_resource3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monarch-initiative/loinc2hpo/HEAD/docs/images/FHIR_resource3.png -------------------------------------------------------------------------------- /docs/images/loinc_examples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monarch-initiative/loinc2hpo/HEAD/docs/images/loinc_examples.png -------------------------------------------------------------------------------- /docs/images/annotation_scheme.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monarch-initiative/loinc2hpo/HEAD/docs/images/annotation_scheme.png -------------------------------------------------------------------------------- /docs/images/annotation_example1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monarch-initiative/loinc2hpo/HEAD/docs/images/annotation_example1.png -------------------------------------------------------------------------------- /docs/images/annotation_example2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monarch-initiative/loinc2hpo/HEAD/docs/images/annotation_example2.png -------------------------------------------------------------------------------- /docs/images/queryPatientFromServer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monarch-initiative/loinc2hpo/HEAD/docs/images/queryPatientFromServer.png -------------------------------------------------------------------------------- /docs/images/convertObservationsToHPO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monarch-initiative/loinc2hpo/HEAD/docs/images/convertObservationsToHPO.png -------------------------------------------------------------------------------- /docs/images/downloadObservationsFromServer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monarch-initiative/loinc2hpo/HEAD/docs/images/downloadObservationsFromServer.png -------------------------------------------------------------------------------- /loinc2hpo-core/src/test/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpocore/exception/Loinc2HpoException.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.exception; 2 | 3 | public class Loinc2HpoException extends Exception { 4 | 5 | public Loinc2HpoException() { super();} 6 | public Loinc2HpoException(String msg) { super(msg);} 7 | } 8 | -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/main/java/org/monarchinitiative/loinc2hpofhir/fhir2hpo/Uberobservation.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpofhir.fhir2hpo; 2 | 3 | import org.monarchinitiative.loinc2hpocore.codesystems.Outcome; 4 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 5 | 6 | import java.util.Optional; 7 | 8 | /** 9 | * This is a proxy for one of the versions of FHIR Observation (dtu3, r4, r5) 10 | */ 11 | public interface Uberobservation { 12 | 13 | Optional getLoincId(); 14 | 15 | Optional getOutcome(); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpocore/annotation/LoincAnnotation.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.annotation; 2 | 3 | import org.monarchinitiative.loinc2hpocore.codesystems.Outcome; 4 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 5 | 6 | import java.util.List; 7 | import java.util.Optional; 8 | 9 | public interface LoincAnnotation { 10 | 11 | LoincId getLoincId(); 12 | 13 | Optional getOutcome(Outcome outcome); 14 | 15 | List allAnnotations(); 16 | 17 | LoincScale scale(); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /loinc2hpo-core/src/test/java/org/monarchinitiative/loinc2hpocore/annotation/LoincScaleTest.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.annotation; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | 7 | public class LoincScaleTest { 8 | 9 | @Test 10 | public void testQn() { 11 | LoincScale scale = LoincScale.QUANTITATIVE; 12 | assertEquals("Qn", scale.shortName()); 13 | } 14 | 15 | @Test 16 | public void testOrdQn() { 17 | LoincScale scale = LoincScale.fromString("OrdQn"); 18 | assertEquals(LoincScale.OrdQn, scale); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /loinc2hpo-core/src/test/resources/loinc2hpoAnnotationTest.tsv: -------------------------------------------------------------------------------- 1 | loincId loincScale hpoLo hpoN hpoHi inversed note flag version createdOn createdBy lastEditedOn lastEditedBy 2823-3 Qn HP:0002900 HP:0011042 HP:0002153 true false 0.1 2018-03-20T15:34:05 JGM:Aaron NA NA 718-7 Qn NA HP:0011902 HP:0001900 true waiting for "Decreased hemoglobin" true 0.2 2018-03-20T15:39:40 JGM:Aaron 2018-03-20T15:40:29 JGM:Aaron 2160-0 Qn HP:0012101 HP:0012100 HP:0003259 true false 0.1 2018-03-20T15:41:16 JGM:Aaron NA NA -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | 2 | # You can set these variables from the command line. 3 | SPHINXOPTS = 4 | SPHINXBUILD = python -msphinx 5 | SPHINXPROJ = loinc2hpo 6 | SOURCEDIR = . 7 | BUILDDIR = _build 8 | html_static_path = ['_static'] 9 | 10 | 11 | 12 | # Put it first so that "make" without argument is like "make help". 13 | help: 14 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 15 | 16 | .PHONY: help Makefile 17 | 18 | # Catch-all target: route all unknown targets to Sphinx using the new 19 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 20 | %: Makefile 21 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 22 | -------------------------------------------------------------------------------- /.github/workflows/maven-master-deploy.yaml: -------------------------------------------------------------------------------- 1 | name: Build and deploy master branch 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - name: Set up Maven Central Repository 12 | uses: actions/setup-java@v3 13 | with: 14 | java-version: '17' 15 | distribution: 'adopt' 16 | server-id: ossrh-mi 17 | server-username: OSSRH_USERNAME_MI 18 | server-password: OSSRH_TOKEN_MI 19 | - name: Maven deploy package 20 | run: mvn --batch-mode deploy 21 | env: 22 | OSSRH_USERNAME_MI: ${{ secrets.OSSRH_USERNAME_MI }} 23 | OSSRH_TOKEN_MI: ${{ secrets.OSSRH_TOKEN_MI }} 24 | -------------------------------------------------------------------------------- /loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpocore/annotation/Hpo2Outcome.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.annotation; 2 | 3 | import org.monarchinitiative.loinc2hpocore.codesystems.Outcome; 4 | import org.monarchinitiative.loinc2hpocore.codesystems.ShortCode; 5 | import org.monarchinitiative.phenol.ontology.data.TermId; 6 | 7 | public class Hpo2Outcome { 8 | 9 | private final TermId hpoTermId; 10 | private final Outcome outcome; 11 | 12 | 13 | public Hpo2Outcome(TermId id, Outcome outcome) { 14 | this.hpoTermId =id; 15 | this.outcome = outcome; 16 | } 17 | 18 | 19 | public TermId getHpoId() {return hpoTermId; } 20 | 21 | public Outcome outcome() { return outcome; } 22 | 23 | public ShortCode shortCode() { return outcome.getCode(); } 24 | } 25 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | tools: 12 | python: "3.9" 13 | # You can also specify other tool versions: 14 | # nodejs: "16" 15 | # rust: "1.55" 16 | # golang: "1.17" 17 | 18 | # Build documentation in the docs/ directory with Sphinx 19 | sphinx: 20 | configuration: docs/conf.py 21 | 22 | # If using Sphinx, optionally build your docs in additional formats such as PDF 23 | # formats: 24 | # - pdf 25 | 26 | # Optionally declare the Python requirements required to build your docs 27 | python: 28 | install: 29 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /loinc2hpo-cli/src/main/java/org/monarchinitiative/loinc2hpocli/Main.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocli; 2 | 3 | import org.monarchinitiative.loinc2hpocli.command.AnnotationQcCommand; 4 | import org.monarchinitiative.loinc2hpocli.command.LoincTableCoreStatsCommand; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import picocli.CommandLine; 8 | 9 | 10 | @CommandLine.Command(name = "loinc2hpo-cli builder", version = "0.0.1", mixinStandardHelpOptions = true) 11 | public class Main implements Runnable { 12 | private static final Logger LOGGER = LoggerFactory.getLogger(Main.class); 13 | 14 | public static void main(String[] args) { 15 | if (args.length == 0) { 16 | // if the user doesn't pass any command or option, add -h to show help 17 | args = new String[]{"-h"}; 18 | } 19 | CommandLine cline = new CommandLine(new Main()) 20 | .addSubcommand("annotation-qc", new AnnotationQcCommand()) 21 | .addSubcommand("stats", new LoincTableCoreStatsCommand()); 22 | cline.setToggleBooleanFlags(false); 23 | int exitCode = cline.execute(args); 24 | System.exit(exitCode); 25 | } 26 | 27 | @Override 28 | public void run() { 29 | // work done in subcommands 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /loinc2hpo-core/src/test/java/org/monarchinitiative/loinc2hpocore/io/LoincTableCoreParserTest.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.io; 2 | 3 | import org.junit.jupiter.api.BeforeAll; 4 | import org.junit.jupiter.api.Test; 5 | import org.monarchinitiative.loinc2hpocore.exception.Loinc2HpoRuntimeException; 6 | import org.monarchinitiative.loinc2hpocore.loinc.LoincEntry; 7 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 8 | 9 | import java.net.URL; 10 | import java.util.Map; 11 | 12 | import static org.junit.jupiter.api.Assertions.assertEquals; 13 | 14 | public class LoincTableCoreParserTest { 15 | 16 | private static String LoincTableCoreTinyPath; 17 | 18 | @BeforeAll 19 | private static void init() { 20 | URL url = LoincTableCoreParserTest.class.getClassLoader().getResource("LoincTableCoreTiny.csv"); 21 | if (url==null) { 22 | throw new Loinc2HpoRuntimeException("Could not get path to \"LoincTableCoreTiny.csv\""); 23 | } 24 | LoincTableCoreTinyPath = url.getPath(); 25 | } 26 | 27 | /** 28 | * We expected 28 entries 29 | */ 30 | @Test 31 | void testParser() { 32 | Map entryMap = LoincTableCoreParser.load(LoincTableCoreTinyPath); 33 | assertEquals(28, entryMap.size()); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpocore/Loinc2Hpo.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore; 2 | 3 | 4 | import org.monarchinitiative.loinc2hpocore.annotation.LoincAnnotation; 5 | import org.monarchinitiative.loinc2hpocore.codesystems.Outcome; 6 | import org.monarchinitiative.loinc2hpocore.annotation.Hpo2Outcome; 7 | import org.monarchinitiative.loinc2hpocore.io.Loinc2HpoAnnotationParser; 8 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 9 | 10 | import java.util.Map; 11 | import java.util.Optional; 12 | 13 | /** 14 | * Entry point for the Loinc2Hpo tool 15 | * @author Aaron Zhang 16 | * @version 1.6.0 17 | */ 18 | public class Loinc2Hpo { 19 | private final Map loincToHpoAnnotationMap; 20 | 21 | public Loinc2Hpo(String path){ 22 | Loinc2HpoAnnotationParser parser = new Loinc2HpoAnnotationParser(path); 23 | loincToHpoAnnotationMap = parser.loincToHpoAnnotationMap(); 24 | } 25 | 26 | public Optional query(LoincId loincId, Outcome outcome) { 27 | if (! loincToHpoAnnotationMap.containsKey(loincId)) { 28 | return Optional.empty(); 29 | } else { 30 | LoincAnnotation annot = loincToHpoAnnotationMap.get(loincId); 31 | return annot.getOutcome(outcome); 32 | } 33 | } 34 | 35 | 36 | } 37 | -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/java/fhir/ObservationWithInterpretationTest.java: -------------------------------------------------------------------------------- 1 | package fhir; 2 | 3 | 4 | import org.hl7.fhir.dstu3.model.Observation; 5 | 6 | import org.junit.jupiter.api.Test; 7 | import org.monarchinitiative.loinc2hpocore.codesystems.Outcome; 8 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 9 | import org.monarchinitiative.loinc2hpofhir.fhir2hpo.ObservationDtu3; 10 | import org.monarchinitiative.loinc2hpofhir.fhir2hpo.Uberobservation; 11 | 12 | import java.util.Optional; 13 | 14 | import static org.junit.jupiter.api.Assertions.assertEquals; 15 | import static org.junit.jupiter.api.Assertions.assertTrue; 16 | 17 | 18 | public class ObservationWithInterpretationTest extends TestBase { 19 | 20 | 21 | /** Low erythrocytes with L interpretation. */ 22 | @Test 23 | public void testLowHemoglobinWithoutInterpretation() { 24 | Observation lowHb = lowHemoglobinObservation(); 25 | Uberobservation uberobservation = new ObservationDtu3(lowHb); 26 | LoincId erysInBlood = new LoincId("789-8"); 27 | Optional opt = uberobservation.getLoincId(); 28 | assertTrue(opt.isPresent()); 29 | assertEquals(erysInBlood, opt.get()); 30 | Optional outcomeOpt = uberobservation.getOutcome(); 31 | assertTrue(outcomeOpt.isPresent()); 32 | assertEquals(Outcome.LOW(), outcomeOpt.get()); 33 | } 34 | 35 | 36 | } -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/java/fhir/ObservationWithCodedValueTest.java: -------------------------------------------------------------------------------- 1 | package fhir; 2 | 3 | 4 | 5 | 6 | import org.hl7.fhir.dstu3.model.Observation; 7 | 8 | import org.junit.jupiter.api.Test; 9 | import org.monarchinitiative.loinc2hpocore.codesystems.Outcome; 10 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 11 | import org.monarchinitiative.loinc2hpofhir.fhir2hpo.ObservationDtu3; 12 | import org.monarchinitiative.loinc2hpofhir.fhir2hpo.Uberobservation; 13 | 14 | import java.util.Optional; 15 | 16 | import static org.junit.jupiter.api.Assertions.*; 17 | 18 | 19 | 20 | public class ObservationWithCodedValueTest extends TestBase { 21 | 22 | 23 | @Test 24 | public void testEcoliBloodCulture() { 25 | Observation ecoliObservation = ecoliNoInterpretationBloodCulture(); 26 | Uberobservation uberobservation = new ObservationDtu3(ecoliObservation); 27 | LoincId loincId = new LoincId("600-7"); 28 | Optional loincOpt = uberobservation.getLoincId(); 29 | assertTrue(loincOpt.isPresent()); 30 | assertEquals(loincId, loincOpt.get()); 31 | Optional outcomeOpt = uberobservation.getOutcome(); 32 | assertTrue(outcomeOpt.isPresent()); 33 | Outcome outcome = outcomeOpt.get(); 34 | assertTrue(outcome.isNominal()); 35 | assertEquals("112283007:http://snomed.info/sct:Escherichia coli", outcome.getOutcome()); 36 | } 37 | 38 | 39 | 40 | 41 | } 42 | -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/java/fhir/ObservationWithValueRangeTest.java: -------------------------------------------------------------------------------- 1 | package fhir; 2 | 3 | import org.hl7.fhir.dstu3.model.Observation; 4 | 5 | import org.junit.jupiter.api.Test; 6 | import org.monarchinitiative.loinc2hpocore.codesystems.Outcome; 7 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 8 | import org.monarchinitiative.loinc2hpofhir.fhir2hpo.ObservationDtu3; 9 | import org.monarchinitiative.loinc2hpofhir.fhir2hpo.Uberobservation; 10 | 11 | import java.util.Optional; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | import static org.junit.jupiter.api.Assertions.assertTrue; 15 | 16 | /** 17 | * Test using an Observation without a FHIR interpretation but with numerical values 18 | */ 19 | public class ObservationWithValueRangeTest extends TestBase { 20 | 21 | /** 22 | * This observation does not have an Interpretation, but we can infer the value to be high 23 | * based on the reference range. 24 | */ 25 | @Test 26 | public void highHbWithReferenceRange() { 27 | Observation observation = highHemoglobinWithValueRangeObservation(); 28 | Uberobservation uberobservation = new ObservationDtu3(observation); 29 | LoincId erysInBlood = new LoincId("789-8"); 30 | Optional opt = uberobservation.getLoincId(); 31 | assertTrue(opt.isPresent()); 32 | assertEquals(erysInBlood, opt.get()); 33 | Optional outcomeOpt = uberobservation.getOutcome(); 34 | assertTrue(outcomeOpt.isPresent()); 35 | assertEquals(Outcome.HIGH(), outcomeOpt.get()); 36 | } 37 | 38 | } -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/main/java/org/monarchinitiative/loinc2hpofhir/fhir2hpo/FhirOutcomeCode.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpofhir.fhir2hpo; 2 | 3 | import org.monarchinitiative.loinc2hpocore.codesystems.ShortCode; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | // TODO add other FHIR CODES 9 | public class FhirOutcomeCode { 10 | 11 | private static final Map fhir2shortcodeMap; 12 | 13 | static { 14 | fhir2shortcodeMap = new HashMap<>(); 15 | fhir2shortcodeMap.put("A", ShortCode.POS); // Abnormal, usually for non-numeric results 16 | fhir2shortcodeMap.put("AA", ShortCode.POS); 17 | fhir2shortcodeMap.put("L", ShortCode.L); 18 | fhir2shortcodeMap.put("LL", ShortCode.L); 19 | fhir2shortcodeMap.put("LU", ShortCode.L); 20 | fhir2shortcodeMap.put("H", ShortCode.H); 21 | fhir2shortcodeMap.put("HH", ShortCode.H); 22 | fhir2shortcodeMap.put("HU", ShortCode.H); 23 | fhir2shortcodeMap.put(">", ShortCode.H); 24 | fhir2shortcodeMap.put("<", ShortCode.L); // < is off-scale low 25 | fhir2shortcodeMap.put("N", ShortCode.N); 26 | fhir2shortcodeMap.put("NEG", ShortCode.NEG); 27 | fhir2shortcodeMap.put("ND", ShortCode.NEG); 28 | fhir2shortcodeMap.put("POS", ShortCode.POS); 29 | fhir2shortcodeMap.put("DET", ShortCode.POS); 30 | fhir2shortcodeMap.put("U", ShortCode.U); 31 | } 32 | 33 | public static ShortCode fhir2shortcode(String fhirOutcome) { 34 | return fhir2shortcodeMap.getOrDefault(fhirOutcome, ShortCode.U); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /loinc2hpo-core/src/test/java/org/monarchinitiative/loinc2hpocore/loinc/LoincIdTest.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.loinc; 2 | 3 | 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | import org.monarchinitiative.loinc2hpocore.exception.Loinc2HpoRuntimeException; 7 | 8 | import static org.junit.jupiter.api.Assertions.assertEquals; 9 | 10 | 11 | public class LoincIdTest { 12 | 13 | 14 | @Test 15 | public void testConstructor() { 16 | String code = "15074-8"; 17 | LoincId id = new LoincId(code); 18 | assertEquals(code,id.toString()); 19 | } 20 | 21 | 22 | @Test 23 | public void testConstructor2() { 24 | String code = "3141-9"; 25 | LoincId id = new LoincId(code); 26 | assertEquals(code,id.toString()); 27 | } 28 | 29 | @Test 30 | public void testBadCode() { 31 | Assertions.assertThrows(Loinc2HpoRuntimeException.class, () -> { 32 | String code = "15074-"; 33 | LoincId id = new LoincId(code); 34 | assertEquals(code,id.toString()); 35 | }); 36 | 37 | } 38 | 39 | @Test 40 | public void testBadCode2() { 41 | Assertions.assertThrows(Loinc2HpoRuntimeException.class, () -> { 42 | String code = "1507423"; 43 | LoincId id = new LoincId(code); 44 | assertEquals(code, id.toString()); 45 | }); 46 | } 47 | 48 | // test custom equals function 49 | @Test 50 | public void testEquals() { 51 | String code1="19048-8"; 52 | String code2="19048-8"; 53 | LoincId id1=new LoincId(code1); 54 | LoincId id2=new LoincId(code2); 55 | assertEquals(id1,id2); 56 | } 57 | 58 | 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/709c959bb0024403a667affaf2b9f476)](https://www.codacy.com/app/peter.robinson/loinc2hpo?utm_source=github.com&utm_medium=referral&utm_content=monarch-initiative/loinc2hpo&utm_campaign=Badge_Grade) 2 | [![Documentation Status](https://readthedocs.org/projects/loinc2hpo/badge/?version=latest)](https://loinc2hpo.readthedocs.io/en/latest/?badge=latest) 3 | 4 | 5 | # loinc2hpo 6 | A Java library to map tests results from [LOINC](https://loinc.org/) codes to 7 | [Human Phenotype Ontology.](https://hpo.jax.org/app/) terms. 8 | For details, please see [Zhang et al. (2012)](https://pubmed.ncbi.nlm.nih.gov/31119199/) Semantic integration of clinical laboratory tests from electronic 9 | health records for deep phenotyping and biomarker discovery. *NPJ Digit Med*. 2019;2:32. 10 | 11 | 12 | ## LOINC2HPO annotation 13 | The LOINC to HPO mapping file is available at the 14 | [loinc2hpoAnnotation](https://github.com/TheJacksonLaboratory/loinc2hpoAnnotation) repository. 15 | 16 | ## Using loinc2hpo 17 | loinc2hpo is intended to be used as a software library. Selected functions are demonstrated in the CLI app. 18 | 19 | ## documentation 20 | Please refer to http://loinc2hpo.readthedocs.io/en/latest/. 21 | 22 | ## spring framework app 23 | We are developing a separate app that will specialize in one functionality of this app - converting FHIR observations into HPO terms. The new app will be coded with the Spring framework and we strive to achieve enterprise-level quality. Please refer to the app with the following link: https://github.com/OCTRI/fhir2hpo 24 | 25 | ## funding 26 | We gratefully acknowledge funding by NCATS (CD2H project, A NATIONAL CENTER FOR DIGITAL HEALTH INFORMATICS INNOVATION), 1U24TR002306 27 | 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project-related - adjust as necessary 2 | data/ 3 | 4 | loinc2hpogui/src/main/java/org/monarchinitiative/loinc2hpo/gui/loinc2hpomain/org/ 5 | loinc2hpo-core/src/main/resources/hp.obo 6 | loinc2hpo-core/src/main/resources/catalog-v001.xml 7 | loinc2hpo-core/src/test/resources/hp.rdfformat 8 | 9 | 10 | OLD/src/main/java/ 11 | loinc2hpogui/dependency-reduced-pom.xml 12 | 13 | loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpo/testresult/ValueQuantity.java 14 | loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpo/testresult/Interpretation.java 15 | loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpo/fhir/INTERPRETATION.java 16 | 17 | loinc2hpogui/src/test/resources/split.sh 18 | loinc2hpogui/src/test/resources/UNC-loinc* 19 | loinc2hpogui/src/test/resources/UNC-data_loinc.txt 20 | loinc2hpo-core/src/test/org/monarchinitiative/loinc2hpo/util/TSTTest.java 21 | loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpo/util/TST.java 22 | 23 | 24 | 25 | loinc-hpoTerm4TestOutcome-mapping.tsv 26 | loinc2hpo-core/src/main/resources/hp.owl 27 | loinc2hpo-core/src/main/resources/data_UNC_annotated.csv 28 | loinc2hpo-core/src/main/resources/loinc_most_frequent.csv 29 | loinc2hpo-core/src/resource/ 30 | 31 | docs/build/* 32 | docs/images/figures.pptx 33 | # Maven 34 | target/ 35 | 36 | # IntelliJ stuff 37 | .idea/ 38 | *.iml 39 | 40 | # Compiled class file 41 | *.class 42 | 43 | # Log file 44 | #*.log 45 | logs/ 46 | 47 | # BlueJ files 48 | *.ctxt 49 | 50 | # Mobile Tools for Java (J2ME) 51 | .mtj.tmp/ 52 | 53 | # Package Files # 54 | *.jar 55 | *.war 56 | *.ear 57 | *.zip 58 | *.tar.gz 59 | *.rar 60 | 61 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 62 | hs_err_pid* 63 | 64 | # Mac junk :) 65 | .DS_Store 66 | /loinc2hpo-core/hp.rdf 67 | /loinc2hpo-core/letters.txt 68 | docs/_build/ 69 | -------------------------------------------------------------------------------- /loinc2hpo-cli/src/main/java/org/monarchinitiative/loinc2hpocli/command/LoincTableCoreStatsCommand.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocli.command; 2 | 3 | 4 | import org.monarchinitiative.loinc2hpocore.annotation.LoincScale; 5 | import org.monarchinitiative.loinc2hpocore.io.LoincTableCoreParser; 6 | import org.monarchinitiative.loinc2hpocore.loinc.LoincEntry; 7 | import picocli.CommandLine; 8 | 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | import static java.util.stream.Collectors.groupingBy; 13 | 14 | @CommandLine.Command(name = "stats", aliases = {"S"}, 15 | mixinStandardHelpOptions = true, 16 | description = "Q/C and stats for the LoincTableCore.csv file") 17 | public class LoincTableCoreStatsCommand implements Runnable{ 18 | @CommandLine.Option(names = {"-l", "--loinc"}, 19 | description = "Path to the loinc2hpo-annotation.tsv file", 20 | required = true) 21 | private String loincTableCorePath; 22 | 23 | public LoincTableCoreStatsCommand(){ 24 | } 25 | 26 | @Override 27 | public void run() { 28 | LoincTableCoreParser parser = new LoincTableCoreParser(loincTableCorePath); 29 | var loincEntryMap = parser.getLoincEntries(); 30 | System.out.printf("%d well formed entries.\n", loincEntryMap.size()); 31 | System.out.printf("%d non-(Qn,Ord,Nom) scale entries (removed).\n", parser.getInvalidScale()); 32 | System.out.printf("%d malformed entries.\n", parser.getMalformed()); 33 | Map> entriesByScale = loincEntryMap.values().stream() 34 | .collect(groupingBy(LoincEntry::getScale)); 35 | int total = loincEntryMap.size(); 36 | for (var e : entriesByScale.entrySet()) { 37 | int size = e.getValue().size(); 38 | String perc = String.format("%.1f%%", 100.0*size/total); 39 | System.out.printf("%s: %d entries (%s)\n", e.getKey(), size, perc); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/resources/json/glucoseNoInterpretationNoReference.fhir: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "f001", 4 | "text": { 5 | "status": "generated", 6 | "div": "

Generated Narrative with Details

id: f001

identifier: 6323 (OFFICIAL)

status: final

code: Glucose [Moles/volume] in Blood (Details : {LOINC code '15074-8' = 'Glucose [Moles/volume] in Blood', given as 'Glucose [Moles/volume] in Blood'})

subject: P. van de Heuvel

effective: 02/04/2013 9:30:10 AM --> (ongoing)

issued: 03/04/2013 3:30:10 PM

performer: A. Langeveld

value: 6.3 mmol/l (Details: UCUM code mmol/L = 'mmol/L')

interpretation: High (Details : {http://hl7.org/fhir/v2/0078 code 'H' = 'High', given as 'High'})

ReferenceRanges

-LowHigh
*3.1 mmol/l (Details: UCUM code mmol/L = 'mmol/L')6.2 mmol/l (Details: UCUM code mmol/L = 'mmol/L')
" 7 | }, 8 | "identifier": [ 9 | { 10 | "use": "official", 11 | "system": "http://www.bmc.nl/zorgportal/identifiers/observations", 12 | "value": "6323" 13 | } 14 | ], 15 | "status": "final", 16 | "code": { 17 | "coding": [ 18 | { 19 | "system": "http://loinc.org", 20 | "code": "15074-8", 21 | "display": "Glucose [Moles/volume] in Blood" 22 | } 23 | ] 24 | }, 25 | "subject": { 26 | "reference": "Patient/f001", 27 | "display": "P. van de Heuvel" 28 | }, 29 | "effectivePeriod": { 30 | "start": "2013-04-02T09:30:10+01:00" 31 | }, 32 | "issued": "2013-04-03T15:30:10+01:00", 33 | "performer": [ 34 | { 35 | "reference": "Practitioner/f005", 36 | "display": "A. Langeveld" 37 | } 38 | ], 39 | "valueQuantity": { 40 | "value": 2.3, 41 | "unit": "mmol/l", 42 | "system": "http://unitsofmeasure.org", 43 | "code": "mmol/L" 44 | } 45 | } -------------------------------------------------------------------------------- /loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpocore/codesystems/ShortCode.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.codesystems; 2 | 3 | import org.monarchinitiative.loinc2hpocore.exception.Loinc2HpoRuntimeException; 4 | 5 | /** 6 | * These are the code used for the results of lab tests. Other codes, e.g., FHIR, should be 7 | * mapped onto these codes for annotation of data. 8 | */ 9 | public enum ShortCode { 10 | L("below normal range"), 11 | N("within normal range"), 12 | H("above normal range"), 13 | NEG("negative"), 14 | POS("positive"), 15 | NOM("nominal"), 16 | U("unknown code"); 17 | 18 | 19 | 20 | private final String name; 21 | 22 | ShortCode(String label) { 23 | this.name = label; 24 | } 25 | 26 | 27 | public static ShortCode fromShortCode(String codeString) throws Loinc2HpoRuntimeException { 28 | if (codeString == null || codeString.isEmpty()) { 29 | return null; 30 | } 31 | if (codeString.equals("L")) { 32 | return L; 33 | } 34 | if (codeString.equals("N")) { 35 | return N; 36 | } 37 | if (codeString.equals("H")) { 38 | return H; 39 | } 40 | if (codeString.equals("NEG")) { 41 | return NEG; 42 | } 43 | if (codeString.equals("POS")) { 44 | return POS; 45 | } 46 | if (codeString.equals("NOM")) { 47 | return NOM; 48 | } 49 | if (codeString.equals("U")) { 50 | return U; 51 | } 52 | throw Loinc2HpoRuntimeException.unrecognizedCode(codeString); 53 | } 54 | 55 | public String shortForm() { 56 | switch (this) { 57 | case L: return "L"; 58 | case H: return "H"; 59 | case N: return "N"; 60 | case NOM: return "NOM"; 61 | case POS: return "POS"; 62 | case NEG: return "NEG"; 63 | case U: return "U"; 64 | } 65 | // needed by compiler, will never happen unless a new constant is added 66 | throw new Loinc2HpoRuntimeException("Could not find short form"); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpocore/annotation/LoincScale.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.annotation; 2 | 3 | import org.monarchinitiative.loinc2hpocore.exception.Loinc2HpoRuntimeException; 4 | 5 | /** 6 | * Scales of measurement used by LOINC codes. For this library, we only use Qn, Ord, and Nom. 7 | * @author Aaron Zhang, Peter Robinson 8 | */ 9 | public enum LoincScale { 10 | 11 | QUANTITATIVE("Qn"), 12 | ORDINAL("Ord"), 13 | NOMINAL("Nom"), 14 | /** Test can be reported as either ORD or QN */ 15 | OrdQn("OrdQn"), 16 | /** Narrative */ 17 | Nar("Nar"), 18 | /** multi-valued */ 19 | Multi("Multi"), 20 | Doc("Doc"), 21 | Set("Set"), 22 | /** some loinc entries such as 70871-9 do not have a scale and just show a dash. */ 23 | Dash("-"), 24 | /** rare entries have an asterisk. */ 25 | Asterisk("*"), 26 | Unknown("Unknown"); 27 | 28 | private final String name; 29 | 30 | LoincScale(String label) { 31 | this.name = label; 32 | } 33 | 34 | public String shortName() { return this.name; } 35 | 36 | public static LoincScale fromString(String scale) { 37 | switch (scale) { 38 | case "Qn": return QUANTITATIVE; 39 | case "Ord": return ORDINAL; 40 | case "Nom": return NOMINAL; 41 | case "OrdQn": return OrdQn; 42 | case "Nar": return Nar; 43 | case "Multi": return Multi; 44 | case "Doc": return Doc; 45 | case "Set": return Set; 46 | case "-": return Dash; 47 | case "*": return Asterisk; 48 | case "Unknown": return Unknown; 49 | default: 50 | throw new Loinc2HpoRuntimeException("MalformedScale: \"" + scale + "\"."); 51 | } 52 | } 53 | 54 | /** 55 | * We are only using Qn, Ord, and Nom scale LOINC codes. We will disregard other codes 56 | * @return true if this is a scale type (Qn, Ord, Nom) that is usable for LOINC2HPO 57 | */ 58 | public boolean validForLoinc2Hpo() { 59 | return this.equals(QUANTITATIVE) || this.equals(ORDINAL) || this.equals(NOMINAL); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpocore/annotation/NominalLoincAnnotation.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.annotation; 2 | 3 | import org.monarchinitiative.loinc2hpocore.codesystems.Outcome; 4 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 5 | import org.monarchinitiative.phenol.ontology.data.TermId; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Optional; 11 | 12 | public class NominalLoincAnnotation implements LoincAnnotation { 13 | 14 | private final LoincId loincId; 15 | private final Map nominalAnnotations; 16 | 17 | public NominalLoincAnnotation(Map annotations) { 18 | this.nominalAnnotations = annotations; 19 | // The assumption is that annotations is not empty and that 20 | // each entry has the same LOINC id. This assumption will always 21 | // hold if the logic of the code is correct and the input data is correct 22 | Loinc2HpoAnnotation annot = annotations.values().iterator().next(); 23 | this.loincId = annot.getLoincId(); 24 | } 25 | 26 | 27 | @Override 28 | public LoincId getLoincId() { 29 | return this.loincId; 30 | } 31 | 32 | @Override 33 | public Optional getOutcome(Outcome outcome) { 34 | if (nominalAnnotations.containsKey(outcome)) { 35 | TermId hpoId = nominalAnnotations.get(outcome).getHpoTermId(); 36 | return Optional.of(new Hpo2Outcome(hpoId, outcome)); 37 | } else { 38 | return Optional.empty(); 39 | } 40 | } 41 | 42 | @Override 43 | public List allAnnotations() { 44 | return new ArrayList<>(this.nominalAnnotations.values()); 45 | } 46 | 47 | @Override 48 | public LoincScale scale() { 49 | return LoincScale.NOMINAL; 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | StringBuilder sb = new StringBuilder(); 55 | sb.append(loincId).append("\n"); 56 | for (var e : nominalAnnotations.entrySet()) { 57 | sb.append("\t").append(e.getKey()).append(": ").append(e.getValue()).append("\n"); 58 | } 59 | return sb.toString(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/resources/json/ecoliNoInterpretation.fhir: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "f206", 4 | "text": { 5 | "status": "generated", 6 | "div": "

Generated Narrative with Details

id: f206

status: final

code: Blood culture (Details : {http://acmelabs.org code '104177' = '104177', given as 'Blood culture'}; {LOINC code '600-7' = 'Bacteria identified in Blood by Culture', given as 'Bacteria identified in Blood by Culture'})

subject: Roel

issued: 11/03/2013 10:28:00 AM

performer: Luigi Maas

value: Staphylococcus aureus (Details : {SNOMED CT code '3092008' = 'Staphylococcus aureus', given as 'Staphylococcus aureus'})

interpretation: Positive (Details : {http://hl7.org/fhir/v2/0078 code 'POS' = 'Positive)

method: Blood culture for bacteria, including anaerobic screen (Details : {SNOMED CT code '104177005' = 'Blood culture for bacteria, including anaerobic screen', given as 'Blood culture for bacteria, including anaerobic screen'})

" 7 | }, 8 | "status": "final", 9 | "code": { 10 | "coding": [ 11 | { 12 | "system": "http://acmelabs.org", 13 | "code": "104177", 14 | "display": "Blood culture" 15 | }, 16 | { 17 | "system": "http://loinc.org", 18 | "code": "600-7", 19 | "display": "Bacteria identified in Blood by Culture" 20 | } 21 | ] 22 | }, 23 | "subject": { 24 | "reference": "Patient/f201", 25 | "display": "Roel" 26 | }, 27 | "issued": "2013-03-11T10:28:00+01:00", 28 | "performer": [ 29 | { 30 | "reference": "Practitioner/f202", 31 | "display": "Luigi Maas" 32 | } 33 | ], 34 | "valueCodeableConcept": { 35 | "coding": [ 36 | { 37 | "system": "http://snomed.info/sct", 38 | "code": "112283007", 39 | "display": "Escherichia coli" 40 | } 41 | ] 42 | }, 43 | "method": { 44 | "coding": [ 45 | { 46 | "system": "http://snomed.info/sct", 47 | "code": "104177005", 48 | "display": "Blood culture for bacteria, including anaerobic screen" 49 | } 50 | ] 51 | } 52 | } -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/resources/json/neisseriaNoInterpretation.fhir: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "f206", 4 | "text": { 5 | "status": "generated", 6 | "div": "

Generated Narrative with Details

id: f206

status: final

code: Blood culture (Details : {http://acmelabs.org code '104177' = '104177', given as 'Blood culture'}; {LOINC code '600-7' = 'Bacteria identified in Blood by Culture', given as 'Bacteria identified in Blood by Culture'})

subject: Roel

issued: 11/03/2013 10:28:00 AM

performer: Luigi Maas

value: Staphylococcus aureus (Details : {SNOMED CT code '3092008' = 'Staphylococcus aureus', given as 'Staphylococcus aureus'})

interpretation: Positive (Details : {http://hl7.org/fhir/v2/0078 code 'POS' = 'Positive)

method: Blood culture for bacteria, including anaerobic screen (Details : {SNOMED CT code '104177005' = 'Blood culture for bacteria, including anaerobic screen', given as 'Blood culture for bacteria, including anaerobic screen'})

" 7 | }, 8 | "status": "final", 9 | "code": { 10 | "coding": [ 11 | { 12 | "system": "http://acmelabs.org", 13 | "code": "104177", 14 | "display": "Blood culture" 15 | }, 16 | { 17 | "system": "http://loinc.org", 18 | "code": "600-7", 19 | "display": "Bacteria identified in Blood by Culture" 20 | } 21 | ] 22 | }, 23 | "subject": { 24 | "reference": "Patient/f201", 25 | "display": "Roel" 26 | }, 27 | "issued": "2013-03-11T10:28:00+01:00", 28 | "performer": [ 29 | { 30 | "reference": "Practitioner/f202", 31 | "display": "Luigi Maas" 32 | } 33 | ], 34 | "valueCodeableConcept": { 35 | "coding": [ 36 | { 37 | "system": "http://snomed.info/sct", 38 | "code": "59083001", 39 | "display": "Neisseria spp" 40 | } 41 | ] 42 | }, 43 | "method": { 44 | "coding": [ 45 | { 46 | "system": "http://snomed.info/sct", 47 | "code": "104177005", 48 | "display": "Blood culture for bacteria, including anaerobic screen" 49 | } 50 | ] 51 | } 52 | } -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/resources/json/staphylococcusNoInterpretation.fhir: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "f206", 4 | "text": { 5 | "status": "generated", 6 | "div": "

Generated Narrative with Details

id: f206

status: final

code: Blood culture (Details : {http://acmelabs.org code '104177' = '104177', given as 'Blood culture'}; {LOINC code '600-7' = 'Bacteria identified in Blood by Culture', given as 'Bacteria identified in Blood by Culture'})

subject: Roel

issued: 11/03/2013 10:28:00 AM

performer: Luigi Maas

value: Staphylococcus aureus (Details : {SNOMED CT code '3092008' = 'Staphylococcus aureus', given as 'Staphylococcus aureus'})

interpretation: Positive (Details : {http://hl7.org/fhir/v2/0078 code 'POS' = 'Positive)

method: Blood culture for bacteria, including anaerobic screen (Details : {SNOMED CT code '104177005' = 'Blood culture for bacteria, including anaerobic screen', given as 'Blood culture for bacteria, including anaerobic screen'})

" 7 | }, 8 | "status": "final", 9 | "code": { 10 | "coding": [ 11 | { 12 | "system": "http://acmelabs.org", 13 | "code": "104177", 14 | "display": "Blood culture" 15 | }, 16 | { 17 | "system": "http://loinc.org", 18 | "code": "600-7", 19 | "display": "Bacteria identified in Blood by Culture" 20 | } 21 | ] 22 | }, 23 | "subject": { 24 | "reference": "Patient/f201", 25 | "display": "Roel" 26 | }, 27 | "issued": "2013-03-11T10:28:00+01:00", 28 | "performer": [ 29 | { 30 | "reference": "Practitioner/f202", 31 | "display": "Luigi Maas" 32 | } 33 | ], 34 | "valueCodeableConcept": { 35 | "coding": [ 36 | { 37 | "system": "http://snomed.info/sct", 38 | "code": "3092008", 39 | "display": "Staphylococcus aureus" 40 | } 41 | ] 42 | }, 43 | "method": { 44 | "coding": [ 45 | { 46 | "system": "http://snomed.info/sct", 47 | "code": "104177005", 48 | "display": "Blood culture for bacteria, including anaerobic screen" 49 | } 50 | ] 51 | } 52 | } -------------------------------------------------------------------------------- /loinc2hpo-core/src/main/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | [%t] %-5level (%F:%L) - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/main/java/org/monarchinitiative/loinc2hpofhir/Loinc2HpoFhir.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpofhir; 2 | 3 | 4 | import org.monarchinitiative.loinc2hpocore.Loinc2Hpo; 5 | import org.monarchinitiative.loinc2hpocore.annotation.Hpo2Outcome; 6 | import org.monarchinitiative.loinc2hpocore.codesystems.Outcome; 7 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 8 | import org.monarchinitiative.loinc2hpofhir.fhir2hpo.ObservationDtu3; 9 | import org.monarchinitiative.loinc2hpofhir.fhir2hpo.ObservationR4; 10 | import org.monarchinitiative.loinc2hpofhir.fhir2hpo.ObservationR5; 11 | import org.monarchinitiative.loinc2hpofhir.fhir2hpo.Uberobservation; 12 | 13 | import java.util.Optional; 14 | 15 | /** 16 | * Retrieve LOINC2HPO results for FHIR versions dstu3, r4, and r5, using a proxy pattern. 17 | * We use HAPI FHIR to to get a {@link LoincId} and an {@link Outcome} from the FHIR Observation 18 | * and then use the {@link Loinc2Hpo} object to transform that into an HPO code. The method returns 19 | * an Optional which is empty if any of the steps fails. 20 | * @author Peter Robinson 21 | */ 22 | public class Loinc2HpoFhir { 23 | private final Loinc2Hpo loinc2Hpo; 24 | 25 | public Loinc2HpoFhir(String path) { 26 | this.loinc2Hpo = new Loinc2Hpo(path); 27 | } 28 | 29 | Optional dstu3(org.hl7.fhir.dstu3.model.Observation dstu3Observation) { 30 | Uberobservation observation = new ObservationDtu3(dstu3Observation); 31 | return query(observation); 32 | } 33 | 34 | Optional r4(org.hl7.fhir.r4.model.Observation r4Observation) { 35 | Uberobservation observation = new ObservationR4(r4Observation); 36 | return query(observation); 37 | } 38 | 39 | Optional r5(org.hl7.fhir.r5.model.Observation r5Observation) { 40 | Uberobservation observation = new ObservationR5(r5Observation); 41 | return query(observation); 42 | } 43 | 44 | 45 | Optional query(Uberobservation uberobservation) { 46 | Optional loincOpt = uberobservation.getLoincId(); 47 | if (loincOpt.isEmpty()) return Optional.empty(); 48 | Optional outcomeOpt = uberobservation.getOutcome(); 49 | if (outcomeOpt.isEmpty()) return Optional.empty(); 50 | return loinc2Hpo.query(loincOpt.get(), outcomeOpt.get()); 51 | } 52 | 53 | 54 | 55 | 56 | 57 | } 58 | -------------------------------------------------------------------------------- /loinc2hpo-core/src/test/java/org/monarchinitiative/loinc2hpocore/annotation/Loinc2HpoAnnotationTest.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.annotation; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.List; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertNotNull; 9 | import static org.monarchinitiative.loinc2hpocore.annotation.Loinc2HpoAnnotation.outcomes2LoincAnnotation; 10 | 11 | public class Loinc2HpoAnnotationTest { 12 | 13 | private static Loinc2HpoAnnotation fromStringList(String [] fields) { 14 | String line = String.join("\t",fields); 15 | return Loinc2HpoAnnotation.fromAnnotationLine(line); 16 | } 17 | 18 | private static final String [] fields1 = {"6047-5", "Qn", "N", "HP:0410235", ".", "OHSU:JP[2018-10-10]", "."}; 19 | private static final String [] fields2 = {"6047-5", "Qn", "H", "HP:0410235", ".", "OHSU:JP[2018-10-10]", "."}; 20 | 21 | private final static Loinc2HpoAnnotation normalAnnot = fromStringList(fields1); 22 | private final static Loinc2HpoAnnotation highAnnot = fromStringList(fields2); 23 | 24 | 25 | 26 | @Test 27 | void testNormalAbnormalPair() { 28 | List outcomelist= List.of(normalAnnot, highAnnot); 29 | assertEquals(2, outcomelist.size()); 30 | LoincAnnotation loincAnnotation = outcomes2LoincAnnotation(outcomelist); 31 | assertNotNull(loincAnnotation); 32 | } 33 | 34 | @Test 35 | void testCreationOfTsv1() { 36 | String expectedTsvLine = String.join("\t", fields1); 37 | assertEquals(expectedTsvLine, normalAnnot.toTsv()); 38 | } 39 | 40 | @Test 41 | void testCreationOfTsv2() { 42 | String expectedTsvLine = String.join("\t", fields2); 43 | assertEquals(expectedTsvLine, highAnnot.toTsv()); 44 | } 45 | 46 | /** 47 | * This checks that the short form of the outcode code 48 | * ({@link org.monarchinitiative.loinc2hpocore.codesystems.ShortCode}) is use for output. 49 | */ 50 | @Test 51 | void checkExportOfOrdNeg() { 52 | String [] fields = {"4622-7", "Ord", "NEG", "HP:0011902", ".", "HPO:nvasilevsky[2019-06-14]", "."}; 53 | Loinc2HpoAnnotation negAnnot = fromStringList(fields); 54 | String expectedTsvLine = String.join("\t", fields); 55 | assertEquals(expectedTsvLine, negAnnot.toTsv()); 56 | } 57 | 58 | 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/resources/json/ecoli.fhir: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "f206", 4 | "text": { 5 | "status": "generated", 6 | "div": "

Generated Narrative with Details

id: f206

status: final

code: Blood culture (Details : {http://acmelabs.org code '104177' = '104177', given as 'Blood culture'}; {LOINC code '600-7' = 'Bacteria identified in Blood by Culture', given as 'Bacteria identified in Blood by Culture'})

subject: Roel

issued: 11/03/2013 10:28:00 AM

performer: Luigi Maas

value: Staphylococcus aureus (Details : {SNOMED CT code '3092008' = 'Staphylococcus aureus', given as 'Staphylococcus aureus'})

interpretation: Positive (Details : {http://hl7.org/fhir/v2/0078 code 'POS' = 'Positive)

method: Blood culture for bacteria, including anaerobic screen (Details : {SNOMED CT code '104177005' = 'Blood culture for bacteria, including anaerobic screen', given as 'Blood culture for bacteria, including anaerobic screen'})

" 7 | }, 8 | "status": "final", 9 | "code": { 10 | "coding": [ 11 | { 12 | "system": "http://acmelabs.org", 13 | "code": "104177", 14 | "display": "Blood culture" 15 | }, 16 | { 17 | "system": "http://loinc.org", 18 | "code": "600-7", 19 | "display": "Bacteria identified in Blood by Culture" 20 | } 21 | ] 22 | }, 23 | "subject": { 24 | "reference": "Patient/f201", 25 | "display": "Roel" 26 | }, 27 | "issued": "2013-03-11T10:28:00+01:00", 28 | "performer": [ 29 | { 30 | "reference": "Practitioner/f202", 31 | "display": "Luigi Maas" 32 | } 33 | ], 34 | "valueCodeableConcept": { 35 | "coding": [ 36 | { 37 | "system": "http://snomed.info/sct", 38 | "code": "112283007", 39 | "display": "Escherichia coli" 40 | } 41 | ] 42 | }, 43 | "interpretation": { 44 | "coding": [ 45 | { 46 | "system": "http://hl7.org/fhir/v2/0078", 47 | "code": "POS" 48 | } 49 | ] 50 | }, 51 | "method": { 52 | "coding": [ 53 | { 54 | "system": "http://snomed.info/sct", 55 | "code": "104177005", 56 | "display": "Blood culture for bacteria, including anaerobic screen" 57 | } 58 | ] 59 | } 60 | } -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/resources/json/neisseria.fhir: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "f206", 4 | "text": { 5 | "status": "generated", 6 | "div": "

Generated Narrative with Details

id: f206

status: final

code: Blood culture (Details : {http://acmelabs.org code '104177' = '104177', given as 'Blood culture'}; {LOINC code '600-7' = 'Bacteria identified in Blood by Culture', given as 'Bacteria identified in Blood by Culture'})

subject: Roel

issued: 11/03/2013 10:28:00 AM

performer: Luigi Maas

value: Staphylococcus aureus (Details : {SNOMED CT code '3092008' = 'Staphylococcus aureus', given as 'Staphylococcus aureus'})

interpretation: Positive (Details : {http://hl7.org/fhir/v2/0078 code 'POS' = 'Positive)

method: Blood culture for bacteria, including anaerobic screen (Details : {SNOMED CT code '104177005' = 'Blood culture for bacteria, including anaerobic screen', given as 'Blood culture for bacteria, including anaerobic screen'})

" 7 | }, 8 | "status": "final", 9 | "code": { 10 | "coding": [ 11 | { 12 | "system": "http://acmelabs.org", 13 | "code": "104177", 14 | "display": "Blood culture" 15 | }, 16 | { 17 | "system": "http://loinc.org", 18 | "code": "600-7", 19 | "display": "Bacteria identified in Blood by Culture" 20 | } 21 | ] 22 | }, 23 | "subject": { 24 | "reference": "Patient/f201", 25 | "display": "Roel" 26 | }, 27 | "issued": "2013-03-11T10:28:00+01:00", 28 | "performer": [ 29 | { 30 | "reference": "Practitioner/f202", 31 | "display": "Luigi Maas" 32 | } 33 | ], 34 | "valueCodeableConcept": { 35 | "coding": [ 36 | { 37 | "system": "http://snomed.info/sct", 38 | "code": "59083001", 39 | "display": "Neisseria spp" 40 | } 41 | ] 42 | }, 43 | "interpretation": { 44 | "coding": [ 45 | { 46 | "system": "http://hl7.org/fhir/v2/0078", 47 | "code": "POS" 48 | } 49 | ] 50 | }, 51 | "method": { 52 | "coding": [ 53 | { 54 | "system": "http://snomed.info/sct", 55 | "code": "104177005", 56 | "display": "Blood culture for bacteria, including anaerobic screen" 57 | } 58 | ] 59 | } 60 | } -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/resources/json/staphylococcus.fhir: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "f206", 4 | "text": { 5 | "status": "generated", 6 | "div": "

Generated Narrative with Details

id: f206

status: final

code: Blood culture (Details : {http://acmelabs.org code '104177' = '104177', given as 'Blood culture'}; {LOINC code '600-7' = 'Bacteria identified in Blood by Culture', given as 'Bacteria identified in Blood by Culture'})

subject: Roel

issued: 11/03/2013 10:28:00 AM

performer: Luigi Maas

value: Staphylococcus aureus (Details : {SNOMED CT code '3092008' = 'Staphylococcus aureus', given as 'Staphylococcus aureus'})

interpretation: Positive (Details : {http://hl7.org/fhir/v2/0078 code 'POS' = 'Positive)

method: Blood culture for bacteria, including anaerobic screen (Details : {SNOMED CT code '104177005' = 'Blood culture for bacteria, including anaerobic screen', given as 'Blood culture for bacteria, including anaerobic screen'})

" 7 | }, 8 | "status": "final", 9 | "code": { 10 | "coding": [ 11 | { 12 | "system": "http://acmelabs.org", 13 | "code": "104177", 14 | "display": "Blood culture" 15 | }, 16 | { 17 | "system": "http://loinc.org", 18 | "code": "600-7", 19 | "display": "Bacteria identified in Blood by Culture" 20 | } 21 | ] 22 | }, 23 | "subject": { 24 | "reference": "Patient/f201", 25 | "display": "Roel" 26 | }, 27 | "issued": "2013-03-11T10:28:00+01:00", 28 | "performer": [ 29 | { 30 | "reference": "Practitioner/f202", 31 | "display": "Luigi Maas" 32 | } 33 | ], 34 | "valueCodeableConcept": { 35 | "coding": [ 36 | { 37 | "system": "http://snomed.info/sct", 38 | "code": "3092008", 39 | "display": "Staphylococcus aureus" 40 | } 41 | ] 42 | }, 43 | "interpretation": { 44 | "coding": [ 45 | { 46 | "system": "http://hl7.org/fhir/v2/0078", 47 | "code": "POS" 48 | } 49 | ] 50 | }, 51 | "method": { 52 | "coding": [ 53 | { 54 | "system": "http://snomed.info/sct", 55 | "code": "104177005", 56 | "display": "Blood culture for bacteria, including anaerobic screen" 57 | } 58 | ] 59 | } 60 | } -------------------------------------------------------------------------------- /loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpocore/loinc/LoincId.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.loinc; 2 | 3 | import com.fasterxml.jackson.annotation.JsonValue; 4 | 5 | import org.monarchinitiative.loinc2hpocore.exception.Loinc2HpoRuntimeException; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.util.Objects; 10 | 11 | public class LoincId implements Comparable { 12 | private static final Logger LOGGER = LoggerFactory.getLogger(LoincId.class); 13 | /** The part of the Loinc code prior to the dash */ 14 | private final int num; 15 | /** The part of the Loinc code following the dash */ 16 | private final int suffix; 17 | 18 | public LoincId(String loinccode) { 19 | this(loinccode, false); 20 | } 21 | 22 | public LoincId(String loinccode, boolean hasPrefix) { 23 | if (hasPrefix){ 24 | loinccode = loinccode.split(":")[1]; 25 | } 26 | int dash_pos=loinccode.indexOf("-"); 27 | if (dash_pos<=0) throw Loinc2HpoRuntimeException.malformedLoincCode("No dash found in "+loinccode); 28 | if (dash_pos >loinccode.length()-2) 29 | throw Loinc2HpoRuntimeException.malformedLoincCode("No character found after dash in " + loinccode); 30 | try { 31 | num=Integer.parseInt(loinccode.substring(0,dash_pos)); 32 | suffix=Integer.parseInt(loinccode.substring(dash_pos+1)); 33 | } catch (NumberFormatException nfe) { 34 | throw Loinc2HpoRuntimeException.malformedLoincCode("Unable to parse numerical part of "+ loinccode); 35 | } 36 | } 37 | 38 | 39 | 40 | @Override 41 | public boolean equals(Object o) { 42 | if (o == this) return true; 43 | if (!(o instanceof LoincId)) { 44 | return false; 45 | } 46 | LoincId other = (LoincId) o; 47 | return (this.num==other.num && this.suffix==other.suffix); 48 | } 49 | 50 | 51 | @Override 52 | public int hashCode() { 53 | return Objects.hash(num, suffix); 54 | } 55 | 56 | @Override 57 | @JsonValue 58 | public String toString() { return String.format("%d-%d",num,suffix); } 59 | 60 | 61 | @Override 62 | public int compareTo(LoincId that) { 63 | if (this.num != that.num) { 64 | return Integer.compare(this.num, that.num); 65 | } else { 66 | return Integer.compare(this.suffix, that.suffix); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /loinc2hpo-cli/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | loinc2hpo 7 | org.monarchinitiative 8 | 2.0.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | loinc2hpo-cli 13 | jar 14 | 15 | Loinc2Hpo CLI 16 | Command line interface to loinc2hpo 17 | 18 | 19 | UTF-8 20 | 21 | 22 | 23 | 24 | 25 | info.picocli 26 | picocli 27 | 4.6.2 28 | 29 | 30 | org.monarchinitiative 31 | loinc2hpo-core 32 | ${project.parent.version} 33 | 34 | 35 | org.monarchinitiative.phenol 36 | phenol-core 37 | 38 | 39 | org.monarchinitiative.phenol 40 | phenol-io 41 | 42 | 43 | 44 | 45 | loinc2hpo-cli 46 | 47 | 48 | src/main/resources 49 | 51 | true 52 | 53 | 54 | 55 | 56 | org.apache.maven.plugins 57 | maven-compiler-plugin 58 | 59 | 60 | org.apache.maven.plugins 61 | maven-jar-plugin 62 | 63 | 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/resources/json/glucoseHighNoInterpretation.fhir: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "f001", 4 | "text": { 5 | "status": "generated", 6 | "div": "

Generated Narrative with Details

id: f001

identifier: 6323 (OFFICIAL)

status: final

code: Glucose [Moles/volume] in Blood (Details : {LOINC code '15074-8' = 'Glucose [Moles/volume] in Blood', given as 'Glucose [Moles/volume] in Blood'})

subject: P. van de Heuvel

effective: 02/04/2013 9:30:10 AM --> (ongoing)

issued: 03/04/2013 3:30:10 PM

performer: A. Langeveld

value: 6.3 mmol/l (Details: UCUM code mmol/L = 'mmol/L')

interpretation: High (Details : {http://hl7.org/fhir/v2/0078 code 'H' = 'High', given as 'High'})

ReferenceRanges

-LowHigh
*3.1 mmol/l (Details: UCUM code mmol/L = 'mmol/L')6.2 mmol/l (Details: UCUM code mmol/L = 'mmol/L')
" 7 | }, 8 | "identifier": [ 9 | { 10 | "use": "official", 11 | "system": "http://www.bmc.nl/zorgportal/identifiers/observations", 12 | "value": "6323" 13 | } 14 | ], 15 | "status": "final", 16 | "code": { 17 | "coding": [ 18 | { 19 | "system": "http://loinc.org", 20 | "code": "15074-8", 21 | "display": "Glucose [Moles/volume] in Blood" 22 | } 23 | ] 24 | }, 25 | "subject": { 26 | "reference": "Patient/f001", 27 | "display": "P. van de Heuvel" 28 | }, 29 | "effectivePeriod": { 30 | "start": "2013-04-02T09:30:10+01:00" 31 | }, 32 | "issued": "2013-04-03T15:30:10+01:00", 33 | "performer": [ 34 | { 35 | "reference": "Practitioner/f005", 36 | "display": "A. Langeveld" 37 | } 38 | ], 39 | "valueQuantity": { 40 | "value": 6.3, 41 | "unit": "mmol/l", 42 | "system": "http://unitsofmeasure.org", 43 | "code": "mmol/L" 44 | }, 45 | "referenceRange": [ 46 | { 47 | "low": { 48 | "value": 3.1, 49 | "unit": "mmol/l", 50 | "system": "http://unitsofmeasure.org", 51 | "code": "mmol/L" 52 | }, 53 | "high": { 54 | "value": 6.2, 55 | "unit": "mmol/l", 56 | "system": "http://unitsofmeasure.org", 57 | "code": "mmol/L" 58 | } 59 | } 60 | ] 61 | } -------------------------------------------------------------------------------- /loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpocore/codesystems/Outcome.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.codesystems; 2 | 3 | import java.util.Objects; 4 | 5 | public class Outcome implements Comparable { 6 | 7 | 8 | private final ShortCode code; 9 | private final String outcome; 10 | 11 | 12 | public Outcome(ShortCode code, String outcome) { 13 | this.code = code; 14 | this.outcome = outcome; 15 | } 16 | 17 | public Outcome(ShortCode code) { 18 | this.code = code; 19 | this.outcome = code.shortForm(); 20 | } 21 | 22 | public ShortCode getCode() { 23 | return code; 24 | } 25 | 26 | public String getOutcome() { 27 | return outcome; 28 | } 29 | 30 | 31 | public static Outcome nominal(String outcomeString) { 32 | return new Outcome(ShortCode.NOM, outcomeString); 33 | } 34 | 35 | public boolean isNominal() { 36 | return this.code.equals(ShortCode.NOM); 37 | } 38 | 39 | public boolean isQuantitative() { 40 | return this.code.equals(ShortCode.L) || this.code.equals(ShortCode.N) || this.code.equals(ShortCode.H); 41 | } 42 | 43 | public boolean isOrdinal() { 44 | return this.code.equals(ShortCode.NEG) || this.code.equals(ShortCode.POS); 45 | } 46 | 47 | public static Outcome LOW() { 48 | return new Outcome(ShortCode.L); 49 | } 50 | 51 | public static Outcome NORMAL() { 52 | return new Outcome(ShortCode.N); 53 | } 54 | 55 | public static Outcome HIGH() { 56 | return new Outcome(ShortCode.H); 57 | } 58 | 59 | public static Outcome POSITIVE() { 60 | return new Outcome(ShortCode.POS); 61 | } 62 | 63 | public static Outcome NEGATIVE() { 64 | return new Outcome(ShortCode.NEG); 65 | } 66 | 67 | @Override 68 | public int hashCode() { 69 | return Objects.hash(this.code, this.outcome); 70 | } 71 | 72 | @Override 73 | public boolean equals(Object obj) { 74 | if (! (obj instanceof Outcome)) { 75 | return false; 76 | } 77 | Outcome that = (Outcome) obj; 78 | return this.code.equals(that.code) && this.outcome.equals(that.outcome); 79 | } 80 | 81 | @Override 82 | public String toString() { 83 | if (code.equals(ShortCode.NOM)) { 84 | return "Nom: " + outcome; 85 | } else { 86 | return code.name(); 87 | } 88 | } 89 | 90 | @Override 91 | public int compareTo(Outcome that) { 92 | int res = this.code.compareTo(that.code); 93 | return res != 0 ? res : this.outcome.compareTo(that.outcome); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/resources/json/glucoseHigh.fhir: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "f001", 4 | "text": { 5 | "status": "generated", 6 | "div": "

Generated Narrative with Details

id: f001

identifier: 6323 (OFFICIAL)

status: final

code: Glucose [Moles/volume] in Blood (Details : {LOINC code '15074-8' = 'Glucose [Moles/volume] in Blood', given as 'Glucose [Moles/volume] in Blood'})

subject: P. van de Heuvel

effective: 02/04/2013 9:30:10 AM --> (ongoing)

issued: 03/04/2013 3:30:10 PM

performer: A. Langeveld

value: 6.3 mmol/l (Details: UCUM code mmol/L = 'mmol/L')

interpretation: High (Details : {http://hl7.org/fhir/v2/0078 code 'H' = 'High', given as 'High'})

ReferenceRanges

-LowHigh
*3.1 mmol/l (Details: UCUM code mmol/L = 'mmol/L')6.2 mmol/l (Details: UCUM code mmol/L = 'mmol/L')
" 7 | }, 8 | "identifier": [ 9 | { 10 | "use": "official", 11 | "system": "http://www.bmc.nl/zorgportal/identifiers/observations", 12 | "value": "6323" 13 | } 14 | ], 15 | "status": "final", 16 | "code": { 17 | "coding": [ 18 | { 19 | "system": "http://loinc.org", 20 | "code": "15074-8", 21 | "display": "Glucose [Moles/volume] in Blood" 22 | } 23 | ] 24 | }, 25 | "subject": { 26 | "reference": "Patient/f001", 27 | "display": "P. van de Heuvel" 28 | }, 29 | "effectivePeriod": { 30 | "start": "2013-04-02T09:30:10+01:00" 31 | }, 32 | "issued": "2013-04-03T15:30:10+01:00", 33 | "performer": [ 34 | { 35 | "reference": "Practitioner/f005", 36 | "display": "A. Langeveld" 37 | } 38 | ], 39 | "valueQuantity": { 40 | "value": 6.3, 41 | "unit": "mmol/l", 42 | "system": "http://unitsofmeasure.org", 43 | "code": "mmol/L" 44 | }, 45 | "interpretation": { 46 | "coding": [ 47 | { 48 | "system": "http://hl7.org/fhir/v2/0078", 49 | "code": "H", 50 | "display": "High" 51 | } 52 | ] 53 | }, 54 | "referenceRange": [ 55 | { 56 | "low": { 57 | "value": 3.1, 58 | "unit": "mmol/l", 59 | "system": "http://unitsofmeasure.org", 60 | "code": "mmol/L" 61 | }, 62 | "high": { 63 | "value": 6.2, 64 | "unit": "mmol/l", 65 | "system": "http://unitsofmeasure.org", 66 | "code": "mmol/L" 67 | } 68 | } 69 | ] 70 | } -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/resources/json/glucoseLow.fhir: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "f001", 4 | "text": { 5 | "status": "generated", 6 | "div": "

Generated Narrative with Details

id: f001

identifier: 6323 (OFFICIAL)

status: final

code: Glucose [Moles/volume] in Blood (Details : {LOINC code '15074-8' = 'Glucose [Moles/volume] in Blood', given as 'Glucose [Moles/volume] in Blood'})

subject: P. van de Heuvel

effective: 02/04/2013 9:30:10 AM --> (ongoing)

issued: 03/04/2013 3:30:10 PM

performer: A. Langeveld

value: 6.3 mmol/l (Details: UCUM code mmol/L = 'mmol/L')

interpretation: High (Details : {http://hl7.org/fhir/v2/0078 code 'H' = 'High', given as 'High'})

ReferenceRanges

-LowHigh
*3.1 mmol/l (Details: UCUM code mmol/L = 'mmol/L')6.2 mmol/l (Details: UCUM code mmol/L = 'mmol/L')
" 7 | }, 8 | "identifier": [ 9 | { 10 | "use": "official", 11 | "system": "http://www.bmc.nl/zorgportal/identifiers/observations", 12 | "value": "6323" 13 | } 14 | ], 15 | "status": "final", 16 | "code": { 17 | "coding": [ 18 | { 19 | "system": "http://loinc.org", 20 | "code": "15074-8", 21 | "display": "Glucose [Moles/volume] in Blood" 22 | } 23 | ] 24 | }, 25 | "subject": { 26 | "reference": "Patient/f001", 27 | "display": "P. van de Heuvel" 28 | }, 29 | "effectivePeriod": { 30 | "start": "2013-04-02T09:30:10+01:00" 31 | }, 32 | "issued": "2013-04-03T15:30:10+01:00", 33 | "performer": [ 34 | { 35 | "reference": "Practitioner/f005", 36 | "display": "A. Langeveld" 37 | } 38 | ], 39 | "valueQuantity": { 40 | "value": 2.3, 41 | "unit": "mmol/l", 42 | "system": "http://unitsofmeasure.org", 43 | "code": "mmol/L" 44 | }, 45 | "interpretation": { 46 | "coding": [ 47 | { 48 | "system": "http://hl7.org/fhir/v2/0078", 49 | "code": "L", 50 | "display": "Low" 51 | } 52 | ] 53 | }, 54 | "referenceRange": [ 55 | { 56 | "low": { 57 | "value": 3.1, 58 | "unit": "mmol/l", 59 | "system": "http://unitsofmeasure.org", 60 | "code": "mmol/L" 61 | }, 62 | "high": { 63 | "value": 6.2, 64 | "unit": "mmol/l", 65 | "system": "http://unitsofmeasure.org", 66 | "code": "mmol/L" 67 | } 68 | } 69 | ] 70 | } -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/resources/json/glucoseNormal.fhir: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "f001", 4 | "text": { 5 | "status": "generated", 6 | "div": "

Generated Narrative with Details

id: f001

identifier: 6323 (OFFICIAL)

status: final

code: Glucose [Moles/volume] in Blood (Details : {LOINC code '15074-8' = 'Glucose [Moles/volume] in Blood', given as 'Glucose [Moles/volume] in Blood'})

subject: P. van de Heuvel

effective: 02/04/2013 9:30:10 AM --> (ongoing)

issued: 03/04/2013 3:30:10 PM

performer: A. Langeveld

value: 6.3 mmol/l (Details: UCUM code mmol/L = 'mmol/L')

interpretation: High (Details : {http://hl7.org/fhir/v2/0078 code 'H' = 'High', given as 'High'})

ReferenceRanges

-LowHigh
*3.1 mmol/l (Details: UCUM code mmol/L = 'mmol/L')6.2 mmol/l (Details: UCUM code mmol/L = 'mmol/L')
" 7 | }, 8 | "identifier": [ 9 | { 10 | "use": "official", 11 | "system": "http://www.bmc.nl/zorgportal/identifiers/observations", 12 | "value": "6323" 13 | } 14 | ], 15 | "status": "final", 16 | "code": { 17 | "coding": [ 18 | { 19 | "system": "http://loinc.org", 20 | "code": "15074-8", 21 | "display": "Glucose [Moles/volume] in Blood" 22 | } 23 | ] 24 | }, 25 | "subject": { 26 | "reference": "Patient/f001", 27 | "display": "P. van de Heuvel" 28 | }, 29 | "effectivePeriod": { 30 | "start": "2013-04-02T09:30:10+01:00" 31 | }, 32 | "issued": "2013-04-03T15:30:10+01:00", 33 | "performer": [ 34 | { 35 | "reference": "Practitioner/f005", 36 | "display": "A. Langeveld" 37 | } 38 | ], 39 | "valueQuantity": { 40 | "value": 5.3, 41 | "unit": "mmol/l", 42 | "system": "http://unitsofmeasure.org", 43 | "code": "mmol/L" 44 | }, 45 | "interpretation": { 46 | "coding": [ 47 | { 48 | "system": "http://hl7.org/fhir/v2/0078", 49 | "code": "N", 50 | "display": "Normal" 51 | } 52 | ] 53 | }, 54 | "referenceRange": [ 55 | { 56 | "low": { 57 | "value": 3.1, 58 | "unit": "mmol/l", 59 | "system": "http://unitsofmeasure.org", 60 | "code": "mmol/L" 61 | }, 62 | "high": { 63 | "value": 6.2, 64 | "unit": "mmol/l", 65 | "system": "http://unitsofmeasure.org", 66 | "code": "mmol/L" 67 | } 68 | } 69 | ] 70 | } -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/resources/json/glucoseAbnormal.fhir: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "f001", 4 | "text": { 5 | "status": "generated", 6 | "div": "

Generated Narrative with Details

id: f001

identifier: 6323 (OFFICIAL)

status: final

code: Glucose [Moles/volume] in Blood (Details : {LOINC code '15074-8' = 'Glucose [Moles/volume] in Blood', given as 'Glucose [Moles/volume] in Blood'})

subject: P. van de Heuvel

effective: 02/04/2013 9:30:10 AM --> (ongoing)

issued: 03/04/2013 3:30:10 PM

performer: A. Langeveld

value: 6.3 mmol/l (Details: UCUM code mmol/L = 'mmol/L')

interpretation: High (Details : {http://hl7.org/fhir/v2/0078 code 'H' = 'High', given as 'High'})

ReferenceRanges

-LowHigh
*3.1 mmol/l (Details: UCUM code mmol/L = 'mmol/L')6.2 mmol/l (Details: UCUM code mmol/L = 'mmol/L')
" 7 | }, 8 | "identifier": [ 9 | { 10 | "use": "official", 11 | "system": "http://www.bmc.nl/zorgportal/identifiers/observations", 12 | "value": "6323" 13 | } 14 | ], 15 | "status": "final", 16 | "code": { 17 | "coding": [ 18 | { 19 | "system": "http://loinc.org", 20 | "code": "15074-8", 21 | "display": "Glucose [Moles/volume] in Blood" 22 | } 23 | ] 24 | }, 25 | "subject": { 26 | "reference": "Patient/f001", 27 | "display": "P. van de Heuvel" 28 | }, 29 | "effectivePeriod": { 30 | "start": "2013-04-02T09:30:10+01:00" 31 | }, 32 | "issued": "2013-04-03T15:30:10+01:00", 33 | "performer": [ 34 | { 35 | "reference": "Practitioner/f005", 36 | "display": "A. Langeveld" 37 | } 38 | ], 39 | "valueQuantity": { 40 | "value": 6.3, 41 | "unit": "mmol/l", 42 | "system": "http://unitsofmeasure.org", 43 | "code": "mmol/L" 44 | }, 45 | "interpretation": { 46 | "coding": [ 47 | { 48 | "system": "http://hl7.org/fhir/v2/0078", 49 | "code": "A", 50 | "display": "Abnormal" 51 | } 52 | ] 53 | }, 54 | "referenceRange": [ 55 | { 56 | "low": { 57 | "value": 3.1, 58 | "unit": "mmol/l", 59 | "system": "http://unitsofmeasure.org", 60 | "code": "mmol/L" 61 | }, 62 | "high": { 63 | "value": 6.2, 64 | "unit": "mmol/l", 65 | "system": "http://unitsofmeasure.org", 66 | "code": "mmol/L" 67 | } 68 | } 69 | ] 70 | } -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/resources/json/hemoglobin.fhir: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "f005", 4 | "text": { 5 | "status": "generated", 6 | "div": "

Generated Narrative with Details

id: f005

identifier: 6327 (OFFICIAL)

status: final

code: Hemoglobin [Mass/volume] in Blood (Details : {LOINC code '718-7' = 'Hemoglobin [Mass/volume] in Blood', given as 'Hemoglobin [Mass/volume] in Blood'})

subject: P. van de Heuvel

effective: 05/04/2013 10:30:10 AM --> 05/04/2013 10:30:10 AM

issued: 05/04/2013 3:30:10 PM

performer: A. Langeveld

value: 7.2 g/dl (Details: UCUM code g/dL = 'g/dL')

interpretation: Low (Details : {http://hl7.org/fhir/v2/0078 code 'L' = 'Low', given as 'Low'})

ReferenceRanges

-LowHigh
*7.5 g/dl (Details: UCUM code g/dL = 'g/dL')10 g/dl (Details: UCUM code g/dL = 'g/dL')
" 7 | }, 8 | "identifier": [ 9 | { 10 | "use": "official", 11 | "system": "http://www.bmc.nl/zorgportal/identifiers/observations", 12 | "value": "6327" 13 | } 14 | ], 15 | "status": "final", 16 | "code": { 17 | "coding": [ 18 | { 19 | "system": "http://loinc.org", 20 | "code": "718-7", 21 | "display": "Hemoglobin [Mass/volume] in Blood" 22 | } 23 | ] 24 | }, 25 | "subject": { 26 | "reference": "Patient/f001", 27 | "display": "P. van de Heuvel" 28 | }, 29 | "effectivePeriod": { 30 | "start": "2013-04-05T10:30:10+01:00", 31 | "end": "2013-04-05T10:30:10+01:00" 32 | }, 33 | "issued": "2013-04-05T15:30:10+01:00", 34 | "performer": [ 35 | { 36 | "reference": "Practitioner/f005", 37 | "display": "A. Langeveld" 38 | } 39 | ], 40 | "valueQuantity": { 41 | "value": 7.2, 42 | "unit": "g/dl", 43 | "system": "http://unitsofmeasure.org", 44 | "code": "g/dL" 45 | }, 46 | "interpretation": { 47 | "coding": [ 48 | { 49 | "system": "http://hl7.org/fhir/v2/0078", 50 | "code": "L", 51 | "display": "Low" 52 | } 53 | ] 54 | }, 55 | "referenceRange": [ 56 | { 57 | "low": { 58 | "value": 7.5, 59 | "unit": "g/dl", 60 | "system": "http://unitsofmeasure.org", 61 | "code": "g/dL" 62 | }, 63 | "high": { 64 | "value": 10, 65 | "unit": "g/dl", 66 | "system": "http://unitsofmeasure.org", 67 | "code": "g/dL" 68 | } 69 | } 70 | ] 71 | } -------------------------------------------------------------------------------- /loinc2hpo-core/src/test/java/org/monarchinitiative/loinc2hpocore/loinc/LoincEntryTest.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.loinc; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.monarchinitiative.loinc2hpocore.annotation.LoincScale; 5 | 6 | import java.util.Arrays; 7 | import java.util.List; 8 | import java.util.stream.Collectors; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | 12 | 13 | /* 14 | "LOINC_NUM","COMPONENT","PROPERTY","TIME_ASPCT","SYSTEM","SCALE_TYP","METHOD_TYP","CLASS","CLASSTYPE","LONG_COMMON_NAME","SHORTNAME","EXTERNAL_COPYRIGHT_NOTICE","STATUS","VersionFirstReleased","VersionLastChanged" 15 | 16 | */ 17 | public class LoincEntryTest { 18 | 19 | @Test 20 | void testConstruction() { 21 | String [] entryFields = {"10000-8","R wave duration.lead AVR","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead AVR","R wave dur L-AVR","","ACTIVE","1.0i","2.48"}; 22 | List quotedEntryFields = Arrays.stream(entryFields).map(w -> String.format("\"%s\"", w)).collect(Collectors.toList()); 23 | String entryLine1 = String.join(",", quotedEntryFields); 24 | LoincEntry entry = LoincEntry.fromQuotedCsvLine(entryLine1); 25 | assertEquals("R wave duration.lead AVR", entry.getComponent()); 26 | assertEquals("Pt", entry.getTimeAspect()); 27 | LoincId id = new LoincId("10000-8"); 28 | assertEquals(id, entry.getLoincId()); 29 | assertEquals("EKG",entry.getMethod()); 30 | } 31 | 32 | @Test 33 | void testDichlorophenoxyacetate() { 34 | String [] fields = { "9806-1","2,4-Dichlorophenoxyacetate","MCnc","Pt","Urine","Qn","","DRUG/TOX","1","2,4-Dichlorophenoxyacetate [Mass/volume] in Urine","2,4D Ur-mCnc","","ACTIVE","1.0i","2.42"}; 35 | List quotedEntryFields = Arrays.stream(fields).map(w -> String.format("\"%s\"", w)).collect(Collectors.toList()); 36 | String line = String.join(",",quotedEntryFields); 37 | LoincEntry entry = LoincEntry.fromQuotedCsvLine(line); 38 | LoincId loincId = new LoincId("9806-1"); 39 | assertEquals(loincId, entry.getLoincId()); 40 | assertEquals("2,4-Dichlorophenoxyacetate", entry.getComponent()); 41 | assertEquals("MCnc", entry.getProperty()); 42 | assertEquals("Pt", entry.getTimeAspect()); 43 | assertEquals("Urine", entry.getSystem()); 44 | assertEquals(LoincScale.QUANTITATIVE, entry.getScale()); 45 | assertEquals("", entry.getMethod()); 46 | assertEquals("2,4-Dichlorophenoxyacetate [Mass/volume] in Urine", entry.getLongName()); 47 | LoincLongName lln = entry.getLoincLongName(); 48 | assertEquals("Urine", lln.getLoincTissue()); 49 | assertEquals("2,4-Dichlorophenoxyacetate", lln.getLoincParameter()); 50 | } 51 | 52 | 53 | } 54 | -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/resources/json/glucoseConflictingInterpretation.fhir: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "f001", 4 | "text": { 5 | "status": "generated", 6 | "div": "

Generated Narrative with Details

id: f001

identifier: 6323 (OFFICIAL)

status: final

code: Glucose [Moles/volume] in Blood (Details : {LOINC code '15074-8' = 'Glucose [Moles/volume] in Blood', given as 'Glucose [Moles/volume] in Blood'})

subject: P. van de Heuvel

effective: 02/04/2013 9:30:10 AM --> (ongoing)

issued: 03/04/2013 3:30:10 PM

performer: A. Langeveld

value: 6.3 mmol/l (Details: UCUM code mmol/L = 'mmol/L')

interpretation: High (Details : {http://hl7.org/fhir/v2/0078 code 'H' = 'High', given as 'High'})

ReferenceRanges

-LowHigh
*3.1 mmol/l (Details: UCUM code mmol/L = 'mmol/L')6.2 mmol/l (Details: UCUM code mmol/L = 'mmol/L')
" 7 | }, 8 | "identifier": [ 9 | { 10 | "use": "official", 11 | "system": "http://www.bmc.nl/zorgportal/identifiers/observations", 12 | "value": "6323" 13 | } 14 | ], 15 | "status": "final", 16 | "code": { 17 | "coding": [ 18 | { 19 | "system": "http://loinc.org", 20 | "code": "15074-8", 21 | "display": "Glucose [Moles/volume] in Blood" 22 | } 23 | ] 24 | }, 25 | "subject": { 26 | "reference": "Patient/f001", 27 | "display": "P. van de Heuvel" 28 | }, 29 | "effectivePeriod": { 30 | "start": "2013-04-02T09:30:10+01:00" 31 | }, 32 | "issued": "2013-04-03T15:30:10+01:00", 33 | "performer": [ 34 | { 35 | "reference": "Practitioner/f005", 36 | "display": "A. Langeveld" 37 | } 38 | ], 39 | "valueQuantity": { 40 | "value": 6.3, 41 | "unit": "mmol/l", 42 | "system": "http://unitsofmeasure.org", 43 | "code": "mmol/L" 44 | }, 45 | "interpretation": { 46 | "coding": [ 47 | { 48 | "system": "http://hl7.org/fhir/v2/0078", 49 | "code": "H", 50 | "display": "High" 51 | }, 52 | { 53 | "system": "http://hl7.org/fhir/v2/0078", 54 | "code": "L", 55 | "display": "Low" 56 | } 57 | ] 58 | }, 59 | "referenceRange": [ 60 | { 61 | "low": { 62 | "value": 3.1, 63 | "unit": "mmol/l", 64 | "system": "http://unitsofmeasure.org", 65 | "code": "mmol/L" 66 | }, 67 | "high": { 68 | "value": 6.2, 69 | "unit": "mmol/l", 70 | "system": "http://unitsofmeasure.org", 71 | "code": "mmol/L" 72 | } 73 | } 74 | ] 75 | } -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/resources/json/erythrocyte.fhir: -------------------------------------------------------------------------------- 1 | { 2 | "resourceType": "Observation", 3 | "id": "f004", 4 | "text": { 5 | "status": "generated", 6 | "div": "

Generated Narrative with Details

id: f004

identifier: 6326 (OFFICIAL)

status: final

code: Erythrocytes [#/volume] in Blood by Automated count (Details : {LOINC code '789-8' = 'Erythrocytes [#/volume] in Blood by Automated count', given as 'Erythrocytes [#/volume] in Blood by Automated count'})

subject: P. van de Heuvel

effective: 02/04/2013 10:30:10 AM --> 05/04/2013 10:30:10 AM

issued: 03/04/2013 3:30:10 PM

performer: A. Langeveld

value: 4.12 10^12/L (Details: UCUM code 10*12/L = '10*12/L')

interpretation: Low (Details : {http://hl7.org/fhir/v2/0078 code 'L' = 'Low', given as 'Low'})

ReferenceRanges

-Text
* 12-14 y Male: 4.4 - 5.2 x 10^12/L ; 12-14 y Female: 4.2 - 4.8 x 10^12/L ; 15-17 y Male: 4.6 - 5.4 x 10^12/L ; 15-17 y Female: 4.2 - 4.8 x 10^12/L ; 18-64 y Male: 4.6 - 5.4 x 10^12/L ; 18-64 y Female: 4.0 - 4.8 x 10^12/L ; 65-74 y Male: 4.3 - 5.3 x 10^12/L ; 65-74 y Female: 4.1 - 4.9 x 10^12/L
" 7 | }, 8 | "identifier": [ 9 | { 10 | "use": "official", 11 | "system": "http://www.bmc.nl/zorgportal/identifiers/observations", 12 | "value": "6326" 13 | } 14 | ], 15 | "status": "final", 16 | "code": { 17 | "coding": [ 18 | { 19 | "system": "http://loinc.org", 20 | "code": "789-8", 21 | "display": "Erythrocytes [#/volume] in Blood by Automated count" 22 | } 23 | ] 24 | }, 25 | "subject": { 26 | "reference": "Patient/f001", 27 | "display": "P. van de Heuvel" 28 | }, 29 | "effectivePeriod": { 30 | "start": "2013-04-02T10:30:10+01:00", 31 | "end": "2013-04-05T10:30:10+01:00" 32 | }, 33 | "issued": "2013-04-03T15:30:10+01:00", 34 | "performer": [ 35 | { 36 | "reference": "Practitioner/f005", 37 | "display": "A. Langeveld" 38 | } 39 | ], 40 | "valueQuantity": { 41 | "value": 4.12, 42 | "unit": "10^12/L", 43 | "system": "http://unitsofmeasure.org", 44 | "code": "10*12/L" 45 | }, 46 | "interpretation": { 47 | "coding": [ 48 | { 49 | "system": "http://hl7.org/fhir/v2/0078", 50 | "code": "L", 51 | "display": "Low" 52 | } 53 | ] 54 | }, 55 | "referenceRange": [ 56 | { 57 | "text": " 12-14 y Male: 4.4 - 5.2 x 10^12/L ; 12-14 y Female: 4.2 - 4.8 x 10^12/L ; 15-17 y Male: 4.6 - 5.4 x 10^12/L ; 15-17 y Female: 4.2 - 4.8 x 10^12/L ; 18-64 y Male: 4.6 - 5.4 x 10^12/L ; 18-64 y Female: 4.0 - 4.8 x 10^12/L ; 65-74 y Male: 4.3 - 5.3 x 10^12/L ; 65-74 y Female: 4.1 - 4.9 x 10^12/L " 58 | } 59 | ] 60 | } -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/java/fhir/Dstu3ObservationTest.java: -------------------------------------------------------------------------------- 1 | package fhir; 2 | 3 | import org.hl7.fhir.dstu3.model.Observation; 4 | import org.junit.jupiter.api.Test; 5 | import org.monarchinitiative.loinc2hpocore.codesystems.Outcome; 6 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 7 | import org.monarchinitiative.loinc2hpofhir.fhir2hpo.ObservationDtu3; 8 | import org.monarchinitiative.loinc2hpofhir.fhir2hpo.Uberobservation; 9 | 10 | import java.io.IOException; 11 | import java.util.Optional; 12 | 13 | import static fhir.TestBase.importDstu3Observation; 14 | import static org.junit.jupiter.api.Assertions.*; 15 | 16 | public class Dstu3ObservationTest { 17 | 18 | 19 | 20 | @Test 21 | void testGlucoseHigh() throws IOException { 22 | String jsonPath = "json/glucoseHigh.fhir"; 23 | Observation glucoseHigh = importDstu3Observation(jsonPath); 24 | Uberobservation uberobservation = new ObservationDtu3(glucoseHigh); 25 | LoincId expectedLoincId = new LoincId("15074-8"); 26 | Optional opt = uberobservation.getLoincId(); 27 | assertTrue(opt.isPresent()); 28 | assertEquals(expectedLoincId, opt.get()); 29 | Optional opt2 = uberobservation.getOutcome(); 30 | assertTrue(opt2.isPresent()); 31 | assertEquals(Outcome.HIGH(), opt2.get()); 32 | assertNotEquals(Outcome.LOW(), opt2.get()); 33 | assertNotEquals(Outcome.NORMAL(), opt2.get()); 34 | } 35 | 36 | @Test 37 | void testGlucoseLow() throws IOException { 38 | String jsonPath = "json/glucoseLow.fhir"; 39 | Observation glucoseAbnormal = importDstu3Observation(jsonPath); 40 | Uberobservation uberobservation = new ObservationDtu3(glucoseAbnormal); 41 | LoincId expectedLoincId = new LoincId("15074-8"); 42 | Optional opt = uberobservation.getLoincId(); 43 | assertTrue(opt.isPresent()); 44 | assertEquals(expectedLoincId, opt.get()); 45 | Optional opt2 = uberobservation.getOutcome(); 46 | assertTrue(opt2.isPresent()); 47 | assertEquals(Outcome.LOW(), opt2.get()); 48 | assertNotEquals(Outcome.NORMAL(), opt2.get()); 49 | assertNotEquals(Outcome.HIGH(), opt2.get()); 50 | } 51 | 52 | @Test 53 | void testGlucoseNormal() throws IOException { 54 | String jsonPath = "json/glucoseNormal.fhir"; 55 | Observation glucoseAbnormal = importDstu3Observation(jsonPath); 56 | Uberobservation uberobservation = new ObservationDtu3(glucoseAbnormal); 57 | LoincId expectedLoincId = new LoincId("15074-8"); 58 | Optional opt = uberobservation.getLoincId(); 59 | assertTrue(opt.isPresent()); 60 | assertEquals(expectedLoincId, opt.get()); 61 | Optional opt2 = uberobservation.getOutcome(); 62 | assertTrue(opt2.isPresent()); 63 | assertEquals(Outcome.NORMAL(), opt2.get()); 64 | assertNotEquals(Outcome.LOW(), opt2.get()); 65 | assertNotEquals(Outcome.HIGH(), opt2.get()); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpocore/exception/Loinc2HpoRuntimeException.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.exception; 2 | 3 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 4 | 5 | public class Loinc2HpoRuntimeException extends RuntimeException { 6 | 7 | 8 | public Loinc2HpoRuntimeException() { super();} 9 | public Loinc2HpoRuntimeException(String msg) { super(msg);} 10 | 11 | 12 | 13 | public static Loinc2HpoRuntimeException unrecognizedCode(String code) { 14 | return new Loinc2HpoRuntimeException("Unrecognized result code: \"" + code + "\""); 15 | } 16 | 17 | public static Loinc2HpoRuntimeException noCodeFound() { 18 | return new Loinc2HpoRuntimeException("No results code found."); 19 | } 20 | 21 | public static Loinc2HpoRuntimeException internalCodeNotFound(String externalCound) { 22 | return new Loinc2HpoRuntimeException("Could not find internal code to match \"" + externalCound + "\"."); 23 | } 24 | 25 | 26 | public static Loinc2HpoRuntimeException ambiguousResults() { 27 | return new Loinc2HpoRuntimeException("Multiple matching codes found (should never happen)."); 28 | } 29 | 30 | public static Loinc2HpoRuntimeException notAnnotated(LoincId loincId) { 31 | return new Loinc2HpoRuntimeException("Could not find annotation for " + loincId.toString()); 32 | } 33 | 34 | public static Loinc2HpoRuntimeException outComenotAnnotated(LoincId loincId) { 35 | return new Loinc2HpoRuntimeException("Could not find annotation for " + loincId.toString()); 36 | } 37 | 38 | public static Loinc2HpoRuntimeException malformedLoincCode(String line) { 39 | return new Loinc2HpoRuntimeException("malformedLoincCode: \"" + line + "\""); 40 | } 41 | 42 | public static Loinc2HpoRuntimeException subjectNotFound() { 43 | return new Loinc2HpoRuntimeException("Could not find subject element"); 44 | } 45 | 46 | public static Loinc2HpoRuntimeException ambiguousSubject() { 47 | return new Loinc2HpoRuntimeException("Found more than one subject element"); 48 | } 49 | 50 | public static Loinc2HpoRuntimeException referenceRangeNotFound() { 51 | return new Loinc2HpoRuntimeException("Did not find a reference range"); 52 | } 53 | 54 | public static Loinc2HpoRuntimeException ambiguousReferenceRange() { 55 | return new Loinc2HpoRuntimeException("Found more than one reference range"); 56 | } 57 | 58 | public static Loinc2HpoRuntimeException unrecognizedLoincCodeException() { 59 | return new Loinc2HpoRuntimeException("Unrecognize LOINC code"); 60 | } 61 | 62 | public static Loinc2HpoRuntimeException loincCodeNotFound() { 63 | return new Loinc2HpoRuntimeException("LOINC code not found"); 64 | } 65 | 66 | public static Loinc2HpoRuntimeException missingPanelComponent() { 67 | return new Loinc2HpoRuntimeException("Missing LOINC panel component"); 68 | } 69 | 70 | 71 | public static Exception malFormedAnnotationLine(String line, int length) { 72 | return new Loinc2HpoRuntimeException(String.format("Malformed line with %d fields: %s", length, line)); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpocore/annotation/OrdinalHpoAnnotation.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.annotation; 2 | 3 | import org.monarchinitiative.loinc2hpocore.codesystems.Outcome; 4 | import org.monarchinitiative.loinc2hpocore.exception.Loinc2HpoRuntimeException; 5 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Optional; 10 | 11 | 12 | public class OrdinalHpoAnnotation implements LoincAnnotation { 13 | private final LoincId loincId; 14 | private final Loinc2HpoAnnotation negative; 15 | private final Loinc2HpoAnnotation positive; 16 | 17 | public OrdinalHpoAnnotation(Loinc2HpoAnnotation absent, Loinc2HpoAnnotation present) { 18 | this.negative = absent; 19 | this.positive = present; 20 | // assumption is that both annotation have the same LoincId, which will be true 21 | // unless there is some insanity 22 | this.loincId = this.negative.getLoincId(); 23 | } 24 | 25 | 26 | @Override 27 | public Optional getOutcome(Outcome outcome) { 28 | switch (outcome.getCode()) { 29 | case NEG: 30 | return Optional.of(new Hpo2Outcome(negative.getHpoTermId(), Outcome.NEGATIVE())); 31 | case POS: 32 | return Optional.of(new Hpo2Outcome(positive.getHpoTermId(), Outcome.POSITIVE())); 33 | default: 34 | return Optional.empty(); 35 | } 36 | } 37 | 38 | @Override 39 | public LoincId getLoincId() { 40 | return this.loincId; 41 | } 42 | 43 | @Override 44 | public List allAnnotations() { 45 | return List.of(negative, positive); 46 | } 47 | 48 | private static String getDebugInfo(Map outcomeMap) { 49 | StringBuilder sb = new StringBuilder("Malformed Ordinal outcomes\nn=").append(outcomeMap.size()); 50 | for (var oc : outcomeMap.values()) { 51 | sb.append("\t[ERROR] ").append(oc).append("\n"); 52 | } 53 | return sb.toString(); 54 | } 55 | 56 | 57 | public static LoincAnnotation fromOutcomeMap(Map outcomeMap) { 58 | // there are only two possible Ordinal outcomes, so we just need to check for size 59 | if (outcomeMap.size() == 2 && outcomeMap.containsKey(Outcome.NEGATIVE()) && outcomeMap.containsKey(Outcome.POSITIVE())) { 60 | return new OrdinalHpoAnnotation(outcomeMap.get(Outcome.NEGATIVE()), outcomeMap.get(Outcome.POSITIVE())); 61 | } else { 62 | String msg = String.format("Could not create LoincAnnotation for ordinal: %s", 63 | getDebugInfo(outcomeMap)); 64 | throw new Loinc2HpoRuntimeException(msg); 65 | } 66 | } 67 | 68 | @Override 69 | public LoincScale scale() { 70 | return LoincScale.ORDINAL; 71 | } 72 | 73 | @Override 74 | public String toString() { 75 | return loincId + "\n\t" + 76 | (negative == null? " NEG: n/a" : "NEG: " + negative) + "\n\t" + 77 | (positive == null? " POS: n/a" : "POS: " + positive); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpocore/io/LoincTableCoreParser.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.io; 2 | 3 | import org.monarchinitiative.loinc2hpocore.exception.Loinc2HpoRuntimeException; 4 | import org.monarchinitiative.loinc2hpocore.loinc.LoincEntry; 5 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.io.BufferedReader; 10 | import java.io.FileReader; 11 | import java.io.IOException; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | /** 16 | * Parse LoincTableCore.csv file to obtain {@link org.monarchinitiative.loinc2hpocore.loinc.LoincEntry} objects 17 | */ 18 | public class LoincTableCoreParser { 19 | private static final Logger LOGGER = LoggerFactory.getLogger(LoincTableCoreParser.class); 20 | 21 | private final Map loincEntries; 22 | 23 | private final int malformed; 24 | /** Count of LOINC codes with scales other than Qn, Ord, and Nom, which we skip. */ 25 | private final int invalidScale; 26 | 27 | public LoincTableCoreParser(String pathToLoincCoreTable) { 28 | Map tmp = new HashMap<>(); 29 | int count_malformed = 0; 30 | int count_invalid_scale = 0; 31 | int n=0; 32 | try (BufferedReader br = new BufferedReader(new FileReader(pathToLoincCoreTable))){ 33 | String line; 34 | String header=br.readLine(); 35 | if (! header.equals(LoincEntry.header)) { 36 | LOGGER.error(String.format("Malformed header line (%s) in Loinc File %s",header,pathToLoincCoreTable)); 37 | throw new Loinc2HpoRuntimeException("Malformed LoincTableCore.tsv header line"); 38 | } 39 | while ((line=br.readLine())!=null) { 40 | n++; 41 | try { 42 | LoincEntry entry = LoincEntry.fromQuotedCsvLine(line); 43 | if (entry.getScale().validForLoinc2Hpo()) { 44 | tmp.put(entry.getLoincId(), entry); 45 | } else { 46 | count_invalid_scale++; 47 | } 48 | } catch (Loinc2HpoRuntimeException e) { 49 | LOGGER.error(e.getMessage()); 50 | LOGGER.error("Line {}: {}", n, line); 51 | count_malformed++; 52 | } 53 | } 54 | } catch (IOException e) { 55 | e.printStackTrace(); 56 | } 57 | 58 | LOGGER.info(tmp.size() + " LOINC entries were created"); 59 | malformed = count_malformed; 60 | invalidScale = count_invalid_scale; 61 | if (count_malformed>0) { 62 | LOGGER.error(count_malformed + " LOINC entries are malformed"); 63 | } 64 | this.loincEntries = Map.copyOf(tmp); // immutable copy 65 | } 66 | 67 | public Map getLoincEntries() { 68 | return loincEntries; 69 | } 70 | 71 | public int getMalformed() { 72 | return malformed; 73 | } 74 | 75 | public int getInvalidScale() { 76 | return invalidScale; 77 | } 78 | 79 | public static Map load(String pathToLoincCoreTable) { 80 | LoincTableCoreParser parser = new LoincTableCoreParser(pathToLoincCoreTable); 81 | return parser.getLoincEntries(); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/java/fhir/FhirOutcomeCodeTest.java: -------------------------------------------------------------------------------- 1 | package fhir; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.monarchinitiative.loinc2hpocore.codesystems.ShortCode; 5 | import org.monarchinitiative.loinc2hpofhir.fhir2hpo.FhirOutcomeCode; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | public class FhirOutcomeCodeTest { 10 | 11 | @Test 12 | public void low() { 13 | ShortCode low = ShortCode.L; 14 | String fhirOffScaleLow = "L"; 15 | assertEquals(low, FhirOutcomeCode.fhir2shortcode(fhirOffScaleLow)); 16 | } 17 | 18 | @Test 19 | public void offScaleLow() { 20 | ShortCode low = ShortCode.L; 21 | String fhirOffScaleLow = "<"; 22 | assertEquals(low, FhirOutcomeCode.fhir2shortcode(fhirOffScaleLow)); 23 | } 24 | @Test 25 | public void criticalLow() { 26 | ShortCode low = ShortCode.L; 27 | String fhirOffScaleLow = "LL"; 28 | assertEquals(low, FhirOutcomeCode.fhir2shortcode(fhirOffScaleLow)); 29 | } 30 | 31 | @Test 32 | public void significantLow() { 33 | ShortCode low = ShortCode.L; 34 | String fhirOffScaleLow = "LU"; 35 | assertEquals(low, FhirOutcomeCode.fhir2shortcode(fhirOffScaleLow)); 36 | } 37 | 38 | @Test 39 | public void high() { 40 | ShortCode high = ShortCode.H; 41 | String fhirOffScaleHigh = "H"; 42 | assertEquals(high, FhirOutcomeCode.fhir2shortcode(fhirOffScaleHigh)); 43 | } 44 | 45 | @Test 46 | public void offScaleHigh() { 47 | ShortCode high = ShortCode.H; 48 | String fhirOffScaleHigh = ">"; 49 | assertEquals(high, FhirOutcomeCode.fhir2shortcode(fhirOffScaleHigh)); 50 | } 51 | 52 | 53 | @Test 54 | public void criticallyHigh() { 55 | ShortCode high = ShortCode.H; 56 | String fhirOffScaleHigh = "HH"; 57 | assertEquals(high, FhirOutcomeCode.fhir2shortcode(fhirOffScaleHigh)); 58 | } 59 | 60 | @Test 61 | public void significantlyHigh() { 62 | ShortCode high = ShortCode.H; 63 | String fhirOffScaleHigh = "HU"; 64 | assertEquals(high, FhirOutcomeCode.fhir2shortcode(fhirOffScaleHigh)); 65 | } 66 | 67 | 68 | @Test 69 | public void abnormal() { 70 | ShortCode pos = ShortCode.POS; 71 | String fhirAbnormal = "A"; 72 | assertEquals(pos, FhirOutcomeCode.fhir2shortcode(fhirAbnormal)); 73 | } 74 | 75 | @Test 76 | public void criticallyAbnormal() { 77 | ShortCode pos = ShortCode.POS; 78 | String fhirCriticallyAbnormal = "AA"; 79 | assertEquals(pos, FhirOutcomeCode.fhir2shortcode(fhirCriticallyAbnormal)); 80 | } 81 | 82 | @Test 83 | public void positive() { 84 | ShortCode pos = ShortCode.POS; 85 | String fhirPositive = "POS"; 86 | assertEquals(pos, FhirOutcomeCode.fhir2shortcode(fhirPositive)); 87 | } 88 | 89 | @Test 90 | public void detected() { 91 | ShortCode pos = ShortCode.POS; 92 | String fhirDetected = "DET"; 93 | assertEquals(pos, FhirOutcomeCode.fhir2shortcode(fhirDetected)); 94 | } 95 | 96 | @Test 97 | public void negative() { 98 | ShortCode neg = ShortCode.NEG; 99 | String fhirNotDetected = "NEG"; 100 | assertEquals(neg, FhirOutcomeCode.fhir2shortcode(fhirNotDetected)); 101 | } 102 | 103 | @Test 104 | public void notDetected() { 105 | ShortCode neg = ShortCode.NEG; 106 | String fhirNotDetected = "ND"; 107 | assertEquals(neg, FhirOutcomeCode.fhir2shortcode(fhirNotDetected)); 108 | } 109 | 110 | 111 | } 112 | -------------------------------------------------------------------------------- /docs/getting_started.rst: -------------------------------------------------------------------------------- 1 | Getting started 2 | =============== 3 | 4 | Overview 5 | -------- 6 | 7 | loinc2hpo is a Java library. It requires at least Java 11. It is intended to be used by other applications 8 | for specific purposes. This tutorial shows how to install loinc2hpo and how to use it in a typical Java program. 9 | 10 | Installation 11 | ------------ 12 | First clone the library from GitHub. :: 13 | 14 | git clone https://github.com/monarch-initiative/loinc2hpo.git 15 | 16 | Now use maven to install the library. :: 17 | 18 | cd loinc2hpo 19 | mvn install 20 | 21 | We plan to place loinc2hpo on maven central in the future, which will make this step unnecessary. 22 | 23 | Using loinc2hpo in maven projects 24 | --------------------------------- 25 | 26 | To use the loinc2hpo in your own Java project, add the following to your pom file. 27 | 28 | .. code-block:: XML 29 | 30 | 31 | 1.7.0 32 | 33 | (...) 34 | 35 | 36 | org.monarchinitiative 37 | loinc2hpo-core 38 | ${loinc2hpo.version} 39 | 40 | 41 | org.monarchinitiative 42 | loinc2hpo-fhir 43 | ${loinc2hpo.version} 44 | 45 | 46 | 47 | The ``loinc2hpo-fhir`` module is only required for working with FHIR of course. 48 | 49 | 50 | Core module 51 | ~~~~~~~~~~~ 52 | 53 | The loinc2hpo annotation file referenced in the following code is available from the 54 | `loinc2hpoAnnotation repository `_. 55 | To run the code, ingest the annotation file and then pass 56 | 57 | 58 | 59 | .. code-block:: java 60 | 61 | import org.monarchinitiative.loinc2hpocore.codesystems.ShortCode; 62 | import org.monarchinitiative.phenol.ontology.data.TermId; 63 | import org.monarchinitiative.loinc2hpocore.Loinc2Hpo; 64 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 65 | import org.monarchinitiative.loinc2hpocore.codesystems.Outcome; 66 | 67 | String annot_file = "loinc2hpo-annotations.tsv"; 68 | Loinc2Hpo loinc2Hpo = new Loinc2Hpo(annot_file); 69 | LoincId loincId = new LoincId("26515-7"); 70 | Optional opt = loinc2Hpo.query(loincId, Outcome.LOW()); 71 | if (opt.isPresent()) { 72 | Hpo2Outcome hpo2outcome = opt.get(); 73 | TermId hpoId = hpo2outcome.getHpoId(); 74 | Outcome outcome = hpo2outcome.outcome(); 75 | // do something with the HPO term and the outcome (low in this example). 76 | } 77 | 78 | 79 | FHIR module 80 | ~~~~~~~~~~~ 81 | 82 | The FHIR module is intended to be used with `HAPI FHIR `_. 83 | It can be used with the FHIR specifications DSTU3, R4, or R5. 84 | 85 | .. code-block:: java 86 | 87 | import org.monarchinitiative.loinc2hpofhir.Loinc2HpoFhir; 88 | import org.hl7.fhir.r5.model.*; 89 | import org.monarchinitiative.loinc2hpocore.codesystems.Outcome; 90 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 91 | 92 | String annot_file = "loinc2hpo-annotations.tsv"; 93 | Loinc2HpoFhir loinc2hpoFHIR = new Loinc2HpoFhir(String path); 94 | // The following is a R5 Observation 95 | Observation observation = getObservationFromSomewhere(); // your code does this 96 | Optional opt = loinc2hpoFHIR.r5(observation); 97 | if (opt.isPresent()) { 98 | Hpo2Outcome hpo2outcome = opt.get(); 99 | TermId hpoId = hpo2outcome.getHpoId(); 100 | Outcome outcome = hpo2outcome.outcome(); 101 | // do something with the HPO term and the outcome. 102 | } 103 | 104 | 105 | The ``Loinc2HpoFhir`` has analogous methods called ``dstu3`` and ``r4`` for the other 106 | FHIR versions. -------------------------------------------------------------------------------- /loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpocore/io/Loinc2HpoAnnotationParser.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.io; 2 | 3 | 4 | import org.monarchinitiative.loinc2hpocore.annotation.*; 5 | import org.monarchinitiative.loinc2hpocore.codesystems.ShortCode; 6 | import org.monarchinitiative.loinc2hpocore.exception.Loinc2HpoRuntimeException; 7 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import java.io.*; 12 | import java.util.*; 13 | import java.util.function.Function; 14 | import java.util.stream.Collectors; 15 | 16 | /** 17 | * This class is responsible for parsing the {@code loinc2hpo-annotations.tsv} file that is available 18 | * at https://github.com/TheJacksonLaboratory/loinc2hpoAnnotation 19 | * @author Peter Robinson, Aaron Zhang 20 | */ 21 | public class Loinc2HpoAnnotationParser { 22 | private final static Logger LOGGER = LoggerFactory.getLogger(Loinc2HpoAnnotationParser.class); 23 | 24 | private final List entries; 25 | 26 | public Loinc2HpoAnnotationParser(String path) { 27 | entries = importAnnotations(path); 28 | } 29 | 30 | private List importAnnotations(String path) { 31 | List entries = new ArrayList<>(); 32 | try (BufferedReader reader = new BufferedReader(new FileReader(path))){ 33 | String line = reader.readLine(); // header 34 | if (!line.equals(String.join("\t", Loinc2HpoAnnotation.headerFields))){ 35 | String msg = String.format("Annotation header (%s) does not match expected fields (%s)", 36 | line, String.join("\t", Loinc2HpoAnnotation.headerFields)); 37 | throw new Loinc2HpoRuntimeException(msg); 38 | } 39 | while ((line = reader.readLine()) != null){ 40 | entries.add(Loinc2HpoAnnotation.fromAnnotationLine(line)); 41 | } 42 | } catch (IOException e) { 43 | throw new Loinc2HpoRuntimeException(e.getMessage()); 44 | } 45 | return entries; 46 | } 47 | 48 | /* Export the list of annotations to file. Intended for use by the loinc2hpominer tool. */ 49 | public static void exportToTsv(List annotations, String path) throws IOException { 50 | File outfile = new File(path); 51 | LOGGER.info("Writing annotation data to {}", outfile.getAbsoluteFile()); 52 | Collections.sort(annotations); 53 | BufferedWriter bw = new BufferedWriter(new FileWriter(outfile)); 54 | String header = String.join("\t", Loinc2HpoAnnotation.headerFields); 55 | bw.write(header + "\n"); 56 | for (var ann : annotations) { 57 | bw.write(ann.toTsv() + "\n"); 58 | } 59 | bw.close(); 60 | } 61 | 62 | 63 | 64 | public List getEntries() { 65 | return entries; 66 | } 67 | 68 | public Map loincToHpoAnnotationMap() { 69 | Map> result = entries.stream() 70 | .collect(Collectors.groupingBy(Loinc2HpoAnnotation::getLoincId, 71 | Collectors.mapping(Function.identity(), 72 | Collectors.toList()))); 73 | Map outcomesMap = new TreeMap<>(); 74 | for (var e : result.entrySet()) { 75 | LoincId loincId = e.getKey(); 76 | List outcomes = e.getValue(); 77 | LoincAnnotation lannot = Loinc2HpoAnnotation.outcomes2LoincAnnotation(outcomes); 78 | outcomesMap.put(loincId, lannot); 79 | } 80 | return outcomesMap; 81 | } 82 | 83 | 84 | 85 | 86 | public static List load(String path) { 87 | Loinc2HpoAnnotationParser parser = new Loinc2HpoAnnotationParser(path); 88 | return parser.getEntries(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | loinc2hpo Documentation 2 | ======================= 3 | 4 | Loinc2hpo is a Java library designed to convert the findings of laboratory tests to HPO codes. 5 | For instance, if the test is `LOINC 26515-7 Platelets [#/volume] in Blood `_ 6 | and the outcome of the test is an abnormally low value, then we can infer the 7 | `Human Phenotype Ontology (HPO) `_ term 8 | `Thrombocytopenia HP:0001873 `_. 9 | 10 | The goal of this library is to encode EHR (Electronic Health Record) laboratory 11 | data using HPO terms to extend the kinds of analysis that can be performed. 12 | Laboratory results can be leveraged as phenotypic features for analysis. 13 | 14 | .. image:: images/mission.png 15 | :align: center 16 | :scale: 60 % 17 | 18 | The library currently has three modules. 19 | 20 | loinc2hpo-core 21 | ============== 22 | 23 | This library contains the core functionality. It imports the annotation file from 24 | the `loinc2hpoAnnotation `_ repository 25 | (loinc2hpo-annotations.tsv), and for any combination of LOINC Id (laboratory test) and 26 | outcome, it finds the appropriate HPO term if one exists. We use set of internal codes 27 | to represent lab outcomes. 28 | 29 | +---------+------------------------------------------------------------------------------+ 30 | | Code | Explanation | 31 | +=========+==============================================================================+ 32 | | L | Low (below normal range). Used for quantitative tests (Qn). | 33 | +---------+------------------------------------------------------------------------------+ 34 | | H | High (above normal range). Used for quantitative tests (Qn). | 35 | +---------+------------------------------------------------------------------------------+ 36 | | N | Normal (within normal range). Used for quantitative tests (Qn). | 37 | +---------+------------------------------------------------------------------------------+ 38 | | NEG | Negative (not present, a normal result). Used for ordinal tests (Ord) | 39 | +---------+------------------------------------------------------------------------------+ 40 | | POS | Positive (present, an abnormal result). Used for ordinal tests (Ord) | 41 | +---------+------------------------------------------------------------------------------+ 42 | | NOM | Nominal (an abnormal result). Used for nominal tests (Nom) | 43 | +---------+------------------------------------------------------------------------------+ 44 | 45 | loinc2hpo-fhir 46 | ============== 47 | 48 | This library provides an interface that extracts LOINC-encoded data from 49 | `FHIR - Fast Healthcare Interoperability Resources `_ data. Specifically, 50 | it provides an interface that takes a FHIR `Observation `_ 51 | and attempts to extract a LOINC code and an outcome; if successful, these are passed to the 52 | core loinc2hpo module to get the corresponding HPO term. 53 | 54 | loinc2hpo-cli 55 | ============= 56 | 57 | This is a command-line interface tool that can be used to obtain descriptive statistics or 58 | perform quality control of the input files. 59 | 60 | Contents 61 | ======== 62 | 63 | .. toctree:: 64 | :maxdepth: 1 65 | 66 | intro_to_LOINC 67 | intro_to_FHIR 68 | FHIR_mapping.rst 69 | getting_started 70 | 71 | GitHub repo 72 | ----------- 73 | The source code of Loinc2hpo can be found at GitHub: 74 | https://github.com/monarch-initiative/loinc2hpo 75 | 76 | Contact 77 | ------- 78 | 79 | Peter Robinson 80 | peter.robinson@jax.org 81 | 82 | `The Jackson Laboratory `_ 83 | 10 Discovery Drive 84 | Farmington, CT 85 | USA 86 | 87 | Xingmin Aaron Zhang 88 | kingmanzhang@gmail.com 89 | 90 | Curation 91 | -------- 92 | 93 | We have developed a JavaFX application to curate loinc2hpo data. This app is not needed to 94 | use the library, but may be of interest to potential contributors: https://github.com/pnrobinson/loinc2hpoMiner. 95 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2017, The Jackson Laboratory 4 | All rights reserved. 5 | 6 | This product includes all or a portion of the LOINC Table, LOINC Table Core, LOINC Panels and Forms File, LOINC Answer File, LOINC Part File, LOINC Group File, LOINC Document Ontology File, LOINC Hierarchies, LOINC Linguistic Variants File, LOINC/RSNA Radiology Playbook, LOINC/IEEE Medical Device Code Mapping Table, and LOINC Display Name File or is derived from one or more of the foregoing, subject to a license from Regenstrief Institute, Inc. Your use of the LOINC Table, LOINC Table Core, LOINC Panels and Forms File, LOINC Answer File, LOINC Part File, LOINC Group File, LOINC Document Ontology File, LOINC Hierarchies, LOINC Linguistic Variants File, LOINC/RSNA Radiology Playbook, LOINC/IEEE Medical Device Code Mapping Table, and LOINC Display Name File also is subject to this license, a copy of which is available https://loinc.org/license/. The current complete LOINC Table, LOINC Table Core, LOINC Panels and Forms File, LOINC Answer File, LOINC Part File, LOINC Group File, LOINC Document Ontology File, LOINC Hierarchies, LOINC Linguistic Variants File, LOINC/RSNA Radiology Playbook, LOINC/IEEE Medical Device Code Mapping Table, and LOINC Display Name File are available for download at http://loinc.org. The LOINC Table and LOINC codes are copyright © 1995-2018, Regenstrief Institute, Inc. and the Logical Observation Identifiers Names and Codes (LOINC) Committee. The LOINC Table, LOINC Table Core, LOINC Panels and Forms File, LOINC Answer File, LOINC Part File, LOINC Group File, LOINC Document Ontology File, LOINC Hierarchies, LOINC Linguistic Variants File, LOINC/RSNA Radiology Playbook, LOINC/IEEE Medical Device Code Mapping Table, and LOINC Display Name File are copyright © 1995-2018, Regenstrief Institute, Inc. All rights reserved. THE LOINC TABLE (IN ALL FORMATS), LOINC TABLE CORE, LOINC PANELS AND FORMS FILE, LOINC ANSWER FILE, LOINC PART FILE, LOINC GROUP FILE, LOINC DOCUMENT ONTOLOGY FILE, LOINC HIERARCHIES, LOINC LINGUISTIC VARIANTS FILE, LOINC/RSNA RADIOLOGY PLAYBOOK, LOINC/IEEE MEDICAL DEVICE CODE MAPPING TABLE, AND LOINC DISPLAY NAME FILE ARE PROVIDED "AS IS." ANY EXPRESS OR IMPLIED WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. LOINC® is a registered United States trademark of Regenstrief Institute, Inc. A small portion of the LOINC Table may include content (e.g., survey instruments) that is subject to copyrights owned by third parties. Such content has been mapped to LOINC terms under applicable copyright and terms of use. Notice of such third-party copyright and license terms would need to be included if such content is included. 7 | 8 | Redistribution and use in source and binary forms, with or without 9 | modification, are permitted provided that the following conditions are met: 10 | 11 | * Redistributions of source code must retain the above copyright notice, this 12 | list of conditions and the following disclaimer. 13 | 14 | * Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | * Neither the name of the copyright holder nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 23 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 24 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 26 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 27 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 28 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 30 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 31 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | -------------------------------------------------------------------------------- /docs/intro_to_LOINC.rst: -------------------------------------------------------------------------------- 1 | Introduction to LOINC 2 | ===================== 3 | 4 | `LOINC `_ (Logical Observation Identifiers Names and Codes) 5 | provides a set of universal names and ID codes for identifying laboratory and clinical 6 | test results. 7 | 8 | LOINC currently provides ~92,000 entries that define the names and IDs of laboratory tests. 9 | The following shows three examples of LOINC codes: 10 | 11 | .. image:: images/loinc_examples.png 12 | 13 | Each LOINC entry represents a laboratory test. 14 | 15 | Parts of LOINC entry 16 | -------------------- 17 | 18 | - ``LOINC``: unique identifier 19 | - ``Name``: structured term name 20 | - ``Component``: defines the analyte in the test 21 | - ``Property``: defines "kinds of quantities". It can be divided into five several categories, mass, substance, catalytic activity, and number or counts. Each category is further divided into subclasses, for example MCnc or "mass concentration" is a subclass of "mass" property, while ``NCnc`` or "number of concentration (count/vol)" and ``Naric`` or "number aeric (number per area)" are subclasses of counts. 22 | - ``Time``: defines whether a measurement was made at a moment, or aggregated from a series of physiologic states. The three examples are all ``PT`` ("points"), meaning that they are measurements at a single time point. As an example, a test on "daily urine amount" will be labeled as ``24H`` ("24 hours"). 23 | - ``Aspect``: defines a modifier for a measurement over a duration. For example, "8H^max heart rate" means the "max" heart rate measured during an 8-hour period. Min, max, first, last, mean typically appear here. 24 | - ``System``: can be considered as the specimen for the test, such as "serum", "blood", "urine", "cerebrospinal fluid" etc. 25 | - ``Scale``" defines the scale of the measurement. Scale is the most important information for our application. The following table summarizes possible values of ``scale``. 26 | - ``Method``: defines the method used for the measurement. 27 | 28 | 29 | Table 1: LOINC Scale Types 30 | 31 | +----------------+------+-------------------------------------------------------------------------------------+ 32 | | Scale Type | Abbr.| Description | 33 | +================+======+=====================================================================================+ 34 | | Quantitative | Qn | The result of the test is a numeric value that relates to a continuous numeric | 35 | | | | scale. | 36 | +----------------+------+-------------------------------------------------------------------------------------+ 37 | | Ordinal | Ord | Ordered categorical responses, e.g., positive, negative; | 38 | +----------------+------+-------------------------------------------------------------------------------------+ 39 | | Quantitative | OrdQn| Test can be reported as either Ord or Qn. | 40 | +----------------+------+-------------------------------------------------------------------------------------+ 41 | | Nominal | Nom | Nominal or categorical responses that do not have a natural ordering. | 42 | +----------------+------+-------------------------------------------------------------------------------------+ 43 | | Narrative | Nar | Text narrative. | 44 | +----------------+------+-------------------------------------------------------------------------------------+ 45 | | “Multi” | Multi| Many separate results structured as one text “glob” | 46 | +----------------+------+-------------------------------------------------------------------------------------+ 47 | | Document | Doc | A document that could be in many formats (XML, narrative, etc.) | 48 | +----------------+------+-------------------------------------------------------------------------------------+ 49 | | Set | Set | Used for clinical attachments | 50 | +----------------+------+-------------------------------------------------------------------------------------+ 51 | 52 | 53 | 54 | ``Qn``, ``Ord`` and ``Nom`` are the three most frequently used LOINC codes, 55 | accounting for about 99% of data. ``Qn`` typically makes up about 80% of cases. 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # loinc2hpo Changelog 2 | 3 | 4 | 5 | ## current 6 | * v 1.1.8 7 | 8 | ## v1.1.8 9 | * Update to phenol 1.5.0 10 | 11 | ### loinc2hpo-core 12 | * Adding enums for FHIR observation codes 13 | 14 | ## v0.0.2 15 | 16 | ### loinc2hpogui 17 | 18 | * Added FHIR to HPO conversion dialog 19 | 20 | ### loinc2hpo-core 21 | 22 | * added FHIR parsing 23 | 24 | ## v1.0.1 25 | This is the first official release. Many features are added. 26 | 27 | ### loinc2hpogui 28 | Multiple features are added. This will be the baseline for future tracking. 29 | 30 | ### loinc2hpo-core 31 | 32 | * The core change is completely switching to FHIR to parse `observation` and `patient` resources. 33 | 34 | * Redesigned the annotation class. 35 | - Use Code (system/namespace, code) to ensure uniqueness 36 | - An HPO term for a coded result is wrapped in a class that also indicate whether the term should be negated. 37 | - A complete annotation for a Loinc code contains many `Code` - `(HPO term, isNegated)` list. 38 | 39 | * Redesigned the logic process from observation to HPO term. 40 | - The app first looks at whether the observation has an interpretation field. 41 | - If it does, it will first try to find an annotation for the interpretation code directly; 42 | - if it fails, it will try to convert convert the interpretation code to the internal code and then find the corresponding HPO term. 43 | 44 | - If the app fails the last step, it will try to use the raw value and interpret it with the reference ranges. 45 | 46 | ## v1.0.2 47 | 48 | * Build Jar with all dependencies with maven-assembly-plugin 49 | 50 | * Add META-INF/services to Core module because it appears that is what Jar requires 51 | 52 | ## v1.0.3 53 | 54 | * Refactor pom files. 55 | 56 | * Refactor gitignore file. 57 | 58 | ## v1.0.4 59 | 60 | * Allow adding multiple labels to Github issues 61 | 62 | * Disable the function to clear annotation fields during manual query 63 | 64 | * Loinc entries change color if they have been annotated 65 | 66 | * Add tooltips to HPO listview and treeview 67 | 68 | * Allow user to switch to previously selected Loinc list 69 | 70 | * Allow user to categorize Loinc entries 71 | 72 | ## v1.1.0 73 | 74 | * New develop version 75 | 76 | Additional changes for this version 77 | 78 | * Change menu `Edit` to `Configuration` 79 | - [ ] update tutorial 80 | 81 | * Create new features that allow user to manipulate a session 82 | 83 | * Automatically retrieve information from auto-saved data for last session 84 | 85 | ## v1.1.1 86 | 87 | * Session data now only saves terms for low, intermediate, and high value, instead for all 6 internal codes 88 | 89 | * Basic and Advanced data are stored separately 90 | 91 | ## v1.1.2 92 | 93 | * Session data now saves to a universal TSV file 94 | 95 | * Restrict internal mappings 96 | 97 | Qn will not be mapped to "Presence" or "Absence" and Ord (of "Presence" type) will not be mapped to "high", "low", "normal" 98 | 99 | * Internal codes changed match FHIR 100 | 101 | "system" is renamed to "FHIR"; 102 | Code for "presence" changed from "P" to "POS", code for "not presence" changed from "NP" to "NEG" to be consistent with FHIR 103 | 104 | * Show version in "About" message 105 | 106 | * Refactor to remove deprecated classes 107 | 108 | * Allow user to specify the path to hp OWL and Obo 109 | 110 | ## v1.1.3 111 | 112 | * Add function to simulate patient data (patient and observation resources) 113 | 114 | * Add function to allow uploading simulated data to hapi-fhir server 115 | 116 | * Add function to allow downloading patient data from fhir server 117 | 118 | ## v1.1.4 119 | 120 | * Add function to restart the app when necessary 121 | 122 | * Prevent app from crashing when user's local HPO is outdated 123 | 124 | * Added classes to appTempData patient phenopacket (phenotype only) 125 | * Refactored to use phenol 1.0.0 126 | 127 | ## v1.1.5 128 | 129 | * Add feature to copy annotation for one LOINC and paste to multiple selections of similar LOINC 130 | 131 | * Add function to annotate LOINC panels 132 | 133 | * Add function to convert FHIR messages for LOINC panels 134 | 135 | * Add a LOINC list for tests with "unspecified specimen". Messages on those should be not converted to HPO. 136 | 137 | ## v1.1.6 138 | 139 | * Add feature to allow easy addition of LOINC lists: allow user to change colors of LOINC lists 140 | 141 | * Added HPO parser for owl format. 142 | 143 | * Refactored to use phenol 1.2.6 144 | 145 | ## v1.1.7 146 | 147 | * Add algorithms to appTempData patients 148 | 149 | -------------------------------------------------------------------------------- /loinc2hpo-core/src/test/resources/LoincTableCoreTiny.csv: -------------------------------------------------------------------------------- 1 | "LOINC_NUM","COMPONENT","PROPERTY","TIME_ASPCT","SYSTEM","SCALE_TYP","METHOD_TYP","CLASS","CLASSTYPE","LONG_COMMON_NAME","SHORTNAME","EXTERNAL_COPYRIGHT_NOTICE","STATUS","VersionFirstReleased","VersionLastChanged" 2 | "10000-8","R wave duration.lead AVR","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead AVR","R wave dur L-AVR","","ACTIVE","1.0i","2.48" 3 | "10001-6","R wave duration.lead I","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead I","R wave dur L-I","","ACTIVE","1.0i","2.48" 4 | "10002-4","R wave duration.lead II","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead II","R wave dur L-II","","ACTIVE","1.0i","2.48" 5 | "10003-2","R wave duration.lead III","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead III","R wave dur L-III","","ACTIVE","1.0i","2.48" 6 | "10004-0","R wave duration.lead V1","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead V1","R wave dur L-V1","","ACTIVE","1.0i","2.48" 7 | "10005-7","R wave duration.lead V2","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead V2","R wave dur L-V2","","ACTIVE","1.0i","2.48" 8 | "10006-5","R wave duration.lead V3","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead V3","R wave dur L-V3","","ACTIVE","1.0i","2.48" 9 | "10007-3","R wave duration.lead V4","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead V4","R wave dur L-V4","","ACTIVE","1.0i","2.48" 10 | "10008-1","R wave duration.lead V5","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead V5","R wave dur L-V5","","ACTIVE","1.0i","2.48" 11 | "1000-9","DBG Ab","PrThr","Pt","Ser/Plas^BPU","Ord","","BLDBK","1","DBG Ab [Presence] in Serum or Plasma from Blood product unit","DBG Ab SerPl BPU Ql","","ACTIVE","1.0","2.56" 12 | "10009-9","R wave duration.lead V6","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead V6","R wave dur L-V6","","ACTIVE","1.0i","2.48" 13 | "10010-7","R' wave amplitude.lead AVF","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead AVF","R' wave Amp L-AVF","","ACTIVE","1.0i","2.48" 14 | "10011-5","R' wave amplitude.lead AVL","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead AVL","R' wave Amp L-AVL","","ACTIVE","1.0i","2.48" 15 | "10012-3","R' wave amplitude.lead AVR","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead AVR","R' wave Amp L-AVR","","ACTIVE","1.0i","2.48" 16 | "10013-1","R' wave amplitude.lead I","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead I","R' wave Amp L-I","","ACTIVE","1.0i","2.48" 17 | "10014-9","R' wave amplitude.lead II","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead II","R' wave Amp L-II","","ACTIVE","1.0i","2.48" 18 | "10015-6","R' wave amplitude.lead III","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead III","R' wave Amp L-III","","ACTIVE","1.0i","2.48" 19 | "10016-4","R' wave amplitude.lead V1","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead V1","R' wave Amp L-V1","","ACTIVE","1.0i","2.48" 20 | "1001-7","DBG Ab","PrThr","Pt","Ser/Plas^Donor","Ord","","BLDBK","1","DBG Ab [Presence] in Serum or Plasma from Donor","DBG Ab SerPl Donr Ql","","ACTIVE","1.0","2.56" 21 | "10017-2","R' wave amplitude.lead V2","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead V2","R' wave Amp L-V2","","ACTIVE","1.0i","2.48" 22 | "10018-0","R' wave amplitude.lead V3","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead V3","R' wave Amp L-V3","","ACTIVE","1.0i","2.48" 23 | "10019-8","R' wave amplitude.lead V4","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead V4","R' wave Amp L-V4","","ACTIVE","1.0i","2.48" 24 | "10020-6","R' wave amplitude.lead V5","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead V5","R' wave Amp L-V5","","ACTIVE","1.0i","2.48" 25 | "10021-4","R' wave amplitude.lead V6","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead V6","R' wave Amp L-V6","","ACTIVE","1.0i","2.48" 26 | "10022-2","R' wave duration.lead AVF","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave duration in lead AVF","R' wave dur L-AVF","","ACTIVE","1.0i","2.48" 27 | "10023-0","R' wave duration.lead AVL","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave duration in lead AVL","R' wave dur L-AVL","","ACTIVE","1.0i","2.48" 28 | "10024-8","R' wave duration.lead AVR","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave duration in lead AVR","R' wave dur L-AVR","","ACTIVE","1.0i","2.48" 29 | "789-8","Erythrocytes [#/volume] in Blood by Automated count","NCnc","Pt","Bld","Qn","Automated count","2","Erythrocytes [#/volume] in Blood by Automated count","RBC # Bld Auto","","ACTIVE","1.0i","2.48" -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/resources/LoincTableCoreTiny.csv: -------------------------------------------------------------------------------- 1 | "LOINC_NUM","COMPONENT","PROPERTY","TIME_ASPCT","SYSTEM","SCALE_TYP","METHOD_TYP","CLASS","CLASSTYPE","LONG_COMMON_NAME","SHORTNAME","EXTERNAL_COPYRIGHT_NOTICE","STATUS","VersionFirstReleased","VersionLastChanged" 2 | "10000-8","R wave duration.lead AVR","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead AVR","R wave dur L-AVR","","ACTIVE","1.0i","2.48" 3 | "10001-6","R wave duration.lead I","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead I","R wave dur L-I","","ACTIVE","1.0i","2.48" 4 | "10002-4","R wave duration.lead II","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead II","R wave dur L-II","","ACTIVE","1.0i","2.48" 5 | "10003-2","R wave duration.lead III","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead III","R wave dur L-III","","ACTIVE","1.0i","2.48" 6 | "10004-0","R wave duration.lead V1","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead V1","R wave dur L-V1","","ACTIVE","1.0i","2.48" 7 | "10005-7","R wave duration.lead V2","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead V2","R wave dur L-V2","","ACTIVE","1.0i","2.48" 8 | "10006-5","R wave duration.lead V3","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead V3","R wave dur L-V3","","ACTIVE","1.0i","2.48" 9 | "10007-3","R wave duration.lead V4","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead V4","R wave dur L-V4","","ACTIVE","1.0i","2.48" 10 | "10008-1","R wave duration.lead V5","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead V5","R wave dur L-V5","","ACTIVE","1.0i","2.48" 11 | "1000-9","DBG Ab","PrThr","Pt","Ser/Plas^BPU","Ord","","BLDBK","1","DBG Ab [Presence] in Serum or Plasma from Blood product unit","DBG Ab SerPl BPU Ql","","ACTIVE","1.0","2.56" 12 | "10009-9","R wave duration.lead V6","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead V6","R wave dur L-V6","","ACTIVE","1.0i","2.48" 13 | "10010-7","R' wave amplitude.lead AVF","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead AVF","R' wave Amp L-AVF","","ACTIVE","1.0i","2.48" 14 | "10011-5","R' wave amplitude.lead AVL","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead AVL","R' wave Amp L-AVL","","ACTIVE","1.0i","2.48" 15 | "10012-3","R' wave amplitude.lead AVR","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead AVR","R' wave Amp L-AVR","","ACTIVE","1.0i","2.48" 16 | "10013-1","R' wave amplitude.lead I","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead I","R' wave Amp L-I","","ACTIVE","1.0i","2.48" 17 | "10014-9","R' wave amplitude.lead II","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead II","R' wave Amp L-II","","ACTIVE","1.0i","2.48" 18 | "10015-6","R' wave amplitude.lead III","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead III","R' wave Amp L-III","","ACTIVE","1.0i","2.48" 19 | "10016-4","R' wave amplitude.lead V1","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead V1","R' wave Amp L-V1","","ACTIVE","1.0i","2.48" 20 | "1001-7","DBG Ab","PrThr","Pt","Ser/Plas^Donor","Ord","","BLDBK","1","DBG Ab [Presence] in Serum or Plasma from Donor","DBG Ab SerPl Donr Ql","","ACTIVE","1.0","2.56" 21 | "10017-2","R' wave amplitude.lead V2","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead V2","R' wave Amp L-V2","","ACTIVE","1.0i","2.48" 22 | "10018-0","R' wave amplitude.lead V3","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead V3","R' wave Amp L-V3","","ACTIVE","1.0i","2.48" 23 | "10019-8","R' wave amplitude.lead V4","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead V4","R' wave Amp L-V4","","ACTIVE","1.0i","2.48" 24 | "10020-6","R' wave amplitude.lead V5","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead V5","R' wave Amp L-V5","","ACTIVE","1.0i","2.48" 25 | "10021-4","R' wave amplitude.lead V6","Elpot","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave amplitude in lead V6","R' wave Amp L-V6","","ACTIVE","1.0i","2.48" 26 | "10022-2","R' wave duration.lead AVF","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave duration in lead AVF","R' wave dur L-AVF","","ACTIVE","1.0i","2.48" 27 | "10023-0","R' wave duration.lead AVL","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave duration in lead AVL","R' wave dur L-AVL","","ACTIVE","1.0i","2.48" 28 | "10024-8","R' wave duration.lead AVR","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R' wave duration in lead AVR","R' wave dur L-AVR","","ACTIVE","1.0i","2.48" 29 | "789-8","Erythrocytes [#/volume] in Blood by Automated count","NCnc","Pt","Bld","Qn","Automated count","2","Erythrocytes [#/volume] in Blood by Automated count","RBC # Bld Auto","","ACTIVE","1.0i","2.48" -------------------------------------------------------------------------------- /loinc2hpo-core/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | loinc2hpo 7 | org.monarchinitiative 8 | 2.0.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | loinc2hpo-core 13 | jar 14 | 15 | Loinc2Hpo Core 16 | A library for mapping LOINC codes to HPO terms 17 | 18 | 19 | UTF-8 20 | 21 | 22 | 23 | 24 | 25 | org.monarchinitiative.phenol 26 | phenol-core 27 | 28 | 29 | org.monarchinitiative.phenol 30 | phenol-io 31 | 32 | 33 | org.junit.jupiter 34 | junit-jupiter-engine 35 | test 36 | 37 | 38 | org.slf4j 39 | slf4j-api 40 | 41 | 42 | 43 | 44 | loinc2hpo-core 45 | 46 | 47 | src/main/resources 48 | 50 | true 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-compiler-plugin 57 | 3.7.0 58 | 59 | ${java.version} 60 | ${java.version} 61 | 62 | 63 | 64 | 65 | org.apache.maven.plugins 66 | maven-surefire-plugin 67 | 68 | 69 | 70 | 71 | maven-resources-plugin 72 | 3.2.0 73 | 74 | 75 | copy-resources 76 | validate 77 | 78 | copy-resources 79 | 80 | 81 | ${project.build.directory}/resources 82 | 83 | 84 | src/main/resources 85 | 87 | true 88 | 89 | application.properties 90 | example.settings 91 | test.settings 92 | log4j2.xml 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | org.apache.maven.plugins 104 | maven-jar-plugin 105 | 3.2.0 106 | 107 | 108 | 109 | true 110 | lib/ 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /loinc2hpo-fhir/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | loinc2hpo 7 | org.monarchinitiative 8 | 2.0.0-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | loinc2hpo-fhir 13 | jar 14 | 15 | Loinc2Hpo FHIR 16 | FHIR interface to loinc2hpo 17 | 18 | 19 | UTF-8 20 | 6.4.0 21 | 22 | 23 | 24 | 25 | 26 | org.monarchinitiative 27 | loinc2hpo-core 28 | ${project.parent.version} 29 | 30 | 31 | org.slf4j 32 | slf4j-api 33 | 34 | 35 | 36 | ca.uhn.hapi.fhir 37 | hapi-fhir-base 38 | ${hapi-fhir.version} 39 | 40 | 41 | ca.uhn.hapi.fhir 42 | hapi-fhir-structures-dstu3 43 | ${hapi-fhir.version} 44 | 45 | 46 | ca.uhn.hapi.fhir 47 | hapi-fhir-structures-r4 48 | ${hapi-fhir.version} 49 | 50 | 51 | ca.uhn.hapi.fhir 52 | hapi-fhir-structures-r5 53 | ${hapi-fhir.version} 54 | 55 | 56 | 57 | 58 | org.junit.jupiter 59 | junit-jupiter-engine 60 | test 61 | 62 | 63 | 64 | 65 | loinc2hpo-fhir 66 | 67 | 68 | src/main/resources 69 | 70 | true 71 | 72 | 73 | 74 | 75 | org.apache.maven.plugins 76 | maven-surefire-plugin 77 | 78 | 79 | org.apache.maven.plugins 80 | maven-compiler-plugin 81 | 82 | 83 | 84 | 85 | maven-resources-plugin 86 | 3.2.0 87 | 88 | 89 | copy-resources 90 | validate 91 | 92 | copy-resources 93 | 94 | 95 | ${project.build.directory}/resources 96 | 97 | 98 | src/main/resources 99 | 101 | true 102 | 103 | application.properties 104 | example.settings 105 | test.settings 106 | log4j2.xml 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | org.apache.maven.plugins 118 | maven-jar-plugin 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # loinc2hpo documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Mar 13 16:28:40 2018. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = [] 35 | 36 | # Add any paths that contain templates here, relative to this directory. 37 | templates_path = ['_templates'] 38 | 39 | # The suffix(es) of source filenames. 40 | # You can specify multiple suffix as a list of string: 41 | # 42 | # source_suffix = ['.rst', '.md'] 43 | source_suffix = '.rst' 44 | 45 | # The master toctree document. 46 | master_doc = 'index' 47 | 48 | # General information about the project. 49 | project = 'loinc2hpo' 50 | copyright = '2021, Peter Robinson, Aaron Zhang' 51 | author = 'Peter Robinson, Aaron Zhang' 52 | 53 | # The version info for the project you're documenting, acts as replacement for 54 | # |version| and |release|, also used in various other places throughout the 55 | # built documents. 56 | # 57 | # The short X.Y version. 58 | version = '1.7.0' 59 | # The full version, including alpha/beta/rc tags. 60 | release = '1.7.0' 61 | 62 | # The language for content autogenerated by Sphinx. Refer to documentation 63 | # for a list of supported languages. 64 | # 65 | # This is also used if you do content translation via gettext catalogs. 66 | # Usually you set "language" from the command line for these cases. 67 | language = None 68 | 69 | # List of patterns, relative to source directory, that match files and 70 | # directories to ignore when looking for source files. 71 | # This patterns also effect to html_static_path and html_extra_path 72 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'LICENSE.rst'] 73 | 74 | # The name of the Pygments (syntax highlighting) style to use. 75 | pygments_style = 'sphinx' 76 | 77 | # If true, `todo` and `todoList` produce output, else they produce nothing. 78 | todo_include_todos = False 79 | 80 | 81 | # -- Options for HTML output ---------------------------------------------- 82 | 83 | # The theme to use for HTML and HTML Help pages. See the documentation for 84 | # a list of builtin themes. 85 | # 86 | html_theme = "sphinx_rtd_theme" 87 | 88 | 89 | # Theme options are theme-specific and customize the look and feel of a theme 90 | # further. For a list of options available for each theme, see the 91 | # documentation. 92 | # 93 | # html_theme_options = {} 94 | 95 | # Add any paths that contain custom static files (such as style sheets) here, 96 | # relative to this directory. They are copied after the builtin static files, 97 | # so a file named "default.css" will overwrite the builtin "default.css". 98 | html_static_path = ['_static'] 99 | # File list in _static 100 | html_css_files = ['loinc2hpo.css'] 101 | 102 | 103 | # -- Options for HTMLHelp output ------------------------------------------ 104 | 105 | # Output file base name for HTML help builder. 106 | htmlhelp_basename = 'loinc2hpodoc' 107 | 108 | 109 | # -- Options for LaTeX output --------------------------------------------- 110 | 111 | latex_elements = { 112 | # The paper size ('letterpaper' or 'a4paper'). 113 | # 114 | # 'papersize': 'letterpaper', 115 | 116 | # The font size ('10pt', '11pt' or '12pt'). 117 | # 118 | # 'pointsize': '10pt', 119 | 120 | # Additional stuff for the LaTeX preamble. 121 | # 122 | # 'preamble': '', 123 | 124 | # Latex figure (float) alignment 125 | # 126 | # 'figure_align': 'htbp', 127 | } 128 | 129 | # Grouping the document tree into LaTeX files. List of tuples 130 | # (source start file, target name, title, 131 | # author, documentclass [howto, manual, or own class]). 132 | latex_documents = [ 133 | (master_doc, 'loinc2hpo.tex', 'loinc2hpo Documentation', 134 | 'Peter Robinson, Aaron Zhang', 'manual'), 135 | ] 136 | 137 | 138 | # -- Options for manual page output --------------------------------------- 139 | 140 | # One entry per manual page. List of tuples 141 | # (source start file, name, description, authors, manual section). 142 | man_pages = [ 143 | (master_doc, 'loinc2hpo', 'loinc2hpo Documentation', 144 | [author], 1) 145 | ] 146 | 147 | 148 | # -- Options for Texinfo output ------------------------------------------- 149 | 150 | # Grouping the document tree into Texinfo files. List of tuples 151 | # (source start file, target name, title, author, 152 | # dir menu entry, description, category) 153 | texinfo_documents = [ 154 | (master_doc, 'loinc2hpo', 'loinc2hpo Documentation', 155 | author, 'loinc2hpo', 'One line description of project.', 156 | 'Miscellaneous'), 157 | ] 158 | 159 | -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/main/java/org/monarchinitiative/loinc2hpofhir/fhir2hpo/ObservationDtu3.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpofhir.fhir2hpo; 2 | 3 | import org.apache.commons.lang.NotImplementedException; 4 | import org.hl7.fhir.dstu3.model.CodeableConcept; 5 | import org.hl7.fhir.dstu3.model.Coding; 6 | 7 | import org.hl7.fhir.dstu3.model.Observation; 8 | import org.monarchinitiative.loinc2hpocore.codesystems.Outcome; 9 | import org.monarchinitiative.loinc2hpocore.codesystems.ShortCode; 10 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.util.List; 15 | import java.util.Optional; 16 | import java.util.stream.Collectors; 17 | 18 | public class ObservationDtu3 implements Uberobservation { 19 | private static final Logger LOGGER = LoggerFactory.getLogger(ObservationDtu3.class); 20 | private final org.hl7.fhir.dstu3.model.Observation observation; 21 | 22 | public ObservationDtu3(org.hl7.fhir.dstu3.model.Observation dstu3Observation) { 23 | this.observation = dstu3Observation; 24 | } 25 | 26 | @Override 27 | public Optional getLoincId() { 28 | LoincId loincId; 29 | for (Coding coding : observation.getCode().getCoding()) { 30 | if (coding.getSystem().equals("http://loinc.org")) { 31 | loincId = new LoincId(coding.getCode()); 32 | return Optional.of(loincId); 33 | } 34 | } 35 | return Optional.empty(); 36 | } 37 | 38 | private Outcome getOutcome(ShortCode code, Observation observation) { 39 | if (code.equals(ShortCode.NOM)) { 40 | throw new NotImplementedException("TODO"); 41 | } 42 | switch (code) { 43 | case H: return Outcome.HIGH(); 44 | case L: return Outcome.LOW(); 45 | case N: return Outcome.NORMAL(); 46 | case NEG: return Outcome.NEGATIVE(); 47 | case POS: return Outcome.POSITIVE(); 48 | default: 49 | throw new NotImplementedException("TODO"); 50 | } 51 | } 52 | 53 | 54 | @Override 55 | public Optional getOutcome() { 56 | if (observation.hasInterpretation()){ 57 | List codes = this.observation.getInterpretation().getCoding(). 58 | stream().map(Coding::getCode).distinct(). 59 | collect(Collectors.toList()); 60 | if (codes.size() > 1) { 61 | LOGGER.error("Multiple interpretation codes returned"); 62 | return Optional.empty(); 63 | } 64 | ShortCode code = ShortCode.fromShortCode(codes.get(0)); 65 | Outcome outcome = getOutcome(code, observation); 66 | return Optional.of(outcome); 67 | } else if (observation.hasValueCodeableConcept()){ 68 | return getOutcomeFromCodedValue(); 69 | } else if (observation.hasValueQuantity()){ 70 | return getOutcomeFromValueQuantity(); 71 | } else { 72 | LOGGER.error("Unable to handle observation {}", observation); 73 | return Optional.empty(); 74 | } 75 | } 76 | 77 | 78 | Optional getOutcomeFromCodedValue() { 79 | CodeableConcept codeableConcept = this.observation.getValueCodeableConcept(); 80 | if (codeableConcept == null) { // should never happen 81 | LOGGER.error("Codable concept null in getOutcomeFromCodedValue"); 82 | } 83 | List codings = codeableConcept != null ? codeableConcept.getCoding() : List.of(); 84 | for (Coding coding : codings) { 85 | String code = coding.getCode(); 86 | String system = coding.getSystem(); 87 | String display = coding.getDisplay(); 88 | String outcomeString = code + ":" + system + ":" + display; 89 | Outcome outcome = Outcome.nominal(outcomeString); 90 | return Optional.of(outcome); 91 | } 92 | return Optional.empty(); 93 | } 94 | 95 | Optional getOutcomeFromValueQuantity() { 96 | List references = 97 | this.observation.getReferenceRange(); 98 | 99 | if (references.size() == 0) { 100 | LOGGER.error("Reference range not found"); 101 | return Optional.empty(); 102 | } 103 | 104 | if (references.size() >= 2){ 105 | LOGGER.error("Reference range had more than two entries"); 106 | return Optional.empty(); 107 | // TODO sometimes this is observed. 108 | //An exception: three reference sizes 109 | //it can happen when there is actually one range but coded in three ranges 110 | //e.g. normal 20-30 111 | //in this case, one range ([20, 30]) is sufficient; 112 | //however, it is written as three ranges: ( , 20) [20, 30] (30, ) 113 | } 114 | Observation.ObservationReferenceRangeComponent targetReference = references.get(0); 115 | double low = targetReference.hasLow() ? 116 | targetReference.getLow().getValue().doubleValue() : Double.MIN_VALUE; 117 | double high = targetReference.hasHigh() ? 118 | targetReference.getHigh().getValue().doubleValue() : Double.MAX_VALUE; 119 | double observed = this.observation.getValueQuantity().getValue().doubleValue(); 120 | if (observed < low) { 121 | return Optional.of(Outcome.LOW()); 122 | } else if (observed > high) { 123 | return Optional.of(Outcome.HIGH()); 124 | } else { 125 | return Optional.of(Outcome.NORMAL()); 126 | } 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/main/java/org/monarchinitiative/loinc2hpofhir/fhir2hpo/ObservationR5.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpofhir.fhir2hpo; 2 | 3 | import org.apache.commons.lang.NotImplementedException; 4 | import org.hl7.fhir.r5.model.Observation; 5 | import org.hl7.fhir.r5.model.CodeableConcept; 6 | import org.hl7.fhir.r5.model.Coding; 7 | import org.monarchinitiative.loinc2hpocore.codesystems.Outcome; 8 | import org.monarchinitiative.loinc2hpocore.codesystems.ShortCode; 9 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import java.util.Collection; 14 | import java.util.List; 15 | import java.util.Optional; 16 | import java.util.stream.Collectors; 17 | 18 | public class ObservationR5 implements Uberobservation { 19 | private static final Logger LOGGER = LoggerFactory.getLogger(ObservationR5.class); 20 | 21 | private final org.hl7.fhir.r5.model.Observation observation; 22 | 23 | 24 | public ObservationR5(org.hl7.fhir.r5.model.Observation observation) { 25 | this.observation = observation; 26 | } 27 | 28 | @Override 29 | public Optional getLoincId() { 30 | LoincId loincId; 31 | for (Coding coding : observation.getCode().getCoding()) { 32 | if (coding.getSystem().equals("http://loinc.org")) { 33 | loincId = new LoincId(coding.getCode()); 34 | return Optional.of(loincId); 35 | } 36 | } 37 | return Optional.empty(); 38 | } 39 | 40 | private Outcome getOutcome(ShortCode code, org.hl7.fhir.r5.model.Observation observation) { 41 | if (code.equals(ShortCode.NOM)) { 42 | throw new NotImplementedException("TODO"); 43 | } 44 | switch (code) { 45 | case H: return Outcome.HIGH(); 46 | case L: return Outcome.LOW(); 47 | case N: return Outcome.NORMAL(); 48 | case NEG: return Outcome.NEGATIVE(); 49 | case POS: return Outcome.POSITIVE(); 50 | default: 51 | throw new NotImplementedException("TODO"); 52 | } 53 | } 54 | 55 | 56 | @Override 57 | public Optional getOutcome() { 58 | if (observation.hasInterpretation()){ 59 | List codes = this.observation.getInterpretation().stream() 60 | .distinct() 61 | .map(CodeableConcept::getCoding) 62 | .flatMap(Collection::stream) 63 | .map(org.hl7.fhir.r5.model.Coding::getCode) 64 | .collect(Collectors.toList()); 65 | if (codes.size() > 1) { 66 | LOGGER.error("Multiple interpretation codes returned"); 67 | return Optional.empty(); 68 | } 69 | ShortCode code = ShortCode.fromShortCode(codes.get(0)); 70 | Outcome outcome = getOutcome(code, observation); 71 | return Optional.of(outcome); 72 | } else if (observation.hasValueCodeableConcept()) { 73 | return getOutcomeFromCodedValue(); 74 | } else if (observation.hasValueQuantity()) { 75 | return getOutcomeFromValueQuantity(); 76 | } else { 77 | LOGGER.error("Unable to handle observation {}", observation); 78 | return Optional.empty(); 79 | } 80 | } 81 | 82 | Optional getOutcomeFromCodedValue() { 83 | CodeableConcept codeableConcept = this.observation.getValueCodeableConcept(); 84 | if (codeableConcept == null) { // should never happen 85 | LOGGER.error("Codable concept null in getOutcomeFromCodedValue"); 86 | } 87 | List codings = codeableConcept != null ? codeableConcept.getCoding() : List.of(); 88 | for (Coding coding : codings) { 89 | String code = coding.getCode(); 90 | String system = coding.getSystem(); 91 | String display = coding.getDisplay(); 92 | String outcomeString = code + ":" + system + ":" + display; 93 | Outcome outcome = Outcome.nominal(outcomeString); 94 | return Optional.of(outcome); 95 | } 96 | return Optional.empty(); 97 | } 98 | 99 | Optional getOutcomeFromValueQuantity() { 100 | List references = 101 | this.observation.getReferenceRange(); 102 | 103 | if (references.size() == 0) { 104 | LOGGER.error("Reference range not found"); 105 | return Optional.empty(); 106 | } 107 | 108 | if (references.size() >= 2) { 109 | LOGGER.error("Reference range had more than two entries"); 110 | return Optional.empty(); 111 | // TODO sometimes this is observed. 112 | //An exception: three reference sizes 113 | //it can happen when there is actually one range but coded in three ranges 114 | //e.g. normal 20-30 115 | //in this case, one range ([20, 30]) is sufficient; 116 | //however, it is written as three ranges: ( , 20) [20, 30] (30, ) 117 | } 118 | Observation.ObservationReferenceRangeComponent targetReference = references.get(0); 119 | double low = targetReference.hasLow() ? 120 | targetReference.getLow().getValue().doubleValue() : Double.MIN_VALUE; 121 | double high = targetReference.hasHigh() ? 122 | targetReference.getHigh().getValue().doubleValue() : Double.MAX_VALUE; 123 | double observed = this.observation.getValueQuantity().getValue().doubleValue(); 124 | if (observed < low) { 125 | return Optional.of(Outcome.LOW()); 126 | } else if (observed > high) { 127 | return Optional.of(Outcome.HIGH()); 128 | } else { 129 | return Optional.of(Outcome.NORMAL()); 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/main/java/org/monarchinitiative/loinc2hpofhir/fhir2hpo/ObservationR4.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpofhir.fhir2hpo; 2 | 3 | import org.apache.commons.lang.NotImplementedException; 4 | import org.hl7.fhir.r4.model.CodeableConcept; 5 | import org.hl7.fhir.r4.model.Coding; 6 | import org.hl7.fhir.r4.model.Observation; 7 | import org.monarchinitiative.loinc2hpocore.codesystems.Outcome; 8 | import org.monarchinitiative.loinc2hpocore.codesystems.ShortCode; 9 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 10 | import org.slf4j.Logger; 11 | import org.slf4j.LoggerFactory; 12 | 13 | import java.util.Collection; 14 | import java.util.List; 15 | import java.util.Optional; 16 | import java.util.stream.Collectors; 17 | 18 | public class ObservationR4 implements Uberobservation { 19 | private static final Logger LOGGER = LoggerFactory.getLogger(ObservationR4.class); 20 | private final org.hl7.fhir.r4.model.Observation observation; 21 | 22 | 23 | public ObservationR4(org.hl7.fhir.r4.model.Observation observation) { 24 | this.observation = observation; 25 | } 26 | 27 | @Override 28 | public Optional getLoincId() { 29 | LoincId loincId; 30 | for (Coding coding : observation.getCode().getCoding()) { 31 | if (coding.getSystem().equals("http://loinc.org")) { 32 | loincId = new LoincId(coding.getCode()); 33 | return Optional.of(loincId); 34 | } 35 | } 36 | return Optional.empty(); 37 | } 38 | 39 | private Outcome getOutcome(ShortCode code, org.hl7.fhir.r4.model.Observation observation) { 40 | if (code.equals(ShortCode.NOM)) { 41 | throw new NotImplementedException("TODO"); 42 | } 43 | switch (code) { 44 | case H: 45 | return Outcome.HIGH(); 46 | case L: 47 | return Outcome.LOW(); 48 | case N: 49 | return Outcome.NORMAL(); 50 | case NEG: 51 | return Outcome.NEGATIVE(); 52 | case POS: 53 | return Outcome.POSITIVE(); 54 | default: 55 | throw new NotImplementedException("TODO"); 56 | } 57 | } 58 | 59 | 60 | @Override 61 | public Optional getOutcome() { 62 | if (observation.hasInterpretation()) { 63 | List codes = this.observation.getInterpretation().stream() 64 | .distinct() 65 | .map(CodeableConcept::getCoding) 66 | .flatMap(Collection::stream) 67 | .map(Coding::getCode) 68 | .collect(Collectors.toList()); 69 | if (codes.size() > 1) { 70 | LOGGER.error("Multiple interpretation codes returned"); 71 | return Optional.empty(); 72 | } 73 | ShortCode code = ShortCode.fromShortCode(codes.get(0)); 74 | Outcome outcome = getOutcome(code, observation); 75 | return Optional.of(outcome); 76 | } else if (observation.hasValueCodeableConcept()) { 77 | return getOutcomeFromCodedValue(); 78 | } else if (observation.hasValueQuantity()) { 79 | return getOutcomeFromValueQuantity(); 80 | } else { 81 | LOGGER.error("Unable to handle observation {}", observation); 82 | return Optional.empty(); 83 | } 84 | } 85 | 86 | Optional getOutcomeFromCodedValue() { 87 | CodeableConcept codeableConcept = this.observation.getValueCodeableConcept(); 88 | if (codeableConcept == null) { // should never happen 89 | LOGGER.error("Codable concept null in getOutcomeFromCodedValue"); 90 | } 91 | List codings = codeableConcept != null ? codeableConcept.getCoding() : List.of(); 92 | for (Coding coding : codings) { 93 | String code = coding.getCode(); 94 | String system = coding.getSystem(); 95 | String display = coding.getDisplay(); 96 | String outcomeString = code + ":" + system + ":" + display; 97 | Outcome outcome = Outcome.nominal(outcomeString); 98 | return Optional.of(outcome); 99 | } 100 | return Optional.empty(); 101 | } 102 | 103 | Optional getOutcomeFromValueQuantity() { 104 | List references = 105 | this.observation.getReferenceRange(); 106 | 107 | if (references.size() == 0) { 108 | LOGGER.error("Reference range not found"); 109 | return Optional.empty(); 110 | } 111 | 112 | if (references.size() >= 2) { 113 | LOGGER.error("Reference range had more than two entries"); 114 | return Optional.empty(); 115 | // TODO sometimes this is observed. 116 | //An exception: three reference sizes 117 | //it can happen when there is actually one range but coded in three ranges 118 | //e.g. normal 20-30 119 | //in this case, one range ([20, 30]) is sufficient; 120 | //however, it is written as three ranges: ( , 20) [20, 30] (30, ) 121 | } 122 | Observation.ObservationReferenceRangeComponent targetReference = references.get(0); 123 | double low = targetReference.hasLow() ? 124 | targetReference.getLow().getValue().doubleValue() : Double.MIN_VALUE; 125 | double high = targetReference.hasHigh() ? 126 | targetReference.getHigh().getValue().doubleValue() : Double.MAX_VALUE; 127 | double observed = this.observation.getValueQuantity().getValue().doubleValue(); 128 | if (observed < low) { 129 | return Optional.of(Outcome.LOW()); 130 | } else if (observed > high) { 131 | return Optional.of(Outcome.HIGH()); 132 | } else { 133 | return Optional.of(Outcome.NORMAL()); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpocore/annotation/QuantitativeLoincAnnotation.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.annotation; 2 | 3 | import org.monarchinitiative.loinc2hpocore.codesystems.Outcome; 4 | import org.monarchinitiative.loinc2hpocore.exception.Loinc2HpoRuntimeException; 5 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Map; 10 | import java.util.Optional; 11 | 12 | /** 13 | * This class is for LOINC2HPO annotations with the {@code Qn} scale. Most of these annotations 14 | * have values for Low, Normal, and High results, but some only have two relevant values. For instance, 15 | * LOINC 6047-5 (Bloodworm IgE Ab [Units/volume] in Serum) does not have a "low" value, because there 16 | * is no abnormal low level of this analyte. In this case, we use the fromNormalAndHigh factory method, 17 | * store a null pointer for {@link #low} and return Optional.empty() for this value (which should actually 18 | * never be queried in the real world). Similarly, in some occasions we just have Low and Normal. 19 | * We do expect to have a normal annotation in all cases, and if not, an Exception is thrown. 20 | * @author Peter Robinson 21 | */ 22 | public class QuantitativeLoincAnnotation implements LoincAnnotation { 23 | 24 | private final LoincId loincId; 25 | private final Loinc2HpoAnnotation low; 26 | private final Loinc2HpoAnnotation normal; 27 | private final Loinc2HpoAnnotation high; 28 | 29 | 30 | public QuantitativeLoincAnnotation(Loinc2HpoAnnotation low, 31 | Loinc2HpoAnnotation normal, 32 | Loinc2HpoAnnotation high) { 33 | this.low = low; 34 | this.normal = normal; 35 | this.high = high; 36 | // assumption is that all annotation have the same LoincId 37 | // we assume that although some annotation have just two values there will always be a 38 | // normal value (this is enforced by the parser and not checked here) 39 | this.loincId = this.normal.getLoincId(); 40 | } 41 | 42 | 43 | private static String getDebugInfo(Map outcomeMap) { 44 | StringBuilder sb = new StringBuilder("Malformed Quantitivate outcomes\nn=").append(outcomeMap.size()); 45 | for (var oc : outcomeMap.values()) { 46 | sb.append("\t[ERROR] ").append(oc).append("\n"); 47 | } 48 | return sb.toString(); 49 | } 50 | 51 | 52 | 53 | 54 | /** 55 | * Create between 1 and 3 components. Note that we demand there be a NORMAL annotation, 56 | * but LOW and HIGH are optional. 57 | * @param outcomeMap outcomes and annotations for a LOINC test 58 | * @return corresponding {@link LoincAnnotation} object 59 | */ 60 | public static LoincAnnotation fromOutcomeMap(Map outcomeMap) { 61 | if (! outcomeMap.containsKey(Outcome.NORMAL())) { 62 | String msg = String.format("Attempt to create Quantitative LoincAnnotation without Normal annotation: %s", 63 | getDebugInfo(outcomeMap)); 64 | throw new Loinc2HpoRuntimeException(msg); 65 | } 66 | if (outcomeMap.size() == 3) { 67 | return new QuantitativeLoincAnnotation(outcomeMap.get(Outcome.LOW()), 68 | outcomeMap.get(Outcome.NORMAL()), 69 | outcomeMap.get(Outcome.HIGH())); 70 | } else if (outcomeMap.size() == 2 && 71 | outcomeMap.containsKey(Outcome.NORMAL()) && 72 | outcomeMap.containsKey(Outcome.HIGH())) { 73 | return new QuantitativeLoincAnnotation(null, outcomeMap.get(Outcome.NORMAL()), 74 | outcomeMap.get(Outcome.HIGH())); 75 | } else if (outcomeMap.size() == 2 && 76 | outcomeMap.containsKey(Outcome.LOW()) && 77 | outcomeMap.containsKey(Outcome.NORMAL())) { 78 | return new QuantitativeLoincAnnotation(outcomeMap.get(Outcome.LOW()), 79 | outcomeMap.get(Outcome.NORMAL()), null); 80 | } else if (outcomeMap.size() == 1 && outcomeMap.containsKey(Outcome.NORMAL())) { 81 | return new QuantitativeLoincAnnotation(null, outcomeMap.get(Outcome.NORMAL()), null); 82 | } 83 | String msg = String.format("\"Unable to create Quantitative LoincAnnotation annotation: %s", 84 | getDebugInfo(outcomeMap)); 85 | throw new Loinc2HpoRuntimeException(msg); 86 | } 87 | 88 | 89 | 90 | @Override 91 | public Optional getOutcome(Outcome outcome) { 92 | switch (outcome.getCode()) { 93 | case L: 94 | if (low == null) return Optional.empty(); 95 | else return Optional.of(new Hpo2Outcome(low.getHpoTermId(), Outcome.LOW())); 96 | case N: 97 | return Optional.of(new Hpo2Outcome(normal.getHpoTermId(), Outcome.NORMAL())); 98 | case H: 99 | if (high == null) return Optional.empty(); 100 | return Optional.of(new Hpo2Outcome(high.getHpoTermId(), Outcome.HIGH())); 101 | default: 102 | return Optional.empty(); 103 | } 104 | } 105 | 106 | @Override 107 | public LoincId getLoincId() { 108 | return this.loincId; 109 | } 110 | 111 | @Override 112 | public List allAnnotations() { 113 | List allAnnots = new ArrayList<>(); 114 | if (low != null) { 115 | allAnnots.add(low); 116 | } 117 | allAnnots.add(normal); 118 | if (high != null) { 119 | allAnnots.add(high); 120 | } 121 | return allAnnots; 122 | } 123 | 124 | @Override 125 | public LoincScale scale() { 126 | return LoincScale.QUANTITATIVE; 127 | } 128 | 129 | @Override 130 | public String toString() { 131 | return loincId + "\n\t" + 132 | (low == null? " L: n/a" : "L: " + low) + "\n\t" + 133 | (normal == null? " N: n/a" : "N: " + normal)+ "\n\t" + 134 | (high == null? " H: n/a" : "H: " + high); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpocore/loinc/LoincEntry.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.loinc; 2 | 3 | import org.monarchinitiative.loinc2hpocore.annotation.LoincScale; 4 | import org.monarchinitiative.loinc2hpocore.exception.Loinc2HpoRuntimeException; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.util.ArrayList; 9 | import java.util.Arrays; 10 | import java.util.List; 11 | import java.util.stream.Collectors; 12 | 13 | public class LoincEntry { 14 | private static final Logger logger = LoggerFactory.getLogger(LoincEntry.class); 15 | 16 | private final LoincId loincId; 17 | 18 | private final String component; 19 | 20 | private final String property; 21 | 22 | private final String timeAspect; 23 | 24 | private final String system; 25 | 26 | private final LoincScale scale; 27 | 28 | private final String method; 29 | 30 | private final LoincLongName loincLongName; 31 | 32 | private static final int MIN_FIELDS_LOINC=10; 33 | 34 | public final static String [] headerFields = { 35 | "LOINC_NUM", "COMPONENT", "PROPERTY", "TIME_ASPCT", "SYSTEM", 36 | "SCALE_TYP", "METHOD_TYP","CLASS", "CLASSTYPE","LONG_COMMON_NAME", 37 | "SHORTNAME", 38 | "EXTERNAL_COPYRIGHT_NOTICE", 39 | "STATUS","VersionFirstReleased", "VersionLastChanged"}; 40 | public final static String header = Arrays.stream(headerFields) 41 | .map(w -> String.format("\"%s\"",w)) 42 | .collect(Collectors.joining(",")); 43 | 44 | private final static int LOINC_ID_FIELD = 0; 45 | private final static int COMPONENT_FIELD = 1; 46 | private final static int PROPERTY_FIELD = 2; 47 | private final static int TIMEASPECT_FIELD = 3; 48 | private final static int SYSTEM_FIELD = 4; 49 | private final static int SCALETYP_FIELD = 5; 50 | private final static int METHODTYP_FIELD = 6; 51 | private final static int LONG_COMMON_NAME_FIELD = 9; 52 | 53 | 54 | 55 | 56 | 57 | 58 | private static final String HEADER_LINE="FLAG\t#LOINC.id\tLOINC.scale\tHPO.low\tHPO.wnl\tHPO.high\tnote"; 59 | 60 | 61 | public LoincEntry(LoincId loincId, String comp, String property, String timeAspect, String system, 62 | LoincScale scale, String method, LoincLongName longName) { 63 | this.loincId = loincId; 64 | this.component = comp; 65 | this.property = property; 66 | this.timeAspect = timeAspect; 67 | this.system = system; 68 | this.scale = scale; 69 | this.method = method; 70 | this.loincLongName = longName; 71 | } 72 | 73 | 74 | public LoincId getLoincId(){ return loincId;} 75 | public String getComponent() { return component; } 76 | public String getProperty() { return property; } 77 | public String getTimeAspect() { return timeAspect; } 78 | public String getMethod() { return method; } 79 | public LoincScale getScale() { return scale; } 80 | public String getSystem() { return system; } 81 | public String getLongName() { return loincLongName.getName(); } 82 | public LoincLongName getLoincLongName() { 83 | return this.loincLongName; 84 | } 85 | 86 | /** 87 | * Method to check that a LOINC is Ord and the outcome is either "Presence" or "Absence" 88 | * @return true if the LOINC is "Ord" and the outcome is either "Presence" or "Absence" 89 | */ 90 | public boolean isPresentOrd() { 91 | return this.loincLongName.getLoincType().startsWith("Presen"); 92 | } 93 | 94 | @Override 95 | public boolean equals(Object obj){ 96 | if (this.loincId != null && obj instanceof LoincEntry) { 97 | LoincEntry other = (LoincEntry) obj; 98 | return this.loincId.equals(other.getLoincId()); 99 | } 100 | return false; 101 | } 102 | 103 | @Override 104 | public int hashCode() { 105 | return this.loincId.hashCode(); 106 | } 107 | 108 | /** 109 | * Split on comma, but not if the comma occurs within a quoted string 110 | * @param line input line such as "9806-1","2,4-Dichlorophenoxyacetate","MCnc","Pt",... 111 | * @return List of String without quotes 112 | */ 113 | private static List splitQuotedCsvLine(String line) { 114 | List result = new ArrayList<>(); 115 | int start = 0; 116 | boolean inQuotes = false; 117 | for (int current = 0; current < line.length(); current++) { 118 | if (line.charAt(current) == '\"') inQuotes = !inQuotes; // toggle state 119 | else if (line.charAt(current) == ',' && !inQuotes) { 120 | result.add(line.substring(start+1, current-1));// the +1 and -1 remove the quotes 121 | start = current + 1; 122 | } 123 | } 124 | result.add(line.substring(start)); 125 | return result; 126 | } 127 | 128 | 129 | /** 130 | * Structure of file: see {@link #headerFields} 131 | * @param line e.g., "10000-8","R wave duration.lead AVR","Time","Pt","Heart","Qn","EKG","EKG.MEAS","2","R wave duration in lead AVR","R wave dur L-AVR","","ACTIVE","1.0i","2.48" 132 | * @return corresponding line 133 | */ 134 | public static LoincEntry fromQuotedCsvLine(String line) { 135 | List fieldsWithNoQuotes = splitQuotedCsvLine(line); 136 | if (fieldsWithNoQuotes.size() quantitativeCodes = Set.of(ShortCode.H, ShortCode.N, ShortCode.L); 37 | private final Set ordinalCodes = Set.of(ShortCode.POS, ShortCode.NEG); 38 | private final Set nominalCodes = Set.of(ShortCode.NOM); 39 | 40 | 41 | 42 | @Override 43 | public void run() { 44 | System.out.println(annotPath); 45 | Loinc2HpoAnnotationParser parser = new Loinc2HpoAnnotationParser(annotPath); 46 | List entries = parser.getEntries(); 47 | System.out.println("[INFO] Got " + entries.size() + " LOINC annotations."); 48 | Ontology ontology = OntologyLoader.loadOntology(new File(hpJsonPath)); 49 | System.out.println("[INFO] Got " + ontology.countNonObsoleteTerms() + " HPO terms in hp.json."); 50 | checkValidityOfHpoTerms(entries, ontology); 51 | 52 | Map mymap = parser.loincToHpoAnnotationMap(); 53 | System.out.println("[INFO] " + mymap.size() + " annotated LOINC terms"); 54 | checkValidityOfLoincAnnotations(mymap); 55 | 56 | } 57 | 58 | 59 | private void checkValidityOfHpoTerms(List entries, Ontology ontology) { 60 | int good = 0; 61 | for (var entry : entries) { 62 | TermId tid = entry.getHpoTermId(); 63 | if (! ontology.containsTerm(tid)) { 64 | System.err.println("[ERROR] HPO does not contain TermId " + tid.getValue()); 65 | } else if ( ! ontology.getPrimaryTermId(tid).equals(tid)) { 66 | System.err.println("[ERROR] Obsolete TermId (" + tid.getValue() + ") used instead of " + 67 | ontology.getPrimaryTermId(tid) + "."); 68 | } else { 69 | good++; 70 | } 71 | } 72 | System.out.printf("[INFO] %d well-formed HPO terms used in LOINC annotations\n", good); 73 | } 74 | 75 | private void checkValidityOfLoincAnnotations(Map annotmap) { 76 | int malformed = 0; 77 | for (var annot : annotmap.values()) { 78 | if (annot.scale().equals(LoincScale.QUANTITATIVE)) { 79 | malformed += checkInvalidQuantitative(annot); 80 | } else if (annot.scale().equals(LoincScale.ORDINAL)) { 81 | malformed += checkInvalidOrdinal(annot); 82 | } else if (annot.scale().equals(LoincScale.NOMINAL)) { 83 | malformed += checkInvalidNominal(annot); 84 | } else { 85 | // should never haqppen 86 | throw new Loinc2HpoRuntimeException("Unrecognized loinc scale"); 87 | } 88 | } 89 | System.out.printf("[INFO] %s well formed, %d malformed annotations.\n", 90 | annotmap.size() - malformed, malformed); 91 | } 92 | 93 | /** 94 | * @param annot 95 | * @return 1 if malformed, 0 if OK 96 | */ 97 | private int checkInvalidQuantitative(LoincAnnotation annot) { 98 | List annots = annot.allAnnotations(); 99 | for (var l2h : annots) { 100 | Outcome out = l2h.getOutcome(); 101 | if (! quantitativeCodes.contains(out.getCode())) { 102 | System.err.printf("[ERROR] Malformed QN outcome code (\"%s\"): %s\n", out.getCode(), annot); 103 | return 1; 104 | } 105 | } 106 | return 0;//ok if we get here 107 | } 108 | 109 | /** 110 | * @param annot 111 | * @return 1 if malformed, 0 if OK 112 | */ 113 | private int checkInvalidOrdinal(LoincAnnotation annot) { 114 | List annots = annot.allAnnotations(); 115 | for (var l2h : annots) { 116 | Outcome out = l2h.getOutcome(); 117 | if (! ordinalCodes.contains(out.getCode())) { 118 | System.err.printf("[ERROR] Malformed ordinal outcome code (\"%s\"): %s\n", out.getCode(), annot); 119 | return 1; 120 | } 121 | } 122 | return 0;//ok if we get here 123 | } 124 | 125 | /** 126 | * @param annot 127 | * @return 1 if malformed, 0 if OK 128 | */ 129 | private int checkInvalidNominal(LoincAnnotation annot) { 130 | List annots = annot.allAnnotations(); 131 | for (var l2h : annots) { 132 | Outcome out = l2h.getOutcome(); 133 | if (! nominalCodes.contains(out.getCode())) { 134 | System.err.printf("[ERROR] Malformed nominal outcome code: %s\n", annot); 135 | return 1; 136 | } 137 | } 138 | return 0;//ok if we get here 139 | } 140 | 141 | 142 | } 143 | -------------------------------------------------------------------------------- /loinc2hpo-fhir/src/test/java/fhir/TestBase.java: -------------------------------------------------------------------------------- 1 | package fhir; 2 | 3 | import ca.uhn.fhir.context.FhirContext; 4 | import ca.uhn.fhir.parser.IParser; 5 | import org.hl7.fhir.dstu3.model.*; 6 | 7 | import java.io.File; 8 | import java.io.FileNotFoundException; 9 | import java.io.FileReader; 10 | import java.io.IOException; 11 | import java.net.URL; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | public class TestBase { 16 | /** 17 | * This page is part of the FHIR Specification (v4.0.1: R4 - Mixed Normative and STU). 18 | * This is the current published version. It defines v2 ABNORMAL FLAGS, 19 | * HL7-defined code system of concepts which specify a categorical assessment of an 20 | * observation value. 21 | * It is being communicated in FHIR in Observation.interpretation. 22 | */ 23 | public static final String hl7Version2Table0078 = "http://hl7.org/fhir/v2/0078"; 24 | 25 | private static final FhirContext ctxDstu3 = FhirContext.forDstu3(); 26 | private static final IParser jsonparserDstu3 = ctxDstu3.newJsonParser(); 27 | 28 | public static org.hl7.fhir.dstu3.model.Observation importDstu3Observation(String path) throws IOException { 29 | URL url = TestBase.class.getClassLoader().getResource(path); 30 | if (url == null) { 31 | throw new FileNotFoundException("Could not find " + path + " for testing"); 32 | } 33 | File f = new File(url.getFile()); 34 | if (! f.isFile()) { 35 | throw new FileNotFoundException("Could not find " + path + " for testing"); 36 | } 37 | return (Observation) jsonparserDstu3.parseResource(new FileReader(f)); 38 | } 39 | 40 | 41 | protected Observation lowHemoglobinObservation() { 42 | Patient patient = getPatient(); 43 | // Create an observation object 44 | Observation observation = new Observation(); 45 | observation.setStatus(Observation.ObservationStatus.FINAL); 46 | observation 47 | .getCode() 48 | .addCoding() 49 | .setSystem("http://loinc.org") 50 | .setCode("789-8") 51 | .setDisplay("Erythrocytes [#/volume] in Blood by Automated count"); 52 | observation.setValue( 53 | new SimpleQuantity() 54 | .setValue(4.12) 55 | .setUnit("10 trillion/L") 56 | .setSystem("http://unitsofmeasure.org") 57 | .setCode("3*12/L")); 58 | Coding coding = new Coding().setSystem(hl7Version2Table0078).setCode("L").setDisplay("Low"); 59 | List codings = new ArrayList<>(); 60 | codings.add(coding); 61 | observation.setInterpretation( 62 | new CodeableConcept() 63 | .setCoding(codings) 64 | ); 65 | // The observation refers to the patient using the ID, which is already set to a temporary UUID 66 | observation.setSubject(new Reference(patient.getId())); 67 | return observation; 68 | } 69 | 70 | 71 | /** 72 | * A normal range in adults is generally considered to be 4.35 to 5.65 million red blood cells 73 | * per microliter (mcL) 74 | * of blood for men and 3.92 to 5.13 million red blood cells per mcL of blood for women. 75 | * @return A FHIR Observation (dstu3) for high hemoglobin with reference range but no interpretation 76 | */ 77 | protected Observation highHemoglobinWithValueRangeObservation() { 78 | Patient patient = getPatient(); 79 | // Create an observation object 80 | Observation observation = new Observation(); 81 | observation.setStatus(Observation.ObservationStatus.FINAL); 82 | observation 83 | .getCode() 84 | .addCoding() 85 | .setSystem("http://loinc.org") 86 | .setCode("789-8") 87 | .setDisplay("Erythrocytes [#/volume] in Blood by Automated count"); 88 | observation.setValue( 89 | new SimpleQuantity() 90 | .setValue(6.17) 91 | .setUnit("10 trillion/L") 92 | .setSystem("http://unitsofmeasure.org") 93 | .setCode("3*12/L")); 94 | SimpleQuantity low = new SimpleQuantity(); 95 | low.setValue(3.92).setSystem("http://unitsofmeasure.org").setUnit("10 trillion/L").setCode("3*12/L"); 96 | observation.getReferenceRangeFirstRep().setLow(low); 97 | SimpleQuantity high = new SimpleQuantity(); 98 | high.setValue(5.13).setSystem("http://unitsofmeasure.org").setUnit("10 trillion/L").setCode("3*12/L"); 99 | observation.getReferenceRangeFirstRep().setHigh(high); 100 | observation.setSubject(new Reference(patient.getId())); 101 | return observation; 102 | } 103 | 104 | /** 105 | * Create a Patient object to user to build Observations. 106 | * @return Patient object with random data. 107 | */ 108 | private Patient getPatient() { 109 | Patient patient = new Patient(); 110 | patient.addName().setFamily("Smith").addGiven("Rob").addGiven("Bruce"); 111 | patient.setGender(Enumerations.AdministrativeGender.MALE); 112 | patient.setActive(true); 113 | CodeableConcept codeableConcept = new CodeableConcept(); 114 | Coding coding = new Coding(); 115 | coding.setSystem( "https://fhir.acme.io/fhir/code/thing" ); 116 | coding.setCode( "thing" ); 117 | coding.setDisplay("Thing stuff..."); 118 | List codings = new ArrayList<>(); 119 | codings.add(coding); 120 | codeableConcept.setCoding( codings ); 121 | Reference assigner = new Reference(); 122 | assigner.setDisplay("Patient thing"); 123 | Identifier thing = new Identifier(); 124 | thing.setUse( Identifier.IdentifierUse.USUAL ); 125 | thing.setType( codeableConcept ); 126 | thing.setSystem( "https://debug.acme.io/thing/0" ); 127 | thing.setValue( "99" ); 128 | thing.setAssigner( assigner ); 129 | List< Identifier > identifiers = patient.getIdentifier(); 130 | identifiers.add( thing ); 131 | patient.setIdentifier( identifiers ); 132 | return patient; 133 | } 134 | 135 | 136 | protected Observation ecoliNoInterpretationBloodCulture() { 137 | Patient patient = getPatient(); 138 | Observation observation = new Observation(); 139 | observation.setStatus(Observation.ObservationStatus.FINAL); 140 | observation 141 | .getCode() 142 | .addCoding() 143 | .setSystem("http://loinc.org") 144 | .setCode("600-7") 145 | .setDisplay("Bacteria identified in Blood by Culture"); 146 | CodeableConcept codeableConcept = new CodeableConcept(); 147 | codeableConcept.addCoding(new Coding() 148 | .setSystem("http://snomed.info/sct").setCode("112283007").setDisplay("Escherichia coli")); 149 | observation.setValue(codeableConcept); 150 | observation.setSubject(new Reference(patient.getId())); 151 | return observation; 152 | } 153 | 154 | 155 | 156 | } 157 | -------------------------------------------------------------------------------- /loinc2hpo-core/src/main/java/org/monarchinitiative/loinc2hpocore/annotation/Loinc2HpoAnnotation.java: -------------------------------------------------------------------------------- 1 | package org.monarchinitiative.loinc2hpocore.annotation; 2 | 3 | import org.monarchinitiative.loinc2hpocore.codesystems.Outcome; 4 | import org.monarchinitiative.loinc2hpocore.codesystems.ShortCode; 5 | import org.monarchinitiative.loinc2hpocore.exception.Loinc2HpoRuntimeException; 6 | import org.monarchinitiative.loinc2hpocore.loinc.LoincId; 7 | import org.monarchinitiative.phenol.ontology.data.TermId; 8 | 9 | import java.util.Comparator; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.Optional; 13 | import java.util.function.Function; 14 | import java.util.stream.Collectors; 15 | 16 | /** 17 | * Represents a line from the {@code loinc2hpo-annotations.tsv} file 18 | * This file has the following format 19 | *
 20 |  * loincId	loincScale	outcome	hpoTermId	supplementalTermId	curation    comment
 21 |  * 600-7	s	NOMINAL	POS	HP:0031864
 22 |  * 600-7	s	NOMINAL	H	HP:0031864
 23 |  * 600-7	s	NOMINAL	N	HP:0031864
 24 |  * (...)
 25 |  * 
26 | */ 27 | public class Loinc2HpoAnnotation implements Comparable { 28 | 29 | private final LoincId loincId; 30 | private final LoincScale loincScale; 31 | private final Outcome outcomeCode; 32 | private final TermId hpoTermId; 33 | private final String biocuration; 34 | /** Term to provide additional information. Can be null. */ 35 | private final TermId supplementalOntologyTermId; 36 | private final String comment; 37 | 38 | public Loinc2HpoAnnotation(LoincId loincId, 39 | LoincScale loincScale, 40 | Outcome code, 41 | TermId hpoTermId, 42 | TermId supplementalOntologyTermId, 43 | String biocuration, 44 | String comment) { 45 | this.loincId = loincId; 46 | this.loincScale = loincScale; 47 | this.outcomeCode = code; 48 | this.hpoTermId = hpoTermId; 49 | this.supplementalOntologyTermId = supplementalOntologyTermId; 50 | this.biocuration = biocuration; 51 | this.comment = comment; 52 | } 53 | 54 | public Loinc2HpoAnnotation(LoincId loincId, 55 | LoincScale loincScale, 56 | Outcome code, 57 | TermId hpoTermId, 58 | String biocuration, 59 | String comment) { 60 | this.loincId = loincId; 61 | this.loincScale = loincScale; 62 | this.outcomeCode = code; 63 | this.hpoTermId = hpoTermId; 64 | this.biocuration = biocuration; 65 | this.supplementalOntologyTermId = null; 66 | this.comment = comment; 67 | } 68 | 69 | public LoincId getLoincId() { 70 | return loincId; 71 | } 72 | 73 | public LoincScale getLoincScale() { 74 | return loincScale; 75 | } 76 | 77 | public Outcome getOutcome() { 78 | return outcomeCode; 79 | } 80 | 81 | public TermId getHpoTermId() { 82 | return hpoTermId; 83 | } 84 | 85 | public String getBiocuration() { 86 | return biocuration; 87 | } 88 | 89 | public Optional getSupplementalOntologyTermId() { 90 | return Optional.ofNullable(supplementalOntologyTermId); 91 | } 92 | 93 | public String getSupplementalOntologyTermIdAsString() { 94 | if (supplementalOntologyTermId == null) { 95 | return "."; 96 | } else { 97 | return supplementalOntologyTermId.getValue(); 98 | } 99 | } 100 | 101 | public String getComment() { 102 | if (comment == null || comment.isEmpty()) return "."; 103 | return comment; 104 | } 105 | 106 | 107 | 108 | public static final String [] headerFields = {"loincId", "loincScale", "outcome", "hpoTermId", 109 | "supplementalTermId", "curation", "comment"}; 110 | private static final int EXPECTED_NUMBER_OF_FIELDS = headerFields.length; 111 | 112 | /** 113 | * @return A tab-separate values line for the loinc2hpo-annotation.tsv file. 114 | */ 115 | public String toTsv() { 116 | return String.format("%s\t%s\t%s\t%s\t%s\t%s\t%s", 117 | loincId, 118 | loincScale.shortName(), 119 | outcomeCode.getOutcome(), 120 | hpoTermId.getValue(), 121 | getSupplementalOntologyTermIdAsString(), 122 | biocuration, 123 | getComment() 124 | ); 125 | } 126 | 127 | 128 | public static Loinc2HpoAnnotation fromAnnotationLine(String line) { 129 | String [] fields = line.split("\t"); 130 | if (fields.length != EXPECTED_NUMBER_OF_FIELDS) { 131 | System.err.printf("Malformed line with %d fields: %s%n", fields.length, line); 132 | 133 | } 134 | LoincId loincId = new LoincId(fields[0]); 135 | LoincScale scale = LoincScale.fromString(fields[1]); 136 | 137 | TermId hpoId = TermId.of(fields[3]); 138 | String curation = fields[5]; 139 | String comment = fields[6]; 140 | String outcomeString = fields[2]; 141 | Outcome outcome; 142 | if (scale.equals(LoincScale.NOMINAL)) { 143 | outcome = Outcome.nominal(outcomeString); 144 | } else { 145 | outcome = new Outcome(ShortCode.fromShortCode(outcomeString)); 146 | } 147 | if (fields[4].equals(".")) { 148 | return new Loinc2HpoAnnotation(loincId,scale,outcome,hpoId,curation,comment); 149 | } 150 | TermId supplementalId = TermId.of(fields[4]); 151 | return new Loinc2HpoAnnotation(loincId, scale, outcome, hpoId, supplementalId, curation,comment); 152 | } 153 | 154 | private static LoincScale getScale(List outcomes) { 155 | List scales = outcomes.stream() 156 | .map(Loinc2HpoAnnotation::getLoincScale) 157 | .distinct().collect(Collectors.toList()); 158 | if (scales.size() == 0) { 159 | // should never happen 160 | throw new Loinc2HpoRuntimeException("Could not extract LoincScale"); 161 | } else if (scales.size() > 1) { 162 | // should never happen 163 | throw new Loinc2HpoRuntimeException("Extracted more than one LoincScale"); 164 | } 165 | return scales.get(0); 166 | } 167 | 168 | public static LoincAnnotation outcomes2LoincAnnotation(List outcomes) { 169 | int n = outcomes.size(); 170 | // map with all of the outcomes for the current LOINC test 171 | Map outcomeMap = outcomes.stream() 172 | .collect(Collectors.toMap(Loinc2HpoAnnotation::getOutcome, Function.identity())); 173 | LoincScale scale = getScale(outcomes); 174 | if (scale.equals(LoincScale.QUANTITATIVE)) { 175 | return QuantitativeLoincAnnotation.fromOutcomeMap(outcomeMap); 176 | } else if (scale.equals(LoincScale.ORDINAL)) { 177 | return OrdinalHpoAnnotation.fromOutcomeMap(outcomeMap); 178 | } else if (scale.equals(LoincScale.NOMINAL)) { 179 | return new NominalLoincAnnotation(outcomeMap); 180 | } 181 | // if we get here, we did not match and there is some problem 182 | for (var oc : outcomes) { 183 | System.err.println("[ERROR] " + oc); 184 | } 185 | StringBuilder sb = new StringBuilder("Malformed outcomes\nn=").append(outcomes.size()); 186 | for (var oc : outcomes) { 187 | sb.append("\t[ERROR] ").append(oc).append("\n"); 188 | } 189 | throw new Loinc2HpoRuntimeException(sb.toString()); 190 | } 191 | 192 | 193 | @Override 194 | public int compareTo(Loinc2HpoAnnotation that) { 195 | //return Comparator.comparing(getLoincId()).thenComparing(getOutcome()); 196 | int res = this.loincId.compareTo(that.loincId); 197 | return res != 0 ? res : this.outcomeCode.compareTo(that.outcomeCode); 198 | } 199 | 200 | @Override 201 | public String toString() { 202 | return toTsv(); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /docs/FHIR_mapping.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | FHIR Mapping 3 | ============ 4 | 5 | loinc2hpo can be used to convert LOINC-encoded laboratory results from FHIR. 6 | 7 | 8 | Mapping LOINC to candidate HPO terms 9 | ==================================== 10 | 11 | LOINC observations have four main categories, quantitative(Qn), ordinal(Ord), 12 | nominal (Nom) and narrative(Nar). 13 | The majority of them are ``Qn`` (~50%) and ``Ord`` (~26%) and a smaller percentage 14 | are ``Nom`` and ``Nar`` type (<10%). Currently, loinc2hpo maps QN, Ord, and Nom codes 15 | but is not able to map Nar codes. 16 | 17 | FHIR Interpretation codes 18 | ========================= 19 | 20 | Test outcomes can represented with a code from the 21 | `FHIR interpretation code valueset `_. 22 | HPO terms are qualitative. For instance, 23 | `Thrombocytopenia HP:0001873 `_ does not 24 | indicate whether there is a mild, moderate or severe degree of low blood platelets. Therefore 25 | we map FHIR codes for different degrees of severity to the same internal code (these codes 26 | are shown on the index page of this documentation). 27 | 28 | 29 | Table 1: FHIR interpretation code set Mapping to internal code system 30 | 31 | +------------------------------------+---------------------------+ 32 | |FHIR interpretation code value set |internal code | 33 | +-------+----------------------------+---------------------------+ 34 | |Code | Meaning |Code | Meaning | 35 | +=======+============================+========+==================+ 36 | |< |Off scale low |L |low | 37 | +-------+----------------------------+--------+------------------+ 38 | |> |Off scale high |H |high | 39 | +-------+----------------------------+--------+------------------+ 40 | |A |Abnormal |A |abnormal | 41 | +-------+----------------------------+--------+------------------+ 42 | |AA |Critically abnormal |A |abnormal | 43 | +-------+----------------------------+--------+------------------+ 44 | |AC |Anti-complementary |POS |present | 45 | | |substances present | | | 46 | +-------+----------------------------+--------+------------------+ 47 | |B |Better |N |normal | 48 | +-------+----------------------------+--------+------------------+ 49 | |D |Significant change down |L |low | 50 | +-------+----------------------------+--------+------------------+ 51 | |DET |Detected |POS |present | 52 | +-------+----------------------------+--------+------------------+ 53 | |H |High |H |high | 54 | +-------+----------------------------+--------+------------------+ 55 | |HH |Critically high |H |high | 56 | +-------+----------------------------+--------+------------------+ 57 | |HM |Hold for Medical Review |U |unknown | 58 | +-------+----------------------------+--------+------------------+ 59 | |HU |Very high |H |high | 60 | +-------+----------------------------+--------+------------------+ 61 | |I |Intermediate |N |normal | 62 | +-------+----------------------------+--------+------------------+ 63 | |IE |Insufficient evidence |U |unknown | 64 | +-------+----------------------------+--------+------------------+ 65 | |IND |Indeterminate |U |unknown | 66 | +-------+----------------------------+--------+------------------+ 67 | |L |Low |L |low | 68 | +-------+----------------------------+--------+------------------+ 69 | |LL |Critically low |L |low | 70 | +-------+----------------------------+--------+------------------+ 71 | |LU |Very low |L |low | 72 | +-------+----------------------------+--------+------------------+ 73 | |MS |Moderately susceptible. |U |unknown | 74 | | |(microbiology) | | | 75 | +-------+----------------------------+--------+------------------+ 76 | |N |Normal |N |normal | 77 | +-------+----------------------------+--------+------------------+ 78 | |ND |Not Detected |NEG |not present | 79 | +-------+----------------------------+--------+------------------+ 80 | |NEG |Negative |NEG |not present | 81 | +-------+----------------------------+--------+------------------+ 82 | |NR |Non-reactive |NEG |not present | 83 | +-------+----------------------------+--------+------------------+ 84 | |NS |Non-susceptible |U |unknown | 85 | +-------+----------------------------+--------+------------------+ 86 | |null |No range defined or normal |U |unknown | 87 | | |ranges don't apply | | | 88 | +-------+----------------------------+--------+------------------+ 89 | |OBX |Interpretation qualifiers |U |unknown | 90 | | |in separate OBX segments | | | 91 | +-------+----------------------------+--------+------------------+ 92 | |POS |Positive |POS |positive | 93 | +-------+----------------------------+--------+------------------+ 94 | |QCF |Quality Control Failure |U |unknown | 95 | +-------+----------------------------+--------+------------------+ 96 | |R |Resistant |U |unknown | 97 | +-------+----------------------------+--------+------------------+ 98 | |RR |Reactive |POS |present | 99 | +-------+----------------------------+--------+------------------+ 100 | |S |Susceptible |U |unknown | 101 | +-------+----------------------------+--------+------------------+ 102 | |SDD |Susceptible-dose dependent |U |unknown | 103 | +-------+----------------------------+--------+------------------+ 104 | |SYN-R |Synergy - resistant |U |unknown | 105 | +-------+----------------------------+--------+------------------+ 106 | |SYN-S |Synergy - susceptible |U |unknown | 107 | +-------+----------------------------+--------+------------------+ 108 | |TOX |Cytotoxic substance present |POS |present | 109 | +-------+----------------------------+--------+------------------+ 110 | |U |Significant change up |H |high | 111 | +-------+----------------------------+--------+------------------+ 112 | |VS |Very susceptible. |U |unknown | 113 | | |(microbiology) | | | 114 | +-------+----------------------------+--------+------------------+ 115 | |W |Worse |A |abnormal | 116 | +-------+----------------------------+--------+------------------+ 117 | |WR |Weakly reactive |POS |present | 118 | +-------+----------------------------+--------+------------------+ 119 | 120 | 121 | The following graph summarizes the mapping. 122 | 123 | .. image:: images/annotation_scheme.png 124 | :width: 400 125 | :alt: Annotation scheme 126 | 127 | Nominal outcomes 128 | ================ 129 | 130 | Nominal observations can use coding systems that are more difficult to handle. 131 | For example, `Loinc 600-7 `_ (Bacteria identified in Blood by Culture) 132 | may use a SNOMED concept to represent the 133 | finding that *Staphylococcus aureus* is detected:: 134 | 135 | "coding":[{ 136 | "system": "http://snomed.info/sct", 137 | "code": "3092008", 138 | "display": "Staphylococcus aureus" 139 | }] 140 | 141 | We currently map this to `Bacteremia HP:0031864 `_, 142 | but this term does not contain information about which bacterium was idenfitied in the blood. 143 | 144 | We are currently extending the annotations to enable one to indicate what kind of bacteremia. In the above mentioned case, 145 | one would use the NCBI `Taxonomy ID: 1280 `_ 146 | for *Staphylococcus aureus*. 147 | 148 | 149 | 150 | 151 | 152 | 153 | --------------------------------------------------------------------------------