├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── build-n-publish.yml ├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.md ├── archABM ├── __init__.py ├── actions.py ├── aerosol_model.py ├── aerosol_model_colorado.py ├── aerosol_model_maxplanck.py ├── aerosol_model_mit.py ├── config.json ├── config.py ├── creator.py ├── database.py ├── engine.py ├── event.py ├── event_generator.py ├── event_model.py ├── options.py ├── parameters.py ├── person.py ├── place.py ├── results.py ├── schema.json ├── snapshot.py ├── snapshot_person.py └── snapshot_place.py ├── data ├── config.json ├── config_basic.json └── config_toy.json ├── designer ├── README.md ├── package.json ├── public │ ├── favicon.ico │ ├── favicon.png │ ├── index.html │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── robots.txt └── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── config.json │ ├── config_basic.js │ ├── config_basic.json │ ├── config_office.js │ ├── index.css │ ├── index.js │ ├── logo.svg │ ├── reportWebVitals.js │ └── setupTests.js ├── docs ├── Makefile ├── index.html ├── make.bat └── source │ ├── _static │ ├── command.png │ ├── favicon.png │ ├── figures │ │ ├── architecture.png │ │ ├── boxplot_place_CO2.png │ │ ├── boxplot_place_quanta.png │ │ ├── distribution_person_quanta.png │ │ ├── floorplan.png │ │ ├── floorplan_CO2.png │ │ ├── floorplan_quanta.png │ │ ├── performance.png │ │ ├── table_1.png │ │ ├── table_2.png │ │ ├── table_3.png │ │ ├── table_4.png │ │ ├── timeline_activity_density.png │ │ ├── timeline_activity_person.png │ │ ├── timeline_person_quanta.png │ │ ├── timeline_place_CO2.png │ │ └── timeline_place_quanta.png │ ├── index.html │ ├── logo_1.png │ ├── logo_2.png │ ├── logo_3.png │ ├── logo_4.png │ ├── main.1f09b65e.css │ ├── main.83d82daa.js │ ├── manifest.json │ ├── my_theme.css │ ├── schedule.png │ ├── vicomtech_logo.png │ └── vicomtech_logo.svg │ ├── api.rst │ ├── archABM │ ├── actions.rst │ ├── aerosol_model.rst │ ├── creator.rst │ ├── database.rst │ ├── engine.rst │ ├── event.rst │ ├── options.rst │ ├── parameters.rst │ ├── person.rst │ ├── place.rst │ ├── results.rst │ ├── schema.rst │ └── snapshot.rst │ ├── authors.rst │ ├── changelog.rst │ ├── citing.rst │ ├── conf.py │ ├── contents.rst │ ├── designer.rst │ ├── example.rst │ ├── framework.rst │ ├── index.rst │ ├── license.rst │ └── references.bib ├── experiments ├── config_0.json ├── config_1.json ├── config_2.json ├── config_3.json ├── config_4.json ├── config_5.json ├── config_6.json ├── config_7.json ├── config_8.json ├── config_performance.json ├── config_validation.json ├── performance.csv └── performance.py ├── main.py ├── main_manual.py ├── models ├── 2020_COVID-19_Aerosol_Transmission_Estimator.xlsx ├── 2021.04.21.21255898v2.full.pdf ├── BEST.pdf ├── COVID-19_Indoor_Safety_Guideline_PNAS_with_CO2_final.xlsx ├── acs.estlett.1c00183.pdf ├── ijerph-17-08114-s001.xlsx ├── ijerph-17-08114-v3.pdf ├── ina.12751.pdf └── thermal_model.ods ├── pyproject.toml ├── requirements.txt ├── requirements_dev.txt ├── requirements_docs.txt ├── run.py ├── setup.cfg ├── setup.py ├── tests.py └── visualization ├── analysis.py ├── analysis_comparison_v1.R ├── analysis_comparison_v2.R ├── analysis_single_v1.R ├── analysis_single_v2.R ├── floorplan ├── floorplan.pdf ├── floorplan.svg ├── floorplan_CO2.pdf ├── floorplan_CO2.svg ├── floorplan_backup.svg ├── floorplan_example_1.svg ├── floorplan_example_2.svg ├── floorplan_example_3.svg ├── floorplan_example_4.svg ├── floorplan_heatmap.svg ├── floorplan_legend_CO2.pdf ├── floorplan_legend_quanta.pdf ├── floorplan_quanta.pdf └── floorplan_quanta.svg ├── performance.R ├── schedule ├── legend.pdf ├── legend.png └── legend.svg └── validation ├── Occupancy-detection-data-master ├── Correlation_Plot.png ├── Model Development.R ├── README.md ├── VarImp_modelRF_All.png ├── datatest.txt ├── datatest2.txt ├── datatraining.txt ├── pairs_plot_green_blue_time.png └── varimportance_no_light.png ├── validation.R └── validation.pdf /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/build-n-publish.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Build & publish to Pypi 4 | 5 | # Controls when the workflow will run 6 | on: 7 | # Triggers the workflow on pushing a tag to the main branch 8 | push: 9 | tags: 10 | - 'v*' 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | jobs: 16 | prepare: 17 | runs-on: ubuntu-latest 18 | steps: 19 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 20 | - uses: actions/checkout@v2 21 | with: 22 | fetch-depth: 0 # Fetch all history to be able to obtain branch of tag 23 | 24 | - id: branch 25 | name: Get branch of tag 26 | run: echo "::set-output name=BRANCH::$(git branch -a --contains ${{ github.ref }} | grep -v HEAD | cut -d '/' -f3)" 27 | 28 | outputs: 29 | branch_name: ${{ steps.branch.outputs.BRANCH }} 30 | 31 | build-n-publish: 32 | name: Build and publish Python 🐍 distributions 📦 to PyPI and TestPyPI 33 | runs-on: ubuntu-latest 34 | needs: prepare 35 | if: ${{ needs.prepare.outputs.branch_name }} == 'main' 36 | 37 | # strategy: 38 | # fail-fast: false 39 | # matrix: 40 | # python-version: [3.7, 3.8, 3.9] 41 | 42 | steps: 43 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 44 | - uses: actions/checkout@v2 45 | 46 | - name: Set up Python 47 | uses: actions/setup-python@v2 48 | with: 49 | python-version: 3.7 50 | 51 | - name: Install dependencies 52 | run: | 53 | # Upgrade pip 54 | python -m pip install --upgrade pip 55 | # Install build deps 56 | python -m pip install flake8 pytest build 57 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 58 | 59 | # - name: Lint with flake8 60 | # run: | 61 | # pip install flake8 62 | # # stop the build if there are Python syntax errors or undefined names 63 | # flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 64 | # # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 65 | # flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 66 | 67 | # - name: Test with pytest 68 | # run: | 69 | # pip install pytest pytest-cov 70 | # pytest tests.py --doctest-modules --junitxml=junit/test-results-${{ matrix.python-version }}.xml --cov=com --cov-report=xml --cov-report=html 71 | 72 | # - name: Upload pytest test results 73 | # uses: actions/upload-artifact@v2 74 | # with: 75 | # name: pytest-results-${{ matrix.python-version }} 76 | # path: junit/test-results-${{ matrix.python-version }}.xml 77 | # # Use always() to always run this step to publish test results when there are test failures 78 | # if: ${{ always() }} 79 | 80 | - name: Build a binary wheel and a source tarball 81 | run: python -m build --sdist --wheel --outdir dist/ . 82 | 83 | # - name: Create a Release 84 | # uses: softprops/action-gh-release@v1 85 | # with: 86 | # name: ArchABM-${{ github.ref }} 87 | # env: 88 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 89 | 90 | - name: Publish distribution 📦 to PyPI 91 | uses: pypa/gh-action-pypi-publish@release/v1 92 | with: 93 | user: __token__ 94 | password: ${{ secrets.PYPI_API_TOKEN }} 95 | 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .Rproj.user 3 | results 4 | docs/build 5 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Vicomtech - Data Intelligence for Energy & Industrial Processes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include archABM/schema.json 2 | include archABM/config.json -------------------------------------------------------------------------------- /archABM/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/archABM/__init__.py -------------------------------------------------------------------------------- /archABM/actions.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | from typing import List 4 | 5 | from simpy import Environment 6 | 7 | from .database import Database 8 | from .event import Event 9 | from .event_model import EventModel 10 | from .person import Person 11 | from .place import Place 12 | 13 | 14 | class Actions: 15 | """Actions applied to any asset in :class:`~archABM.database.Database`""" 16 | 17 | env: Environment 18 | db: Database 19 | 20 | def __init__(self, env: Environment, db: Database) -> None: 21 | self.env = env 22 | self.db = db 23 | 24 | def find_place(self, model: EventModel, person: Person) -> None: 25 | """Find a place to carry out a certain :class:`~archABM.event_model.EventModel`. 26 | 27 | The selected place must share the same activity as the indicated :class:`~archABM.event_model.EventModel`. 28 | Places that are open (``allow = True``) and not full (``num_people < capacity``) are only considered as valid. 29 | 30 | .. note:: 31 | Movement restrictions (between buildings and between departments) are also considered. 32 | 33 | .. attention:: 34 | The list of places is shuffled after each search procedure. 35 | 36 | Args: 37 | model (EventModel): activity model to which find a place 38 | person (Person): person invoking the place search 39 | 40 | Returns: 41 | Place: selected place 42 | """ 43 | places = self.db.places 44 | random.shuffle(places) 45 | for place in places: 46 | if model.params.activity in place.params.activity: 47 | # check if place is allowed and not full 48 | if place.params.allow and not place.full(): 49 | # check if movement between buildings is not allowed 50 | if not self.db.options.params.movement_buildings: 51 | # check if person has a place (initial condition) 52 | if person.place is not None: 53 | # check if person has building defined 54 | if person.params.building is not None: 55 | # check if place has building defined 56 | if place.params.building is not None: 57 | if person.params.building != place.params.building: 58 | continue 59 | 60 | if not self.db.options.params.movement_department: 61 | # check if person has a place (initial condition) 62 | if person.place is not None: 63 | # check if person has department defined 64 | if person.params.department is not None: 65 | # check if place has department defined 66 | if place.params.department is not None: 67 | if person.params.department not in place.params.department: 68 | continue 69 | 70 | return place 71 | return None 72 | 73 | @staticmethod 74 | def create_event(model: EventModel, place: Place, duration: int) -> Event: 75 | """Wrapper to create an :class:`~archABM.event.Event` 76 | 77 | Args: 78 | model (EventModel): type of event or activity 79 | place (Place): physical location of the event 80 | duration (int): time duration in minutes 81 | 82 | Returns: 83 | Event: created :class:`~archABM.event.Event` instance 84 | """ 85 | return Event(model, place, duration) 86 | 87 | @staticmethod 88 | def assign_event(event: Event, people: List[Person]) -> None: 89 | """Assign an event to certain people 90 | 91 | Args: 92 | event (Event): :class:`~archABM.event.Event` instance to be assigned 93 | people (List[Person]): list of people to be interrupted from their current events 94 | """ 95 | for person in people: 96 | person.assign_event(event) 97 | 98 | def create_collective_event(self, model: EventModel, place: Place, duration: int, person: Person) -> None: 99 | """Creates a collective event of certain type :class:`~archABM.event_model.EventModel` at a physical location :class:`~archABM.place.Place` and for a time duration in minutes. 100 | 101 | .. important:: 102 | The number of people called into the collective event is a random integer between 103 | the current number of people at that place and the total place capacity. 104 | 105 | .. note:: 106 | Movement restrictions (between buildings and between departments) are also considered to select the people called into the collective event. 107 | 108 | Args: 109 | model (EventModel): type of event or activity 110 | place (Place): physical location of the event 111 | duration (int): time duration in minutes 112 | person (Person): person invoking the collective event 113 | 114 | Returns: 115 | Event: generated collective event 116 | """ 117 | # create event 118 | event = self.create_event(model, place, duration) 119 | 120 | # select people 121 | people = self.db.people 122 | # from the same building if option applied 123 | if not self.db.options.params.movement_buildings: 124 | building = event.place.params.building 125 | if building is not None: 126 | people_filter = [] 127 | for p in people: 128 | if p.place is not None: 129 | if p.place.params.building is None or p.place.params.building == building: 130 | people_filter.append(p) 131 | people = people_filter 132 | 133 | # from the same department if option applied 134 | if not self.db.options.params.movement_department: 135 | department = event.place.params.department 136 | if department is not None: 137 | people_filter = [] 138 | for p in people: 139 | if p.params.department is None or p.params.department in department: 140 | people_filter.append(p) 141 | people = people_filter 142 | 143 | if len(people) > 1: 144 | num_people = place.people_attending() 145 | num_people = min(len(people), num_people) 146 | people = random.sample(people, k=num_people) 147 | people = [p for p in people if p.generator.valid_activity(model) and p.model != model] 148 | 149 | logging.info( 150 | "[%.2f] Person %d invoked collective event %s at place %s for %d minutes for %d people" 151 | % (self.env.now, person.id, model.params.activity, place.params.name, duration, len(people),) 152 | ) 153 | 154 | self.assign_event(event, people) 155 | return event 156 | -------------------------------------------------------------------------------- /archABM/aerosol_model.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | from .parameters import Parameters 3 | 4 | 5 | class AerosolModel: 6 | """Aerosol transmission estimator""" 7 | 8 | name: str 9 | params: Parameters 10 | 11 | def __init__(self, params: Parameters): 12 | self.params = params 13 | 14 | def get_risk(self, inputs: Parameters) -> Tuple[float, float]: 15 | """Calculate the infection risk of an individual in a room 16 | and the CO\ :sub:`2` thrown into the air. 17 | 18 | Args: 19 | inputs (Parameters): model parameters 20 | 21 | Returns: 22 | Tuple[float, float]: CO\ :sub:`2` concentration (ppm), and infection risk probability 23 | """ 24 | return None, None 25 | -------------------------------------------------------------------------------- /archABM/aerosol_model_colorado.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import Tuple 3 | 4 | from .aerosol_model import AerosolModel 5 | from .parameters import Parameters 6 | 7 | 8 | class AerosolModelColorado(AerosolModel): 9 | """Aerosol transmission estimator 10 | 11 | COVID-19 Airborne Transmission Estimator :cite:`doi:10.1021/acs.estlett.1c00183,https://doi.org/10.1111/ina.12751,Peng2021.04.21.21255898` 12 | 13 | The model combines two submodels: 14 | 15 | #. A standard atmospheric box model, which assumes that the emissions are completely mixed across a control volume quickly \ 16 | (such as an indoor room or other space). \ 17 | See for example Chapter 3 of the Jacob Atmos. Chem. textbook :cite:`10.2307/j.ctt7t8hg`, and Chapter 21 of the Cooper and \ 18 | Alley Air Pollution Control Engineering Textbook :cite:`cooper2010air` for indoor applications. \ 19 | This is an approximation that allows easy calculation, is approximately correct as long as near-field effects \ 20 | are avoided by social distancing, and is commonly used in air quality modeling. 21 | 22 | #. A standard aerosol infection model (Wells-Riley model), as formulated in Miller et al. 2020 :cite:`https://doi.org/10.1111/ina.12751`,\ 23 | and references therein :cite:`10.1093/oxfordjournals.aje.a112560,BUONANNO2020105794,BUONANNO2020106112`. 24 | 25 | .. important:: 26 | The propagation of COVID-19 is only by aerosol transmission. 27 | 28 | The model is based on a standard model of aerosol disease transmission, the Wells-Riley model. 29 | It is calibrated to COVID-19 per recent literature on quanta emission rate. 30 | 31 | This is not an epidemiological model, and does not include droplet or contact / fomite transmission, and assumes that 6 ft / 2 m social distancing is respected. Otherwise higher transmission will result. 32 | 33 | """ 34 | 35 | name: str = "Colorado" 36 | 37 | def __init__(self, params): 38 | super().__init__(params) 39 | self.params = params 40 | 41 | def get_risk(self, inputs: Parameters) -> Tuple[float, float]: 42 | """Calculate the infection risk of an individual in a room 43 | and the CO\ :sub:`2` thrown into the air. 44 | 45 | Args: 46 | inputs (Parameters): model parameters 47 | 48 | Returns: 49 | Tuple[float, float]: CO\ :sub:`2` concentration (ppm), and infection risk probability 50 | """ 51 | 52 | params = self.params 53 | 54 | # length = 8 55 | # width = 6 56 | height = inputs.room_height 57 | area = inputs.room_area # width * length 58 | volume = area * height 59 | event_duration = inputs.event_duration # 50 / 60 # hours 60 | 61 | # PRESSURE 62 | pressure = params.pressure # 0.95 63 | 64 | # TEMPERATURE 65 | temperature_ext = params.temperature 66 | temperature_int = inputs.temperature 67 | area_transfer = 2*math.sqrt(area)*height # suppose half of the room is exposed 68 | 69 | heat_transfer_coefficient = 1.2 # W / m2 K # https://www.researchgate.net/publication/325614742_A_Dynamic_Model_for_Indoor_Temperature_Prediction_in_Buildings 70 | heat_transfer = heat_transfer_coefficient*area_transfer # W/K 71 | heat_capacity_coefficient = 144e3 # J/(m2 K) # https://www.researchgate.net/publication/325614742_A_Dynamic_Model_for_Indoor_Temperature_Prediction_in_Buildings 72 | heat_capacity = heat_capacity_coefficient*area_transfer # J/K 73 | heat_per_person = 150 # W # https://www.researchgate.net/publication/271444362_Predicting_Energy_Requirement_for_Cooling_the_Building_Using_Artificial_Neural_Network/figures?lo=1 74 | P = heat_per_person*inputs.num_people # W 75 | Q = inputs.room_ventilation_rate * volume / 3600 # m3 / s # https://www.ncbi.nlm.nih.gov/books/NBK143289/ 76 | delta_time = event_duration * 3600 # seconds 77 | temperature = (temperature_int + delta_time*(P/heat_capacity + temperature_ext*heat_transfer/heat_capacity + temperature_ext*Q/volume))/(1 + delta_time*heat_transfer/heat_capacity + delta_time*Q/volume) 78 | 79 | 80 | relative_humidity = params.relative_humidity # 50 # TODO: include formula 81 | 82 | CO2_background = params.CO2_background # 415 83 | ventilation = inputs.room_ventilation_rate # 3 84 | decay_rate = params.decay_rate # 0.62 85 | deposition_rate = params.deposition_rate # 0.3 86 | 87 | hepa_flow_rate = params.hepa_flow_rate 88 | hepa_removal = hepa_flow_rate * volume 89 | 90 | recirculated_flow_rate = inputs.recirculated_flow_rate 91 | filter_efficiency = params.filter_efficiency 92 | ducts_removal = params.ducts_removal 93 | other_removal = params.other_removal 94 | 95 | ach_additional = recirculated_flow_rate / volume * min(1, filter_efficiency + ducts_removal + other_removal) 96 | additional_measures = hepa_removal + ach_additional 97 | 98 | loss_rate = ventilation + decay_rate + deposition_rate + additional_measures 99 | 100 | # ventilation_person = volume * (ventilation + additional_measures) * 1000 / 3600 / num_people 101 | 102 | num_people = inputs.num_people 103 | infective_people = inputs.infective_people # 1 104 | fraction_immune = params.fraction_immune # 0 105 | susceptible_people = (num_people - infective_people) * (1 - fraction_immune) 106 | 107 | # density_area_person = area / num_people 108 | # density_people_area = num_people / area 109 | # density_volume_person = volume / num_people 110 | 111 | breathing_rate = params.breathing_rate # 0.52 112 | breathing_rate_relative = breathing_rate / (0.0048 * 60) 113 | CO2_emission_person = params.CO2_emission_person # 0.005 114 | CO2_emission = CO2_emission_person * num_people / pressure * (273.15 + temperature_int) / 273.15 115 | 116 | quanta_exhalation = params.quanta_exhalation # 25 117 | quanta_enhancement = params.quanta_enhancement # 1 118 | quanta_exhalation_relative = quanta_exhalation / 2 119 | 120 | mask_efficiency_exhalation = inputs.mask_efficiency # 50 / 100 121 | mask_efficiency_inhalation = inputs.mask_efficiency # 30 / 100 122 | people_with_masks = params.people_with_masks # 100 / 100 123 | 124 | # probability_infective = 0.20 / 100 125 | # hospitalization_rate = 20 / 100 126 | # death_rate = 1 / 100 127 | 128 | net_emission_rate = quanta_exhalation * (1 - mask_efficiency_exhalation * people_with_masks) * infective_people * quanta_enhancement 129 | quanta_concentration = net_emission_rate / loss_rate / volume * (1 - (1 / loss_rate / event_duration) * (1 - math.exp(-loss_rate * event_duration))) 130 | # TODO: NEW FORMULA 131 | # TODO: infection risk dynamic 132 | quanta_concentration = ( 133 | net_emission_rate / loss_rate / volume * (1 - (1 / loss_rate / event_duration) * (1 - math.exp(-loss_rate * event_duration))) 134 | + math.exp(-loss_rate * event_duration) * (inputs.quanta_level - 0) 135 | + 0 136 | ) 137 | quanta_inhaled_per_person = quanta_concentration * breathing_rate * event_duration * (1 - mask_efficiency_inhalation * people_with_masks) 138 | 139 | # probability_infection = 1 - math.exp(-quanta_inhaled_per_person) 140 | # probability_infection = probability_infection * susceptible_people 141 | # probability_hospitalization = probability_infection * hospitalization_rate 142 | # probability_death = probability_infection * death_rate 143 | 144 | # if susceptible_people == 0 or infective_people == 0: 145 | # infection_risk = 0.0 146 | # infection_risk_relative = 0.0 147 | # else: 148 | # infection_risk = ( 149 | # breathing_rate_relative 150 | # * quanta_exhalation_relative 151 | # * (1 - mask_efficiency_exhalation * people_with_masks) 152 | # * (1 - mask_efficiency_inhalation * people_with_masks) 153 | # * event_duration 154 | # * susceptible_people 155 | # / (loss_rate * volume) 156 | # * (1 - (1 - math.exp(-loss_rate * event_duration)) / (loss_rate * event_duration)) 157 | # + math.exp(-loss_rate * event_duration) * (inputs.infection_risk - 0) 158 | # + 0 159 | # ) 160 | 161 | # infection_risk_relative = infection_risk / susceptible_people 162 | # infection_risk = (1 - math.exp(-infection_risk_relative))*susceptible_people # TODO: review Taylor approximation 163 | 164 | CO2_mixing_ratio = ( 165 | (CO2_emission * 3.6 / ventilation / volume * (1 - (1 / ventilation / event_duration) * (1 - math.exp(-ventilation * event_duration)))) * 1e6 166 | + math.exp(-ventilation * event_duration) * (inputs.CO2_level - CO2_background) 167 | + CO2_background 168 | ) 169 | CO2_mixing_ratio_delta = CO2_mixing_ratio - inputs.CO2_level 170 | CO2_concentration = CO2_mixing_ratio_delta * 40.9 / 1e6 * 44 * 298 / (273.15 + temperature) * pressure 171 | CO2_reinhaled_grams = CO2_concentration * breathing_rate * event_duration 172 | CO2_reinhaled_ppm = CO2_mixing_ratio_delta * event_duration 173 | # CO2_probability_infection_= CO2_reinhaled_ppm / 1e4 / probability_infection 174 | # CO2_inhale_ppm = CO2_mixing_ratio_delta * event_duration * 0.01 / probability_infection + CO2_background 175 | 176 | # return CO2_mixing_ratio, quanta_inhaled_per_person, quanta_concentration 177 | return CO2_mixing_ratio, quanta_inhaled_per_person, quanta_concentration, temperature, relative_humidity 178 | -------------------------------------------------------------------------------- /archABM/aerosol_model_maxplanck.py: -------------------------------------------------------------------------------- 1 | import math 2 | from typing import Tuple 3 | 4 | from .parameters import Parameters 5 | from .aerosol_model import AerosolModel 6 | 7 | 8 | class AerosolModelMaxPlanck(AerosolModel): 9 | """Aerosol transmission estimator 10 | 11 | Model Calculations of Aerosol Transmission and Infection Risk of COVID-19 in Indoor Environments :cite:`ijerph17218114` 12 | 13 | An adjustable algorithm to estimate the infection risk for different indoor environments, 14 | constrained by published data of human aerosol emissions, SARS-CoV-2 viral loads, infective dose and other parameters. 15 | Evaluates typical indoor settings such as an office, a classroom, choir practice, and a reception/party. 16 | 17 | The model includes a number of modifiable environmental factors that represent relevant physiological parameters and environmental conditions. 18 | For simplicity, all subjects are assumed to be equal in terms of breathing, speaking and susceptibility to infection. 19 | The model parameters can be easily adjusted to account for different environmental conditions and activities. 20 | 21 | """ 22 | 23 | name: str = "MaxPlanck" 24 | 25 | def __init__(self, params): 26 | super().__init__(params) 27 | self.params = params 28 | 29 | def get_risk(self, inputs: Parameters) -> Tuple[float, float]: 30 | """Calculate the infection risk of an individual in a room 31 | and the CO\ :sub:`2` thrown into the air. 32 | 33 | Args: 34 | inputs (Parameters): model parameters 35 | 36 | Returns: 37 | Tuple[float, float]: CO\ :sub:`2` concentration (ppm), and infection risk probability 38 | """ 39 | params = self.params 40 | # inputs: room_area, room_height, room_ventilation_rate, mask_efficiency, time_in_room_h, susceptible_people 41 | 42 | infection_probability = 1 - 10 ** (math.log10(0.5) / params.RNA_D50) 43 | RNA_content = params.RNA_concentration * math.pi / 6 * (params.aerosol_diameter / 10000) ** 3 44 | aerosol_emission = (params.emission_breathing * (1 - params.speaking_breathing_ratio) + params.emission_speaking * params.speaking_breathing_ratio) * 1000 * params.respiratory_rate * 60 45 | aerosol_concentration = aerosol_emission / (inputs.room_area * inputs.room_height * 1000) 46 | RNA_concentration = aerosol_concentration * RNA_content 47 | RNA_dosis = params.respiratory_rate * 60 * RNA_concentration * params.deposition_rate 48 | 49 | dosis_infectious = RNA_dosis / (inputs.room_ventilation_rate + 1 / params.virus_lifetime) * (1 - inputs.mask_efficiency) * inputs.time_in_room_h 50 | risk_one_person = (1 - ((1 - infection_probability) ** dosis_infectious) ** inputs.num_people) * 100 51 | 52 | # Return results 53 | dosis_min, dosis_max = 0, 1 54 | co2_min, co2_max = 0, 80 55 | co2_dosis = (dosis_infectious - dosis_min) / (dosis_max - dosis_min) * (co2_max - co2_min) + co2_min 56 | 57 | air_contamination = co2_dosis 58 | infection_risk = risk_one_person 59 | 60 | return air_contamination, infection_risk 61 | -------------------------------------------------------------------------------- /archABM/aerosol_model_mit.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | from .parameters import Parameters 3 | from .aerosol_model import AerosolModel 4 | 5 | 6 | class AerosolModelMIT(AerosolModel): 7 | """Aerosol transmission estimator 8 | 9 | MIT COVID-19 Indoor Safety Guideline :cite:`Bazante2018995118,Bazant2021.04.04.21254903,Risbeck2021.06.21.21259287` 10 | 11 | Theoretical model that quantifies the extent to which transmission risk is reduced in large rooms 12 | with high air exchange rates, increased for more vigorous respiratory activities, and dramatically reduced by the use of face masks. 13 | Consideration of a number of outbreaks yields self-consistent estimates for the infectiousness of the new coronavirus. 14 | 15 | """ 16 | 17 | name: str = "MIT" 18 | 19 | def __init__(self, params): 20 | super().__init__(params) 21 | self.params = params 22 | 23 | def get_risk(self, inputs: Parameters) -> Tuple[float, float]: 24 | """Calculate the infection risk of an individual in a room 25 | and the CO\ :sub:`2` thrown into the air. 26 | 27 | Args: 28 | inputs (Parameters): model parameters 29 | 30 | Returns: 31 | Tuple[float, float]: CO\ :sub:`2` concentration (ppm), and infection risk probability 32 | """ 33 | params = self.params 34 | 35 | area = inputs.room_area 36 | height = inputs.room_height 37 | volume = area * height 38 | 39 | ventilation = inputs.room_ventilation_rate 40 | ventilation_flow = ventilation * volume 41 | 42 | recirculation = 1 43 | recirculation_flow = recirculation * volume 44 | 45 | air_flow = ventilation_flow + recirculation_flow 46 | air_outdoor_fraction = ventilation_flow / air_flow 47 | 48 | filtration_efficiency = params.filtration_efficiency # 0.01 49 | filtration_rate = filtration_efficiency * recirculation_flow / volume 50 | relative_humidity = params.relative_humidity / 100 # 60 / 100 51 | 52 | breathing_rate = params.breathing_rate # 0.49 53 | aerosol_radius = params.aerosol_radius # 2 54 | aerosol_radius_humidity = aerosol_radius * (0.4 / (1 - relative_humidity)) ** (1 / 3) 55 | 56 | infectiousness = params.infectiousness # 72 57 | deactivation_rate = params.deactivation_rate # 0.3 58 | deactivation_rate_humidity = deactivation_rate * relative_humidity / 50 59 | transmissibility = params.transmissibility # 1 60 | 61 | settling_speed = (2 / 9) * 1100 / (1.86 * 10 ** (-5)) * 9.8 / 1e9 * aerosol_radius_humidity ** 2 # * 60 * 60 / 1000 62 | relaxation_rate = ventilation + deactivation_rate_humidity + filtration_rate + settling_speed * 60 * 60 / 1000 / ventilation 63 | dillution_factor = breathing_rate / (relaxation_rate * volume) 64 | infectiousness_room = infectiousness * dillution_factor 65 | 66 | mask_passage_probability = 1 - inputs.mask_efficiency # 0.145 67 | transmission_rate = (breathing_rate * mask_passage_probability) ** 2 * infectiousness * transmissibility / (volume * relaxation_rate) 68 | 69 | # if risk tolerance is fixed 70 | # risk_tolerance = 0.1 71 | # exposure_time = inputs.time_in_room_h # 10 72 | # maximum_occupancy_transient = 1 + risk_tolerance * (1 + 1/(relaxation_rate*exposure_time)) / (transmission_rate * exposure_time) 73 | # maximum_occupancy_steady = 1 + risk_tolerance / (transmission_rate * exposure_time) 74 | 75 | # occupancy = inputs.num_people 76 | # maximum_exposure_time_steady = risk_tolerance / (occupancy - 1) / transmission_rate 77 | # maximum_exposure_time_transient = maximum_exposure_time_steady * (1 + math.sqrt(1 + 4/(relaxation_rate*maximum_exposure_time_steady) )) / 2 78 | 79 | # background_co2 = params.background_co2 # 410 80 | # average_co2 = params.average_co2 # 700 81 | # maximum_exposure_time_co2 = risk_tolerance * 38000 * relaxation_rate / \ 82 | # ((average_co2 - background_co2) * breathing_rate * infectiousness * \ 83 | # transmissibility * mask_passage_probability * mask_passage_probability * ventilation ) 84 | 85 | # co2_concentration = background_co2 + risk_tolerance * 38000 * relaxation_rate / (exposure_time * breathing_rate * infectiousness * transmissibility * mask_passage_probability * mask_passage_probability * ventilation) 86 | 87 | # if exposure time and occupancy is fixed 88 | exposure_time = inputs.time_in_room_h # 10 89 | occupancy = inputs.num_people 90 | risk_tolerance_steady = (occupancy - 1) * transmission_rate * exposure_time 91 | risk_tolerance_transient = risk_tolerance_steady / (1 + 1 / (relaxation_rate * exposure_time)) 92 | 93 | background_co2 = params.background_co2 # 410 94 | co2_concentration = background_co2 + risk_tolerance_steady * 38000 * relaxation_rate / ( 95 | exposure_time * breathing_rate * infectiousness * transmissibility * mask_passage_probability * mask_passage_probability * ventilation 96 | ) 97 | 98 | # Return results 99 | air_contamination = co2_concentration - background_co2 100 | infection_risk = risk_tolerance_steady 101 | 102 | print("hey", risk_tolerance_steady, relaxation_rate, exposure_time, breathing_rate, infectiousness, transmissibility, mask_passage_probability, ventilation) 103 | 104 | return air_contamination, infection_risk 105 | -------------------------------------------------------------------------------- /archABM/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | here = os.path.dirname(__file__) 5 | with open(os.path.join(here, 'config.json'), "r") as f: 6 | config = json.load(f) -------------------------------------------------------------------------------- /archABM/creator.py: -------------------------------------------------------------------------------- 1 | from random import sample 2 | from typing import List 3 | 4 | from simpy import Environment 5 | 6 | from .actions import Actions 7 | from .database import Database 8 | from .event_model import EventModel 9 | from .options import Options 10 | from .parameters import Parameters 11 | from .person import Person 12 | from .place import Place 13 | 14 | 15 | class Creator: 16 | """Initializes the required assets to run a simulation: 17 | :class:`~archABM.options.Options`, 18 | :class:`~archABM.aerosol_model.AerosolModel`, 19 | :class:`~archABM.event.Event`, 20 | :class:`~archABM.place.Place`, 21 | :class:`~archABM.actions.Actions`, 22 | :class:`~archABM.person.Person`. 23 | """ 24 | 25 | env: Environment 26 | config: dict 27 | db: Database 28 | 29 | def __init__(self, env: Environment, config: dict, db: Database) -> None: 30 | self.env = env 31 | self.config = config 32 | self.db = db 33 | 34 | Person.reset() 35 | Place.reset() 36 | EventModel.reset() 37 | 38 | def create_options(self) -> Options: 39 | """Initializes general :class:`~archABM.options.Options` for the simulation. 40 | 41 | Returns: 42 | Options: general :class:`~archABM.options.Options` for the simulation 43 | """ 44 | p = self.config["options"] 45 | params = Parameters(p) 46 | options = Options(self.env, self.db, params) 47 | 48 | return options 49 | 50 | def create_events(self) -> List[EventModel]: 51 | """Initializes list of :class:`~archABM.event_model.EventModel` and their respective :class:`~archABM.parameters.Parameters`. 52 | 53 | Returns: 54 | List[EventModel]: types of events or activities 55 | """ 56 | events = [] 57 | for e in self.config["events"]: 58 | params = Parameters(e) 59 | event = EventModel(params) 60 | events.append(event) 61 | 62 | return events 63 | 64 | def create_places(self) -> List[Place]: 65 | """Initializes list of :class:`~archABM.place.Place` and their respective :class:`~archABM.parameters.Parameters`. 66 | 67 | Returns: 68 | List[Place]: list of available places 69 | """ 70 | places = [] 71 | for p in self.config["places"]: 72 | params = Parameters(p) 73 | place = Place(self.env, self.db, params) 74 | places.append(place) 75 | 76 | return places 77 | 78 | def create_actions(self) -> Actions: 79 | """Initializes instance of class :class:`~archABM.actions.Actions`. 80 | 81 | Returns: 82 | Actions: instance of class :class:`~archABM.actions.Actions` 83 | """ 84 | return Actions(self.env, self.db) 85 | 86 | def create_people(self) -> List[Person]: 87 | """Initializes list of :class:`~archABM.person.Person` and their respective :class:`~archABM.parameters.Parameters`. 88 | 89 | It also sets the status of certain :class:`~archABM.person.Person` from ``susceptible`` to ``infected``. 90 | This is controlled by the ``ratio_infected`` configuration parameter. 91 | 92 | Returns: 93 | List[Person]: simulation people 94 | """ 95 | people = [] 96 | for p in self.config["people"]: 97 | params = Parameters(p) 98 | person = Person(self.env, self.db, params) 99 | person.start() 100 | people.append(person) 101 | 102 | num_people = len(people) 103 | num_infected = int(max(1, self.config["options"]["ratio_infected"] * num_people)) 104 | for p in sample(people, num_infected): 105 | p.status = 1 106 | 107 | return people 108 | 109 | def create_model(self): 110 | """Initializes the selected COVID19 aerosol model. 111 | 112 | At the moment, there are three models available: 113 | 114 | #. :class:`~archABM.aerosol_model_colorado.AerosolModelColorado`: COVID-19 Airborne Transmission Estimator \ 115 | :cite:`doi:10.1021/acs.estlett.1c00183,https://doi.org/10.1111/ina.12751,Peng2021.04.21.21255898` 116 | 117 | #. :class:`~archABM.aerosol_model_mit.AerosolModelMIT`: MIT COVID-19 Indoor Safety Guideline \ 118 | :cite:`Bazante2018995118,Bazant2021.04.04.21254903,Risbeck2021.06.21.21259287` 119 | 120 | #. :class:`~archABM.aerosol_model_maxplanck.AerosolModelMaxPlanck`: Model Calculations of Aerosol Transmission and \ 121 | Infection Risk of COVID-19 in Indoor Environments :cite:`ijerph17218114` 122 | 123 | Returns: 124 | AerosolModel: selected aerosol model 125 | """ 126 | options = self.config["options"] 127 | selection = options["model"] 128 | params = Parameters(options["model_parameters"][selection]) 129 | model = None 130 | if selection == "MaxPlanck": 131 | from .aerosol_model_maxplanck import AerosolModelMaxPlanck 132 | 133 | model = AerosolModelMaxPlanck(params) 134 | elif selection == "MIT": 135 | from .aerosol_model_mit import AerosolModelMIT 136 | 137 | model = AerosolModelMIT(params) 138 | elif selection == "Colorado": 139 | from .aerosol_model_colorado import AerosolModelColorado 140 | 141 | model = AerosolModelColorado(params) 142 | 143 | return model 144 | -------------------------------------------------------------------------------- /archABM/database.py: -------------------------------------------------------------------------------- 1 | class Database: 2 | """In-memory database of the simulation components 3 | 4 | It registers: 5 | :class:`~archABM.options.Options`, 6 | :class:`~archABM.aerosol_model.AerosolModel`, 7 | :class:`~archABM.actions.Actions`, 8 | :class:`~archABM.event.Event` list, 9 | :class:`~archABM.place.Place` list, 10 | :class:`~archABM.person.Person` list, 11 | simulation run ID. 12 | """ 13 | 14 | model: None 15 | actions: None 16 | options: None 17 | events: list 18 | places: list 19 | people: list 20 | run: int 21 | 22 | def __init__(self) -> None: 23 | self.options = None 24 | self.model = None 25 | self.actions = None 26 | self.events = [] 27 | self.places = [] 28 | self.people = [] 29 | 30 | self.run = -1 31 | 32 | def next(self) -> None: 33 | """Increments one unit the simulation run ID""" 34 | self.run += 1 35 | -------------------------------------------------------------------------------- /archABM/engine.py: -------------------------------------------------------------------------------- 1 | from jsonschema import validate 2 | from simpy import Environment 3 | from tqdm import tqdm 4 | 5 | from .creator import Creator 6 | from .database import Database 7 | from .results import Results 8 | 9 | import json 10 | import os 11 | import copy 12 | 13 | 14 | class Engine: 15 | """Core class of the archABM package 16 | 17 | Launches the agent-based simulation with the specified configuration. 18 | """ 19 | 20 | config: dict 21 | db: Database 22 | env: Environment 23 | 24 | def __init__(self, config: dict) -> None: 25 | schema = self.retrieve_schema() 26 | validate(instance=config, schema=schema) 27 | 28 | self.config = self.preprocess(config) 29 | 30 | def retrieve_schema(self): 31 | """Get configuration file JSON schema 32 | 33 | Returns: 34 | dict: json-schema 35 | """ 36 | dir_path = os.path.dirname(os.path.realpath(__file__)) 37 | with open(dir_path + "/schema.json", "r") as f: 38 | schema = json.load(f) 39 | return schema 40 | 41 | def preprocess(self, config) -> None: 42 | """Processes the configuration dictionary to generate people. 43 | 44 | Based on the specified configuration of number of people per group, 45 | this method generates an array of people, and assignes a incremental name to each person. 46 | """ 47 | config = copy.deepcopy(config) 48 | 49 | num_people = 0 50 | for person in config["people"]: 51 | num_people += person["num_people"] 52 | 53 | for place in config["places"]: 54 | if place["capacity"] is None: 55 | place["capacity"] = num_people + 1 56 | 57 | people = [] 58 | cont = 0 59 | for person in config["people"]: 60 | num_people = person.pop("num_people") 61 | for i in range(num_people): 62 | person["name"] = "person" + str(cont) 63 | people.append(person.copy()) 64 | cont += 1 65 | config["people"] = people 66 | return config 67 | 68 | def setup(self) -> None: 69 | """Setup for a simulation run. 70 | 71 | Creates the environment and the required assets to run a simulation: 72 | :class:`~archABM.options.Options`, 73 | :class:`~archABM.aerosol_model.AerosolModel`, 74 | :class:`~archABM.event.Event`, 75 | :class:`~archABM.place.Place`, 76 | :class:`~archABM.actions.Actions`, 77 | :class:`~archABM.person.Person`. 78 | """ 79 | self.env = Environment() 80 | self.db.next() 81 | 82 | god = Creator(self.env, self.config, self.db) 83 | self.db.options = god.create_options() 84 | self.db.model = god.create_model() 85 | self.db.events = god.create_events() 86 | self.db.places = god.create_places() 87 | self.db.actions = god.create_actions() 88 | self.db.people = god.create_people() 89 | 90 | def run(self, until: int = None, number_runs: int = None) -> dict: 91 | """Launches a batch of simulations 92 | 93 | Args: 94 | until (int, optional): duration of each simulation, in minutes. Defaults to None. 95 | number_runs (int, optional): number of simulation runs. Defaults to None. 96 | 97 | Returns: 98 | dict: simulation history and configuration 99 | """ 100 | self.db = Database() 101 | self.db.results = Results(self.config) 102 | 103 | if until is None: 104 | until = 1440 105 | if number_runs is None: 106 | number_runs = self.config["options"]["number_runs"] 107 | 108 | with tqdm(total=number_runs) as pbar: 109 | for i in range(number_runs): 110 | self.setup() 111 | self.env.run(until) 112 | pbar.update(1) 113 | return self.db.results.done() 114 | -------------------------------------------------------------------------------- /archABM/event.py: -------------------------------------------------------------------------------- 1 | from .event_model import EventModel 2 | from .place import Place 3 | 4 | 5 | class Event: 6 | """Event primitive 7 | 8 | An event is defined by an activity :class:`~archABM.event_model.EventModel`, that happens at some physical location 9 | :class:`~archABM.place.Place``, for a finite period of time, in minutes (duration). 10 | """ 11 | 12 | model: EventModel 13 | place: Place 14 | duration: int 15 | 16 | def __init__(self, model: EventModel, place: Place, duration: int) -> None: 17 | self.model = model 18 | self.place = place 19 | self.duration = duration 20 | -------------------------------------------------------------------------------- /archABM/event_generator.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from simpy import Environment 4 | 5 | from .database import Database 6 | from .event_model import EventModel 7 | 8 | 9 | class EventGenerator: 10 | """Generates events 11 | 12 | An event is defined by an activity :class:`~archABM.event_model.EventModel`, that happens at a given physical location 13 | :class:`~archABM.place.Place`, for a finite period of time, in minutes (duration). 14 | 15 | A event generator has certain event models to choose from, 16 | each one related to an activity. 17 | 18 | """ 19 | 20 | env: Environment 21 | db: Database 22 | models: list 23 | activities: list 24 | 25 | def __init__(self, env: Environment, db: Database): 26 | self.env = env 27 | self.db = db 28 | 29 | # generate new for allowed events 30 | self.models = [m.new() for m in self.db.events if m.params.allow] 31 | self.activities = [m.params.activity for m in self.models] 32 | 33 | def generate(self, now: int, person): 34 | """Generates events 35 | 36 | First, it computes the probabilities and the validity 37 | of each :class:`~archABM.event_model.EventModel` at the current timestamp. 38 | Then, the activity is selected based on these probabilities as follows: 39 | 40 | * If there exists any probability among the list of :class:`~archABM.event_model.EventModel`, the activity is selected randomly according to the relative probabilities. 41 | * If all :class:`~archABM.event_model.EventModel` have ``0`` probability, then the activity is selected randomly among the valid ones. 42 | * Otherwise a random activity is returned. 43 | 44 | Once the activity type :class:`~archABM.event_model.EventModel` has been selected, 45 | the event duration can computed and the physical location :class:`~archABM.place.Place`` can also be chosen. 46 | 47 | The selected activity is counted (consumed) from the list of :class:`~archABM.event_model.EventModel` of the invoking person. 48 | 49 | .. note:: 50 | Collective activities are consumed individually after the current event interruption. 51 | 52 | Args: 53 | now (int): current timestamp in minutes 54 | person (Person): person that invokes the event generation 55 | 56 | Returns: 57 | Event: generated :class:`~archABM.event.Event`, which is a set of 58 | a) activity :class:`~archABM.event_model.EventModel`, 59 | b) physical location :class:`~archABM.place.Place`` and 60 | c) time duration. 61 | """ 62 | # Get probabilities for each model event 63 | p = [m.probability(now) for m in self.models] 64 | v = [m.valid() for m in self.models] 65 | 66 | # Select event model 67 | if sum(p) > 0: 68 | model = random.choices(self.models, weights=p)[0] 69 | elif sum(v) > 0: 70 | model = random.choices(self.models, weights=v)[0] 71 | else: 72 | model = random.choice(self.models) 73 | 74 | # Create event based on selected model 75 | activity = model.params.activity 76 | duration = model.duration(now) 77 | # duration += 0.001 78 | place = self.db.actions.find_place(model, person) 79 | if place is None: 80 | return None 81 | if model.params.collective: 82 | return self.db.actions.create_collective_event(model, place, duration, person) 83 | else: 84 | model.consume() 85 | return self.db.actions.create_event(model, place, duration) 86 | 87 | def consume_activity(self, model: EventModel): 88 | """Consumes a unit from a given :class:`~archABM.event_model.EventModel`. 89 | 90 | Args: 91 | model (EventModel): event model to consume from 92 | """ 93 | for m in self.models: 94 | if m.params.activity == model.params.activity: 95 | m.consume() 96 | 97 | def valid_activity(self, model: EventModel): 98 | """Checks whether a given :class:`~archABM.event_model.EventModel` is valid. 99 | 100 | Args: 101 | model (EventModel): event model to check validity from 102 | 103 | Returns: 104 | [bool]: whether the event model is valid 105 | """ 106 | for m in self.models: 107 | if m.params.activity == model.params.activity: 108 | return m.valid() 109 | -------------------------------------------------------------------------------- /archABM/event_model.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import random 3 | from .parameters import Parameters 4 | 5 | 6 | class EventModel: 7 | """Defines an event model, also called "activity" 8 | 9 | An event model is defined by these parameters: 10 | 11 | * Activity name: :obj:`str` 12 | * Schedule: :obj:`list` of :obj:`tuple` (in minutes :obj:`int`) 13 | * Repetitions range: minimum (:obj:`int`) and maximum (:obj:`int`) 14 | * Duration range: minimum (:obj:`int`) and maximum (:obj:`int`) in minutes 15 | * Other parameters: 16 | 17 | * mask efficiency ratio: :obj:`float` 18 | * collective event: :obj:`bool` 19 | * shared event: :obj:`bool` 20 | 21 | The schedule defines the allowed periods of time in which an activity can happen. 22 | For example, ``schedule=[(120,180),(240,300)]`` allows people to carry out this activity from 23 | the time ``120`` to ``180`` and also from time ``240`` until ``300``. 24 | Notice that the schedule units are in minutes. 25 | 26 | Each activity is limited to a certain duration, and its priority follows 27 | a piecewise linear function, parametrized by: 28 | 29 | * ``r``: repeat\ :sub:`min` 30 | * ``R``: repeat\ :sub:`max` 31 | * ``e``: event count 32 | 33 | .. math:: 34 | Priority(e) = 35 | \\left\{\\begin{matrix} 36 | 1-(1-\\alpha)\\cfrac{e}{r}\,,\quad 0 \leq e < r \\\\ 37 | \\alpha\\cfrac{R-e}{R-r}\,,\quad r \leq e < R \\ 38 | \end{matrix}\\right. 39 | 40 | 41 | 42 | .. tikz:: Priority piecewise linear function 43 | \pgfmathsetmacro{\\N}{10}; 44 | \pgfmathsetmacro{\\M}{6}; 45 | \pgfmathsetmacro{\\NN}{\\N-1}; 46 | \pgfmathsetmacro{\\MM}{\\M-1}; 47 | \pgfmathsetmacro{\\repmin}{2.25}; 48 | \pgfmathsetmacro{\\repmax}{8.5}; 49 | \pgfmathsetmacro{\\a}{2}; 50 | \coordinate (A) at (0,\\MM); 51 | \coordinate (B) at (\\NN,0); 52 | \coordinate (C) at (\\repmin, \\a); 53 | \coordinate (D) at (\\repmax, 0); 54 | \coordinate (E) at (\\repmin, 0); 55 | \coordinate (F) at (0, \\a); 56 | \draw[stepx=1,thin, black!20] (0,0) grid (\\N,\\M); 57 | \draw[->, very thick] (0,0) to (\\N,0) node[right] {Event count}; 58 | \draw[->, very thick] (0,0) to (0,\\M) node[above] {Priority}; 59 | \draw (0.1,0) -- (-0.1, 0) node[anchor=east] {0}; 60 | \draw (0, 0.1) -- (0, -0.1); 61 | \draw (\\repmin,0.1) -- (\\repmin,-0.1) node[anchor=north] {$repeat_{min}$}; 62 | \draw (\\repmax,0.1) -- (\\repmax,-0.1) node[anchor=north] {$repeat_{max}$}; 63 | \draw[ultra thick] (0.1, \\MM) -- (-0.1, \\MM) node[left] {1}; 64 | \draw[very thick, black!50, dashed] (C) -- (F) node[left] {$\\alpha$}; 65 | \draw[very thick, black!50, dashed] (C) -- (E); 66 | \draw[ultra thick, red] (A) -- (C); 67 | \draw[ultra thick, red] (C) -- (D); 68 | :xscale: 80 69 | :align: left 70 | 71 | """ 72 | 73 | id: int = -1 74 | params: Parameters 75 | count: int 76 | noise: int 77 | 78 | def __init__(self, params: Parameters) -> None: 79 | self.next() 80 | self.id = EventModel.id 81 | 82 | self.params = params 83 | self.count = 0 84 | self.noise = None 85 | 86 | @classmethod 87 | def reset(cls) -> None: 88 | """Resets :class:`~archABM.event_model.EventModel` ID.""" 89 | EventModel.id = -1 90 | 91 | @staticmethod 92 | def next() -> None: 93 | """Increments one unit the :class:`~archABM.event_model.EventModel` ID.""" 94 | EventModel.id += 1 95 | 96 | def get_noise(self) -> int: 97 | """Generates random noise 98 | 99 | Returns: 100 | int: noise amount in minutes 101 | """ 102 | if self.noise is None: 103 | m = 15 # minutes # TODO: review hardcoded value 104 | if m == 0: 105 | self.noise = 0 106 | else: 107 | self.noise = random.randrange(m) # minutes 108 | return self.noise 109 | 110 | def new(self): 111 | """Generates a :class:`~archABM.event_model.EventModel` copy, with reset count and noise 112 | 113 | Returns: 114 | EventModel: cloned instance 115 | """ 116 | self.count = 0 117 | self.noise = None 118 | return copy.copy(self) 119 | 120 | def duration(self, now) -> int: 121 | """Generates a random duration between :attr:`duration_min` and :attr:`duration_max`. 122 | 123 | .. note:: 124 | If the generated duration, together with the current timestamp, 125 | exceeds the allowed schedule, the duration is limited to finish 126 | at the scheduled time interval. 127 | 128 | 129 | The :attr:`noise` attribute is used to model the schedule's time tolerance. 130 | 131 | Args: 132 | now (int): current timestamp in minutes 133 | 134 | Returns: 135 | int: event duration in minutes 136 | """ 137 | duration = random.randint(self.params.duration_min, self.params.duration_max) 138 | estimated = now + duration 139 | noise = self.get_noise() # minutes 140 | for interval in self.params.schedule: 141 | a, b = interval 142 | if a - noise <= now <= b + noise < estimated: 143 | duration = b + noise - now + 1 144 | break 145 | return duration 146 | 147 | def priority(self) -> float: 148 | """Computes the priority of a certain event. 149 | 150 | The priority function follows a piecewise linear function, parametrized by: 151 | 152 | * ``r``: repeat\ :sub:`min` 153 | * ``R``: repeat\ :sub:`max` 154 | * ``e``: event count 155 | 156 | .. math:: 157 | Priority(e) = 158 | \\left\{\\begin{matrix} 159 | 1-(1-\\alpha)\\cfrac{e}{r}\,,\quad 0 \leq e < r \\\\ 160 | \\alpha\\cfrac{R-e}{R-r}\,,\quad r \leq e < R \\ 161 | \end{matrix}\\right. 162 | 163 | Returns: 164 | float: priority value [0-1] 165 | """ 166 | alpha = 0.5 # TODO: review hardcoded value 167 | if self.params.repeat_max is None: 168 | return random.uniform(0.0, 1.0) 169 | if self.count == self.params.repeat_max: 170 | return 0.0 171 | if self.count < self.params.repeat_min: 172 | return 1 - (1 - alpha) * self.count / self.params.repeat_min 173 | if self.params.repeat_min == self.params.repeat_max: 174 | return alpha 175 | return alpha * (self.params.repeat_max - self.count) / (self.params.repeat_max - self.params.repeat_min) 176 | 177 | def probability(self, now: int) -> float: 178 | """Wrapper to call the priority function 179 | 180 | If the event :attr:`count` is equal to the :attr:`repeat_max` parameters, 181 | it yields a ``0`` probability. Otherwise, it computes the :meth:`priority` function 182 | described above. 183 | 184 | Args: 185 | now (int): current timestamp in minutes 186 | 187 | Returns: 188 | float: event probability [0-1] 189 | """ 190 | p = 0.0 191 | if self.count == self.params.repeat_max: 192 | return p 193 | 194 | noise = self.get_noise() # minutes 195 | for interval in self.params.schedule: 196 | a, b = interval 197 | if a - noise <= now <= b + noise: 198 | p = self.priority() 199 | break 200 | 201 | return p 202 | 203 | def valid(self) -> bool: 204 | """Computes whether the event count has reached the :attr:`repeat_max` limit. 205 | 206 | It yields ``True`` 207 | if :attr:`repeat_max` is ``undefined`` or 208 | if the event :attr:`count` is less than :attr:`repeat_max`. 209 | Otherwise, it yields ``False``. 210 | 211 | Returns: 212 | bool: valid event 213 | """ 214 | if self.params.repeat_max is None: 215 | return True 216 | return self.count < self.params.repeat_max 217 | 218 | def consume(self) -> None: 219 | """Increments one unit the event count""" 220 | self.count += 1 221 | # logging.info("Event %s repeated %d out of %d" % (self.name, self.count, self.target)) 222 | 223 | def supply(self) -> None: 224 | """Decrements one unit the event count""" 225 | self.count -= 1 226 | -------------------------------------------------------------------------------- /archABM/options.py: -------------------------------------------------------------------------------- 1 | from simpy import Environment 2 | 3 | from .database import Database 4 | from .parameters import Parameters 5 | 6 | 7 | class Options: 8 | """Stores general options for the simulation""" 9 | 10 | env: Environment 11 | db: Database 12 | params: Parameters 13 | 14 | def __init__(self, env: Environment, db: Database, params: Parameters) -> None: 15 | self.env = env 16 | self.db = db 17 | self.params = params 18 | -------------------------------------------------------------------------------- /archABM/parameters.py: -------------------------------------------------------------------------------- 1 | from typing import Generator 2 | 3 | 4 | class Parameters: 5 | """Helper to access dictionary elements as class attributes 6 | """ 7 | 8 | __dict__: dict 9 | 10 | def __init__(self, __dict__: dict = None) -> None: 11 | if __dict__ is not None: 12 | self.__dict__ = __dict__ 13 | 14 | def __repr__(self) -> str: 15 | return str(self.__dict__) 16 | 17 | def __iter__(self) -> Generator[str, None, None]: 18 | for key, value in self.__dict__.items(): 19 | yield key, value 20 | 21 | def __copy__(self): 22 | newone = type(self)() 23 | newone.__dict__.update(self.__dict__) 24 | return newone 25 | 26 | def copy(self): 27 | """Return a shallow copy of the instance. 28 | 29 | Returns: 30 | Parameters: cloned object 31 | """ 32 | newone = type(self)() 33 | newone.__dict__.update(self.__dict__) 34 | return newone 35 | -------------------------------------------------------------------------------- /archABM/person.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from simpy import Environment, Interrupt, Process, Timeout 4 | 5 | from .database import Database 6 | from .place import Place 7 | from .event import Event 8 | from .event_generator import EventGenerator 9 | from .event_model import EventModel 10 | from .parameters import Parameters 11 | from .snapshot_person import SnapshotPerson 12 | 13 | 14 | class Person: 15 | """Person primitive""" 16 | 17 | id = -1 18 | 19 | def __init__(self, env: Environment, db: Database, params: Parameters) -> None: 20 | self.next() 21 | self.id = Person.id 22 | 23 | self.env = env 24 | self.db = db 25 | self.params = params 26 | 27 | self.generator = EventGenerator(env, db) 28 | self.current_process = None 29 | self.place = None 30 | self.event = None 31 | self.model = None 32 | self.duration = None 33 | 34 | self.status = 0 # 0: susceptible, 1: infective 35 | 36 | self.CO2_level = self.db.model.params.CO2_background 37 | self.quanta_inhaled = 0.0 38 | 39 | self.elapsed = 0.0 40 | self.last_updated = 0 41 | 42 | self.snapshot = SnapshotPerson() 43 | 44 | @classmethod 45 | def reset(cls) -> None: 46 | """Resets :class:`~archABM.person.Person` ID.""" 47 | Person.id = -1 48 | 49 | @staticmethod 50 | def next() -> None: 51 | """Increments one unit the :class:`~archABM.person.Person` ID.""" 52 | Person.id += 1 53 | 54 | def start(self) -> None: 55 | """Initiates the event queue processing""" 56 | logging.info("[%.2f] Person %d starting up" % (self.env.now, self.id)) 57 | self.env.process(self.process()) 58 | 59 | def process(self) -> None: 60 | """Processes the chain of discrete events 61 | 62 | The person is moved from the current :class:`~archABM.place.Place` to the new one based on the generated :class:`~archABM.event.Event`, 63 | and stays there for a defined duration (in minutes). 64 | 65 | Once an event or task get fulfilled, the :class:`~archABM.event_generator.EventGenerator` produces a new :class:`~archABM.event.Event`. 66 | If, after a limited number of trials, the :class:`~archABM.event_generator.EventGenerator` is not able to correctly generate an event, 67 | a random one is created. 68 | 69 | If a person gets interrupted while carrying out (waiting) its current task, the assigned event happens to be the new one. 70 | 71 | .. note:: 72 | State snapshots are taken after each event if fulfilled. 73 | 74 | Yields: 75 | Process: an event yielding generator 76 | """ 77 | cont_event = 0 78 | cont_event_max = 1000 # TODO: review maximum allowed number of events per person 79 | while True: 80 | # generate event 81 | cont_generator = 0 82 | cont_generator_max = 3 83 | while self.event is None: # TODO: review maximum allowed number of events per person 84 | self.event = self.generator.generate(self.env.now, self) 85 | cont_generator += 1 86 | # if exceeded, generate random event 87 | if cont_generator > cont_generator_max: 88 | duration = self.model.duration(self.env.now) 89 | self.event = Event(self.model, self.place, duration) 90 | break 91 | self.model = self.event.model 92 | self.duration = self.event.duration 93 | activity = self.model.params.activity 94 | 95 | # TODO: review if we want to save only new places or all of them => self.place != self.event.place and not self.event.place.full() 96 | # move from current place to new one 97 | if self.event is not None and self.event.place is not None: 98 | 99 | if self.place != self.event.place and not self.event.place.full(): 100 | # remove from current place 101 | if self.place is not None: 102 | self.place.remove_person(self) 103 | 104 | # add to new place 105 | self.place = self.event.place 106 | self.place.add_person(self, self.event) 107 | else: 108 | self.place.update_place() 109 | 110 | # save snapshot (if first event or elapsed time > 0) 111 | elapsed = self.env.now - self.last_updated 112 | if elapsed > 0 or cont_event == 0: 113 | self.save_snapshot() 114 | pass 115 | 116 | logging.info("[%.2f] Person %d event %s at place %s for %d minutes" % ( 117 | self.env.now, self.id, self.model.params.activity, self.place.params.name, self.duration,)) 118 | 119 | self.event = None 120 | self.last_updated = self.env.now 121 | self.current_process = self.env.process(self.wait()) 122 | yield self.current_process 123 | 124 | cont_event += 1 125 | if cont_event > cont_event_max: 126 | break 127 | 128 | def wait(self) -> None: 129 | """Wait for a certain amount of time 130 | 131 | Yields: 132 | Timeout: event triggered after a delay has passed 133 | """ 134 | try: 135 | yield self.env.timeout(self.duration) 136 | except Interrupt: 137 | # print("interrupted") 138 | pass 139 | 140 | def assign_event(self, event: Event) -> None: 141 | """Interrupt current task and assign new event 142 | 143 | Args: 144 | event (Event): new assigned event 145 | """ 146 | if self.current_process is not None and not self.current_process.triggered: 147 | self.current_process.interrupt("Need to go!") 148 | logging.info("[%.2f] Person %d interrupted current event" % (self.env.now, self.id)) 149 | self.event = event 150 | self.generator.consume_activity(event.model) 151 | 152 | # TODO: review if we need to update the risk of infected people as well 153 | # TODO: review infection risk metric: average vs cumulative 154 | def update(self, elapsed: float, quanta_inhaled: float, CO2_level: float) -> None: 155 | """Update the infection risk probability and the CO\ :sub:`2` concentration (ppm). 156 | 157 | Args: 158 | elapsed (float): event elapsed time (in minutes) 159 | infection_risk (float): infection risk probability 160 | CO2_level (float): CO\ :sub:`2` concentration (ppm) 161 | """ 162 | # self.elapsed += elapsed 163 | # self.infection_risk_avg += elapsed * (infection_risk - self.infection_risk_avg) / self.elapsed 164 | # self.infection_risk_cum += infection_risk 165 | # self.CO2_level += elapsed * (CO2_level - self.CO2_level) / self.elapsed 166 | 167 | self.CO2_level = CO2_level 168 | self.quanta_inhaled += quanta_inhaled 169 | 170 | def save_snapshot(self) -> None: 171 | """Saves state snapshot on :class:`~archABM.snapshot_person.SnapshotPerson`""" 172 | self.snapshot.set("run", self.db.run) 173 | self.snapshot.set("time", self.env.now, 0) 174 | self.snapshot.set("person", self.id) 175 | self.snapshot.set("status", self.status) 176 | self.snapshot.set("place", self.place.id) 177 | self.snapshot.set("event", self.model.id) 178 | self.snapshot.set("CO2_level", self.CO2_level, 2) 179 | self.snapshot.set("quanta_inhaled", self.quanta_inhaled, 6) 180 | self.db.results.write_person(self.snapshot) 181 | -------------------------------------------------------------------------------- /archABM/place.py: -------------------------------------------------------------------------------- 1 | from random import randrange 2 | from simpy import Environment 3 | 4 | from .database import Database 5 | from .parameters import Parameters 6 | from .snapshot_place import SnapshotPlace 7 | 8 | 9 | class Place: 10 | """Place primitive""" 11 | 12 | id: int = -1 13 | 14 | def __init__(self, env: Environment, db: Database, params: Parameters) -> None: 15 | self.next() 16 | self.id = Place.id 17 | 18 | self.env = env 19 | self.db = db 20 | self.params = params 21 | 22 | self.people = [] 23 | self.num_people = 0 24 | self.infective_people = 0 25 | 26 | self.CO2_baseline = self.db.model.params.CO2_background 27 | self.CO2_level = self.CO2_baseline 28 | self.quanta_level = 0.0 29 | self.temperature = self.db.model.params.temperature 30 | self.relative_humidity = self.db.model.params.relative_humidity 31 | 32 | self.elapsed = 0.0 33 | self.last_updated = 0.0 34 | 35 | self.events = self.get_events() 36 | self.event = None 37 | self.snapshot = SnapshotPlace() 38 | 39 | @classmethod 40 | def reset(cls) -> None: 41 | """Resets :class:`~archABM.place.Place` ID.""" 42 | Place.id = -1 43 | 44 | @staticmethod 45 | def next() -> None: 46 | """Increments one unit the :class:`~archABM.place.Place` ID.""" 47 | Place.id += 1 48 | 49 | def get_events(self) -> None: 50 | """Yields the corresponding :class:`~archABM.event_model.EventModel` 51 | 52 | Returns: 53 | EventModel: place's type of activity 54 | """ 55 | events = [] 56 | for e in self.db.events: 57 | if e.params.activity in self.params.activity: 58 | events.append(e) 59 | return events 60 | 61 | def add_person(self, person, event): 62 | """Add person to place 63 | 64 | Prior to the inclusion of the person, the ``air quality`` of the place is updated. 65 | Then, the number of people in the place is incremented by one unit, 66 | and the number of infective people is updated in case the entering person's status is ``infective``. 67 | Finally, a ``snapshot`` is taken and saved into the simulation history. 68 | 69 | Args: 70 | person (Person): person to be added 71 | """ 72 | # update air quality 73 | self.update_air(event) 74 | 75 | # add to list 76 | self.people.append(person) 77 | self.num_people += 1 78 | self.infective_people += person.status 79 | 80 | # save snapshot 81 | self.save_snapshot() 82 | 83 | def remove_person(self, person) -> None: 84 | """Remove person from place 85 | 86 | Prior to the exclusion of the person, the ``air quality`` of the place is updated. 87 | Then, the number of people in the place is decremented by one unit, 88 | and the number of infective people is updated in case the leaving person's status is ``infective``. 89 | Finally, a ``snapshot`` is taken and saved into the simulation history. 90 | 91 | Args: 92 | person (Person): person to be removed 93 | """ 94 | # update air quality 95 | self.update_air() 96 | 97 | # remove from list 98 | self.people.remove(person) 99 | self.num_people -= 1 100 | self.infective_people -= person.status 101 | 102 | # save snapshot 103 | self.save_snapshot() 104 | 105 | def update_place(self): 106 | """Update place air quality 107 | 108 | Updates the ``air quality`` of the place and saves a ``snapshot`` into the simulation history. 109 | """ 110 | # update air quality 111 | self.update_air() 112 | 113 | # save snapshot 114 | self.save_snapshot() 115 | 116 | def update_air(self, event=None) -> None: 117 | """Air quality update 118 | 119 | This method updates the air quality based on the selected :class:`~archABM.aerosol_model.AerosolModel`. 120 | 121 | The infection risk is also computed by the aerosol model, 122 | and is transferred to every person in the room. 123 | 124 | """ 125 | elapsed = self.env.now - self.last_updated 126 | if event is None: 127 | event = self.event 128 | good = any(event.model.params.activity == e.params.activity for e in self.events) 129 | if not good: 130 | for e in self.events: 131 | print(f, event.model.params.activity, e.params.activity, good) 132 | raise BaseException 133 | if event.model.params.shared and elapsed > 0: 134 | inputs = Parameters( 135 | { 136 | "room_area": self.params.area, 137 | "room_height": self.params.height, 138 | "room_ventilation_rate": self.params.ventilation, 139 | "recirculated_flow_rate": self.params.recirculated_flow_rate, 140 | "mask_efficiency": event.model.params.mask_efficiency, 141 | "event_duration": elapsed / 60, 142 | "num_people": self.num_people, 143 | "infective_people": self.infective_people, 144 | "CO2_level": self.CO2_level, 145 | "quanta_level": self.quanta_level, 146 | "temperature": self.temperature, 147 | "relative_humidity": self.relative_humidity 148 | } 149 | ) 150 | CO2_level, quanta_inhaled, quanta_level, temperature, relative_humidity = self.db.model.get_risk(inputs) 151 | 152 | # update place 153 | self.CO2_level = CO2_level 154 | self.quanta_level = quanta_level 155 | self.temperature = temperature 156 | self.relative_humidity = relative_humidity 157 | 158 | # self.elapsed += elapsed 159 | # self.infection_risk_avg += elapsed * (infection_risk - self.infection_risk_avg) / self.elapsed 160 | # self.infection_risk_cum += infection_risk 161 | 162 | # update people # TODO: review if we need to update the risk of infected people as well 163 | for p in self.people: 164 | p.update(elapsed, quanta_inhaled, CO2_level) 165 | self.last_updated = self.env.now 166 | self.event = event 167 | 168 | def people_attending(self) -> int: 169 | """Number of people attending a collective event 170 | 171 | .. note:: 172 | If the place is full, this method yields ``0`` people. 173 | 174 | Returns: 175 | int: number of people 176 | """ 177 | if self.full(): 178 | return 0 179 | return randrange(int(self.params.capacity - self.num_people)) 180 | 181 | def full(self) -> bool: 182 | """Checks whether the place is full ``num_people < capacity`` 183 | 184 | Returns: 185 | bool: place is full 186 | """ 187 | return self.params.capacity == self.num_people 188 | 189 | def save_snapshot(self) -> None: 190 | """Saves state snapshot on :class:`~archABM.snapshot_place.SnapshotPlace`""" 191 | self.snapshot.set("run", self.db.run) 192 | self.snapshot.set("time", self.env.now, 0) 193 | self.snapshot.set("place", self.id) 194 | self.snapshot.set("activity", self.event.model.params.activity) 195 | self.snapshot.set("num_people", self.num_people) 196 | self.snapshot.set("infective_people", self.infective_people) 197 | self.snapshot.set("CO2_level", self.CO2_level, 2) 198 | self.snapshot.set("quanta_level", self.quanta_level, 6) 199 | self.snapshot.set("temperature", self.temperature, 2) 200 | self.snapshot.set("relative_humidity", self.relative_humidity, 2) 201 | self.db.results.write_place(self.snapshot) 202 | -------------------------------------------------------------------------------- /archABM/results.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import json 3 | import logging 4 | import os 5 | 6 | from .person import Person 7 | from .place import Place 8 | from .snapshot_person import SnapshotPerson 9 | from .snapshot_place import SnapshotPlace 10 | 11 | 12 | class Results: 13 | """Simulation history processing and export""" 14 | 15 | output: dict 16 | 17 | def __init__(self, config: dict) -> None: 18 | # TODO: review hardcoded names 19 | self.people_name = "people" 20 | self.places_name = "places" 21 | self.results_name = "results" 22 | self.config_name = "config" 23 | self.output_name = "output" 24 | self.log_name = "app.log" 25 | 26 | self.config = config 27 | 28 | self.log = False 29 | self.save_log = self.config["options"]["save_log"] 30 | self.save_config = self.config["options"]["save_config"] 31 | self.save_csv = self.config["options"]["save_csv"] 32 | self.save_json = self.config["options"]["save_json"] 33 | self.return_output = self.config["options"]["return_output"] 34 | 35 | self.output = None 36 | 37 | if self.save_log or self.save_config or self.save_csv or self.save_json: 38 | self.mkpath() 39 | self.mkdir() 40 | 41 | self.setup_log() 42 | if self.save_config: 43 | self.write_config() 44 | if self.save_csv: 45 | self.open_people_csv() 46 | self.open_places_csv() 47 | if self.save_json: 48 | self.open_json() 49 | if self.return_output or self.save_json: 50 | self.init_results() 51 | 52 | def mkpath(self) -> None: 53 | """Creates the path where the simulation results should be saved. 54 | 55 | If the ``directory`` option is specified, another folder level is added to the path. 56 | """ 57 | cwd = os.getcwd() 58 | now = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S-%f") 59 | folder = "results" 60 | self.path = os.path.join(cwd, folder, now) 61 | if "directory" in self.config["options"]: 62 | directory = self.config["options"]["directory"] 63 | if directory is not None: 64 | self.path = os.path.join(cwd, folder, directory, now) 65 | 66 | def mkdir(self) -> None: 67 | """Creates the directory where the simulation results should be saved.""" 68 | os.makedirs(self.path) 69 | 70 | def setup_log(self) -> None: 71 | """Logging setup""" 72 | if self.save_log: 73 | logging.basicConfig( 74 | filename=os.path.join(self.path, self.log_name), filemode="w", format="%(message)s", level=logging.INFO, 75 | ) 76 | elif self.log: 77 | logging.basicConfig(format="%(message)s", level=logging.INFO) 78 | else: 79 | logging.disable(logging.INFO) 80 | 81 | def open_people_csv(self) -> None: 82 | """Creates and opens the *csv* file to save people state history""" 83 | self.people_csv = open(os.path.join(self.path, self.people_name + ".csv"), "a") 84 | self.people_csv.write(SnapshotPerson.get_header()) 85 | 86 | def close_people_csv(self) -> None: 87 | """Closes the people *csv* file""" 88 | self.people_csv.close() 89 | 90 | def open_places_csv(self) -> None: 91 | """Creates and opens the *csv* file to save places state history""" 92 | self.places_csv = open(os.path.join(self.path, self.places_name + ".csv"), "a") 93 | self.places_csv.write(SnapshotPlace.get_header()) 94 | 95 | def close_places_csv(self) -> None: 96 | """Closes the places *csv* file""" 97 | self.places_csv.close() 98 | 99 | def open_json(self) -> None: 100 | """Creates and opens the *json* file to save all results""" 101 | self.output_json = open(os.path.join(self.path, self.output_name + ".json"), "w") 102 | 103 | def write_json(self) -> None: 104 | """Writes the :attr:`output` dictionary to the *json* file""" 105 | json.dump(self.output, self.output_json) 106 | 107 | def close_json(self) -> None: 108 | """Closes the *json* file""" 109 | self.output_json.close() 110 | 111 | def init_results(self) -> None: 112 | """Initializes the results dictionary""" 113 | self.output = {} 114 | self.results = dict.fromkeys([self.people_name, self.places_name], {}) 115 | self.results[self.people_name] = dict.fromkeys(SnapshotPerson.header) 116 | self.results[self.places_name] = dict.fromkeys(SnapshotPlace.header) 117 | 118 | self.output[self.config_name] = self.config 119 | self.output[self.results_name] = self.results 120 | 121 | for key in SnapshotPerson.header: 122 | self.results[self.people_name][key] = [] 123 | for key in SnapshotPlace.header: 124 | self.results[self.places_name][key] = [] 125 | 126 | def write_person(self, person: Person) -> None: 127 | """Appends a new row to the person state history. 128 | 129 | Args: 130 | person (Person): person state to be saved 131 | """ 132 | if self.save_csv: 133 | self.people_csv.write(person.get_data()) 134 | if self.save_json or self.return_output: 135 | for key, value in person.store.items(): 136 | self.results[self.people_name][key].append(value) 137 | 138 | def write_place(self, place: Place) -> None: 139 | """Appends a new row to the place state history. 140 | 141 | Args: 142 | place (Place): place state to be saved 143 | """ 144 | if self.save_csv: 145 | self.places_csv.write(place.get_data()) 146 | if self.save_json or self.return_output: 147 | for key, value in place.store.items(): 148 | self.results[self.places_name][key].append(value) 149 | 150 | def write_config(self) -> None: 151 | """Writes the configuration dictionary into a *json* file.""" 152 | with open(os.path.join(self.path, self.config_name + ".json"), "w") as f: 153 | json.dump(self.config, f) 154 | 155 | def done(self) -> None: 156 | """Closes all file connections and returns a :obj:`dict` with the complete simulation history. 157 | 158 | Returns: 159 | dict: complete simulation history 160 | """ 161 | if self.save_csv: 162 | self.close_people_csv() 163 | self.close_places_csv() 164 | if self.save_json: 165 | self.write_json() 166 | self.close_json() 167 | if self.return_output: 168 | return self.output 169 | return None 170 | -------------------------------------------------------------------------------- /archABM/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Configuration Schema", 3 | "type": "object", 4 | "properties": { 5 | "events": { 6 | "title": "Events", 7 | "type": "array", 8 | "minItems": 1, 9 | "uniqueItems": true, 10 | "items": { 11 | "type": "object", 12 | "properties": { 13 | "activity": { 14 | "type": "string" 15 | }, 16 | "schedule": { 17 | "type": "array", 18 | "minItems": 1, 19 | "uniqueItems": true, 20 | "items": { 21 | "type": "array", 22 | "minItems": 2, 23 | "maxItems": 2, 24 | "items": { 25 | "type": "integer" 26 | } 27 | } 28 | }, 29 | "repeat_min": { 30 | "type": ["integer", "null"] 31 | }, 32 | "repeat_max": { 33 | "type": ["integer", "null"] 34 | }, 35 | "duration_min": { 36 | "type": "integer", 37 | "minimum": 0 38 | }, 39 | "duration_max": { 40 | "type": "integer", 41 | "minimum": 0 42 | }, 43 | "mask_efficiency": { 44 | "type": ["number", "null"], 45 | "minimum": 0, 46 | "maximum": 1 47 | }, 48 | "collective": { 49 | "type": "boolean" 50 | }, 51 | "shared": { 52 | "type": "boolean" 53 | }, 54 | "allow": { 55 | "type": "boolean" 56 | } 57 | 58 | }, 59 | "required": ["activity", "schedule", "repeat_min", "repeat_max", "duration_min", "duration_max", "mask_efficiency", "collective", "shared", "allow"] 60 | } 61 | }, 62 | "places": { 63 | "title": "Places", 64 | "type": "array", 65 | "minItems": 1, 66 | "uniqueItems": true, 67 | "items": { 68 | "type": "object", 69 | "properties": { 70 | "name": { 71 | "type": "string" 72 | }, 73 | "activity": { 74 | "type": ["array", "null"], 75 | "items": { 76 | "type": "string" 77 | }, 78 | "minItems": 1 79 | }, 80 | "building": { 81 | "type": ["string", "null"] 82 | }, 83 | "department": { 84 | "type": ["array", "null"], 85 | "items": { 86 | "type": "string" 87 | }, 88 | "minItems": 1 89 | }, 90 | "area": { 91 | "type": ["number", "null"] 92 | }, 93 | "height": { 94 | "type": ["number", "null"] 95 | }, 96 | "capacity": { 97 | "type": ["integer", "null"] 98 | }, 99 | "ventilation": { 100 | "type": ["number", "null"] 101 | }, 102 | "recirculated_flow_rate": { 103 | "type": ["number", "null"] 104 | }, 105 | "allow": { 106 | "type": "boolean" 107 | } 108 | }, 109 | "required": ["name", "activity", "building", "department", "area", "height", "capacity", "ventilation", "recirculated_flow_rate", "allow"] 110 | } 111 | }, 112 | "people": { 113 | "title": "People", 114 | "type": "array", 115 | "minItems": 1, 116 | "uniqueItems": true, 117 | "items": { 118 | "type": "object", 119 | "properties": { 120 | "department": { 121 | "type": "string" 122 | }, 123 | "building": { 124 | "type": "string" 125 | }, 126 | "num_people": { 127 | "type": "integer", 128 | "minimum": 0 129 | } 130 | }, 131 | "required": ["department", "building", "num_people"] 132 | } 133 | }, 134 | "options": { 135 | "title": "Options", 136 | "type": "object", 137 | "properties": { 138 | "movement_buildings": { 139 | "type": "boolean" 140 | }, 141 | "movement_department": { 142 | "type": "boolean" 143 | }, 144 | "number_runs": { 145 | "type": "integer", 146 | "minimum": 1 147 | }, 148 | "save_log": { 149 | "type": "boolean" 150 | }, 151 | "save_config": { 152 | "type": "boolean" 153 | }, 154 | "save_csv": { 155 | "type": "boolean" 156 | }, 157 | "save_json": { 158 | "type": "boolean" 159 | }, 160 | "return_output": { 161 | "type": "boolean" 162 | }, 163 | "directory": { 164 | "type": ["string", "null"] 165 | }, 166 | "ratio_infected": { 167 | "type": "number", 168 | "minimum": 0, 169 | "maximum": 1 170 | }, 171 | "model": { 172 | "type": "string" 173 | }, 174 | "model_parameters": { 175 | "type": "object" 176 | } 177 | 178 | }, 179 | "required": ["movement_buildings", "movement_department", "number_runs", "save_log", "save_config", "save_csv", "save_json", "return_output", "directory", "ratio_infected", "model", "model_parameters"] 180 | } 181 | }, 182 | "required": ["events", "places", "people", "options"] 183 | } -------------------------------------------------------------------------------- /archABM/snapshot.py: -------------------------------------------------------------------------------- 1 | class Snapshot: 2 | """Stores the state of an agent at a given time 3 | """ 4 | 5 | header: list 6 | store: dict 7 | 8 | def __init__(self, header) -> None: 9 | self.header = header 10 | self.store = {} 11 | self.reset() 12 | 13 | def reset(self) -> None: 14 | """Initializes dictionary :attr:`store` with :attr:`header` as keys.""" 15 | self.store = dict.fromkeys(self.header, "") 16 | 17 | @classmethod 18 | def get_header(cls, sep=",") -> str: 19 | """Takes all items in :attr:`header` and joins them into one string. 20 | 21 | A string must be specified as the separator, by default "," 22 | 23 | Args: 24 | sep (str, optional): separator. Defaults to ",". 25 | 26 | Returns: 27 | str: a string created by joining the elements of :attr:`header` by string separator 28 | """ 29 | return sep.join(cls.header) + "\n" 30 | 31 | def get_data(self, sep=",") -> str: 32 | """Takes all items in :attr:`store` and joins them into one string. 33 | 34 | A string must be specified as the separator, by default "," 35 | 36 | Args: 37 | sep (str, optional): separator. Defaults to ",". 38 | 39 | Returns: 40 | str: a string created by joining the elements of :attr:`store` by string separator 41 | """ 42 | return sep.join(map(str, self.store.values())) + "\n" 43 | 44 | def set(self, key: str, value, digits: int = 2) -> None: 45 | """Add a key:value pair to the :attr:`store` dictionary 46 | 47 | * If :attr:`value` is :obj:`bool`, it is casted to :obj:`int`. 48 | * If :attr:`value` is :obj:`list`, the elements are joined into one :obj:`str`, separated by ";". 49 | * If :attr:`value` is :obj:`float`, it is rounded to the specified number :attr:`digits`. 50 | * If :attr:`digits` is :const:`0`, :attr:`value` is casted to :obj:`int`. 51 | 52 | Args: 53 | key (str): item key 54 | value (Union[bool, int, float, list]): item value 55 | digits (int, optional): number of digits to store. Defaults to 2. 56 | 57 | Raises: 58 | BaseException: raises if the specified :attr:`key` is not a key in :attr:`store` 59 | """ 60 | if key in self.header: 61 | typ = type(value) 62 | if typ == bool: 63 | value = int(value) 64 | elif typ == int: 65 | pass 66 | elif typ == float: 67 | if digits == 0: 68 | value = int(value) 69 | else: 70 | value = round(value, digits) 71 | elif typ == list: 72 | value = ";".join(map(str, value)) 73 | self.store[key] = value 74 | else: 75 | raise BaseException 76 | -------------------------------------------------------------------------------- /archABM/snapshot_person.py: -------------------------------------------------------------------------------- 1 | from .snapshot import Snapshot 2 | 3 | 4 | class SnapshotPerson(Snapshot): 5 | """Stores the state of a person at a given time 6 | 7 | It saves the following attributes: 8 | 9 | .. list-table:: 10 | :header-rows: 1 11 | 12 | * - Attribute 13 | - Description 14 | - Type 15 | * - *run* 16 | - Simulation run 17 | - :obj:`int` 18 | * - *time* 19 | - Simulation time (minutes) 20 | - :obj:`int` 21 | * - *person* 22 | - Person ID 23 | - :obj:`int` 24 | * - *status* 25 | - Person status (0: susceptible, 1: infective) 26 | - :obj:`bool` 27 | * - *place* 28 | - Place ID 29 | - :obj:`int` 30 | * - *event* 31 | - Event ID 32 | - :obj:`int` 33 | * - *CO2_level* 34 | - Average CO\ :sub:`2` level (ppm) 35 | - :obj:`float` 36 | * - *quanta_inhaled* 37 | - Quanta inhaled (quanta) 38 | - :obj:`float` 39 | """ 40 | 41 | header = ["run", "time", "person", "status", "place", "event", "CO2_level", "quanta_inhaled"] 42 | 43 | def __init__(self) -> None: 44 | super(SnapshotPerson, self).__init__(SnapshotPerson.header) 45 | -------------------------------------------------------------------------------- /archABM/snapshot_place.py: -------------------------------------------------------------------------------- 1 | from .snapshot import Snapshot 2 | 3 | 4 | class SnapshotPlace(Snapshot): 5 | """Stores the state of a place at a given time 6 | 7 | It saves the following attributes: 8 | 9 | .. list-table:: 10 | :header-rows: 1 11 | 12 | * - Attribute 13 | - Description 14 | - Type 15 | * - *run* 16 | - Simulation run 17 | - :obj:`int` 18 | * - *time* 19 | - Simulation time (minutes) 20 | - :obj:`int` 21 | * - *place* 22 | - Place ID 23 | - :obj:`int` 24 | * - *activity* 25 | - Activity 26 | - :obj:`str` 27 | * - *num_people* 28 | - Number of people 29 | - :obj:`int` 30 | * - *infective_people* 31 | - Number of infective people 32 | - :obj:`int` 33 | * - *CO2_level* 34 | - CO\ :sub:`2` level (ppm) 35 | - :obj:`float` 36 | * - *quanta_level* 37 | - quanta level (ppm) 38 | - :obj:`float` 39 | * - *temperature* 40 | - Room temperature 41 | - :obj:`float` 42 | * - *relative_humidity* 43 | - Room relative humidity 44 | - :obj:`float` 45 | """ 46 | 47 | header = ["run", "time", "place", "activity", "num_people", "infective_people", "CO2_level", "quanta_level", "temperature", "relative_humidity"] 48 | 49 | def __init__(self) -> None: 50 | super(SnapshotPlace, self).__init__(SnapshotPlace.header) 51 | -------------------------------------------------------------------------------- /data/config_basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "events": [{ 3 | "activity": "home", 4 | "schedule": [ 5 | [0, 480], 6 | [1020, 1440] 7 | ], 8 | "repeat_min": 0, 9 | "repeat_max": null, 10 | "duration_min": 300, 11 | "duration_max": 360, 12 | "mask_efficiency": null, 13 | "collective": false, 14 | "shared": false, 15 | "allow": true 16 | }, 17 | { 18 | "activity": "work", 19 | "schedule": [ 20 | [480, 1020] 21 | ], 22 | "repeat_min": 0, 23 | "repeat_max": null, 24 | "duration_min": 30, 25 | "duration_max": 60, 26 | "mask_efficiency": 0.8, 27 | "collective": false, 28 | "shared": true, 29 | "allow": true 30 | } 31 | ], 32 | "places": [{ 33 | "name": "home", 34 | "activity": ["home"], 35 | "building": null, 36 | "department": null, 37 | "area": null, 38 | "height": null, 39 | "capacity": null, 40 | "ventilation": null, 41 | "recirculated_flow_rate": null, 42 | "allow": true 43 | }, 44 | { 45 | "name": "office1", 46 | "activity": ["work"], 47 | "building": "building1", 48 | "department": ["department1"], 49 | "area": 200.0, 50 | "height": 3.0, 51 | "capacity": 50, 52 | "ventilation": 3, 53 | "recirculated_flow_rate": 0, 54 | "allow": true 55 | }, 56 | { 57 | "name": "office2", 58 | "activity": ["work"], 59 | "building": "building1", 60 | "department": ["department2"], 61 | "area": 200.0, 62 | "height": 3.0, 63 | "capacity": 50, 64 | "ventilation": 3, 65 | "recirculated_flow_rate": 0, 66 | "allow": true 67 | } 68 | ], 69 | "people": [{ 70 | "department": "department1", 71 | "building": "building1", 72 | "num_people": 20 73 | }, 74 | { 75 | "department": "department2", 76 | "building": "building1", 77 | "num_people": 20 78 | } 79 | ], 80 | "options": { 81 | "movement_buildings": true, 82 | "movement_department": true, 83 | "number_runs": 1, 84 | "save_log": true, 85 | "save_config": true, 86 | "save_csv": false, 87 | "save_json": false, 88 | "return_output": false, 89 | "directory": null, 90 | "ratio_infected": 0.05, 91 | "model": "Colorado", 92 | "model_parameters": { 93 | "Colorado": { 94 | "pressure": 0.95, 95 | "temperature": 20, 96 | "relative_humidity": 50, 97 | "CO2_background": 415, 98 | "decay_rate": 0.62, 99 | "deposition_rate": 0.3, 100 | "hepa_flow_rate": 0.0, 101 | "filter_efficiency": 0.20, 102 | "ducts_removal": 0.10, 103 | "other_removal": 0.00, 104 | "fraction_immune": 0, 105 | "breathing_rate": 0.52, 106 | "CO2_emission_person": 0.005, 107 | "quanta_exhalation": 25, 108 | "quanta_enhancement": 1, 109 | "people_with_masks": 1.00 110 | } 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /data/config_toy.json: -------------------------------------------------------------------------------- 1 | { 2 | "events": [{ 3 | "activity": "home", 4 | "schedule": [ 5 | [0, 480], 6 | [1020, 1440] 7 | ], 8 | "repeat_min": 0, 9 | "repeat_max": null, 10 | "duration_min": 300, 11 | "duration_max": 360, 12 | "mask_efficiency": null, 13 | "collective": false, 14 | "shared": false, 15 | "allow": true 16 | }, 17 | { 18 | "activity": "work", 19 | "schedule": [ 20 | [480, 1020] 21 | ], 22 | "repeat_min": 0, 23 | "repeat_max": null, 24 | "duration_min": 30, 25 | "duration_max": 60, 26 | "mask_efficiency": 0.8, 27 | "collective": false, 28 | "shared": true, 29 | "allow": true 30 | }, 31 | { 32 | "activity": "meeting", 33 | "schedule": [ 34 | [540, 960] 35 | ], 36 | "repeat_min": 0, 37 | "repeat_max": 5, 38 | "duration_min": 15, 39 | "duration_max": 60, 40 | "mask_efficiency": 0.8, 41 | "collective": false, 42 | "shared": true, 43 | "allow": true 44 | }, 45 | { 46 | "activity": "lunch", 47 | "schedule": [ 48 | [780, 900] 49 | ], 50 | "repeat_min": 1, 51 | "repeat_max": 1, 52 | "duration_min": 20, 53 | "duration_max": 45, 54 | "mask_efficiency": 0.0, 55 | "collective": true, 56 | "shared": true, 57 | "allow": true 58 | }, 59 | { 60 | "activity": "coffee", 61 | "schedule": [ 62 | [600, 660], 63 | [900, 960] 64 | ], 65 | "repeat_min": 0, 66 | "repeat_max": 2, 67 | "duration_min": 5, 68 | "duration_max": 15, 69 | "mask_efficiency": 0.5, 70 | "collective": false, 71 | "shared": true, 72 | "allow": true 73 | }, 74 | { 75 | "activity": "restroom", 76 | "schedule": [ 77 | [480, 1020] 78 | ], 79 | "repeat_min": 0, 80 | "repeat_max": 4, 81 | "duration_min": 2, 82 | "duration_max": 5, 83 | "mask_efficiency": 0.5, 84 | "collective": false, 85 | "shared": true, 86 | "allow": true 87 | } 88 | ], 89 | "places": [{ 90 | "name": "home", 91 | "activity": ["home"], 92 | "building": null, 93 | "department": null, 94 | "area": null, 95 | "height": null, 96 | "capacity": null, 97 | "ventilation": null, 98 | "recirculated_flow_rate": null, 99 | "allow": true 100 | }, 101 | { 102 | "name": "office1", 103 | "activity": ["work"], 104 | "building": "building1", 105 | "department": ["department1"], 106 | "area": 200.0, 107 | "height": 3.0, 108 | "capacity": 50, 109 | "ventilation": 3, 110 | "recirculated_flow_rate": 0, 111 | "allow": true 112 | }, 113 | { 114 | "name": "office2", 115 | "activity": ["work"], 116 | "building": "building1", 117 | "department": ["department2"], 118 | "area": 200.0, 119 | "height": 3.0, 120 | "capacity": 50, 121 | "ventilation": 3, 122 | "recirculated_flow_rate": 0, 123 | "allow": true 124 | }, 125 | { 126 | "name": "meeting1", 127 | "activity": ["meeting"], 128 | "building": "building1", 129 | "department": ["department1"], 130 | "area": 40.0, 131 | "height": 3.0, 132 | "capacity": 10, 133 | "ventilation": 3, 134 | "recirculated_flow_rate": 0, 135 | "allow": true 136 | }, 137 | { 138 | "name": "meeting2", 139 | "activity": ["meeting"], 140 | "building": "building1", 141 | "department": null, 142 | "area": 40.0, 143 | "height": 3.0, 144 | "capacity": 10, 145 | "ventilation": 3, 146 | "recirculated_flow_rate": 0, 147 | "allow": true 148 | }, 149 | { 150 | "name": "lunch1", 151 | "activity": ["lunch"], 152 | "building": "building1", 153 | "department": ["department1", "department2"], 154 | "area": 200.0, 155 | "height": 3.0, 156 | "capacity": 50, 157 | "ventilation": 3, 158 | "recirculated_flow_rate": 0, 159 | "allow": true 160 | }, 161 | { 162 | "name": "coffee1", 163 | "activity": ["coffee"], 164 | "building": "building1", 165 | "department": ["department1", "department2"], 166 | "area": 25.0, 167 | "height": 3.0, 168 | "capacity": 10, 169 | "ventilation": 3, 170 | "recirculated_flow_rate": 0, 171 | "allow": true 172 | }, 173 | { 174 | "name": "restroom1", 175 | "activity": ["restroom"], 176 | "building": "building1", 177 | "department": null, 178 | "area": 20.0, 179 | "height": 3.0, 180 | "capacity": 4, 181 | "ventilation": 3, 182 | "recirculated_flow_rate": 0, 183 | "allow": true 184 | }, 185 | { 186 | "name": "restroom2", 187 | "activity": ["restroom"], 188 | "building": "building2", 189 | "department": null, 190 | "area": 15.0, 191 | "height": 3.0, 192 | "capacity": 4, 193 | "ventilation": 3, 194 | "recirculated_flow_rate": 0, 195 | "allow": true 196 | } 197 | ], 198 | "people": [{ 199 | "department": "department1", 200 | "building": "building1", 201 | "num_people": 20 202 | }, 203 | { 204 | "department": "department2", 205 | "building": "building1", 206 | "num_people": 20 207 | } 208 | ], 209 | "options": { 210 | "movement_buildings": true, 211 | "movement_department": true, 212 | "number_runs": 1, 213 | "save_log": true, 214 | "save_config": true, 215 | "save_csv": false, 216 | "save_json": false, 217 | "return_output": false, 218 | "directory": null, 219 | "ratio_infected": 0.05, 220 | "model": "Colorado", 221 | "model_parameters": { 222 | "MaxPlanck": { 223 | "RNA_D50": 316, 224 | "deposition_rate": 0.5, 225 | "emission_breathing": 0.06, 226 | "emission_speaking": 0.6, 227 | "speaking_breathing_ratio": 0.1, 228 | "respiratory_rate": 10, 229 | "RNA_concentration": 5e8, 230 | "aerosol_diameter": 5, 231 | "virus_lifetime": 1.7, 232 | "CO2_background": 410 233 | }, 234 | "MIT": { 235 | "filtration_efficiency": 0.01, 236 | "relative_humidity": 60, 237 | "breathing_rate": 0.49, 238 | "aerosol_radius": 2, 239 | "infectiousness": 72, 240 | "deactivation_rate": 0.3, 241 | "transmissibility": 1, 242 | "CO2_background": 410 243 | }, 244 | "Colorado": { 245 | "pressure": 0.95, 246 | "temperature": 20, 247 | "relative_humidity": 50, 248 | "CO2_background": 415, 249 | "decay_rate": 0.62, 250 | "deposition_rate": 0.3, 251 | "hepa_flow_rate": 0.0, 252 | "recirculated_flow_rate": 300, 253 | "filter_efficiency": 0.20, 254 | "ducts_removal": 0.10, 255 | "other_removal": 0.00, 256 | "fraction_immune": 0, 257 | "breathing_rate": 0.52, 258 | "CO2_emission_person": 0.005, 259 | "quanta_exhalation": 25, 260 | "quanta_enhancement": 1, 261 | "people_with_masks": 1.00 262 | } 263 | } 264 | } 265 | } -------------------------------------------------------------------------------- /designer/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /designer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "builder", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@testing-library/jest-dom": "^5.16.1", 7 | "@testing-library/react": "^12.1.2", 8 | "@testing-library/user-event": "^13.5.0", 9 | "antd": "^4.17.3", 10 | "react": "^17.0.2", 11 | "react-dom": "^17.0.2", 12 | "react-scripts": "5.0.0", 13 | "web-vitals": "^2.1.2" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test", 19 | "eject": "react-scripts eject" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /designer/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/designer/public/favicon.ico -------------------------------------------------------------------------------- /designer/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/designer/public/favicon.png -------------------------------------------------------------------------------- /designer/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | archABM 28 | 29 | 30 | 31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /designer/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/designer/public/logo192.png -------------------------------------------------------------------------------- /designer/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/designer/public/logo512.png -------------------------------------------------------------------------------- /designer/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /designer/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /designer/src/App.css: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.css'; 2 | 3 | 4 | -------------------------------------------------------------------------------- /designer/src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /designer/src/config_basic.js: -------------------------------------------------------------------------------- 1 | let config = { 2 | "events": [{ 3 | "activity": "home", 4 | "schedule": [ 5 | [0, 480], 6 | [1020, 1439] 7 | ], 8 | "repeat_min": 0, 9 | "repeat_max": 60, 10 | "duration_min": 45, 11 | "duration_max": 60, 12 | "mask_efficiency": null, 13 | "collective": false, 14 | "shared": false, 15 | "allow": true 16 | }, 17 | { 18 | "activity": "work", 19 | "schedule": [ 20 | [480, 1020] 21 | ], 22 | "repeat_min": 0, 23 | "repeat_max": 60, 24 | "duration_min": 30, 25 | "duration_max": 60, 26 | "mask_efficiency": 0.8, 27 | "collective": false, 28 | "shared": true, 29 | "allow": true 30 | } 31 | ], 32 | "places": [{ 33 | "name": "home", 34 | "activity": "home", 35 | "building": null, 36 | "department": null, 37 | "area": null, 38 | "height": null, 39 | "capacity": null, 40 | "ventilation": null, 41 | "recirculated_flow_rate": null, 42 | "allow": true 43 | }, 44 | { 45 | "name": "office1", 46 | "activity": "work", 47 | "building": "building1", 48 | "department": ["department1", "department2"], 49 | "area": 200.0, 50 | "height": 3.0, 51 | "capacity": 50, 52 | "ventilation": 3, 53 | "recirculated_flow_rate": 0, 54 | "allow": true 55 | }, 56 | { 57 | "name": "office2", 58 | "activity": "work", 59 | "building": "building1", 60 | "department": ["department2"], 61 | "area": 200.0, 62 | "height": 3.0, 63 | "capacity": 50, 64 | "ventilation": 3, 65 | "recirculated_flow_rate": 0, 66 | "allow": true 67 | } 68 | ], 69 | "people": [{ 70 | "department": "department1", 71 | "building": "building1", 72 | "num_people": 20 73 | }, 74 | { 75 | "department": "department2", 76 | "building": "building1", 77 | "num_people": 20 78 | } 79 | ], 80 | "options": { 81 | "movement_buildings": true, 82 | "movement_department": true, 83 | "number_runs": 1, 84 | "save_log": true, 85 | "save_config": true, 86 | "save_csv": false, 87 | "save_json": false, 88 | "return_output": false, 89 | "directory": null, 90 | "ratio_infected": 0.05, 91 | "model": "Colorado", 92 | "model_parameters": { 93 | "Colorado": { 94 | "pressure": 0.95, 95 | "temperature": 20, 96 | "CO2_background": 415, 97 | "decay_rate": 0.62, 98 | "deposition_rate": 0.3, 99 | "hepa_flow_rate": 0.0, 100 | "filter_efficiency": 0.20, 101 | "ducts_removal": 0.10, 102 | "other_removal": 0.00, 103 | "fraction_immune": 0, 104 | "breathing_rate": 0.52, 105 | "CO2_emission_person": 0.005, 106 | "quanta_exhalation": 25, 107 | "quanta_enhancement": 1, 108 | "people_with_masks": 1.00 109 | } 110 | } 111 | } 112 | } 113 | 114 | export default config -------------------------------------------------------------------------------- /designer/src/config_basic.json: -------------------------------------------------------------------------------- 1 | { 2 | "events": [{ 3 | "activity": "home", 4 | "schedule": [ 5 | [0, 480], 6 | [1020, 1440] 7 | ], 8 | "repeat_min": 0, 9 | "repeat_max": null, 10 | "duration_min": 300, 11 | "duration_max": 360, 12 | "mask_efficiency": null, 13 | "collective": false, 14 | "shared": false, 15 | "allow": true 16 | }, 17 | { 18 | "activity": "work", 19 | "schedule": [ 20 | [480, 1020] 21 | ], 22 | "repeat_min": 0, 23 | "repeat_max": null, 24 | "duration_min": 30, 25 | "duration_max": 60, 26 | "mask_efficiency": 0.8, 27 | "collective": false, 28 | "shared": true, 29 | "allow": true 30 | } 31 | ], 32 | "places": [{ 33 | "name": "home", 34 | "activity": "home", 35 | "building": null, 36 | "department": null, 37 | "area": null, 38 | "height": null, 39 | "capacity": null, 40 | "ventilation": null, 41 | "recirculated_flow_rate": null, 42 | "allow": true 43 | }, 44 | { 45 | "name": "office1", 46 | "activity": "work", 47 | "building": "building1", 48 | "department": ["department1"], 49 | "area": 200.0, 50 | "height": 3.0, 51 | "capacity": 50, 52 | "ventilation": 3, 53 | "recirculated_flow_rate": 0, 54 | "allow": true 55 | }, 56 | { 57 | "name": "office2", 58 | "activity": "work", 59 | "building": "building1", 60 | "department": ["department2"], 61 | "area": 200.0, 62 | "height": 3.0, 63 | "capacity": 50, 64 | "ventilation": 3, 65 | "recirculated_flow_rate": 0, 66 | "allow": true 67 | } 68 | ], 69 | "people": [{ 70 | "department": "department1", 71 | "building": "building1", 72 | "num_people": 20 73 | }, 74 | { 75 | "department": "department2", 76 | "building": "building1", 77 | "num_people": 20 78 | } 79 | ], 80 | "options": { 81 | "movement_buildings": true, 82 | "movement_department": true, 83 | "number_runs": 1, 84 | "save_log": true, 85 | "save_config": true, 86 | "save_csv": false, 87 | "save_json": false, 88 | "return_output": false, 89 | "directory": null, 90 | "ratio_infected": 0.05, 91 | "model": "Colorado", 92 | "model_parameters": { 93 | "Colorado": { 94 | "pressure": 0.95, 95 | "temperature": 20, 96 | "CO2_background": 415, 97 | "decay_rate": 0.62, 98 | "deposition_rate": 0.3, 99 | "hepa_flow_rate": 0.0, 100 | "filter_efficiency": 0.20, 101 | "ducts_removal": 0.10, 102 | "other_removal": 0.00, 103 | "fraction_immune": 0, 104 | "breathing_rate": 0.52, 105 | "CO2_emission_person": 0.005, 106 | "quanta_exhalation": 25, 107 | "quanta_enhancement": 1, 108 | "people_with_masks": 1.00 109 | } 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /designer/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /designer/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | // 9 |
10 | 11 |
, 12 | //
, 13 | document.getElementById('root') 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | // reportWebVitals(); 20 | -------------------------------------------------------------------------------- /designer/src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /designer/src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /designer/src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/_static/command.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/command.png -------------------------------------------------------------------------------- /docs/source/_static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/favicon.png -------------------------------------------------------------------------------- /docs/source/_static/figures/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/figures/architecture.png -------------------------------------------------------------------------------- /docs/source/_static/figures/boxplot_place_CO2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/figures/boxplot_place_CO2.png -------------------------------------------------------------------------------- /docs/source/_static/figures/boxplot_place_quanta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/figures/boxplot_place_quanta.png -------------------------------------------------------------------------------- /docs/source/_static/figures/distribution_person_quanta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/figures/distribution_person_quanta.png -------------------------------------------------------------------------------- /docs/source/_static/figures/floorplan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/figures/floorplan.png -------------------------------------------------------------------------------- /docs/source/_static/figures/floorplan_CO2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/figures/floorplan_CO2.png -------------------------------------------------------------------------------- /docs/source/_static/figures/floorplan_quanta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/figures/floorplan_quanta.png -------------------------------------------------------------------------------- /docs/source/_static/figures/performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/figures/performance.png -------------------------------------------------------------------------------- /docs/source/_static/figures/table_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/figures/table_1.png -------------------------------------------------------------------------------- /docs/source/_static/figures/table_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/figures/table_2.png -------------------------------------------------------------------------------- /docs/source/_static/figures/table_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/figures/table_3.png -------------------------------------------------------------------------------- /docs/source/_static/figures/table_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/figures/table_4.png -------------------------------------------------------------------------------- /docs/source/_static/figures/timeline_activity_density.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/figures/timeline_activity_density.png -------------------------------------------------------------------------------- /docs/source/_static/figures/timeline_activity_person.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/figures/timeline_activity_person.png -------------------------------------------------------------------------------- /docs/source/_static/figures/timeline_person_quanta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/figures/timeline_person_quanta.png -------------------------------------------------------------------------------- /docs/source/_static/figures/timeline_place_CO2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/figures/timeline_place_CO2.png -------------------------------------------------------------------------------- /docs/source/_static/figures/timeline_place_quanta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/figures/timeline_place_quanta.png -------------------------------------------------------------------------------- /docs/source/_static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | archABM 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/source/_static/logo_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/logo_1.png -------------------------------------------------------------------------------- /docs/source/_static/logo_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/logo_2.png -------------------------------------------------------------------------------- /docs/source/_static/logo_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/logo_3.png -------------------------------------------------------------------------------- /docs/source/_static/logo_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/logo_4.png -------------------------------------------------------------------------------- /docs/source/_static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /docs/source/_static/my_theme.css: -------------------------------------------------------------------------------- 1 | .wy-nav-content { 2 | max-width: 900px !important; 3 | } -------------------------------------------------------------------------------- /docs/source/_static/schedule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/schedule.png -------------------------------------------------------------------------------- /docs/source/_static/vicomtech_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/docs/source/_static/vicomtech_logo.png -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | .. _API: 2 | 3 | API Reference 4 | ============= 5 | 6 | This part of the documentation covers all the interfaces of ``archABM``. 7 | For parts where ``archABM`` depends on external libraries, we document the 8 | most important right here and provide links to the canonical documentation. 9 | 10 | .. toctree:: 11 | :maxdepth: 4 12 | :hidden: 13 | 14 | 15 | archABM/actions 16 | archABM/aerosol_model 17 | archABM/creator 18 | archABM/database 19 | archABM/engine 20 | archABM/event 21 | archABM/options 22 | archABM/parameters 23 | archABM/person 24 | archABM/place 25 | archABM/results 26 | archABM/schema 27 | archABM/snapshot 28 | 29 | 30 | .. autosummary:: 31 | ~archABM.actions.Actions 32 | ~archABM.aerosol_model.AerosolModel 33 | ~archABM.creator.Creator 34 | ~archABM.database.Database 35 | ~archABM.engine.Engine 36 | ~archABM.event.Event 37 | ~archABM.options.Options 38 | ~archABM.parameters.Parameters 39 | ~archABM.person.Person 40 | ~archABM.place.Place 41 | ~archABM.results.Results 42 | ~archABM.snapshot.Snapshot 43 | 44 | 45 | -------------------------------------------------------------------------------- /docs/source/archABM/actions.rst: -------------------------------------------------------------------------------- 1 | Actions 2 | ======= 3 | 4 | .. automodule:: archABM.actions 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/source/archABM/aerosol_model.rst: -------------------------------------------------------------------------------- 1 | Aerosol Model 2 | ============= 3 | 4 | .. automodule:: archABM.aerosol_model 5 | :members: 6 | :undoc-members: 7 | 8 | Colorado Model 9 | -------------- 10 | 11 | .. automodule:: archABM.aerosol_model_colorado 12 | :members: 13 | :undoc-members: 14 | 15 | 16 | MIT Model 17 | --------- 18 | 19 | .. automodule:: archABM.aerosol_model_mit 20 | :members: 21 | :undoc-members: 22 | 23 | 24 | Max-Planck Model 25 | ---------------- 26 | 27 | .. automodule:: archABM.aerosol_model_maxplanck 28 | :members: 29 | :undoc-members: 30 | 31 | .. bibliography:: 32 | :filter: docname in docnames 33 | 34 | -------------------------------------------------------------------------------- /docs/source/archABM/creator.rst: -------------------------------------------------------------------------------- 1 | Creator 2 | ======= 3 | 4 | .. automodule:: archABM.creator 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/source/archABM/database.rst: -------------------------------------------------------------------------------- 1 | Database 2 | ======== 3 | 4 | .. automodule:: archABM.database 5 | :members: 6 | :undoc-members: -------------------------------------------------------------------------------- /docs/source/archABM/engine.rst: -------------------------------------------------------------------------------- 1 | Engine 2 | ====== 3 | 4 | .. automodule:: archABM.engine 5 | :members: 6 | :undoc-members: -------------------------------------------------------------------------------- /docs/source/archABM/event.rst: -------------------------------------------------------------------------------- 1 | Event 2 | ===== 3 | 4 | .. automodule:: archABM.event 5 | :members: 6 | :undoc-members: 7 | 8 | 9 | Event Model 10 | ----------- 11 | 12 | .. automodule:: archABM.event_model 13 | :members: 14 | :undoc-members: 15 | 16 | 17 | Event Generator 18 | --------------- 19 | 20 | .. automodule:: archABM.event_generator 21 | :members: 22 | :undoc-members: -------------------------------------------------------------------------------- /docs/source/archABM/options.rst: -------------------------------------------------------------------------------- 1 | Options 2 | ======= 3 | 4 | .. automodule:: archABM.options 5 | :members: 6 | :undoc-members: -------------------------------------------------------------------------------- /docs/source/archABM/parameters.rst: -------------------------------------------------------------------------------- 1 | Parameters 2 | ========== 3 | 4 | .. automodule:: archABM.parameters 5 | :members: 6 | :undoc-members: -------------------------------------------------------------------------------- /docs/source/archABM/person.rst: -------------------------------------------------------------------------------- 1 | Person 2 | ====== 3 | 4 | .. automodule:: archABM.person 5 | :members: 6 | :undoc-members: -------------------------------------------------------------------------------- /docs/source/archABM/place.rst: -------------------------------------------------------------------------------- 1 | Place 2 | ===== 3 | 4 | .. automodule:: archABM.place 5 | :members: 6 | :undoc-members: 7 | -------------------------------------------------------------------------------- /docs/source/archABM/results.rst: -------------------------------------------------------------------------------- 1 | Results 2 | ======= 3 | 4 | .. automodule:: archABM.results 5 | :members: 6 | :undoc-members: -------------------------------------------------------------------------------- /docs/source/archABM/schema.rst: -------------------------------------------------------------------------------- 1 | Schema 2 | ====== 3 | 4 | .. jsonschema:: ../../../archABM/schema.json#/properties/events 5 | .. jsonschema:: ../../../archABM/schema.json#/properties/people 6 | .. jsonschema:: ../../../archABM/schema.json#/properties/places 7 | .. jsonschema:: ../../../archABM/schema.json#/properties/options -------------------------------------------------------------------------------- /docs/source/archABM/snapshot.rst: -------------------------------------------------------------------------------- 1 | Snapshot 2 | ======== 3 | 4 | .. automodule:: archABM.snapshot 5 | :members: 6 | :undoc-members: 7 | 8 | Snapshot Person 9 | --------------- 10 | 11 | .. automodule:: archABM.snapshot_person 12 | :members: 13 | :undoc-members: 14 | 15 | 16 | Snapshot Place 17 | -------------- 18 | 19 | .. automodule:: archABM.snapshot_place 20 | :members: 21 | :undoc-members: -------------------------------------------------------------------------------- /docs/source/authors.rst: -------------------------------------------------------------------------------- 1 | Authors 2 | ======= 3 | 4 | This package is being developed and maintained by 5 | the department of Data Intelligence for Energy & Industrial Processes at 6 | `Vicomtech `_. 7 | 8 | .. image:: _static/vicomtech_logo.png 9 | :align: center 10 | :alt: Vicomtech 11 | 12 | 13 | Lead Development Team 14 | --------------------- 15 | 16 | - Igor Garcia Olaizola (`iolaizola@vicomtech.org `_) 17 | - Jan Lukas Bruse (`jbruse@vicomtech.org `_) 18 | - Iñigo Martinez (`imartinez@vicomtech.org `_) 19 | - Ane Miren Florez Tapia (`amflorez@vicomtech.org `_) 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/source/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | Version 0.4.0 5 | ------------- 6 | 7 | - Added temperature model based on person heat generation and heat transfer with outdoor ambient temperature. 8 | - Included functionality to run multiple events at the same place. 9 | 10 | 11 | Version 0.3.0 12 | ------------- 13 | 14 | - Added interactive configuration designer 15 | 16 | 17 | Version 0.2.8 18 | ------------- 19 | 20 | - Updated README 21 | - Added pandas to requirements 22 | - Included basic configuration JSON on package files 23 | 24 | 25 | Version 0.2.7 26 | ------------- 27 | 28 | - Included bibtex entry and DOI information 29 | 30 | Version 0.2.6 31 | ------------- 32 | 33 | - Updated article citation information 34 | 35 | Version 0.2.5 36 | ------------- 37 | 38 | - Included ArchABM disclaimer at README and documentation 39 | 40 | Version 0.2.4 41 | ------------- 42 | 43 | - Updated README 44 | - Updated getting-started section in the documentation 45 | 46 | Version 0.2.3 47 | ------------- 48 | 49 | - Updated requirements for python packages: typer[all] 50 | - Included package data and MANIFEST 51 | 52 | Version 0.2.2 53 | ------------- 54 | 55 | - Command-line-interface for archABM execution 56 | - Updated documentation with examples and results 57 | 58 | 59 | Version 0.1.0 60 | ------------- 61 | 62 | - Public release of archABM source and library 63 | 64 | 65 | Version 0.0.1 66 | ------------- 67 | 68 | - Release of archABM with documentation 69 | 70 | -------------------------------------------------------------------------------- /docs/source/citing.rst: -------------------------------------------------------------------------------- 1 | .. _citing: 2 | 3 | Citing archABM 4 | ============== 5 | 6 | If you use ArchABM in your work or project, please cite the following article, published in the 7 | `Building and Environment `_ journal. 8 | (https://doi.org/10.1016/j.buildenv.2021.108495): 9 | 10 | Bibtex entry:: 11 | 12 | @article{MARTINEZ2021108495, 13 | title = {ArchABM: An agent-based simulator of human interaction with the built environment. CO2 and viral load analysis for indoor air quality}, 14 | journal = {Building and Environment}, 15 | pages = {108495}, 16 | year = {2021}, 17 | issn = {0360-1323}, 18 | doi = {https://doi.org/10.1016/j.buildenv.2021.108495}, 19 | url = {https://www.sciencedirect.com/science/article/pii/S036013232100891X}, 20 | author = {Iñigo Martinez and Jan L. Bruse and Ane M. Florez-Tapia and Elisabeth Viles and Igor G. Olaizola}, 21 | keywords = {Agent-based modeling, Indoor air quality, Building ventilation, Aerosol model, Building design, Simulation} 22 | } 23 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | import os 10 | import sys 11 | 12 | # If extensions (or modules to document with autodoc) are in another directory, 13 | # add these directories to sys.path here. If the directory is relative to the 14 | # documentation root, use os.path.abspath to make it absolute, like shown here. 15 | 16 | sys.path.insert(0, os.path.abspath("../..")) 17 | sys.path.insert(0, os.path.abspath("../../archABM")) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = "archABM" 23 | copyright = "2021, Vicomtech" 24 | author = "Vicomtech" 25 | 26 | # The full version, including alpha/beta/rc tags 27 | release = "0.4.1" 28 | 29 | 30 | # -- General configuration --------------------------------------------------- 31 | 32 | # Add any Sphinx extension module names here, as strings. They can be 33 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 34 | # ones. 35 | extensions = [ 36 | "sphinx.ext.napoleon", 37 | "sphinx.ext.autodoc", 38 | "sphinx.ext.autosummary", 39 | 'sphinx.ext.intersphinx', 40 | 'sphinx.ext.mathjax', 41 | 'sphinxcontrib.bibtex', 42 | 'sphinx.ext.viewcode', 43 | "sphinx_rtd_theme", 44 | 'sphinxcontrib.tikz', 45 | 'sphinx-jsonschema', 46 | "sphinx.ext.autosectionlabel", 47 | "sphinx_copybutton", 48 | 'sphinxcontrib.programoutput', 49 | 'sphinx_tabs.tabs' 50 | ] 51 | 52 | # Add any paths that contain templates here, relative to this directory. 53 | templates_path = ["_templates"] 54 | source_suffix = '.rst' 55 | gettext_compact = False 56 | 57 | # List of patterns, relative to source directory, that match files and 58 | # directories to ignore when looking for source files. 59 | # This pattern also affects html_static_path and html_extra_path. 60 | exclude_patterns = [] 61 | 62 | pygments_style = "sphinx" 63 | 64 | # Copy button 65 | copybutton_prompt_text = r">>> |\.\.\. |\$ |In \[\d*\]: | {2,5}\.\.\.: | {5,8}: " 66 | copybutton_prompt_is_regexp = True 67 | 68 | # The master toctree document. 69 | master_doc = "contents" 70 | 71 | 72 | # Example configuration for intersphinx: refer to the Python standard library. 73 | intersphinx_mapping = { 74 | 'http://docs.python.org/3/': None, 75 | 'https://simpy.readthedocs.io/en/latest/': None 76 | } 77 | 78 | # Autodoc 79 | autosummary_generate = True 80 | autodoc_member_order = 'bysource' 81 | 82 | # Bibtex 83 | bibtex_bibfiles = ['references.bib'] 84 | bibtex_encoding = 'latin' 85 | # bibtex_default_style = 'plain' 86 | 87 | # -- Options for HTML output ------------------------------------------------- 88 | 89 | # The theme to use for HTML and HTML Help pages. See the documentation for 90 | # a list of builtin themes. 91 | # 92 | 93 | html_theme = "sphinx_rtd_theme" 94 | 95 | # Add any paths that contain custom static files (such as style sheets) here, 96 | # relative to this directory. They are copied after the builtin static files, 97 | # so a file named "default.css" will overwrite the builtin "default.css". 98 | html_static_path = ["_static"] 99 | html_favicon = "_static/favicon.png" 100 | html_logo = "_static/logo_3.png" 101 | html_show_sourcelink = True 102 | 103 | html_context = { 104 | 'display_github': True, 105 | 'github_user': 'Vicomtech', 106 | 'github_repo': 'ArchABM', 107 | 'github_version': 'develop/docs/source/', 108 | } 109 | 110 | html_theme_options = { 111 | 'logo_only': True, 112 | 'display_version': True, 113 | 'prev_next_buttons_location': 'bottom', 114 | 'style_external_links': False, 115 | 'style_nav_header_background': '#2980B9', 116 | 'vcs_pageview_mode': 'blob', 117 | # Toc options 118 | 'collapse_navigation': False, 119 | 'sticky_navigation': True, 120 | 'navigation_depth': 4, 121 | 'includehidden': True, 122 | 'titles_only': False 123 | } 124 | 125 | 126 | def setup(app): 127 | app.add_css_file('my_theme.css') 128 | -------------------------------------------------------------------------------- /docs/source/contents.rst: -------------------------------------------------------------------------------- 1 | .. _contents: 2 | 3 | ========================= 4 | Documentation for archABM 5 | ========================= 6 | 7 | .. toctree:: 8 | :maxdepth: 4 9 | 10 | index 11 | framework 12 | designer 13 | example 14 | api 15 | authors 16 | citing 17 | changelog 18 | license 19 | 20 | 21 | 22 | Indices and tables 23 | ================== 24 | 25 | * :ref:`genindex` 26 | * :ref:`modindex` 27 | * :ref:`search` -------------------------------------------------------------------------------- /docs/source/designer.rst: -------------------------------------------------------------------------------- 1 | .. designer: 2 | 3 | Designer 4 | ======== 5 | 6 | .. raw:: html 7 | 8 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docs/source/framework.rst: -------------------------------------------------------------------------------- 1 | Framework 2 | ========= 3 | 4 | 5 | Engine 6 | ------ 7 | 8 | **archABM** is an event-based multi-agent simulation framework designed to simulate *complex human interaction* patterns with and within the built environment and to calculate indoor air quality metrics and physiological responses. 9 | 10 | 11 | **archABM** is implemented using `Python 3.7.9 `_ , and adopts an object-oriented paradigm, where each agent is a class instance. The simulation engine is developed on top of the `SimPy 4.0.1 `_ library, a process-based discrete-event simulation framework. Under this paradigm, *processes* are used to model the behavior of active components, such as users. 12 | 13 | Processes live in an *environment* and interact with the environment and with each other via *events*. The most important event type for our application is the *timeout*, which allows a process to sleep for the given time, determining the duration of the activity. 14 | 15 | .. important:: 16 | Agent-based simulators can be implemented in two ways: a) **continuous** simulation and b) **event-based** simulation. 17 | An event-based approach is followed for **archABM**. 18 | 19 | * **Continuous** simulations have a fixed time-step, and the system state is updated in every step. It is critical to select an appropriate period parameter, which indicates how much time elapses between state updates. 20 | * In **discrete event-based** simulations, the system is only updated when a new event occurs. The simulator processes new events in sequential order as they are fired or triggered by the simulated entities or agents. 21 | 22 | 23 | 24 | Components 25 | ---------- 26 | 27 | The simulator's core is composed of a discrete event-based engine that manages every activity carried out by the agents during their life-cycle using a priority event queue, ordered by time. The event-based engine is at the core of the simulator and is fed by events produced by the agents. 28 | 29 | During the simulation execution, events are handled sequentially, in chronological order. Whenever any agent does an action or takes a decision, it generates and inserts new events into the priority queue. As actions and activities occur, each event is registered on the simulation history to be further exploited for visualization and data analysis purposes. 30 | 31 | 32 | .. image:: _static/figures/architecture.png 33 | :width: 250 34 | :align: center 35 | 36 | 37 | The workflow of the simulator is described as follows: first, Simpy's environment is created, and the provided configuration data is used to generate *events*, *places*, and *people*, as well as to initialize the *aerosol model*. People are introduced into the environment at the start of the day, and their goal is to complete events until the end of the day arrives. 38 | 39 | 40 | 41 | Events 42 | ^^^^^^ 43 | 44 | An **event** is an activity that takes place at a specific physical location for a finite time. Event models (for example: work, meeting, coffee, lunch, etc.) are restricted to a *schedule*, a set *duration*, and a number of *repetitions*. The schedule specifies the times when an activity is permitted to take place. Lower and upper bounds apply to both the duration :math:`\tau` and the number of repetitions. Concerning the aerosol model, the *mask efficiency* is also indicated for each activity. Activities invoked by an individual but involving many people, such as meetings, can also be defined. These are called *collective* events. 45 | 46 | .. image:: _static/schedule.png 47 | :align: center 48 | :width: 500 49 | 50 | 51 | Event selection 52 | """"""""""""""" 53 | 54 | The event generation process selects the next activity based on the priority values of each event model. Priority values are used to weigh the importance of each event model rather than sampling from a uniform discrete distribution. The *priority* value is determined by a piecewise linear function, which is parametrized by the a) minimum number of repetitions ``r``, b) maximum number of repetitions ``R``, and c) the event repetition count ``e``. 55 | 56 | .. math:: 57 | Priority(e) = 58 | \left\{\begin{matrix} 59 | 1-(1-\alpha)\cfrac{e}{r}\,,\quad 0 \leq e < r \\ 60 | \alpha\cfrac{R-e}{R-r}\,,\quad r \leq e < R 61 | \end{matrix}\right. 62 | 63 | 64 | .. tikz:: Priority piecewise linear function 65 | \pgfmathsetmacro{\N}{10}; 66 | \pgfmathsetmacro{\M}{6}; 67 | \pgfmathsetmacro{\NN}{\N-1}; 68 | \pgfmathsetmacro{\MM}{\M-1}; 69 | \pgfmathsetmacro{\repmin}{2.25}; 70 | \pgfmathsetmacro{\repmax}{8.5}; 71 | \pgfmathsetmacro{\a}{2}; 72 | \coordinate (A) at (0,\MM); 73 | \coordinate (B) at (\NN,0); 74 | \coordinate (C) at (\repmin, \a); 75 | \coordinate (D) at (\repmax, 0); 76 | \coordinate (E) at (\repmin, 0); 77 | \coordinate (F) at (0, \a); 78 | \draw[stepx=1,thin, black!20] (0,0) grid (\N,\M); 79 | \draw[->, very thick] (0,0) to (\N,0) node[right] {Event count}; 80 | \draw[->, very thick] (0,0) to (0,\M) node[above] {Priority}; 81 | \draw (0.1,0) -- (-0.1, 0) node[anchor=east] {0}; 82 | \draw (0, 0.1) -- (0, -0.1); 83 | \draw (\repmin,0.1) -- (\repmin,-0.1) node[anchor=north] {$repeat_{min}$}; 84 | \draw (\repmax,0.1) -- (\repmax,-0.1) node[anchor=north] {$repeat_{max}$}; 85 | \draw[ultra thick] (0.1, \MM) -- (-0.1, \MM) node[left] {1}; 86 | \draw[very thick, black!50, dashed] (C) -- (F) node[left] {$\alpha$}; 87 | \draw[very thick, black!50, dashed] (C) -- (E); 88 | \draw[ultra thick, red] (A) -- (C); 89 | \draw[ultra thick, red] (C) -- (D); 90 | :xscale: 70 91 | :align: center 92 | 93 | 94 | Following the selection of the event model, the duration and physical location of the event can be determined. Next, the selected activity is counted (consumed) from the invoking person's list of events. Collective activities are consumed individually after the current event interruption. Finally, based on the generated event, the person is moved from the current location to the new location and remains there for a specified time. Once the activity is fulfilled, the event generator produces a new event. If a person is interrupted while performing his current task, the assigned event becomes the new current. 95 | 96 | 97 | Places 98 | ^^^^^^ 99 | 100 | A **place** is an enclosed section of a building designed for specific activities and is defined by the following parameters: building, departments allowed to enter, area and height (or volume), capacity :math:`N`, and passive :math:`\lambda_a` and active :math:`\lambda_r` ventilation. 101 | 102 | 103 | 104 | People 105 | ^^^^^^ 106 | 107 | Regarding the **people** dimension, specific departments or groups need to be defined, each one associated with a building and some people. 108 | 109 | Aerosol Model 110 | ^^^^^^^^^^^^^ 111 | 112 | The **aerosol model** estimates the indoor aerosolized virus quanta concentration, based on adjustable parameters such as room size, number of exposed subjects, inhalation volume, and aerosol production from breathing and vocalization, among others. The model developed by Peng et al. at the University of Colorado, :cite:`doi:10.1021/acs.estlett.1c00183,https://doi.org/10.1111/ina.12751,Peng2021.04.21.21255898`., calculates both the virus quanta concentration and the CO\ :sub:`2` mixing ratio present in a specific place. These two metrics provide an overall picture of indoor air quality, which is why this model was selected for ArchABM. The model combines two submodels: 113 | 114 | #. A **standard atmospheric box model**, which assumes that the emissions are completely mixed across a control volume quickly (such as an indoor room or other space). See for example Chapter 3 of the Jacob Atmos. Chem. textbook :cite:`10.2307/j.ctt7t8hg`, and Chapter 21 of the Cooper and Alley Air Pollution Control Engineering Textbook :cite:`cooper2010air` for indoor applications. This is an approximation that allows easy calculation, is approximately correct as long as near-field effects are avoided by social distancing, and is commonly used in air quality modeling. 115 | 116 | #. A **standard aerosol infection model** (Wells-Riley model), as formulated in Miller et al. 2020 :cite:`https://doi.org/10.1111/ina.12751`, and references therein :cite:`10.1093/oxfordjournals.aje.a112560,BUONANNO2020105794,BUONANNO2020106112`. 117 | 118 | .. important:: 119 | The propagation of COVID-19 is only by aerosol transmission. 120 | 121 | The model is based on a standard model of aerosol disease transmission, the Wells-Riley model. 122 | It is calibrated to COVID-19 per recent literature on quanta emission rate. 123 | 124 | This is not an epidemiological model, and does not include droplet or contact / fomite transmission, and assumes that 6 ft / 2 m social distancing is respected. Otherwise higher transmission will result. 125 | 126 | 127 | Performance 128 | ----------- 129 | 130 | In order to analyze **archABM**'s computational performance, several simulations were computed with a different number of people and places. A grid of values for the number of people *{6, 30, 60, 120, 300, 600, 1200, 2400}* and the number of places *{15, 20, 25, 30, 35}* was established. The computational time required to compute 24h of simulated time is measured. In order to yield stable results, the simulations are repeated 20 times. 131 | 132 | .. image:: _static/figures/performance.png 133 | :align: center 134 | :width: 500 135 | 136 | The number of people is indeed the most influential parameter concerning the simulator's performance. Using the number of people as the predictor, the univariate linear regression model applied to the response variable time yields a slope parameter of 2.4 10\ :sup:`-3` seconds per person. Thus, on average, **archABM** is able to run 24h of simulated time with 1000 people and 20 places in approximately 2.4 seconds. -------------------------------------------------------------------------------- /docs/source/license.rst: -------------------------------------------------------------------------------- 1 | License 2 | ======= 3 | 4 | .. literalinclude:: ../../LICENSE -------------------------------------------------------------------------------- /docs/source/references.bib: -------------------------------------------------------------------------------- 1 | % MIT MODEL 2 | @article {Bazante2018995118, 3 | author = {Bazant, Martin Z. and Bush, John W. M.}, 4 | title = {A guideline to limit indoor airborne transmission of COVID-19}, 5 | volume = {118}, 6 | number = {17}, 7 | elocation-id = {e2018995118}, 8 | year = {2021}, 9 | doi = {10.1073/pnas.2018995118}, 10 | publisher = {National Academy of Sciences}, 11 | issn = {0027-8424}, 12 | journal = {Proceedings of the National Academy of Sciences} 13 | } 14 | 15 | @article {Bazant2021.04.04.21254903, 16 | author = {Bazant, Martin Z. and Kodio, Ousmane and Cohen, Alexander E. and Khan, Kasim and Gu, Zongyu and Bush, John W. M.}, 17 | title = {Monitoring carbon dioxide to quantify the risk of indoor airborne transmission of COVID-19}, 18 | elocation-id = {2021.04.04.21254903}, 19 | year = {2021}, 20 | doi = {10.1101/2021.04.04.21254903}, 21 | publisher = {Cold Spring Harbor Laboratory Press}, 22 | journal = {medRxiv} 23 | } 24 | 25 | @article {Risbeck2021.06.21.21259287, 26 | author = {Risbeck, Michael J. and Bazant, Martin Z. and Jiang, Zhanhong and Lee, Young M. and Drees, Kirk H. and Douglas, Jonathan D.}, 27 | title = {Quantifying the Tradeoff Between Energy Consumption and the Risk of Airborne Disease Transmission for Building HVAC Systems}, 28 | elocation-id = {2021.06.21.21259287}, 29 | year = {2021}, 30 | doi = {10.1101/2021.06.21.21259287}, 31 | publisher = {Cold Spring Harbor Laboratory Press}, 32 | journal = {medRxiv} 33 | } 34 | 35 | 36 | % MAXPLANCK MODEL 37 | @Article{ijerph17218114, 38 | AUTHOR = {Lelieveld, Jos and Helleis, Frank and Borrmann, Stephan and Cheng, Yafang and Drewnick, Frank and Haug, Gerald and Klimach, Thomas and Sciare, Jean and Su, Hang and Poschl, Ulrich}, 39 | TITLE = {Model Calculations of Aerosol Transmission and Infection Risk of COVID-19 in Indoor Environments}, 40 | JOURNAL = {International Journal of Environmental Research and Public Health}, 41 | VOLUME = {17}, 42 | YEAR = {2020}, 43 | NUMBER = {21}, 44 | ARTICLE-NUMBER = {8114}, 45 | PubMedID = {33153155}, 46 | ISSN = {1660-4601}, 47 | DOI = {10.3390/ijerph17218114} 48 | } 49 | 50 | % COLORADO MODEL 51 | @article{doi:10.1021/acs.estlett.1c00183, 52 | author = {Peng, Zhe and Jimenez, Jose L.}, 53 | title = {Exhaled CO2 as a COVID-19 Infection Risk Proxy for Different Indoor Environments and Activities}, 54 | journal = {Environmental Science \& Technology Letters}, 55 | volume = {8}, 56 | number = {5}, 57 | pages = {392-397}, 58 | year = {2021}, 59 | doi = {10.1021/acs.estlett.1c00183}, 60 | } 61 | 62 | @article{https://doi.org/10.1111/ina.12751, 63 | author = {Miller, Shelly L. and Nazaroff, William W and Jimenez, Jose L. and Boerstra, Atze and Buonanno, Giorgio and Dancer, Stephanie J. and Kurnitski, Jarek and Marr, Linsey C. and Morawska, Lidia and Noakes, Catherine}, 64 | title = {Transmission of SARS-CoV-2 by inhalation of respiratory aerosol in the Skagit Valley Chorale superspreading event}, 65 | journal = {Indoor Air}, 66 | volume = {31}, 67 | number = {2}, 68 | pages = {314-323}, 69 | keywords = {aerosol transmission, infectious disease, pandemic, risk, ventilation, virus}, 70 | doi = {https://doi.org/10.1111/ina.12751}, 71 | year = {2021} 72 | } 73 | 74 | @article{Peng2021.04.21.21255898, 75 | author = {Peng, Z. and Bahnfleth, W. and Buonanno, G. and Dancer, S. J. and Kurnitski, J. and Li, Y. and Loomans, M.G.L.C. and Marr, L.C. and Morawska, L. and Nazaroff, W. and Noakes, C. and Querol, X. and Sekhar, C. and Tellier, R. and Greenhalgh, T. and Bourouiba, L. and Boerstra, A. and Tang, J. and Miller, S. and Jimenez, J.L.}, 76 | title = {Indicators for Risk of Airborne Transmission in Shared Indoor Environments and their application to COVID-19 Outbreaks}, 77 | elocation-id = {2021.04.21.21255898}, 78 | year = {2021}, 79 | doi = {10.1101/2021.04.21.21255898}, 80 | publisher = {Cold Spring Harbor Laboratory Press}, 81 | journal = {medRxiv} 82 | } 83 | 84 | 85 | @book{10.2307/j.ctt7t8hg, 86 | ISBN = {9780691001852}, 87 | URL = {http://www.jstor.org/stable/j.ctt7t8hg}, 88 | author = {Daniel J. Jacob}, 89 | publisher = {Princeton University Press}, 90 | title = {Introduction to Atmospheric Chemistry}, 91 | year = {1999} 92 | } 93 | @book{cooper2010air, 94 | title={Air pollution control: A design approach}, 95 | author={Cooper, C David and Alley, Forrest Christopher}, 96 | year={2010}, 97 | publisher={Waveland press} 98 | } 99 | 100 | @article{10.1093/oxfordjournals.aje.a112560, 101 | author = {RILEY, E. C. and MURPHY, G. and RILEY, R. L.}, 102 | title = "{AIRBORNE SPREAD OF MEASLES IN A SUBURBAN ELEMENTARY SCHOOL}", 103 | journal = {American Journal of Epidemiology}, 104 | volume = {107}, 105 | number = {5}, 106 | pages = {421-432}, 107 | year = {1978}, 108 | month = {05}, 109 | issn = {0002-9262}, 110 | doi = {10.1093/oxfordjournals.aje.a112560}, 111 | } 112 | 113 | @article{BUONANNO2020105794, 114 | title = {Estimation of airborne viral emission: Quanta emission rate of SARS-CoV-2 for infection risk assessment}, 115 | journal = {Environment International}, 116 | volume = {141}, 117 | pages = {105794}, 118 | year = {2020}, 119 | issn = {0160-4120}, 120 | doi = {https://doi.org/10.1016/j.envint.2020.105794}, 121 | author = {G. Buonanno and L. Stabile and L. Morawska}, 122 | keywords = {SARS-CoV-2 (CoVID19), Virus airborne transmission, Indoor, Ventilation, Coronavirus, Viral load}, 123 | } 124 | 125 | @article{BUONANNO2020106112, 126 | title = {Quantitative assessment of the risk of airborne transmission of SARS-CoV-2 infection: Prospective and retrospective applications}, 127 | journal = {Environment International}, 128 | volume = {145}, 129 | pages = {106112}, 130 | year = {2020}, 131 | issn = {0160-4120}, 132 | doi = {https://doi.org/10.1016/j.envint.2020.106112}, 133 | author = {G. Buonanno and L. Morawska and L. Stabile}, 134 | keywords = {SARS-CoV-2 (COVID-19) assessment, Virus airborne transmission, Indoor, Ventilation, Coronavirus}, 135 | } 136 | 137 | 138 | -------------------------------------------------------------------------------- /experiments/config_validation.json: -------------------------------------------------------------------------------- 1 | { 2 | "events": [{ 3 | "activity": "home", 4 | "schedule": [ 5 | [0, 450], 6 | [1020, 1440] 7 | ], 8 | "repeat_min": 0, 9 | "repeat_max": null, 10 | "duration_min": 5, 11 | "duration_max": 5, 12 | "mask_efficiency": null, 13 | "collective": false, 14 | "shared": false, 15 | "allow": true 16 | }, 17 | { 18 | "activity": "work", 19 | "schedule": [ 20 | [450, 1020] 21 | ], 22 | "repeat_min": 0, 23 | "repeat_max": null, 24 | "duration_min": 3, 25 | "duration_max": 8, 26 | "mask_efficiency": 0.0, 27 | "collective": false, 28 | "shared": true, 29 | "allow": true 30 | }, 31 | { 32 | "activity": "restroom", 33 | "schedule": [ 34 | [500, 1020] 35 | ], 36 | "repeat_min": 0, 37 | "repeat_max": 4, 38 | "duration_min": 3, 39 | "duration_max": 6, 40 | "mask_efficiency": 0.0, 41 | "collective": false, 42 | "shared": true, 43 | "allow": true 44 | }, 45 | { 46 | "activity": "coffee", 47 | "schedule": [ 48 | [680, 720] 49 | ], 50 | "repeat_min": 0, 51 | "repeat_max": 1, 52 | "duration_min": 15, 53 | "duration_max": 45, 54 | "mask_efficiency": 0.0, 55 | "collective": true, 56 | "shared": true, 57 | "allow": true 58 | }, 59 | { 60 | "activity": "lunch", 61 | "schedule": [ 62 | [700, 810] 63 | ], 64 | "repeat_min": 1, 65 | "repeat_max": 1, 66 | "duration_min": 110, 67 | "duration_max": 120, 68 | "mask_efficiency": 0.0, 69 | "collective": true, 70 | "shared": true, 71 | "allow": true 72 | } 73 | ], 74 | "places": [{ 75 | "name": "home", 76 | "activity": ["home"], 77 | "building": null, 78 | "department": null, 79 | "area": null, 80 | "height": null, 81 | "capacity": null, 82 | "ventilation": null, 83 | "recirculated_flow_rate": null, 84 | "allow": true 85 | }, 86 | { 87 | "name": "office", 88 | "activity": ["work"], 89 | "building": "building1", 90 | "department": null, 91 | "area": 20.475, 92 | "height": 3.53, 93 | "capacity": 3, 94 | "ventilation": 0.25, 95 | "recirculated_flow_rate": 0, 96 | "allow": true 97 | }, 98 | { 99 | "name": "restroom", 100 | "activity": ["restroom"], 101 | "building": "building1", 102 | "department": null, 103 | "area": 20.0, 104 | "height": 2.7, 105 | "capacity": 4, 106 | "ventilation": 1.0, 107 | "recirculated_flow_rate": 0, 108 | "allow": true 109 | }, 110 | { 111 | "name": "coffee", 112 | "activity": ["coffee"], 113 | "building": "building1", 114 | "department": null, 115 | "area": 25.0, 116 | "height": 2.7, 117 | "capacity": 10, 118 | "ventilation": 1.5, 119 | "recirculated_flow_rate": 0, 120 | "allow": true 121 | }, 122 | { 123 | "name": "lunch", 124 | "activity": ["lunch"], 125 | "building": "building1", 126 | "department": null, 127 | "area": 150.0, 128 | "height": 2.7, 129 | "capacity": 60, 130 | "ventilation": 1.5, 131 | "recirculated_flow_rate": 0, 132 | "allow": true 133 | } 134 | ], 135 | "people": [{ 136 | "department": "department1", 137 | "building": "building1", 138 | "num_people": 2 139 | }], 140 | "options": { 141 | "movement_buildings": true, 142 | "movement_department": false, 143 | "number_runs": 30, 144 | "save_log": true, 145 | "save_config": true, 146 | "save_csv": false, 147 | "save_json": false, 148 | "return_output": false, 149 | "directory": "validation", 150 | "ratio_infected": 0, 151 | "model": "Colorado", 152 | "model_parameters": { 153 | "MaxPlanck": { 154 | "RNA_D50": 316, 155 | "deposition_rate": 0.5, 156 | "emission_breathing": 0.06, 157 | "emission_speaking": 0.6, 158 | "speaking_breathing_ratio": 0.1, 159 | "respiratory_rate": 10, 160 | "RNA_concentration": 5e8, 161 | "aerosol_diameter": 5, 162 | "virus_lifetime": 1.7, 163 | "CO2_background": 410 164 | }, 165 | "MIT": { 166 | "filtration_efficiency": 0.01, 167 | "relative_humidity": 60, 168 | "breathing_rate": 0.49, 169 | "aerosol_radius": 2, 170 | "infectiousness": 72, 171 | "deactivation_rate": 0.3, 172 | "transmissibility": 1, 173 | "CO2_background": 410 174 | }, 175 | "Colorado": { 176 | "pressure": 0.95, 177 | "temperature": 22, 178 | "CO2_background": 440, 179 | "decay_rate": 0.62, 180 | "deposition_rate": 0.3, 181 | "hepa_flow_rate": 0.0, 182 | "recirculated_flow_rate": 300, 183 | "filter_efficiency": 0.20, 184 | "ducts_removal": 0.10, 185 | "other_removal": 0.00, 186 | "fraction_immune": 0, 187 | "breathing_rate": 0.52, 188 | "CO2_emission_person": 0.006, 189 | "quanta_exhalation": 25, 190 | "quanta_enhancement": 1, 191 | "people_with_masks": 1.00 192 | } 193 | } 194 | } 195 | } -------------------------------------------------------------------------------- /experiments/performance.csv: -------------------------------------------------------------------------------- 1 | num_people,num_places,num_events,ratio,number_runs,time 2 | 3,15,6,0.1,5,0.047754625810703 3 | 3,15,6,0.1,5,0.058664073219479 4 | 3,15,6,0.1,5,0.052915484620025 5 | 3,15,6,0.1,5,0.06266587126891 6 | 3,15,6,0.1,5,0.061019128455082 7 | 3,20,6,0.1,5,0.045960215997184 8 | 3,20,6,0.1,5,0.045161768997787 9 | 3,20,6,0.1,5,0.04552603999764 10 | 3,20,6,0.1,5,0.055821660003858 11 | 3,20,6,0.1,5,0.043493924000359 12 | 3,25,6,0.1,5,0.055588663999515 13 | 3,25,6,0.1,5,0.062799765000818 14 | 3,25,6,0.1,5,0.051777372995275 15 | 3,25,6,0.1,5,0.052626582000812 16 | 3,25,6,0.1,5,0.048792966998008 17 | 3,30,6,0.1,5,0.051653072994668 18 | 3,30,6,0.1,5,0.048519290001423 19 | 3,30,6,0.1,5,0.043197145001614 20 | 3,30,6,0.1,5,0.050131468997279 21 | 3,30,6,0.1,5,0.04701069300063 22 | 3,35,6,0.1,5,0.046893883998564 23 | 3,35,6,0.1,5,0.05211663699447 24 | 3,35,6,0.1,5,0.070729619001213 25 | 3,35,6,0.1,5,0.053129640000407 26 | 3,35,6,0.1,5,0.046769449996646 27 | 29,15,6,0.5,5,0.327443533408732 28 | 29,15,6,0.5,5,0.326655514137674 29 | 29,15,6,0.5,5,0.345631385519155 30 | 29,15,6,0.5,5,0.373242725550372 31 | 29,15,6,0.5,5,0.31809578405846 32 | 29,20,6,0.5,5,0.335280008002883 33 | 29,20,6,0.5,5,0.359525234998728 34 | 29,20,6,0.5,5,0.362311243996373 35 | 29,20,6,0.5,5,0.371270665003976 36 | 29,20,6,0.5,5,0.315664047004248 37 | 29,25,6,0.5,5,0.348002079001162 38 | 29,25,6,0.5,5,0.377580690001196 39 | 29,25,6,0.5,5,0.354520463995868 40 | 29,25,6,0.5,5,0.382347831000516 41 | 29,25,6,0.5,5,0.319667349998781 42 | 29,30,6,0.5,5,0.354627931999858 43 | 29,30,6,0.5,5,0.377117694006302 44 | 29,30,6,0.5,5,0.369430009995995 45 | 29,30,6,0.5,5,0.372434291006357 46 | 29,30,6,0.5,5,0.335963994999474 47 | 29,35,6,0.5,5,0.371090849999746 48 | 29,35,6,0.5,5,0.377627551999467 49 | 29,35,6,0.5,5,0.362381494000147 50 | 29,35,6,0.5,5,0.389515240996843 51 | 29,35,6,0.5,5,0.349687524998444 52 | 60,15,6,1,5,0.668579903042337 53 | 60,15,6,1,5,0.677625396510193 54 | 60,15,6,1,5,0.658230759643484 55 | 60,15,6,1,5,0.719079060432341 56 | 60,15,6,1,5,0.663251372944287 57 | 60,20,6,1,5,0.707911506993696 58 | 60,20,6,1,5,0.69048351800302 59 | 60,20,6,1,5,0.671127882997098 60 | 60,20,6,1,5,0.769729920000827 61 | 60,20,6,1,5,0.617127520999929 62 | 60,25,6,1,5,0.657815869002661 63 | 60,25,6,1,5,0.694917854001687 64 | 60,25,6,1,5,0.693714068998816 65 | 60,25,6,1,5,0.802591366002162 66 | 60,25,6,1,5,0.688153871997201 67 | 60,30,6,1,5,0.698541869001929 68 | 60,30,6,1,5,0.690208009000344 69 | 60,30,6,1,5,0.698643351002829 70 | 60,30,6,1,5,0.742896600000677 71 | 60,30,6,1,5,0.702978130000702 72 | 60,35,6,1,5,0.700664657000743 73 | 60,35,6,1,5,0.725391419997322 74 | 60,35,6,1,5,0.733656463999068 75 | 60,35,6,1,5,0.722789957995701 76 | 60,35,6,1,5,0.699374101001013 77 | 120,15,6,2,5,1.28222438181743 78 | 120,15,6,2,5,1.27901002685721 79 | 120,15,6,2,5,1.2657181285054 80 | 120,15,6,2,5,1.33537890160151 81 | 120,15,6,2,5,1.26167812990796 82 | 120,20,6,2,5,1.45795051599998 83 | 120,20,6,2,5,1.31443006599875 84 | 120,20,6,2,5,1.33425978499872 85 | 120,20,6,2,5,1.31745886400313 86 | 120,20,6,2,5,1.26083448900317 87 | 120,25,6,2,5,1.45543657899543 88 | 120,25,6,2,5,1.32941614800075 89 | 120,25,6,2,5,1.36783143999492 90 | 120,25,6,2,5,1.34215873300127 91 | 120,25,6,2,5,1.3508335430015 92 | 120,30,6,2,5,1.67933505499968 93 | 120,30,6,2,5,1.37953953800024 94 | 120,30,6,2,5,1.35417142299411 95 | 120,30,6,2,5,1.3134324720013 96 | 120,30,6,2,5,1.36262347300362 97 | 120,35,6,2,5,1.38952481900196 98 | 120,35,6,2,5,1.3841902330023 99 | 120,35,6,2,5,1.37055854599748 100 | 120,35,6,2,5,1.36301711299893 101 | 120,35,6,2,5,1.34615112499887 102 | 300,15,6,5,5,3.1078617261711 103 | 300,15,6,5,5,3.09673258151844 104 | 300,15,6,5,5,3.12344651732499 105 | 300,15,6,5,5,3.09209678953513 106 | 300,15,6,5,5,3.07279359552725 107 | 300,20,6,5,5,3.15598559800128 108 | 300,20,6,5,5,3.15706432900333 109 | 300,20,6,5,5,3.25078990300244 110 | 300,20,6,5,5,3.59916222199536 111 | 300,20,6,5,5,3.22016648700082 112 | 300,25,6,5,5,3.23449196900037 113 | 300,25,6,5,5,3.22232535399962 114 | 300,25,6,5,5,3.25780314800068 115 | 300,25,6,5,5,3.50378734699916 116 | 300,25,6,5,5,3.2247064569965 117 | 300,30,6,5,5,3.40046320099646 118 | 300,30,6,5,5,3.2154752360002 119 | 300,30,6,5,5,3.3277398670034 120 | 300,30,6,5,5,3.48084886900324 121 | 300,30,6,5,5,3.17844297499687 122 | 300,35,6,5,5,4.05145237300167 123 | 300,35,6,5,5,3.43785694699909 124 | 300,35,6,5,5,3.7086092219979 125 | 300,35,6,5,5,3.37458109799627 126 | 300,35,6,5,5,3.31339420800214 127 | 600,15,6,10,5,6.94094105397693 128 | 600,15,6,10,5,6.16984511984294 129 | 600,15,6,10,5,6.22162505650143 130 | 600,15,6,10,5,6.22806752393415 131 | 600,15,6,10,5,6.23455163636565 132 | 600,20,6,10,5,6.41469801399944 133 | 600,20,6,10,5,6.16682127900276 134 | 600,20,6,10,5,6.96372669400444 135 | 600,20,6,10,5,6.35486466899602 136 | 600,20,6,10,5,6.44304408999597 137 | 600,25,6,10,5,6.42003294999449 138 | 600,25,6,10,5,6.65963987500436 139 | 600,25,6,10,5,7.39031757300108 140 | 600,25,6,10,5,6.51486596500035 141 | 600,25,6,10,5,6.51991918000567 142 | 600,30,6,10,5,6.78324250700098 143 | 600,30,6,10,5,6.48080387400114 144 | 600,30,6,10,5,7.24603624500014 145 | 600,30,6,10,5,6.70923002200288 146 | 600,30,6,10,5,6.68740501399589 147 | 600,35,6,10,5,6.74191667800187 148 | 600,35,6,10,5,6.7624744969944 149 | 600,35,6,10,5,6.87320408500091 150 | 600,35,6,10,5,6.73144045699883 151 | 600,35,6,10,5,6.80445001800399 152 | 1200,15,6,20,5,12.8865032277825 153 | 1200,15,6,20,5,12.98671140675 154 | 1200,15,6,20,5,13.4894574587839 155 | 1200,15,6,20,5,13.0225970184655 156 | 1200,15,6,20,5,12.9471430454993 157 | 1200,20,6,20,5,13.3756361100022 158 | 1200,20,6,20,5,13.5013748400015 159 | 1200,20,6,20,5,13.0335577500009 160 | 1200,20,6,20,5,13.2718842260001 161 | 1200,20,6,20,5,13.3510426790017 162 | 1200,25,6,20,5,13.3540971980037 163 | 1200,25,6,20,5,14.0874768150024 164 | 1200,25,6,20,5,14.2067029570026 165 | 1200,25,6,20,5,13.8741225289996 166 | 1200,25,6,20,5,13.0606014939985 167 | 1200,30,6,20,5,13.8516615150002 168 | 1200,30,6,20,5,14.2527583570045 169 | 1200,30,6,20,5,13.6284005350026 170 | 1200,30,6,20,5,13.8213593279943 171 | 1200,30,6,20,5,15.0929273510046 172 | 1200,35,6,20,5,13.8363080640047 173 | 1200,35,6,20,5,15.760986659996 174 | 1200,35,6,20,5,16.2058845799984 175 | 1200,35,6,20,5,16.2724327999968 176 | 1200,35,6,20,5,15.2564739719965 177 | 2400,15,6,40,5,28.9667859152437 178 | 2400,15,6,40,5,27.8521225032535 179 | 2400,15,6,40,5,29.4907789493991 180 | 2400,15,6,40,5,28.4750157779327 181 | 2400,15,6,40,5,28.7687522952037 182 | 2400,20,6,40,5,28.9883766919957 183 | 2400,20,6,40,5,28.3077030060012 184 | 2400,20,6,40,5,28.9670370480017 185 | 2400,20,6,40,5,28.2195880840009 186 | 2400,20,6,40,5,28.619329592002 187 | 2400,25,6,40,5,28.6152752060007 188 | 2400,25,6,40,5,28.3267613159987 189 | 2400,25,6,40,5,28.8022774799974 190 | 2400,25,6,40,5,28.762772901995 191 | 2400,25,6,40,5,30.2826578899985 192 | 2400,30,6,40,5,29.0868630710029 193 | 2400,30,6,40,5,29.211695885002 194 | 2400,30,6,40,5,29.1702910220047 195 | 2400,30,6,40,5,28.8560729749952 196 | 2400,30,6,40,5,29.4540042849985 197 | 2400,35,6,40,5,29.4017415080016 198 | 2400,35,6,40,5,32.2333320140024 199 | 2400,35,6,40,5,31.9577328489977 200 | 2400,35,6,40,5,29.5716699340046 201 | 2400,35,6,40,5,30.081196510997 202 | -------------------------------------------------------------------------------- /experiments/performance.py: -------------------------------------------------------------------------------- 1 | from archABM.engine import Engine 2 | import pandas as pd 3 | import numpy as np 4 | import timeit 5 | import random 6 | import string 7 | import json 8 | 9 | def main(config): 10 | simulation = Engine(config) 11 | results = simulation.run() 12 | 13 | # varying number of people and capacity 14 | number_repetitions = 5 15 | number_runs = 5 16 | ratio_arr = [0.1, 0.5, 1, 2, 5, 10, 20, 40] 17 | extra_places_arr = [0, 5, 10, 15, 20] 18 | results = [] 19 | for i in range(number_repetitions): 20 | for ratio in ratio_arr: 21 | for extra_places in extra_places_arr: 22 | 23 | with open("experiments/config_performance.json", "r") as f: 24 | config = json.load(f) 25 | 26 | config["options"]["number_runs"] = number_runs 27 | 28 | for k in range(extra_places): 29 | name = ''.join(random.choices(string.ascii_uppercase + string.digits, k=6)) 30 | activity = random.choice(["work", "lunch", "meeting", "restroom", "coffee"]) 31 | config["places"].append({ 32 | "name": name, 33 | "activity": activity, 34 | "building": "building1", 35 | "department": None, 36 | "area": 150.0, 37 | "height": 2.7, 38 | "capacity": 60, 39 | "ventilation": 1.5, 40 | "recirculated_flow_rate": 0, 41 | "allow": True 42 | }) 43 | 44 | num_events = len(config["events"]) 45 | num_departments = len(config["people"]) 46 | num_places = len(config["places"]) 47 | 48 | num_people = 0 49 | for i in range(num_departments): 50 | if config["people"][i]["num_people"] is not None: 51 | config["people"][i]["num_people"] *= ratio 52 | config["people"][i]["num_people"] = int(config["people"][i]["num_people"]) 53 | num_people += config["people"][i]["num_people"] 54 | 55 | for j in range(num_places): 56 | if config["places"][j]["capacity"] is not None: 57 | config["places"][j]["capacity"] *= ratio 58 | config["places"][j]["capacity"] = int(config["places"][j]["capacity"]) 59 | 60 | 61 | 62 | time = timeit.repeat(lambda: main(config), number=1, repeat=1)[0] 63 | results.append({"num_people": num_people, "num_places": num_places, "num_events": num_events, "ratio": ratio, "number_runs": number_runs, "time": time}) 64 | 65 | pd.DataFrame(results).to_csv("performance.csv", index=False) 66 | 67 | 68 | 69 | # print(results) 70 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import typer 4 | import json 5 | from pathlib import Path 6 | from archABM.engine import Engine 7 | 8 | app = typer.Typer(name="archABM", help="ArchABM simulation helper") 9 | 10 | @app.command() 11 | def run(config_file: Path = typer.Argument(..., exists=True, help="The name of the configuration file"), 12 | interactive: bool = typer.Option(False, "--interactive", "-i", prompt=False, help="Interactive CLI mode"), 13 | save_log: bool = typer.Option(False, "--save-log", "-l", help="Save events logs"), 14 | save_config: bool = typer.Option(True, "--save-config", "-c", help="Save configuration file"), 15 | save_csv: bool = typer.Option(True, "--save-csv", "-t", help="Export results to csv format"), 16 | save_json: bool = typer.Option(False, "--save-json", "-j", help="Export results to json format"), 17 | return_output: bool = typer.Option(False, "--return-output", "-o", help="Return results dictionary") 18 | ): 19 | """ArchABM simulation helper""" 20 | if config_file.is_file(): 21 | with open(config_file, "r") as f: 22 | config = json.load(f) 23 | 24 | if interactive: 25 | save_log = typer.confirm("Save events logs", default=False) 26 | save_config = typer.confirm("Save configuration", default=True) 27 | save_csv = typer.confirm("Export to csv", default=True) 28 | save_json = typer.confirm("Export to json", default=False) 29 | return_output = typer.confirm("Return results", default=False) 30 | 31 | config["options"]["save_log"] = save_log 32 | config["options"]["save_config"] = save_config 33 | config["options"]["save_csv"] = save_csv 34 | config["options"]["save_json"] = save_json 35 | config["options"]["return_output"] = return_output 36 | 37 | typer.secho(f"Running archABM", fg=typer.colors.BLACK, bg=typer.colors.BRIGHT_GREEN) 38 | simulation = Engine(config) 39 | results = simulation.run() 40 | else: 41 | typer.secho(f"Not working", fg=typer.colors.WHITE, bg=typer.colors.RED, err=True) 42 | raise typer.Exit(code=1) 43 | 44 | 45 | if __name__ == "__main__": 46 | app() -------------------------------------------------------------------------------- /main_manual.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | with open("data/config.json", "r") as f: 4 | config = json.load(f) 5 | 6 | from archABM.engine import Engine 7 | 8 | simulation = Engine(config) 9 | results = simulation.run() -------------------------------------------------------------------------------- /models/2020_COVID-19_Aerosol_Transmission_Estimator.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/models/2020_COVID-19_Aerosol_Transmission_Estimator.xlsx -------------------------------------------------------------------------------- /models/2021.04.21.21255898v2.full.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/models/2021.04.21.21255898v2.full.pdf -------------------------------------------------------------------------------- /models/BEST.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/models/BEST.pdf -------------------------------------------------------------------------------- /models/COVID-19_Indoor_Safety_Guideline_PNAS_with_CO2_final.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/models/COVID-19_Indoor_Safety_Guideline_PNAS_with_CO2_final.xlsx -------------------------------------------------------------------------------- /models/acs.estlett.1c00183.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/models/acs.estlett.1c00183.pdf -------------------------------------------------------------------------------- /models/ijerph-17-08114-s001.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/models/ijerph-17-08114-s001.xlsx -------------------------------------------------------------------------------- /models/ijerph-17-08114-v3.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/models/ijerph-17-08114-v3.pdf -------------------------------------------------------------------------------- /models/ina.12751.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/models/ina.12751.pdf -------------------------------------------------------------------------------- /models/thermal_model.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/models/thermal_model.ods -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools>=42", 4 | "wheel" 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | simpy 2 | tqdm 3 | jsonschema 4 | typer[all] 5 | pandas -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | simpy 2 | tqdm 3 | jsonschema 4 | numpy 5 | matplotlib 6 | pandas 7 | seaborn 8 | networkx 9 | igraph 10 | odfpy 11 | licenseheaders -------------------------------------------------------------------------------- /requirements_docs.txt: -------------------------------------------------------------------------------- 1 | sphinx 2 | sphinx-rtd-theme 3 | sphinx-autobuild 4 | sphinxcontrib-bibtex 5 | sphinxcontrib-tikz 6 | sphinx-jsonschema 7 | sphinx-copybutton 8 | sphinxcontrib-programoutput 9 | sphinx-tabs -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import json 2 | from archABM.engine import Engine 3 | 4 | experiments = range(8) 5 | 6 | for xp in experiments: 7 | with open("experiments/config_" + str(xp) + ".json", "r") as f: 8 | print(xp) 9 | config = json.load(f) 10 | simulation = Engine(config) 11 | results = simulation.run() 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/setup.cfg -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | import setuptools 5 | 6 | with open("README.md", "r") as fh: 7 | README = fh.read() 8 | 9 | NAME = "archABM" 10 | DESCRIPTION = "Agent based simulation for architectural spaces" 11 | URL = "https://github.com/Vicomtech/ArchABM" 12 | AUTHOR = "Vicomtech" 13 | AUTHOR_EMAIL = "info@vicomtech.org" 14 | CLASSIFIERS = [ 15 | "Development Status :: 2 - Pre-Alpha", 16 | "Intended Audience :: Developers", 17 | "License :: OSI Approved :: MIT License", 18 | "Natural Language :: English", 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3.5", 21 | "Programming Language :: Python :: 3.6", 22 | "Programming Language :: Python :: 3.7", 23 | "Programming Language :: Python :: 3.8", 24 | "Programming Language :: Python :: 3.9", 25 | ] 26 | ENTRY_POINTS = { 27 | "console_scripts": [], 28 | } 29 | PROJECT_URLS = { 30 | "Bug Reports": URL + "/issues", 31 | "Documentation": "https://vicomtech.github.io/ArchABM/", 32 | "Source Code": URL, 33 | } 34 | REQUIRES_PYTHON = ">=3.5, <4" 35 | EXTRAS_REQUIRE = {} 36 | KEYWORDS = ["agent simulation", "architecture", "building", "workplace", "discrete-event"] 37 | LICENSE = "MIT license" 38 | TEST_SUITE = "tests" 39 | REQUIREMENTS = ["simpy", "tqdm", "jsonschema", "typer[all]", "pandas"] 40 | SETUP_REQUIREMENTS = [] 41 | TEST_REQUIREMENTS = ["pytest", "pytest-cov"] 42 | VERSION = "0.4.1" 43 | 44 | setuptools.setup( 45 | author=AUTHOR, 46 | author_email=AUTHOR_EMAIL, 47 | classifiers=CLASSIFIERS, 48 | description=DESCRIPTION, 49 | entry_points=ENTRY_POINTS, 50 | extras_require=EXTRAS_REQUIRE, 51 | include_package_data=True, 52 | install_requires=REQUIREMENTS, 53 | keywords=KEYWORDS, 54 | license=LICENSE, 55 | long_description=README, 56 | long_description_content_type='text/markdown', 57 | name=NAME, 58 | package_data={}, 59 | packages=setuptools.find_packages(), 60 | project_urls=PROJECT_URLS, 61 | python_requires=REQUIRES_PYTHON, 62 | setup_requires=SETUP_REQUIREMENTS, 63 | test_suite=TEST_SUITE, 64 | tests_require=TEST_REQUIREMENTS, 65 | url=URL, 66 | version=VERSION, 67 | zip_safe=False, 68 | ) 69 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | from archABM.engine import Engine 3 | 4 | with open("archABM/config.json", "r") as f: 5 | config = json.load(f) 6 | simulation = Engine(config) 7 | results = simulation.run() 8 | -------------------------------------------------------------------------------- /visualization/analysis.py: -------------------------------------------------------------------------------- 1 | # %% 2 | import os 3 | import json 4 | import numpy as np 5 | import pandas as pd 6 | import matplotlib.pyplot as plt 7 | import seaborn as sns 8 | 9 | # %% 10 | 11 | path = "../results" 12 | experiments = os.listdir(path) 13 | experiments.sort() 14 | experiment = experiments[-1] 15 | print(experiment) 16 | 17 | config_file = os.path.join(path, experiment, "config.json") 18 | places_file = os.path.join(path, experiment, "places.csv") 19 | people_file = os.path.join(path, experiment, "people.csv") 20 | 21 | config = json.load(open(config_file)) 22 | places = pd.read_csv(places_file) 23 | people = pd.read_csv(people_file) 24 | 25 | places_info = pd.DataFrame(config["Places"]) 26 | people_info = pd.DataFrame(config["People"]) 27 | events_info = pd.DataFrame(config["Events"]) 28 | options_info = pd.DataFrame(config["Options"]) 29 | 30 | places = pd.merge(places, places_info, left_on="place", right_index=True) 31 | people = pd.merge(people, people_info, left_on="person", right_index=True) 32 | # %% 33 | 34 | plt.figure(figsize=(8, 6)) 35 | sns.scatterplot(data=places, x="time", y="num_people", hue="activity", ci=False, linewidth=0) 36 | 37 | # %% Hist num_people 38 | g = sns.FacetGrid(places, col="name", hue="activity", col_wrap=4, despine=False) 39 | g.map_dataframe(sns.histplot, x="num_people", binwidth=1) 40 | g.set_axis_labels("Num People", "Count") 41 | g.add_legend() 42 | 43 | # %% Hist occupancy 44 | places["occupancy"] = places["num_people"] / places["capacity"] 45 | g = sns.FacetGrid(places, col="name", hue="activity", col_wrap=4, despine=False) 46 | g.map_dataframe(sns.histplot, x="occupancy") 47 | g.set_axis_labels("Occupancy", "Count") 48 | g.set(xlim=(0, 1), ylim=(0, None)) 49 | g.add_legend() 50 | 51 | # %% Hist air_quality 52 | g = sns.FacetGrid(places, col="name", hue="activity", col_wrap=4, despine=False) 53 | g.map_dataframe(sns.histplot, x="air_quality") 54 | g.set_axis_labels("Air Quality", "Count") 55 | g.add_legend() 56 | 57 | # %% Timeline num_people 58 | g = sns.FacetGrid(places, col="name", hue="activity", col_wrap=4, despine=False) 59 | g.map_dataframe(sns.scatterplot, x="time", y="num_people", ci=False, linewidth=0) 60 | g.set_axis_labels("Time", "Num People") 61 | g.set(xlim=(0, 1440), ylim=(0, None)) 62 | g.add_legend() 63 | 64 | # %% Timeline air_quality 65 | g = sns.FacetGrid(places, col="name", hue="activity", col_wrap=4, despine=False) 66 | g.map_dataframe(sns.lineplot, x="time", y="air_quality", ci=False) 67 | g.set_axis_labels("Time", "Air Quality") 68 | g.set(xlim=(0, 1440), ylim=(0, None)) 69 | g.add_legend() 70 | 71 | 72 | # %% Timeline place per person 73 | sns.catplot(x="time", y="name", hue="activity", kind="swarm", data=people) 74 | 75 | # %% Count of activities per person 76 | sns.catplot(x="name", hue="activity", kind="count", data=people, aspect=2) 77 | 78 | # %% Risk per person 79 | 80 | plt.figure(figsize=(10, 8)) 81 | sns.lineplot(data=people, x="time", y="risk", hue="department", size="name", legend="full") 82 | 83 | 84 | # %% 85 | 86 | nodes = places_info 87 | # nodes = places_info.iloc[np.unique(people.place)].reset_index() 88 | 89 | edges = [] 90 | for name, group in people.groupby("person"): 91 | # print(group) 92 | a = group.place.values 93 | b = group.place.shift(1).values 94 | 95 | x = np.vstack([b, a]).T 96 | x = x[~np.isnan(x).any(axis=1), :] 97 | edges.append(x) 98 | 99 | edges = np.concatenate(edges).astype(np.uint8) 100 | u, counts = np.unique(edges, axis=0, return_counts=True) 101 | edges = np.concatenate((u, counts[:, None]), axis=1) 102 | 103 | 104 | # %% 105 | 106 | import networkx as nx 107 | 108 | G = nx.Graph() 109 | 110 | k = 0 111 | for n in nodes.to_dict("records"): 112 | G.add_node(k, attr_dict=n) 113 | k += 1 114 | 115 | for e in edges: 116 | G.add_edge(e[0], e[1], weight=e[2]) 117 | 118 | pos = nx.kamada_kawai_layout(G) 119 | pos = nx.circular_layout(G) 120 | cmap = plt.cm.tab10 121 | categories = pd.unique(nodes.activity) 122 | activities = pd.Categorical(nodes.activity, categories) 123 | m = len(categories) 124 | 125 | plt.figure(figsize=(8, 8)) 126 | nx.draw( 127 | G, 128 | pos=pos, 129 | with_labels=True, 130 | node_color=activities.codes / m, 131 | node_shape="o", 132 | vmin=0.0, 133 | vmax=1.0, 134 | cmap=cmap, 135 | width=edges[:, 2] * 0.25, 136 | labels=nodes.name, 137 | edge_color="#d9d9d990", 138 | node_size=10 * nodes.area * nodes.height, 139 | ) 140 | 141 | for i in range(6): 142 | plt.scatter([], [], c=[cmap(i / m)], label=activities.categories[i]) 143 | 144 | plt.legend() 145 | 146 | 147 | # %% 148 | 149 | import igraph 150 | 151 | g = igraph.Graph() 152 | g.add_vertices(len(nodes)) 153 | for key in nodes: 154 | g.vs[key] = nodes[key] 155 | # g.vs['label'] = G.vs['name'] 156 | # g.add_edges(edges.tolist()) 157 | # g.add_edges(list(map(tuple, edges))) 158 | g.add_edges(edges[:, :2]) 159 | g.es["weight"] = edges[:, 2] 160 | 161 | layout = g.layout("kk") 162 | igraph.plot(g, layout=layout, bbox=(500, 500), vertex_label=g.vs["name"]) 163 | # %% 164 | -------------------------------------------------------------------------------- /visualization/floorplan/floorplan.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/visualization/floorplan/floorplan.pdf -------------------------------------------------------------------------------- /visualization/floorplan/floorplan_CO2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/visualization/floorplan/floorplan_CO2.pdf -------------------------------------------------------------------------------- /visualization/floorplan/floorplan_backup.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 38 | 40 | 41 | 43 | image/svg+xml 44 | 46 | 47 | 48 | 49 | 50 | 55 | 62 | 69 | 76 | 83 | 90 | 97 | 104 | 111 | 118 | 125 | 132 | 139 | 146 | 153 | 154 | 155 | -------------------------------------------------------------------------------- /visualization/floorplan/floorplan_legend_CO2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/visualization/floorplan/floorplan_legend_CO2.pdf -------------------------------------------------------------------------------- /visualization/floorplan/floorplan_legend_quanta.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/visualization/floorplan/floorplan_legend_quanta.pdf -------------------------------------------------------------------------------- /visualization/floorplan/floorplan_quanta.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/visualization/floorplan/floorplan_quanta.pdf -------------------------------------------------------------------------------- /visualization/performance.R: -------------------------------------------------------------------------------- 1 | library(magrittr) 2 | Sys.setlocale("LC_ALL", 'en_US.UTF-8') 3 | Sys.setenv(LANG = "en_US.UTF-8") 4 | 5 | df <- data.table::fread("../experiments/performance.csv") 6 | 7 | num_places <- df$num_places %>% unique 8 | num_events <- df$num_events %>% unique 9 | 10 | df %>% 11 | dplyr::mutate(time_unitary = time / number_runs) %>% 12 | dplyr::group_by(num_people, num_places) %>% 13 | dplyr::mutate( 14 | time_mean = mean(time_unitary), 15 | time_sd = sd(time_unitary), 16 | time_max = max(time_unitary), 17 | time_min = min(time_unitary), 18 | time_upper = time_mean + time_sd, 19 | time_lower = time_mean - time_sd, 20 | ) %>% 21 | dplyr::ungroup() %>% 22 | ggplot2::ggplot()+ 23 | # ggplot2::geom_point(ggplot2::aes(x=num_people, y=time_unitary))+ 24 | # ggplot2::geom_point(ggplot2::aes(x=num_people, y=time_mean, color=as.factor(num_places)))+ 25 | # ggplot2::geom_line(ggplot2::aes(x=num_people, y=time_mean, color=as.factor(num_places)), size=1)+ 26 | ggplot2::geom_pointrange(ggplot2::aes(x=num_people, y=time_mean, 27 | ymin=time_min, ymax=time_max, color=as.factor(num_places)), 28 | size=0.1)+ 29 | ggplot2::geom_ribbon(ggplot2::aes(x=num_people, ymin=time_min, ymax=time_max, fill=as.factor(num_places)), alpha=0.1)+ 30 | ggplot2::geom_smooth(ggplot2::aes(x=num_people, y=time_mean), method='lm', formula = 'y ~ 0 + x', 31 | size=0.1, linetype="dashed", se=F, color="black", na.rm=T)+ 32 | # ggpmisc::stat_poly_eq( 33 | # ggplot2::aes(x=num_people, y=time_mean, label = paste(..eq.label.., ..rr.label.., sep = "~~~")), 34 | # formula = y~x+0, eq.with.lhs = "hat(time)~`=`~", eq.x.rhs = "~people", parse = T,) + 35 | # ggplot2::geom_text(x = 0, y = 6, label=latex2exp::TeX("$\\hat{y} = 2.4·10^{-3} \\; x$"), hjust=0, check_overlap = T)+ 36 | ggplot2::scale_y_continuous(n.breaks = 6, limits=c(0, NA))+ 37 | ggplot2::labs( 38 | x = "Number of people", 39 | y = "Time per simulation (s)", 40 | fill = "Number of\nplaces", 41 | color = "Number of\nplaces" 42 | # caption = paste("simulated", num_places, "places, and with", num_events, "events available."), 43 | )+ 44 | ggsci::scale_color_jama()+ 45 | ggsci::scale_fill_locuszoom()+ 46 | ggplot2::theme_bw()+ 47 | ggplot2::theme( 48 | legend.position = c(0.9, 0.3), 49 | legend.background = ggplot2::element_blank(), 50 | # plot.margin = grid::unit(c(0, 0, 0, 0), "null"), 51 | panel.spacing = grid::unit(c(0, 0, 0, 0), "null"), 52 | ) 53 | 54 | ggplot2::ggsave("performance.pdf", width=6, height=4, device = cairo_pdf) 55 | 56 | -------------------------------------------------------------------------------- /visualization/schedule/legend.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/visualization/schedule/legend.pdf -------------------------------------------------------------------------------- /visualization/schedule/legend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/visualization/schedule/legend.png -------------------------------------------------------------------------------- /visualization/schedule/legend.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | Activity 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | coffee 36 | home 37 | lunch 38 | meeting 39 | restroom 40 | work 41 | 42 | 43 | -------------------------------------------------------------------------------- /visualization/validation/Occupancy-detection-data-master/Correlation_Plot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/visualization/validation/Occupancy-detection-data-master/Correlation_Plot.png -------------------------------------------------------------------------------- /visualization/validation/Occupancy-detection-data-master/README.md: -------------------------------------------------------------------------------- 1 | # Occupancy-detection-data 2 | 3 | This is a repository for data for the publication: 4 | 5 | Accurate occupancy detection of an office room from light, temperature, humidity and CO2 measurements using statistical learning models. Luis M. Candanedo, Véronique Feldheim. Energy and Buildings. Volume 112, 15 January 2016, Pages 28-39. 6 | 7 | This repository hosts the experimental measurements for the occupancy detection tasks. 8 | It includes a clear description of the data files. 9 | 10 | * Description of the data columns(units etc). 11 | * The scripts to reproduce exploratory figures. 12 | * The scripts for model training. 13 | * The commands for model testing. 14 | * Also note that when training and testing the models you have to use the seed command to ensure reproducibility. 15 | * There may be small variations in the reported accuracy. 16 | 17 | 18 | Please read the commented lines in the model development file. Install all the packages dependencies before trying to train and test the models. 19 | 20 | It is advised to execute each command one by one in case you find any errors/warnings about a missing package. 21 | 22 | Please do not forget to cite the publication! Thank you! 23 | 24 | Keywords: Linear discriminant analysis, Classification and Regression Trees, Random forests, energy conservation in buildings, occupancy detection, GBM models. R, Rstudio, Caret, ggplot2. 25 | 26 | 27 | -------------------------------------------------------------------------------- /visualization/validation/Occupancy-detection-data-master/VarImp_modelRF_All.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/visualization/validation/Occupancy-detection-data-master/VarImp_modelRF_All.png -------------------------------------------------------------------------------- /visualization/validation/Occupancy-detection-data-master/pairs_plot_green_blue_time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/visualization/validation/Occupancy-detection-data-master/pairs_plot_green_blue_time.png -------------------------------------------------------------------------------- /visualization/validation/Occupancy-detection-data-master/varimportance_no_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/visualization/validation/Occupancy-detection-data-master/varimportance_no_light.png -------------------------------------------------------------------------------- /visualization/validation/validation.R: -------------------------------------------------------------------------------- 1 | library(magrittr) 2 | Sys.setlocale("LC_ALL", 'en_US.UTF-8') 3 | Sys.setenv(LANG = "en_US.UTF-8") 4 | 5 | 6 | # EMPIRICAL DATA ---------------------------------------------------------- 7 | 8 | df <- data.table::fread("Occupancy-detection-data-master/datatest.txt", drop = 1) 9 | 10 | # sensor: Telaire 6613, accuracy 400-1250 +. 30 ppm and 1250-2000 +- 5% of reading + 30ppm 11 | # room: 5.85m x 3.50m x 3.53m 12 | df %>% 13 | dplyr::filter("2015-02-03 06:00" < date & date < "2015-02-03 18:00") %>% 14 | tidyr::pivot_longer(-date) %>% 15 | ggplot2::ggplot()+ 16 | ggplot2::geom_line(ggplot2::aes(x=date, y=value, color=name))+ 17 | ggplot2::facet_grid(dplyr::vars(name), scales="free_y") 18 | 19 | df_empirical <- df %>% 20 | dplyr::filter("2015-02-03 07:00:00" < date & date < "2015-02-03 18:00:00") %>% 21 | dplyr::mutate( 22 | hour = lubridate::hour(date), 23 | minutes = lubridate::minute(date), 24 | date = paste0("1970-01-01 ", hour , ":", minutes), 25 | date = as.POSIXct(date, origin="1970-01-01", format="%Y-%m-%d %H:%M", tz="GMT") 26 | ) %>% 27 | dplyr::select(date, CO2) %>% 28 | dplyr::mutate( 29 | CO2_error = ifelse(dplyr::between(CO2, 400, 1250), 30, 0.05*CO2 + 30), 30 | CO2_mean = CO2, 31 | CO2_upper = CO2_mean + CO2_error, 32 | CO2_lower = CO2_mean - CO2_error 33 | ) 34 | 35 | # IMPORT ------------------------------------------------------------------ 36 | 37 | experiments <- list.files(path = "../../results/validation/", full.names = T) 38 | n <- length(experiments) 39 | 40 | experiment <- experiments[n] 41 | 42 | config_file <- file.path(experiment, "config.json") 43 | places_file <- file.path(experiment, "places.csv") 44 | people_file <- file.path(experiment, "people.csv") 45 | 46 | print(experiment) 47 | config <- jsonlite::fromJSON(config_file) 48 | 49 | 50 | places <- data.table::fread(places_file) %>% 51 | dplyr::mutate( 52 | hour = floor(time/60), 53 | minutes = time %% 60, 54 | date = paste0("1970-01-01 ", hour , ":", minutes), 55 | date = as.POSIXct(date, origin="1970-01-01", format="%Y-%m-%d %H:%M", tz="GMT") 56 | ) %>% 57 | dplyr::group_by(run, place) %>% 58 | dplyr::mutate( 59 | CO2_level_delta = CO2_level - dplyr::lag(CO2_level, default=CO2_level[1]), 60 | quanta_level_delta = quanta_level - dplyr::lag(quanta_level, default=quanta_level[1]), 61 | ) %>% 62 | dplyr::mutate( 63 | infective_people_mean = mean(infective_people, na.rm=T), 64 | CO2_level_max = max(CO2_level, na.rm=T), 65 | quanta_level_max = max(quanta_level, na.rm=T), 66 | CO2_level_delta_max = max(CO2_level_delta, na.rm=T), 67 | quanta_level_delta_max = max(quanta_level_delta, na.rm=T), 68 | ) %>% 69 | dplyr::ungroup() 70 | 71 | people <- data.table::fread(people_file) %>% 72 | dplyr::mutate( 73 | hour = floor(time/60), 74 | minutes = time %% 60, 75 | date = paste0("1970-01-01 ", hour , ":", minutes), 76 | date = as.POSIXct(date, origin="1970-01-01", format="%Y-%m-%d %H:%M", tz="GMT") 77 | ) %>% 78 | dplyr::mutate(infection_risk = 1 - exp(-quanta_inhaled)) %>% 79 | dplyr::group_by(run, person) %>% 80 | dplyr::arrange(time) %>% 81 | dplyr::mutate( 82 | elapsed = time - dplyr::lag(time, default = 0), 83 | quanta_inhaled_delta = quanta_inhaled - dplyr::lag(quanta_inhaled, default=quanta_inhaled[1]), 84 | ) %>% 85 | dplyr::mutate( 86 | CO2_level_mean = weighted.mean(CO2_level, elapsed, na.rm=T), 87 | quanta_inhaled_max = max(quanta_inhaled, na.rm=T), 88 | quanta_inhaled_delta_max = max(quanta_inhaled_delta, na.rm=T), 89 | ) %>% 90 | dplyr::ungroup() 91 | 92 | 93 | events_info <- config$events %>% 94 | tibble::rowid_to_column(var = "event") %>% 95 | dplyr::mutate(event = event-1) 96 | 97 | places_info <- config$places %>% 98 | tibble::rowid_to_column(var = "place") %>% 99 | dplyr::mutate(place = place-1) 100 | 101 | people_info <- config$people %>% 102 | tibble::rowid_to_column(var = "person") %>% 103 | dplyr::mutate(person = person-1) 104 | 105 | places <- merge(places, places_info, by = "place") 106 | people <- merge(people, people_info, by = "person") 107 | people <- merge(people, events_info, by = "event") 108 | 109 | 110 | # VISUALIZATION ----------------------------------------------------------- 111 | 112 | color_palette <- ggsci::pal_d3()(2) 113 | places %>% 114 | dplyr::filter(name == "office") %>% 115 | dplyr::filter(run == 0) %>% 116 | dplyr::mutate( 117 | CO2_level_noise = CO2_level + rnorm(dplyr::n(), 0, 5) 118 | ) %>% 119 | ggplot2::ggplot()+ 120 | ggplot2::geom_line(data = df_empirical, ggplot2::aes(x=date, y=CO2, color="Empirical data"))+ 121 | ggplot2::geom_ribbon(data = df_empirical, ggplot2::aes(x=date, ymin=CO2_lower, ymax=CO2_upper), fill=color_palette[1], alpha=0.2)+ 122 | ggplot2::geom_line(ggplot2::aes(x=date, y=CO2_level_noise, group=run, color="Simulated data"), 123 | alpha=1, size=1, linetype="solid")+ 124 | ggplot2::scale_color_manual(values=color_palette)+ 125 | ggplot2::scale_x_datetime(date_breaks = "2 hours", date_labels = "%H:%M")+ 126 | ggplot2::labs(x =NULL, y=latex2exp::TeX("$CO_2 \\, (ppm)$"), color=NULL)+ 127 | ggplot2::theme_bw()+ 128 | ggplot2::theme( 129 | legend.position = c(0.82, 0.13), 130 | legend.background = ggplot2::element_blank(), 131 | plot.margin = grid::unit(c(0, 0, 0, 0), "null"), 132 | panel.spacing = grid::unit(c(0, 0, 0, 0), "null"), 133 | ) 134 | 135 | ggplot2::ggsave("validation.pdf", width=5, height=3, device = cairo_pdf) 136 | 137 | 138 | 139 | # people %>% 140 | # dplyr::filter(activity == "work") %>% 141 | # dplyr::filter(run == 0) %>% 142 | # ggplot2::ggplot()+ 143 | # ggplot2::geom_line(ggplot2::aes(x=date, y=CO2_level, color=name))+ 144 | # ggplot2::geom_point(ggplot2::aes(x=date, y=CO2_level)) 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /visualization/validation/validation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Vicomtech/ArchABM/a5cbe99ab81a88113c06124f95f15a50a1aba899/visualization/validation/validation.pdf --------------------------------------------------------------------------------