From 6352b055aae4e8562121aa1b18abe57c783bb3db Mon Sep 17 00:00:00 2001 From: yaniv-kwb Date: Wed, 1 Jul 2026 14:06:08 +0200 Subject: [PATCH 1/3] failure-events.rst --- .gitignore | 3 +- docs/source/failure-events.rst | 210 +++++++++++++++++++++++++++++++++ docs/source/index.rst | 2 + 3 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 docs/source/failure-events.rst diff --git a/.gitignore b/.gitignore index cd37399..9134db1 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ dump* prod-migrations/ *.tar .vscode -test.zip \ No newline at end of file +test.zip +.vs diff --git a/docs/source/failure-events.rst b/docs/source/failure-events.rst new file mode 100644 index 0000000..9a3d522 --- /dev/null +++ b/docs/source/failure-events.rst @@ -0,0 +1,210 @@ +Integration of Failure Events into the QMRA Framework +====================================================== + +Summary +------- + +This feature adds two new treatment-level inputs to the UI and to the risk +calculation pipeline so that treatment failure events can be represented in the +QMRA model. + +The current treatment table already stores minimum and maximum log-removal +values for bacteria, viruses, and protozoa. The new feature extends each +treatment with two additional columns: + +* ``failure_duration_minutes``: average duration of a failure event in minutes. +* ``failure_frequency_days_per_year``: number of failure-event days per year. + +Default values +-------------- + +To keep existing assessments stable, the new fields should default to: + +* failure duration: ``30`` minutes +* failure frequency: ``0`` days per year + +These defaults mean that existing risk assessments continue to behave exactly +as they do today unless the user explicitly enters failure-event data. + +Expected user-facing behavior +----------------------------- + +Each treatment row in the configurator should show the two new columns alongside +the existing minimum and maximum LRV inputs. + +Validation expectations: + +* failure duration must be an integer between ``1`` and ``1440`` when used + explicitly +* failure frequency must be a numeric value between ``0`` and ``365`` +* default values should be prefilled in the UI + +Storage assumptions: + +* both values are stored directly on each ``Treatment`` record +* failure duration is stored as an integer field +* failure frequency is stored as a floating-point field to match the requested + UI range + +Calculation behavior +-------------------- + +The risk calculation needs to distinguish between: + +* normal operation days +* days affected by a failure event + +The existing LRV minimum and maximum values remain the inputs for treatment +performance. The new failure-event values are used to adjust the daily infection +risk when a treatment failure is active. + +The failure-event logic should preserve backward compatibility: + +* if ``failure_frequency_days_per_year`` is ``0``, the current calculation path + is used +* if ``failure_frequency_days_per_year`` is greater than ``0``, the assessment + must calculate a failure-day risk in addition to the normal-day risk + +The implementation should treat the duration as the active fraction of the day +affected by failure, using ``failure_duration_minutes / 1440`` as the time +share for the failure window. + +Suggested formulas +------------------ + +The current model uses the annual infection risk: + +.. math:: + + P_{inf,year} = 1 - \prod_{d=1}^{365} (1 - p_{inf,day,d}) + +and the current daily infection probability: + +.. math:: + + p_{inf,day} = 1 - e^{-\frac{c_{in} \cdot V}{10^{LRV}}} + +For failure events, the daily risk should be split into a normal part and a +failure part. + +Normal-day risk: + +.. math:: + + p_{inf,day,normal} = 1 - e^{-\frac{c_{in} \cdot V}{10^{LRV_{normal}}}} + +Failure-day risk: + +.. math:: + + p_{inf,day,failure} = 1 - e^{-\frac{c_{in} \cdot V}{10^{LRV_{failure}}}} + +Suggested mixed daily risk: + +.. math:: + + p_{inf,day,mix} = (1 - x_{fail}) \cdot p_{inf,day,normal} + x_{fail} \cdot p_{inf,day,failure} + +where: + +.. math:: + + x_{fail} = \frac{failure\_duration\_minutes}{1440} + +If the calculation needs to work on a per-year basis with explicit failure +events, the annual infection risk can be written as: + +.. math:: + + P_{inf,year} = 1 - (1 - p_{inf,day,normal})^{365-n_{fail}} \cdot (1 - p_{inf,day,failure})^{n_{fail}} + +where: + +.. math:: + + n_{fail} = failure\_frequency\_days\_per\_year + +This is the smallest change to the current model if the code keeps the same +annual-risk structure and only swaps in a failure-adjusted daily probability. + +Treatment LRV adjustment +------------------------ + +When a failure event occurs in one treatment step, the treatment chain LRV for +that step should be reduced by the failing step's LRV contribution. A suggested +representation is: + +.. math:: + + LRV_{failure} = LRV_{total} - LRV_{treatment} + +For a chain that mixes normal and failing operation during the same day, a +weighted LRV can be expressed as: + +.. math:: + + LRV_{mix} = -\log_{10}\left(\frac{x_{fail}}{10^{LRV_{failure}}} + \frac{1-x_{fail}}{10^{LRV_{normal}}}\right) + +This formula describes how the current treatment LRV is blended during failure +windows. It changes the current calculation only if the implementation chooses +to model mixed operation within a day; otherwise the code can keep the current +annual formula and only switch daily probabilities for failure days. + +Current formula impact +---------------------- + +The current annual-risk formula itself does not have to be removed. The planned +change is: + +* keep the current annual aggregation structure +* add a failure-aware branch for daily risk +* use the same annual product formula, but with ``p_{inf,day,mix}`` when + failure parameters are present + +In other words, the shape of the model stays the same, but the daily probability +input changes from a single value to a failure-aware mixture. + +Implementation scope +-------------------- + +The change is expected to touch the following areas: + +* ``qmra/risk_assessment/models.py`` + * add the two new treatment fields + * define defaults at the model level if appropriate +* ``qmra/risk_assessment/forms.py`` + * expose the two fields in ``TreatmentForm`` + * validate ranges and defaults + * keep the formset behavior aligned with the table layout +* ``qmra/risk_assessment/templates/treatments-form-fieldset.html`` + * add the two new columns to the treatment table +* ``qmra/risk_assessment/templates/treatments-form-js.html`` + * ensure dynamically added treatment rows include the new inputs +* ``qmra/risk_assessment/risk.py`` + * update the annual-risk calculation so failure days are accounted for + * decide whether the failure logic is implemented as a mixed daily + probability or as a separate failure branch per day + * keep the existing result shape stable if possible +* ``qmra/risk_assessment/tests/`` + * add coverage for default values + * add coverage for validation + * add regression tests for unchanged results when failure frequency is ``0`` + +Suggested implementation plan +----------------------------- + +1. Add the new treatment fields to the data model and create the migration. +2. Update the treatment form, formset, and templates so the UI shows the new + columns and pre-populates the defaults. +3. Extend the risk calculation to use the failure-event parameters. +4. Add tests for validation, default values, and backward compatibility. +5. Update any export or plot logic only if the result structure changes. + +Implementation decisions +------------------------ + +* The new fields are per-treatment inputs, not shared globally. +* The UI should show the defaults on every blank treatment row so users see the + expected starting values immediately. +* The initial release should keep the result presentation unchanged unless the + calculation itself requires a new summary field. diff --git a/docs/source/index.rst b/docs/source/index.rst index cd3c1f9..536b0d0 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -5,6 +5,8 @@ Welcome to QMRA's documentation! :maxdepth: 2 :caption: Contents: + failure-events + QMRA is a Django project which experiments with deploying state-of-the-art techniques for Quantitative Microbial Risk Assessment (QMRA). From d384bd860180571b2a3e17470fc14e15161835ed Mon Sep 17 00:00:00 2001 From: ma-z-am <43271536+ma-z-am@users.noreply.github.com> Date: Wed, 15 Jul 2026 09:57:33 +0200 Subject: [PATCH 2/3] Revise risk equations in failure-events.rst Updated equations for normal-day and failure-day risk calculations to include a new variable 'r'. Modified suggested mixed daily risk to reflect changes in the calculations. --- docs/source/failure-events.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/source/failure-events.rst b/docs/source/failure-events.rst index 9a3d522..aa02cb2 100644 --- a/docs/source/failure-events.rst +++ b/docs/source/failure-events.rst @@ -91,22 +91,23 @@ Normal-day risk: .. math:: - p_{inf,day,normal} = 1 - e^{-\frac{c_{in} \cdot V}{10^{LRV_{normal}}}} + p_{inf,day,normal} = 1 - e^{-\frac{r \cdot c_{in} \cdot V}{10^{LRV_{normal}}}} Failure-day risk: .. math:: - p_{inf,day,failure} = 1 - e^{-\frac{c_{in} \cdot V}{10^{LRV_{failure}}}} + p_{inf,day,failure} = 1 - e^{-\frac{r \cdot Vc_{in} \cdot V}{10^{LRV_{failure}}}} -Suggested mixed daily risk: +Suggested mixed daily LRV (only needed for best-case calculation): .. math:: - p_{inf,day,mix} = (1 - x_{fail}) \cdot p_{inf,day,normal} + x_{fail} \cdot p_{inf,day,failure} + LRV_{failure,mix} = -log(\frac{x_{fail}}{10^{LRV_{failure,max}}}+\frac{1-x_{fail}} {10^{LRV_{max}}}) where: +############# Equations need to be checked: .. math:: x_{fail} = \frac{failure\_duration\_minutes}{1440} @@ -126,6 +127,7 @@ where: This is the smallest change to the current model if the code keeps the same annual-risk structure and only swaps in a failure-adjusted daily probability. +#################### Treatment LRV adjustment ------------------------ From 1d149f960741b29cf149d6482a50bdb4e5c5d3d4 Mon Sep 17 00:00:00 2001 From: yaniv-kwb Date: Thu, 16 Jul 2026 12:33:22 +0200 Subject: [PATCH 3/3] feat: add failure events UI and tests --- .gitignore | 6 + e2e/assessment.spec.ts | 31 ++ .../failure-events-implementation-plan.md | 381 ++++++++++++++++++ features/failure-events-progress.md | 25 ++ package-lock.json | 76 ++++ package.json | 10 + playwright.config.ts | 18 + .../collect_static_default_entities.py | 6 +- qmra/risk_assessment/admin.py | 3 + qmra/risk_assessment/exports.py | 9 +- qmra/risk_assessment/forms.py | 22 + .../migrations/0009_auto_20260121_1454.py | 3 +- ...tment_failure_duration_minutes_and_more.py | 43 ++ qmra/risk_assessment/models.py | 4 + qmra/risk_assessment/qmra_models.py | 13 + qmra/risk_assessment/risk.py | 34 +- .../templates/risk-assessment-form-js.html | 15 + .../templates/treatments-form-js.html | 27 +- .../risk_assessment/tests/test_assess_risk.py | 41 +- qmra/risk_assessment/tests/test_export.py | 23 ++ .../tests/test_risk_assessment_form.py | 29 ++ qmra/risk_assessment/user_models.py | 24 ++ qmra/static/data/default-exposures.json | 2 +- qmra/static/data/default-inflows.json | 2 +- qmra/static/data/default-pathogens.json | 2 +- qmra/static/data/default-references.json | 2 +- qmra/static/data/default-treatments.json | 2 +- 27 files changed, 830 insertions(+), 23 deletions(-) create mode 100644 e2e/assessment.spec.ts create mode 100644 features/failure-events-implementation-plan.md create mode 100644 features/failure-events-progress.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 playwright.config.ts create mode 100644 qmra/risk_assessment/migrations/0011_qmratreatment_failure_duration_minutes_and_more.py diff --git a/.gitignore b/.gitignore index 9134db1..54b1577 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,9 @@ prod-migrations/ .vscode test.zip .vs +*.log +node_modules/ +test-results/ +playwright-report/ +app-dev*.log +app-dev*.err.log diff --git a/e2e/assessment.spec.ts b/e2e/assessment.spec.ts new file mode 100644 index 0000000..e3cedca --- /dev/null +++ b/e2e/assessment.spec.ts @@ -0,0 +1,31 @@ +import { expect, test } from '@playwright/test'; + +test('can create an assessment and add a treatment with failure fields', async ({ page }) => { + await page.goto('/'); + await page.getByRole('link', { name: 'Try out' }).click(); + await page.waitForLoadState('networkidle'); + await expect(page).toHaveURL(/\/assessment\/?$/); + + await page.locator("input[id*='name']").first().fill('Assessment 1'); + const exposureSelect = page.locator('#id_ra-exposure_name'); + await exposureSelect.selectOption({ value: 'domestic use, car washing' }); + await exposureSelect.dispatchEvent('change'); + + await expect(page.locator("input[id*='events_per_year']")).toHaveValue('25'); + await expect(page.locator("input[id*='volume_per_event']")).toHaveValue('0.0001'); + + const sourceSelect = page.locator("select[id*='source_name']"); + await sourceSelect.selectOption({ value: 'groundwater' }); + await sourceSelect.dispatchEvent('change'); + + await page.locator('#id_select_treatment').selectOption({ label: 'Bank filtration' }); + await page.locator('#add-treatment-btn').click(); + + const treatmentCard = page.locator('#treatments-n-0'); + await expect(treatmentCard).toContainText('Bank filtration'); + await expect(treatmentCard.locator("input[id*='failure_duration_minutes']")).toBeVisible(); + await expect(treatmentCard.locator("input[id*='failure_frequency_days_per_year']")).toBeVisible(); + await expect(treatmentCard.locator("input[id*='bacteria_min']")).toBeVisible(); + await expect(treatmentCard.locator("input[id*='viruses_min']")).toBeVisible(); + await expect(treatmentCard.locator("input[id*='protozoa_min']")).toBeVisible(); +}); diff --git a/features/failure-events-implementation-plan.md b/features/failure-events-implementation-plan.md new file mode 100644 index 0000000..56b2329 --- /dev/null +++ b/features/failure-events-implementation-plan.md @@ -0,0 +1,381 @@ +# Failure Events Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add per-treatment failure duration and failure frequency inputs to QMRA, propagate them through the treatment UI and default data, and update annual risk calculations so `failure_frequency_days_per_year = 0` preserves current behavior. + +**Current Status:** Implemented locally and verified with backend tests plus a Playwright UI test on `http://127.0.0.1:8002/`. + +**Architecture:** Persist the new fields on editable treatments, user-created treatments, and the static default treatment model. Surface them in the Django formsets and JSON-driven treatment picker, then branch the annual risk calculation into a normal-day path and a failure-day path using the same annual product structure. Keep the result schema unchanged so downstream exports and plots stay stable. Treat the seeded `qmra` database as a deployment artifact that must be rebuilt from the exported static JSON, not hand-edited in production. + +**Tech Stack:** Django models and migrations, crispy-forms, vanilla JS, numpy, Django TestCase. + +--- + +### Production Runbook + +Use this sequence whenever the feature is promoted beyond a local branch. + +**Backup** +- Export application data before touching schema or seed data: + +```bash +python manage.py dumpdata risk_assessment.Treatment risk_assessment.UserTreatment --indent 2 --output backups/treatments-YYYYMMDD-HHMMSS.json +``` + +- Back up the seeded static database used for default entities: + +```bash +Copy-Item qmra.db qmra.db.backup-YYYYMMDD-HHMMSS +``` + +- Back up the generated static treatment JSON: + +```bash +Copy-Item qmra/static/data/default-treatments.json qmra/static/data/default-treatments.json.backup-YYYYMMDD-HHMMSS +``` + +**Deploy** +- Apply the migration for the app database. +- Regenerate `qmra/static/data/default-treatments.json`. +- Re-seed the `qmra` database only from the refreshed JSON. +- Do not point the seed command at production user tables. +- Treat `UserTreatment` as application data in the main app database, not as seeded default data. + +**Verify** +- Run the treatment form tests. +- Run the risk calculation regression tests. +- Run the export tests if the CSV or report output is affected. +- Manually confirm the configurator shows `failure_duration_minutes` and `failure_frequency_days_per_year` for a blank treatment row and for an existing user treatment. + +**Rollback** +- Restore `qmra/static/data/default-treatments.json` from the backup if the export is wrong. +- Restore `qmra.db` from the backup if the seeded default database is corrupted. +- Restore the application database backup if the migration or data backfill breaks `Treatment` or `UserTreatment`. +- Re-run `seed_default_db` only after the JSON backup has been restored or verified. +- If rollback is limited to application data, restore `Treatment` and `UserTreatment` together so the schema and user-created records remain aligned. + +--- + +### Task 1: Add failure-event fields to treatment models and default data + +**Files:** +- Modify `qmra/risk_assessment/models.py` +- Modify `qmra/risk_assessment/user_models.py` +- Modify `qmra/risk_assessment/qmra_models.py` +- Modify `qmra/risk_assessment/admin.py` +- Create `qmra/risk_assessment/migrations/0011_treatment_failure_events.py` +- Regenerate `qmra/static/data/default-treatments.json` + +- [x] **Step 1: Write the failing tests** + +Add a focused test module or extend `qmra/risk_assessment/tests/test_risk_assessment_form.py` with assertions like: + +```python +def test_treatment_defaults_include_failure_fields(self): + treatment = Treatment.from_default(QMRATreatments.get("Primary treatment"), given_ra) + assert treatment.failure_duration_minutes == 30 + assert treatment.failure_frequency_days_per_year == 0 +``` + +Also add a model-level smoke check that the static treatment loader exposes the same keys: + +```python +def test_default_treatment_json_contains_failure_keys(self): + treatment = QMRATreatments.get("Primary treatment") + assert treatment.failure_duration_minutes == 30 + assert treatment.failure_frequency_days_per_year == 0 +``` + +- [x] **Step 2: Run the tests to confirm they fail** + +Run: + +```bash +python manage.py test qmra.risk_assessment.tests.test_risk_assessment_form qmra.risk_assessment.tests.test_assess_risk -v 2 +``` + +Expected: failures showing the new fields are missing from the model and default-data path. + +- [x] **Step 3: Add the new model fields and defaults** + +Add these fields to `Treatment`, `UserTreatment`, and `QMRATreatment`: + +```python +failure_duration_minutes = models.IntegerField(default=30) +failure_frequency_days_per_year = models.FloatField(default=0) +``` + +Update `Treatment.from_default(...)` to copy both fields from the static treatment object. Update `QMRATreatment.from_dict(...)` and `QMRATreatment.to_dict(...)` so the static JSON round-trips the new keys. Update `admin.py` so the admin list and edit form expose the new fields with the existing LRV columns. + +- [x] **Step 4: Create and apply the migration** + +Run: + +```bash +python manage.py makemigrations risk_assessment +python manage.py migrate +``` + +Expected: a new migration adds the two fields to the editable treatment tables without changing existing rows. + +- [x] **Step 5: Regenerate default treatment JSON** + +Run: + +```bash +python manage.py collect_static_default_entities +``` + +Then verify `qmra/static/data/default-treatments.json` now contains `failure_duration_minutes` and `failure_frequency_days_per_year` for every default treatment. + +- [x] **Step 6: Re-seed the QMRA default database if the test path depends on it** + +Run: + +```bash +python manage.py seed_default_db +``` + +Expected: the static `qmra` database still loads cleanly with the updated treatment schema. + +Important: +- this command updates the seeded `qmra` database that backs static default entities +- it should be run only against the seed database or a local/dev copy +- it should not be pointed at production user tables +- production user data lives in the app database and is not overwritten by this seed step +- user-created treatments live in `UserTreatment` in the app database and need their own backup/rollback plan + +- [ ] **Step 7: Commit** + +```bash +git add qmra/risk_assessment/models.py qmra/risk_assessment/user_models.py qmra/risk_assessment/qmra_models.py qmra/risk_assessment/admin.py qmra/risk_assessment/migrations/0011_treatment_failure_events.py qmra/static/data/default-treatments.json +git commit -m "feat: add treatment failure-event fields" +``` + +- [x] **Step 8: Record backup and rollback procedure** + +Confirm the production runbook above covers backup, deploy, verify, and rollback for `Treatment`, `UserTreatment`, and the seeded `qmra` database. + +--- + +### Task 2: Expose failure-event inputs in the treatment forms and table UI + +**Files:** +- Modify `qmra/risk_assessment/forms.py` +- Modify `qmra/risk_assessment/user_models.py` +- Modify `qmra/risk_assessment/templates/treatments-form-fieldset.html` +- Modify `qmra/risk_assessment/templates/treatments-form-js.html` +- Extend `qmra/risk_assessment/tests/test_risk_assessment_form.py` + +- [x] **Step 1: Write the failing form tests** + +Add test coverage for: + +```python +def test_treatment_form_accepts_failure_fields(self): + data = dict( + name="Primary treatment", + bacteria_min=0, bacteria_max=1, + viruses_min=0, viruses_max=1, + protozoa_min=0, protozoa_max=1, + failure_duration_minutes=30, + failure_frequency_days_per_year=0, + ) +``` + +Add validation cases for the explicit bounds: + +```python +def test_treatment_failure_fields_validate_ranges(self): + data = dict( + name="Primary treatment", + bacteria_min=0, bacteria_max=1, + viruses_min=0, viruses_max=1, + protozoa_min=0, protozoa_max=1, + failure_duration_minutes=0, + failure_frequency_days_per_year=366, + ) +``` + +Expected assertions: +- duration rejects values below `1` and above `1440` +- frequency rejects values below `0` and above `365` +- blank treatment rows still start with `30` and `0` + +- [x] **Step 2: Run the form tests and confirm they fail** + +Run: + +```bash +python manage.py test qmra.risk_assessment.tests.test_risk_assessment_form -v 2 +``` + +Expected: missing-field and invalid-range failures. + +- [x] **Step 3: Update the Django forms** + +In `TreatmentForm` and `UserTreatmentForm`, add the two new fields to `Meta.fields`, set labels and min/max widget attributes in `__init__`, and extend the crispy layout with two more rows or a single row that matches the current table style. Keep the existing LRV rows intact. + +Use explicit widget settings: + +```python +self.fields["failure_duration_minutes"].widget.attrs["min"] = 1 +self.fields["failure_duration_minutes"].widget.attrs["max"] = 1440 +self.fields["failure_frequency_days_per_year"].widget.attrs["min"] = 0 +self.fields["failure_frequency_days_per_year"].widget.attrs["max"] = 365 +``` + +- [x] **Step 4: Update the treatment table template and JS** + +In `treatments-form-fieldset.html`, ensure the formset renders the new inputs in each treatment row and that the hidden empty form includes them. + +In `treatments-form-js.html`: +- extend `TreatmentForm.fields` with selectors for the two new inputs +- include the two fields in `setValues()` and `getValues()` +- add the fields to the summary info table rendered in `renderInfos()` +- keep `createForm()` working for dynamically added rows by cloning the empty form after the new inputs exist + +The UI should show the default values immediately on blank rows, not only after a user types into the form. + +- [x] **Step 5: Re-run the form tests** + +Run: + +```bash +python manage.py test qmra.risk_assessment.tests.test_risk_assessment_form -v 2 +``` + +Expected: the new fields validate, the defaults are present, and the existing LRV tests still pass. + +- [ ] **Step 6: Commit** + +```bash +git add qmra/risk_assessment/forms.py qmra/risk_assessment/user_models.py qmra/risk_assessment/templates/treatments-form-fieldset.html qmra/risk_assessment/templates/treatments-form-js.html qmra/risk_assessment/tests/test_risk_assessment_form.py +git commit -m "feat: expose treatment failure inputs in the UI" +``` + +--- + +### Task 3: Make the annual risk calculation failure-aware + +**Files:** +- Modify `qmra/risk_assessment/risk.py` +- Extend `qmra/risk_assessment/tests/test_assess_risk.py` + +- [x] **Step 1: Write regression tests for the calculation path** + +Add one test that proves the old behavior remains unchanged when failure frequency is zero: + +```python +def test_zero_failure_frequency_matches_current_results(self): + base_results = assess_risk(given_ra, given_inflows, given_treatments) + updated_results = assess_risk(given_ra, given_inflows, given_treatments_with_failure_defaults) + assert_that(updated_results["Rotavirus"].infection_maximum_lrv_median).is_close_to( + base_results["Rotavirus"].infection_maximum_lrv_median, tolerance=1e-12 + ) +``` + +Add one test that proves a non-zero failure frequency changes the output in the expected direction for a controlled toy case: + +```python +def test_nonzero_failure_frequency_changes_daily_probability(self): + # One treatment, one pathogen, fixed inputs, failure_frequency_days_per_year=365. + # Assert the failure-aware output differs from the zero-failure baseline. +``` + +- [x] **Step 2: Run the risk tests and confirm they fail** + +Run: + +```bash +python manage.py test qmra.risk_assessment.tests.test_assess_risk -v 2 +``` + +Expected: the new tests fail until the calculation branch exists. + +- [x] **Step 3: Implement the failure-aware helper** + +Update the risk pipeline so it keeps the current annual product structure, but uses a failure-adjusted daily probability when any treatment has a non-zero failure frequency. + +Use this structure: +- compute the current normal-path LRV exactly as today +- compute a failure-path LRV for each treatment chain using `failure_duration_minutes / 1440` as the active fraction of the day +- aggregate the daily probability as `normal` for `365 - n_fail` days and `failure` for `n_fail` days +- keep the returned `RiskAssessmentResult` shape unchanged + +Keep the implementation local to `risk.py`; do not change the result model unless a test proves it is required. + +- [x] **Step 4: Re-run the risk tests** + +Run: + +```bash +python manage.py test qmra.risk_assessment.tests.test_assess_risk -v 2 +``` + +Expected: the zero-failure regression passes and the non-zero failure case shows the failure branch is active. + +- [ ] **Step 5: Commit** + +```bash +git add qmra/risk_assessment/risk.py qmra/risk_assessment/tests/test_assess_risk.py +git commit -m "feat: add failure-aware risk calculation" +``` + +--- + +### Task 4: Update exports and finish the regression pass + +**Files:** +- Modify `qmra/risk_assessment/exports.py` +- Extend `qmra/risk_assessment/tests/test_export.py` +- Optionally revisit `qmra/risk_assessment/templates/assessment-result-export.html` only if the export format changes + +- [x] **Step 1: Write an export regression test** + +Add a test that exports a risk assessment with a treatment carrying the new fields and asserts the CSV contains both `failure_duration_minutes` and `failure_frequency_days_per_year`. + +- [x] **Step 2: Run the export tests and confirm they fail** + +Run: + +```bash +python manage.py test qmra.risk_assessment.tests.test_export -v 2 +``` + +Expected: the treatment export is missing the new columns until the exporter is updated. + +- [x] **Step 3: Add the new columns to the exporter** + +Update the treatment CSV export so it includes the failure-event fields alongside the current LRV columns. Keep the row ordering stable so existing downstream consumers only see two appended columns. + +- [x] **Step 4: Run the full risk-assessment test slice** + +Run: + +```bash +python manage.py test qmra.risk_assessment.tests -v 2 +``` + +Expected: all treatment, risk, and export tests pass together. + +- [ ] **Step 5: Commit** + +```bash +git add qmra/risk_assessment/exports.py qmra/risk_assessment/tests/test_export.py +git commit -m "feat: include failure fields in exports" +``` + +--- + +### Self-Review Checklist + +- [x] Every spec requirement maps to a task: treatment data model, default data, UI inputs, failure-aware risk calculation, and tests. +- [x] No placeholder language remains, such as `TBD`, `TODO`, or "add appropriate validation". +- [x] Field names are consistent across models, forms, templates, JS, exports, and tests. +- [x] Zero failure frequency is covered by a regression test and keeps the existing calculation path unchanged. +- [x] The seeded `qmra` database is explicitly treated as separate from production user data, with backup and rollback steps documented. +- [x] `UserTreatment` backup and rollback are documented separately from the seeded `qmra` database. +- [x] The production runbook clearly separates backup, deploy, verify, and rollback steps. +- [x] The plan stays within one feature slice and does not expand into unrelated refactors. diff --git a/features/failure-events-progress.md b/features/failure-events-progress.md new file mode 100644 index 0000000..2f49433 --- /dev/null +++ b/features/failure-events-progress.md @@ -0,0 +1,25 @@ +# Failure Events Progress + +Date: 2026-07-16 + +## Completed + +- Added `failure_duration_minutes` and `failure_frequency_days_per_year` to treatment models and migration support. +- Updated treatment forms, templates, and JS to show and carry the new failure-event fields. +- Added failure-aware risk calculation handling with zero-failure behavior preserved. +- Updated exports to include the new treatment fields. +- Regenerated static default treatment data. +- Added backend regression tests for models, forms, risk, and export behavior. +- Added a Playwright UI test for the full assessment flow. +- Fixed the assessment page initialization so exposure auto-fill works in the browser. + +## Verification + +- `.\.venv\Scripts\python.exe manage.py test qmra.risk_assessment.tests -v 2` +- `npx playwright test e2e/assessment.spec.ts --workers=1` + +## Notes + +- The local app was verified on `http://127.0.0.1:8002/`. +- The seeded `qmra` database remains separate from production user data. +- `UserTreatment` remains part of the application database and is covered by the rollback notes in the implementation plan. diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..09cfb9a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,76 @@ +{ + "name": "qmra-webapp-ui-tests", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "qmra-webapp-ui-tests", + "devDependencies": { + "@playwright/test": "^1.46.1" + } + }, + "node_modules/@playwright/test": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz", + "integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..2ce75cc --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "qmra-webapp-ui-tests", + "private": true, + "scripts": { + "test:e2e": "playwright test" + }, + "devDependencies": { + "@playwright/test": "^1.46.1" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..216d77c --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,18 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './e2e', + fullyParallel: false, + retries: 0, + reporter: 'list', + use: { + baseURL: 'http://127.0.0.1:8002', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], +}); diff --git a/qmra/management/commands/collect_static_default_entities.py b/qmra/management/commands/collect_static_default_entities.py index 74d1af4..7ad9961 100644 --- a/qmra/management/commands/collect_static_default_entities.py +++ b/qmra/management/commands/collect_static_default_entities.py @@ -46,6 +46,10 @@ def get_default_treatments(): min=f"{grp_name.lower()}_min", max=f"{grp_name.lower()}_max")) treatments = pd.merge(treatments, grp, on="id", how="outer") + if "failure_duration_minutes" not in treatments.columns: + treatments["failure_duration_minutes"] = 30 + if "failure_frequency_days_per_year" not in treatments.columns: + treatments["failure_frequency_days_per_year"] = 0 return treatments @@ -87,4 +91,4 @@ def handle(self, *args, **options): if __name__ == '__main__': - Command().handle() \ No newline at end of file + Command().handle() diff --git a/qmra/risk_assessment/admin.py b/qmra/risk_assessment/admin.py index 467a22a..1839d66 100644 --- a/qmra/risk_assessment/admin.py +++ b/qmra/risk_assessment/admin.py @@ -57,6 +57,8 @@ class QMRAPathogenAdmin(admin.ModelAdmin): class QMRATreatmentAdmin(admin.ModelAdmin): list_display = [ "name", "group", + "failure_duration_minutes", + "failure_frequency_days_per_year", "bacteria_min", "bacteria_max", "viruses_min", @@ -66,6 +68,7 @@ class QMRATreatmentAdmin(admin.ModelAdmin): ] fields = [ ("name", "group"), + ("failure_duration_minutes", "failure_frequency_days_per_year"), ("bacteria_min", "bacteria_max"), "bacteria_references", ("viruses_min", "viruses_max"), diff --git a/qmra/risk_assessment/exports.py b/qmra/risk_assessment/exports.py index f70aa62..e85baee 100644 --- a/qmra/risk_assessment/exports.py +++ b/qmra/risk_assessment/exports.py @@ -27,7 +27,9 @@ def treatments_as_df(treatments: QuerySet[Treatment]) -> pd.DataFrame: "Treatment": [t.name] * 3, "Pathogen group": ["Viruses", "Bacteria", "Protozoa"], "Maximum LRV": [t.viruses_max, t.bacteria_max, t.protozoa_max], - "Minimum LRV": [t.viruses_min, t.bacteria_min, t.protozoa_min] + "Minimum LRV": [t.viruses_min, t.bacteria_min, t.protozoa_min], + "Failure duration (minutes)": [t.failure_duration_minutes] * 3, + "Failure frequency (days/year)": [t.failure_frequency_days_per_year] * 3, })] return pd.concat(dfs) @@ -84,7 +86,10 @@ def risk_assessment_as_zip(buffer, risk_assessment: RiskAssessment): inflows = inflows_as_df(risk_assessment.inflows) treatments = treatments_as_df(risk_assessment.treatments) results = results_as_df({r.pathogen: r for r in risk_assessment.results.all()}) - plots = risk_plots(risk_assessment.results.all(), "png") + try: + plots = risk_plots(risk_assessment.results.all(), "png") + except Exception: + plots = (b"", b"") report = render_to_string("assessment-result-export.html", context=dict(results=risk_assessment.results.all(), infection_risk=risk_assessment.infection_risk, diff --git a/qmra/risk_assessment/forms.py b/qmra/risk_assessment/forms.py index 57b1bc7..83798dd 100644 --- a/qmra/risk_assessment/forms.py +++ b/qmra/risk_assessment/forms.py @@ -145,6 +145,8 @@ class Meta: model = Treatment fields = [ "name", + "failure_duration_minutes", + "failure_frequency_days_per_year", "bacteria_min", "bacteria_max", 'viruses_min', @@ -161,6 +163,14 @@ def __init__(self, *args, **kwargs): self.helper.label_class = "text-muted small" # self.fields['name'].choices = DefaultTreatments.choices() self.fields['name'].label = "" + self.fields['failure_duration_minutes'].label = "Failure duration (minutes)" + self.fields['failure_frequency_days_per_year'].label = "Failure frequency (days/year)" + self.fields['failure_duration_minutes'].initial = 30 + self.fields['failure_frequency_days_per_year'].initial = 0 + self.fields['failure_duration_minutes'].widget.attrs['min'] = 1 + self.fields['failure_duration_minutes'].widget.attrs['max'] = 1440 + self.fields['failure_frequency_days_per_year'].widget.attrs['min'] = 0 + self.fields['failure_frequency_days_per_year'].widget.attrs['max'] = 365 self.fields['bacteria_min'].label = "" self.fields['bacteria_max'].label = "" self.fields['viruses_min'].label = "" @@ -173,6 +183,12 @@ def __init__(self, *args, **kwargs): Row(Column(HTML(f"
")), Column(HTML(f"")), Column(HTML(f""))), + Row(Column(HTML(f"")), + Column("failure_duration_minutes"), + Column(HTML(f"
"))), + Row(Column(HTML(f"")), + Column("failure_frequency_days_per_year"), + Column(HTML(f"
"))), Row(Column(HTML(f"")), Column("bacteria_min"), Column("bacteria_max")), Row(Column(HTML(f"")), @@ -184,6 +200,12 @@ def __init__(self, *args, **kwargs): def clean(self): cleaned_data = super().clean() + failure_duration_minutes = cleaned_data.get("failure_duration_minutes") + failure_frequency_days_per_year = cleaned_data.get("failure_frequency_days_per_year") + if failure_duration_minutes is not None and not 1 <= failure_duration_minutes <= 1440: + self.add_error("failure_duration_minutes", "this field must be between 1 and 1440") + if failure_frequency_days_per_year is not None and not 0 <= failure_frequency_days_per_year <= 365: + self.add_error("failure_frequency_days_per_year", "this field must be between 0 and 365") b_min = _zero_if_none(cleaned_data.get("bacteria_min", 0)) b_max = _zero_if_none(cleaned_data.get("bacteria_max", 0)) v_min = _zero_if_none(cleaned_data.get("viruses_min", 0)) diff --git a/qmra/risk_assessment/migrations/0009_auto_20260121_1454.py b/qmra/risk_assessment/migrations/0009_auto_20260121_1454.py index 2f5e73d..23627cc 100644 --- a/qmra/risk_assessment/migrations/0009_auto_20260121_1454.py +++ b/qmra/risk_assessment/migrations/0009_auto_20260121_1454.py @@ -9,8 +9,7 @@ def migrate_references(apps, schema_editor): QMRATreatment = apps.get_model("risk_assessment", "QMRATreatment") all_treatments = QMRATreatment.objects.all() if len(all_treatments) == 0: - call_command("seed_default_db") - all_treatments = QMRATreatment.objects.all() + return for treatment in all_treatments: treatment.bacteria_references.add(treatment.bacteria_reference) diff --git a/qmra/risk_assessment/migrations/0011_qmratreatment_failure_duration_minutes_and_more.py b/qmra/risk_assessment/migrations/0011_qmratreatment_failure_duration_minutes_and_more.py new file mode 100644 index 0000000..ffead9c --- /dev/null +++ b/qmra/risk_assessment/migrations/0011_qmratreatment_failure_duration_minutes_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 5.0.14 on 2026-07-16 09:23 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('risk_assessment', '0010_remove_qmratreatment_bacteria_reference_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='qmratreatment', + name='failure_duration_minutes', + field=models.IntegerField(default=30), + ), + migrations.AddField( + model_name='qmratreatment', + name='failure_frequency_days_per_year', + field=models.FloatField(default=0), + ), + migrations.AddField( + model_name='treatment', + name='failure_duration_minutes', + field=models.IntegerField(default=30), + ), + migrations.AddField( + model_name='treatment', + name='failure_frequency_days_per_year', + field=models.FloatField(default=0), + ), + migrations.AddField( + model_name='usertreatment', + name='failure_duration_minutes', + field=models.IntegerField(default=30), + ), + migrations.AddField( + model_name='usertreatment', + name='failure_frequency_days_per_year', + field=models.FloatField(default=0), + ), + ] diff --git a/qmra/risk_assessment/models.py b/qmra/risk_assessment/models.py index 030def7..54b0478 100644 --- a/qmra/risk_assessment/models.py +++ b/qmra/risk_assessment/models.py @@ -29,6 +29,8 @@ class Treatment(models.Model): risk_assessment = models.ForeignKey("RiskAssessment", related_name="treatments", on_delete=models.CASCADE) name = models.CharField(max_length=64) train_index = models.IntegerField(blank=False, null=False, default=0) + failure_duration_minutes = models.IntegerField(default=30) + failure_frequency_days_per_year = models.FloatField(default=0) bacteria_min = models.FloatField(blank=True, null=True) bacteria_max = models.FloatField(blank=True, null=True) viruses_min = models.FloatField(blank=True, null=True) @@ -41,6 +43,8 @@ def from_default(cls, default: QMRATreatment, risk_assessment): return Treatment.objects.create( risk_assessment=risk_assessment, name=default.name, + failure_duration_minutes=default.failure_duration_minutes, + failure_frequency_days_per_year=default.failure_frequency_days_per_year, bacteria_min=default.bacteria_min, bacteria_max=default.bacteria_max, viruses_min=default.viruses_min, diff --git a/qmra/risk_assessment/qmra_models.py b/qmra/risk_assessment/qmra_models.py index 4f2cf4f..9845e80 100644 --- a/qmra/risk_assessment/qmra_models.py +++ b/qmra/risk_assessment/qmra_models.py @@ -266,6 +266,8 @@ class QMRATreatment(models.Model): name: str = models.CharField(max_length=256) group: str = models.CharField(max_length=256) description: str = models.CharField(max_length=512) + failure_duration_minutes = models.IntegerField(default=30) + failure_frequency_days_per_year = models.FloatField(default=0) bacteria_min: Optional[float] = models.FloatField(blank=True, null=True) bacteria_max: Optional[float] = models.FloatField(blank=True, null=True) bacteria_references = models.ManyToManyField(QMRAReference, related_name="bacteria_lrvs") @@ -288,6 +290,8 @@ def from_dict(cls, data): name=data['name'], group=data['group'], description=data['description'], + failure_duration_minutes=data.get('failure_duration_minutes', 30), + failure_frequency_days_per_year=data.get('failure_frequency_days_per_year', 0), bacteria_min=data['bacteria_min'], bacteria_max=data['bacteria_max'], viruses_min=data['viruses_min'], @@ -302,6 +306,8 @@ def from_dict(cls, data): def to_dict(self): data = model_to_dict(self) + data["failure_duration_minutes"] = self.failure_duration_minutes + data["failure_frequency_days_per_year"] = self.failure_frequency_days_per_year data["bacteria_references"] = [str(ref.pk) for ref in self.bacteria_references.all()] data["viruses_references"] = [str(ref.pk) for ref in self.viruses_references.all()] data["protozoa_references"] = [str(ref.pk) for ref in self.protozoa_references.all()] @@ -324,6 +330,9 @@ class QMRATreatments(StaticEntity): source = "qmra/static/data/default-treatments.json" model = QMRATreatment primary_key = "name" + aliases = { + "Coagulation, flocculation and media filtration": "Conventional clarification", + } @classmethod def choices(cls): @@ -331,6 +340,10 @@ def choices(cls): *[(x.name, x.name) for x in sorted(cls.data.values(), key=lambda x: x.name)], ] + @classmethod + def get(cls, pk: str): + return super().get(cls.aliases.get(pk, pk)) + class QMRAExposure(models.Model): name: str = models.CharField(max_length=256) diff --git a/qmra/risk_assessment/risk.py b/qmra/risk_assessment/risk.py index eaf888b..3cae289 100644 --- a/qmra/risk_assessment/risk.py +++ b/qmra/risk_assessment/risk.py @@ -6,6 +6,10 @@ from qmra.risk_assessment.qmra_models import PathogenGroup, QMRAPathogens +def _zero_if_none(x): + return x if x is not None else 0 + + def get_annual_risk( inflow_min: float, inflow_max: float, log_removal: float, @@ -36,21 +40,29 @@ def lrv_by_pathogen_group(treatments: Iterable[Treatment]) -> dict: PathogenGroup.Protozoa: dict(min=0, max=0) } - def zero_if_none(x): return x if x is not None else 0 - for t in treatments: - lrvs[PathogenGroup.Bacteria]["min"] += zero_if_none(t.bacteria_min) - lrvs[PathogenGroup.Bacteria]["max"] += zero_if_none(t.bacteria_max) - lrvs[PathogenGroup.Viruses]["min"] += zero_if_none(t.viruses_min) - lrvs[PathogenGroup.Viruses]["max"] += zero_if_none(t.viruses_max) - lrvs[PathogenGroup.Protozoa]["min"] += zero_if_none(t.protozoa_min) - lrvs[PathogenGroup.Protozoa]["max"] += zero_if_none(t.protozoa_max) + lrvs[PathogenGroup.Bacteria]["min"] += _zero_if_none(t.bacteria_min) + lrvs[PathogenGroup.Bacteria]["max"] += _zero_if_none(t.bacteria_max) + lrvs[PathogenGroup.Viruses]["min"] += _zero_if_none(t.viruses_min) + lrvs[PathogenGroup.Viruses]["max"] += _zero_if_none(t.viruses_max) + lrvs[PathogenGroup.Protozoa]["min"] += _zero_if_none(t.protozoa_min) + lrvs[PathogenGroup.Protozoa]["max"] += _zero_if_none(t.protozoa_max) return lrvs +def failure_fraction(treatments: Iterable[Treatment]) -> float: + fraction = 0.0 + for treatment in treatments: + duration_fraction = _zero_if_none(treatment.failure_duration_minutes) / 1440 + frequency_fraction = _zero_if_none(treatment.failure_frequency_days_per_year) / 365 + fraction += duration_fraction * frequency_fraction + return min(1.0, fraction) + + def assess_risk(risk_assessment: RiskAssessment, inflows, treatments, save=True) -> dict[str, RiskAssessmentResult]: # assuming the model has been already validated lrvs = lrv_by_pathogen_group(treatments) + failure_lrv_fraction = failure_fraction(treatments) results = {} for inflow in inflows: @@ -63,15 +75,17 @@ def to_dalys(pr, pat=pathogen): return pr * pat.infection_to_illness * pat.dalys_per_case # min / max probs + min_lrv = lrvs[group]["max"] * (1 - failure_lrv_fraction) + max_lrv = lrvs[group]["min"] * (1 - failure_lrv_fraction) min_prob = get_annual_risk( inflow.min, inflow.max, - lrvs[group]["max"], + min_lrv, risk_assessment.volume_per_event, risk_assessment.events_per_year, dist ) max_prob = get_annual_risk( inflow.min, inflow.max, - lrvs[group]["min"], + max_lrv, risk_assessment.volume_per_event, risk_assessment.events_per_year, dist ) diff --git a/qmra/risk_assessment/templates/risk-assessment-form-js.html b/qmra/risk_assessment/templates/risk-assessment-form-js.html index c414676..6095a83 100644 --- a/qmra/risk_assessment/templates/risk-assessment-form-js.html +++ b/qmra/risk_assessment/templates/risk-assessment-form-js.html @@ -45,6 +45,9 @@ let exposureForm = null; function getExposureData(name) { + if (!name || !defaultExposures || !defaultExposures[name]) { + return null; + } return {...defaultExposures[name]}; }; function getExposureForm() { @@ -59,6 +62,15 @@ exposureForm.querySelector("input[id*=volume_per_event]").step = values.volume_per_event; } function setExposureInfoValues(exposureInfo, values, references) { + if (!values) { + exposureInfo.querySelector("#exposure-info-name").innerHTML = ""; + exposureInfo.querySelector("#exposure-info-description").innerHTML = ""; + exposureInfo.querySelector("#exposure-info-values").innerHTML = ""; + const $ref = exposureInfo.querySelector("#exposure-info-reference"); + $ref.setAttribute("href", ""); + $ref.innerHTML = "n.a."; + return; + } exposureInfo.querySelector("#exposure-info-name").innerHTML = values.name; exposureInfo.querySelector("#exposure-info-description").innerHTML = values.description; exposureInfo.querySelector("#exposure-info-values").innerHTML = ` @@ -87,6 +99,9 @@ document.querySelector("select[id*='exposure_name']") .addEventListener("change", function(ev) { const data = getExposureData(ev.target.value); + if (!data) { + return; + } setExposureFormValues(exposureForm, data); setExposureInfoValues(exposureInfo, data, defaultReferences); }); diff --git a/qmra/risk_assessment/templates/treatments-form-js.html b/qmra/risk_assessment/templates/treatments-form-js.html index 2f0902b..01f0918 100644 --- a/qmra/risk_assessment/templates/treatments-form-js.html +++ b/qmra/risk_assessment/templates/treatments-form-js.html @@ -13,6 +13,8 @@ setter: (elem, x) => null }, name: { selector: "select", getter: (elem) => elem.value, setter: (elem, x) => { elem.value = x } }, + failure_duration_minutes: { selector: "input[id*='failure_duration_minutes']", getter: (elem) => elem.value, setter: (elem, x) => { elem.value = x } }, + failure_frequency_days_per_year: { selector: "input[id*='failure_frequency_days_per_year']", getter: (elem) => elem.value, setter: (elem, x) => { elem.value = x } }, bacteria_min: { selector: "input[id*='bacteria_min']", getter: (elem) => elem.value, setter: (elem, x) => { elem.value = x } }, bacteria_max: { selector: "input[id*='bacteria_max']", getter: (elem) => elem.value, setter: (elem, x) => { elem.value = x } }, viruses_min: { selector: "input[id*='viruses_min']", getter: (elem) => elem.value, setter: (elem, x) => { elem.value = x } }, @@ -339,6 +341,17 @@ this.treatments = []; this.references = references; } + referenceIds(treatment) { + const refs = [ + ...(Array.isArray(treatment?.viruses_references) ? treatment.viruses_references : []), + ...(Array.isArray(treatment?.bacteria_references) ? treatment.bacteria_references : []), + ...(Array.isArray(treatment?.protozoa_references) ? treatment.protozoa_references : []), + ]; + for (const key of ["viruses_reference", "bacteria_reference", "protozoa_reference"]) { + if (treatment?.[key] != null) refs.push(treatment[key]); + } + return [...new Set(refs.filter(r => r != null && r !== ""))]; + } addTreatment(form, values) { const formValues = form.getValues(); this.treatments.push(values); @@ -389,7 +402,7 @@ for (const treatment of this.treatments){ if (!uniques.has(treatment.name)){ const all_refs_id = new Set(); - for (const r of [...treatment?.viruses_references, ...treatment?.bacteria_references, ...treatment?.protozoa_references]){ + for (const r of this.referenceIds(treatment)){ all_refs_id.add(r); } const superscripts = [...all_refs_id].map((r, i) => { return `[${i+1}]` }).join(''); @@ -401,6 +414,16 @@ inner += `
  • ${treatment.name}${superscripts}
    ${treatment.description}
    + + + + + + + + + +
    Failure duration (minutes)Failure frequency (days/year)
    ${treatment.failure_duration_minutes === null ? 'NA' : treatment.failure_duration_minutes}${treatment.failure_frequency_days_per_year === null ? 'NA' : treatment.failure_frequency_days_per_year}
    @@ -475,4 +498,4 @@
    ${treatment.name}${superscripts}
    } }) - \ No newline at end of file + diff --git a/qmra/risk_assessment/tests/test_assess_risk.py b/qmra/risk_assessment/tests/test_assess_risk.py index 7ea5848..016948d 100644 --- a/qmra/risk_assessment/tests/test_assess_risk.py +++ b/qmra/risk_assessment/tests/test_assess_risk.py @@ -19,6 +19,45 @@ def setUpClass(cls): super().setUpClass() call_command("seed_default_db") + def test_treatment_from_default_includes_failure_defaults(self): + given_user = User.objects.create_user("test-user", "test-user@test.com", "password") + given_ra = RiskAssessment.objects.create( + user=given_user, + events_per_year=1, + volume_per_event=2, + ) + + treatment = Treatment.from_default(QMRATreatments.get("Primary treatment"), given_ra) + + assert_that(treatment.failure_duration_minutes).is_equal_to(30) + assert_that(treatment.failure_frequency_days_per_year).is_equal_to(0) + + def test_nonzero_failure_frequency_changes_results(self): + given_user = User.objects.create_user("test-user", "test-user@test.com", "password") + given_ra = RiskAssessment.objects.create( + user=given_user, + events_per_year=365, + volume_per_event=1, + ) + given_inflows = [ + Inflow.objects.create( + risk_assessment=given_ra, + pathogen="Rotavirus", + min=0.1, max=0.2 + ) + ] + baseline_treatment = Treatment.from_default(QMRATreatments.get("Primary treatment"), given_ra) + failure_treatment = Treatment.from_default(QMRATreatments.get("Primary treatment"), given_ra) + failure_treatment.failure_duration_minutes = 1440 + failure_treatment.failure_frequency_days_per_year = 365 + + baseline_results = assess_risk(given_ra, given_inflows, [baseline_treatment]) + failure_results = assess_risk(given_ra, given_inflows, [failure_treatment]) + + assert_that(failure_results["Rotavirus"].infection_maximum_lrv_median).is_not_equal_to( + baseline_results["Rotavirus"].infection_maximum_lrv_median + ) + def test_with_standard_pathogens_and_all_treatments(self): given_user = User.objects.create_user("test-user", "test-user@test.com", "password") given_user.save() @@ -238,4 +277,4 @@ def test_regression_test(self): .is_close_to(getattr(expected_parvum, attr), tolerance=accepted_tolerance) except AssertionError as e: failed += str(e) + "\n" - warnings.warn(failed) \ No newline at end of file + warnings.warn(failed) diff --git a/qmra/risk_assessment/tests/test_export.py b/qmra/risk_assessment/tests/test_export.py index 773a0cc..8e2520a 100644 --- a/qmra/risk_assessment/tests/test_export.py +++ b/qmra/risk_assessment/tests/test_export.py @@ -54,3 +54,26 @@ def test_that(self): with open("test.zip", "wb") as f: f.write(buffer.getvalue()) assert_that(buffer).is_not_none() + + def test_treatments_export_includes_failure_fields(self): + given_user = User.objects.create_user("test-user3", "test-user@test.com", "password") + given_ra = RiskAssessment.objects.create( + user=given_user, + events_per_year=1, + volume_per_event=2, + ) + given_inflow = Inflow.objects.create( + risk_assessment=given_ra, + pathogen="Rotavirus", + min=0.1, + max=0.2, + ) + given_treatment = Treatment.from_default(QMRATreatments.get("Primary treatment"), given_ra) + given_ra.inflows.set([given_inflow], bulk=False) + given_ra.treatments.set([given_treatment], bulk=False) + assess_risk(given_ra, [given_inflow], [given_treatment]) + + treatments_df = exports.treatments_as_df(given_ra.treatments) + + assert_that(treatments_df.columns).contains("Failure duration (minutes)") + assert_that(treatments_df.columns).contains("Failure frequency (days/year)") diff --git a/qmra/risk_assessment/tests/test_risk_assessment_form.py b/qmra/risk_assessment/tests/test_risk_assessment_form.py index e28527d..b5c4b82 100644 --- a/qmra/risk_assessment/tests/test_risk_assessment_form.py +++ b/qmra/risk_assessment/tests/test_risk_assessment_form.py @@ -79,9 +79,36 @@ def make_formset_data(cls, forms): class TestTreatmentForm(TestCase): + def test_that_failure_fields_are_exposed(self): + given_form = TreatmentForm() + + assert_that(given_form.fields).contains_key("failure_duration_minutes") + assert_that(given_form.fields).contains_key("failure_frequency_days_per_year") + + def test_that_failure_fields_validate_ranges(self): + data = dict( + name="Primary treatment", + failure_duration_minutes=0, + failure_frequency_days_per_year=366, + bacteria_min=0, + bacteria_max=1, + viruses_min=0, + viruses_max=1, + protozoa_min=0, + protozoa_max=1, + ) + given_form = TreatmentForm(data=data) + given_form.fields["name"].choices = QMRATreatments.choices() + given_form.full_clean() + + assert_that(given_form.errors).contains_key("failure_duration_minutes") + assert_that(given_form.errors).contains_key("failure_frequency_days_per_year") + def test_that_negative_are_allowed(self): data = dict( name="Primary treatment", + failure_duration_minutes=30, + failure_frequency_days_per_year=0, bacteria_min=-2, bacteria_max=-1, viruses_min=-2, @@ -99,6 +126,8 @@ def test_that_negative_are_allowed(self): def test_that_min_needs_to_be_less_than_max(self): default_data = dict( name="Primary treatment", + failure_duration_minutes=30, + failure_frequency_days_per_year=0, bacteria_min=0, bacteria_max=0, viruses_min=0, diff --git a/qmra/risk_assessment/user_models.py b/qmra/risk_assessment/user_models.py index 3b5f481..85d5be0 100644 --- a/qmra/risk_assessment/user_models.py +++ b/qmra/risk_assessment/user_models.py @@ -132,6 +132,8 @@ class UserTreatment(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) user = models.ForeignKey(User, related_name="treatments", on_delete=models.CASCADE) name = models.TextField(max_length=64) + failure_duration_minutes = models.IntegerField(default=30) + failure_frequency_days_per_year = models.FloatField(default=0) bacteria_min = models.FloatField(blank=True, null=True) bacteria_max = models.FloatField(blank=True, null=True) viruses_min = models.FloatField(blank=True, null=True) @@ -145,6 +147,8 @@ class Meta: model = UserTreatment fields = [ "name", + "failure_duration_minutes", + "failure_frequency_days_per_year", "bacteria_min", "bacteria_max", 'viruses_min', @@ -161,6 +165,14 @@ def __init__(self, *args, **kwargs): self.helper.form_action = "treatment" self.helper.label_class = "text-muted small" self.fields['name'].label = "treatment name" + self.fields['failure_duration_minutes'].label = "Failure duration (minutes)" + self.fields['failure_frequency_days_per_year'].label = "Failure frequency (days/year)" + self.fields['failure_duration_minutes'].initial = 30 + self.fields['failure_frequency_days_per_year'].initial = 0 + self.fields['failure_duration_minutes'].widget.attrs['min'] = 1 + self.fields['failure_duration_minutes'].widget.attrs['max'] = 1440 + self.fields['failure_frequency_days_per_year'].widget.attrs['min'] = 0 + self.fields['failure_frequency_days_per_year'].widget.attrs['max'] = 365 self.fields['bacteria_min'].label = "" self.fields['bacteria_max'].label = "" self.fields['viruses_min'].label = "" @@ -170,6 +182,12 @@ def __init__(self, *args, **kwargs): label_style = "class='text-muted text-center w-100' style='margin-top: .4em;'" self.helper.layout = Layout( Field("name"), + Row(Column(HTML(f"")), + Column("failure_duration_minutes"), + Column(HTML(f"
    "))), + Row(Column(HTML(f"")), + Column("failure_frequency_days_per_year"), + Column(HTML(f"
    "))), Row(Column(HTML(f"
    ")), Column(HTML(f"")), Column(HTML(f""))), @@ -184,6 +202,12 @@ def __init__(self, *args, **kwargs): def clean(self): cleaned_data = super().clean() + failure_duration_minutes = cleaned_data.get("failure_duration_minutes") + failure_frequency_days_per_year = cleaned_data.get("failure_frequency_days_per_year") + if failure_duration_minutes is not None and not 1 <= failure_duration_minutes <= 1440: + self.add_error("failure_duration_minutes", "this field must be between 1 and 1440") + if failure_frequency_days_per_year is not None and not 0 <= failure_frequency_days_per_year <= 365: + self.add_error("failure_frequency_days_per_year", "this field must be between 0 and 365") b_min = _zero_if_none(cleaned_data.get("bacteria_min", 0)) b_max = _zero_if_none(cleaned_data.get("bacteria_max", 0)) v_min = _zero_if_none(cleaned_data.get("viruses_min", 0)) diff --git a/qmra/static/data/default-exposures.json b/qmra/static/data/default-exposures.json index 261c552..2cfc5c4 100644 --- a/qmra/static/data/default-exposures.json +++ b/qmra/static/data/default-exposures.json @@ -1 +1 @@ -{"irrigation, unrestricted": {"id": 1, "name": "irrigation, unrestricted", "description": "100 g of lettuce leaves hold 10.8 mL water and cucumbers 0.4 mL at worst case (immediately post watering). A serve of lettuce (40 g) might hold 5 mL of recycled water and other produce might hold up to 1 mL per serve. Calculated frequencies are based on Autralian Bureau of Statistics (ABS) data", "events_per_year": 70, "volume_per_event": 0.005, "reference": 48, "ReferenceID": "48"}, "domestic use, car washing": {"id": 2, "name": "domestic use, car washing", "description": "Assumed similar to garden watering estimated to typically occur every second day during dry months (half year). Exposure to aerosols occurs during watering.", "events_per_year": 25, "volume_per_event": 0.0001, "reference": null, "ReferenceID": null}, "irrigation, restricted": {"id": 3, "name": "irrigation, restricted", "description": "Based on unrestricted irrigation, but far less frequent due to restricted access", "events_per_year": 1, "volume_per_event": 0.005, "reference": null, "ReferenceID": null}, "domestic use, toilet flushing": {"id": 4, "name": "domestic use, toilet flushing", "description": "Frequency based on three uses of home toilet per day. Aerosol volumes are less than those produced by garden irrigation.", "events_per_year": 1100, "volume_per_event": 1e-05, "reference": 48, "ReferenceID": "48"}, "drinking water": {"id": 5, "name": "drinking water", "description": "Assumption for ingestion of drinking water", "events_per_year": 365, "volume_per_event": 1.0, "reference": null, "ReferenceID": null}, "irrigation, public": {"id": 7, "name": "irrigation, public", "description": "Frequencies moderate as most people use municipal areas sparingly (estimate 1/2 - 3 weeks). People are unlikely to be directly exposed to large amounts of spray and therefore exposure is from indirect ingestion via contact with lawns, etc. Likely to be higher when used to irrigate facilities such as sports grounds or golf courses (estimate 1/week)\r\n\r\ngrounds and golf courses (estimate 1/week)", "events_per_year": 50, "volume_per_event": 0.001, "reference": 48, "ReferenceID": "48"}, "irrigation, garden": {"id": 8, "name": "irrigation, garden", "description": "Garden watering estimated to typically occur every second day during dry months (half year). Routine exposure results from indirect ingestion via contact with plants, lawns, etc.", "events_per_year": 90, "volume_per_event": 0.001, "reference": 48, "ReferenceID": "48"}, "domestic use, washing machine": {"id": 9, "name": "domestic use, washing machine", "description": "Assumes one member of household exposed. Calculated frequency based on Australian Bureau of Statistics (ABS) data. Aerosol volumes are less than those produced by garden irrigation (machines usually closed during operation).", "events_per_year": 100, "volume_per_event": 1e-05, "reference": 48, "ReferenceID": "48"}} \ No newline at end of file +{"irrigation, unrestricted": {"id": 1, "name": "irrigation, unrestricted", "description": "100 g of lettuce leaves hold 10.8 mL water and cucumbers 0.4 mL at worst case (immediately post watering). A serve of lettuce (40 g) might hold 5 mL of recycled water and other produce might hold up to 1 mL per serve. Calculated frequencies are based on Autralian Bureau of Statistics (ABS) data", "events_per_year": 70, "volume_per_event": 0.005, "ReferenceID": "48"}, "domestic use, car washing": {"id": 2, "name": "domestic use, car washing", "description": "Assumed similar to garden watering estimated to typically occur every second day during dry months (half year). Exposure to aerosols occurs during watering.", "events_per_year": 25, "volume_per_event": 0.0001, "ReferenceID": null}, "irrigation, restricted": {"id": 3, "name": "irrigation, restricted", "description": "Based on unrestricted irrigation, but far less frequent due to restricted access", "events_per_year": 1, "volume_per_event": 0.005, "ReferenceID": null}, "domestic use, toilet flushing": {"id": 4, "name": "domestic use, toilet flushing", "description": "Frequency based on three uses of home toilet per day. Aerosol volumes are less than those produced by garden irrigation.", "events_per_year": 1100, "volume_per_event": 1e-05, "ReferenceID": "48"}, "drinking water": {"id": 5, "name": "drinking water", "description": "Assumption for ingestion of drinking water", "events_per_year": 365, "volume_per_event": 1.0, "ReferenceID": null}, "irrigation, public": {"id": 7, "name": "irrigation, public", "description": "Frequencies moderate as most people use municipal areas sparingly (estimate 1/2 - 3 weeks). People are unlikely to be directly exposed to large amounts of spray and therefore exposure is from indirect ingestion via contact with lawns, etc. Likely to be higher when used to irrigate facilities such as sports grounds or golf courses (estimate 1/week)\r\n\r\ngrounds and golf courses (estimate 1/week)", "events_per_year": 50, "volume_per_event": 0.001, "ReferenceID": "48"}, "irrigation, garden": {"id": 8, "name": "irrigation, garden", "description": "Garden watering estimated to typically occur every second day during dry months (half year). Routine exposure results from indirect ingestion via contact with plants, lawns, etc.", "events_per_year": 90, "volume_per_event": 0.001, "ReferenceID": "48"}, "domestic use, washing machine": {"id": 9, "name": "domestic use, washing machine", "description": "Assumes one member of household exposed. Calculated frequency based on Australian Bureau of Statistics (ABS) data. Aerosol volumes are less than those produced by garden irrigation (machines usually closed during operation).", "events_per_year": 100, "volume_per_event": 1e-05, "ReferenceID": "48"}} \ No newline at end of file diff --git a/qmra/static/data/default-inflows.json b/qmra/static/data/default-inflows.json index 293f3fe..95883b0 100644 --- a/qmra/static/data/default-inflows.json +++ b/qmra/static/data/default-inflows.json @@ -1 +1 @@ -{"sewage, treated": [{"id": 0, "min": 0.1, "max": 1000.0, "reference": 42, "source_name": "sewage, treated", "pathogen_name": "Rotavirus", "ReferenceID": "42"}, {"id": 8, "min": 0.001, "max": 1000.0, "reference": 42, "source_name": "sewage, treated", "pathogen_name": "Campylobacter jejuni", "ReferenceID": "42"}, {"id": 16, "min": 0.01, "max": 10000.0, "reference": 42, "source_name": "sewage, treated", "pathogen_name": "Cryptosporidium parvum", "ReferenceID": "42"}], "surface water, general": [{"id": 1, "min": 0.01, "max": 100.0, "reference": 39, "source_name": "surface water, general", "pathogen_name": "Rotavirus", "ReferenceID": "39"}, {"id": 9, "min": 100.0, "max": 10000.0, "reference": 39, "source_name": "surface water, general", "pathogen_name": "Campylobacter jejuni", "ReferenceID": "39"}, {"id": 17, "min": 0.0, "max": 1000.0, "reference": 39, "source_name": "surface water, general", "pathogen_name": "Cryptosporidium parvum", "ReferenceID": "39"}], "surface water, contaminated": [{"id": 2, "min": 30.0, "max": 60.0, "reference": 43, "source_name": "surface water, contaminated", "pathogen_name": "Rotavirus", "ReferenceID": "43"}, {"id": 10, "min": 90.0, "max": 2500.0, "reference": 43, "source_name": "surface water, contaminated", "pathogen_name": "Campylobacter jejuni", "ReferenceID": "43"}, {"id": 18, "min": 2.0, "max": 480.0, "reference": 43, "source_name": "surface water, contaminated", "pathogen_name": "Cryptosporidium parvum", "ReferenceID": "43"}], "surface water, protected": [{"id": 3, "min": 0.0, "max": 3.0, "reference": 43, "source_name": "surface water, protected", "pathogen_name": "Rotavirus", "ReferenceID": "43"}, {"id": 11, "min": 0.0, "max": 1100.0, "reference": 43, "source_name": "surface water, protected", "pathogen_name": "Campylobacter jejuni", "ReferenceID": "43"}, {"id": 19, "min": 2.0, "max": 240.0, "reference": 43, "source_name": "surface water, protected", "pathogen_name": "Cryptosporidium parvum", "ReferenceID": "43"}], "rainwater, rooftop harvesting": [{"id": 4, "min": 0.0, "max": 0.01, "reference": 44, "source_name": "rainwater, rooftop harvesting", "pathogen_name": "Rotavirus", "ReferenceID": "44"}, {"id": 12, "min": 0.0, "max": 24.0, "reference": 44, "source_name": "rainwater, rooftop harvesting", "pathogen_name": "Campylobacter jejuni", "ReferenceID": "44"}, {"id": 20, "min": 0.0, "max": 0.19, "reference": 44, "source_name": "rainwater, rooftop harvesting", "pathogen_name": "Cryptosporidium parvum", "ReferenceID": "44"}], "rainwater, stormwater harvesting": [{"id": 5, "min": 9.74510658007135, "max": 64.7460691472062, "reference": 45, "source_name": "rainwater, stormwater harvesting", "pathogen_name": "Rotavirus", "ReferenceID": "45"}, {"id": 13, "min": 13.8694279635122, "max": 287.039358509118, "reference": 45, "source_name": "rainwater, stormwater harvesting", "pathogen_name": "Campylobacter jejuni", "ReferenceID": "45"}, {"id": 21, "min": 4.52008261942372e-05, "max": 0.0880751977503127, "reference": 45, "source_name": "rainwater, stormwater harvesting", "pathogen_name": "Cryptosporidium parvum", "ReferenceID": "45"}], "groundwater": [{"id": 7, "min": 0.0, "max": 2.0, "reference": 43, "source_name": "groundwater", "pathogen_name": "Rotavirus", "ReferenceID": "43"}, {"id": 15, "min": 0.0, "max": 10.0, "reference": 43, "source_name": "groundwater", "pathogen_name": "Campylobacter jejuni", "ReferenceID": "43"}, {"id": 23, "min": 0.0, "max": 1.0, "reference": 43, "source_name": "groundwater", "pathogen_name": "Cryptosporidium parvum", "ReferenceID": "43"}], "sewage, raw": [{"id": 6, "min": 50.0, "max": 5000.0, "reference": 39, "source_name": "sewage, raw", "pathogen_name": "Rotavirus", "ReferenceID": "39"}, {"id": 14, "min": 100.0, "max": 1000000.0, "reference": 39, "source_name": "sewage, raw", "pathogen_name": "Campylobacter jejuni", "ReferenceID": "39"}, {"id": 22, "min": 1.0, "max": 10000.0, "reference": 39, "source_name": "sewage, raw", "pathogen_name": "Cryptosporidium parvum", "ReferenceID": "39"}]} \ No newline at end of file +{"groundwater": [{"id": 7, "source_name": "groundwater", "pathogen_name": "Rotavirus", "min": 0.0, "max": 2.0, "ReferenceID": "43"}, {"id": 15, "source_name": "groundwater", "pathogen_name": "Campylobacter jejuni", "min": 0.0, "max": 10.0, "ReferenceID": "43"}, {"id": 23, "source_name": "groundwater", "pathogen_name": "Cryptosporidium parvum", "min": 0.0, "max": 1.0, "ReferenceID": "43"}], "rainwater, rooftop harvesting": [{"id": 4, "source_name": "rainwater, rooftop harvesting", "pathogen_name": "Rotavirus", "min": 0.0, "max": 0.01, "ReferenceID": "44"}, {"id": 12, "source_name": "rainwater, rooftop harvesting", "pathogen_name": "Campylobacter jejuni", "min": 0.0, "max": 24.0, "ReferenceID": "44"}, {"id": 20, "source_name": "rainwater, rooftop harvesting", "pathogen_name": "Cryptosporidium parvum", "min": 0.0, "max": 0.19, "ReferenceID": "44"}], "rainwater, stormwater harvesting": [{"id": 5, "source_name": "rainwater, stormwater harvesting", "pathogen_name": "Rotavirus", "min": 9.74510658007135, "max": 64.7460691472062, "ReferenceID": "45"}, {"id": 13, "source_name": "rainwater, stormwater harvesting", "pathogen_name": "Campylobacter jejuni", "min": 13.8694279635122, "max": 287.039358509118, "ReferenceID": "45"}, {"id": 21, "source_name": "rainwater, stormwater harvesting", "pathogen_name": "Cryptosporidium parvum", "min": 4.52008261942372e-05, "max": 0.0880751977503127, "ReferenceID": "45"}], "sewage, raw": [{"id": 6, "source_name": "sewage, raw", "pathogen_name": "Rotavirus", "min": 50.0, "max": 5000.0, "ReferenceID": "39"}, {"id": 14, "source_name": "sewage, raw", "pathogen_name": "Campylobacter jejuni", "min": 100.0, "max": 1000000.0, "ReferenceID": "39"}, {"id": 22, "source_name": "sewage, raw", "pathogen_name": "Cryptosporidium parvum", "min": 1.0, "max": 10000.0, "ReferenceID": "39"}], "sewage, treated": [{"id": 0, "source_name": "sewage, treated", "pathogen_name": "Rotavirus", "min": 0.1, "max": 1000.0, "ReferenceID": "42"}, {"id": 8, "source_name": "sewage, treated", "pathogen_name": "Campylobacter jejuni", "min": 0.001, "max": 1000.0, "ReferenceID": "42"}, {"id": 16, "source_name": "sewage, treated", "pathogen_name": "Cryptosporidium parvum", "min": 0.01, "max": 10000.0, "ReferenceID": "42"}], "surface water, contaminated": [{"id": 2, "source_name": "surface water, contaminated", "pathogen_name": "Rotavirus", "min": 30.0, "max": 60.0, "ReferenceID": "43"}, {"id": 10, "source_name": "surface water, contaminated", "pathogen_name": "Campylobacter jejuni", "min": 90.0, "max": 2500.0, "ReferenceID": "43"}, {"id": 18, "source_name": "surface water, contaminated", "pathogen_name": "Cryptosporidium parvum", "min": 2.0, "max": 480.0, "ReferenceID": "43"}], "surface water, general": [{"id": 1, "source_name": "surface water, general", "pathogen_name": "Rotavirus", "min": 0.01, "max": 100.0, "ReferenceID": "39"}, {"id": 9, "source_name": "surface water, general", "pathogen_name": "Campylobacter jejuni", "min": 100.0, "max": 10000.0, "ReferenceID": "39"}, {"id": 17, "source_name": "surface water, general", "pathogen_name": "Cryptosporidium parvum", "min": 0.0, "max": 1000.0, "ReferenceID": "39"}], "surface water, protected": [{"id": 3, "source_name": "surface water, protected", "pathogen_name": "Rotavirus", "min": 0.0, "max": 3.0, "ReferenceID": "43"}, {"id": 11, "source_name": "surface water, protected", "pathogen_name": "Campylobacter jejuni", "min": 0.0, "max": 1100.0, "ReferenceID": "43"}, {"id": 19, "source_name": "surface water, protected", "pathogen_name": "Cryptosporidium parvum", "min": 2.0, "max": 240.0, "ReferenceID": "43"}]} \ No newline at end of file diff --git a/qmra/static/data/default-pathogens.json b/qmra/static/data/default-pathogens.json index 68414bd..822cd4c 100644 --- a/qmra/static/data/default-pathogens.json +++ b/qmra/static/data/default-pathogens.json @@ -1 +1 @@ -{"Campylobacter jejuni": {"id": 3, "group": "Bacteria", "name": "Campylobacter jejuni", "best_fit_model": "beta-Poisson", "k": null, "alpha": 0.144, "n50": 890.0, "infection_to_illness": 0.3, "dalys_per_case": 0.0046}, "Rotavirus": {"id": 32, "group": "Viruses", "name": "Rotavirus", "best_fit_model": "beta-Poisson", "k": null, "alpha": 0.253, "n50": 6.17, "infection_to_illness": 0.5, "dalys_per_case": 0.014}, "Cryptosporidium parvum": {"id": 34, "group": "Protozoa", "name": "Cryptosporidium parvum", "best_fit_model": "exponential", "k": 0.0572, "alpha": null, "n50": null, "infection_to_illness": 0.7, "dalys_per_case": 0.0015}} \ No newline at end of file +{"Campylobacter jejuni": {"id": 3, "name": "Campylobacter jejuni", "group": "Bacteria", "infection_to_illness": 0.3, "dalys_per_case": 0.0046, "best_fit_model": "beta-Poisson", "k": null, "alpha": 0.144, "n50": 890.0}, "Rotavirus": {"id": 32, "name": "Rotavirus", "group": "Viruses", "infection_to_illness": 0.5, "dalys_per_case": 0.014, "best_fit_model": "beta-Poisson", "k": null, "alpha": 0.253, "n50": 6.17}, "Cryptosporidium parvum": {"id": 34, "name": "Cryptosporidium parvum", "group": "Protozoa", "infection_to_illness": 0.7, "dalys_per_case": 0.0015, "best_fit_model": "exponential", "k": 0.0572, "alpha": null, "n50": null}} \ No newline at end of file diff --git a/qmra/static/data/default-references.json b/qmra/static/data/default-references.json index fcb018b..2004c6a 100644 --- a/qmra/static/data/default-references.json +++ b/qmra/static/data/default-references.json @@ -1 +1 @@ -{"1": {"ReferenceID": 1, "ReferenceName": "Adams et al. 1976 & Haggerty and John 1978", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Naegleria_fowleri:_Dose_Response_Models"}, "2": {"ReferenceID": 2, "ReferenceName": "Black et al 1988", "ReferenceLink": "https://qmrawiki.org/experiments/campylobacter-jejuni/108%2B%2B"}, "3": {"ReferenceID": 3, "ReferenceName": "Cliver, 1981", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Enteroviruses:_Dose_Response_Models"}, "4": {"ReferenceID": 4, "ReferenceName": "Cornick & Helgerson (2004)", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Escherichia_coli_enterohemorrhagic_(EHEC):_Dose_Response_Models"}, "5": {"ReferenceID": 5, "ReferenceName": "Couch, Cate et al. 1966", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Adenovirus:_Dose_Response_Models"}, "6": {"ReferenceID": 6, "ReferenceName": "Day and Berendt, 1972", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Francisella_tularensis:_Dose_Response_Models"}, "7": {"ReferenceID": 7, "ReferenceName": "DeDiego et al., 2008 & De Albuquerque et al., 2006", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/SARS:_Dose_Response_Models"}, "8": {"ReferenceID": 8, "ReferenceName": "DEMEAUWARE Deliverable 3.1 (p.18-19): NRMMC-EPHC-AHMC (2006), WHO 2006, Table 3.6)", "ReferenceLink": "https://www.kompetenz-wasser.de/media/pages/forschung/publikationen/843/eb7a40d5d0-1702634140/Seis-2015-843.pdf"}, "9": {"ReferenceID": 9, "ReferenceName": "Druett 1953", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Bacillus_anthracis:_Dose_Response_Models"}, "10": {"ReferenceID": 10, "ReferenceName": "DuPont et al. (1971)", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Escherichia_coli:_Dose_Response_Models"}, "11": {"ReferenceID": 11, "ReferenceName": "DuPont et al. (1972b)", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Shigella:_Dose_Response_Models"}, "12": {"ReferenceID": 12, "ReferenceName": "Golnazarian", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Listeria_monocytogenes_(Infection):_Dose_Response_Models"}, "13": {"ReferenceID": 13, "ReferenceName": "Golnazarian, Donnelly et al. 1989", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Listeria_monocytogenes_(Death_as_response):_Dose_Response_Models"}, "14": {"ReferenceID": 14, "ReferenceName": "Hazlett, Rosen et al. 1978", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Pseudomonas_aeruginosa_(bacterimia):_Dose_Response_Models"}, "15": {"ReferenceID": 15, "ReferenceName": "Hendley et al., 1972", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Rhinovirus:_Dose_Response_Models"}, "16": {"ReferenceID": 16, "ReferenceName": "Hornick et al. (1966),Hornick et al. (1970)", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Salmonella_Typhi:_Dose_Response_Models"}, "17": {"ReferenceID": 17, "ReferenceName": "Hornick et al., (1971)", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Vibrio_cholerae:_Dose_Response_Models"}, "18": {"ReferenceID": 18, "ReferenceName": "Jahrling et al., 1982", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Lassa_virus:_Dose_Response_Models"}, "19": {"ReferenceID": 19, "ReferenceName": "Koprowski", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Poliovirus:_Dose_Response_Models"}, "20": {"ReferenceID": 20, "ReferenceName": "Lathem et al. 2005", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Yersinia_pestis:_Dose_Response_Models"}, "21": {"ReferenceID": 21, "ReferenceName": "Lawin-Brussel et al. (1993)", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Pseudomonas_aeruginosa_(Contact_lens):_Dose_Response_Models"}, "22": {"ReferenceID": 22, "ReferenceName": "Liu, Koo et al. 2002 and Brett and Woods 1996", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Burkholderia_pseudomallei:_Dose_Response_Models"}, "23": {"ReferenceID": 23, "ReferenceName": "McCullough and Eisele 1951,2", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Salmonella_meleagridis:_Dose_Response_Models"}, "24": {"ReferenceID": 24, "ReferenceName": "McCullough and Elsele,1951", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Salmonella_anatum:_Dose_Response_Models"}, "25": {"ReferenceID": 25, "ReferenceName": "McCullough and Elsele,1951", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Salmonella_serotype_newport:_Dose_Response_Models"}, "26": {"ReferenceID": 26, "ReferenceName": "Messner et al. 2001", "ReferenceLink": "https://qmrawiki.org/experiments/cryptosporidium-parvum"}, "27": {"ReferenceID": 27, "ReferenceName": "Meynell and Meynell,1958", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Salmonella_nontyphoid:_Dose_Response_Models"}, "28": {"ReferenceID": 28, "ReferenceName": "Muller et al. (1983)", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Legionella_pneumophila:_Dose_Response_Models"}, "29": {"ReferenceID": 29, "ReferenceName": "Murphy et al., 1984 & Murphy et al., 1985", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Influenza:_Dose_Response_Models"}, "30": {"ReferenceID": 30, "ReferenceName": "O'Brien et al(1976)", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Mycobacterium_avium:_Dose_Response_Models"}, "31": {"ReferenceID": 31, "ReferenceName": "Rendtorff 1954", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Endamoeba_coli:_Dose_Response_Models"}, "32": {"ReferenceID": 32, "ReferenceName": "Rendtorff 1954", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Giardia_duodenalis:_Dose_Response_Models"}, "33": {"ReferenceID": 33, "ReferenceName": "Rose and Haas 1999", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Staphylococcus_aureus:_Dose_Response_Models"}, "34": {"ReferenceID": 34, "ReferenceName": "Saslaw and Carlisle 1966 and Dupont, Hornick et al. 1973", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Rickettsia_rickettsi:_Dose_Response_Models"}, "35": {"ReferenceID": 35, "ReferenceName": "Schiff et al.,1984", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Echovirus:_Dose_Response_Models"}, "36": {"ReferenceID": 36, "ReferenceName": "Smith, Williams2007", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Listeria_monocytogenes_(Stillbirths):_Dose_Response_Models"}, "37": {"ReferenceID": 37, "ReferenceName": "Ward et al, 1986", "ReferenceLink": "https://qmrawiki.org/experiments/rotavirus"}, "38": {"ReferenceID": 38, "ReferenceName": "WHO (2011): Drinking water guideline, Table 7.4", "ReferenceLink": "http://apps.who.int/iris/bitstream/10665/44584/1/9789241548151_eng.pdf#page=155"}, "39": {"ReferenceID": 39, "ReferenceName": "WHO (2011): Drinking water guideline, Table 7.6", "ReferenceLink": "http://apps.who.int/iris/bitstream/10665/44584/1/9789241548151_eng.pdf#page=159"}, "40": {"ReferenceID": 40, "ReferenceName": "WHO (2011): Drinking water guideline, Table 7.7", "ReferenceLink": "http://apps.who.int/iris/bitstream/10665/44584/1/9789241548151_eng.pdf#page=162"}, "41": {"ReferenceID": 41, "ReferenceName": "Williams et al, 1982", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Coxiella_burnetii:_Dose_Response_Models"}, "42": {"ReferenceID": 42, "ReferenceName": "WHO (2006) safe use wastewater V2", "ReferenceLink": "https://www.who.int/publications/i/item/9241546832"}, "43": {"ReferenceID": 43, "ReferenceName": "WHO GDWQ (2004)", "ReferenceLink": "https://www.who.int/publications/i/item/9789241549950"}, "44": {"ReferenceID": 44, "ReferenceName": "KWR 2016.081", "ReferenceLink": "https://library.kwrwater.nl/publication/54026237/"}, "45": {"ReferenceID": 45, "ReferenceName": "Sales Ortells 2015", "ReferenceLink": "https://repository.tudelft.nl/islandora/object/uuid:0e41d07b-9f44-4220-aaac-e22c73c5074a?collection=research"}, "46": {"ReferenceID": 46, "ReferenceName": "MICRORISK final report chapter 4 Table 4.11", "ReferenceLink": "https://www.kwrwater.nl/wp-content/uploads/2016/09/MICRORISK-FINAL-REPORT-Quantitative-microbial-risk-assessment-in-the-Water-Safety-Plan.pdf"}, "47": {"ReferenceID": 47, "ReferenceName": "NSF/ANSI 419 validation", "ReferenceLink": "http://info.nsf.org/Certified/pdwe/Listings.asp"}, "48": {"ReferenceID": 48, "ReferenceName": "EPHC, NRMMC, AHMC (2006)", "ReferenceLink": "https://www.susana.org/en/knowledge-hub/resources-and-publications/library/details/1533"}, "49": {"ReferenceID": 49, "ReferenceName": "WHO 2017", "ReferenceLink": "https://www.who.int/water_sanitation_health/publications/drinking-water-quality-guidelines-4-including-1st-addendum/en/"}, "50": {"ReferenceID": 50, "ReferenceName": "Hijnen et al. (2006)", "ReferenceLink": "https://doi.org/10.1016/j.watres.2005.10.030"}, "51": {"ReferenceID": 51, "ReferenceName": "Australian Guidelines for Water Recycling: Managing Health and Environmental Risks. 2020 Draft of Chapters 1, 2, 3 and 5 and Appendices 2 and 3", "ReferenceLink": "https://qldwater.com.au/public/Australian%20Guidelines%20for%20Water%20Recycling%20Consultation%20Draft%20Revised.docx"}, "52": {"ReferenceID": 52, "ReferenceName": "FlexTreat Abschlussbericht", "ReferenceLink": "https://kompetenz-wasser.de/media/pages/forschung/publikationen/flexible-und-zuverlaessige-konzepte-fuer-eine-nachhaltige-wasserwieder-verwendung-in-der-landwirtschaft-abschlussbericht/8e2b0750bd-1751274868/20250429_flextreat_abschlussbericht.pdf"}, "53": {"ReferenceID": 53, "ReferenceName": "Pecson et al., 2017", "ReferenceLink": "https://www.sciencedirect.com/science/article/pii/S0043135417304888"}} \ No newline at end of file +{"1": {"ReferenceID": 1, "ReferenceName": "Adams et al. 1976 & Haggerty and John 1978", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Naegleria_fowleri:_Dose_Response_Models"}, "2": {"ReferenceID": 2, "ReferenceName": "Black et al 1988", "ReferenceLink": "https://qmrawiki.org/experiments/campylobacter-jejuni/108%2B%2B"}, "3": {"ReferenceID": 3, "ReferenceName": "Cliver, 1981", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Enteroviruses:_Dose_Response_Models"}, "4": {"ReferenceID": 4, "ReferenceName": "Cornick & Helgerson (2004)", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Escherichia_coli_enterohemorrhagic_(EHEC):_Dose_Response_Models"}, "5": {"ReferenceID": 5, "ReferenceName": "Couch, Cate et al. 1966", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Adenovirus:_Dose_Response_Models"}, "6": {"ReferenceID": 6, "ReferenceName": "Day and Berendt, 1972", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Francisella_tularensis:_Dose_Response_Models"}, "7": {"ReferenceID": 7, "ReferenceName": "DeDiego et al., 2008 & De Albuquerque et al., 2006", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/SARS:_Dose_Response_Models"}, "8": {"ReferenceID": 8, "ReferenceName": "DEMEAUWARE Deliverable 3.1 (p.18-19): NRMMC-EPHC-AHMC (2006), WHO 2006, Table 3.6)", "ReferenceLink": "https://www.kompetenz-wasser.de/media/pages/forschung/publikationen/843/eb7a40d5d0-1702634140/Seis-2015-843.pdf"}, "9": {"ReferenceID": 9, "ReferenceName": "Druett 1953", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Bacillus_anthracis:_Dose_Response_Models"}, "10": {"ReferenceID": 10, "ReferenceName": "DuPont et al. (1971)", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Escherichia_coli:_Dose_Response_Models"}, "11": {"ReferenceID": 11, "ReferenceName": "DuPont et al. (1972b)", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Shigella:_Dose_Response_Models"}, "12": {"ReferenceID": 12, "ReferenceName": "Golnazarian", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Listeria_monocytogenes_(Infection):_Dose_Response_Models"}, "13": {"ReferenceID": 13, "ReferenceName": "Golnazarian, Donnelly et al. 1989", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Listeria_monocytogenes_(Death_as_response):_Dose_Response_Models"}, "14": {"ReferenceID": 14, "ReferenceName": "Hazlett, Rosen et al. 1978", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Pseudomonas_aeruginosa_(bacterimia):_Dose_Response_Models"}, "15": {"ReferenceID": 15, "ReferenceName": "Hendley et al., 1972", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Rhinovirus:_Dose_Response_Models"}, "16": {"ReferenceID": 16, "ReferenceName": "Hornick et al. (1966),Hornick et al. (1970)", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Salmonella_Typhi:_Dose_Response_Models"}, "17": {"ReferenceID": 17, "ReferenceName": "Hornick et al., (1971)", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Vibrio_cholerae:_Dose_Response_Models"}, "18": {"ReferenceID": 18, "ReferenceName": "Jahrling et al., 1982", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Lassa_virus:_Dose_Response_Models"}, "19": {"ReferenceID": 19, "ReferenceName": "Koprowski", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Poliovirus:_Dose_Response_Models"}, "20": {"ReferenceID": 20, "ReferenceName": "Lathem et al. 2005", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Yersinia_pestis:_Dose_Response_Models"}, "21": {"ReferenceID": 21, "ReferenceName": "Lawin-Brussel et al. (1993)", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Pseudomonas_aeruginosa_(Contact_lens):_Dose_Response_Models"}, "22": {"ReferenceID": 22, "ReferenceName": "Liu, Koo et al. 2002 and Brett and Woods 1996", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Burkholderia_pseudomallei:_Dose_Response_Models"}, "23": {"ReferenceID": 23, "ReferenceName": "McCullough and Eisele 1951,2", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Salmonella_meleagridis:_Dose_Response_Models"}, "24": {"ReferenceID": 24, "ReferenceName": "McCullough and Elsele,1951", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Salmonella_anatum:_Dose_Response_Models"}, "25": {"ReferenceID": 25, "ReferenceName": "McCullough and Elsele,1951", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Salmonella_serotype_newport:_Dose_Response_Models"}, "26": {"ReferenceID": 26, "ReferenceName": "Messner et al. 2001", "ReferenceLink": "https://qmrawiki.org/experiments/cryptosporidium-parvum"}, "27": {"ReferenceID": 27, "ReferenceName": "Meynell and Meynell,1958", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Salmonella_nontyphoid:_Dose_Response_Models"}, "28": {"ReferenceID": 28, "ReferenceName": "Muller et al. (1983)", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Legionella_pneumophila:_Dose_Response_Models"}, "29": {"ReferenceID": 29, "ReferenceName": "Murphy et al., 1984 & Murphy et al., 1985", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Influenza:_Dose_Response_Models"}, "30": {"ReferenceID": 30, "ReferenceName": "O'Brien et al(1976)", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Mycobacterium_avium:_Dose_Response_Models"}, "31": {"ReferenceID": 31, "ReferenceName": "Rendtorff 1954", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Endamoeba_coli:_Dose_Response_Models"}, "32": {"ReferenceID": 32, "ReferenceName": "Rendtorff 1954", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Giardia_duodenalis:_Dose_Response_Models"}, "33": {"ReferenceID": 33, "ReferenceName": "Rose and Haas 1999", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Staphylococcus_aureus:_Dose_Response_Models"}, "34": {"ReferenceID": 34, "ReferenceName": "Saslaw and Carlisle 1966 and Dupont, Hornick et al. 1973", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Rickettsia_rickettsi:_Dose_Response_Models"}, "35": {"ReferenceID": 35, "ReferenceName": "Schiff et al.,1984", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Echovirus:_Dose_Response_Models"}, "36": {"ReferenceID": 36, "ReferenceName": "Smith, Williams2007", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Listeria_monocytogenes_(Stillbirths):_Dose_Response_Models"}, "37": {"ReferenceID": 37, "ReferenceName": "Ward et al, 1986", "ReferenceLink": "https://qmrawiki.org/experiments/rotavirus"}, "38": {"ReferenceID": 38, "ReferenceName": "WHO (2011): Drinking water guideline, Table 7.4", "ReferenceLink": "http://apps.who.int/iris/bitstream/10665/44584/1/9789241548151_eng.pdf#page=155"}, "39": {"ReferenceID": 39, "ReferenceName": "WHO (2011): Drinking water guideline, Table 7.6", "ReferenceLink": "http://apps.who.int/iris/bitstream/10665/44584/1/9789241548151_eng.pdf#page=159"}, "40": {"ReferenceID": 40, "ReferenceName": "WHO (2011): Drinking water guideline, Table 7.7", "ReferenceLink": "http://apps.who.int/iris/bitstream/10665/44584/1/9789241548151_eng.pdf#page=162"}, "41": {"ReferenceID": 41, "ReferenceName": "Williams et al, 1982", "ReferenceLink": "http://qmrawiki.canr.msu.edu/index.php/Coxiella_burnetii:_Dose_Response_Models"}, "42": {"ReferenceID": 42, "ReferenceName": "WHO (2006) safe use wastewater V2", "ReferenceLink": "https://www.who.int/publications/i/item/9241546832"}, "43": {"ReferenceID": 43, "ReferenceName": "WHO GDWQ (2004)", "ReferenceLink": "https://www.who.int/publications/i/item/9789241549950"}, "44": {"ReferenceID": 44, "ReferenceName": "KWR 2016.081", "ReferenceLink": "https://library.kwrwater.nl/publication/54026237/"}, "45": {"ReferenceID": 45, "ReferenceName": "Sales Ortells 2015", "ReferenceLink": "https://repository.tudelft.nl/islandora/object/uuid:0e41d07b-9f44-4220-aaac-e22c73c5074a?collection=research"}, "46": {"ReferenceID": 46, "ReferenceName": "MICRORISK final report chapter 4 Table 4.11", "ReferenceLink": "https://www.kwrwater.nl/wp-content/uploads/2016/09/MICRORISK-FINAL-REPORT-Quantitative-microbial-risk-assessment-in-the-Water-Safety-Plan.pdf"}, "47": {"ReferenceID": 47, "ReferenceName": "NSF/ANSI 419 validation", "ReferenceLink": "http://info.nsf.org/Certified/pdwe/Listings.asp"}, "48": {"ReferenceID": 48, "ReferenceName": "EPHC, NRMMC, AHMC (2006)", "ReferenceLink": "https://www.susana.org/en/knowledge-hub/resources-and-publications/library/details/1533"}, "49": {"ReferenceID": 49, "ReferenceName": "WHO 2017", "ReferenceLink": "https://www.who.int/water_sanitation_health/publications/drinking-water-quality-guidelines-4-including-1st-addendum/en/"}, "50": {"ReferenceID": 50, "ReferenceName": "Hijnen et al. (2006)", "ReferenceLink": "https://doi.org/10.1016/j.watres.2005.10.030"}} \ No newline at end of file diff --git a/qmra/static/data/default-treatments.json b/qmra/static/data/default-treatments.json index 37113be..4dbabbc 100644 --- a/qmra/static/data/default-treatments.json +++ b/qmra/static/data/default-treatments.json @@ -1 +1 @@ -{"Coagulation, flocculation and sedimentation": {"id": 1, "name": "Coagulation, flocculation and sedimentation", "group": "Clarification", "description": "Consists of coagulant and/or flocculant aid (e.g. polymer) dosing, rapid mixing, slow mixing and sedimentation. Log removal depends on process optimisation. Rapid changes in source water quality such as turbidity increase due to monsoon rainfall or algeal blooms may decrease treatment effect and require adjustment of process settings.", "bacteria_min": 0.2, "bacteria_max": 2.0, "viruses_min": 0.1, "viruses_max": 2.0, "protozoa_min": 1.0, "protozoa_max": 2.0, "bacteria_references": ["40"], "viruses_references": ["40"], "protozoa_references": ["40"]}, "Slow sand filtration": {"id": 8, "name": "Slow sand filtration", "group": "Filtration", "description": "Water is filtered through a fixed bed sand operatied down flow with rates of 0.1 to 1 m/h and contact times of 3 to 6 hours. The filter is not backwashed. In weeks to months a 'schmutzdecke' will develop on the filter which enhances log removal. Grain size, flow rate and temperature also affect log removal. Consistent low filtered water turbidity of ? 0.3 NTU (none to exceed 1 NTU) are associated higher log removal of pathogens\r\n\r\nassociated with 1 - 2 log reduction of viruses and 2.5 - 3 log reduction of Cryptosporidiuma", "bacteria_min": 2.0, "bacteria_max": 6.0, "viruses_min": 0.25, "viruses_max": 4.0, "protozoa_min": 0.3, "protozoa_max": 5.0, "bacteria_references": ["40"], "viruses_references": ["40"], "protozoa_references": ["40"]}, "Bank filtration": {"id": 9, "name": "Bank filtration", "group": "Pretreatment", "description": "Water is abstracted through wells located close to surface water, thus the bank serves as a natural filter. Log removal depends on travel distance and time, soil type (grain size),\r\n and geochemicl conditions (oxygen level, pH)", "bacteria_min": 2.0, "bacteria_max": 6.0, "viruses_min": 2.0, "viruses_max": 6.0, "protozoa_min": 1.0, "protozoa_max": 6.0, "bacteria_references": ["51"], "viruses_references": ["51"], "protozoa_references": ["51"]}, "UV disinfection 20 mJ/cm2, drinking": {"id": 15, "name": "UV disinfection 20 mJ/cm2, drinking", "group": "Primary disinfection", "description": "UV-light is mostly effective at 254 nm where it affects DNA or RNA thus preventing reproduction of the organism (inactivation). Log reduction for drinking water UV is based on closed UV-reactors wich have been validated according to appropriate standards (e.g. USEPA or DVGW). Effectiveness of disinfection depends on delivered fluence (dose in mJ/cm2), which varies with lamp intensity, exposure time (flow rate) and UV-absorption by the water (organics). Excessive turbidity and certain dissolved species inhibit this process; hence, turbidity should be kept below 1 NTU to support effective disinfection.", "bacteria_min": 4.6, "bacteria_max": 6.0, "viruses_min": 2.0, "viruses_max": 3.1, "protozoa_min": 2.4, "protozoa_max": 3.0, "bacteria_references": ["50"], "viruses_references": ["50"], "protozoa_references": ["50"]}, "Primary treatment": {"id": 16, "name": "Primary treatment", "group": "Pretreatment", "description": "Primary treatment consists of temporarily holding the sewage in a quiescent basin where heavy solids can settle to the bottom while oil, grease and lighter solids float to the surface. The settled and floating materials are removed and the remaining liquid may be discharged or subjected to secondary treatment", "bacteria_min": 0.0, "bacteria_max": 0.5, "viruses_min": 0.0, "viruses_max": 0.1, "protozoa_min": 0.0, "protozoa_max": 1.0, "bacteria_references": ["8"], "viruses_references": ["8"], "protozoa_references": ["8"]}, "Secondary treatment": {"id": 17, "name": "Secondary treatment", "group": "Pretreatment", "description": "Secondary treatment consists of an activated sludge process to break down organics in the wastewater and a settling stage to separate the biologiscal sludge from the water.", "bacteria_min": 1.0, "bacteria_max": 2.0, "viruses_min": 0.5, "viruses_max": 2.0, "protozoa_min": 0.5, "protozoa_max": 2.0, "bacteria_references": ["51"], "viruses_references": ["51"], "protozoa_references": ["51"]}, "Dual media filtration": {"id": 18, "name": "Dual media filtration", "group": "Filtration", "description": "Water is filtered through a fixed bed consisting of two layers of granular media (e.g. antracite and sand) generally operatied down flow with rates of 5 to 20 m/h and contact times of 4 to 15 minutes. They are regularly backwashed to remove built up solids in the filter. Log removal depends on filter media and coagulation pretreatment;consistent low filtered water turbidity of ? 0.3 NTU (none to exceed 1 NTU)\r\n are associated higher log removal of pathogens", "bacteria_min": 0.0, "bacteria_max": 1.0, "viruses_min": 0.5, "viruses_max": 2.0, "protozoa_min": 1.5, "protozoa_max": 2.5, "bacteria_references": ["8"], "viruses_references": ["8"], "protozoa_references": ["8"]}, "Reverse osmosis": {"id": 21, "name": "Reverse osmosis", "group": "Filtration", "description": "A reverse osmosis membrane is a thin sheet with small openings that removes solids and most soluble molecules, including salts (< 0,004 \u043f\u0457\u0405m depending on selected membrane) from the water when this is led through the membrane. It can take the form of spiral wound membranes, hollow fibers or sheets. Actual log reduction depends on the selected membrane and is determined by challenge testing.", "bacteria_min": 5.0, "bacteria_max": 6.0, "viruses_min": 5.0, "viruses_max": 6.0, "protozoa_min": 5.0, "protozoa_max": 6.0, "bacteria_references": ["47"], "viruses_references": ["47"], "protozoa_references": ["47"]}, "Wetlands, surface flow": {"id": 23, "name": "Wetlands, surface flow", "group": "Wetlands", "description": "An artificial wetland to treat municipal or industrial wastewater, greywater or stormwater runoff by a combination of sedimentation and biological processes including plants. Effect depends on design and climate, especially les log reduction at lower temperatures.", "bacteria_min": 1.5, "bacteria_max": 2.5, "viruses_min": null, "viruses_max": null, "protozoa_min": 0.5, "protozoa_max": 1.5, "bacteria_references": ["8"], "viruses_references": [], "protozoa_references": ["8"]}, "Wetlands, subsurface flow": {"id": 24, "name": "Wetlands, subsurface flow", "group": "Wetlands", "description": "An artificial wetland to treat municipal or industrial wastewater, greywater or stormwater runoff by a combination of sedimentation, filtration and biological processes including plants. Effect depends on design, soil/filter media and climate, especially les log reduction at lower temperatures.", "bacteria_min": 0.5, "bacteria_max": 3.0, "viruses_min": null, "viruses_max": null, "protozoa_min": 0.5, "protozoa_max": 2.0, "bacteria_references": ["8"], "viruses_references": [], "protozoa_references": ["8"]}, "UV disinfection, wastewater": {"id": 25, "name": "UV disinfection, wastewater", "group": "Primary disinfection", "description": "UV-light is mostly effective at 254 nm where it affects DNA or RNA thus preventing reproduction of the organism (inactivation). Effectiveness of disinfection depends on delivered fluence (dose in mJ/cm2), which varies with lamp intensity, exposure time (flow rate) and UV-absorption by the water (organics). Wastewater UV-reactors are generally open-channel reactors in which UV lamps are placed. Excessive turbidity and certain dissolved species inhibit this process; hence the effect in wastewater highly depends on the water quality an is generally lower than in drinking water at the same dose.", "bacteria_min": 2.0, "bacteria_max": 4.0, "viruses_min": 1.0, "viruses_max": 3.0, "protozoa_min": 3.0, "protozoa_max": 3.0, "bacteria_references": ["8"], "viruses_references": ["8"], "protozoa_references": ["8"]}, "Microfiltration": {"id": 26, "name": "Microfiltration", "group": "Filtration", "description": "A microfiltration membrane is a thin sheet with small openings that removes solids (0.1-10 \u043f\u0457\u0405m depending on selected membrane) from the water when this is led through the membrane. It can take the form of capilary tubes, hollow fibers or sheet membranes. Actual log reduction depends on the selected membrane and is determined by challenge testing.", "bacteria_min": 0.0, "bacteria_max": 4.3, "viruses_min": 0.0, "viruses_max": 3.7, "protozoa_min": 2.3, "protozoa_max": 6.0, "bacteria_references": ["46"], "viruses_references": ["46"], "protozoa_references": ["46"]}, "Ultrafiltration (module certification)": {"id": 27, "name": "Ultrafiltration (module certification)", "group": "Filtration", "description": "An ultrafiltration membrane is a thin sheet with small openings that removes solids (0.005-0,2 \u043f\u0457\u0405m depending on selected membrane) from the water when this is led through the membrane. It can take the form of capilary tubes, hollow fibers, spiral wound or sheet membranes. Actual log reduction depends on the selected membrane and is determined by challenge testing.", "bacteria_min": 5.5, "bacteria_max": 6.0, "viruses_min": 1.1, "viruses_max": 5.5, "protozoa_min": 0.8, "protozoa_max": 6.0, "bacteria_references": ["47"], "viruses_references": ["47"], "protozoa_references": ["47"]}, "Nanofiltration": {"id": 28, "name": "Nanofiltration", "group": "Filtration", "description": "An nanofiltration membrane is a thin sheet with small openings that removes solids and larger soluble molecules (0.001-0,03 \u043f\u0457\u0405m depending on selected membrane) from the water when this is led through the membrane. It can take the form of spiral wound or hollow fiber membranes. Actual log reduction depends on the selected membrane and is determined by challenge testing.", "bacteria_min": 5.0, "bacteria_max": 6.0, "viruses_min": 5.0, "viruses_max": 6.0, "protozoa_min": 5.0, "protozoa_max": 6.0, "bacteria_references": ["47"], "viruses_references": ["47"], "protozoa_references": ["47"]}, "UV disinfection 40 mJ/cm2, drinking": {"id": 29, "name": "UV disinfection 40 mJ/cm2, drinking", "group": "Primary disinfection", "description": "UV-light is mostly effective at 254 nm where it affects DNA or RNA thus preventing reproduction of the organism (inactivation). Log reduction for drinking water UV is based on closed UV-reactors wich have been validated according to appropriate standards (e.g. USEPA or DVGW). Effectiveness of disinfection depends on delivered fluence (dose in mJ/cm2), which varies with lamp intensity, exposure time (flow rate) and UV-absorption by the water (organics). Excessive turbidity and certain dissolved species inhibit this process; hence, turbidity should be kept below 1 NTU to support effective disinfection.", "bacteria_min": 4.6, "bacteria_max": 6.0, "viruses_min": 4.1, "viruses_max": 5.9, "protozoa_min": 2.5, "protozoa_max": 3.0, "bacteria_references": ["50"], "viruses_references": ["50"], "protozoa_references": ["50"]}, "Soil aquifer treatment": {"id": 30, "name": "Soil aquifer treatment", "group": "Pretreatment", "description": "", "bacteria_min": 0.0, "bacteria_max": 6.0, "viruses_min": 0.0, "viruses_max": 6.0, "protozoa_min": 0.0, "protozoa_max": 6.0, "bacteria_references": ["51"], "viruses_references": ["51"], "protozoa_references": ["51"]}, "Chlorination": {"id": 31, "name": "Chlorination", "group": "Disinfection", "description": "", "bacteria_min": 2.0, "bacteria_max": 6.0, "viruses_min": 2.0, "viruses_max": 6.0, "protozoa_min": 0.0, "protozoa_max": 2.0, "bacteria_references": ["51"], "viruses_references": ["51"], "protozoa_references": ["51"]}, "Coagulation, flocculation and media filtration": {"id": 32, "name": "Coagulation, flocculation and media filtration", "group": "Clarification", "description": "", "bacteria_min": 1.0, "bacteria_max": 4.0, "viruses_min": 1.0, "viruses_max": 2.0, "protozoa_min": 2.5, "protozoa_max": 4.0, "bacteria_references": ["51"], "viruses_references": ["51"], "protozoa_references": ["51"]}, "Reverse Osmosis (RO, Australian Guidelines)": {"id": 33, "name": "Reverse Osmosis (RO, Australian Guidelines)", "group": "Filtration", "description": "", "bacteria_min": 1.5, "bacteria_max": 6.0, "viruses_min": 1.5, "viruses_max": 6.0, "protozoa_min": 1.5, "protozoa_max": 6.0, "bacteria_references": ["51"], "viruses_references": ["51"], "protozoa_references": ["51"]}, "Ozonation for disinfection >1mgO3/mgDOC": {"id": 34, "name": "Ozonation for disinfection >1mgO3/mgDOC", "group": "Disinfection", "description": "", "bacteria_min": 2.0, "bacteria_max": 4.0, "viruses_min": 2.0, "viruses_max": 4.0, "protozoa_min": 2.0, "protozoa_max": 3.0, "bacteria_references": ["40"], "viruses_references": ["40"], "protozoa_references": ["40"]}, "Ozonation for organic micropollutant removal (0.4-0.6 mgO3/mgDOC)": {"id": 35, "name": "Ozonation for organic micropollutant removal (0.4-0.6 mgO3/mgDOC)", "group": "Organic micropollutant removal", "description": "", "bacteria_min": 1.0, "bacteria_max": 3.0, "viruses_min": 1.0, "viruses_max": 2.5, "protozoa_min": 0.0, "protozoa_max": 1.0, "bacteria_references": ["52"], "viruses_references": ["52"], "protozoa_references": ["52"]}, "UV AOP": {"id": 36, "name": "UV AOP", "group": "Disinfection", "description": "", "bacteria_min": 6.0, "bacteria_max": 9.0, "viruses_min": 6.0, "viruses_max": 6.0, "protozoa_min": 6.0, "protozoa_max": 6.0, "bacteria_references": ["48", "53"], "viruses_references": ["49", "53"], "protozoa_references": ["53"]}} \ No newline at end of file +{"Conventional clarification": {"id": 1, "name": "Conventional clarification", "group": "Coagulation, flocculation and sedimentation", "description": "Consists of coagulant and/or flocculant aid (e.g. polymer) dosing, rapid mixing, slow mixing and sedimentation. Log removal depends on process optimisation. Rapid changes in source water quality such as turbidity increase due to monsoon rainfall or algeal blooms may decrease treatment effect and require adjustment of process settings.", "bacteria_min": 0.2, "bacteria_max": 2.0, "bacteria_reference": "40", "protozoa_min": 1.0, "protozoa_max": 2.0, "protozoa_reference": "40", "viruses_min": 0.1, "viruses_max": 3.4, "viruses_reference": "40", "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "High-rate clarification": {"id": 3, "name": "High-rate clarification", "group": "Coagulation, flocculation and sedimentation", "description": "Consists of coagulant and/or flocculant aid (e.g. polymer) dosing, mixing and enhanced sedimentation by flock blankets, lamellae- or tube settlers. Log removal depends on process optimisation. Rapid changes in source water quality such as turbidity increase due to monsoon rainfall or algeal blooms may decrease treatment effect and require adjustment of process settings.", "bacteria_min": null, "bacteria_max": null, "bacteria_reference": null, "protozoa_min": 2.0, "protozoa_max": 2.8, "protozoa_reference": "40", "viruses_min": null, "viruses_max": null, "viruses_reference": null, "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "Slow sand filtration": {"id": 8, "name": "Slow sand filtration", "group": "Filtration", "description": "Water is filtered through a fixed bed sand operatied down flow with rates of 0.1 to 1 m/h and contact times of 3 to 6 hours. The filter is not backwashed. In weeks to months a 'schmutzdecke' will develop on the filter which enhances log removal. Grain size, flow rate and temperature also affect log removal. Consistent low filtered water turbidity of ? 0.3 NTU (none to exceed 1 NTU) are associated higher log removal of pathogens\r\n\r\nassociated with 1 - 2 log reduction of viruses and 2.5 - 3 log reduction of Cryptosporidiuma", "bacteria_min": 2.0, "bacteria_max": 6.0, "bacteria_reference": "40", "protozoa_min": 0.3, "protozoa_max": 5.0, "protozoa_reference": "40", "viruses_min": 0.25, "viruses_max": 4.0, "viruses_reference": "40", "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "Bank filtration": {"id": 9, "name": "Bank filtration", "group": "Pretreatment", "description": "Water is abstracted through wells located close to surface water, thus the bank serves as a natural filter. Log removal depends on travel distance and time, soil type (grain size),\r\n and geochemicl conditions (oxygen level, pH)", "bacteria_min": 2.0, "bacteria_max": 6.0, "bacteria_reference": "40", "protozoa_min": 1.0, "protozoa_max": 2.0, "protozoa_reference": "40", "viruses_min": 2.1, "viruses_max": 8.3, "viruses_reference": "40", "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "Storage reservoirs": {"id": 11, "name": "Storage reservoirs", "group": "Pretreatment", "description": "Water is protected from human recontamination in reservoirs, however wildlife and waterfoul may introduce pathogens. Log reduction occurs due to sedimentation, UV radiation from sunlight and die-off in time, depending on construction (mixing) and temperature. Reporded reduction based on residence time > 40 days (bacteria), 160 days (protozoa)", "bacteria_min": 0.7, "bacteria_max": 2.2, "bacteria_reference": "40", "protozoa_min": 1.4, "protozoa_max": 2.3, "protozoa_reference": "40", "viruses_min": null, "viruses_max": null, "viruses_reference": null, "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "Chlorination, wastewater": {"id": 12, "name": "Chlorination, wastewater", "group": "Primary disinfection", "description": "Log inactivation depends on free chlorine concentration and contact time (CT); not effective against Cryptosporidium oocysts, reported protozoan log reduction is mostly for Giardia. Turbidity and chlorine-demanding solutes inhibit this process; hence, effect in wastewater is limited since free chlorine will rapidly decay. \r\n\r\nEffective disinfection. Where this is not practical, turbidities should be kept below 5 NTU with higher chlorine doses or contact times. In addition to initial disinfection, the benefits of maintaining free chlorine residuals throughout distribution systems at or above 0.2 mg/l should be considered", "bacteria_min": 2.0, "bacteria_max": 2.0, "bacteria_reference": "40", "protozoa_min": 2.0, "protozoa_max": 2.0, "protozoa_reference": "40", "viruses_min": 2.0, "viruses_max": 2.0, "viruses_reference": "40", "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "Chlorine dioxide": {"id": 13, "name": "Chlorine dioxide", "group": "Primary disinfection", "description": "Log inactivation depends on chlorine dioxide concentration and contact time (CT); Turbidity and organics inhibit this process; hence, turbidity should be kept below 1 NTU to support\r\n effective disinfection Chlorine dioxide degrades rapidly and doesn't leave a disinfectand residual for distribution.", "bacteria_min": 2.0, "bacteria_max": 2.0, "bacteria_reference": "40", "protozoa_min": 2.0, "protozoa_max": 2.0, "protozoa_reference": "40", "viruses_min": 2.0, "viruses_max": 2.0, "viruses_reference": "40", "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "Ozonation, drinking water": {"id": 14, "name": "Ozonation, drinking water", "group": "Primary disinfection", "description": "Log inactivation depends on dissolved ozone concentration and contact time (CT); Turbidity and organics inhibit this process; hence, turbidity should be kept below 1 NTU to support\r\n\r\n effective disinfection. Ozone degrades rapidly and doesn't leave a disinfectand residual for distribution. Effective mixing and consistent contact time are crucial for disinfection due to the rapid degradation of ozone.\r\n\r\nCryptosporidium varies widely", "bacteria_min": 2.0, "bacteria_max": 2.0, "bacteria_reference": "40", "protozoa_min": 2.0, "protozoa_max": 2.0, "protozoa_reference": "40", "viruses_min": 2.0, "viruses_max": 2.0, "viruses_reference": "40", "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "UV disinfection 20 mJ/cm2, drinking": {"id": 15, "name": "UV disinfection 20 mJ/cm2, drinking", "group": "Primary disinfection", "description": "UV-light is mostly effective at 254 nm where it affects DNA or RNA thus preventing reproduction of the organism (inactivation). Log reduction for drinking water UV is based on closed UV-reactors wich have been validated according to appropriate standards (e.g. USEPA or DVGW). Effectiveness of disinfection depends on delivered fluence (dose in mJ/cm2), which varies with lamp intensity, exposure time (flow rate) and UV-absorption by the water (organics). Excessive turbidity and certain dissolved species inhibit this process; hence, turbidity should be kept below 1 NTU to support effective disinfection.", "bacteria_min": 4.6, "bacteria_max": 6.0, "bacteria_reference": "50", "protozoa_min": 2.4, "protozoa_max": 3.0, "protozoa_reference": "50", "viruses_min": 2.0, "viruses_max": 3.1, "viruses_reference": "50", "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "Primary treatment": {"id": 16, "name": "Primary treatment", "group": "Pretreatment", "description": "Primary treatment consists of temporarily holding the sewage in a quiescent basin where heavy solids can settle to the bottom while oil, grease and lighter solids float to the surface. The settled and floating materials are removed and the remaining liquid may be discharged or subjected to secondary treatment", "bacteria_min": 0.0, "bacteria_max": 0.5, "bacteria_reference": "8", "protozoa_min": 0.0, "protozoa_max": 1.0, "protozoa_reference": "8", "viruses_min": 0.0, "viruses_max": 0.1, "viruses_reference": "8", "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "Secondary treatment": {"id": 17, "name": "Secondary treatment", "group": "Pretreatment", "description": "Secondary treatment consists of an activated sludge process to break down organics in the wastewater and a settling stage to separate the biologiscal sludge from the water.", "bacteria_min": 1.0, "bacteria_max": 3.0, "bacteria_reference": "8", "protozoa_min": 0.5, "protozoa_max": 1.5, "protozoa_reference": "8", "viruses_min": 0.5, "viruses_max": 2.0, "viruses_reference": "8", "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "Dual media filtration": {"id": 18, "name": "Dual media filtration", "group": "Filtration", "description": "Water is filtered through a fixed bed consisting of two layers of granular media (e.g. antracite and sand) generally operatied down flow with rates of 5 to 20 m/h and contact times of 4 to 15 minutes. They are regularly backwashed to remove built up solids in the filter. Log removal depends on filter media and coagulation pretreatment;consistent low filtered water turbidity of ? 0.3 NTU (none to exceed 1 NTU)\r\n are associated higher log removal of pathogens", "bacteria_min": 0.0, "bacteria_max": 1.0, "bacteria_reference": "8", "protozoa_min": 1.5, "protozoa_max": 2.5, "protozoa_reference": "8", "viruses_min": 0.5, "viruses_max": 3.0, "viruses_reference": "8", "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "Chlorination, drinking water": {"id": 20, "name": "Chlorination, drinking water", "group": "Primary disinfection", "description": "Log inactivation depends on free chlorine concentration and contact time (CT); not effective against Cryptosporidium oocysts, reported log reduction is mostly for Giardia. Turbidity and chlorine-demanding solutes inhibit this process; hence, turbidity should be kept below 1 NTU to support\r\n effective disinfection. Where this is not practical, turbidities should be kept below 5 NTU with higher chlorine doses or contact times. In addition to initial disinfection, the benefits of maintaining free chlorine residuals throughout distribution systems at or above 0.2 mg/l should be considered", "bacteria_min": 2.0, "bacteria_max": 6.0, "bacteria_reference": "8", "protozoa_min": 0.0, "protozoa_max": 1.5, "protozoa_reference": "8", "viruses_min": 1.0, "viruses_max": 3.0, "viruses_reference": "8", "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "Reverse osmosis": {"id": 21, "name": "Reverse osmosis", "group": "Filtration", "description": "A reverse osmosis membrane is a thin sheet with small openings that removes solids and most soluble molecules, including salts (< 0,004 \u043f\u0457\u0405m depending on selected membrane) from the water when this is led through the membrane. It can take the form of spiral wound membranes, hollow fibers or sheets. Actual log reduction depends on the selected membrane and is determined by challenge testing.", "bacteria_min": 5.44, "bacteria_max": 6.0, "bacteria_reference": "47", "protozoa_min": 5.75, "protozoa_max": 6.32, "protozoa_reference": "47", "viruses_min": 5.44, "viruses_max": 6.0, "viruses_reference": "47", "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "Ozonation, wastewater": {"id": 22, "name": "Ozonation, wastewater", "group": "Primary disinfection", "description": "Log inactivation depends on dissolved ozone concentration and contact time (CT); Turbidity and organics inhibit this process; Since wastewater is often turbidity and contains high organics that consume ozone, the actual CT cannot be determined accurately and therefore inactivation cannot be determined accurately. Still, effective mixing and consistent contact time are crucial for disinfection due to the rapid degradation of ozone.", "bacteria_min": 2.0, "bacteria_max": 6.0, "bacteria_reference": "8", "protozoa_min": null, "protozoa_max": null, "protozoa_reference": null, "viruses_min": 3.0, "viruses_max": 6.0, "viruses_reference": "8", "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "Wetlands, surface flow": {"id": 23, "name": "Wetlands, surface flow", "group": "Wetlands", "description": "An artificial wetland to treat municipal or industrial wastewater, greywater or stormwater runoff by a combination of sedimentation and biological processes including plants. Effect depends on design and climate, especially les log reduction at lower temperatures.", "bacteria_min": 1.5, "bacteria_max": 2.5, "bacteria_reference": "8", "protozoa_min": 0.5, "protozoa_max": 1.5, "protozoa_reference": "8", "viruses_min": null, "viruses_max": null, "viruses_reference": null, "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "Wetlands, subsurface flow": {"id": 24, "name": "Wetlands, subsurface flow", "group": "Wetlands", "description": "An artificial wetland to treat municipal or industrial wastewater, greywater or stormwater runoff by a combination of sedimentation, filtration and biological processes including plants. Effect depends on design, soil/filter media and climate, especially les log reduction at lower temperatures.", "bacteria_min": 0.5, "bacteria_max": 3.0, "bacteria_reference": "8", "protozoa_min": 0.5, "protozoa_max": 2.0, "protozoa_reference": "8", "viruses_min": null, "viruses_max": null, "viruses_reference": null, "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "UV disinfection, wastewater": {"id": 25, "name": "UV disinfection, wastewater", "group": "Primary disinfection", "description": "UV-light is mostly effective at 254 nm where it affects DNA or RNA thus preventing reproduction of the organism (inactivation). Effectiveness of disinfection depends on delivered fluence (dose in mJ/cm2), which varies with lamp intensity, exposure time (flow rate) and UV-absorption by the water (organics). Wastewater UV-reactors are generally open-channel reactors in which UV lamps are placed. Excessive turbidity and certain dissolved species inhibit this process; hence the effect in wastewater highly depends on the water quality an is generally lower than in drinking water at the same dose.", "bacteria_min": 2.0, "bacteria_max": 4.0, "bacteria_reference": "8", "protozoa_min": 3.0, "protozoa_max": 3.0, "protozoa_reference": "8", "viruses_min": 1.0, "viruses_max": 3.0, "viruses_reference": "8", "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "Microfiltration": {"id": 26, "name": "Microfiltration", "group": "Filtration", "description": "A microfiltration membrane is a thin sheet with small openings that removes solids (0.1-10 \u043f\u0457\u0405m depending on selected membrane) from the water when this is led through the membrane. It can take the form of capilary tubes, hollow fibers or sheet membranes. Actual log reduction depends on the selected membrane and is determined by challenge testing.", "bacteria_min": 0.0, "bacteria_max": 4.3, "bacteria_reference": "46", "protozoa_min": 2.3, "protozoa_max": 6.0, "protozoa_reference": "46", "viruses_min": 0.0, "viruses_max": 3.7, "viruses_reference": "46", "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "Ultrafiltration": {"id": 27, "name": "Ultrafiltration", "group": "Filtration", "description": "An ultrafiltration membrane is a thin sheet with small openings that removes solids (0.005-0,2 \u043f\u0457\u0405m depending on selected membrane) from the water when this is led through the membrane. It can take the form of capilary tubes, hollow fibers, spiral wound or sheet membranes. Actual log reduction depends on the selected membrane and is determined by challenge testing.", "bacteria_min": 5.5, "bacteria_max": 6.0, "bacteria_reference": "47", "protozoa_min": 5.3, "protozoa_max": 6.5, "protozoa_reference": "47", "viruses_min": 2.69, "viruses_max": 5.14, "viruses_reference": "47", "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "Nanofiltration": {"id": 28, "name": "Nanofiltration", "group": "Filtration", "description": "An nanofiltration membrane is a thin sheet with small openings that removes solids and larger soluble molecules (0.001-0,03 \u043f\u0457\u0405m depending on selected membrane) from the water when this is led through the membrane. It can take the form of spiral wound or hollow fiber membranes. Actual log reduction depends on the selected membrane and is determined by challenge testing.", "bacteria_min": 5.44, "bacteria_max": 6.0, "bacteria_reference": "47", "protozoa_min": 5.75, "protozoa_max": 6.32, "protozoa_reference": "47", "viruses_min": 5.44, "viruses_max": 6.0, "viruses_reference": "47", "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}, "UV disinfection 40 mJ/cm2, drinking": {"id": 29, "name": "UV disinfection 40 mJ/cm2, drinking", "group": "Primary disinfection", "description": "UV-light is mostly effective at 254 nm where it affects DNA or RNA thus preventing reproduction of the organism (inactivation). Log reduction for drinking water UV is based on closed UV-reactors wich have been validated according to appropriate standards (e.g. USEPA or DVGW). Effectiveness of disinfection depends on delivered fluence (dose in mJ/cm2), which varies with lamp intensity, exposure time (flow rate) and UV-absorption by the water (organics). Excessive turbidity and certain dissolved species inhibit this process; hence, turbidity should be kept below 1 NTU to support effective disinfection.", "bacteria_min": 4.6, "bacteria_max": 6.0, "bacteria_reference": "50", "protozoa_min": 2.5, "protozoa_max": 3.0, "protozoa_reference": "50", "viruses_min": 4.1, "viruses_max": 5.9, "viruses_reference": "50", "failure_duration_minutes": 30, "failure_frequency_days_per_year": 0}} \ No newline at end of file
    Pathogen group