Repository: monarch-initiative/pyphetools (language: python)

Issue:
Refactor time elem



Discussion / hints:

Fix patch:
diff --git a/docs/api/creation/iso_age.md b/docs/api/creation/iso_age.md
deleted file mode 100644
index 925b330c..00000000
--- a/docs/api/creation/iso_age.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# IsoAge
-
-::: pyphetools.creation.IsoAge
\ No newline at end of file
diff --git a/docs/img/kmf_esrd.png b/docs/img/kmf_esrd.png
new file mode 100644
index 00000000..6428a235
Binary files /dev/null and b/docs/img/kmf_esrd.png differ
diff --git a/docs/img/kmf_plot_vstatus.png b/docs/img/kmf_plot_vstatus.png
new file mode 100644
index 00000000..67777ec4
Binary files /dev/null and b/docs/img/kmf_plot_vstatus.png differ
diff --git a/docs/visualization/index.md b/docs/visualization/index.md
new file mode 100644
index 00000000..bf764135
--- /dev/null
+++ b/docs/visualization/index.md
@@ -0,0 +1,3 @@
+# Visualization
+
+pyphetools offers several options to visualize cohorts.
\ No newline at end of file
diff --git a/docs/visualization/kaplan_meier_visualizer.md b/docs/visualization/kaplan_meier_visualizer.md
new file mode 100644
index 00000000..760392a5
--- /dev/null
+++ b/docs/visualization/kaplan_meier_visualizer.md
@@ -0,0 +1,72 @@
+# Kaplan Meier Visualization
+
+A brief introduction to Kaplan-Meier analysis is available [here](https://pubmed.ncbi.nlm.nih.gov/20723767/). The following texts were extracted from that article.
+
+Time-to-event is a clinical course duration variable for each subject having a beginning and an end anywhere along the time line of the complete study. For example, it may begin when the subject is enrolled into a study or when treatment begins, and ends when the end-point (event of interest) is reached or the subject is censored from the study.  In preparing Kaplan-Meier survival analysis, each subject is characterized by three variables: 1) their serial time, 2) their status at the end of their serial time (event occurrence or censored), and 3) the study group they are in.  For the construction of survival time probabilities and curves, the serial times for individual subjects are arranged from the shortest to the longest, without regard to when they entered the study. By this maneuver, all subjects within the group begin the analysis at the same point and all are surviving until something happens to one of them. The two things that can happen are: 1) a subject can have the event of interest or 2) they are censored.
+
+
+
+
+| SUBJECT | SERIAL TIME (years) | STATUS AT SERIAL TIME (1=event; 0=censored) | Group (1 or 2) |
+|---------|---------------------|---------------------------------------------|----------------|
+| B       | 1                   | 1                                           | 1              |
+| E       | 2                   | 1                                           | 1              |
+| F       | 3                   | 1                                           | 1              |
+| A       | 4                   | 1                                           | 1              |
+| D       | 4.5                 | 1                                           | 1              |
+| C       | 5                   | 0                                           | 1              |
+| U       | 0.5                 | 1                                           | 2              |
+| Z       | 0.75                | 1                                           | 2              |
+| W       | 1                   | 1                                           | 2              |
+
+
+
+Censoring means the total survival time for that subject cannot be accurately determined. This can happen when something negative for the study occurs, such as the subject drops out, is lost to follow-up, or required data is not available or, conversely, something good happens, such as the study ends before the subject had the event of interest occur, i.e., they survived at least until the end of the study, but there is no knowledge of what happened thereafter. Thus censoring can occur within the study or terminally at the end.
+
+Currently, pyphetools shows a survival curve for the entire cohort. This is the corresponding Python code.
+There are two options. First, we plot the time up to the event represented by the age of onset of an HPO term.
+
+```python
+from pyphetools.visualization import KaplanMeierVisualizer, PhenopacketIngestor, SimplePatient
+from lifelines import KaplanMeierFitter
+import matplotlib.pyplot as plt # only needed to save file 
+phenopackets_dir = "../phenopackets/" # directory containing phenopackets to plot
+ingestor = PhenopacketIngestor(indir=phenopackets_dir)
+ppkt_list = ingestor.get_phenopacket_list()
+simple_pt_list = [SimplePatient(ppkt) for ppkt in ppkt_list]
+hpo_id = "HP:0003774" # TermId of HPO term for the KM plot
+kmv = KaplanMeierVisualizer(simple_patient_list=simple_pt_list, target_tid=stage5crd)
+T, E = kmv.get_time_and_event()
+# plot Kaplan Meier curve
+kmf = KaplanMeierFitter()
+kmf.fit(T, E, label="Age at stage 5 kidney disease")
+plt.plot()
+ax = kmf.plot_survival_function()
+ax.set_xlabel("Years");
+plt.savefig("kmf_plot.png", format="png"); ## optional
+```
+
+
+<figure markdown>
+![Validation results](../img/kmf_esrd.png){ width="1000" }
+<figcaption>Kaplan Meier Survival Plot of a cohort of individuals with pathogenic variants in the UMOD gene with respect to age of onset of stage 5 kidney failure.
+</figcaption>
+</figure>
+```
+
+It is also possible to plot a curve for survival, which makes use of the VitalStatus message of the phenopackets. The code is exactly the same
+as the above, except that we do not pass the target_tid argument.
+```python
+# same as above
+kmv = KaplanMeierVisualizer(simple_patient_list=simple_pt_list)
+# same as above except that we change the title of the plot
+kmf.fit(T, E, label="Survival")
+```
+
+![Kaplan-Meier Plot]()
+
+<figure markdown>
+![Validation results](../img/kmf_plot_vstatus.png){ width="1000" }
+<figcaption>Kaplan Meier Survival Plot of a cohort of individuals with pathogenic variants in the UMOD gene.
+</figcaption>
+</figure>
\ No newline at end of file
diff --git a/mkdocs.yml b/mkdocs.yml
index eb6b1f46..e4249826 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -52,6 +52,9 @@ nav:
       - Cohort encoder: 'tabular/cohort_encoder.md'
       - Validation: 'tabular/validation.md'
       - Visualization: 'tabular/visualization.md'
+  - Visualization:
+    - Overview: 'visualization/index.md'
+    - Kaplan Meier: 'visualization/kaplan_meier_visualizer.md'
   - Developers:
     - For developers: 'developers/developers.md'
     - Installation: 'developers/installation.md'
@@ -75,7 +78,6 @@ nav:
       - HpoParser: "api/creation/hpo_parser.md"
       - HpTerm: "api/creation/hp_term.md"
       - Individual: "api/creation/individual.md"
-      - IsoAge: "api/creation/iso_age.md"
       - MetaData: "api/creation/metadata.md"
       - OptionColumnMapper: "api/creation/option_column_mapper.md"
       - SexColumnMapper: "api/creation/sex_column_mapper.md"
diff --git a/src/pyphetools/__init__.py b/src/pyphetools/__init__.py
index 75c230a4..98630198 100644
--- a/src/pyphetools/__init__.py
+++ b/src/pyphetools/__init__.py
@@ -5,7 +5,7 @@
 from . import validation
 
 
-__version__ = "0.9.97"
+__version__ = "0.9.98"
 
 
 __all__ = [
diff --git a/src/pyphetools/creation/__init__.py b/src/pyphetools/creation/__init__.py
index 615bca6b..ce353d9d 100644
--- a/src/pyphetools/creation/__init__.py
+++ b/src/pyphetools/creation/__init__.py
@@ -23,7 +23,7 @@
 from .metadata import MetaData
 from .mode_of_inheritance import Moi
 from .option_column_mapper import OptionColumnMapper
-from .pyphetools_age import PyPheToolsAge, IsoAge, HpoAge, GestationalAge, HPO_ONSET_TERMS
+from .pyphetools_age import PyPheToolsAge, AgeSorter, HPO_ONSET_TERMS
 from .sex_column_mapper import SexColumnMapper
 from .simple_column_mapper import SimpleColumnMapper
 from .scm_generator import SimpleColumnMapperGenerator
@@ -62,7 +62,7 @@
     "Individual",
     "MetaData",
     "OptionColumnMapper",
-    "PyPheToolsAge", "IsoAge", "HpoAge", "GestationalAge", "HPO_ONSET_TERMS",
+    "PyPheToolsAge", "AgeSorter", "HPO_ONSET_TERMS",
     "SexColumnMapper",
     "SimpleColumnMapper",
     "SimpleColumnMapperGenerator",
diff --git a/src/pyphetools/creation/age_column_mapper.py b/src/pyphetools/creation/age_column_mapper.py
index dad32986..acd1db56 100644
--- a/src/pyphetools/creation/age_column_mapper.py
+++ b/src/pyphetools/creation/age_column_mapper.py
@@ -1,13 +1,13 @@
 import re
 from collections import defaultdict
-from enum import Enum
 import abc
 import math
 import pandas as pd
-
+import typing
 from .age_isoformater import AgeIsoFormater
-from .pyphetools_age import HPO_ONSET_TERMS, PyPheToolsAge, IsoAge, NoneAge, GestationalAge, HpoAge
+from .pyphetools_age import HPO_ONSET_TERMS, PyPheToolsAge
 from .constants import Constants
+from ..pp.v202 import TimeElement as TimeElement202
 
 ISO8601_REGEX = r"^P(\d+Y)?(\d+M)?(\d+D)?"
 # e.g., 14 y 8 m or 8 y
@@ -32,8 +32,6 @@ class AgeColumnMapper(metaclass=abc.ABCMeta):
 
     def __init__(self, column_name:str, string_to_iso_d=None) -> None:
         """
-        :param ageEncodingType: Formatting convention used to represent the age
-        :type ageEncodingType: one of Year (e.g. 42), ISO 8601 (e.g. P42Y2M), year/month (e.g. 42y2m)
         :param column_name: Name of the Age column in the original table
         :type column_name: str
         :param string_to_iso_d: dictionary from free text (input table) to ISO8601 strings
@@ -140,14 +138,14 @@ class Iso8601AgeColumnMapper(AgeColumnMapper):
     def __init__(self, column_name) -> None:
         super().__init__(column_name=column_name)
 
-    def map_cell(self, cell_contents) -> PyPheToolsAge:
+    def map_cell(self, cell_contents) -> typing.Optional[TimeElement202]:
         contents = self._clean_contents(cell_contents=cell_contents)
         match = re.search(ISO8601_REGEX, contents)
         if match:
-            return IsoAge.from_iso8601(contents)
+            return PyPheToolsAge.get_age_pp201(age_string=contents)
         else:
             self._erroneous_input_counter[contents] += 1
-            return NoneAge(contents)
+            return None
 
 
 class YearMonthAgeColumnMapper(AgeColumnMapper):
@@ -157,7 +155,7 @@ class YearMonthAgeColumnMapper(AgeColumnMapper):
     def __init__(self, column_name) -> None:
         super().__init__(column_name=column_name)
 
-    def map_cell(self, cell_contents) -> PyPheToolsAge:
+    def map_cell(self, cell_contents) -> typing.Optional[TimeElement202]:
         contents = self._clean_contents(cell_contents=cell_contents)
         try:
             match = re.search(YEAR_AND_MONTH_REGEX, contents)
@@ -165,20 +163,20 @@ def map_cell(self, cell_contents) -> PyPheToolsAge:
                 years = int(match.group(1))
                 months = int(match.group(2))
                 age_string = f"P{years}Y{months}M"
-                return IsoAge(y=years, m=months, age_string=age_string)
+                return PyPheToolsAge.get_age_pp201(age_string=age_string)
             match = re.search(YEAR_REGEX, contents)
             if match:
                 years = int(match.group(1))
                 age_string = f"P{years}Y"
-                return IsoAge(y=years, age_string=age_string)
+                return PyPheToolsAge.get_age_pp201(age_string=age_string)
             match = re.search(MONTH_REGEX, contents)
             if match:
                 months = int(match.group(1))
                 age_string = f"P{months}M"
-                return IsoAge(m=months, age_string=age_string)
+                return PyPheToolsAge.get_age_pp201(age_string=age_string)
         except ValueError as verr:
             print(f"Could not parse {cell_contents} as year/month: {verr}")
-            return NoneAge(contents)
+            return None
 
 class MonthAgeColumnMapper(AgeColumnMapper):
     """Mapper for entries such as P1Y2M (ISO 8601 period to represent age)
@@ -187,7 +185,7 @@ class MonthAgeColumnMapper(AgeColumnMapper):
     def __init__(self, column_name) -> None:
         super().__init__(column_name=column_name)
 
-    def map_cell(self, cell_contents) -> PyPheToolsAge:
+    def map_cell(self, cell_contents) -> typing.Optional[TimeElement202]:
         # assume month encoded by integer or float.
         contents = self._clean_contents(cell_contents=cell_contents)
         month = str(contents)
@@ -195,7 +193,7 @@ def map_cell(self, cell_contents) -> PyPheToolsAge:
             full_months = int(month)
             days = 0
             age_string = AgeIsoFormater.from_numerical_month(full_months)
-            return IsoAge(m=full_months, age_string=age_string)
+            return PyPheToolsAge.get_age_pp201(age_string=age_string)
         elif month.replace('.', '', 1).isdigit() and month.count('.') < 2:
             # a float such as 0.9 (months)
             months = float(month)
@@ -205,15 +203,15 @@ def map_cell(self, cell_contents) -> PyPheToolsAge:
                 days = int(months * avg_num_days_in_month)
                 full_months = 0
                 age_string = f"P{days}D"
-                return IsoAge(d=days, age_string=age_string)
+                return PyPheToolsAge.get_age_pp201(age_string=age_string)
             else:
                 remainder = months - floor_months
                 full_months = int(months - remainder)
                 days = int(remainder * avg_num_days_in_month)
                 age_string = f"P{full_months}M{days}D"
-                return IsoAge(m=full_months, d=days, age_string=age_string)
+                return PyPheToolsAge.get_age_pp201(age_string=age_string)
         else:
-            return NoneAge("na")
+            return None
 
 
 
@@ -222,33 +220,34 @@ class YearAgeColumnMapper(AgeColumnMapper):
     def __init__(self, column_name) -> None:
         super().__init__(column_name=column_name)
 
-    def map_cell(self, cell_contents) -> PyPheToolsAge:
+    def map_cell(self, cell_contents) -> typing.Optional[TimeElement202]:
         """
         Extract an iso8601 string for age recorded as a year (either an int such as 4 or a float such as 4.25 for P4Y3M)
         :param age: an int representing years or a float such as 2.5 for two and a half years
         :return: an ISO 8601 string such as P2Y6M
         """
         if isinstance(cell_contents, int):
-            return IsoAge(y=cell_contents, age_string=contents)
+            age_str = f"P{cell_contents}Y"
+            return PyPheToolsAge.get_age_pp201(age_string=age_str)
         elif isinstance(cell_contents, float):
-            age = str(age)
+            age = str(cell_contents)
         elif not isinstance(cell_contents, str):
-            raise ValueError(f"Malformed agestring {age}, type={type(age)}")
+            raise ValueError(f"Malformed agestring {cell_contents}, type={type(cell_contents)}")
         contents = self._clean_contents(cell_contents=cell_contents)
         int_or_float = r"(\d+)(\.\d+)?"
         p = re.compile(int_or_float)
         results = p.search(contents).groups()
         if len(results) != 2:
-            return NoneAge(contents)
+            return None
         if results[0] is None:
-            return NoneAge(contents)
+            return None
         y = int(results[0])
         if results[1] is None:
-            return IsoAge(y=y, age_string=f"P{y}Y")
+            return PyPheToolsAge.get_age_pp201(age_string=f"P{y}Y")
         else:
             m = float(results[1])  # something like .25
             months = round(12 * m)
-            return IsoAge(y=y, m=months, age_string=f"P{y}Y{months}M")
+            return PyPheToolsAge.get_age_pp201(age_string=f"P{y}Y{months}M")
 
 
 class CustomAgeColumnMapper(AgeColumnMapper):
@@ -260,12 +259,12 @@ def __init__(self, column_name:str, string_to_iso_d) -> None:
         super().__init__(column_name=column_name)
         self._string_to_iso_d = string_to_iso_d
 
-    def map_cell(self, cell_contents) -> PyPheToolsAge:
+    def map_cell(self, cell_contents) -> typing.Optional[TimeElement202]:
         if cell_contents not in self._string_to_iso_d:
             print(f"[WARNING] Could not find \"{cell_contents}\" in custom dictionary")
-            return NoneAge(cell_contents)
+            return None
         iso8601 = self._string_to_iso_d.get(cell_contents, Constants.NOT_PROVIDED)
-        return IsoAge.from_iso8601(iso8601)
+        return PyPheToolsAge.get_age_pp201(age_string=iso8601)
 
 class NotProvidedAgeColumnMapper(AgeColumnMapper):
     """Mapper if there is no information
@@ -274,11 +273,8 @@ class NotProvidedAgeColumnMapper(AgeColumnMapper):
     def __init__(self, column_name:str) -> None:
         super().__init__(column_name=column_name)
 
-    def map_cell(self, cell_contents) -> str:
-        if cell_contents is None or math.isnan(cell_contents):
-            cell_contents = "na"
-        contents = self._clean_contents(cell_contents=cell_contents)
-        return NoneAge(age_string=contents)
+    def map_cell(self, cell_contents) -> typing.Optional[TimeElement202]:
+        return None
 
 
 class HpoAgeColumnMapper(AgeColumnMapper):
@@ -290,10 +286,10 @@ class HpoAgeColumnMapper(AgeColumnMapper):
     def __init__(self, column_name:str) -> None:
         super().__init__(column_name=column_name)
 
-    def map_cell(self, cell_contents) -> PyPheToolsAge:
+    def map_cell(self, cell_contents) -> typing.Optional[TimeElement202]:
         contents = self._clean_contents(cell_contents=cell_contents)
         if contents in HPO_ONSET_TERMS:
-            return HpoAge(hpo_onset_label=contents)
+            return PyPheToolsAge.get_age_pp201(age_string=contents)
         else:
             self._erroneous_input_counter[contents] += 1
-            return NoneAge(cell_contents)
\ No newline at end of file
+            return None
diff --git a/src/pyphetools/creation/age_of_death_mapper.py b/src/pyphetools/creation/age_of_death_mapper.py
index 70d63984..a75950a0 100644
--- a/src/pyphetools/creation/age_of_death_mapper.py
+++ b/src/pyphetools/creation/age_of_death_mapper.py
@@ -4,9 +4,9 @@
 from .age_isoformater import AgeIsoFormater
 from .constants import Constants
 
-from pyphetools.pp.v202 import VitalStatus as pptVitalStatus
-from pyphetools.pp.v202 import TimeElement as pptTimeElement
-from pyphetools.pp.v202 import Age as pptAge
+from pyphetools.pp.v202 import VitalStatus as VitalStatus202
+from pyphetools.pp.v202 import TimeElement as TimeElement202
+from pyphetools.pp.v202 import Age as Age202
 
 
 class AgeOfDeathColumnMapper:
@@ -26,7 +26,7 @@ def __init__(self, column_name, string_to_iso_d=None) -> None:
         self._column_name = column_name
         self._string_to_iso_d = string_to_iso_d
 
-    def map_cell_to_vital_status(self, cell_contents) -> Optional[pptVitalStatus]:
+    def map_cell_to_vital_status(self, cell_contents) -> Optional[VitalStatus202]:
 
         """
         Map a single cell of the table
@@ -39,9 +39,9 @@ def map_cell_to_vital_status(self, cell_contents) -> Optional[pptVitalStatus]:
         if contents not in self._string_to_iso_d:
             return None
         # Wrap the Age (iso8601) in a TimeElement.
-        iso_age = pptAge(self._string_to_iso_d.get(contents))
-        telem = pptTimeElement(iso_age)
-        vstatus = pptVitalStatus(status=pptVitalStatus.Status.DECEASED, time_of_death=telem)
+        iso_age = Age202(self._string_to_iso_d.get(contents))
+        telem = TimeElement202(iso_age)
+        vstatus = VitalStatus202(status=VitalStatus202.Status.DECEASED, time_of_death=telem)
         return vstatus
 
     @property
diff --git a/src/pyphetools/creation/case_template_encoder.py b/src/pyphetools/creation/case_template_encoder.py
index 49200fc4..7694454f 100644
--- a/src/pyphetools/creation/case_template_encoder.py
+++ b/src/pyphetools/creation/case_template_encoder.py
@@ -1,5 +1,5 @@
 import abc
-from typing import Dict, List
+import typing
 from pyphetools.creation.citation import Citation
 from pyphetools.creation.constants import Constants
 from pyphetools.creation.disease import Disease
@@ -7,7 +7,8 @@
 from pyphetools.creation.metadata import MetaData
 from pyphetools.creation.hp_term import HpTerm
 from pyphetools.creation.individual import Individual
-from pyphetools.creation.pyphetools_age import NoneAge, PyPheToolsAge
+from pyphetools.creation.pyphetools_age import PyPheToolsAge
+from ..pp.v202 import TimeElement as TimeElement202
 import os
 import re
 import pandas as pd
@@ -54,10 +55,10 @@ class DataEncoder(CellEncoder):
     def __init__(self, h1:str, h2:str):
         super().__init__(name=h1)
 
-    def encode(self, cell_contents):
+    def encode(self, cell_contents) -> str:
         return str(cell_contents)
 
-    def columntype(self):
+    def columntype(self) -> "CellType":
         return CellType.DATA
 
 class HpoEncoder(CellEncoder):
@@ -112,7 +113,7 @@ def needs_attention(self) -> bool:
     def get_error(self) -> str:
         return self._error
 
-    def encode(self, cell_contents) -> None:
+    def encode(self, cell_contents) -> typing.Optional[HpTerm]:
         """
         Parses one cell from the template. Valid entries are observed, excluded, na, and ISO8601 age strings.
         Any other entry will lead to raising an Exception, probably the user entered something erroneous.
@@ -126,9 +127,11 @@ def encode(self, cell_contents) -> None:
             return None
         elif len(cell_contents) > 0:
             try:
-                onset = PyPheToolsAge.get_age(cell_contents)
-                if onset.is_valid(): # valid OSO8601 age of onset
+                onset = PyPheToolsAge.get_age_pp201(cell_contents)
+                if onset is not None: # valid age of onset
                     return HpTerm(hpo_id=self._hpo_id, label=self._hpo_label, onset=onset)
+                else:
+                    raise ValueError(f"Could not code age of onset {cell_contents}")
             except Exception as parse_error:
                 raise ValueError(f"Could not parse HPO column cell_contents: \”{str(parse_error)}\"")
             # if we cannot parse successfully, there is probably an error in the format. Drop down to end of function to warn user
@@ -142,7 +145,7 @@ def __init__(self, hpo_cr:HpoConceptRecognizer, h1=None, h2=None, ):
         super().__init__(name="Miscellaneous HPO column")
         self._hpo_cr = hpo_cr
 
-    def encode(self, cell_contents):
+    def encode(self, cell_contents) -> typing.List[HpTerm]:
         if isinstance(cell_contents, float):
             return []
         cell_contents = str(cell_contents)
@@ -170,10 +173,10 @@ class NullEncoder(CellEncoder):
     def __init__(self, h1=None, h2=None):
         super().__init__(name="Begin of HPO column")
 
-    def encode(self, cell_contents):
+    def encode(self, cell_contents) -> None:
         return None
 
-    def columntype(self):
+    def columntype(self) -> "CellType":
         return CellType.NULL
 
 EXPECTED_HEADERS = {"PMID", "title", "individual_id", "comment", "disease_id", "disease_label",
@@ -263,7 +266,10 @@ def __init__(self, df:pd.DataFrame, hpo_cr:HpoConceptRecognizer, created_by:str,
             metadata.default_versions_with_hpo(CaseTemplateEncoder.HPO_VERSION)
             self._metadata_d[i.id] = metadata
 
-    def  _process_header(self, header_1:List, header_2:List, hpo_cr:HpoConceptRecognizer) -> Dict[int, CellEncoder]:
+    def  _process_header(self, 
+                         header_1:typing.List[str], 
+                         header_2:typing.List[str], 
+                         hpo_cr:HpoConceptRecognizer) -> typing.Dict[int, CellEncoder]:
         index_to_decoder_d = {}
         in_hpo_range = False
         for i in range(self._n_columns):
@@ -307,7 +313,8 @@ def  _process_header(self, header_1:List, header_2:List, hpo_cr:HpoConceptRecogn
                 print(f"ERROR: {e}")
         return index_to_decoder_d
 
-    def _check_for_duplicate_individual_ids(self, df:pd.DataFrame) -> None:
+    def _check_for_duplicate_individual_ids(self, 
+                                            df:pd.DataFrame) -> None:
         """Check that no two individuals in the dataframe have the same identifier
         Duplicate identifiers can lead to other errors in the code
         An identifier is made from the combination of PMID and individual_id and must be unique
@@ -332,9 +339,13 @@ def _check_for_duplicate_individual_ids(self, df:pd.DataFrame) -> None:
             raise ValueError(err_str)
         # else, all is OK, no duplicate ids
 
-    def _parse_individual(self, row:pd.Series):
+    def _parse_individual(self, 
+                          row:pd.Series) -> Individual:
+        """
+        Parse one row of the Data ingest (Excel) template, corresponding to one individual
+        """
         if not isinstance(row, pd.Series):
-            raise ValueError(f"argument df must be pandas DSeriestaFrame but was {type(row)}")
+            raise ValueError(f"argument df must be pandas Series but was {type(row)}")
         data = row.values.tolist()
         if len(data) != self._n_columns:
             # Should never happen
@@ -345,9 +356,9 @@ def _parse_individual(self, row:pd.Series):
             encoder = self._index_to_decoder.get(i)
             cell_contents = data[i]
             if encoder is None:
-                print(f"Encoder {i} was None for data \"{cell_contents}\"")
+                print(f"Encoder for column {i} was None for data \"{cell_contents}\"")
                 self._debug_row(i, row)
-                raise ValueError(f"Encoder {i} was None for data \"{cell_contents}\"")
+                raise ValueError(f"Encoder for column {i} was None for data \"{cell_contents}\"")
             elif encoder.columntype == CellType.NTR:
                 continue ## cannot be use yet because new term request.
             encoder_type = encoder.columntype()
@@ -397,22 +408,23 @@ def _parse_individual(self, row:pd.Series):
             raise ValueError(f"Unrecognized sex symbol: {sex} for individual \"{individual_id}\"")
         onset_age = data_items.get(AGE_OF_ONSET_FIELDNAME)
         if onset_age is not None and isinstance(onset_age, str):
-            onset_age = PyPheToolsAge.get_age(onset_age)
+            onset_age = PyPheToolsAge.get_age_pp201(onset_age)
         else:
-            onset_age = NoneAge("na")
+            onset_age = None
         encounter_age = data_items.get(AGE_AT_LAST_ENCOUNTER_FIELDNAME)
         if encounter_age is not None and isinstance(encounter_age, str):
-            encounter_age = PyPheToolsAge.get_age(encounter_age)
+            encounter_age = PyPheToolsAge.get_age_pp201(encounter_age)
         else:
-            encounter_age = NoneAge("na")
+            encounter_age = None
         vitStat = None
         if "deceased" in data_items:
             decsd = data_items.get("deceased")
-            if decsd == "yes" and encounter_age.is_valid():
-                timeelem = encounter_age.to_ga4gh_time_element()
-                vitStat = VitalStatus(status=VitalStatus.Status.DECEASED, time_of_death=timeelem)
+            if decsd == "yes" and encounter_age is not None:
+                vitStat = VitalStatus(status=VitalStatus.Status.DECEASED, time_of_death=encounter_age)
+            elif decsd == "no":
+                vitStat = VitalStatus(status=VitalStatus.Status.ALIVE)
             else:
-                vitStat = VitalStatus(status=VitalStatus.Status.DECEASED)
+                vitStat = VitalStatus(status=VitalStatus.Status.UNKNOWN_STATUS)
         disease_id = data_items.get("disease_id")
         disease_label = data_items.get("disease_label")
         # common error -- e.g. PMID: 3000312 or OMIM: 600123 (whitespace after colon)
@@ -438,22 +450,22 @@ def _debug_row(self, target_idx:int, row:pd.Series):
             else:
                 print(f"[{j}] {hdr}={row_items[j]}")
 
-    def get_individuals(self) -> List[Individual]:
+    def get_individuals(self) -> typing.List[Individual]:
         return self._individuals
 
-    def get_allele1_d(self)-> Dict[str,str]:
+    def get_allele1_d(self)-> typing.Dict[str,str]:
         return self._allele1_d
 
-    def get_allele2_d(self)-> Dict[str,str]:
+    def get_allele2_d(self)-> typing.Dict[str,str]:
         return self._allele2_d
 
     def _is_biallelic(self) -> bool:
         return self._is_biallelic
 
-    def get_metadata_d(self) -> Dict[str,MetaData]:
+    def get_metadata_d(self) -> typing.Dict[str,MetaData]:
         return self._metadata_d
 
-    def get_phenopackets(self) -> List[PPKt.Phenopacket]:
+    def get_phenopackets(self) -> typing.List[PPKt.Phenopacket]:
         ppack_list = []
         for individual in self._individuals:
             cite = individual._citation
@@ -465,7 +477,8 @@ def get_phenopackets(self) -> List[PPKt.Phenopacket]:
 
 
 
-    def _transform_individuals_to_phenopackets(self, individual_list:List[Individual]):
+    def _transform_individuals_to_phenopackets(self, 
+                                               individual_list:typing.List[Individual]):
         """Create one phenopacket for each of the individuals
 
         :param individual_list: List of individual objects
@@ -486,7 +499,9 @@ def _transform_individuals_to_phenopackets(self, individual_list:List[Individual
             ppkt_list.append(phenopckt)
         return ppkt_list
 
-    def output_individuals_as_phenopackets(self, individual_list:List[Individual], outdir="phenopackets") -> None:
+    def output_individuals_as_phenopackets(self, 
+                                           individual_list:typing.List[Individual], 
+                                           outdir:str="phenopackets") -> None:
         """write a list of Individual objects to file in GA4GH Phenopacket format
         Note that the individual_list needs to be passed to this object, because we expect that
         the QC code will have been used to cleanse the data of redundancies etc before output.
@@ -526,7 +541,8 @@ def output_individuals_as_phenopackets(self, individual_list:List[Individual], o
         print(f"We output {written} GA4GH phenopackets to the directory {outdir}")
 
 
-    def print_individuals_as_phenopackets(self, individual_list:List[Individual]) -> None:
+    def print_individuals_as_phenopackets(self, 
+                                          individual_list:typing.List[Individual]) -> None:
         """Function designed to show all phenopackets in a notebook for Q/C
         :param individual_list: List of individual objects
         :type individual_list:List[Individual]
diff --git a/src/pyphetools/creation/cohort_encoder.py b/src/pyphetools/creation/cohort_encoder.py
index 9af98b0d..710ad561 100644
--- a/src/pyphetools/creation/cohort_encoder.py
+++ b/src/pyphetools/creation/cohort_encoder.py
@@ -1,16 +1,14 @@
 import pandas as pd
 from math import isnan
-from typing import Dict, List
+import typing
 
 from .abstract_encoder import AbstractEncoder
 from .age_column_mapper import AgeColumnMapper
-from .citation import Citation
 from .column_mapper import ColumnMapper
 from .constants import Constants
 from .disease import Disease
 from .hpo_cr import HpoConceptRecognizer
 from .individual import Individual
-from .pyphetools_age import NoneAge
 from .sex_column_mapper import SexColumnMapper
 from .variant_column_mapper import VariantColumnMapper
 
@@ -50,16 +48,16 @@ class CohortEncoder(AbstractEncoder):
     """
 
     def __init__(self,
-                df:pd.DataFrame,
-                hpo_cr: HpoConceptRecognizer,
-                column_mapper_list:List[ColumnMapper],
-                individual_column_name:str,
-                metadata,
-                age_of_onset_mapper:AgeColumnMapper=AgeColumnMapper.not_provided(),
-                age_at_last_encounter_mapper:AgeColumnMapper=AgeColumnMapper.not_provided(),
-                sexmapper:SexColumnMapper=SexColumnMapper.not_provided(),
-                variant_mapper:VariantColumnMapper=None,
-                delimiter:str=None):
+                 df: pd.DataFrame,
+                 hpo_cr: HpoConceptRecognizer,
+                 column_mapper_list: typing.List[ColumnMapper],
+                 individual_column_name: str,
+                 metadata,
+                 age_of_onset_mapper: AgeColumnMapper = AgeColumnMapper.not_provided(),
+                 age_at_last_encounter_mapper: AgeColumnMapper = AgeColumnMapper.not_provided(),
+                 sexmapper: SexColumnMapper = SexColumnMapper.not_provided(),
+                 variant_mapper: VariantColumnMapper = None,
+                 delimiter: str = None):
         """Constructor
         """
         super().__init__(metadata=metadata)
@@ -117,14 +115,14 @@ def preview_dataframe(self):
                 hpo_terms.extend(terms)
             hpo_string = "\n".join([h.to_string() for h in hpo_terms])
             d = {'id': individual_id,
-                'sex': sex,
-                'age': age,
-                'phenotypic features': hpo_string}
+                 'sex': sex,
+                 'age': age,
+                 'phenotypic features': hpo_string}
             individuals.append(d)
         df = pd.DataFrame(individuals)
         return df.set_index('id')
 
-    def set_disease(self, disease:Disease):
+    def set_disease(self, disease: Disease):
         """Set the disease diagnosis for all patients in the cohort
 
         If all patients in the cohort have the same disease we can set it with this method
@@ -134,7 +132,8 @@ def set_disease(self, disease:Disease):
         self._disease = disease
         self._disease_dictionary = None
 
-    def set_disease_dictionary(self, disease_d:Dict[str, Disease]):
+    def set_disease_dictionary(self,
+                               disease_d: typing.Dict[str, Disease]):
         """Set the dictionary of disease ontology terms
 
         For tables with multiple different diseases, we provide a dictionary that has as key
@@ -143,24 +142,22 @@ def set_disease_dictionary(self, disease_d:Dict[str, Disease]):
         self._disease_dictionary = disease_d
         self._disease = None
 
-
-    def _get_age(row:pd.Series, mapper:AgeColumnMapper):
+    def _get_age(row: pd.Series, mapper: AgeColumnMapper):
         import math
         column_name = mapper.get_column_name()
         if column_name == Constants.NOT_PROVIDED:
-            return NoneAge("na")
+            return None
         age_cell_contents = row[column_name]
         if isinstance(age_cell_contents, float) and math.isnan(age_cell_contents):
-            return NoneAge("na")
+            return None
         try:
             age = mapper.map_cell(age_cell_contents)
         except Exception as ee:
             print(f"Warning: Could not parse age {ee}. Setting age to \"not provided\"")
-            age = NoneAge("na")
+            age = None
         return age
 
-
-    def get_individuals(self) -> List[Individual]:
+    def get_individuals(self) -> typing.List[Individual]:
         """Get a list of all Individual objects in the cohort
 
         :returns: a list of all Individual objects in the cohort
@@ -214,22 +211,22 @@ def get_individuals(self) -> List[Individual]:
                     raise ValueError(f"Could not find disease link for {individual_id}")
                 disease = self._disease_dictionary.get(individual_id)
                 indi = Individual(individual_id=individual_id,
-                                sex=sex,
-                                age_of_onset=age_of_onset,
-                                age_at_last_encounter=age_last_encounter,
-                                hpo_terms=hpo_terms,
-                                citation=self._metadata.get_citation(),
-                                interpretation_list=interpretation_list,
-                                disease=disease)
+                                  sex=sex,
+                                  age_of_onset=age_of_onset,
+                                  age_at_last_encounter=age_last_encounter,
+                                  hpo_terms=hpo_terms,
+                                  citation=self._metadata.get_citation(),
+                                  interpretation_list=interpretation_list,
+                                  disease=disease)
             elif self._disease_dictionary is None and self._disease is not None:
                 indi = Individual(individual_id=individual_id,
-                                sex=sex,
-                                age_of_onset=age_of_onset,
-                                age_at_last_encounter=age_last_encounter,
-                                hpo_terms=hpo_terms,
-                                citation=self._metadata.get_citation(),
-                                interpretation_list=interpretation_list,
-                                disease=self._disease)
+                                  sex=sex,
+                                  age_of_onset=age_of_onset,
+                                  age_at_last_encounter=age_last_encounter,
+                                  hpo_terms=hpo_terms,
+                                  citation=self._metadata.get_citation(),
+                                  interpretation_list=interpretation_list,
+                                  disease=self._disease)
             else:
                 raise ValueError(f"Could not find disease data for '{individual_id}'")
             individuals.append(indi)
diff --git a/src/pyphetools/creation/hp_term.py b/src/pyphetools/creation/hp_term.py
index 64b08f72..2b863819 100644
--- a/src/pyphetools/creation/hp_term.py
+++ b/src/pyphetools/creation/hp_term.py
@@ -1,9 +1,11 @@
+import typing
 import pandas as pd
 import phenopackets as PPKt
-from .constants import Constants
-from .pyphetools_age import HpoAge, IsoAge, NoneAge, PyPheToolsAge
+from .pyphetools_age import PyPheToolsAge
+from ..pp.v202 import TimeElement as TimeElement202
 import hpotk
 
+
 class HpTerm:
     """
     Class to represent a phenotypic observation as an HPO term with optional modifiers
@@ -21,7 +23,14 @@ class HpTerm:
     :param resolution: an ISO8601 string representing the age of resolution, optional
     :type resolution: str
     """
-    def __init__(self, hpo_id:str, label:str, observed:bool=True, measured:bool=True, onset=NoneAge("na"), resolution=NoneAge("na")):
+
+    def __init__(self,
+                 hpo_id: str,
+                 label: str,
+                 observed: bool = True,
+                 measured: bool = True,
+                 onset: TimeElement202 = None,
+                 resolution: TimeElement202 = None):
         if hpo_id is None or len(hpo_id) == 0 or not hpo_id.startswith("HP"):
             raise ValueError(f"invalid id argument: '{hpo_id}'")
         if label is None or len(label) == 0:
@@ -30,8 +39,8 @@ def __init__(self, hpo_id:str, label:str, observed:bool=True, measured:bool=True
         self._label = label
         self._observed = observed
         self._measured = measured
-        if not isinstance(onset, PyPheToolsAge):
-            raise ValueError(f"onset argument must be PyPheToolsAge or subclass but was {type(onset)}")
+        #if not onset is None or str(type(onset)) != "<class 'pyphetools.pp.v202._base.TimeElement'>":
+        #    raise ValueError(f"onset argument must be TimeElement202 or None but was {type(onset)}")
         self._onset = onset
         self._resolution = resolution
 
@@ -77,24 +86,23 @@ def measured(self) -> bool:
         return self._measured
 
     @property
-    def onset(self) -> PyPheToolsAge:
+    def onset(self) -> typing.Optional[TimeElement202]:
         """
         :returns: A PyPheToolsAge object representing the age this abnormality first was observed
-        :rtype: PyPheToolsAge
+        :rtype: typing.Optional[TimeElement202]
         """
         return self._onset
 
-    def set_onset(self, onset:PyPheToolsAge) -> None:
-        if not isinstance(onset, PyPheToolsAge):
-            raise ValueError(f"argument of set_onset but be PyPheToolsAge but was {type(onset)}")
+    def set_onset(self, onset: TimeElement202) -> None:
+        if not isinstance(onset, TimeElement202):
+            raise ValueError(f"argument of set_onset but be TimeElement202 but was {type(onset)}")
         self._onset = onset
 
-
     @property
-    def resolution(self) -> PyPheToolsAge:
+    def resolution(self) -> typing.Optional[TimeElement202]:
         """
         :returns: A PyPheToolsAge object representing the age this abnormality resolved
-        :rtype: PyPheToolsAge
+        :rtype: typing.Optional[TimeElement202]
         """
         return self._resolution
 
@@ -120,8 +128,8 @@ def hpo_term_and_id(self) -> str:
         return f"{self._label} ({self._id})"
 
     def _term_and_id_with_onset(self) -> str:
-        if self._onset is not None and self._onset.is_valid():
-            return f"{self.hpo_term_and_id}: onset {self._onset.age_string}"
+        if self._onset is not None:
+            return f"{self.hpo_term_and_id}: onset {self._onset}"
         else:
             return self.hpo_term_and_id
 
@@ -152,13 +160,12 @@ def to_ga4gh_phenotypic_feature(self) -> PPKt.PhenotypicFeature:
         pf.type.label = self._label
         if not self._observed:
             pf.excluded = True
-        if self._onset.is_valid():
-            pf.onset.CopyFrom(self._onset.to_ga4gh_time_element())
-        if self._resolution.is_valid():
-            pf.resolution.CopyFrom(self._resolution.to_ga4gh_time_element())
+        if self._onset is not None:
+            pf.onset.CopyFrom(self._onset.to_message())
+        if self._resolution is not None:
+            pf.resolution.CopyFrom(self._resolution.to_message())
         return pf
 
-
     @staticmethod
     def term_list_to_dataframe(hpo_list) -> pd.DataFrame:
         if not isinstance(hpo_list, list):
@@ -171,12 +178,12 @@ def term_list_to_dataframe(hpo_list) -> pd.DataFrame:
             return pd.DataFrame(columns=['Col1', 'Col2', 'Col3'])
         items = []
         for hp in hpo_list:
-            d = { "id": hp.id, "label": hp.label, "observed":hp.observed, "measured": hp.measured }
+            d = {"id": hp.id, "label": hp.label, "observed": hp.observed, "measured": hp.measured}
             items.append(d)
         return pd.DataFrame(items)
 
     @staticmethod
-    def from_hpo_tk_term(hpotk_term:hpotk.Term) -> "HpTerm":
+    def from_hpo_tk_term(hpotk_term: hpotk.Term) -> "HpTerm":
         """Create a pyphetools HpTerm object from an hpo-toolkit Term object
 
         :param hpotk_term: A term from the HPO toolkit
@@ -191,10 +198,12 @@ def from_hpo_tk_term(hpotk_term:hpotk.Term) -> "HpTerm":
 
 class HpTermBuilder:
 
-    def __init__(self, hpo_id:str, hpo_label:str):
+    def __init__(self,
+                 hpo_id: str,
+                 hpo_label: str):
         if not hpo_id.startswith("HP:"):
             raise ValueError(f"Malformed HPO id {hpo_id}")
-        if not len(hpo_id) == 10:
+        if len(hpo_id) != 10:
             raise ValueError(f"Malformed HPO id with length {len(hpo_id)}: {hpo_id}")
         self._hpo_id = hpo_id
         if hpo_label is None or len(hpo_label) < 3:
@@ -202,8 +211,8 @@ def __init__(self, hpo_id:str, hpo_label:str):
         self._hpo_label = hpo_label
         self._observed = True
         self._measured = True
-        self._onset = NoneAge("na")
-        self._resolution = NoneAge("na")
+        self._onset = None
+        self._resolution = None
 
     def excluded(self):
         self._observed = False
@@ -218,98 +227,96 @@ def iso8601_onset(self, onset):
         """
         if not isinstance(onset, str):
             raise ValueError(f"onset argument must be iso8601 string but was {type(onset)}")
-        self._onset = IsoAge.from_iso8601(onset)
+        self._onset = PyPheToolsAge.get_age_pp201(onset)
         return self
 
     def embryonal_onset(self):
         """Onset of disease at up to 8 weeks following fertilization (corresponding to 10 weeks of gestation).
         """
-        self._onsetTerm = HpoAge("Embryonal onset") # HP:0011460
+        self._onsetTerm = PyPheToolsAge.get_age_pp201("Embryonal onset")  # HP:0011460
         return self
 
     def fetal_onset(self):
         """Onset prior to birth but after 8 weeks of embryonic development (corresponding to a gestational age of 10 weeks).
         """
-        self._onset = HpoAge("Fetal onset") # HP:0011461
+        self._onset = PyPheToolsAge.get_age_pp201("Fetal onset")  # HP:0011461
         return self
 
     def second_trimester_onset(self):
         """second trimester, which comprises the range of gestational ages from 14 0/7 weeks to 27 6/7 (inclusive)
         """
-        self._onset = HpoAge("Second trimester onset") # HP:0034198
+        self._onset = PyPheToolsAge.get_age_pp201("Second trimester onset")  # HP:0034198
         return self
 
     def late_first_trimester_onset(self):
         """late first trimester during the early fetal period, which is defined as 11 0/7 to 13 6/7 weeks of gestation (inclusive).
         """
-        self._onset = HpoAge("Late first trimester onset") # HP:0034199
+        self._onset = PyPheToolsAge.get_age_pp201("Late first trimester onset")  # HP:0034199
         return self
 
     def third_trimester_onset(self):
         """third trimester, which is defined as 28 weeks and zero days (28+0) of gestation and beyond.
         """
-        self._onset = HpoAge("Third trimester onset") # HP:0034197
+        self._onset = PyPheToolsAge.get_age_pp201("Third trimester onset")  # HP:0034197
         return self
 
     def antenatal_onset(self):
         """onset prior to birth
         """
-        self._onset = HpoAge("Antenatal onset") # HP:0030674
+        self._onset = PyPheToolsAge.get_age_pp201("Antenatal onset")  # HP:0030674
         return self
 
     def congenital_onset(self):
         """A phenotypic abnormality that is present at birth.
         """
-        self._onset = HpoAge("Congenital onset") # HP:0003577
+        self._onset = PyPheToolsAge.get_age_pp201("Congenital onset")  # HP:0003577
         return self
 
     def neonatal_onset(self):
         """Onset of signs or symptoms of disease within the first 28 days of life.
         """
-        self._onset = HpoAge("Neonatal onset") # HP:0003623)
+        self._onset = PyPheToolsAge.get_age_pp201("Neonatal onset")  # HP:0003623)
         return self
 
-
     def infantile_onset(self):
         """Onset of signs or symptoms of disease between 28 days to one year of life.
         """
-        self._onset = HpoAge("Infantile onset") # HP:0003593
+        self._onset = PyPheToolsAge.get_age_pp201("Infantile onset")  # HP:0003593
         return self
 
     def childhood_onset(self):
         """Onset of disease at the age of between 1 and 5 years.
         """
-        self._onset = HpoAge("Childhood onset") # HP:0011463
+        self._onset = PyPheToolsAge.get_age_pp201("Childhood onset")  # HP:0011463
         return self
 
     def juvenile_onset(self):
         """Onset of signs or symptoms of disease between the age of 5 and 15 years.
         """
-        self._onset = HpoAge("Juvenile onset") # HP:0003621
+        self._onset = PyPheToolsAge.get_age_pp201("Juvenile onset")  # HP:0003621
         return self
 
     def young_adult_onset(self):
         """Onset of disease at the age of between 16 and 40 years.
         """
-        self._onset = HpoAge("Young adult onset") # HP:0011462
+        self._onset = PyPheToolsAge.get_age_pp201("Young adult onset")  # HP:0011462
         return self
 
     def middle_age_onset(self):
         """onset of symptoms at the age of 40 to 60 years.
         """
-        self._onset = HpoAge("Middle age onset") # HP:0003596
+        self._onset = PyPheToolsAge.get_age_pp201("Middle age onset")  # HP:0003596
         return self
 
     def late_onset(self):
         """Onset of symptoms after the age of 60 years.
         """
-        self._onset = HpoAge("Late onset") # HP:0003584
+        self._onset = PyPheToolsAge.get_age_pp201("Late onset")  # HP:0003584
         return self
 
     def build(self) -> HpTerm:
         return HpTerm(hpo_id=self._hpo_id,
-                    label=self._hpo_label,
-                    observed=self._observed,
-                    measured=self._measured,
-                    onset=self._onset)
-
+                      label=self._hpo_label,
+                      observed=self._observed,
+                      measured=self._measured,
+                      onset=self._onset)
diff --git a/src/pyphetools/creation/import_template.py b/src/pyphetools/creation/import_template.py
index 77caade8..ba2c2214 100644
--- a/src/pyphetools/creation/import_template.py
+++ b/src/pyphetools/creation/import_template.py
@@ -126,6 +126,8 @@ def import_phenopackets_from_template(self,
                                         hemizygous:bool=False,
                                         leniant_MOI:bool=False):
         """Import the data from an Excel template and create a collection of Phenopackets
+        This method writes the individuals as Phenopackets to file and also returns Individuals and the CValidator.
+        ToDo -- refactor to avoid side effects.
 
         Note that things will be completely automatic if the template just has HGNC encoding variants
         If there are structural variants, we need to encode them by hand by passing them as
diff --git a/src/pyphetools/creation/individual.py b/src/pyphetools/creation/individual.py
index fba9e7fe..251cca43 100644
--- a/src/pyphetools/creation/individual.py
+++ b/src/pyphetools/creation/individual.py
@@ -1,3 +1,5 @@
+import typing
+
 import phenopackets as PPKt
 import re
 import os
@@ -9,8 +11,11 @@
 from .hp_term import HpTerm
 from .hgvs_variant import Variant
 from .metadata import MetaData, Resource
-from .pyphetools_age import PyPheToolsAge, NoneAge, IsoAge
-from ..pp.v202 import VitalStatus
+from .pyphetools_age import PyPheToolsAge, AgeSorter
+from ..pp.v202 import TimeElement as TimeElement202
+from ..pp.v202 import VitalStatus as VitalStatus202
+from ..pp.v202 import OntologyClass as OntologyClass202
+
 
 class Individual:
     """
@@ -31,15 +36,15 @@ class Individual:
     """
 
     def __init__(self,
-                individual_id:str,
-                hpo_terms:List[HpTerm]=None,
-                citation:Citation=None,
-                sex:str=Constants.NOT_PROVIDED,
-                age_of_onset:str=NoneAge("na"),
-                age_at_last_encounter:str=NoneAge("na"),
-                vital_status:VitalStatus=None,
-                interpretation_list:List[PPKt.VariantInterpretation]=None,
-                disease:Disease=None):
+                 individual_id: str,
+                 hpo_terms: List[HpTerm] = None,
+                 citation: Citation = None,
+                 sex: str = Constants.NOT_PROVIDED,
+                 age_of_onset: TimeElement202 = None,
+                 age_at_last_encounter: TimeElement202 = None,
+                 vital_status: VitalStatus202 = None,
+                 interpretation_list: List[PPKt.VariantInterpretation] = None,
+                 disease: Disease = None):
         """Constructor
         """
         if isinstance(individual_id, int):
@@ -69,7 +74,6 @@ def __init__(self,
             self._interpretation_list = interpretation_list
         self._disease = disease
         self._citation = citation
-        self._vital_status = None
 
     @property
     def id(self):
@@ -91,37 +95,21 @@ def set_sex(self, sex):
         self._sex = sex
 
     @property
-    def age_of_onset(self):
+    def age_of_onset(self) -> typing.Optional[TimeElement202]:
         """
-        :returns: an iso8601 representation of age or HPO term label
-        :rtype: str
+        :returns: a representation of age when the disease first manifested
+        :rtype: PyPheToolsAge
         """
         return self._age_of_onset
 
     @property
-    def age_at_last_encounter(self):
+    def age_at_last_encounter(self) -> typing.Optional[TimeElement202]:
         """
-        :returns: an iso8601 representation of age or HPO term label
-        :rtype: str
+        :returns: a representation of age when the individual was last seen in a medical context
+        :rtype: PyPheToolsAge
         """
         return self._age_at_last_encounter
 
-    def set_age_of_onset(self, age):
-        """
-        :param age: iso8601 string or HPO onset label
-        :type age: str
-        """
-        if not isinstance(age, str):
-            raise ValueError(f"age argument must be a string but was {type(age)}")
-        self._age_of_onset = PyPheToolsAge.get_age(age)
-
-    def set_age_at_last_encounter(self, age):
-        """
-        :param age: iso8601 string or HPO onset label
-        :type age: str
-        """
-        self._age_at_last_encounter = age
-
     @property
     def hpo_terms(self):
         """
@@ -138,7 +126,7 @@ def interpretation_list(self) -> List[PPKt.VariantInterpretation]:
         """
         return self._interpretation_list
 
-    def add_variant(self, v:Union[Variant, PPKt.VariantInterpretation], acmg:str=None):
+    def add_variant(self, v: Union[Variant, PPKt.VariantInterpretation], acmg: str = None):
         """
         :param v: A Variant obeserved in this individual
         :type v: Union[Variant, PPKt.VariantInterpretation]
@@ -152,11 +140,10 @@ def add_variant(self, v:Union[Variant, PPKt.VariantInterpretation], acmg:str=Non
         if isinstance(variant, PPKt.VariantInterpretation):
             self._interpretation_list.append(variant)
         else:
-            raise ValueError(f"variant argument must be pyphetools Variant or GA4GH VariantInterpretation but was {type(variant)}")
-
+            raise ValueError(
+                f"variant argument must be pyphetools Variant or GA4GH VariantInterpretation but was {type(variant)}")
 
-
-    def add_hpo_term(self, term:HpTerm) -> None:
+    def add_hpo_term(self, term: HpTerm) -> None:
         """
         Adds one HPO term to the current individual.
         :param term: An HPO term (observed or excluded, potentially with Age of observation
@@ -166,7 +153,7 @@ def add_hpo_term(self, term:HpTerm) -> None:
             raise ValueError(f"\"term\" argument must be HpTerm but was {type(term)}")
         self._hpo_terms.append(term)
 
-    def set_disease(self, disease:Disease) -> None:
+    def set_disease(self, disease: Disease) -> None:
         """
         This method is typically useful for a cohort with multiple diagnoses; otherwise, the disease can be set by the
         CohortEncoder
@@ -182,7 +169,7 @@ def disease_count(self):
         else:
             return 1
 
-    def set_hpo_terms(self, cleansed_hpo_terms:List[HpTerm]):
+    def set_hpo_terms(self, cleansed_hpo_terms: List[HpTerm]):
         """
         :param cleansed_hpo_terms: a list of HpTerm objects that has been cleansed by OntologyQC
         :type cleansed_hpo_terms: List[pyphetools.creation.HpTerm]
@@ -193,18 +180,21 @@ def set_hpo_terms(self, cleansed_hpo_terms:List[HpTerm]):
     def pmid(self):
         return self._citation.pmid
 
-    def set_citation(self, citation:Citation):
+    def set_citation(self, citation: Citation):
         """
         :param citation: Object with the title and PubMed identifier for the publication in which this individual was described (e.g. PMID:321..)
         :type citation: Citation
         """
         self._citation = citation
 
-    def set_vital_status(self, vstatus:VitalStatus):
-        if not isinstance(vstatus, VitalStatus):
+    def set_vital_status(self, vstatus: VitalStatus202):
+        if not isinstance(vstatus, VitalStatus202):
             raise ValueError(f"vstatus argument must be pyphetools.pp.v202.VitalStatus but was{type(vstatus)}")
         self._vital_status = vstatus
 
+    def get_vital_status(self) -> VitalStatus202:
+        return self._vital_status
+
     def get_phenopacket_id(self, phenopacket_id=None) -> str:
         """
         :returns: the Phenopacket identifier for this individual
@@ -220,7 +210,7 @@ def get_phenopacket_id(self, phenopacket_id=None) -> str:
         else:
             ppkt_id = phenopacket_id
         # strip non alphanumeric characters
-        ppkt_id = ''.join(e if e.isalnum()  else "_" for e in ppkt_id)
+        ppkt_id = ''.join(e if e.isalnum() else "_" for e in ppkt_id)
         ppkt_id = ppkt_id.replace("__", "_")
         if ppkt_id.endswith("_"):
             ppkt_id = ppkt_id[:-1]
@@ -233,8 +223,7 @@ def get_citation(self) -> Citation:
         """
         return self._citation
 
-
-    def _get_onset(self) -> Union[PyPheToolsAge,str]:
+    def _get_onset(self) -> typing.Optional[TimeElement202]:
         """The assumption of this method is that if we have a valid age of onset field, use this.
         Otherwise, try to find an age of onset from the phenotypic features and take the youngest age
         """
@@ -246,15 +235,13 @@ def _get_onset(self) -> Union[PyPheToolsAge,str]:
                 if not hp.measured:
                     continue
                 pf = hp.to_ga4gh_phenotypic_feature()
-                if pf.onset.age.iso8601duration is None and self._age_of_onset != Constants.NOT_PROVIDED:
-                    phenotypic_feature_onsets.add(pf.onset.age.iso8601duration)
+                if pf.onset.age.iso8601duration is None:
+                    phenotypic_feature_onsets.add(pf.onset)
         if len(phenotypic_feature_onsets) == 0:
-            return Constants.NOT_PROVIDED
-        age_format_list = list()
-        for o in phenotypic_feature_onsets:
-            age_format_list.append(IsoAge.from_iso8601(o))
-        sorted_age = sorted(age_format_list, lambda x: x.total_days)
-        youngest_age = sorted_age[0]
+            return None
+        age_format_list = list(phenotypic_feature_onsets)
+        sorted_age_list = AgeSorter.sort_by_age(age_format_list)
+        youngest_age = sorted_age_list[0]
         return youngest_age
 
     def _get_disease_object(self):
@@ -269,14 +256,12 @@ def _get_disease_object(self):
             print("[WARNING] could not find disease information")
         disease_object = PPKt.Disease()
         disease_object.term.CopyFrom(disease_term)
-        if self.age_of_onset is not None and self.age_of_onset.is_valid():
-            disease_object.onset.CopyFrom(self.age_of_onset.to_ga4gh_time_element())
-        if iso_age is not None and iso_age.is_valid():
-            disease_object.onset.CopyFrom(iso_age.to_ga4gh_time_element())
+        if self.age_of_onset is not None:
+            disease_object.onset.CopyFrom(self.age_of_onset.to_message())
+        if iso_age is not None:
+            disease_object.onset.CopyFrom(iso_age.to_message())
         return disease_object
 
-
-
     def to_ga4gh_phenopacket(self, metadata, phenopacket_id=None) -> PPKt.Phenopacket:
         """
         Transform the data into GA4GH Phenopacket format
@@ -286,12 +271,13 @@ def to_ga4gh_phenopacket(self, metadata, phenopacket_id=None) -> PPKt.Phenopacke
         if isinstance(metadata, MetaData):
             metadata = metadata.to_ga4gh()
         if not isinstance(metadata, PPKt.MetaData):
-            raise ValueError(f"metadata argument must be pyphetools.MetaData or GA4GH MetaData but was {type(metadata)}")
+            raise ValueError(
+                f"metadata argument must be pyphetools.MetaData or GA4GH MetaData but was {type(metadata)}")
         php = PPKt.Phenopacket()
         php.id = self.get_phenopacket_id(phenopacket_id=phenopacket_id)
         php.subject.id = self._individual_id
-        if self._age_at_last_encounter is not None and self._age_at_last_encounter.is_valid():
-            php.subject.time_at_last_encounter.CopyFrom(self._age_at_last_encounter.to_ga4gh_time_element())
+        if self._age_at_last_encounter is not None:
+            php.subject.time_at_last_encounter.CopyFrom(self._age_at_last_encounter.to_message())
         if self._sex == Constants.MALE_SYMBOL:
             php.subject.sex = PPKt.Sex.MALE
         elif self._sex == Constants.FEMALE_SYMBOL:
@@ -359,7 +345,7 @@ def __str__(self):
         return f"{self._individual_id}: {self._age_of_onset}, {self._sex}: {self._disease} {hpo_str}"
 
     @staticmethod
-    def output_individuals_as_phenopackets(individual_list, metadata:MetaData, outdir="phenopackets"):
+    def output_individuals_as_phenopackets(individual_list, metadata: MetaData, outdir="phenopackets"):
         """write a list of Individual objects to file in GA4GH Phenopacket format
 
         This methods depends on the MetaData object having a PMID and will fail otherwise
@@ -377,7 +363,8 @@ def output_individuals_as_phenopackets(individual_list, metadata:MetaData, outdi
             os.makedirs(outdir)
         written = 0
         if not isinstance(metadata, MetaData):
-            raise ValueError(f"metadata argument must be pyphetools MetaData object (not GA4GH metadata message), but was {type(metadata)}")
+            raise ValueError(
+                f"metadata argument must be pyphetools MetaData object (not GA4GH metadata message), but was {type(metadata)}")
         pmid = metadata.get_pmid()
         for individual in individual_list:
             phenopckt = individual.to_ga4gh_phenopacket(metadata=metadata)
@@ -395,13 +382,11 @@ def output_individuals_as_phenopackets(individual_list, metadata:MetaData, outdi
                 written += 1
         print(f"We output {written} GA4GH phenopackets to the directory {outdir}")
 
-
-
     @staticmethod
-    def from_ga4gh_metadata(mdata:PPKt.MetaData) -> MetaData:
+    def from_ga4gh_metadata(mdata: PPKt.MetaData) -> MetaData:
         created_by = mdata.created_by
         created_time = str(mdata.created)
-        if len (mdata.external_references) > 1:
+        if len(mdata.external_references) > 1:
             raise ValueError("multiple external references not supported")
         elif len(mdata.external_references) == 0:
             id = None
@@ -414,13 +399,14 @@ def from_ga4gh_metadata(mdata:PPKt.MetaData) -> MetaData:
             description = eref.description
         resource_list = []
         for resource in mdata.resources:
-            resource_id=resource.id
+            resource_id = resource.id
             name = resource.name
             namespace_prefix = resource.namespace_prefix
             iri_prefix = resource.iri_prefix
             url = resource.url
             version = resource.version
-            r = Resource(resource_id=resource_id,name=name, namespace_prefix=namespace_prefix, iriprefix=iri_prefix, url=url, version=version)
+            r = Resource(resource_id=resource_id, name=name, namespace_prefix=namespace_prefix, iriprefix=iri_prefix,
+                         url=url, version=version)
             resource_list.append(r)
         cite = Citation(pmid=id, title=description)
         metadata = MetaData(created_by=created_by, citation=cite)
@@ -429,7 +415,7 @@ def from_ga4gh_metadata(mdata:PPKt.MetaData) -> MetaData:
         return metadata
 
     @staticmethod
-    def get_variants_and_disease(ppkt:PPKt.Phenopacket):
+    def get_variants_and_disease(ppkt: PPKt.Phenopacket):
         """extract the pyphetools Disease object and the VariantInterpretation objects that can be used to construct an Individual
 
         :param ppkt: a GA4GH phenopacket
@@ -441,7 +427,8 @@ def get_variants_and_disease(ppkt:PPKt.Phenopacket):
             print(f"No interpretation found for {ppkt.id}")
             return None, []
         if len(ppkt.interpretations) > 1:
-            raise ValueError(f"pyphetools dpoes not currently support multiple Interpretation messages in one phenopacket but we found {len(ppkt.interpretations)}")
+            raise ValueError(
+                f"pyphetools dpoes not currently support multiple Interpretation messages in one phenopacket but we found {len(ppkt.interpretations)}")
         interpretation = ppkt.interpretations[0]
         if interpretation.HasField("diagnosis") and interpretation.diagnosis.HasField("disease"):
             d = interpretation.diagnosis.disease
@@ -450,17 +437,16 @@ def get_variants_and_disease(ppkt:PPKt.Phenopacket):
             disease = None
         if len(interpretation.diagnosis.genomic_interpretations) == 0:
             return disease, []
-        else :
+        else:
             variant_list = []
             for gen_interpretation in interpretation.diagnosis.genomic_interpretations:
                 variant_list.append(gen_interpretation.variant_interpretation)
             return disease, variant_list
 
-
     @staticmethod
-    def from_ga4gh_phenopacket(ppkt:PPKt.Phenopacket):
+    def from_ga4gh_phenopacket(ppkt: PPKt.Phenopacket):
         """
-        Transform a GA4GH Phenopacket into an Individual obect -- useful for testing
+        Transform a GA4GH Phenopacket into an Individual object -- useful for testing
         :returns:  an individual object corresponding to the GA4GH Phenopacket
         :rtype: Individual
         """
@@ -468,33 +454,31 @@ def from_ga4gh_phenopacket(ppkt:PPKt.Phenopacket):
             raise ValueError(f"argument must be a GA4GH Phenopacket Message but was {type(ppkt)}")
         #metadata = ppkt.meta_data
         #pypt_metadata = Individual.from_ga4gh_metadata(mdata=metadata)
-        subject_id =  ppkt.subject.id
+        subject_id = ppkt.subject.id
         sex = ppkt.subject.sex
         age_at_last_encounter = ppkt.subject.time_at_last_encounter.age.iso8601duration
-        age_of_onset = NoneAge("na")
+        age_of_onset = None
         if len(ppkt.diseases) > 0:
             d = ppkt.diseases[0]
             if d.HasField("onset") and d.onset.HasField("age"):
-                age_of_onset = d.onset.age
+                age_of_onset = TimeElement202.from_message(d.onset.age)
         hpo_terms = []
         for pf in ppkt.phenotypic_features:
             hpo_id = pf.type.id
             hpo_label = pf.type.label
             observed = not pf.excluded
-            onset_age = NoneAge("na")
+            onset_age = None
             if pf.HasField("onset"):
-                onset = pf.onset
-                if onset.HasField("age") and onset.age.HasField("iso8601duration"):
-                        onset_age = pf.onset.age.iso8601duration
+                onset_age = TimeElement202.from_message(pf.onset)
             hpo_terms.append(HpTerm(hpo_id=hpo_id, label=hpo_label, observed=observed, onset=onset_age))
         disease, var_list = Individual.get_variants_and_disease(ppkt)
         indi = Individual(individual_id=subject_id,
-                            hpo_terms=hpo_terms,
-                            citation=None,
-                            sex=sex,
-                            age_of_onset=age_of_onset,
-                            age_at_last_encounter=age_at_last_encounter,
-                            interpretation_list=var_list)
+                          hpo_terms=hpo_terms,
+                          citation=None,
+                          sex=sex,
+                          age_of_onset=age_of_onset,
+                          age_at_last_encounter=age_at_last_encounter,
+                          interpretation_list=var_list)
         if disease is not None:
             indi.set_disease(disease=disease)
         return indi
diff --git a/src/pyphetools/creation/pyphetools_age.py b/src/pyphetools/creation/pyphetools_age.py
index 7beb739e..7ac059a8 100644
--- a/src/pyphetools/creation/pyphetools_age.py
+++ b/src/pyphetools/creation/pyphetools_age.py
@@ -1,13 +1,23 @@
 import math
 import abc
 import re
+import typing
+import numpy as np
+
 DAYS_IN_WEEK = 7
 AVERAGE_DAYS_IN_MONTH = 30.437
 AVERAGE_DAYS_IN_YEAR = 365.25
-import phenopackets as PPKt
 
-from .constants import Constants
 
+from ..pp.v202 import OntologyClass as OntologyClass202
+from ..pp.v202 import TimeElement as TimeElement202
+from ..pp.v202 import Age as Age202
+from ..pp.v202 import GestationalAge as GestationalAge202
+from ..pp.v202 import AgeRange as AgeRange202
+from ..pp.v202 import Timestamp as Timestamp202
+from ..pp.v202 import TimeInterval as TimeInterval202
+
+from .constants import Constants
 
 # The following terms are to simplify making HpoAge objects
 HPO_ONSET_TERMS = {
@@ -16,7 +26,7 @@
     # Onset of symptoms after the age of 40 years.
     "Middle age onset": "HP:0003596",
     # Onset of symptoms after the age of 16 years.
-    "Young adult onset":"HP:0011462",
+    "Young adult onset": "HP:0011462",
     # Onset of disease at an age of greater than or equal to 25 to under 40 years.
     "Late young adult onset": "HP:0025710",
     # Onset of disease at an age of greater than or equal to 19 to under 25 years.
@@ -24,15 +34,15 @@
     # Onset of disease at an age of greater than or equal to 16 to under 19 years.
     "Early young adult onset": "HP:0025708",
     # Onset of disease after 16 years  .
-    "Adult onset":  "HP:0003581",
+    "Adult onset": "HP:0003581",
     #Onset of signs or symptoms of disease between the age of 5 and 15 years.
     "Juvenile onset": "HP:0003621",
     #Onset of disease at the age of between 1 and 5 years.
-    "Childhood onset":"HP:0011463",
+    "Childhood onset": "HP:0011463",
     # Onset of signs or symptoms of disease between 28 days to one year of life.
     "Infantile onset": "HP:0003593",
     # Onset of signs or symptoms of disease within the first 28 days of life.
-    "Neonatal onset":"HP:0003623",
+    "Neonatal onset": "HP:0003623",
     # A phenotypic abnormality that is present at birth.
     "Congenital onset": "HP:0003577",
     #  onset prior to birth
@@ -42,14 +52,168 @@
     # Onset prior to birth but after 8 weeks of embryonic development (corresponding to a gestational age of 10 weeks).
     "Fetal onset": "HP:0011461",
     #late first trimester during the early fetal period, which is defined as 11 0/7 to 13 6/7 weeks of gestation (inclusive).
-    "Late first trimester onset":"HP:0034199",
+    "Late first trimester onset": "HP:0034199",
     # second trimester, which comprises the range of gestational ages from 14 0/7 weeks to 27 6/7 (inclusive)
-    "Second trimester onset":"HP:0034198",
+    "Second trimester onset": "HP:0034198",
     #third trimester, which is defined as 28 weeks and zero days (28+0) of gestation and beyond.
-    "Third trimester onset":  "HP:0034197",
+    "Third trimester onset": "HP:0034197",
 }
 
 
+class AgeSorter:
+    MOST_NEGATIVE_INT32 = np.iinfo(np.int32).min
+
+    ISO8601_REGEX = r"^P(\d+Y)?(\d+M)?(\d+D)?"
+
+    HPO_AGE_TO_DAYS = {
+        "Antenatal onset": -1,
+        "Embryonal onset": -7 * 40,
+        "Fetal onset": -7 * 29,
+        "Late first trimester onset": -7 * 29,
+        "Second trimester onset": -7 * 26,
+        "Third trimester onset": -7 * 22,
+        "Congenital onset": 0,
+        "Neonatal onset": 1,
+        "Pediatrial onset": 29,
+        "Infantile onset": 29,
+        "Childhood onset": 365.25,
+        "Juvenile onset": 5 * 365.25,
+        "Adult onset": 16 * 365.25,
+        "Young adult onset": 16 * 365.25,
+        "Early young adult onset": 16 * 365.25,
+        "Intermediate young adult onset": 19 * 365.25,
+        "Late young adult onset": 25 * 365.25,
+        "Middle age onset": 40 * 365.25,
+        "Late onset": 60 * 365.25,
+    }
+
+    HPO_AGE_TO_YEARS = {
+        "Antenatal onset": 0,
+        "Embryonal onset": 0,
+        "Fetal onset": 0,
+        "Late first trimester onset": 0,
+        "Second trimester onset": 0,
+        "Third trimester onset": 0,
+        "Congenital onset": 0,
+        "Neonatal onset": 0,
+        "Pediatrial onset": 0,
+        "Infantile onset": 0,
+        "Childhood onset": 1,
+        "Juvenile onset": 5,
+        "Adult onset": 16,
+        "Young adult onset": 16,
+        "Early young adult onset": 16 ,
+        "Intermediate young adult onset": 19,
+        "Late young adult onset": 25 ,
+        "Middle age onset": 40 ,
+        "Late onset": 60,
+    }
+
+    def __init__(self,
+                time_element: typing.Union[
+                    GestationalAge202, Age202, AgeRange202, OntologyClass202, Timestamp202, TimeInterval202]
+        ):
+        if not isinstance(time_element, TimeElement202):
+            time_element = TimeElement202.from_message(time_element)
+        element = time_element.element
+        if isinstance(element, GestationalAge202):
+            days = 7 * element.weeks + element.days
+            self._num_days = -1 * days
+            self._num_years = 0
+        elif isinstance(element, Age202):
+            age_str = element.iso8601duration
+            match = re.search(AgeSorter.ISO8601_REGEX, age_str)
+            if match:
+                years = match.group(1)
+                months = match.group(2)
+                days = match.group(3)
+                self._num_days = AgeSorter.get_days_from_match(years, months, days)
+                self._num_years = AgeSorter.get_years_from_match(years, months, days)
+            else:
+                self._num_days = 0
+                self._num_years = 0
+        elif isinstance(element, AgeRange202):
+            age_str = element.start.iso8601duration
+            match = re.search(AgeSorter.ISO8601_REGEX, age_str)
+            if match:
+                years = match.group(1)
+                months = match.group(2)
+                days = match.group(3)
+                self._num_days = AgeSorter.get_days_from_match(years, months, days)
+                self._num_years = AgeSorter.get_years_from_match(years, months, days)
+            else:
+                self._num_days = 0
+                self._num_years = 0
+        elif isinstance(element, OntologyClass202):
+            if element.label not in AgeSorter.HPO_AGE_TO_DAYS:
+                raise ValueError(f"Could not find HPO class for {element.label}")
+            self._num_days = AgeSorter.HPO_AGE_TO_DAYS.get(element.label)
+            self._num_years = AgeSorter.HPO_AGE_TO_YEARS.get(element.label)
+        elif isinstance(element, Timestamp202):
+            self._num_days = AgeSorter.MOST_NEGATIVE_INT32
+            self._num_years = AgeSorter.MOST_NEGATIVE_INT32
+        elif isinstance(element, TimeInterval202):
+            self._num_days = AgeSorter.MOST_NEGATIVE_INT32
+            self._num_years = AgeSorter.MOST_NEGATIVE_INT32
+        else:
+            print(f"[WARN] Unknown element type: {type(element)}")
+            self._num_days = None
+            self._num_years = None
+        
+    @staticmethod
+    def get_days_from_match(years: typing.Optional[str],
+                            months: typing.Optional[str],
+                            days: typing.Optional[str]) -> int:
+        total_days = 0
+        if years is not None:
+            # years with be something like 42Y
+            total_days += 365.25 * int(years[:-1])
+        if months is not None:
+            total_days += int(months[:-1]) * 30.436875
+        if days is not None:
+            total_days += int(days[:-1])
+        return int(total_days)
+    
+    @staticmethod
+    def get_years_from_match(years: typing.Optional[str],
+                            months: typing.Optional[str],
+                            days: typing.Optional[str]) -> float:
+        total_years = 0
+        if years is not None:
+            # years with be something like 42Y
+            total_years += float(years[:-1])
+        if months is not None:
+            total_years += float(months[:-1])/12
+        if days is not None:
+            total_years += float(days[:-1])/365.25
+        return total_years
+
+    @property
+    def element(self) -> typing.Union[
+                    GestationalAge202, Age202, AgeRange202, OntologyClass202, Timestamp202, TimeInterval202]:
+        return self._element
+
+    @property
+    def num_days(self) -> int:
+        return self._num_days
+    
+    @property
+    def num_years(self) -> float:
+        return self._num_years
+    
+    @staticmethod
+    def convert_to_years(time_elem:TimeElement202) -> float:
+        age_sorter = AgeSorter(time_element=time_elem)
+        return age_sorter.num_years
+
+    @staticmethod
+    def sort_by_age(onset_list: typing.List[TimeElement202]) -> typing.List[TimeElement202]:
+        agesorter_list = [AgeSorter(x) for x in onset_list]
+        sorted_list = sorted(agesorter_list, key=lambda x: x.num_days)
+        sorted_time_elements = [x.element for x in sorted_list]
+        return sorted_time_elements
+
+
 class PyPheToolsAge(metaclass=abc.ABCMeta):
     """Class for managing the various ways we have of representing Age as either an ISI 8601 string,
     a gestational age, or an HPO onset term.
@@ -58,13 +222,6 @@ class PyPheToolsAge(metaclass=abc.ABCMeta):
     def __init__(self, age_string) -> None:
         self._age_string = age_string
 
-    @abc.abstractmethod
-    def to_ga4gh_time_element(self) -> PPKt.TimeElement:
-        """
-        :returns: a representation of Age formated as one of the options of GA4GH TimeElement
-        :rtype: PPKt.TimeElement
-        """
-        pass
 
     @abc.abstractmethod
     def is_valid(self) -> bool:
@@ -86,67 +243,40 @@ def to_hpo_age(self) -> "HpoAge":
         """
         pass
 
-    @property
-    def age_string(self):
-        if self.is_valid():
-            return self._age_string
-        else:
-            return Constants.NOT_PROVIDED
+
 
     @staticmethod
-    def get_age(age_string) -> "PyPheToolsAge":
-        """Return an appropriate subclass of PyPheToolsAge or None
-        - if starts with P interpret as an ISO 8601 age string
-        - if starts with HP interpret as an HPO Onset term
-        - if is a string such as 34+2 interpret as a gestation age
-        - If we cannot parse, return a NoneAge obejct, a signal that no age is available
-        :returns:PyPheToolsAge object (one of the subclasses)
-        :rtype: PyPheToolsAge
+    def get_age_pp201(age_string: str) -> typing.Optional[TimeElement202]:
+        """
+        Encode the age string as a TimeElement if possible
         """
-        if age_string is None:
-            return NoneAge("na")
+        if age_string is None or len(age_string) == 0:
+            return None
         if isinstance(age_string, float) and math.isnan(age_string):
-            return NoneAge("na") # sometimes pandas returns an empty cell as a float NaN
-        if len(age_string) == 0:
-            return NoneAge("na")
-        elif age_string.startswith("P"):
-            return IsoAge.from_iso8601(age_string)
+            return None  # sometimes pandas returns an empty cell as a float NaN
+        if age_string.startswith("P"):
+            return TimeElement202(Age202(age_string))
         elif age_string in HPO_ONSET_TERMS:
-            return HpoAge(hpo_onset_label=age_string)
+            hpo_id = HPO_ONSET_TERMS.get(age_string)
+            onsetClz = OntologyClass202(id=hpo_id, label=age_string)
+            return TimeElement202(onsetClz)
         elif GestationalAge.is_gestational_age(age_string):
-            return GestationalAge(age_string)
+            ga = GestationalAge(age_string)
+            ga202 = GestationalAge202(weeks=ga.weeks, days=ga.days)
+            return TimeElement202(ga202)
         else:
             # only warn if the user did not enter na=not available
             if age_string != 'na':
                 raise ValueError(f"Could not parse \"{age_string}\" as age.")
-            return NoneAge(age_string=age_string)
+            return None
 
 
 
-class NoneAge(PyPheToolsAge):
-    """class to be used if no age information was available
-    """
-    def __init__(self, age_string:str):
-        super().__init__(age_string)
-
-    def to_ga4gh_time_element(self):
-        return None
-
-    def is_valid(self):
-        return False
-
-    def to_hpo_age(self):
-        """There is no information about age, so return the NoneAge object to denote this.
-        Client code should always check the is_valid function
-        """
-        return self
-
-
 class IsoAge(PyPheToolsAge):
     """Class to record and sort ages formated according to iso8601
     """
 
-    def __init__(self, age_string:str, y=None, m=None, w=None, d=None):
+    def __init__(self, age_string: str, y=None, m=None, w=None, d=None):
         super().__init__(age_string)
         total_days = 0
         if y is None:
@@ -172,7 +302,8 @@ def __init__(self, age_string:str, y=None, m=None, w=None, d=None):
         if days > AVERAGE_DAYS_IN_MONTH:
             extra_months = math.floor(days / AVERAGE_DAYS_IN_MONTH)
             months = months + int(extra_months)
-            days = days % int(AVERAGE_DAYS_IN_MONTH)  # modulo arithmetic, get remaining days after months are subtracted
+            days = days % int(
+                AVERAGE_DAYS_IN_MONTH)  # modulo arithmetic, get remaining days after months are subtracted
         if months > 11:
             extra_years = months // 12
             months = months % 12
@@ -210,7 +341,7 @@ def to_iso8601(self):
         if self._days > 0:
             components.append(f"{self._days}D")
         if len(components) == 1:
-            return "P0D" # newborn
+            return "P0D"  # newborn
         else:
             return "".join(components)
 
@@ -237,20 +368,8 @@ def to_hpo_age(self):
             raise ValueError(f"[ERROR] Could not calculate HpoAge for {self.age_string}")
 
 
-    def to_ga4gh_time_element(self) -> PPKt.TimeElement:
-        """
-        :returns: a representation of Age formated as one of the options of GA4GH TimeElement
-        :rtype: PPKt.TimeElement
-        """
-        time_elem = PPKt.TimeElement()
-        iso8601_age = self.to_iso8601()
-        if iso8601_age is None:
-            raise ValueError(f"iso8601 was None")
-        time_elem.age.iso8601duration  = iso8601_age
-        return time_elem
-
     @staticmethod
-    def from_iso8601(iso_age:str):
+    def from_iso8601(iso_age: str):
         """
         :returns: IsoAge object representing the years, months, and days of the Age
         :rtype: IsoAge
@@ -258,23 +377,23 @@ def from_iso8601(iso_age:str):
         original_age_string = iso_age
         if not iso_age.startswith("P"):
             raise ValueError(f"Malformed isoage string {iso_age}")
-        iso_age = iso_age[1:] # remove P
+        iso_age = iso_age[1:]  # remove P
         y_idx = iso_age.find("Y")
         if y_idx > 0:
             y = int(iso_age[:y_idx])
-            iso_age = iso_age[(1+y_idx):]
+            iso_age = iso_age[(1 + y_idx):]
         else:
             y = 0
         m_idx = iso_age.find("M")
         if m_idx > 0:
             m = int(iso_age[:m_idx])
-            iso_age = iso_age[(1+m_idx):]
+            iso_age = iso_age[(1 + m_idx):]
         else:
             m = 0
         w_idx = iso_age.find("W")
         if w_idx > 0:
             w = int(iso_age[:w_idx])
-            iso_age = iso_age[(1+w_idx):]
+            iso_age = iso_age[(1 + w_idx):]
         else:
             w = 0
         d_idx = iso_age.find("D")
@@ -284,6 +403,7 @@ def from_iso8601(iso_age:str):
             d = 0
         return IsoAge(y=y, m=m, w=w, d=d, age_string=original_age_string)
 
+
 class HpoAge(PyPheToolsAge):
     def __init__(self, hpo_onset_label) -> None:
         super().__init__(age_string=hpo_onset_label)
@@ -292,18 +412,6 @@ def __init__(self, hpo_onset_label) -> None:
         self._onset_label = hpo_onset_label
         self._onset_id = HPO_ONSET_TERMS.get(hpo_onset_label)
 
-    def to_ga4gh_time_element(self) -> PPKt.TimeElement:
-        """
-        :returns: a representation of Age formated as an OntologyClass (HPO Onset term)
-        :rtype: PPKt.TimeElement
-        """
-        time_elem = PPKt.TimeElement()
-        clz = PPKt.OntologyClass()
-        clz.id = self._onset_id
-        clz.label = self._onset_label
-        time_elem.ontology_class.CopyFrom(clz)
-        return time_elem
-
     def to_hpo_age(self):
         """Return self, this is already an HpoAge object
         """
@@ -312,6 +420,7 @@ def to_hpo_age(self):
     def is_valid(self):
         return True
 
+
 class GestationalAge(PyPheToolsAge):
 
     def __init__(self, age_string) -> None:
@@ -323,30 +432,27 @@ def __init__(self, age_string) -> None:
         else:
             raise ValueError(f"Could not extract gestation age from \"{age_string}\".")
 
+    @property
+    def weeks(self):
+        return self._weeks
+
+    @property
+    def days(self):
+        return self._days
+
     def is_valid(self):
         return True
 
-    def to_ga4gh_time_element(self) -> PPKt.TimeElement:
-        """
-        :returns: a representation of Age formated as an OntologyClass (HPO Onset term)
-        :rtype: PPKt.TimeElement
-        """
-        time_elem = PPKt.TimeElement()
-        gest_age = PPKt.GestationalAge()
-        gest_age.weeks = self._weeks
-        gest_age.days = self._days
-        time_elem.age.gestational_age.CopyFrom(gest_age)
-        return time_elem
 
     def to_hpo_age(self):
         """Return self, this is already an HpoAge object
         """
         if self._weeks >= 28:
             # prior to birth during the third trimester, which is defined as 28 weeks and zero days (28+0) of gestation and beyond.
-            return HpoAge("Third trimester onset") # HP:0034197
+            return HpoAge("Third trimester onset")  # HP:0034197
         elif self._weeks >= 14:
             # prior to birth during the second trimester, which comprises the range of gestational ages from 14 0/7 weeks to 27 6/7 (inclusive).
-            return HpoAge("Second trimester onset") # HP:0034198
+            return HpoAge("Second trimester onset")  # HP:0034198
         elif self._weeks >= 11:
             # 11 0/7 to 13 6/7 weeks of gestation (inclusive).
             return HpoAge("Late first trimester onset")  #  HP:0034199
diff --git a/src/pyphetools/pp/v202/_base.py b/src/pyphetools/pp/v202/_base.py
index 87295f37..20dcc40b 100644
--- a/src/pyphetools/pp/v202/_base.py
+++ b/src/pyphetools/pp/v202/_base.py
@@ -91,6 +91,9 @@ def __eq__(self, other):
             and self._id == other._id \
             and self._label == other._label
 
+    def __hash__(self):
+        return hash((self._id, self._label,))
+
     def __repr__(self):
         return f'OntologyClass(id={self._id}, label={self._label})'
 
@@ -343,6 +346,9 @@ def __eq__(self, other):
             and self._weeks == other._weeks \
             and self._days == other._days
 
+    def __hash__(self):
+        return hash((self._weeks, self._days))
+
     def __repr__(self):
         return f'GestationalAge(weeks={self._weeks}, days={self._days})'
 
@@ -400,6 +406,9 @@ def __eq__(self, other):
         return isinstance(other, Age) \
             and self._iso8601duration == other._iso8601duration
 
+    def __hash__(self):
+        return hash((self._iso8601duration,))
+
     def __repr__(self):
         return f'Age(iso8601duration={self._iso8601duration})'
 
@@ -473,6 +482,9 @@ def __eq__(self, other):
             and self._start == other._start \
             and self._end == other._end
 
+    def __hash__(self):
+        return hash((self._start, self._end))
+
     def __repr__(self):
         return f'AgeRange(start={self._start}, end={self._end})'
 
@@ -546,6 +558,9 @@ def __eq__(self, other):
             and self._start == other._start \
             and self._end == other._end
 
+    def __hash__(self):
+        return hash((self._start, self._end))
+
     def __repr__(self):
         return f'TimeInterval(start={self._start}, end={self._end})'
 
@@ -679,6 +694,9 @@ def from_message(cls, msg: Message):
     def __eq__(self, other):
         return isinstance(other, TimeElement) and self._element == other._element
 
+    def __hash__(self):
+        return hash((self._element,))
+
     def __repr__(self):
         return f'TimeElement(element={self._element})'
 
diff --git a/src/pyphetools/pp/v202/_individual.py b/src/pyphetools/pp/v202/_individual.py
index 3804e9f9..5db5eebf 100644
--- a/src/pyphetools/pp/v202/_individual.py
+++ b/src/pyphetools/pp/v202/_individual.py
@@ -82,6 +82,8 @@ def __init__(
             survival_time_in_days: typing.Optional[int] = None,
     ):
         self._status = status
+        if time_of_death is not None:
+            assert isinstance(time_of_death, TimeElement)
         self._time_of_death = time_of_death
         self._cause_of_death = cause_of_death
         self._survival_time_in_days = survival_time_in_days
diff --git a/src/pyphetools/validation/ontology_qc.py b/src/pyphetools/validation/ontology_qc.py
index 745ce23b..930766d6 100644
--- a/src/pyphetools/validation/ontology_qc.py
+++ b/src/pyphetools/validation/ontology_qc.py
@@ -1,11 +1,10 @@
 import hpotk
 from collections import Counter
-from typing import List, Optional, Set
+from typing import List, Optional
 from ..creation.hp_term import HpTerm
 from .validation_result import ValidationResult, ValidationResultBuilder
 from collections import defaultdict
 from ..creation.individual import Individual
-from ..creation.constants import Constants
 
 
 
@@ -18,7 +17,11 @@ class OntologyQC:
 
     """
 
-    def __init__(self, ontology:hpotk.MinimalOntology, individual:Individual, fix_conflicts=True, fix_redundancies=True):
+    def __init__(self,
+                 ontology:hpotk.MinimalOntology,
+                 individual:Individual,
+                 fix_conflicts=True,
+                 fix_redundancies=True):
         self._ontology = ontology
         self._individual = individual
         self._phenopacket_id = individual.get_phenopacket_id()
@@ -28,7 +31,9 @@ def __init__(self, ontology:hpotk.MinimalOntology, individual:Individual, fix_co
         self._clean_hpo_terms = self._clean_terms()
 
 
-    def _fix_conflicts(self, observed_hpo_terms:List[HpTerm], excluded_hpo_terms) -> List[HpTerm]:
+    def _fix_conflicts(self,
+                       observed_hpo_terms:List[HpTerm],
+                       excluded_hpo_terms) -> List[HpTerm]:
         """
         This class detects excluded superclasses that have observed subclasses -- a conflict.
 
@@ -72,7 +77,8 @@ def _fix_conflicts(self, observed_hpo_terms:List[HpTerm], excluded_hpo_terms) ->
 
 
 
-    def _fix_redundancies(self, hpo_terms:List[HpTerm]) -> List[HpTerm]:
+    def _fix_redundancies(self,
+                          hpo_terms:List[HpTerm]) -> List[HpTerm]:
         """
         Remove redundant terms from a list of HPO terms.
 
@@ -108,7 +114,8 @@ def _fix_redundancies(self, hpo_terms:List[HpTerm]) -> List[HpTerm]:
         return non_redundant_terms
 
 
-    def _check_term_ids_and_labels(self, hpo_terms:List[HpTerm]) -> None:
+    def _check_term_ids_and_labels(self,
+                                   hpo_terms:List[HpTerm]) -> None:
         """
         Check whether the term identifiers (e.g., HP:0001234) are present in the ontology as primary ids and whether
         the label matches the current priumary label; if not, flag the errors in self._errors
@@ -131,7 +138,7 @@ def _clean_terms(self) -> List[HpTerm]:
         :returns: list of HPO terms without redundancies/conflicts
         :rtype hpo_terms: List[HpTerm]
         """
-        by_age_dictionary =  defaultdict(list)
+        by_age_dictionary = defaultdict(list)
         # collect all terms without a defined age of onset
         # We will assume these terms exist at all specific ages of onset, thus we need this to calculate redundancy
         observed_terms_without_onset = list()
@@ -140,8 +147,8 @@ def _clean_terms(self) -> List[HpTerm]:
             if not term.measured:
                 self._errors.append(ValidationResultBuilder(self._phenopacket_id).not_measured(term=term).build())
             else:
-                if term.onset.is_valid():
-                    by_age_dictionary[term.onset.age_string].append(term)
+                if term.onset is not None:
+                    by_age_dictionary[term.onset].append(term)
                 else:
                     if term.observed:
                         observed_terms_without_onset.append(term)
diff --git a/src/pyphetools/visualization/__init__.py b/src/pyphetools/visualization/__init__.py
index 38077517..62f6c7e9 100644
--- a/src/pyphetools/visualization/__init__.py
+++ b/src/pyphetools/visualization/__init__.py
@@ -2,6 +2,7 @@
 from .focus_count_table import FocusCountTable
 from .hpoa_table_creator import HpoaTableCreator, HpoaTableBuilder
 from .individual_table import IndividualTable
+from .kaplan_meier_visualizer import KaplanMeierVisualizer
 from .phenopacket_charts import PhenopacketCharts
 from .phenopacket_ingestor import PhenopacketIngestor
 from .phenopacket_table import PhenopacketTable
diff --git a/src/pyphetools/visualization/individual_table.py b/src/pyphetools/visualization/individual_table.py
index 209aae3d..7e49dbfc 100644
--- a/src/pyphetools/visualization/individual_table.py
+++ b/src/pyphetools/visualization/individual_table.py
@@ -126,7 +126,8 @@ def _simple_patient_to_table_row(self, spat:SimplePatient) -> List[str]:
         """
         row_items = []
         # Patient information
-        pat_info = spat.get_subject_id() + " (" + spat.get_sex() + "; " + spat.get_age() + ")"
+        age_string = spat.get_age() or "age: n/a"
+        pat_info = spat.get_subject_id() + " (" + spat.get_sex() + "; " + age_string + ")"
         row_items.append( pat_info)
         row_items.append( spat.get_disease())
         # Variant information
diff --git a/src/pyphetools/visualization/kaplan_meier_visualizer.py b/src/pyphetools/visualization/kaplan_meier_visualizer.py
new file mode 100644
index 00000000..5633f7f6
--- /dev/null
+++ b/src/pyphetools/visualization/kaplan_meier_visualizer.py
@@ -0,0 +1,113 @@
+import typing
+import hpotk
+import numpy as np
+from pyphetools.visualization.simple_patient import SimplePatient
+
+
+
+class KaplanMeierVisualizer:
+    """
+    Display a Kaplan Meier (survival) curve for a cohort with respect to the age of onset of a specific feature.
+    For instance
+
+        from pyphetools.visualization import KaplanMeierVisualizer, PhenopacketIngestor, SimplePatient
+        from lifelines import KaplanMeierFitter
+        umod_dir = "../phenopackets/" # directory containing phenopackets to plot
+        ingestor = PhenopacketIngestor(indir=umod_dir)
+        ppkt_list = ingestor.get_phenopacket_list()
+        simple_pt_list = [SimplePatient(ppkt) for ppkt in ppkt_list]
+        hpo_id = "HP:0003774" # TermId of HPO term for the KM plot
+        kmv = KaplanMeierVisualizer(simple_patient_list=simple_pt_list, target_tid=stage5crd)
+        T, E = kmv.get_time_and_event()
+        # plot Kaplan Meier curve
+        kmf = KaplanMeierFitter()
+        kmf.fit(T, E, label="Age at stage 5 kidney disease")
+        ax = kmf.plot_survival_function()
+        ax.set_xlabel("Years");
+    """
+    def __init__(self, 
+                 simple_patient_list: typing.List[SimplePatient], 
+                 target_tid: typing.Union[str, hpotk.TermId] = None) -> None:
+        """
+        The goal of this class is to provide a data for a visualization as a KaplanMeier survival curve with respect to 
+        the age of onset of a specific HPO term (feature of the disease).
+        TODO - also add support for survival curves with GA4GH Phenopacket VitalStatus element.
+        This will be performed if the target term is None
+        """
+        if target_tid is None:
+            self._T, self._E = self._get_time_and_event_for_vital_status(simple_patient_list=simple_patient_list)
+        else:
+            self._T, self._E = self._get_time_and_event_for_hpo_term(target_tid=target_tid, simple_patient_list=simple_patient_list)
+            
+        
+       
+
+
+    def _get_time_and_event_for_hpo_term(self, 
+                                         target_tid:str,
+                                         simple_patient_list: typing.List[SimplePatient]) -> typing.Tuple[typing.List[int], typing.List[int]]:
+        time_in_years = list()
+        event = list()
+        n_observed = 0
+        n_excluded = 0
+        n_invalid = 0
+        for spat in simple_patient_list:
+            years_at_last_exam = spat.get_age_in_years() ## float or None
+            if spat.contains_observed_term_id(target_tid) and spat.contains_excluded_term_id(target_tid):
+                raise ValueError(f"{spat.pat_id} listed as both observed/excluded for {target_tid}")
+            if spat.contains_observed_term_id(target_tid):
+                observed_term = spat.get_observed_term_by_id(target_tid)
+                event_age = observed_term.onset
+                event_years = SimplePatient.age_in_years(time_elem=event_age)
+                if event_years is None or np.isnan(event_years):
+                    print(f"[WARN] could not find age at event for {spat.get_phenopacket_id()} (Omitting)")
+                    continue
+                time_in_years.append(years_at_last_exam)
+                event.append(1)
+                n_observed += 1
+            elif spat.contains_excluded_term_id(target_tid):
+                if years_at_last_exam is None:
+                    print(f"[WARN] {target_tid} is excluded in {spat.get_phenopacket_id()} but we did not find a last exam age")
+                    continue
+                else:
+                    time_in_years.append(years_at_last_exam)
+                    event.append(0)
+                    n_excluded += 1
+            else:
+                print(f"[WARN] skipping {spat.get_phenopacket_id()} because target term {target_tid} was neither observed nor excluded")
+                n_invalid += 1
+                continue
+        print(f"observed events {n_observed}, right-censored cases {n_excluded}, invalid {n_invalid}")
+        return time_in_years, event
+
+
+    def _get_time_and_event_for_vital_status(self,
+                                             simple_patient_list: typing.List[SimplePatient]) -> typing.Tuple[typing.List[int], typing.List[int]]:
+        time_in_years = list()
+        event = list()
+        n_observed = 0
+        n_excluded = 0
+        n_invalid = 0
+        for spat in simple_patient_list:
+            years_at_last_exam = spat.get_age_in_years() ## float or None
+            if years_at_last_exam is None:
+                print(f"[WARN] skipping individual, age at last encounter not available")
+                continue
+            if spat.is_deceased():
+                time_in_years.append(years_at_last_exam)
+                event.append(1)
+                n_observed += 1
+            else:
+                time_in_years.append(years_at_last_exam)
+                event.append(0)
+                n_excluded += 1
+        return time_in_years, event      
+
+
+
+        
+    def get_time_and_event(self) -> typing.Tuple[typing.List[int], typing.List[int]]:
+        """
+        Return lists of times and event status suitable for plotting a Kaplan Meier curve
+        """
+        return self._T, self._E
\ No newline at end of file
diff --git a/src/pyphetools/visualization/simple_patient.py b/src/pyphetools/visualization/simple_patient.py
index 561c4ba1..855a1966 100644
--- a/src/pyphetools/visualization/simple_patient.py
+++ b/src/pyphetools/visualization/simple_patient.py
@@ -1,11 +1,18 @@
 import os
 import phenopackets
 from google.protobuf.json_format import Parse
+import typing
 import json
+import typing
 from collections import defaultdict
 from ..creation.constants import Constants
 from ..creation.hp_term import HpTerm
+from ..creation.individual import Individual
+from ..creation.pyphetools_age import AgeSorter
 from .simple_variant import SimpleVariant
+from ..pp.v202 import TimeElement as TimeElement202
+from ..pp.v202 import VitalStatus as VitalStatus202
+
 
 class SimplePatient:
     """
@@ -19,13 +26,17 @@ class SimplePatient:
     :type ga4gh_phenopacket: phenopackets.schema.v2.phenopackets_pb2.Phenopacket
     """
 
+    CONST_DECEASED = "deceased"
+    CONST_ALIVE = "alive"
+    CONST_UNKNOWN_VITAL_STATUS = "unknown vital status"
+
     def __init__(self, ga4gh_phenopacket) -> None:
         if str(type(ga4gh_phenopacket)) != "<class 'phenopackets.schema.v2.phenopackets_pb2.Phenopacket'>":
             raise ValueError(f"phenopacket argument must be GA4GH Phenopacket Schema Phenopacket but was {type(ga4gh_phenopacket)}")
         else:
             ppack = ga4gh_phenopacket
-        observed_hpo_terms = defaultdict(HpTerm)
-        excluded_hpo_terms = defaultdict(HpTerm)
+        self._observed_hpo_terms = defaultdict(HpTerm)
+        self._excluded_hpo_terms = defaultdict(HpTerm)
         self._by_age_dictionary = defaultdict(list)
         self._phenopacket_id = ppack.id
         if not ppack.HasField("subject"):
@@ -38,13 +49,7 @@ def __init__(self, ga4gh_phenopacket) -> None:
             self._subject_id = subj.id
         self._time_at_last_encounter = None
         if subj.HasField("time_at_last_encounter"):
-            time_at_last_encounter = phenopackets.TimeElement()
-            time_at_last_encounter.CopyFrom(subj.time_at_last_encounter)
-            if time_at_last_encounter.HasField("age"):
-                self._time_at_last_encounter = time_at_last_encounter.age.iso8601duration
-            elif time_at_last_encounter.HasField("ontology_class"):
-                clz = time_at_last_encounter.ontology_class
-                self._time_at_last_encounter = f"{clz.label} ({clz.id})"
+            self._time_at_last_encounter = TimeElement202.from_message(subj.time_at_last_encounter)
         if ppack.subject.sex == phenopackets.MALE:
             self._sex = "MALE"
         elif ppack.subject.sex == phenopackets.FEMALE:
@@ -53,22 +58,36 @@ def __init__(self, ga4gh_phenopacket) -> None:
             self._sex = "OTHER"
         else:
             self._sex = "UNKNOWN"
+        ## get vital status if possible
+        self._survival_time_in_days = None
+        self._cause_of_death = None
+        self._vstat = None
+        if ppack.subject.HasField("vital_status"):
+            vstat = VitalStatus202.from_message(ppack.subject.vital_status)
+            if vstat.status == VitalStatus202.Status.DECEASED:
+                self._vstat = SimplePatient.CONST_DECEASED
+            elif vstat.status == VitalStatus202.Status.ALIVE:
+                self._vstat = SimplePatient.CONST_ALIVE
+            else:
+                self._vstat = SimplePatient.CONST_UNKNOWN_VITAL_STATUS
+            if vstat.survival_time_in_days is not None:
+                self._survival_time_in_days = vstat.survival_time_in_days
+            self._cause_of_death = vstat.cause_of_death
+       
         for pf in ppack.phenotypic_features:
-            hpterm = HpTerm(hpo_id=pf.type.id, label=pf.type.label, observed=not pf.excluded)
+            hpterm = HpTerm(hpo_id=pf.type.id, label=pf.type.label, onset=pf.onset, observed=not pf.excluded)
             if pf.excluded:
-                excluded_hpo_terms[pf.type.id] = hpterm
+                self._excluded_hpo_terms[pf.type.id] = hpterm
             else:
-                observed_hpo_terms[pf.type.id] = hpterm
-            if pf.onset is not None and pf.onset.age is not None and pf.onset.age.iso8601duration:
-                term_onset = pf.onset.age.iso8601duration
+                self._observed_hpo_terms[pf.type.id] = hpterm
+            if pf.onset is not None:
+                telem = TimeElement202.from_message(pf.onset)
+                self._by_age_dictionary[telem].append(hpterm)
             else:
-                term_onset = Constants.NOT_PROVIDED
-            self._by_age_dictionary[term_onset].append(hpterm)
-        for k, v in observed_hpo_terms.items():
-            if k in excluded_hpo_terms:
-                excluded_hpo_terms.pop(k) # remove observed terms that may have been excluded at other occasion
-        self._observed = observed_hpo_terms
-        self._excluded = excluded_hpo_terms
+                self._by_age_dictionary[Constants.NOT_PROVIDED].append(hpterm)
+        for k, v in self._observed_hpo_terms.items():
+            if k in self._excluded_hpo_terms:
+                self._excluded_hpo_terms.pop(k) # remove observed terms that may have been excluded at other occasion
         # Add information about variants
         self._variant_list = []
         self._disease = None
@@ -95,7 +114,7 @@ def __init__(self, ga4gh_phenopacket) -> None:
             self._pmid = eref.id
 
     @staticmethod
-    def from_file(phenopacket_file):
+    def from_file(phenopacket_file: str) -> "SimplePatient":
         """
         Return a SimplePatient object that corresponds to a phenopacket (JSON) file
         :param phenopacket_file: A phenopacket file (JSON format)
@@ -111,7 +130,7 @@ def from_file(phenopacket_file):
 
 
     @staticmethod
-    def from_individual(individual, metadata):
+    def from_individual(individual: Individual, metadata):
         """
         Return a SimplePatient object that corresponds to a pyphetools Individual object
         :param individual: Am Individual object
@@ -129,11 +148,24 @@ def get_phenopacket_id(self) -> str:
     def get_subject_id(self) -> str:
         return self._subject_id
 
-    def get_sex(self):
+    def get_sex(self) -> str:
         return self._sex
 
-    def get_age(self)-> str:
-        return self._time_at_last_encounter or "n/a"
+    def get_age(self)-> typing.Optional[TimeElement202]:
+        return self._time_at_last_encounter
+    
+    @staticmethod
+    def age_in_years(time_elem:TimeElement202) -> typing.Optional[float]:
+        if time_elem is None:
+            return None
+        return AgeSorter.convert_to_years(time_elem)
+        
+    
+    def get_age_in_years(self) -> typing.Optional[float]:
+        if self._time_at_last_encounter is None:
+            return None
+        return SimplePatient.age_in_years(time_elem=self._time_at_last_encounter)
+       
 
     def get_disease(self) -> str:
         return self._disease or "n/a"
@@ -142,20 +174,20 @@ def get_observed_hpo_d(self):
         """
         returns map of observed phenotypic features with key (string) HP id, value, HpTerm from creation submodule
         """
-        return self._observed
+        return self._observed_hpo_terms
 
     def get_excluded_hpo_d(self):
         """
         :return: map of excluded phenotypic features with key (string) HP id, value, HpTerm from creation submodule
         """
-        return self._excluded
+        return self._excluded_hpo_terms
 
     def get_total_hpo_count(self):
         """
         :return: total count of HPO terms (observed and excluded)
         :rtype: int
         """
-        return len(self._observed) + len(self._excluded)
+        return len(self._observed_hpo_terms) + len(self._excluded_hpo_terms)
 
     def get_variant_list(self):
         return self._variant_list
@@ -166,9 +198,26 @@ def has_pmid(self):
     def get_pmid(self):
         return self._pmid
 
-    def contains_observed_term_id(self, hpo_term_id):
-        return hpo_term_id in self._observed
-
+    def contains_observed_term_id(self, hpo_term_id) -> bool:
+        return hpo_term_id in self._observed_hpo_terms
+
+    def contains_excluded_term_id(self, hpo_term_id) -> bool:
+        return hpo_term_id in self._excluded_hpo_terms
+    
+    def get_observed_term_by_id(self, hpo_term_id) -> typing.Optional[HpTerm]:
+        return self._observed_hpo_terms.get(hpo_term_id)
+    
+    def get_excluded_term_by_id(self, hpo_term_id)-> typing.Optional[HpTerm]:
+        return self._excluded_hpo_terms.get(hpo_term_id)
+    
+    
     def get_term_by_age_dict(self):
         return self._by_age_dictionary
+    
+    def is_deceased(self) -> bool:
+        return self._vstat == SimplePatient.CONST_DECEASED
+    
+    def is_alive(self) -> bool:
+        return self._vstat == SimplePatient.CONST_ALIVE
+
 


Test patch:
diff --git a/test/test_age_column_mapper.py b/test/test_age_column_mapper.py
index 5ee354e1..20bda796 100644
--- a/test/test_age_column_mapper.py
+++ b/test/test_age_column_mapper.py
@@ -31,46 +31,46 @@ class TestOptionMapper(unittest.TestCase):
     def test_year(self):
         ageMapper = AgeColumnMapper.by_year(column_name=TEST_COLUMN)
         p3 = ageMapper.map_cell("3")
-        self.assertEqual("P3Y", p3.age_string)
+        self.assertEqual("P3Y", p3.age.iso8601duration)
         p42 = ageMapper.map_cell("42")
-        self.assertEqual("P42Y", p42.age_string)
+        self.assertEqual("P42Y", p42.age.iso8601duration)
 
     def test_iso8601(self):
         ageMapper = AgeColumnMapper.iso8601(column_name=TEST_COLUMN)
         p3y = ageMapper.map_cell("P3Y")
-        self.assertEqual("P3Y", p3y.age_string)
+        self.assertEqual("P3Y", p3y.age.iso8601duration)
         p3m25d = ageMapper.map_cell("P3Y2M5D")
-        self.assertEqual("P3Y2M5D", p3m25d.age_string)
+        self.assertEqual("P3Y2M5D", p3m25d.age.iso8601duration)
 
     def test_year_month_both_1(self):
         ageMapper = AgeColumnMapper.by_year_and_month(column_name=TEST_COLUMN)
         age_string = "14 y 8 m"
         age_iso = ageMapper.map_cell(age_string)
-        self.assertEqual("P14Y8M", age_iso.age_string)
+        self.assertEqual("P14Y8M", age_iso.age.iso8601duration)
 
     def test_year_month_both_2(self):
         ageMapper = AgeColumnMapper.by_year_and_month(column_name=TEST_COLUMN)
         age_string = "7 y 6 m"
         age_iso = ageMapper.map_cell(age_string)
-        self.assertEqual("P7Y6M", age_iso.age_string)
+        self.assertEqual("P7Y6M", age_iso.age.iso8601duration)
 
     def test_year_month_both_3(self):
         ageMapper = AgeColumnMapper.by_year_and_month(column_name=TEST_COLUMN)
         age_string = "7y6m"
         age_iso = ageMapper.map_cell(age_string)
-        self.assertEqual("P7Y6M", age_iso.age_string)
+        self.assertEqual("P7Y6M", age_iso.age.iso8601duration)
 
     def test_year_month_just_year_1(self):
         ageMapper = AgeColumnMapper.by_year_and_month(column_name=TEST_COLUMN)
         age_string = "7 y"
         age_iso = ageMapper.map_cell(age_string)
-        self.assertEqual("P7Y", age_iso.age_string)
+        self.assertEqual("P7Y", age_iso.age.iso8601duration)
 
     def test_year_month_just_month_1(self):
         ageMapper = AgeColumnMapper.by_year_and_month(column_name=TEST_COLUMN)
         age_string = "2 m"
         age_iso = ageMapper.map_cell(age_string)
-        self.assertEqual("P2M", age_iso.age_string)
+        self.assertEqual("P2M", age_iso.age.iso8601duration)
 
     def test_int_or_float_regex(self):
         int_or_float = r"(\d+)(\.\d+)?"
@@ -90,53 +90,53 @@ def test_fractional_year_strings(self):
         ageMapper = AgeColumnMapper.by_year(column_name=TEST_COLUMN)
         age_string = "2"
         age_iso = ageMapper.map_cell(age_string)
-        self.assertEqual("P2Y", age_iso.age_string)
+        self.assertEqual("P2Y", age_iso.age.iso8601duration)
         age_string = "4.75"
         age_iso = ageMapper.map_cell(age_string)
-        self.assertEqual("P4Y9M", age_iso.age_string)
+        self.assertEqual("P4Y9M", age_iso.age.iso8601duration)
         age_string = "5.9"
         age_iso = ageMapper.map_cell(age_string)
-        self.assertEqual("P5Y11M", age_iso.age_string)
+        self.assertEqual("P5Y11M", age_iso.age.iso8601duration)
         age_string = "6.25"
         age_iso = ageMapper.map_cell(age_string)
-        self.assertEqual("P6Y3M", age_iso.age_string)
+        self.assertEqual("P6Y3M", age_iso.age.iso8601duration)
         age_string = "8.1"
         age_iso = ageMapper.map_cell(age_string)
-        self.assertEqual("P8Y1M", age_iso.age_string)
+        self.assertEqual("P8Y1M", age_iso.age.iso8601duration)
 
     def test_custom_dictionary(self):
         ageMapper = AgeColumnMapper.custom_dictionary(column_name=TEST_COLUMN, string_to_iso_d=string_to_iso_dict)
         age_iso = ageMapper.map_cell("1.5")
-        self.assertEqual("P1Y6M", age_iso.age_string)
+        self.assertEqual("P1Y6M", age_iso.age.iso8601duration)
         age_iso = ageMapper.map_cell("3.5")
-        self.assertEqual("P3Y6M", age_iso.age_string)
+        self.assertEqual("P3Y6M", age_iso.age.iso8601duration)
         age_iso = ageMapper.map_cell("birth")
-        self.assertEqual("P1D", age_iso.age_string)
-        age_iso = ageMapper.map_cell("NOT THERE")
-        self.assertFalse(age_iso.is_valid())
+        self.assertEqual("P1D", age_iso.age.iso8601duration)
+        age_iso = ageMapper.map_cell("NOT THERE")  ## Malformed
+        self.assertIsNone(age_iso)
 
     def test_by_month(self):
         ageMapper = AgeColumnMapper.by_month(column_name=TEST_COLUMN)
         age_iso = ageMapper.map_cell(5)
-        self.assertEqual("P5M", age_iso.age_string)
+        self.assertEqual("P5M", age_iso.age.iso8601duration)
 
 
     def test_by_month2(self):
         ageMapper = AgeColumnMapper.by_month(column_name=TEST_COLUMN)
         age_iso = ageMapper.map_cell(0.5)
-        self.assertEqual("P15D", age_iso.age_string)
+        self.assertEqual("P15D", age_iso.age.iso8601duration)
 
     def test_by_month(self):
         ageMapper = AgeColumnMapper.by_month(column_name=TEST_COLUMN)
         age_iso = ageMapper.map_cell("5")
-        self.assertEqual("P5M", age_iso.age_string)
+        self.assertEqual("P5M", age_iso.age.iso8601duration)
         age_iso = ageMapper.map_cell(0.8)
-        self.assertEqual("P24D", age_iso.age_string)
+        self.assertEqual("P24D", age_iso.age.iso8601duration)
 
     def test_by_month_one_year(self):
         ageMapper = AgeColumnMapper.by_month(column_name=TEST_COLUMN)
         age_iso = ageMapper.map_cell(12)
-        self.assertEqual("P1Y", age_iso.age_string)
+        self.assertEqual("P1Y", age_iso.age.iso8601duration)
 
 
 
diff --git a/test/test_hp_term.py b/test/test_hp_term.py
index 841fed36..ee65fba1 100644
--- a/test/test_hp_term.py
+++ b/test/test_hp_term.py
@@ -1,6 +1,7 @@
 import unittest
 
-from pyphetools.creation import HpTerm, HpTermBuilder, IsoAge
+from pyphetools.creation import HpTerm, HpTermBuilder, PyPheToolsAge
+from pyphetools.pp.v202 import TimeElement as TimeElement202
 
 
 class TestHpTerm(unittest.TestCase):
@@ -18,11 +19,13 @@ def test_excluded(self):
     def test_create_phenotypic_feature(self):
         """test creation of GA4GH PhenotypicFeature object with onset and resolution
         """
-        hpo_term = HpTerm(hpo_id="HP:0001250", label="test label", onset=IsoAge.from_iso8601("P2M"), resolution=IsoAge.from_iso8601("P6M"))
+        onset202 = PyPheToolsAge.get_age_pp201("P2M")
+        resolution202 = PyPheToolsAge.get_age_pp201("P6M")
+        hpo_term = HpTerm(hpo_id="HP:0001250", label="test label", onset=onset202, resolution=resolution202)
         self.assertEqual("HP:0001250", hpo_term.id)
         self.assertEqual("test label", hpo_term.label)
-        self.assertEqual("P2M", hpo_term.onset.age_string)
-        self.assertEqual("P6M", hpo_term.resolution.age_string)
+        self.assertEqual("P2M", hpo_term.onset.age.iso8601duration)
+        self.assertEqual("P6M", hpo_term.resolution.age.iso8601duration)
         self.assertTrue(hpo_term.observed)
         self.assertTrue(hpo_term.measured)
         pfeat = hpo_term.to_ga4gh_phenotypic_feature()
@@ -40,12 +43,13 @@ def test_fetal_onset(self):
         self.assertEqual("HP:0010942", hpterm.id)
         self.assertEqual("Echogenic intracardiac focus", hpterm.label)
         self.assertIsNotNone(hpterm.onset)
-        self.assertTrue(hpterm.onset.is_valid())
         onset_term = hpterm.onset
         print(type(onset_term))
         # Fetal onset HP:0011461
-        self.assertEqual("Fetal onset", onset_term.age_string)
-        #self.assertEqual("HP:0011461", onset_term[1])
+        self.assertIsNotNone(onset_term.ontology_class)
+        oclazz = onset_term.ontology_class
+        self.assertEqual("Fetal onset", oclazz.label)
+        self.assertEqual("HP:0011461", oclazz.id)
         phenotypic_feature = hpterm.to_ga4gh_phenotypic_feature()
         self.assertIsNotNone(phenotypic_feature.onset)
         self.assertIsNotNone(phenotypic_feature.onset.ontology_class)
@@ -57,11 +61,12 @@ def test_late_onset(self):
         hpterm = HpTermBuilder(hpo_id="HP:0000726", hpo_label="Dementia").late_onset().build()
         self.assertEqual("HP:0000726", hpterm.id)
         self.assertEqual("Dementia", hpterm.label)
-        self.assertTrue(hpterm.onset.is_valid())
+        self.assertIsNotNone(hpterm.onset.ontology_class)
         self.assertIsNotNone(hpterm.onset)
-        onset_term = hpterm.onset
+        onset_term = hpterm.onset.ontology_class
         # Late onset HP:0003584
-        self.assertEqual("Late onset", onset_term.age_string)
+        self.assertEqual("Late onset", onset_term.label)
+        self.assertEqual("HP:0003584", onset_term.id)
         phenotypic_feature = hpterm.to_ga4gh_phenotypic_feature()
         self.assertIsNotNone(phenotypic_feature.onset)
         self.assertIsNotNone(phenotypic_feature.onset.ontology_class)
diff --git a/test/test_individual.py b/test/test_individual.py
index 3bbbc9b7..9544638b 100644
--- a/test/test_individual.py
+++ b/test/test_individual.py
@@ -2,6 +2,7 @@
 import pytest
 
 from pyphetools.creation import Citation, Disease,Individual, HpTerm
+from pyphetools.pp.v202 import VitalStatus, TimeElement, Age, OntologyClass
 
 
 
@@ -34,6 +35,25 @@ def ind_c(self) -> Individual:
         i.add_hpo_term(HpTerm(hpo_id="HP:0000490", label="Deeply set eye"))
         i.set_disease(disease=Disease(disease_id="OMIM:123456", disease_label="label"))
         return i
+    
+    @pytest.fixture
+    def ind_d(self) -> Individual:
+        cite = Citation(pmid="PMID:36446582", title="some title")
+        time_at_last_encounter=TimeElement(
+            element=Age(
+                iso8601duration='P6M',
+            )
+        )
+        vital_status=VitalStatus(
+            status=VitalStatus.Status.DECEASED,
+            time_of_death=TimeElement(element=Age(iso8601duration='P1Y')),
+            cause_of_death=OntologyClass(id='NCIT:C7541', label='Retinoblastoma'),
+            survival_time_in_days=180,
+        )
+        i = Individual(individual_id="Low, 2016_P17 (10)", age_at_last_encounter=time_at_last_encounter, vital_status=vital_status, citation=cite)
+        i.add_hpo_term(HpTerm(hpo_id="HP:0000490", label="Deeply set eye"))
+        i.set_disease(disease=Disease(disease_id="OMIM:123456", disease_label="label"))
+        return i
 
 
     
@@ -55,4 +75,21 @@ def test_phenopacket_id_B(self, ind_b: Individual):
     def test_phenopacket_id_C(self, ind_c: Individual):
         phenopacket_id = ind_c.get_phenopacket_id()
         expected = "PMID_36446582_Low_2016_P17_10"
-        assert expected == phenopacket_id
\ No newline at end of file
+        assert expected == phenopacket_id
+
+    def test_phenopacket_vital_status(self, ind_d: Individual):
+        vstat = ind_d.get_vital_status()
+        assert vstat is not None
+        assert vstat.status == VitalStatus.Status.DECEASED
+        assert 180 == vstat.survival_time_in_days
+        #  cause_of_death=OntologyClass(id='NCIT:C7541', label='Retinoblastoma'),
+        assert "NCIT:C7541" == vstat.cause_of_death.id
+        assert "Retinoblastoma" == vstat.cause_of_death.label
+
+    def test_phenopacket_last_encounter(self, ind_d: Individual):
+        last_encounter = ind_d.age_at_last_encounter
+        assert last_encounter is not None 
+        assert last_encounter.age is not None
+        assert last_encounter.age_range is None 
+        age = last_encounter.age
+        assert age.iso8601duration == "P6M"
diff --git a/test/test_ontology_qc.py b/test/test_ontology_qc.py
index 0c433b6e..15dfdb13 100644
--- a/test/test_ontology_qc.py
+++ b/test/test_ontology_qc.py
@@ -1,7 +1,7 @@
 import hpotk
 import pytest
 
-from pyphetools.creation import HpTerm, Individual, IsoAge
+from pyphetools.creation import HpTerm, Individual, PyPheToolsAge
 from pyphetools.validation import OntologyQC
 
 
@@ -122,7 +122,7 @@ def test_redundancy_with_and_without_onset(self, hpo: hpotk.Ontology):
         Myoclonic seizure HP:0032794 ("grandchild" of Seizure)
         Seizure HP:0001250
         """
-        onset = IsoAge.from_iso8601("P1Y")
+        onset = PyPheToolsAge.get_age_pp201("P1Y")
         myoclonic_seiz = HpTerm(hpo_id="HP:0032794", label="Myoclonic seizure", observed=True, onset=onset)
         seiz = HpTerm(hpo_id="HP:0001250", label="Seizure", observed=True)
         hpo_terms = [myoclonic_seiz, seiz]
diff --git a/test/test_pyphetools_age.py b/test/test_pyphetools_age.py
index ae829200..09248805 100644
--- a/test/test_pyphetools_age.py
+++ b/test/test_pyphetools_age.py
@@ -1,23 +1,45 @@
 import unittest
-from pyphetools.creation import PyPheToolsAge, HpoAge, IsoAge
+import pytest
+from pyphetools.creation import PyPheToolsAge
+from pyphetools.pp.v202 import TimeElement as TimeElement202
 
 
 class TestSimpleAge(unittest.TestCase):
     #'age_of_onset': 'Infantile onset', 'age_at_last_encounter': 'P3Y9M', 'sex': 'M'}
     def test_infantile_onset(self):
-        onset_age = PyPheToolsAge.get_age('Infantile onset')
+        onset_age = PyPheToolsAge.get_age_pp201('Infantile onset')
         self.assertIsNotNone(onset_age)
-        self.assertEqual('Infantile onset', onset_age.age_string)
-        self.assertTrue(isinstance(onset_age, HpoAge))
+        self.assertEqual('Infantile onset', onset_age.ontology_class.label)
+        self.assertTrue(isinstance(onset_age, TimeElement202))
 
     def test_3m9m(self):
-        onset_age = PyPheToolsAge.get_age('P3Y9M')
+        onset_age = PyPheToolsAge.get_age_pp201('P3Y9M')
         self.assertIsNotNone(onset_age)
-        self.assertEqual('P3Y9M', onset_age.age_string)
-        self.assertTrue(isinstance(onset_age, IsoAge))
+        self.assertEqual('P3Y9M', onset_age.age.iso8601duration)
+        self.assertTrue(isinstance(onset_age, TimeElement202))
 
     def test_2m(self):
-        onset_age = PyPheToolsAge.get_age('P2M')
+        onset_age = PyPheToolsAge.get_age_pp201('P2M')
         self.assertIsNotNone(onset_age)
-        self.assertEqual('P2M', onset_age.age_string)
-        self.assertTrue(isinstance(onset_age, IsoAge))
+        self.assertEqual('P2M', onset_age.age.iso8601duration)
+        self.assertTrue(isinstance(onset_age, TimeElement202))
+
+
+    def test_age_key_converter_iso(self):
+        time_elem202 = PyPheToolsAge.get_age_pp201("P41Y")
+        assert time_elem202 is not None
+        assert time_elem202.age
+        assert time_elem202.age.iso8601duration == "P41Y"
+
+    def test_age_key_converter_hpterm(self):
+        time_elem202 = PyPheToolsAge.get_age_pp201("Congenital onset")
+        assert time_elem202 is not None
+        assert not time_elem202.age
+        assert time_elem202.ontology_class
+        oterm = time_elem202.ontology_class
+        assert oterm.id == "HP:0003577"
+        assert oterm.label == "Congenital onset"
+
+    def test_malformed_key_converter(self):
+        with pytest.raises(ValueError) as verror:
+            time_elem = PyPheToolsAge.get_age_pp201("MALFORMED LABEL")