Repository: CodeForPhilly/chime
Pull request #305 (2020-03-28 18:25:37)

Add time as a parameter
Use negatives days.

Add current_date and base all computed dates from it.

Add date to dispositions_df and sim_sir_w_date_df. Push these through to the views.

Huge pull-request-O-doom.


Patch:
diff --git a/date_first_hospitalized.cfg b/date_first_hospitalized.cfg
new file mode 100644
index 0000000..573a2eb
--- /dev/null
+++ b/date_first_hospitalized.cfg
@@ -0,0 +1,14 @@
+--current-hospitalized 14
+--date-first-hospitalized 2020-03-07
+--hospitalized-los 7
+--hospitalized-rate 0.025
+--icu-los 9
+--icu-rate 0.0075
+--infectious-days 14
+--known-infected 510
+--market_share 0.15
+--n-days 60
+--population 4119405
+--relative-contact-rate 0.3
+--ventilated-los 10
+--ventilated-rate 0.005
diff --git a/doubling_time.cfg b/doubling_time.cfg
new file mode 100644
index 0000000..8cceeeb
--- /dev/null
+++ b/doubling_time.cfg
@@ -0,0 +1,14 @@
+--current-hospitalized 14
+--doubling-time 4.0
+--hospitalized-los 7
+--hospitalized-rate 0.025
+--icu-los 9
+--icu-rate 0.0075
+--infectious-days 14
+--known-infected 510
+--market_share 0.15
+--n-days 60
+--population 4119405
+--relative-contact-rate 0.3
+--ventilated-los 10
+--ventilated-rate 0.005
diff --git a/settings.cfg b/settings.cfg
index f8d1a3b..a852855 100644
--- a/settings.cfg
+++ b/settings.cfg
@@ -1,13 +1,13 @@
 --current-hospitalized 14
---doubling-time 4.0 
---known-infected 510
---relative-contact-rate 0.3
+--doubling-time 4.0
 --hospitalized-los 7
 --hospitalized-rate 0.025
 --icu-los 9
 --icu-rate 0.0075
+--infectious-days 14
 --market_share 0.15
 --n-days 60
 --population 4119405
+--relative-contact-rate 0.3
 --ventilated-los 10
 --ventilated-rate 0.005
diff --git a/src/app.py b/src/app.py
index 056220a..9ea939b 100644
--- a/src/app.py
+++ b/src/app.py
@@ -4,25 +4,22 @@ import altair as alt  # type: ignore
 import streamlit as st  # type: ignore
 
 from penn_chime.presentation import (
-    build_download_link,
+    display_download_link,
     display_header,
+    display_more_info,
     display_sidebar,
-    draw_census_table,
-    draw_projected_admissions_table,
-    draw_raw_sir_simulation_table,
     hide_menu_style,
-    show_additional_projections,
-    show_more_info_about_this_tool,
     write_definitions,
     write_footer,
 )
 from penn_chime.settings import DEFAULTS
 from penn_chime.models import SimSirModel
 from penn_chime.charts import (
-    additional_projections_chart,
-    admitted_patients_chart,
-    new_admissions_chart,
-    chart_descriptions
+    build_admits_chart,
+    build_census_chart,
+    build_descriptions,
+    build_sim_sir_w_date_chart,
+    build_table,
 )
 
 # This is somewhat dangerous:
@@ -38,71 +35,67 @@ display_header(st, m, p)
 
 if st.checkbox("Show more info about this tool"):
     notes = "The total size of the susceptible population will be the entire catchment area for Penn Medicine entities (HUP, PAH, PMC, CCH)"
-    show_more_info_about_this_tool(st=st, model=m, parameters=p, defaults=DEFAULTS, notes=notes)
+    display_more_info(st=st, model=m, parameters=p, defaults=DEFAULTS, notes=notes)
 
 st.subheader("New Admissions")
 st.markdown("Projected number of **daily** COVID-19 admissions at Penn hospitals")
-new_admit_chart = new_admissions_chart(alt, m.admits_df, parameters=p)
-st.altair_chart(
-    new_admissions_chart(alt, m.admits_df, parameters=p),
-    use_container_width=True,
+admits_chart = build_admits_chart(alt=alt, admits_df=m.admits_df, max_y_axis=p.max_y_axis)
+st.altair_chart(admits_chart, use_container_width=True)
+st.markdown(build_descriptions(chart=admits_chart, labels=p.labels))
+display_download_link(
+    st,
+    filename=f"{p.current_date}_projected_admits.csv",
+    df=m.admits_df,
 )
 
-st.markdown(chart_descriptions(new_admit_chart, p.labels))
-
 if st.checkbox("Show Projected Admissions in tabular form"):
-    if st.checkbox("Show Daily Counts"):
-        draw_projected_admissions_table(st, m.admits_df, p.labels, 1, as_date=p.as_date)
-    else:
-        admissions_day_range = st.slider(
-            label="Interval of Days",
-            key="admissions_day_range_slider",
-            min_value=1,
-            max_value=10,
-            value=7 
-        )
-        draw_projected_admissions_table(st, m.admits_df, p.labels, admissions_day_range, as_date=p.as_date)
-    build_download_link(st,
-        filename="projected_admissions.csv",
+    admits_modulo = 1
+    if not st.checkbox("Show Daily Counts"):
+        admits_modulo = 7
+    table_df = build_table(
         df=m.admits_df,
-        parameters=p
-    )
+        labels=p.labels,
+        modulo=admits_modulo)
+    st.table(table_df)
+
+
 st.subheader("Admitted Patients (Census)")
-st.markdown(
-    "Projected **census** of COVID-19 patients, accounting for arrivals and discharges at Penn hospitals"
-)
-census_chart = admitted_patients_chart(alt=alt, census=m.census_df, parameters=p)
-st.altair_chart(
-    admitted_patients_chart(alt=alt, census=m.census_df, parameters=p),
-    use_container_width=True,
+st.markdown("Projected **census** of COVID-19 patients, accounting for arrivals and discharges at Penn hospitals")
+census_chart = build_census_chart(alt=alt, census_df=m.census_df, max_y_axis=p.max_y_axis)
+st.altair_chart(census_chart, use_container_width=True)
+st.markdown(build_descriptions(chart=census_chart, labels=p.labels, suffix=" Census"))
+display_download_link(
+    st,
+    filename=f"{p.current_date}_projected_census.csv",
+    df=m.census_df,
 )
-st.markdown(chart_descriptions(census_chart, p.labels, suffix=" Census"))
+
 if st.checkbox("Show Projected Census in tabular form"):
-    if st.checkbox("Show Daily Census Counts"):
-        draw_census_table(st, m.census_df, p.labels, 1, as_date=p.as_date)
-    else:
-        census_day_range = st.slider(
-            label='Interval of Days',
-            key="census_day_range_slider",
-            min_value=1,
-            max_value=10,
-            value=7 
-        )
-        draw_census_table(st, m.census_df, p.labels, census_day_range, as_date=p.as_date)
-    build_download_link(st,
-        filename="projected_census.csv",
+    census_modulo = 1
+    if not st.checkbox("Show Daily Census Counts"):
+        census_modulo = 7
+    table_df = build_table(
         df=m.census_df,
-        parameters=p
-    )
+        labels=p.labels,
+        modulo=census_modulo)
+    st.table(table_df)
+
 
-st.markdown(
-    """**Click the checkbox below to view additional data generated by this simulation**"""
+st.subheader("Susceptible, Infected, and Recovered")
+st.markdown("The number of susceptible, infected, and recovered individuals in the hospital catchment region at any given moment")
+sim_sir_w_date_chart = build_sim_sir_w_date_chart(alt=alt, sim_sir_w_date_df=m.sim_sir_w_date_df)
+st.altair_chart(sim_sir_w_date_chart, use_container_width=True)
+display_download_link(
+    st,
+    filename=f"{p.current_date}_sim_sir_w_date.csv",
+    df=m.sim_sir_w_date_df,
 )
-if st.checkbox("Show Additional Projections"):
-    show_additional_projections(
-        st, alt, additional_projections_chart, model=m, parameters=p
-    )
-    if st.checkbox("Show Raw SIR Simulation Data"):
-        draw_raw_sir_simulation_table(st, model=m, parameters=p)
+
+if st.checkbox("Show SIR Simulation in tabular form"):
+    table_df = build_table(
+        df=m.sim_sir_w_date_df,
+        labels=p.labels)
+    st.table(table_df)
+
 write_definitions(st)
 write_footer(st)
diff --git a/src/penn_chime/charts.py b/src/penn_chime/charts.py
index f45ed5b..fb6cf4c 100644
--- a/src/penn_chime/charts.py
+++ b/src/penn_chime/charts.py
@@ -1,153 +1,131 @@
 
+from datetime import datetime
 from math import ceil
-import datetime
+from typing import Dict, Optional
 
 from altair import Chart  # type: ignore
 import pandas as pd  # type: ignore
 import numpy as np
 
+from .constants import DATE_FORMAT
 from .parameters import Parameters
-from .utils import add_date_column
-from .presentation import DATE_FORMAT
 
 
-def new_admissions_chart(
-    alt, projection_admits: pd.DataFrame, parameters: Parameters
+def build_admits_chart(
+    *,
+    alt,
+    admits_df: pd.DataFrame,
+    max_y_axis: Optional[int] = None,
 ) -> Chart:
-    """docstring"""
-    plot_projection_days = parameters.n_days - 10
-    max_y_axis = parameters.max_y_axis
-    as_date = parameters.as_date
-
+    """Build admits chart."""
     y_scale = alt.Scale()
-
     if max_y_axis is not None:
         y_scale.domain = (0, max_y_axis)
 
-    tooltip_dict = {False: "day", True: "date:T"}
-    if as_date:
-        projection_admits = add_date_column(projection_admits, parameters.date_first_hospitalized)
-        x_kwargs = {"shorthand": "date:T", "title": "Date", "axis": alt.Axis(format=(DATE_FORMAT))}
-    else:
-        x_kwargs = {"shorthand": "day", "title": "Days from today"}
-
-    # TODO fix the fold to allow any number of dispositions
+    ceil_df = admits_df.copy()
+    ceil_df.hospitalized = np.ceil(ceil_df.hospitalized)
+    ceil_df.icu = np.ceil(ceil_df.icu)
+    ceil_df.ventilated = np.ceil(ceil_df.ventilated)
 
-    ceiled_admits = projection_admits.copy()
+    x = dict(shorthand="date:T", title="Date", axis=alt.Axis(format=(DATE_FORMAT)))
+    y = dict(shorthand="value:Q", title="Daily admissions", scale=y_scale)
+    color = "key:N"
+    tooltip=["date:T", alt.Tooltip("value:Q", format=".0f", title="Admit"), "key:N"]
 
-    ceiled_admits.hospitalized = np.ceil(ceiled_admits.hospitalized)
-    ceiled_admits.icu = np.ceil(ceiled_admits.icu)
-    ceiled_admits.ventilated = np.ceil(ceiled_admits.ventilated)
-
-    return (
-        alt.Chart(ceiled_admits.head(plot_projection_days))
+    # TODO fix the fold to allow any number of dispositions
+    points = (
+        alt.Chart()
         .transform_fold(fold=["hospitalized", "icu", "ventilated"])
+        .encode(x=alt.X(**x), y=alt.Y(**y), color=color, tooltip=tooltip)
         .mark_line(point=True)
-        .encode(
-            x=alt.X(**x_kwargs),
-            y=alt.Y("value:Q", title="Daily admissions", scale=y_scale),
-            color="key:N",
-            tooltip=[
-                tooltip_dict[as_date],
-                alt.Tooltip("value:Q", format=".0f", title="Admissions"),
-                "key:N",
-            ],
-        )
-        .interactive()
     )
+    bar = (
+        alt.Chart()
+        .encode(x=alt.X(**x))
+        .transform_filter(alt.datum.day == 0)
+        .mark_rule(color="black", opacity=0.35, size=2)
+    )
+    return alt.layer(points, bar, data=admits_df)
 
 
-def admitted_patients_chart(
-    alt, census: pd.DataFrame, parameters: Parameters
-) -> Chart:
-    """docstring"""
-
-    plot_projection_days = parameters.n_days - 10
-    max_y_axis = parameters.max_y_axis
-    as_date = parameters.as_date
-    if as_date:
-        census = add_date_column(census, parameters.date_first_hospitalized)
-        x_kwargs = {"shorthand": "date:T", "title": "Date", "axis": alt.Axis(format=(DATE_FORMAT))}
-        idx = "date:T"
-    else:
-        x_kwargs = {"shorthand": "day", "title": "Days from today"}
-        idx = "day"
 
+def build_census_chart(
+    *,
+    alt,
+    census_df: pd.DataFrame,
+    max_y_axis: Optional[int] = None,
+) -> Chart:
+    """Build census chart."""
     y_scale = alt.Scale()
-
     if max_y_axis:
         y_scale.domain = (0, max_y_axis)
 
+    x = dict(shorthand="date:T", title="Date", axis=alt.Axis(format=(DATE_FORMAT)))
+    y = dict(shorthand="value:Q", title="Census", scale=y_scale)
+    color = "key:N"
+    tooltip = ["date:T", alt.Tooltip("value:Q", format=".0f", title="Census"), "key:N"]
+
     # TODO fix the fold to allow any number of dispositions
-    return (
-        alt.Chart(census.head(plot_projection_days))
+    points = (
+        alt.Chart()
         .transform_fold(fold=["hospitalized", "icu", "ventilated"])
+        .encode(x=alt.X(**x), y=alt.Y(**y), color=color, tooltip=tooltip)
         .mark_line(point=True)
-        .encode(
-            x=alt.X(**x_kwargs),
-            y=alt.Y("value:Q", title="Census", scale=y_scale),
-            color="key:N",
-            tooltip=[
-                idx,
-                alt.Tooltip("value:Q", format=".0f", title="Census"),
-                "key:N",
-            ],
-        )
-        .interactive()
     )
+    bar = (
+        alt.Chart()
+        .encode(x=alt.X(**x))
+        .transform_filter(alt.datum.day == 0)
+        .mark_rule(color="black", opacity=0.35, size=2)
+    )
+    return alt.layer(points, bar, data=census_df)
 
 
-def additional_projections_chart(
-    alt, model, parameters
+def build_sim_sir_w_date_chart(
+    *,
+    alt,
+    sim_sir_w_date_df: pd.DataFrame,
+    max_y_axis: Optional[int] = None,
 ) -> Chart:
-
-    # TODO use subselect of df_raw instead of creating a new df
-    raw_df = model.raw_df
-    dat = pd.DataFrame({
-        "infected": raw_df.infected,
-        "recovered": raw_df.recovered
-    })
-    dat["day"] = dat.index
-
-    if parameters.n_days_since_first_hospitalized:
-        dat["day"] = dat.day - parameters.n_days_since_first_hospitalized
-
-    as_date = parameters.as_date
-    max_y_axis = parameters.max_y_axis
-
-    if as_date:
-        dat = add_date_column(dat, parameters.date_first_hospitalized)
-        x_kwargs = {"shorthand": "date:T", "title": "Date", "axis": alt.Axis(format=(DATE_FORMAT))}
-    else:
-        x_kwargs = {"shorthand": "day", "title": "Days from today"}
-
+    """Build sim sir w date chart."""
     y_scale = alt.Scale()
-
     if max_y_axis is not None:
         y_scale.domain = (0, max_y_axis)
 
-    return (
-        alt.Chart(dat)
-        .transform_fold(fold=["infected", "recovered"])
+    x = dict(shorthand="date:T", title="Date", axis=alt.Axis(format=(DATE_FORMAT)))
+    y = dict(shorthand="value:Q", title="Count", scale=y_scale)
+    color = "key:N"
+    tooltip = ["key:N", "value:Q"]
+
+    # TODO fix the fold to allow any number of dispositions
+    points = (
+        alt.Chart()
+        .transform_fold(fold=["susceptible", "infected", "recovered"])
+        .encode(x=alt.X(**x), y=alt.Y(**y), color=color, tooltip=tooltip)
         .mark_line()
-        .encode(
-            x=alt.X(**x_kwargs),
-            y=alt.Y("value:Q", title="Case Volume", scale=y_scale),
-            tooltip=["key:N", "value:Q"],
-            color="key:N",
-        )
-        .interactive()
     )
+    bar = (
+        alt.Chart()
+        .encode(x=alt.X(**x))
+        .transform_filter(alt.datum.day == 0)
+        .mark_rule(color="black", opacity=0.35, size=2)
+    )
+    return alt.layer(points, bar, data=sim_sir_w_date_df)
 
 
-def chart_descriptions(chart: Chart, labels, suffix: str = ""):
+def build_descriptions(
+    *,
+    chart: Chart,
+    labels: Dict[str, str],
+    suffix: str = ""
+) -> str:
     """
 
-    :param chart: Chart: The alt chart to be used in finding max points
-    :param suffix: str: The assumption is that the charts have similar column names.
+    :param chart: The alt chart to be used in finding max points
+    :param suffix: The assumption is that the charts have similar column names.
                    The census chart adds " Census" to the column names.
                    Make sure to include a space or underscore as appropriate
-    :return: str: Returns a multi-line string description of the results
+    :return: Returns a multi-line string description of the results
     """
     messages = []
 
@@ -159,14 +137,11 @@ def chart_descriptions(chart: Chart, labels, suffix: str = ""):
         if chart.data[col].idxmax() + 1 == len(chart.data):
             asterisk = True
 
-        on = chart.data[day][chart.data[col].idxmax()]
-        if day == "date":
-            on = datetime.datetime.strftime(on, "%b %d")  # todo: bring this to an optional arg / i18n
-        else:
-            on += 1  # 0 index issue
+        # todo: bring this to an optional arg / i18n
+        on = datetime.strftime(chart.data[day][chart.data[col].idxmax()], "%b %d")
 
         messages.append(
-            "{}{} peaks at {:,} on day {}{}".format(
+            "{}{} peaks at {:,} on {}{}".format(
                 labels[col],
                 suffix,
                 ceil(chart.data[col].max()),
@@ -178,3 +153,14 @@ def chart_descriptions(chart: Chart, labels, suffix: str = ""):
     if asterisk:
         messages.append("_* The max is at the upper bound of the data, and therefore may not be the actual max_")
     return "\n\n".join(messages)
+
+
+def build_table(
+    *,
+    df: pd.DataFrame,
+    labels: Dict[str, str],
+    modulo: int = 1,
+) -> pd.DataFrame:
+    table_df = df[np.mod(df.day, modulo) == 0].copy()
+    table_df.rename(labels)
+    return table_df
diff --git a/src/penn_chime/cli.py b/src/penn_chime/cli.py
index a4ec533..1bcbaf8 100644
--- a/src/penn_chime/cli.py
+++ b/src/penn_chime/cli.py
@@ -8,9 +8,10 @@ from datetime import datetime
 
 from pandas import DataFrame
 
-from penn_chime.parameters import Parameters
-from penn_chime.models import SimSirModel
-from penn_chime.utils import RateLos
+from .constants import CHANGE_DATE
+from .parameters import Parameters, RateLos
+from .models import SimSirModel as Model
+
 
 class FromFile(Action):
     """From File."""
@@ -20,11 +21,19 @@ class FromFile(Action):
             parser.parse_args(f.read().split(), namespace)
 
 
-def validator(cast, min_value, max_value):
+def cast_date(string):
+    return datetime.strptime(string, '%Y-%m-%d').date()
+
+
+def validator(arg, cast, min_value, max_value, required=True):
     """Validator."""
 
     def validate(string):
         """Validate."""
+        if string == '' and cast != str:
+            if required:
+                raise AssertionError('%s is required.')
+            return None
         value = cast(string)
         if min_value is not None:
             assert value >= min_value
@@ -37,20 +46,25 @@ def validator(cast, min_value, max_value):
 
 def parse_args():
     """Parse args."""
-    parser = ArgumentParser(description="CHIME")
-
+    parser = ArgumentParser(description=f"penn_chime: {CHANGE_DATE}")
     parser.add_argument("--file", type=open, action=FromFile)
-    parser.add_argument(
-        "--prefix", type=str, default=datetime.now().strftime("%Y.%m.%d.%H.%M."),
-    )
 
-    for arg, cast, min_value, max_value, help in (
+    for arg, cast, min_value, max_value, help, required in (
         (
             "--current-hospitalized",
             int,
             0,
             None,
             "Currently Hospitalized COVID-19 Patients (>= 0)",
+            True,
+        ),
+        (
+            "--date-first-hospitalized",
+            cast_date,
+            None,
+            None,
+            "Current date",
+            False,
         ),
         (
             "--doubling-time",
@@ -58,44 +72,42 @@ def parse_args():
             0.0,
             None,
             "Doubling time before social distancing (days)",
+            True,
         ),
-        ("--hospitalized-los", int, 0, None, "Hospitalized Length of Stay (days)"),
+        ("--hospitalized-los", int, 0, None, "Hospitalized Length of Stay (days)", True),
         (
             "--hospitalized-rate",
             float,
             0.00001,
             1.0,
             "Hospitalized Rate: 0.00001 - 1.0",
+            True,
         ),
-        ("--icu-los", int, 0, None, "ICU Length of Stay (days)"),
-        ("--icu-rate", float, 0.0, 1.0, "ICU Rate: 0.0 - 1.0"),
-        (
-            "--known-infected",
-            int,
-            0,
-            None,
-            "Currently Known Regional Infections (>=0) (only used to compute detection rate - does not change projections)",
-        ),
+        ("--icu-los", int, 0, None, "ICU Length of Stay (days)", True),
+        ("--icu-rate", float, 0.0, 1.0, "ICU Rate: 0.0 - 1.0", True),
         (
             "--market_share",
             float,
             0.00001,
             1.0,
             "Hospital Market Share (0.00001 - 1.0)",
+            True,
         ),
-        ("--n-days", int, 0, None, "Nuber of days to project >= 0"),
+        ("--infectious-days", float, 0.0, None, "Infectious days", True),
+        ("--n-days", int, 0, None, "Number of days to project >= 0", True),
         (
             "--relative-contact-rate",
             float,
             0.0,
             1.0,
             "Social Distancing Reduction Rate: 0.0 - 1.0",
+            True,
         ),
-        ("--population", int, 1, None, "Regional Population >= 1"),
-        ("--ventilated-los", int, 0, None, "Ventilated Length of Stay (days)"),
-        ("--ventilated-rate", float, 0.0, 1.0, "Ventilated Rate: 0.0 - 1.0"),
+        ("--population", int, 1, None, "Regional Population >= 1", True),
+        ("--ventilated-los", int, 0, None, "Ventilated Length of Stay (days)", True),
+        ("--ventilated-rate", float, 0.0, 1.0, "Ventilated Rate: 0.0 - 1.0", True),
     ):
-        parser.add_argument(arg, type=validator(cast, min_value, max_value))
+        parser.add_argument(arg, type=validator(arg, cast, min_value, max_value, required))
     return parser.parse_args()
 
 
@@ -105,8 +117,9 @@ def main():
 
     p = Parameters(
         current_hospitalized=a.current_hospitalized,
+        date_first_hospitalized=a.date_first_hospitalized,
         doubling_time=a.doubling_time,
-        known_infected=a.known_infected,
+        infectious_days=a.infectious_days,
         market_share=a.market_share,
         n_days=a.n_days,
         relative_contact_rate=a.relative_contact_rate,
@@ -117,15 +130,14 @@ def main():
         ventilated=RateLos(a.ventilated_rate, a.ventilated_los),
     )
 
-    m = SimSirModel(p)
+    m = Model(p)
 
-    prefix = a.prefix
     for df, name in (
-        (m.raw_df, "raw"),
-        (m.admits_df, "admits"),
-        (m.census_df, "census"),
+        (m.sim_sir_w_date_df, "sim_sir_w_date"),
+        (m.admits_df, "projected_admits"),
+        (m.census_df, "projected_census"),
     ):
-        df.to_csv(prefix + name + ".csv")
+        df.to_csv(f"{p.current_date}_{name}.csv")
 
 
 if __name__ == "__main__":
diff --git a/src/penn_chime/constants.py b/src/penn_chime/constants.py
new file mode 100644
index 0000000..9f5d20f
--- /dev/null
+++ b/src/penn_chime/constants.py
@@ -0,0 +1,18 @@
+"""Constants."""
+
+from datetime import date
+
+"""
+This reflects a date from which previously-run reports will no
+longer match current results, indicating when users should
+re-run their reports
+"""
+CHANGE_DATE = date(year=2020, month=3, day=30)
+
+DATE_FORMAT = "%b, %d"  # see https://strftime.org
+DOCS_URL = "https://code-for-philly.gitbook.io/chime"
+
+EPSILON = 1.0e-7
+
+FLOAT_INPUT_MIN = 0.001
+FLOAT_INPUT_STEP = FLOAT_INPUT_MIN
diff --git a/src/penn_chime/defaults.py b/src/penn_chime/defaults.py
deleted file mode 100644
index 89c0e64..0000000
--- a/src/penn_chime/defaults.py
+++ /dev/null
@@ -1,60 +0,0 @@
-"""Defaults."""
-from typing import Optional
-from datetime import date
-
-from .utils import RateLos
-
-
-class Regions:
-    """Arbitrary number of counties."""
-
-    def __init__(self, **kwargs):
-        population = 0
-        for key, value in kwargs.items():
-            setattr(self, key, value)
-            population += value
-        self.population = population
-
-
-
-class Constants:
-    def __init__(
-        self,
-        *,
-        current_hospitalized: int,
-        doubling_time: float,
-        known_infected: int,
-        relative_contact_rate: float,
-        region: Regions,
-
-        hospitalized: RateLos,
-        icu: RateLos,
-        ventilated: RateLos,
-
-        date_first_hospitalized: Optional[date] = None,
-        as_date: bool = False,
-        market_share: float = 1.0,
-        max_y_axis: Optional[int] = None,
-        n_days: int = 60,
-        recovery_days: int = 14,
-    ):
-        self.region = region
-        self.current_hospitalized = current_hospitalized
-        self.known_infected = known_infected
-        self.doubling_time = doubling_time
-        self.relative_contact_rate = relative_contact_rate
-
-        self.hospitalized = hospitalized
-        self.icu = icu
-        self.ventilated = ventilated
-
-        self.date_first_hospitalized = date_first_hospitalized
-        self.n_days_since_first_hospitalized = (date.today() - date_first_hospitalized).days
-        self.as_date = as_date
-        self.market_share = market_share
-        self.max_y_axis = max_y_axis
-        self.n_days = n_days
-        self.recovery_days = recovery_days
-
-    def __repr__(self) -> str:
-        return f"Constants(population_default: {self.region.population}, known_infected: {self.known_infected})"
diff --git a/src/penn_chime/models.py b/src/penn_chime/models.py
index 677449d..5880a19 100644
--- a/src/penn_chime/models.py
+++ b/src/penn_chime/models.py
@@ -1,57 +1,57 @@
 """Models.
 
 Changes affecting results or their presentation should also update
-parameters.py `change_date`, so users can see when results have last
-changed
+constants.py `change_date`,
 """
 
 from __future__ import annotations
 
+from datetime import date, datetime, timedelta
+from logging import INFO, basicConfig, getLogger
+from sys import stdout
 from typing import Dict, Generator, Tuple, Optional
-from datetime import date, timedelta
 
 import numpy as np  # type: ignore
 import pandas as pd  # type: ignore
 
+from .constants import EPSILON, CHANGE_DATE
 from .parameters import Parameters
 
+
+basicConfig(
+    level=INFO,
+    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
+    stream=stdout,
+)
+logger = getLogger(__name__)
+
+
 class SimSirModel:
 
     def __init__(self, p: Parameters):
-        # TODO missing initial non-zero 'recovered' value
-        recovered = 0.0
-        recovery_days = p.recovery_days
 
-        rates = {
+        self.rates = {
             key: d.rate
             for key, d in p.dispositions.items()
         }
 
-        lengths_of_stay = {
+        self.lengths_of_stay = {
             key: d.length_of_stay
             for key, d in p.dispositions.items()
         }
 
-        self._rates = rates
-        self._lengths_of_stay = lengths_of_stay
-
         # Note: this should not be an integer.
         # We're appoximating infected from what we do know.
         # TODO market_share > 0, hosp_rate > 0
         infected = (
-            p.current_hospitalized / p.market_share / p.hospitalized.rate
+            1.0 / p.market_share / p.hospitalized.rate
         )
 
         susceptible = p.population - infected
 
-        detection_probability = (
-            p.known_infected / infected if infected > 1.0e-7 else None
-        )
-
-        # (2.0 ** (1.0 / p.doubling_time) - 1.0) if p.doubling_time > 0.0 else 0.0
-        intrinsic_growth_rate = self._intrinsic_growth_rate(p.doubling_time)
+        intrinsic_growth_rate = get_growth_rate(p.doubling_time)
 
-        gamma = 1.0 / recovery_days
+        gamma = 1.0 / p.infectious_days
 
         # Contact rate, beta
         beta = (
@@ -66,186 +66,144 @@ class SimSirModel:
         # Simplify equation to avoid division by zero:
         # self.r_naught = r_t / (1.0 - relative_contact_rate)
         r_naught = (intrinsic_growth_rate + gamma) / gamma
-        doubling_time_t = 1.0 / np.log2(
-            beta * susceptible - gamma + 1)
-
-        raw_df = sim_sir_df(
-            susceptible,
-            infected,
-            recovered,
-            beta,
-            gamma,
-            p.n_days,
-        )
-        dispositions_df = build_dispositions_df(raw_df, rates, p.market_share)
-        admits_df = build_admits_df(dispositions_df, p.n_days_since_first_hospitalized)
-        census_df = build_census_df(admits_df, lengths_of_stay, p.n_days_since_first_hospitalized)
 
         self.susceptible = susceptible
         self.infected = infected
-        self.recovered = recovered
+        self.recovered = p.recovered
 
-        self.detection_probability = detection_probability
-        self.recovered = recovered
-        self.intrinsic_growth_rate = intrinsic_growth_rate
-        self.gamma = gamma
         self.beta = beta
-        self.r_t = r_t
-        self.r_naught = r_naught
-        self.doubling_time_t = doubling_time_t
-        self.raw_df = raw_df
-        self.dispositions_df = dispositions_df
-        self.admits_df = admits_df
-        self.census_df = census_df
-
-        if p.n_days_since_first_hospitalized is None and p.doubling_time is not None:
-            p.n_days_since_first_hospitalized = int(self.argmin_ds(p))
-            p.date_first_hospitalized = date.today() - timedelta(days=p.n_days_since_first_hospitalized)
-            admits_df = build_admits_df(dispositions_df, p.n_days_since_first_hospitalized)
-            census_df = build_census_df(admits_df, lengths_of_stay, p.n_days_since_first_hospitalized)
-            self.admits_df = admits_df
-            self.census_df = census_df
-        if p.n_days_since_first_hospitalized is not None and p.doubling_time is None:
-            # optimize doubling_time
-            argmin_dt = None
-            min_loss = 2.0**99
-            censes = dict()
-            current_infecteds = dict()
-            for dt in np.linspace(1,15,29):
-                censes[dt], current_infecteds[dt] = self.run_projection_dt(p, dt)
-                # self.current_infected = current_infecteds[dt] # log it into state for no reason really
-                self.census_df = censes[dt] # log it into state for loss
-                loss_dt = self.loss_dt(p)
-                if loss_dt < min_loss:
-                    min_loss = loss_dt
-                    argmin_dt = dt
-            self.census_df = censes[argmin_dt]
-            p.doubling_time = argmin_dt
-            current_infected = current_infecteds[argmin_dt]
-
-            infected = 1 / p.hospitalized.rate / p.market_share
-            # update all state that is dependent on doubling time.
-            intrinsic_growth_rate = self._intrinsic_growth_rate(p.doubling_time)
-            gamma = 1 / recovery_days
-            beta = self._beta(intrinsic_growth_rate, gamma, susceptible, p.relative_contact_rate)
-            r_t = beta / gamma * susceptible
-            r_naught = (intrinsic_growth_rate + gamma) / gamma
-            doubling_time_t = 1.0 / np.log2(beta * susceptible - gamma + 1)
-            raw_df = sim_sir_df(
-                susceptible,
-                current_infected,
-                recovered,
-                beta,
-                gamma,
-                p.n_days # + p.n_days_since_first_hospitalized
-            )
-            dispositions_df = build_dispositions_df(raw_df, rates, p.market_share)
-            admits_df = build_admits_df(dispositions_df, p.n_days_since_first_hospitalized)
-            census_df = build_census_df(admits_df, lengths_of_stay, p.n_days_since_first_hospitalized)
-
-            self.population = p.population
-            self.infected = current_infected
-            self.intrinsic_growth_rate = intrinsic_growth_rate
-            self.gamma = gamma
-            self.beta = beta
-            self.r_t = r_t
-            self.r_naught = r_naught
-            self.doubling_time_t = doubling_time_t
-            self.raw_df = raw_df
-            self.dispositions_df = dispositions_df
-            self.admits_df = admits_df
-            self.census_df = census_df
-
-        self.daily_growth = daily_growth_helper(p.doubling_time)
-        self.daily_growth_t = daily_growth_helper(doubling_time_t)
-
-        return None
-
-    def run_projection_dt(self, p: Parameters, doubling_time: float) -> Tuple[pd.DataFrame, float]:
-        intrinsic_growth_rate = self._intrinsic_growth_rate(doubling_time)
-
-        recovery_days = p.recovery_days
-        market_share = p.market_share
-        initial_i = 1 / p.hospitalized.rate / market_share
-
-        S, I, R = p.population - initial_i, initial_i, self.recovered
-
-        # mean recovery rate (inv_recovery_days)
-        gamma = 1 / recovery_days
-
-        # contact rate
-        beta = (intrinsic_growth_rate + gamma) / S
-
-        n_days = p.n_days
-
-        raw_df = sim_sir_df(S,I,R,beta,gamma,n_days)
-
-        current_infected = raw_df.infected.loc[p.n_days_since_first_hospitalized]
-        dispositions_df = build_dispositions_df(raw_df, self._rates, p.market_share)
-
-#       i_dict_v = get_dispositions(raw_df.infected, self._rates, market_share)
-#       r_dict_v = get_dispositions(raw_df.recovered, self._rates, market_share)
-#
-#       dispositions = {
-#           key: value + r_dict_v[key]
-#           for key, value in i_dict_v.items()
-#       }
-#
-#       dispositions_df = pd.DataFrame(dispositions)
-#       dispositions_df = dispositions_df.assign(day=dispositions_df.index)
+        self.gamma = gamma
+        self.beta_t = get_beta(intrinsic_growth_rate, self.gamma, self.susceptible, p.relative_contact_rate)
+        self.intrinsic_growth_rate = intrinsic_growth_rate
 
-        admits_df = build_admits_df(dispositions_df, p.n_days_since_first_hospitalized)
-        census_df = build_census_df(admits_df, self._lengths_of_stay, p.n_days_since_first_hospitalized)
-        return census_df, current_infected
+        if p.date_first_hospitalized is None and p.doubling_time is not None:
+            logger.info('Using doubling_time: %s', p.doubling_time)
+            self.i_day = 0
+            self.beta = (
+                (intrinsic_growth_rate + gamma)
+                / susceptible
+            )
 
-    def loss_dt(self, p: Parameters) -> float:
-        """Squared error: predicted_current_hospitalized vs. actual current hospitalized
+            self.i_day = 0 # seed to the full length
+            self.beta_t = self.beta
+            self.run_projection(p)
+            self.i_day = i_day = int(get_argmin_ds(self.census_df, p.current_hospitalized))
+
+            self.beta_t = get_beta(intrinsic_growth_rate, self.gamma, self.susceptible, p.relative_contact_rate)
+            self.run_projection(p)
+            self.infected = self.raw_df['infected'].values[i_day]
+            self.susceptible = self.raw_df['susceptible'].values[i_day]
+            self.recovered = self.raw_df['recovered'].values[i_day]
+            self.r_t = self.beta_t / gamma * susceptible
+            self.r_naught = self.beta / gamma * susceptible
+            logger.info('Set i_day = %s', i_day)
+            p.date_first_hospitalized = p.current_date - timedelta(days=i_day)
+            logger.info(
+                'Estimated date_first_hospitalized: %s; current_date: %s; i_day: %s',
+                p.date_first_hospitalized,
+                p.current_date,
+                self.i_day)
+
+        elif p.date_first_hospitalized is not None and p.doubling_time is None:
+            self.i_day = (p.current_date - p.date_first_hospitalized).days
+            logger.info(
+                'Using date_first_hospitalized: %s; current_date: %s; i_day: %s',
+                p.date_first_hospitalized,
+                p.current_date,
+                self.i_day)
+            min_loss = 2.0**99
+            dts = np.linspace(1, 15, 29)
+            losses = np.zeros(dts.shape[0])
+            self.current_hospitalized = p.current_hospitalized
+            for i, i_dt in enumerate(dts):
+                intrinsic_growth_rate = get_growth_rate(i_dt)
+                self.beta = get_beta(intrinsic_growth_rate, self.gamma, self.susceptible, 0.0)
+                self.beta_t = get_beta(intrinsic_growth_rate, self.gamma, self.susceptible, p.relative_contact_rate)
+
+                self.run_projection(p)
+                loss = self.get_loss()
+                losses[i] = loss
+
+            p.doubling_time = dts[pd.Series(losses).argmin()]
+            logger.info('Estimated doubling_time: %s', p.doubling_time)
+            intrinsic_growth_rate = get_growth_rate(p.doubling_time)
+            self.beta = get_beta(intrinsic_growth_rate, self.gamma, self.susceptible, 0.0)
+            self.beta_t = get_beta(intrinsic_growth_rate, self.gamma, self.susceptible, p.relative_contact_rate)
+            self.run_projection(p)
 
-        gets prediction of current hospitalized from a census_df which
-        is dependent on a given doubling_time in state.
-        """
-        # get the predicted number of hospitalized today
-        predicted_current_hospitalized = self.census_df.hospitalized.loc[p.n_days_since_first_hospitalized]
+            self.intrinsic_growth_rate = intrinsic_growth_rate
+            self.population = p.population
+        else:
+            logger.info(
+                'doubling_time: %s; date_first_hospitalized: %s',
+                p.doubling_time,
+                p.date_first_hospitalized,
+            )
+            raise AssertionError('doubling_time or date_first_hospitalized must be provided.')
 
-        # compare against actual / user inputted number
-        # we shall optimize squared distance
-        return (p.current_hospitalized - predicted_current_hospitalized) ** 2
+        logger.info('len(np.arange(-i_day, n_days+1)): %s', len(np.arange(-self.i_day, p.n_days+1)))
+        logger.info('len(raw_df): %s', len(self.raw_df))
 
-    def loss_ds(self, p: Parameters) -> float:
-        """loss with respect to days_since """
+        self.r_t = self.beta_t / gamma * susceptible
+        self.r_naught = self.beta / gamma * susceptible
 
-        ## get the predicted number hospitalized today
-        pred_current_hospitalized = self.census_df['hospitalized'].loc[p.n_days_since_first_hospitalized]
+        doubling_time_t = 1.0 / np.log2(
+            self.beta_t * susceptible - gamma + 1)
+        self.doubling_time_t = doubling_time_t
 
-        ## compare against the actual (user inputed) number
-        ## squared difference is the loss to be optimized
-        return (p.current_hospitalized - pred_current_hospitalized)**2
+        self.sim_sir_w_date_df = build_sim_sir_w_date_df(self.raw_df, p.current_date)
+
+        self.daily_growth_rate = get_growth_rate(p.doubling_time)
+        self.daily_growth_rate_t = get_growth_rate(self.doubling_time_t)
+
+    def run_projection(self, p):
+        self.raw_df = sim_sir_df(
+            self.susceptible,
+            self.infected,
+            p.recovered,
+            self.gamma,
+            -self.i_day,
+            self.beta,
+            self.i_day,
+            self.beta_t,
+            p.n_days
+        )
+        self.dispositions_df = build_dispositions_df(self.raw_df, self.rates, p.market_share, p.current_date)
+        self.admits_df = build_admits_df(self.dispositions_df)
+        self.census_df = build_census_df(self.admits_df, self.lengths_of_stay)
+        self.current_infected = self.raw_df.infected.loc[self.i_day]
+
+    def get_loss(self) -> float:
+        """Squared error: predicted vs. actual current hospitalized."""
+        predicted = self.census_df.hospitalized.loc[self.i_day]
+        return (self.current_hospitalized - predicted) ** 2.0
+
+
+def get_argmin_ds(census_df: pd.DataFrame, current_hospitalized: float) -> float:
+    losses_df = (census_df.hospitalized - current_hospitalized) ** 2.0
+    return losses_df.argmin()
+
+
+def get_beta(
+    intrinsic_growth_rate: float,
+    gamma: float,
+    susceptible: float,
+    relative_contact_rate: float
+) -> float:
+    return (
+        (intrinsic_growth_rate + gamma)
+        / susceptible
+        * (1.0 - relative_contact_rate)
+    )
 
-    def argmin_ds(self, p: Parameters) -> float:
-        losses_df = (self.census_df.hospitalized - p.current_hospitalized) ** 2
-        return losses_df.argmin()
 
-    @staticmethod
-    def _intrinsic_growth_rate(doubling_time: Optional[float]) -> float:
-        if doubling_time is not None:
-            return (2.0 ** (1.0 / doubling_time) - 1.0) if doubling_time > 0.0 else 0.0
+def get_growth_rate(doubling_time: Optional[float]) -> float:
+    """Calculates average daily growth rate from doubling time."""
+    if doubling_time is None or doubling_time == 0.0:
         return 0.0
+    return (2.0 ** (1.0 / doubling_time) - 1.0)
 
-    @staticmethod
-    def _beta(
-            intrinsic_growth_rate: float,
-            gamma: float,
-            susceptible: float,
-            relative_contact_rate: float) -> float:
-        return (
-            (intrinsic_growth_rate + gamma)
-            / susceptible
-            * (1.0 - relative_contact_rate)
-        )
 
-###################
-##  MODEL FUNCS  ##
-###################
 def sir(
     s: float, i: float, r: float, beta: float, gamma: float, n: float
 ) -> Tuple[float, float, float]:
@@ -253,60 +211,77 @@ def sir(
     s_n = (-beta * s * i) + s
     i_n = (beta * s * i - gamma * i) + i
     r_n = gamma * i + r
-    if s_n < 0.0:
-        s_n = 0.0
-    if i_n < 0.0:
-        i_n = 0.0
-    if r_n < 0.0:
-        r_n = 0.0
 
+    # TODO:
+    #   Post check dfs for negative values and
+    #   warn the user that their input data is bad.
+    #   JL: I suspect that these adjustments covered bugs.
+
+    #if s_n < 0.0:
+    #    s_n = 0.0
+    #if i_n < 0.0:
+    #    i_n = 0.0
+    #if r_n < 0.0:
+    #    r_n = 0.0
     scale = n / (s_n + i_n + r_n)
     return s_n * scale, i_n * scale, r_n * scale
 
 
 def gen_sir(
-    s: float, i: float, r: float, beta: float, gamma: float, n_days: int
+    s: float, i: float, r: float, gamma: float, i_day: int, *args
 ) -> Generator[Tuple[int, float, float, float], None, None]:
-    """Simulate SIR model forward in time yielding tuples."""
+    """Simulate SIR model forward in time yielding tuples.
+    Parameter order has changed to allow multiple (beta, n_days)
+    to reflect multiple changing social distancing policies.
+    """
     s, i, r = (float(v) for v in (s, i, r))
     n = s + i + r
-    for d in range(n_days + 1):
-        yield d, s, i, r
-        s, i, r = sir(s, i, r, beta, gamma, n)
+    d = i_day
+    while args:
+        beta, n_days, *args = args
+        for _ in range(n_days):
+            yield d, s, i, r
+            s, i, r = sir(s, i, r, beta, gamma, n)
+            d += 1
+    yield d, s, i, r
 
 
 def sim_sir_df(
-    s: float, i: float, r: float, beta: float, gamma: float, n_days: int
+    s: float, i: float, r: float, gamma: float, i_day: int, *args
 ) -> pd.DataFrame:
     """Simulate the SIR model forward in time."""
-    dat = pd.DataFrame(
-        data=gen_sir(s, i, r, beta, gamma, n_days),
+    return pd.DataFrame(
+        data=gen_sir(s, i, r, gamma, i_day, *args),
         columns=("day", "susceptible", "infected", "recovered"),
     )
-    return dat
 
 
-def get_dispositions(
-    patients: np.ndarray,
-    rates: Dict[str, float],
-    market_share: float,
-) -> Dict[str, np.ndarray]:
-    """Get dispositions of patients adjusted by rate and market_share."""
-    return {
-        key: patients * rate * market_share
-        for key, rate in rates.items()
-    }
+def build_sim_sir_w_date_df(
+    raw_df: pd.DataFrame,
+    current_date: datetime,
+) -> pd.DataFrame:
+    day = raw_df.day
+    return pd.DataFrame({
+        "day": day,
+        "date": day.astype('timedelta64[D]') + np.datetime64(current_date),
+        "susceptible": raw_df.susceptible,
+        "infected": raw_df.infected,
+        "recovered": raw_df.recovered,
+    })
 
 
 def build_dispositions_df(
-    sim_sir_df: pd.DataFrame,
+    raw_df: pd.DataFrame,
     rates: Dict[str, float],
     market_share: float,
+    current_date: datetime,
 ) -> pd.DataFrame:
-    """Get dispositions of patients adjusted by rate and market_share."""
-    patients = sim_sir_df.infected + sim_sir_df.recovered
+    """Build dispositions dataframe of patients adjusted by rate and market_share."""
+    patients = raw_df.infected + raw_df.recovered
+    day = raw_df.day
     return pd.DataFrame({
-        "day": sim_sir_df.day,
+        "day": day,
+        "date": day.astype('timedelta64[D]') + np.datetime64(current_date),
         **{
             key: patients * rate * market_share
             for key, rate in rates.items()
@@ -314,24 +289,22 @@ def build_dispositions_df(
     })
 
 
-def build_admits_df(dispositions_df: pd.DataFrame, n_days_since_first_hospitalized: int) -> pd.DataFrame:
+def build_admits_df(dispositions_df: pd.DataFrame) -> pd.DataFrame:
     """Build admits dataframe from dispositions."""
     admits_df = dispositions_df.iloc[:-1, :] - dispositions_df.shift(1)
-    if n_days_since_first_hospitalized is not None:
-        admits_df.day = dispositions_df.day - n_days_since_first_hospitalized
-    else:
-        admits_df.day = dispositions_df.day
+    admits_df.day = dispositions_df.day
+    admits_df.date = dispositions_df.date
     return admits_df
 
 
 def build_census_df(
     admits_df: pd.DataFrame,
     lengths_of_stay: Dict[str, int],
-    n_days_since_first_hospitalized: int
 ) -> pd.DataFrame:
     """Average Length of Stay for each disposition of COVID-19 case (total guesses)"""
     return pd.DataFrame({
         'day': admits_df.day,
+        'date': admits_df.date,
         **{
             key: (
                 admits_df[key].cumsum().iloc[:-los]
@@ -340,15 +313,3 @@ def build_census_df(
             for key, los in lengths_of_stay.items()
         }
     })
-
-
-
-#############
-##  UTILS  ##
-#############
-def daily_growth_helper(doubling_time: float) -> float:
-    """Calculates average daily growth rate from doubling time"""
-    result = 0
-    if doubling_time != 0 and doubling_time is not None:
-        result = (np.power(2, 1.0 / doubling_time) - 1) * 100
-    return result
diff --git a/src/penn_chime/parameters.py b/src/penn_chime/parameters.py
index 0dca750..2dc494a 100644
--- a/src/penn_chime/parameters.py
+++ b/src/penn_chime/parameters.py
@@ -1,13 +1,28 @@
 """Parameters.
 
 Changes affecting results or their presentation should also update
-`change_date`, so users can see when results have last changed
+constants.py `change_date``.
 """
 
-from typing import Optional
+from collections import namedtuple
 from datetime import date
+from typing import Optional
+
+
+# (0.02, 7) is 2%, 7 days
+# be sure to multiply by 100 when using as a default to the percent widgets!
+RateLos = namedtuple("RateLos", ("rate", "length_of_stay"))
+
 
-from .utils import RateLos
+class Regions:
+    """Arbitrary regions to sum population."""
+
+    def __init__(self, **kwargs):
+        population = 0
+        for key, value in kwargs.items():
+            setattr(self, key, value)
+            population += value
+        self.population = population
 
 
 class Parameters:
@@ -17,43 +32,45 @@ class Parameters:
         self,
         *,
         current_hospitalized: int,
-        doubling_time: float,
-        known_infected: int,
-        relative_contact_rate: float,
-        population: int,
-
         hospitalized: RateLos,
         icu: RateLos,
+        relative_contact_rate: float,
         ventilated: RateLos,
-
-        as_date: bool = False,
-        market_share: float = 1.0,
+        current_date: date = date.today(),
         date_first_hospitalized: Optional[date] = None,
+        doubling_time: Optional[float] = None,
+        infectious_days: int = 14,
+        market_share: float = 1.0,
         max_y_axis: Optional[int] = None,
         n_days: int = 60,
-        recovery_days: int = 14,
+        population: Optional[int] = None,
+        recovered: int = 0,
+        region: Optional[Regions] = None,
     ):
         self.current_hospitalized = current_hospitalized
-        self.doubling_time = doubling_time
-        self.known_infected = known_infected
         self.relative_contact_rate = relative_contact_rate
-        self.population = population
 
         self.hospitalized = hospitalized
         self.icu = icu
         self.ventilated = ventilated
 
-        self.as_date = as_date
+        if region is not None and population is None:
+            self.region = region
+            self.population = region.population
+        elif population is not None:
+            self.region = None
+            self.population = population
+        else:
+            raise AssertionError('population or regions must be provided.')
+
+        self.current_date = current_date
+        self.date_first_hospitalized = date_first_hospitalized
+        self.doubling_time = doubling_time
+        self.infectious_days = infectious_days
         self.market_share = market_share
         self.max_y_axis = max_y_axis
         self.n_days = n_days
-        self.recovery_days = recovery_days
-        if date_first_hospitalized:
-            self.date_first_hospitalized = date_first_hospitalized # needed for utils.add_date_column
-            self.n_days_since_first_hospitalized = (date.today() - date_first_hospitalized).days
-        else:
-            self.date_first_hospitalized = None # needed for utils.add_date_column
-            self.n_days_since_first_hospitalized = None
+        self.recovered = recovered
 
         self.labels = {
             "hospitalized": "Hospitalized",
@@ -71,11 +88,3 @@ class Parameters:
             "icu": icu,
             "ventilated": ventilated,
         }
-
-    def change_date(self):
-        """
-        This reflects a date from which previously-run reports will no
-        longer match current results, indicating when users should
-        re-run their reports
-        """
-        return "March 23 2020"
diff --git a/src/penn_chime/presentation.py b/src/penn_chime/presentation.py
index 24ab029..7590b60 100644
--- a/src/penn_chime/presentation.py
+++ b/src/penn_chime/presentation.py
@@ -7,15 +7,17 @@ import altair as alt  # type: ignore
 import numpy as np  # type: ignore
 import pandas as pd  # type: ignore
 
-from .defaults import Constants, RateLos
-from .utils import add_date_column, dataframe_to_base64
-from .parameters import Parameters
-
-DATE_FORMAT = "%b, %d"  # see https://strftime.org
-DOCS_URL = "https://code-for-philly.gitbook.io/chime"
-
-FLOAT_INPUT_MIN = 0.001
-FLOAT_INPUT_STEP = FLOAT_INPUT_MIN
+from .constants import (
+    CHANGE_DATE,
+    DATE_FORMAT,
+    DOCS_URL,
+    FLOAT_INPUT_MIN,
+    FLOAT_INPUT_STEP,
+)
+
+from .utils import dataframe_to_base64
+from .parameters import Parameters, RateLos
+from .models import SimSirModel as Model
 
 hide_menu_style = """
         <style>
@@ -31,18 +33,6 @@ hide_menu_style = """
 
 def display_header(st, m, p):
 
-    detection_prob_str = (
-        "{detection_prob:.0%}".format(detection_prob=m.detection_probability)
-        if m.detection_probability
-        else "unknown"
-    )
-
-    infection_warning_str = (
-        """(Warning: The number of known infections is greater than the estimate of infected patients based on inputs for current hospitalization, market share, and hospitalization rate. Please verify the market share value in the sidebar, and see if the hospitalization rate needs to be lowered.)"""
-        if p.known_infected > m.infected
-        else ""
-    )
-
     infected_population_warning_str = (
         """(Warning: The number of estimated infections is greater than the total regional population. Please verify the values entered in the sidebar.)"""
         if m.infected > p.population
@@ -63,25 +53,19 @@ def display_header(st, m, p):
     st.markdown(
         """[Documentation](https://code-for-philly.gitbook.io/chime/) | [Github](https://github.com/CodeForPhilly/chime/) | [Slack](https://codeforphilly.org/chat?channel=covid19-chime-penn)"""
     )
-    st.markdown(
-        """**IMPORTANT NOTICE**: Admissions and Census calculations were previously **undercounting**. Please update your reports generated before """ + p.change_date() + """. See more about changes [here](https://github.com/CodeForPhilly/chime/labels/models)."""
-    )
     st.markdown(
         """*This tool was developed by the [Predictive Healthcare team](http://predictivehealthcare.pennmedicine.org/) at
     Penn Medicine to assist hospitals and public health officials with hospital capacity planning,
     but can be used anywhere in the world.
-    Customize it for your region by modifying data inputs in the left panel.
-    For questions on how to use this tool see the [User docs]({docs_url}). Code can be found on [Github](https://github.com/CodeForPhilly/chime)*.
+    Customize it for your region by modifying data inputs in the left panel.*
     """.format(docs_url=DOCS_URL)
     )
 
     st.markdown(
-        """The estimated number of currently infected individuals is **{total_infections:.0f}**. The **{initial_infections}**
-    confirmed cases in the region imply a **{detection_prob_str}** rate of detection. This is based on current inputs for
+        """The estimated number of currently infected individuals is **{total_infections:.0f}**. This is based on current inputs for
     Hospitalizations (**{current_hosp}**), Hospitalization rate (**{hosp_rate:.0%}**), Region size (**{S}**),
     and Hospital market share (**{market_share:.0%}**).
 
-{infection_warning_str}
 {infected_population_warning_str}
 
 An initial doubling time of **{doubling_time}** days and a recovery time of **{recovery_days}** days imply an $R_0$ of
@@ -92,23 +76,20 @@ outbreak **{impact_statement:s} {doubling_time_t:.1f}** days, implying an effect
 and daily growth rate of **{daily_growth_t:.2f}%**.
 """.format(
             total_infections=m.infected,
-            initial_infections=p.known_infected,
-            detection_prob_str=detection_prob_str,
             current_hosp=p.current_hospitalized,
             hosp_rate=p.hospitalized.rate,
             S=p.population,
             market_share=p.market_share,
-            recovery_days=p.recovery_days,
+            recovery_days=p.infectious_days,
             r_naught=m.r_naught,
             doubling_time=p.doubling_time,
             relative_contact_rate=p.relative_contact_rate,
             r_t=m.r_t,
             doubling_time_t=abs(m.doubling_time_t),
             impact_statement=("halves the infections every" if m.r_t < 1 else "reduces the doubling time to"),
-            daily_growth=m.daily_growth,
-            daily_growth_t=m.daily_growth_t,
+            daily_growth=m.daily_growth_rate * 100.0,
+            daily_growth_t=m.daily_growth_rate_t * 100.0,
             docs_url=DOCS_URL,
-            infection_warning_str=infection_warning_str,
             infected_population_warning_str=infected_population_warning_str
         )
     )
@@ -133,11 +114,13 @@ class NumberInput(Input):
         kwargs = dict(min_value=min_value, max_value=max_value, step=step, format=format, key=key)
         super().__init__(st_obj.number_input, label, value, kwargs)
 
+
 class DateInput(Input):
     def __init__(self, st_obj, label, value=None, key=None):
         kwargs = dict(key=key)
         super().__init__(st_obj.date_input, label, value, kwargs)
 
+
 class PercentInput(NumberInput):
     def __init__(self, st_obj, label, min_value=0.0, max_value=100.0, value=None, step=FLOAT_INPUT_STEP, format="%f", key=None):
         super().__init__(st_obj, label, min_value, max_value, value * 100.0, step, format, key)
@@ -152,14 +135,12 @@ class CheckboxInput(Input):
         super().__init__(st_obj.checkbox, label, value, kwargs)
 
 
-def display_sidebar(st, d: Constants) -> Parameters:
+def display_sidebar(st, d: Parameters) -> Parameters:
     # Initialize variables
     # these functions create input elements and bind the values they are set to
     # to the variables they are set equal to
     # it's kindof like ember or angular if you are familiar with those
 
-    if d.known_infected < 1:
-        raise ValueError("Known cases must be larger than one to enable predictions.")
     st_obj = st.sidebar
     current_hospitalized_input = NumberInput(
         st_obj,
@@ -185,6 +166,11 @@ def display_sidebar(st, d: Constants) -> Parameters:
         step=FLOAT_INPUT_STEP,
         format="%f",
     )
+    current_date_input = DateInput(
+        st_obj,
+        "Current date (Default is today)",
+        value=d.current_date,
+    )
     date_first_hospitalized_input = DateInput(
         st_obj,
         "Date of first hospitalized case",
@@ -244,27 +230,28 @@ def display_sidebar(st, d: Constants) -> Parameters:
         st_obj,
         "Regional Population",
         min_value=1,
-        value=d.region.population,
+        value=(d.region.population or d.population),
         step=1,
         format="%i",
     )
-    known_infected_input = NumberInput(
+    infectious_days_input = NumberInput(
         st_obj,
-        "Currently Known Regional Infections (only used to compute detection rate - does not change projections)",
+        "Infectious Days",
         min_value=0,
-        value=d.known_infected,
+        value=d.infectious_days,
         step=1,
         format="%i",
     )
-    as_date_input = CheckboxInput(st_obj, "Present result as dates instead of days", value=False)
     max_y_axis_set_input = CheckboxInput(st_obj, "Set the Y-axis on graphs to a static value")
     max_y_axis_input = NumberInput(st_obj, "Y-axis static value", value=500, format="%i", step=25)
 
     # Build in desired order
+    current_date = current_date_input()
+
     st.sidebar.markdown("### Regional Parameters [ℹ]({docs_url}/what-is-chime/parameters)".format(docs_url=DOCS_URL))
     population = population_input()
     market_share = market_share_pct_input()
-    known_infected = known_infected_input()
+    #known_infected = known_infected_input()
     current_hospitalized = current_hospitalized_input()
 
     st.sidebar.markdown("### Spread and Contact Parameters [ℹ]({docs_url}/what-is-chime/parameters)"
@@ -274,11 +261,13 @@ def display_sidebar(st, d: Constants) -> Parameters:
         date_first_hospitalized = date_first_hospitalized_input()
         doubling_time = None
     else:
-        date_first_hospitalized = None
         doubling_time = doubling_time_input()
+        date_first_hospitalized = None
+
     relative_contact_rate = relative_contact_pct_input()
 
     st.sidebar.markdown("### Severity Parameters [ℹ]({docs_url}/what-is-chime/parameters)".format(docs_url=DOCS_URL))
+    infectious_days = infectious_days_input()
     hospitalized_rate = hospitalized_pct_input()
     icu_rate = icu_pct_input()
     ventilated_rate = ventilated_pct_input()
@@ -289,32 +278,37 @@ def display_sidebar(st, d: Constants) -> Parameters:
     st.sidebar.markdown("### Display Parameters [ℹ]({docs_url}/what-is-chime/parameters)".format(docs_url=DOCS_URL))
     n_days = n_days_input()
     max_y_axis_set = max_y_axis_set_input()
-    as_date = as_date_input()
 
     max_y_axis = None
     if max_y_axis_set:
         max_y_axis = max_y_axis_input()
 
     return Parameters(
-        as_date=as_date,
         current_hospitalized=current_hospitalized,
-        market_share=market_share,
-        known_infected=known_infected,
-        doubling_time=doubling_time,
-        date_first_hospitalized=date_first_hospitalized,
+        hospitalized=RateLos(hospitalized_rate, hospitalized_los),
+        icu=RateLos(icu_rate, icu_los),
+        #known_infected=known_infected,
+        relative_contact_rate=relative_contact_rate,
+        ventilated=RateLos(ventilated_rate, ventilated_los),
 
+        current_date=current_date,
+        date_first_hospitalized=date_first_hospitalized,
+        doubling_time=doubling_time,
+        infectious_days=infectious_days,
+        market_share=market_share,
         max_y_axis=max_y_axis,
         n_days=n_days,
-        relative_contact_rate=relative_contact_rate,
         population=population,
-
-        hospitalized=RateLos(hospitalized_rate, hospitalized_los),
-        icu=RateLos(icu_rate, icu_los),
-        ventilated=RateLos(ventilated_rate, ventilated_los),
     )
 
 
-def show_more_info_about_this_tool(st, model, parameters, defaults, notes: str = ""):
+def display_more_info(
+    st,
+    model: Model,
+    parameters: Parameters,
+    defaults: Parameters,
+    notes: str = "",
+):
     """a lot of streamlit writing to screen."""
     st.subheader(
         "[Discrete-time SIR modeling](https://mathworld.wolfram.com/SIRModel.html) of infections/recovery"
@@ -352,7 +346,7 @@ $$\\beta$$ can be interpreted as the _effective contact rate_:
 $\\gamma$ is the inverse of the mean recovery time, in days.  I.e.: if $\\gamma = 1/{recovery_days}$, then the average infection will clear in {recovery_days} days.
 
 An important descriptive parameter is the _basic reproduction number_, or $R_0$.  This represents the average number of people who will be infected by any given infected person.  When $R_0$ is greater than 1, it means that a disease will grow.  Higher $R_0$'s imply more rapid growth.  It is defined as """.format(
-            recovery_days=int(parameters.recovery_days)
+            recovery_days=int(parameters.infectious_days)
         )
     )
     st.latex("R_0 = \\beta /\\gamma")
@@ -384,7 +378,7 @@ We need to express the two parameters $\\beta$ and $\\gamma$ in terms of quantit
 - To estimate $$\\beta$$ directly, we'd need to know transmissibility and social contact rates.  since we don't know these things, we can extract it from known _doubling times_.  The AHA says to expect a doubling time $T_d$ of 7-10 days. That means an early-phase rate of growth can be computed by using the doubling time formula:
 """.format(
             doubling_time=parameters.doubling_time,
-            recovery_days=parameters.recovery_days,
+            recovery_days=parameters.infectious_days,
             r_naught=model.r_naught,
             relative_contact_rate=parameters.relative_contact_rate,
             doubling_time_t=model.doubling_time_t,
@@ -433,93 +427,8 @@ def write_footer(st):
     st.markdown("© 2020, The Trustees of the University of Pennsylvania")
 
 
-def show_additional_projections(
-    st, alt, charting_func, model, parameters
-):
-    st.subheader(
-        "The number of infected and recovered individuals in the hospital catchment region at any given moment"
-    )
-
-    st.altair_chart(
-        charting_func(
-            alt,
-            model=model,
-            parameters=parameters
-        ),
-        use_container_width=True,
-    )
-
-
-##########
-# Tables #
-##########
-
-
-def draw_projected_admissions_table(
-        st,
-        projection_admits: pd.DataFrame,
-        labels,
-        day_range,
-        date_first_hospitalized: Optional[date] = None,
-        as_date: bool = False
-):
-    admits_table = projection_admits[np.mod(projection_admits.index, day_range) == 0].copy()
-    admits_table["day"] = admits_table.index
-    admits_table.index = range(admits_table.shape[0])
-    admits_table = admits_table.fillna(0).astype(int)
-
-    if as_date:
-        admits_table = add_date_column(
-            admits_table, date_first_hospitalized, drop_day_column=True, date_format=DATE_FORMAT
-        )
-    admits_table.rename(labels)
-    st.table(admits_table)
-    return None
-
-
-def draw_census_table(
-        st, census_df: pd.DataFrame, labels, day_range, date_first_hospitalized: Optional[date] = None, as_date: bool = False):
-    census_table = census_df[np.mod(census_df.index, day_range) == 0].copy()
-    census_table.index = range(census_table.shape[0])
-    census_table.loc[0, :] = 0
-    census_table = census_table.dropna().astype(int)
-
-    if as_date:
-        census_table = add_date_column(
-            census_table, date_first_hospitalized, drop_day_column=True, date_format=DATE_FORMAT
-        )
-
-    census_table.rename(labels)
-    st.table(census_table)
-    return None
-
-
-def draw_raw_sir_simulation_table(st, model, parameters):
-    as_date = parameters.as_date
-    projection_area = model.raw_df
-    if parameters.n_days_since_first_hospitalized is not None:
-        projection_area.loc[:, "day"] = projection_area.day - parameters.n_days_since_first_hospitalized
-    infect_table = (projection_area.iloc[::7, :]).apply(np.floor)
-    infect_table.index = range(infect_table.shape[0])
-    infect_table["day"] = infect_table.day.astype(int)
-
-    if as_date:
-        infect_table = add_date_column(
-            infect_table, parameters.date_first_hospitalized, drop_day_column=True, date_format=DATE_FORMAT
-        )
-
-    st.table(infect_table)
-    build_download_link(st,
-        filename="raw_sir_simulation_data.csv",
-        df=projection_area,
-        parameters=parameters
-    )
-
-def build_download_link(st, filename: str, df: pd.DataFrame, parameters: Parameters):
-    if parameters.as_date:
-        df = add_date_column(df, parameters.date_first_hospitalized, drop_day_column=True, date_format="%Y-%m-%d")
-
+def display_download_link(st, filename: str, df: pd.DataFrame):
     csv = dataframe_to_base64(df)
     st.markdown("""
-        <a download="{filename}" href="data:file/csv;base64,{csv}">Download full table as CSV</a>
+        <a download="{filename}" href="data:file/csv;base64,{csv}">Download {filename}</a>
 """.format(csv=csv,filename=filename), unsafe_allow_html=True)
diff --git a/src/penn_chime/settings.py b/src/penn_chime/settings.py
index ec7ea45..1e14215 100644
--- a/src/penn_chime/settings.py
+++ b/src/penn_chime/settings.py
@@ -2,32 +2,24 @@
 
 from datetime import date
 
-from .defaults import Constants, Regions, RateLos
+from .parameters import Parameters, Regions, RateLos
 
-delaware = 564696
-chester = 519293
-montgomery = 826075
-bucks = 628341
-philly = 1581000
-
-DEFAULTS = Constants(
-    # EDIT YOUR DEFAULTS HERE
+DEFAULTS = Parameters(
     region=Regions(
-        delaware=delaware,
-        chester=chester,
-        montgomery=montgomery,
-        bucks=bucks,
-        philly=philly,
+        delaware=564696,
+        chester=519293,
+        montgomery=826075,
+        bucks=628341,
+        philly=1581000,
     ),
     current_hospitalized=32,
-    doubling_time=4.0,
     date_first_hospitalized=date(2020,3,7),
-
-    known_infected=510,
-    n_days=60,
-    market_share=0.15,
-    relative_contact_rate=0.3,
+    doubling_time=4.0,
     hospitalized=RateLos(0.025, 7),
     icu=RateLos(0.0075, 9),
+    infectious_days=14,
+    market_share=0.15,
+    n_days=60,
+    relative_contact_rate=0.3,
     ventilated=RateLos(0.005, 10),
 )
diff --git a/src/penn_chime/utils.py b/src/penn_chime/utils.py
index e34b2c2..3fc1fad 100644
--- a/src/penn_chime/utils.py
+++ b/src/penn_chime/utils.py
@@ -1,84 +1,9 @@
 """Utils."""
 
-from collections import namedtuple
-from datetime import datetime, timedelta, date
-from typing import Optional
 from base64 import b64encode
 
-import numpy as np  # type: ignore
-import pandas as pd  # type: ignore
+import pandas as pd
 
-# from .parameters import Parameters
-
-
-# (0.02, 7) is 2%, 7 days
-# be sure to multiply by 100 when using as a default to the pct widgets!
-RateLos = namedtuple("RateLos", ("rate", "length_of_stay"))
-
-
-
-
-def add_date_column(
-    df: pd.DataFrame, date_first_hospitalized: Optional[date], drop_day_column: bool = False, date_format: Optional[str] = None,
-) -> pd.DataFrame:
-    """Copies input data frame and converts "day" column to "date" column
-
-    Assumes that day=0 is today and allocates dates for each integer day.
-    Day range can must not be continous.
-    Columns will be organized as original frame with difference that date
-    columns come first.
-
-    Arguments:
-        df: The data frame to convert.
-        drop_day_column: If true, the returned data frame will not have a day column.
-        date_format: If given, converts date_time objetcts to string format specified.
-
-    Raises:
-        KeyError: if "day" column not in df
-        ValueError: if "day" column is not of type int
-    """
-    if not "day" in df:
-        raise KeyError("Input data frame for converting dates has no 'day column'.")
-    if not pd.api.types.is_integer_dtype(df.day):
-        raise KeyError("Column 'day' for dates converting data frame is not integer.")
-
-    df = df.copy()
-    # Prepare columns for sorting
-    non_date_columns = [col for col in df.columns if not col == "day"]
-
-    # Allocate (day) continous range for dates
-    n_days = int(df.day.max())
-
-    today = date.today()
-    if date_first_hospitalized:
-        delta = today - date_first_hospitalized
-        start = today - delta
-        delta_days = delta.days
-    else:
-        start = today
-        delta_days = 0
-        delta = timedelta(days=0)
-    end = start + timedelta(days=n_days + 1) + delta
-
-    # And pick dates present in frame
-    dates = pd.date_range(
-        start=start, end=end, freq="D", closed="right"
-    )[[x + delta_days for x in df.day.tolist()]]
-
-    if date_format is not None:
-        dates = dates.strftime(date_format)
-
-    df["date"] = dates
-
-    if drop_day_column:
-        df.pop("day")
-        date_columns = ["date"]
-    else:
-        date_columns = ["day", "date"]
-
-    # sort columns
-    df = df[date_columns + non_date_columns]
-    return df
 
 def dataframe_to_base64(df: pd.DataFrame) -> str:
     """Converts a dataframe to a base64-encoded CSV representation of that data.


Test patch:
diff --git a/e2e/cypress/integration/tests/actions.spec.js b/e2e/cypress/integration/tests/actions.spec.js
index f655b37..cb13136 100644
--- a/e2e/cypress/integration/tests/actions.spec.js
+++ b/e2e/cypress/integration/tests/actions.spec.js
@@ -10,7 +10,7 @@ context('Actions', () => {
 
     // This gets the "first" input from the sidebar. From clicking step up,
     // the Regional Population should increase from default 4119405 to 4219405.
-    cy.get('input.st-al')
+    cy.get('input.st-al').eq(1)
       .should('has.value', '4119406')
   })
 });
diff --git a/tests/by_date_first_hospitalized/2020-03-28_projected_admits.csv b/tests/by_date_first_hospitalized/2020-03-28_projected_admits.csv
new file mode 100644
index 0000000..956b1d2
--- /dev/null
+++ b/tests/by_date_first_hospitalized/2020-03-28_projected_admits.csv
@@ -0,0 +1,83 @@
+,day,date,hospitalized,icu,ventilated
+0,-21,2020-03-07,,,
+1,-20,2020-03-08,2.0286562559739014,0.6085968767921708,0.40573125119477993
+2,-19,2020-03-09,2.1756307989909693,0.6526892396972892,0.43512615979819413
+3,-18,2020-03-10,2.3331900866282673,0.699957025988482,0.46663801732565346
+4,-17,2020-03-11,2.5020868607396523,0.7506260582218944,0.5004173721479299
+5,-16,2020-03-12,2.6831259576055686,0.8049377872816716,0.5366251915211153
+6,-15,2020-03-13,2.8771676957295433,0.8631503087188612,0.5754335391459069
+7,-14,2020-03-14,3.085131450543564,0.9255394351630688,0.6170262901087131
+8,-13,2020-03-15,3.3079994209680343,0.9923998262904128,0.6615998841936079
+9,-12,2020-03-16,3.546820591907732,1.064046177572319,0.7093641183815453
+10,-11,2020-03-17,3.8027148956743986,1.1408144687023203,0.7605429791348799
+11,-10,2020-03-18,4.076877573970123,1.223063272191034,0.8153755147940238
+12,-9,2020-03-19,4.370583740399979,1.3111751221199928,0.8741167480799952
+13,-8,2020-03-20,4.685193141466399,1.4055579424399198,0.9370386282932817
+14,-7,2020-03-21,5.022155111568729,1.5066465334706187,1.004431022313744
+15,-6,2020-03-22,5.383013714639972,1.6149041143919902,1.076602742927994
+16,-5,2020-03-23,5.769413061625826,1.7308239184877472,1.153882612325166
+17,-4,2020-03-24,6.183102788978275,1.8549308366934874,1.236620557795657
+18,-3,2020-03-25,6.62594367861783,1.9877831035853433,1.3251887357235645
+19,-2,2020-03-26,7.099913394323451,2.129974018297041,1.4199826788646916
+20,-1,2020-03-27,7.60711230314665,2.2821336909439935,1.5214224606293278
+21,0,2020-03-28,8.149769343103543,2.444930802931058,1.62995386862071
+22,1,2020-03-29,8.73024788998022,2.61907436699407,1.7460495779960432
+23,2,2020-03-30,9.351051566453606,2.805315469936076,1.8702103132907197
+24,3,2020-03-31,10.014829925785108,3.0044489777355423,2.002965985157026
+25,4,2020-04-01,10.724383929936607,3.217315178980982,2.144876785987318
+26,5,2020-04-02,11.482671127981348,3.4448013383943987,2.296534225596268
+27,6,2020-04-03,12.292810424996532,3.687843127498965,2.458562084999315
+28,7,2020-04-04,13.1580863141223,3.9474258942366873,2.631617262824456
+29,8,2020-04-05,14.081952425037827,4.224585727511339,2.8163904850075525
+30,9,2020-04-06,15.068034220666533,4.520410266199967,3.013606844133328
+31,10,2020-04-07,16.120130650410488,4.836039195123149,3.2240261300820805
+32,11,2020-04-08,17.24221454261297,5.172664362783891,3.448442908522601
+33,12,2020-04-09,18.438431491314248,5.531529447394277,3.6876862982628467
+34,13,2020-04-10,19.713096962765974,5.913929088829789,3.9426193925531905
+35,14,2020-04-11,21.07069131584842,6.3212073947545235,4.214138263169694
+36,15,2020-04-12,22.515852397759147,6.754755719327747,4.503170479551827
+37,16,2020-04-13,24.05336534257168,7.216009602771507,4.810673068514333
+38,17,2020-04-14,25.688149166121377,7.706444749836393,5.137629833224267
+39,18,2020-04-15,27.425239716937256,8.227571915081171,5.485047943387443
+40,19,2020-04-16,29.26976851068298,8.780930553204882,5.853953702136607
+41,20,2020-04-17,31.226936946079718,9.368081083823938,6.245387389215949
+42,21,2020-04-18,33.30198537518976,9.990595612556916,6.660397075037935
+43,22,2020-04-19,35.500156482205284,10.650046944661568,7.10003129644106
+44,23,2020-04-20,37.82665241485097,11.34799572445533,7.565330482970211
+45,24,2020-04-21,40.28658511389142,12.085975534167432,8.057317022778278
+46,25,2020-04-22,42.88491930224427,12.865475790673258,8.576983860448877
+47,26,2020-04-23,45.626407629339155,13.68792228880173,9.125281525867791
+48,27,2020-04-24,48.515517522643336,14.554655256792984,9.703103504528656
+49,28,2020-04-25,51.556349380960796,15.466904814288256,10.31126987619217
+50,29,2020-04-26,54.75254585772677,16.42576375731801,10.950509171545349
+51,30,2020-04-27,58.10719213163725,17.43215763949121,11.621438426327472
+52,31,2020-04-28,61.62270725114081,18.48681217534221,12.324541450228168
+53,32,2020-04-29,65.30072687252357,19.590218061757128,13.060145374504714
+54,33,2020-04-30,69.14197799201725,20.742593397605162,13.82839559840346
+55,34,2020-05-01,73.14614660261168,21.943843980783527,14.629229320522313
+56,35,2020-05-02,77.31173958658405,23.193521875975193,15.462347917316833
+57,36,2020-05-03,81.63594258333728,24.490782775001207,16.327188516667434
+58,37,2020-05-04,86.1144760443101,25.834342813293006,17.22289520886204
+59,38,2020-05-05,90.74145219419847,27.222435658259542,18.148290438839695
+60,39,2020-05-06,95.50923614806652,28.652770844419877,19.10184722961327
+61,40,2020-05-07,100.40831496997339,30.12249449099204,20.081662993994655
+62,41,2020-05-08,105.42717897810962,31.628153693432978,21.085435795621947
+63,42,2020-05-09,110.55222007658494,33.165666022975415,22.110444015317
+64,43,2020-05-10,115.76765229224179,34.73029568767254,23.15353045844836
+65,44,2020-05-11,121.05545997914805,36.31663799374428,24.21109199582952
+66,45,2020-05-12,126.39537928358777,37.918613785076445,25.279075856717725
+67,46,2020-05-13,131.7649183970052,39.52947551910165,26.35298367940095
+68,47,2020-05-14,137.13942182331402,41.14182654699414,27.42788436466276
+69,48,2020-05-15,142.4921833147282,42.74765499441844,28.498436662945664
+70,49,2020-05-16,147.79461125937132,44.3383833778114,29.558922251874378
+71,50,2020-05-17,153.01644911955327,45.904934735865936,30.603289823910472
+72,51,2020-05-18,158.12605202306713,47.43781560692025,31.625210404613654
+73,52,2020-05-19,163.09071882314674,48.927215646943864,32.61814376462917
+74,53,2020-05-20,167.87707691020114,50.363123073060365,33.575415382040205
+75,54,2020-05-21,172.45151485003908,51.735454455011904,34.49030297000786
+76,55,2020-05-22,176.78065563171685,53.034196689515056,35.356131126343485
+77,56,2020-05-23,180.83186104892002,54.249558314675824,36.16637220978396
+78,57,2020-05-24,184.57375564209042,55.372126692627035,36.91475112841795
+79,58,2020-05-25,187.9767568340576,56.39302705021737,37.595351366811656
+80,59,2020-05-26,191.01359653660074,57.304078960980405,38.202719307320194
+81,60,2020-05-27,,,
diff --git a/tests/by_date_first_hospitalized/2020-03-28_projected_census.csv b/tests/by_date_first_hospitalized/2020-03-28_projected_census.csv
new file mode 100644
index 0000000..2370c6a
--- /dev/null
+++ b/tests/by_date_first_hospitalized/2020-03-28_projected_census.csv
@@ -0,0 +1,83 @@
+,day,date,hospitalized,icu,ventilated
+0,-21,2020-03-07,,,
+1,-20,2020-03-08,3.0,1.0,1.0
+2,-19,2020-03-09,5.0,2.0,1.0
+3,-18,2020-03-10,7.0,2.0,2.0
+4,-17,2020-03-11,10.0,3.0,2.0
+5,-16,2020-03-12,12.0,4.0,3.0
+6,-15,2020-03-13,15.0,5.0,3.0
+7,-14,2020-03-14,18.0,6.0,4.0
+8,-13,2020-03-15,19.0,7.0,5.0
+9,-12,2020-03-16,21.0,8.0,5.0
+10,-11,2020-03-17,22.0,8.0,6.0
+11,-10,2020-03-18,24.0,9.0,7.0
+12,-9,2020-03-19,26.0,10.0,7.0
+13,-8,2020-03-20,27.0,10.0,7.0
+14,-7,2020-03-21,29.0,11.0,8.0
+15,-6,2020-03-22,31.0,12.0,9.0
+16,-5,2020-03-23,34.0,12.0,9.0
+17,-4,2020-03-24,36.0,13.0,10.0
+18,-3,2020-03-25,39.0,14.0,10.0
+19,-2,2020-03-26,41.0,15.0,11.0
+20,-1,2020-03-27,44.0,16.0,12.0
+21,0,2020-03-28,47.0,17.0,13.0
+22,1,2020-03-29,51.0,19.0,14.0
+23,2,2020-03-30,54.0,20.0,14.0
+24,3,2020-03-31,58.0,21.0,15.0
+25,4,2020-04-01,62.0,23.0,17.0
+26,5,2020-04-02,67.0,24.0,18.0
+27,6,2020-04-03,71.0,26.0,19.0
+28,7,2020-04-04,76.0,28.0,20.0
+29,8,2020-04-05,82.0,30.0,22.0
+30,9,2020-04-06,87.0,32.0,23.0
+31,10,2020-04-07,93.0,34.0,25.0
+32,11,2020-04-08,100.0,37.0,26.0
+33,12,2020-04-09,107.0,39.0,28.0
+34,13,2020-04-10,114.0,42.0,30.0
+35,14,2020-04-11,122.0,45.0,32.0
+36,15,2020-04-12,131.0,48.0,34.0
+37,16,2020-04-13,140.0,51.0,37.0
+38,17,2020-04-14,149.0,54.0,39.0
+39,18,2020-04-15,159.0,58.0,42.0
+40,19,2020-04-16,170.0,62.0,45.0
+41,20,2020-04-17,182.0,66.0,48.0
+42,21,2020-04-18,194.0,71.0,51.0
+43,22,2020-04-19,207.0,76.0,54.0
+44,23,2020-04-20,221.0,81.0,58.0
+45,24,2020-04-21,235.0,86.0,62.0
+46,25,2020-04-22,251.0,92.0,66.0
+47,26,2020-04-23,267.0,98.0,70.0
+48,27,2020-04-24,284.0,104.0,75.0
+49,28,2020-04-25,303.0,111.0,80.0
+50,29,2020-04-26,322.0,118.0,85.0
+51,30,2020-04-27,342.0,125.0,90.0
+52,31,2020-04-28,364.0,133.0,96.0
+53,32,2020-04-29,386.0,141.0,102.0
+54,33,2020-04-30,409.0,150.0,108.0
+55,34,2020-05-01,434.0,159.0,115.0
+56,35,2020-05-02,460.0,168.0,122.0
+57,36,2020-05-03,487.0,178.0,129.0
+58,37,2020-05-04,515.0,189.0,136.0
+59,38,2020-05-05,544.0,199.0,144.0
+60,39,2020-05-06,574.0,211.0,152.0
+61,40,2020-05-07,605.0,222.0,161.0
+62,41,2020-05-08,638.0,234.0,169.0
+63,42,2020-05-09,671.0,247.0,178.0
+64,43,2020-05-10,705.0,260.0,188.0
+65,44,2020-05-11,740.0,273.0,197.0
+66,45,2020-05-12,776.0,286.0,207.0
+67,46,2020-05-13,812.0,300.0,217.0
+68,47,2020-05-14,849.0,314.0,227.0
+69,48,2020-05-15,886.0,328.0,238.0
+70,49,2020-05-16,923.0,342.0,248.0
+71,50,2020-05-17,960.0,356.0,259.0
+72,51,2020-05-18,997.0,371.0,
+73,52,2020-05-19,1034.0,,
+74,53,2020-05-20,1070.0,,
+75,54,2020-05-21,,,
+76,55,2020-05-22,,,
+77,56,2020-05-23,,,
+78,57,2020-05-24,,,
+79,58,2020-05-25,,,
+80,59,2020-05-26,,,
+81,60,2020-05-27,,,
diff --git a/tests/by_date_first_hospitalized/2020-03-28_sim_sir_w_date.csv b/tests/by_date_first_hospitalized/2020-03-28_sim_sir_w_date.csv
new file mode 100644
index 0000000..ed3eca4
--- /dev/null
+++ b/tests/by_date_first_hospitalized/2020-03-28_sim_sir_w_date.csv
@@ -0,0 +1,83 @@
+,day,date,susceptible,infected,recovered
+0,-21,2020-03-07,4115671.6666666665,3756.2813399807924,0.0
+1,-20,2020-03-08,4115130.6916650734,4028.9505315752044,268.30580999862804
+2,-19,2020-03-09,4114550.523452009,4321.3365638126625,556.0879908254283
+3,-18,2020-03-10,4113928.339428908,4634.8536894983445,864.7548882406184
+4,-17,2020-03-11,4113261.1162660443,4971.015874540941,1195.8158660619288
+5,-16,2020-03-12,4112545.6160106827,5331.443567435216,1550.8884285291388
+6,-15,2020-03-13,4111778.3712918223,5717.870888622482,1931.7058262030832
+7,-14,2020-03-14,4110955.6695716768,6132.153259580113,2340.125175390403
+8,-13,2020-03-15,4110073.5363927516,6576.275491392059,2778.136122503268
+9,-12,2020-03-16,4109127.717568244,7052.360352229924,3247.87008617413
+10,-11,2020-03-17,4108113.66026273,7562.677632583818,3751.6101113334093
+11,-10,2020-03-18,4107026.4929096713,8109.653726172242,4291.801370803682
+12,-9,2020-03-19,4105861.0039122314,8695.881743171363,4871.062351244556
+13,-8,2020-03-20,4104611.6190745067,9324.132170669212,5492.196761471081
+14,-7,2020-03-21,4103272.377711422,9997.36409299212,6158.2062022331675
+15,-6,2020-03-22,4101836.907387518,10718.73698168239,6872.30363744689
+16,-5,2020-03-23,4100298.397237751,11491.623061329106,7637.927707567061
+17,-4,2020-03-24,4098649.569827357,12319.620253056952,8458.757926233426
+18,-3,2020-03-25,4096882.6515130587,13206.565692136679,9338.73080145178
+19,-2,2020-03-26,4094989.341274572,14156.549809756034,10282.056922318685
+20,-1,2020-03-27,4092960.777993734,15173.930961326847,11293.239051586976
+21,0,2020-03-28,4090787.5061689056,16263.350574631113,12377.091263110318
+22,1,2020-03-29,4088459.4400649117,17429.7487804379,13538.759161298258
+23,2,2020-03-30,4085965.826313857,18678.380475746635,14783.74121704382
+24,3,2020-03-31,4083295.205000314,20014.83175530743,16117.91125102572
+25,4,2020-04-01,4080435.369285664,21445.036630292383,17547.542090690535
+26,5,2020-04-02,4077373.323651536,22975.293933685563,19079.330421425704
+27,6,2020-04-03,4074095.240871537,24612.284289849962,20720.422845260386
+28,7,2020-04-04,4070586.417854437,26363.087000531392,22478.443151678235
+29,8,2020-04-05,4066831.2305410947,28235.196670979705,24361.52079457334
+30,9,2020-04-06,4062813.0880822493,30236.539367611294,26378.320556786166
+31,10,2020-04-07,4058514.386575474,32375.48806241518,28538.073368758407
+32,11,2020-04-08,4053916.4626974436,34660.87707884423,30850.60823035949
+33,12,2020-04-09,4048999.547633093,37102.015208991535,33326.38516456265
+34,13,2020-04-10,4043742.721776356,39708.69712222972,35976.52910806205
+35,14,2020-04-11,4038123.870758796,42491.212631058625,38812.864616792736
+36,15,2020-04-12,4032119.6434527263,45460.35332062354,41847.95123329692
+37,16,2020-04-13,4025705.412694708,48627.41598431241,45095.11932762717
+38,17,2020-04-14,4018855.239583742,52004.20223925578,48568.50618364948
+39,18,2020-04-15,4011541.8423258928,55603.01362287314,52283.09205788205
+40,19,2020-04-16,4003736.5707230433,59436.641395516715,56254.73588808727
+41,20,2020-04-17,3995409.3875374217,63518.350195743944,60500.210273481316
+42,21,2020-04-18,3986528.8581040385,67861.85461514615,65037.23528746304
+43,22,2020-04-19,3977062.1497087833,72481.28768074761,69884.51061711632
+44,23,2020-04-20,3966975.042398156,77391.16015703541,75061.74545145544
+45,24,2020-04-21,3956231.9530344526,82606.30950952297,80589.68546267228
+46,25,2020-04-22,3944795.9745538537,88141.8373108698,86490.13614192391
+47,26,2020-04-23,3932628.932519363,94013.03382315526,92785.98166412888
+48,27,2020-04-24,3919691.461179992,100235.28846087282,99501.19836578285
+49,28,2020-04-25,3905943.1013450683,106823.98483430484,106660.86182727374
+50,29,2020-04-26,3891342.4224496745,113794.37909867684,114291.14645829551
+51,30,2020-04-27,3875847.171214572,121161.46039816034,122419.31639391532
+52,31,2020-04-28,3859414.449280934,128939.79230335788,131073.70642235532
+53,32,2020-04-29,3842000.922114928,137143.33430483865,140283.6915868809
+54,33,2020-04-30,3823563.0613170564,145785.24265236425,150079.6440372265
+55,34,2020-05-01,3804057.422223027,154877.65012836803,160492.87565525252
+56,35,2020-05-02,3783440.9583332706,164431.42472324037,171555.56495013591
+57,36,2020-05-03,3761671.373644381,174455.90764618458,183300.66671608164
+58,37,2020-05-04,3738707.5133658983,184958.63166422554,195761.8029765234
+59,38,2020-05-05,3714509.792780779,195945.02141618612,208973.1338096824
+60,39,2020-05-06,3689040.6631412944,207418.0780973715,222969.2067679814
+61,40,2020-05-07,3662265.1124826344,219378.051749076,237784.78377493648
+62,41,2020-05-08,3634151.198088472,231822.10530401886,253454.6446141562
+63,42,2020-05-09,3604670.60606805,244743.97551701157,270013.3664215862
+64,43,2020-05-10,3573799.232123452,258133.63692467994,287495.07895851554
+65,44,2020-05-11,3541517.776129012,271976.975995928,305933.1958817069
+66,45,2020-05-12,3507812.3416533894,286255.48361469957,325360.122738559
+67,46,2020-05-13,3472675.030080854,300945.97492904135,345806.94299675175
+68,47,2020-05-14,3436104.517594637,316020.3463488983,367303.08406311186
+69,48,2020-05-15,3398106.6020440436,331445.3800174283,389875.9659451761
+70,49,2020-05-16,3358694.7057082104,347182.6063520159,413550.63594642084
+71,50,2020-05-17,3317890.3192763305,363188.235187324,438349.3935429935
+72,51,2020-05-18,3275723.3720701784,379413.1655943809,464291.410342088
+73,52,2020-05-19,3232232.5137173394,395803.08354762127,491392.35074168665
+74,53,2020-05-20,3187465.293207952,412298.6552321782,519663.99956651666
+75,54,2020-05-21,3141478.2225812753,428835.8219136996,549113.9035116724
+76,55,2020-05-22,3094336.7144128173,445346.19994546473,579745.0336483652
+77,56,2020-05-23,3046114.8847997724,461757.58670526254,611555.4765016128
+78,57,2020-05-24,2996895.2166285478,477994.57011182496,644538.1612662743
+79,58,2020-05-25,2946768.081472799,493979.23597387184,678680.6305599761
+80,59,2020-05-26,2895831.1223963723,509631.96390930796,713964.861700967
+81,60,2020-05-27,2844188.5040726494,524872.2990966522,750367.1448373462
diff --git a/tests/by_date_first_hospitalized/settings.cfg b/tests/by_date_first_hospitalized/settings.cfg
new file mode 100644
index 0000000..7734509
--- /dev/null
+++ b/tests/by_date_first_hospitalized/settings.cfg
@@ -0,0 +1,13 @@
+--current-hospitalized 14
+--date-first-hospitalized 2020-03-07
+--hospitalized-los 7
+--hospitalized-rate 0.025
+--icu-los 9
+--icu-rate 0.0075
+--infectious-days 14
+--market_share 0.15
+--n-days 60
+--population 4119405
+--relative-contact-rate 0.3
+--ventilated-los 10
+--ventilated-rate 0.005
diff --git a/tests/by_doubling_time/2020-03-28_projected_admits.csv b/tests/by_doubling_time/2020-03-28_projected_admits.csv
new file mode 100644
index 0000000..d872896
--- /dev/null
+++ b/tests/by_doubling_time/2020-03-28_projected_admits.csv
@@ -0,0 +1,66 @@
+,day,date,hospitalized,icu,ventilated
+0,-4,2020-03-24,,,
+1,-3,2020-03-25,2.5542297270266676,0.7662689181079996,0.5108459454053333
+2,-2,2020-03-26,2.8373214956844457,0.8511964487053332,0.5674642991368888
+3,-1,2020-03-27,3.1516454732157406,0.9454936419647222,0.6303290946431486
+4,0,2020-03-28,3.5006137311112084,1.0501841193333625,0.7001227462222417
+5,1,2020-03-29,3.88800322148769,1.1664009664463082,0.7776006442975385
+6,2,2020-03-30,4.31799294303201,1.2953978829096027,0.8635985886064015
+7,3,2020-03-31,4.795204451744276,1.4385613355232838,0.959040890348855
+8,4,2020-04-01,5.3247459090950855,1.5974237727285256,1.064949181819017
+9,5,2020-04-02,5.912259841750263,1.773677952525075,1.1824519683500512
+10,6,2020-04-03,6.563974757964566,1.9691924273893697,1.3127949515929132
+11,7,2020-04-04,7.286760723218194,2.186028216965461,1.4573521446436413
+12,8,2020-04-05,8.088188938167391,2.426456681450219,1.6176377876334769
+13,9,2020-04-06,8.976595281287373,2.6929785843862106,1.7953190562574761
+14,10,2020-04-07,9.961147671659262,2.9883443014977793,1.9922295343318517
+15,11,2020-04-08,11.051916968220098,3.3155750904660266,2.2103833936440225
+16,12,2020-04-09,12.259950943431463,3.677985283029436,2.4519901886862883
+17,13,2020-04-10,13.597350643582402,4.079205193074728,2.719470128716477
+18,14,2020-04-11,15.077348165468635,4.523204449640588,3.0154696330937334
+19,15,2020-04-12,16.714384529423825,5.014315358827147,3.3428769058847614
+20,16,2020-04-13,18.524185899929222,5.557255769978767,3.704837179985848
+21,17,2020-04-14,20.523835884642892,6.157150765392856,4.104767176928576
+22,18,2020-04-15,22.731841017472107,6.819552305241636,4.546368203494424
+23,19,2020-04-16,25.16818578810276,7.550455736430834,5.033637157620554
+24,20,2020-04-17,27.854372707073594,8.356311812122058,5.570874541414717
+25,21,2020-04-18,30.813441882360053,9.244032564708036,6.162688376472005
+26,22,2020-04-19,34.06996342529601,10.220989027588786,6.813992685059198
+27,23,2020-04-20,37.64999470248415,11.294998410745265,7.529998940496839
+28,24,2020-04-21,41.5809930189904,12.4742979056971,8.316198603798071
+29,25,2020-04-22,45.891672784950515,13.767501835485163,9.178334556990094
+30,26,2020-04-23,50.611794632607655,15.183538389782257,10.122358926521528
+31,27,2020-04-24,55.77187239202874,16.731561717608656,11.15437447840577
+32,28,2020-04-25,61.40278241639521,18.420834724918535,12.280556483279028
+33,29,2020-04-26,67.5352586324093,20.26057758972283,13.507051726481876
+34,30,2020-04-27,74.19925609320217,22.25977682796062,14.839851218640405
+35,31,2020-04-28,81.42316600764082,24.426949802292256,16.28463320152818
+36,32,2020-04-29,89.23286655550487,26.76985996665144,17.84657331110097
+37,33,2020-04-30,97.65059668266485,29.29517900479948,19.530119336533005
+38,34,2020-05-01,106.69364496902926,32.0080934907088,21.33872899380583
+39,35,2020-05-02,116.37285306930698,34.91185592079211,23.274570613861385
+40,36,2020-05-03,126.69094362249552,38.00728308674866,25.338188724499105
+41,37,2020-05-04,137.64069630669042,41.29220889200701,27.528139261338026
+42,38,2020-05-05,149.2030130957985,44.76090392873954,29.84060261915971
+43,39,2020-05-06,161.34493465979835,48.403480397939575,32.26898693195972
+44,40,2020-05-07,174.01769368515147,52.20530810554544,34.80353873703024
+45,41,2020-05-08,187.15491649835917,56.14647494950782,37.43098329967194
+46,42,2020-05-09,200.67110980336838,60.20133294101049,40.13422196067364
+47,43,2020-05-10,214.46059177742654,64.33817753322796,42.89211835548531
+48,44,2020-05-11,228.39704254298385,68.51911276289513,45.679408508596794
+49,45,2020-05-12,242.3338538065277,72.70015614195825,48.4667707613055
+50,46,2020-05-13,256.1054466215901,76.83163398647696,51.22108932431797
+51,47,2020-05-14,269.52969557485676,80.85890867245723,53.90593911497149
+52,48,2020-05-15,282.41154427939273,84.72346328381764,56.4823088558785
+53,49,2020-05-16,294.54782028530826,88.36434608559262,58.909564057061516
+54,50,2020-05-17,305.73316012459054,91.71994803737675,61.14663202491806
+55,51,2020-05-18,315.7668439892359,94.73005319677077,63.15336879784718
+56,52,2020-05-19,324.46022546863696,97.33806764059136,64.89204509372746
+57,53,2020-05-20,331.64433922865464,99.49330176859644,66.32886784573111
+58,54,2020-05-21,337.1771945508717,101.15315836526133,67.43543891017407
+59,55,2020-05-22,340.9502303433592,102.28506910300803,68.19004606867179
+60,56,2020-05-23,342.89342858806685,102.86802857641965,68.57868571761355
+61,57,2020-05-24,342.97866224276277,102.89359867282906,68.59573244855255
+62,58,2020-05-25,341.22098555910907,102.36629566773263,68.24419711182168
+63,59,2020-05-26,337.67774575921976,101.30332372776593,67.53554915184418
+64,60,2020-05-27,,,
diff --git a/tests/by_doubling_time/2020-03-28_projected_census.csv b/tests/by_doubling_time/2020-03-28_projected_census.csv
new file mode 100644
index 0000000..789baf0
--- /dev/null
+++ b/tests/by_doubling_time/2020-03-28_projected_census.csv
@@ -0,0 +1,66 @@
+,day,date,hospitalized,icu,ventilated
+0,-4,2020-03-24,,,
+1,-3,2020-03-25,3.0,1.0,1.0
+2,-2,2020-03-26,6.0,2.0,2.0
+3,-1,2020-03-27,9.0,3.0,2.0
+4,0,2020-03-28,13.0,4.0,3.0
+5,1,2020-03-29,16.0,5.0,4.0
+6,2,2020-03-30,21.0,7.0,5.0
+7,3,2020-03-31,26.0,8.0,6.0
+8,4,2020-04-01,28.0,10.0,7.0
+9,5,2020-04-02,31.0,11.0,8.0
+10,6,2020-04-03,35.0,13.0,9.0
+11,7,2020-04-04,39.0,14.0,10.0
+12,8,2020-04-05,43.0,15.0,11.0
+13,9,2020-04-06,47.0,17.0,12.0
+14,10,2020-04-07,53.0,19.0,14.0
+15,11,2020-04-08,58.0,21.0,15.0
+16,12,2020-04-09,65.0,23.0,17.0
+17,13,2020-04-10,72.0,26.0,18.0
+18,14,2020-04-11,80.0,28.0,20.0
+19,15,2020-04-12,88.0,31.0,22.0
+20,16,2020-04-13,98.0,35.0,25.0
+21,17,2020-04-14,108.0,39.0,27.0
+22,18,2020-04-15,120.0,43.0,30.0
+23,19,2020-04-16,133.0,47.0,34.0
+24,20,2020-04-17,147.0,52.0,37.0
+25,21,2020-04-18,163.0,58.0,41.0
+26,22,2020-04-19,180.0,64.0,46.0
+27,23,2020-04-20,199.0,71.0,50.0
+28,24,2020-04-21,220.0,78.0,56.0
+29,25,2020-04-22,244.0,86.0,61.0
+30,26,2020-04-23,269.0,95.0,68.0
+31,27,2020-04-24,297.0,105.0,75.0
+32,28,2020-04-25,327.0,116.0,83.0
+33,29,2020-04-26,361.0,128.0,91.0
+34,30,2020-04-27,397.0,141.0,100.0
+35,31,2020-04-28,437.0,155.0,111.0
+36,32,2020-04-29,481.0,171.0,122.0
+37,33,2020-04-30,528.0,188.0,134.0
+38,34,2020-05-01,579.0,206.0,147.0
+39,35,2020-05-02,634.0,226.0,161.0
+40,36,2020-05-03,693.0,247.0,176.0
+41,37,2020-05-04,756.0,270.0,192.0
+42,38,2020-05-05,824.0,294.0,210.0
+43,39,2020-05-06,896.0,320.0,229.0
+44,40,2020-05-07,972.0,348.0,249.0
+45,41,2020-05-08,1053.0,378.0,270.0
+46,42,2020-05-09,1137.0,408.0,292.0
+47,43,2020-05-10,1225.0,441.0,315.0
+48,44,2020-05-11,1316.0,474.0,340.0
+49,45,2020-05-12,1409.0,509.0,365.0
+50,46,2020-05-13,1504.0,545.0,391.0
+51,47,2020-05-14,1599.0,581.0,417.0
+52,48,2020-05-15,1694.0,617.0,444.0
+53,49,2020-05-16,1788.0,653.0,470.0
+54,50,2020-05-17,1880.0,689.0,497.0
+55,51,2020-05-18,1967.0,723.0,
+56,52,2020-05-19,2049.0,,
+57,53,2020-05-20,2125.0,,
+58,54,2020-05-21,,,
+59,55,2020-05-22,,,
+60,56,2020-05-23,,,
+61,57,2020-05-24,,,
+62,58,2020-05-25,,,
+63,59,2020-05-26,,,
+64,60,2020-05-27,,,
diff --git a/tests/by_doubling_time/2020-03-28_sim_sir_w_date.csv b/tests/by_doubling_time/2020-03-28_sim_sir_w_date.csv
new file mode 100644
index 0000000..2d8aeab
--- /dev/null
+++ b/tests/by_doubling_time/2020-03-28_sim_sir_w_date.csv
@@ -0,0 +1,66 @@
+,day,date,susceptible,infected,recovered
+0,-4,2020-03-24,4115671.6666666665,3733.3333333333335,0.0
+1,-3,2020-03-25,4114990.5387394596,4147.794593873778,266.6666666666667
+2,-2,2020-03-26,4114233.919673944,4608.142616970075,562.9377090862222
+3,-1,2020-03-27,4113393.4808810866,5119.428365758315,892.0907531555133
+4,0,2020-03-28,4112459.983886123,5687.251906024281,1257.7642078525355
+5,1,2020-03-29,4111423.18302706,6317.820486085931,1663.9964868542697
+6,2,2020-03-30,4110271.7182422513,7018.012379031186,2115.2693787175504
+7,3,2020-03-31,4108992.9970551194,7795.446967660766,2616.555977219778
+8,4,2020-04-01,4107573.064812694,8658.561569538924,3173.3736177669757
+9,5,2020-04-02,4105996.4621882276,9616.695510467165,3791.8423013054703
+10,6,2020-04-03,4104246.068919437,10680.181957081491,4478.749123481696
+11,7,2020-04-04,4102302.932726579,11860.448010148142,5241.619263273231
+12,8,2020-04-05,4100146.082343067,13170.123535791723,6088.794121140954
+13,9,2020-04-06,4097752.323601391,14623.159167768945,7029.517230840365
+14,10,2020-04-07,4095096.017555615,16234.953844418396,8074.028599966718
+15,11,2020-04-08,4092148.8396974225,18022.492142294825,9233.668160282316
+16,12,2020-04-09,4088879.5194458417,20004.49152656977,10520.98902758909
+17,13,2020-04-10,4085253.559274219,22201.559446293908,11949.881279486928
+18,14,2020-04-11,4081232.933096761,24636.35994901693,13535.706954222207
+19,15,2020-04-12,4076775.7638889146,27333.78916050493,15295.44695058056
+20,16,2020-04-13,4071835.980982267,30321.15855568809,17247.860462045195
+21,17,2020-04-14,4066362.9580796952,33628.38441856752,19413.6575017372
+22,18,2020-04-15,4060301.1338083693,37288.18123142431,21815.68496020631
+23,19,2020-04-16,4053589.6175982086,41336.25592505474,24479.126476736616
+24,20,2020-04-17,4046161.7848763224,45811.49893800854,27431.7161856691
+25,21,2020-04-18,4037944.8670410262,50756.16684916109,30703.966109812565
+26,22,2020-04-19,4028859.5434609475,56216.04994001422,34329.40659903836
+27,23,2020-04-20,4018819.5448736185,62240.61638877091,38344.83873761081
+28,24,2020-04-21,4007731.2800685544,68883.12288035135,42790.597051094446
+29,25,2020-04-22,3995493.5006592344,76200.67922678925,47710.82011397668
+30,26,2020-04-23,3981997.0220905384,84254.25213642826,53153.72577303305
+31,27,2020-04-24,3967124.5227859975,93108.59057408152,59171.88663992078
+32,28,2020-04-25,3950750.447474959,102832.05227268583,65822.50025235518
+33,29,2020-04-26,3932741.045172983,113496.30798375554,73167.64684326132
+34,30,2020-04-27,3912954.5768814622,125175.89713357927,81274.52598495815
+35,31,2020-04-28,3891241.732612759,137947.6058927421,90215.66149449954
+36,32,2020-04-29,3867446.30153129,151889.63655329993,100069.06191540968
+37,33,2020-04-30,3841406.1424159138,167080.5359148701,110918.32166921685
+38,34,2020-05-01,3812954.503757505,183597.8505793586,122852.6456631361
+39,35,2020-05-02,3781921.7429390233,201516.47921360054,135966.777847376
+40,36,2020-05-03,3748137.491306358,220906.69661672314,150360.8120769189
+41,37,2020-05-04,3711433.305624574,241831.83254016982,166139.86183525628
+42,38,2020-05-05,3671645.8354656943,264345.6003747516,183413.5641595541
+43,39,2020-05-06,3628620.519556415,288489.0876858347,202295.39275775064
+44,40,2020-05-07,3582215.8012403743,314287.44259574404,222901.75616388168
+45,41,2020-05-08,3532307.8235074785,341746.31728608673,245350.85920643483
+46,42,2020-05-09,3478795.527559913,370848.16199893114,269761.3104411553
+47,43,2020-05-10,3421606.0364192664,401548.49871108314,296250.4648696504
+48,44,2020-05-11,3360700.158407804,433772.3411003253,324932.50049187057
+49,45,2020-05-12,3296077.79739273,467410.963465376,355916.2391418938
+50,46,2020-05-13,3227783.011626973,502319.25184074935,389302.7365322778
+51,47,2020-05-14,3155908.4261403442,538313.8907673245,425182.6830923313
+52,48,2020-05-15,3080598.680999173,575172.6437108299,463633.6752899973
+53,49,2020-05-16,3002052.5955897574,612634.9688551861,504717.43555505655
+54,50,2020-05-17,2920523.752889867,650404.1709225634,548477.0761875699
+55,51,2020-05-18,2836319.2611594032,688151.2218728436,594934.5169677528
+56,52,2020-05-19,2749796.534367767,725520.2899592768,644088.1756729559
+57,53,2020-05-20,2661358.043906793,762135.9025660174,695911.0535271901
+58,54,2020-05-21,2571444.1253598933,797611.5423582009,750349.3322819056
+59,55,2020-05-22,2480524.0639349977,831559.3507575109,807321.5853074914
+60,56,2020-05-23,2389085.816311514,863600.5018983157,866718.681790171
+61,57,2020-05-24,2297624.839713443,893375.7283607922,928404.4319257648
+62,58,2020-05-25,2206632.5768976817,920555.4391507832,992216.9839515359
+63,59,2020-05-26,2116585.178028555,944848.8780805673,1057970.9438908773
+64,60,2020-05-27,2027933.0223149187,966011.828217021,1125460.1494680608
diff --git a/tests/by_doubling_time/settings.cfg b/tests/by_doubling_time/settings.cfg
new file mode 100644
index 0000000..a852855
--- /dev/null
+++ b/tests/by_doubling_time/settings.cfg
@@ -0,0 +1,13 @@
+--current-hospitalized 14
+--doubling-time 4.0
+--hospitalized-los 7
+--hospitalized-rate 0.025
+--icu-los 9
+--icu-rate 0.0075
+--infectious-days 14
+--market_share 0.15
+--n-days 60
+--population 4119405
+--relative-contact-rate 0.3
+--ventilated-los 10
+--ventilated-rate 0.005
diff --git a/tests/test_app.py b/tests/test_app.py
index 696663e..af3720f 100644
--- a/tests/test_app.py
+++ b/tests/test_app.py
@@ -1,23 +1,58 @@
 """Tests."""
 
+from copy import copy
 from math import ceil  # type: ignore
-import datetime  # type: ignore
+from datetime import date, datetime  # type: ignore
 import pytest  # type: ignore
 import pandas as pd  # type: ignore
 import numpy as np  # type: ignore
 import altair as alt  # type: ignore
 
-from src.penn_chime.charts import new_admissions_chart, admitted_patients_chart, chart_descriptions
-from src.penn_chime.models import SimSirModel, sir, sim_sir_df, build_admits_df, daily_growth_helper
-from src.penn_chime.parameters import Parameters
+from src.penn_chime.charts import (
+    build_admits_chart,
+    build_census_chart,
+    build_descriptions,
+)
+from src.penn_chime.models import (
+    SimSirModel as Model,
+    sir,
+    sim_sir_df,
+    build_admits_df,
+    get_growth_rate,
+)
+from src.penn_chime.parameters import (
+    Parameters,
+    RateLos,
+    Regions,
+)
 from src.penn_chime.presentation import display_header
-from src.penn_chime.settings import DEFAULTS
-from src.penn_chime.defaults import RateLos
+
+EPSILON = 1.e-7
+
+# The defaults in settings will change and break the tests
+DEFAULTS = Parameters(
+    region=Regions(
+        delaware=564696,
+        chester=519293,
+        montgomery=826075,
+        bucks=628341,
+        philly=1581000,
+    ),
+    current_date=datetime(year=2020, month=3, day=28),
+    current_hospitalized=14,
+    date_first_hospitalized=datetime(year=2020, month=3, day=7),
+    doubling_time=4.0,
+    n_days=60,
+    market_share=0.15,
+    relative_contact_rate=0.3,
+    hospitalized=RateLos(0.025, 7),
+    icu=RateLos(0.0075, 9),
+    ventilated=RateLos(0.005, 10),
+)
 
 PARAM = Parameters(
     current_hospitalized=100,
     doubling_time=6.0,
-    known_infected=5000,
     market_share=0.05,
     relative_contact_rate=0.15,
     population=500000,
@@ -30,7 +65,6 @@ PARAM = Parameters(
 HALVING_PARAM = Parameters(
     current_hospitalized=100,
     doubling_time=6.0,
-    known_infected=5000,
     market_share=0.05,
     relative_contact_rate=0.7,
     population=500000,
@@ -40,8 +74,8 @@ HALVING_PARAM = Parameters(
     n_days=60,
 )
 
-MODEL = SimSirModel(PARAM)
-HALVING_MODEL = SimSirModel(HALVING_PARAM)
+MODEL = Model(copy(PARAM))
+HALVING_MODEL = Model(copy(HALVING_PARAM))
 
 
 # set up
@@ -97,7 +131,7 @@ def test_mitigation_statement():
     header_test_helper(expected_halving, HALVING_MODEL, HALVING_PARAM)
 
 
-def test_daily_growth_presentation():
+def test_growth_rate():
     initial_growth = "and daily growth rate of **12.25%**."
     mitigated_growth = "and daily growth rate of **9.34%**."
     mitigated_halving = "and daily growth rate of **-1.33%**."
@@ -182,7 +216,15 @@ def test_sim_sir():
     """
     Rounding to move fast past decimal place issues
     """
-    raw_df = sim_sir_df(5, 6, 7, 0.1, 0.1, 40)
+    raw_df = sim_sir_df(
+        5, # s
+        6, # i
+        7, # r
+        0.1, # gamma
+        0, # i_day
+        0.1, # beta1
+        40, # n_days1
+    )
 
     first = raw_df.iloc[0, :]
     last = raw_df.iloc[-1, :]
@@ -197,118 +239,139 @@ def test_sim_sir():
     assert isinstance(raw_df, pd.DataFrame)
 
 
-def test_new_admissions_chart():
-    projection_admits = pd.read_csv("tests/projection_admits.csv")
-    chart = new_admissions_chart(alt, projection_admits, PARAM)
-    assert isinstance(chart, alt.Chart)
-    # COMMENTING OUT because chart tests oughtn't bother with numeric info anyway
-    # assert chart.data.iloc[1].hospitalized < 1 
-    assert round(chart.data.iloc[40].icu, 0) == 25
+def test_admits_chart():
+    admits_df = pd.read_csv("tests/by_doubling_time/2020-03-28_projected_admits.csv")
+    chart = build_admits_chart(alt=alt, admits_df=admits_df)
+    assert isinstance(chart, (alt.Chart, alt.LayerChart))
+    assert round(chart.data.iloc[40].icu, 0) == 38
 
     # test fx call with no params
     with pytest.raises(TypeError):
-        new_admissions_chart()
-    
-    # unnecessary
-    # empty_chart = new_admissions_chart(alt, pd.DataFrame(), PARAM)
-    # assert empty_chart.data.empty
+        build_admits_chart()
 
 
-def test_admitted_patients_chart():
-    census_df = pd.read_csv("tests/census_df.csv")
-    chart = admitted_patients_chart(alt, census_df, PARAM)
-    assert isinstance(chart, alt.Chart)
-    assert chart.data.iloc[1].hospitalized == 1
-    assert chart.data.iloc[49].ventilated == 203
+def test_census_chart():
+    census_df = pd.read_csv("tests/by_doubling_time/2020-03-28_projected_census.csv")
+    chart = build_census_chart(alt=alt, census_df=census_df)
+    assert isinstance(chart, (alt.Chart, alt.LayerChart))
+    assert chart.data.iloc[1].hospitalized == 3
+    assert chart.data.iloc[49].ventilated == 365
 
     # test fx call with no params
     with pytest.raises(TypeError):
-        admitted_patients_chart()
+        build_census_chart()
 
-    empty_chart = admitted_patients_chart(alt, pd.DataFrame(), PARAM)
-    assert empty_chart.data.empty
 
-
-def test_model(model=MODEL, param=PARAM):
+def test_model():
     # test the Model
+    param = copy(PARAM)
+    model = Model(param)
 
-    assert model.infected == 40000.0
+    assert round(model.infected, 0) == 45810.0
     assert isinstance(model.infected, float)  # based off note in models.py
 
     # test the class-calculated attributes
-    assert model.detection_probability == 0.125
+    # we're talking about getting rid of detection probability
+    # assert model.detection_probability == 0.125
     assert model.intrinsic_growth_rate == 0.12246204830937302
-    assert model.beta == 3.582761451679408e-07
+    assert abs(model.beta - 4.21501347256401e-07) < EPSILON
     assert model.r_t == 2.307298374881539
     assert model.r_naught == 2.7144686763312222
     assert model.doubling_time_t == 7.764405988534983
 
-    # test the things n_days creates, which in turn tests sim_sir, sir, and get_dispositions
-    assert len(model.raw_df) == param.n_days + 1 == 61
 
+def test_model_raw_start():
+    param = copy(PARAM)
+    model = Model(param)
     raw_df = model.raw_df
+
+    # test the things n_days creates, which in turn tests sim_sir, sir, and get_dispositions
+
+    # print('n_days: %s; i_day: %s' % (param.n_days, model.i_day))
+    assert len(raw_df) == (len(np.arange(-model.i_day, param.n_days + 1))) == 104
+
     first = raw_df.iloc[0, :]
     second = raw_df.iloc[1, :]
-    last = raw_df.iloc[-1, :]
 
     assert first.susceptible + first.infected + first.recovered == param.population
-    assert last.susceptible + last.infected + last.recovered == param.population
+    assert first.susceptible == 499600.0
+    assert round(second.infected, 0) == 449.0
+    assert list(model.dispositions_df.iloc[0, :]) == [-43, date(year=2020, month=2, day=15), 1.0, 0.4, 0.2]
+    assert round(raw_df.recovered[30], 0) == 7083.0
+
+    d, dt, s, i, r = list(model.dispositions_df.iloc[60, :])
+    assert dt == date(year=2020, month=4, day=15)
+    assert [round(v, 0) for v in (d, s, i, r)] == [17, 549.0, 220.0, 110.0]
+
+
+def test_model_raw_end():
+    param = copy(PARAM)
+    model = Model(param)
+    raw_df = model.raw_df
 
-    assert first.susceptible == 460000.0
-    assert round(second.infected, 0) == 43735
+    last = raw_df.iloc[-1, :]
+    assert last.susceptible + last.infected + last.recovered == param.population
+    assert round(last.susceptible, 0) == 83391.0
 
-    assert round(last.susceptible, 0) == 59497
-    assert round(raw_df.recovered[30], 0) == 216711
 
-    assert list(model.dispositions_df.iloc[0, :]) == [0, 100.0, 40.0, 20.0]
-    assert [round(i, 0) for i in model.dispositions_df.iloc[60, :]] == [60, 1101.0, 441.0, 220.0]
+def test_model_cumulative_census():
+    # test that census is being properly calculated
+    param = copy(PARAM)
+    model = Model(param)
 
-    # test that admissions are being properly calculated
-    cumulative_admits = model.admits_df.cumsum()
-    diff = cumulative_admits.hospitalized[1:-1] - (
-        0.05 * 0.05 * (raw_df.infected[1:-1] + raw_df.recovered[1:-1]) - 100
+    raw_df = model.raw_df
+    admits_df = model.admits_df
+    df = pd.DataFrame({
+        "hospitalized": admits_df.hospitalized,
+        "icu": admits_df.icu,
+        "ventilated": admits_df.ventilated
+    })
+    admits = df.cumsum()
+
+    # TODO: is 1.0 for ceil function?
+    diff = admits.hospitalized[1:-1] - (
+        0.05 * 0.05 * (raw_df.infected[1:-1] + raw_df.recovered[1:-1]) - 1.0
     )
     assert (diff.abs() < 0.1).all()
 
 
-def test_daily_growth_helper():
-    assert np.round(daily_growth_helper(5), decimals=4) == 14.8698
-    assert np.round(daily_growth_helper(0), decimals=4) == 0.0
-    assert np.round(daily_growth_helper(-4), decimals=4) == -15.9104
+def test_growth_rate():
+    assert np.round(get_growth_rate(5) * 100.0, decimals=4) == 14.8698
+    assert np.round(get_growth_rate(0) * 100.0, decimals=4) == 0.0
+    assert np.round(get_growth_rate(-4) * 100.0, decimals=4) == -15.9104
 
 
-def test_chart_descriptions(p=PARAM):
-    # new admissions chart
-    projection_admits = pd.read_csv('tests/projection_admits.csv')
-    chart = new_admissions_chart(alt, projection_admits, p)
-    description = chart_descriptions(chart, p.labels)
+def test_build_descriptions():
+    param = copy(PARAM)
 
-    hosp, icu, vent, asterisk = description.split("\n\n")  # break out the description into lines
+    admits_file = 'tests/by_doubling_time/2020-03-28_projected_admits.csv'
+    census_file = 'tests/by_doubling_time/2020-03-28_projected_census.csv'
+
+    admits_df = pd.read_csv(admits_file, parse_dates=['date'])
+    chart = build_admits_chart(alt=alt, admits_df=admits_df)
+    description = build_descriptions(chart=chart, labels=param.labels)
+
+    hosp, icu, vent = description.split("\n\n")  # break out the description into lines
 
     max_hosp = chart.data['hospitalized'].max()
     assert str(ceil(max_hosp)) in hosp
 
-    max_icu_ix = chart.data['icu'].idxmax()
-    assert max_icu_ix + 1 == len(chart.data)
-    assert "*" in icu
+    # TODO add test for asterisk
 
-    # test asterisk
-    param = PARAM
+    # test no asterisk
     param.n_days = 600
 
-    projection_admits = pd.read_csv('tests/projection_admits.csv')
-    # projection_admits = projection_admits.rename(columns={'hospitalized': 'Hospitalized', 'icu': 'ICU', 'ventilated': 'Ventilated'})
-    chart = new_admissions_chart(alt, projection_admits, p)
-    description = chart_descriptions(chart, p.labels)
+    admits_df = pd.read_csv(admits_file, parse_dates=['date'])
+    chart = build_admits_chart(alt=alt, admits_df=admits_df)
+    description = build_descriptions(chart=chart, labels=param.labels)
     assert "*" not in description
 
+
     # census chart
-    census_df = pd.read_csv('tests/census_df.csv')
-    # census_df = census_df.rename(columns={'hospitalized': 'Hospitalized', 'icu': 'ICU', 'ventilated': 'Ventilated'})
-    PARAM.as_date = True
-    chart = admitted_patients_chart(alt, census_df, p)
-    description = chart_descriptions(chart, p.labels)
+    census_df = pd.read_csv(census_file, parse_dates=['date'])
+    chart = build_census_chart(alt=alt, census_df=census_df)
+    description = build_descriptions(chart=chart, labels=param.labels)
 
     assert str(ceil(chart.data['ventilated'].max())) in description
     assert str(chart.data['icu'].idxmax()) not in description
-    assert datetime.datetime.strftime(chart.data.iloc[chart.data['icu'].idxmax()].date, '%b %d') in description
+    assert datetime.strftime(chart.data.iloc[chart.data['icu'].idxmax()].date, '%b %d') in description