From a2dcf33b821bad4541851134c540e21fe8f38280 Mon Sep 17 00:00:00 2001 From: Chris Mackey Date: Wed, 24 Feb 2021 00:12:03 -0500 Subject: [PATCH] feat(recipe): Add the full specification for the recipe --- .github/workflows/ci.yaml | 82 ++++++++ .gitignore | 19 ++ .releaserc.json | 13 ++ LICENSE | 164 +++++++++++++++ README.md | 13 +- deploy.sh | 6 + dev-requirements.txt | 4 + pollination/pmv_comfort_map/__init__.py | 6 + pollination/pmv_comfort_map/entry.py | 252 ++++++++++++++++++++++++ requirements.txt | 7 + setup.cfg | 5 + setup.py | 33 ++++ 12 files changed, 603 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/ci.yaml create mode 100644 .gitignore create mode 100644 .releaserc.json create mode 100644 LICENSE create mode 100644 deploy.sh create mode 100644 dev-requirements.txt create mode 100644 pollination/pmv_comfort_map/__init__.py create mode 100644 pollination/pmv_comfort_map/entry.py create mode 100644 requirements.txt create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..6f715d4 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,82 @@ +--- +name: CI + +on: [push, pull_request] + +jobs: + + deploy: + name: Deploy to GitHub and PyPI + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/master' && github.repository_owner == 'pollination' + steps: + - uses: actions/checkout@v2 + - name: set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: set up node # we need node for for semantic release + uses: actions/setup-node@v2.1.2 + with: + node-version: 14.2.0 + - name: install python dependencies + run: | + pip install -r dev-requirements.txt + pip install . + - name: install semantic-release + run: + npm install @semantic-release/exec + - name: run semantic release + id: new_release + run: | + nextRelease="`npx semantic-release --dryRun | grep -oP 'Published release \K.*? ' || true`" + npx semantic-release + echo "::set-output name=tag::$nextRelease" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PYPI_USERNAME: ${{ secrets.POLLINATION_PYPI_USERNAME }} + PYPI_PASSWORD: ${{ secrets.POLLINATION_PYPI_PASSWORD }} + + outputs: + tag: ${{ steps.new_release.outputs.tag }} + + deploy-to-staging: + name: Deploy to Pollination Staging + runs-on: ubuntu-latest + needs: deploy + if: ${{ github.ref == 'refs/heads/master' && github.repository_owner == 'pollination' && contains(needs.deploy.outputs.tag, '.') }} + steps: + - uses: actions/checkout@v2 + - name: set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: install python dependencies + run: pip install . + - name: deploy to staging + run: | + queenbee + pollination dsl push pollination-pmv-comfort-map --tag ${{needs.deploy.outputs.tag}} -e https://api.staging.pollination.cloud -src https://api.staging.pollination.cloud/registries + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + QB_POLLINATION_TOKEN: ${{ secrets.POLLINATION_STAGING_LADYBUGBOT_TOKEN }} + + deploy-to-production: + name: Deploy to Pollination Production + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/production' && github.repository_owner == 'pollination' + steps: + - uses: actions/checkout@v2 + - name: set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: install python dependencies + run: pip install pollination-pmv-comfort-map + - name: deploy to production + run: | + queenbee + pollination dsl push pollination-pmv-comfort-map -e https://api.pollination.cloud -src https://api.pollination.cloud/registries + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + QB_POLLINATION_TOKEN: ${{ secrets.POLLINATION_LADYBUGBOT_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..38eff34 --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +*.pyc +test.py +.pytest_cache +*__pycache__ +.coverage +*.ipynb +.ipynb_checkpoints +.tox +*.egg-info +tox.ini +/.cache +/.vscode +.eggs +*.code-workspace + +venv +test.py +dist/ +build/ diff --git a/.releaserc.json b/.releaserc.json new file mode 100644 index 0000000..0459b65 --- /dev/null +++ b/.releaserc.json @@ -0,0 +1,13 @@ +{ + "plugins": [ + "@semantic-release/commit-analyzer", + "@semantic-release/release-notes-generator", + "@semantic-release/github", + [ + "@semantic-release/exec", + { + "publishCmd": "bash deploy.sh" + } + ] + ] +} diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3f09b05 --- /dev/null +++ b/LICENSE @@ -0,0 +1,164 @@ +# PolyForm Shield License 1.0.0 + + + +## Acceptance + +In order to get any license under these terms, you must agree +to them as both strict obligations and conditions to all +your licenses. + +## Copyright License + +The licensor grants you a copyright license for the +software to do everything you might do with the software +that would otherwise infringe the licensor's copyright +in it for any permitted purpose. However, you may +only distribute the software according to [Distribution +License](#distribution-license) and make changes or new works +based on the software according to [Changes and New Works +License](#changes-and-new-works-license). + +## Distribution License + +The licensor grants you an additional copyright license +to distribute copies of the software. Your license +to distribute covers distributing the software with +changes and new works permitted by [Changes and New Works +License](#changes-and-new-works-license). + +## Notices + +You must ensure that anyone who gets a copy of any part of +the software from you also gets a copy of these terms or the +URL for them above, as well as copies of any plain-text lines +beginning with `Required Notice:` that the licensor provided +with the software. For example: + +> Required Notice: Copyright Yoyodyne, Inc. (http://example.com) + +## Changes and New Works License + +The licensor grants you an additional copyright license to +make changes and new works based on the software for any +permitted purpose. + +## Patent License + +The licensor grants you a patent license for the software that +covers patent claims the licensor can license, or becomes able +to license, that you would infringe by using the software. + +## Noncompete + +Any purpose is a permitted purpose, except for providing any +product that competes with the software or any product the +licensor or any of its affiliates provides using the software. + +## Competition + +Goods and services compete even when they provide functionality +through different kinds of interfaces or for different technical +platforms. Applications can compete with services, libraries +with plugins, frameworks with development tools, and so on, +even if they're written in different programming languages +or for different computer architectures. Goods and services +compete even when provided free of charge. If you market a +product as a practical substitute for the software or another +product, it definitely competes. + +## New Products + +If you are using the software to provide a product that does +not compete, but the licensor or any of its affiliates brings +your product into competition by providing a new version of +the software or another product using the software, you may +continue using versions of the software available under these +terms beforehand to provide your competing product, but not +any later versions. + +## Discontinued Products + +You may begin using the software to compete with a product +or service that the licensor or any of its affiliates has +stopped providing, unless the licensor includes a plain-text +line beginning with `Licensor Line of Business:` with the +software that mentions that line of business. For example: + +> Licensor Line of Business: YoyodyneCMS Content Management +System (http://example.com/cms) + +## Sales of Business + +If the licensor or any of its affiliates sells a line of +business developing the software or using the software +to provide a product, the buyer can also enforce +[Noncompete](#noncompete) for that product. + +## Fair Use + +You may have "fair use" rights for the software under the +law. These terms do not limit them. + +## No Other Rights + +These terms do not allow you to sublicense or transfer any of +your licenses to anyone else, or prevent the licensor from +granting licenses to anyone else. These terms do not imply +any other licenses. + +## Patent Defense + +If you make any written claim that the software infringes or +contributes to infringement of any patent, your patent license +for the software granted under these terms ends immediately. If +your company makes such a claim, your patent license ends +immediately for work on behalf of your company. + +## Violations + +The first time you are notified in writing that you have +violated any of these terms, or done anything with the software +not covered by your licenses, your licenses can nonetheless +continue if you come into full compliance with these terms, +and take practical steps to correct past violations, within +32 days of receiving notice. Otherwise, all your licenses +end immediately. + +## No Liability + +***As far as the law allows, the software comes as is, without +any warranty or condition, and the licensor will not be liable +to you for any damages arising out of these terms or the use +or nature of the software, under any kind of legal claim.*** + +## Definitions + +The **licensor** is the individual or entity offering these +terms, and the **software** is the software the licensor makes +available under these terms. + +A **product** can be a good or service, or a combination +of them. + +**You** refers to the individual or entity agreeing to these +terms. + +**Your company** is any legal entity, sole proprietorship, +or other kind of organization that you work for, plus all +its affiliates. + +**Affiliates** means the other organizations than an +organization has control over, is under the control of, or is +under common control with. + +**Control** means ownership of substantially all the assets of +an entity, or the power to direct its management and policies +by vote, contract, or otherwise. Control can be direct or +indirect. + +**Your licenses** are all the licenses granted to you for the +software under these terms. + +**Use** means anything you do with the software requiring one +of your licenses. diff --git a/README.md b/README.md index fedb7dd..5f8376d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,13 @@ # pmv-comfort-map -PMVthermal comfort map for pollination + +PMV thermal comfort map recipe for Pollination. + +Compute spatially-resolved operative temperature and PMV thermal comfort from +a HBJSON model. + +This recipe uses EnergyPlus to obtain air temperatures and longwave radiant temperatures. +A Radiance-based enhanced 2-phase method is used for shortwave solar calculations, +which includes an accurate direct sun calculation using precise solar positions. The +energy properties of the model geometry are what determine the outcome of the +simulation but the model's SensorGrids are what determine where the comfort +mapping occurs. diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..057a123 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +echo "Building distribution" +python setup.py sdist bdist_wheel +echo "Pushing new version to PyPi" +twine upload dist/* -u $PYPI_USERNAME -p $PYPI_PASSWORD diff --git a/dev-requirements.txt b/dev-requirements.txt new file mode 100644 index 0000000..be32de8 --- /dev/null +++ b/dev-requirements.txt @@ -0,0 +1,4 @@ +pytest==6.2.2 +twine==3.3.0 +wheel==0.36.2 +setuptools==53.0.0 diff --git a/pollination/pmv_comfort_map/__init__.py b/pollination/pmv_comfort_map/__init__.py new file mode 100644 index 0000000..9576378 --- /dev/null +++ b/pollination/pmv_comfort_map/__init__.py @@ -0,0 +1,6 @@ +from .entry import PMVComfortMapEntryPoint + + +__pollination__ = { + 'entry_point': PMVComfortMapEntryPoint +} diff --git a/pollination/pmv_comfort_map/entry.py b/pollination/pmv_comfort_map/entry.py new file mode 100644 index 0000000..61e0c83 --- /dev/null +++ b/pollination/pmv_comfort_map/entry.py @@ -0,0 +1,252 @@ +from pollination_dsl.dag import Inputs, DAG, task, Outputs +from dataclasses import dataclass +from typing import Dict, List + +# pollination plugins and recipes +from pollination.ladybug.translate import EpwToWea +from pollination.ladybug_comfort.map import PmvMap, MapResultInfo +from pollination.honeybee_radiance.translate import CreateRadiantEnclosureInfo +from pollination.honeybee_radiance.edit import MirrorModelSensorGrids +from pollination.honeybee_energy.settings import SimParComfort +from pollination.honeybee_energy.simulate import SimulateModel +from pollination.lbt_honeybee.edit import ModelModifiersFromConstructions +from pollination.annual_radiation.entry import AnnualRadiationEntryPoint + +# input/output alias +from pollination.alias.inputs.model import hbjson_model_input +from pollination.alias.inputs.ddy import ddy_input +from pollination.alias.inputs.data import value_or_data +from pollination.alias.inputs.north import north_input +from pollination.alias.inputs.bool_options import write_set_map_input + + +@dataclass +class PMVComfortMapEntryPoint(DAG): + """PMV comfort map entry point.""" + + # inputs + model = Inputs.file( + description='A Honeybee model in HBJSON file format.', + extensions=['json', 'hbjson'], + alias=hbjson_model_input + ) + + epw = Inputs.file( + description='EPW weather file to be used for the comfort map simulation.', + extensions=['epw'] + ) + + ddy = Inputs.file( + description='A DDY file with design days to be used for the initial ' + 'sizing calculation.', extensions=['ddy'], + alias=ddy_input + ) + + north = Inputs.float( + default=0, + description='A a number between -360 and 360 for the counterclockwise ' + 'difference between the North and the positive Y-axis in degrees.', + spec={'type': 'number', 'minimum': -360, 'maximum': 360}, + alias=north_input + ) + + run_period = Inputs.str( + description='An AnalysisPeriod string to set the start and end dates of ' + 'the simulation (eg. "6/21 to 9/21 between 0 and 23 @1"). If None, ' + 'the simulation will be annual.', default='' + ) + + sensor_count = Inputs.int( + default=200, + description='The maximum number of grid points per parallel execution.', + spec={'type': 'integer', 'minimum': 1} + ) + + write_set_map = Inputs.str( + description='A switch to note whether the output temperature CSV should ' + 'record Operative Temperature or Standard Effective Temperature (SET). ' + 'SET is relatively intense to compute and so only recording Operative ' + 'Temperature can greatly reduce run time, particularly when air speeds ' + 'are low. However, SET accounts for all 6 PMV model inputs and so is a ' + 'more representative "feels-like" temperature for the PMV model.', + default='write-op-map', alias=write_set_map_input, + spec={'type': 'string', 'enum': ['write-op-map', 'write-set-map']} + ) + + air_speed = Inputs.str( + description='A single number for air speed in m/s or a string of a JSON array ' + 'with numbers that align with the result-sql reporting period. This ' + 'will be used for all indoor comfort evaluation.', default='0.1', + alias=value_or_data + ) + + met_rate = Inputs.str( + description='A single number for metabolic rate in met or a string of a ' + 'JSON array with numbers that align with the result-sql reporting period.', + default='1.1', alias=value_or_data + ) + + clo_value = Inputs.str( + description='A single number for clothing level in clo or a string of a JSON ' + 'array with numbers that align with the result-sql reporting period.', + default='0.7', alias=value_or_data + ) + + solarcal_parameters = Inputs.str( + description='A SolarCalParameter string to customize the assumptions of ' + 'the SolarCal model.', default='--posture seated --sharp 135 ' + '--absorptivity 0.7 --emissivity 0.95' + ) + + comfort_parameters = Inputs.str( + description='An PMVParameter string to customize the assumptions of ' + 'the PMV comfort model.', default='--ppd-threshold 10' + ) + + radiance_parameters = Inputs.str( + description='Radiance parameters for ray tracing.', + default='-ab 2 -ad 5000 -lw 2e-05' + ) + + # tasks + @task(template=EpwToWea) + def create_wea(self, epw=epw, period=run_period) -> List[Dict]: + return [ + {'from': EpwToWea()._outputs.wea, + 'to': 'in.wea'} + ] + + @task(template=SimParComfort) + def create_sim_par(self, ddy=ddy, run_period=run_period, north=north) -> List[Dict]: + return [ + {'from': SimParComfort()._outputs.sim_par_json, + 'to': 'energy/simulation_parameter.json'} + ] + + @task(template=SimulateModel, needs=[create_sim_par]) + def run_energy_simulation( + self, model=model, epw=epw, + sim_par=create_sim_par._outputs.sim_par_json + ) -> List[Dict]: + return [ + {'from': SimulateModel()._outputs.sql, 'to': 'energy/eplusout.sql'} + ] + + @task(template=CreateRadiantEnclosureInfo) + def get_enclosure_info(self, model=model) -> List[Dict]: + return [ + { + 'from': CreateRadiantEnclosureInfo()._outputs.output_folder, + 'to': 'radiance/enclosures' + }, + { + 'from': CreateRadiantEnclosureInfo()._outputs.enclosure_list_file, + 'to': 'results/grids_info.json' + }, + { + 'from': CreateRadiantEnclosureInfo()._outputs.enclosure_list, + 'description': 'Information about exported enclosure JSONs.' + } + ] + + @task(template=ModelModifiersFromConstructions) + def set_modifiers_from_constructions( + self, model=model, use_visible='solar', exterior_offset=0.03 + ) -> List[Dict]: + return [ + {'from': ModelModifiersFromConstructions()._outputs.new_model, + 'to': 'radiance/hbjson/1_energy_modifiers.hbjson'} + ] + + @task(template=MirrorModelSensorGrids, needs=[set_modifiers_from_constructions]) + def mirror_sensor_grids( + self, model=set_modifiers_from_constructions._outputs.new_model + ) -> List[Dict]: + return [ + {'from': MirrorModelSensorGrids()._outputs.new_model, + 'to': 'radiance/hbjson/2_mirrored_grids.hbjson'} + ] + + @task( + template=AnnualRadiationEntryPoint, + needs=[create_wea, mirror_sensor_grids], + sub_folder='radiance/shortwave', # create a subfolder for the whole simulation + ) + def run_irradiance_simulation( + self, model=mirror_sensor_grids._outputs.new_model, wea=create_wea._outputs.wea, + north=north, sensor_count=sensor_count, radiance_parameters=radiance_parameters + ) -> List[Dict]: + pass + + @task( + template=PmvMap, + needs=[run_energy_simulation, run_irradiance_simulation, get_enclosure_info], + loop=get_enclosure_info._outputs.enclosure_list, + sub_folder='results', # create a subfolder for each grid + sub_paths={ + 'enclosure_info': '{{item.id}}.json', # sub_path for enclosure_info arg + 'total_irradiance': '{{item.id}}.ill', # sub_path for total irradiance arg + 'direct_irradiance': '{{item.id}}.ill', # sub_path for total direct_irradiance arg + 'ref_irradiance': '{{item.id}}_ref.ill', # sub_path for reflected irradiance arg + 'sun_up_hours': 'sun-up-hours.txt' + } + ) + def run_comfort_map( + self, result_sql=run_energy_simulation._outputs.sql, + enclosure_info=get_enclosure_info._outputs.output_folder, epw=epw, + total_irradiance='radiance/shortwave/results/total', + direct_irradiance='radiance/shortwave/results/direct', + ref_irradiance='radiance/shortwave/results/total', + sun_up_hours='radiance/shortwave/results/total', + air_speed=air_speed, met_rate=met_rate, clo_value=clo_value, + solarcal_par=solarcal_parameters, comfort_par=comfort_parameters, + run_period=run_period, write_set_map=write_set_map + ) -> List[Dict]: + return [ + { + 'from': PmvMap()._outputs.temperature_map, + 'to': 'temperature/{{item.id}}.csv' + }, + { + 'from': PmvMap()._outputs.condition_map, + 'to': 'condition/{{item.id}}.csv' + }, + { + 'from': PmvMap()._outputs.pmv_map, + 'to': 'condition_intensity/{{item.id}}.csv' + } + ] + + @task(template=MapResultInfo) + def create_result_info( + self, comfort_model='pmv', run_period=run_period, qualifier=write_set_map + ) -> List[Dict]: + return [ + {'from': MapResultInfo()._outputs.results_info_file, + 'to': 'results/results_info.json'} + ] + + # outputs + results = Outputs.folder( + source='results', + description='A folder containing all results.' + ) + + temperature = Outputs.folder( + source='results/temperature', description='A folder containing CSV maps of ' + 'Operative Temperature for each sensor grid. Values are in Celsius.' + ) + + condition = Outputs.folder( + source='results/condition', description='A folder containing CSV maps of ' + 'comfort conditions for each sensor grid. -1 indicates unacceptably cold ' + 'conditions. +1 indicates unacceptably hot conditions. 0 indicates neutral ' + '(comfortable) conditions.' + ) + + condition_intensity = Outputs.folder( + source='results/condition_intensity', description='A folder containing CSV maps ' + 'of the Predicted Mean Vote (PMV) for each sensor grid. This can be used ' + 'to understand not just whether conditions are acceptable but how ' + 'uncomfortably hot or cold they are.' + ) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..ebf6495 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +pollination-ladybug>=0.1.4 +pollination-ladybug-comfort>=0.1.3 +pollination-honeybee-radiance>=0.6.2 +pollination-honeybee-energy>=0.2.6 +pollination-lbt-honeybee>=0.1.0 +pollination-annual-radiation>=0.1.1 +pollination-alias>=0.2.3 diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..ed8a958 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +[bdist_wheel] +universal = 1 + +[metadata] +license_file = LICENSE diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..1887e13 --- /dev/null +++ b/setup.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +import setuptools + +with open("README.md", "r") as fh: + long_description = fh.read() + +with open('requirements.txt') as f: + requirements = f.read().splitlines() + +# normal setuptool inputs +setuptools.setup( + name='pollination-pmv-comfort-map', # will be used for package name + author='ladybug-tools', # the owner account for this package - required if pushed to Pollination + author_email='info@ladybug.tools', + packages=setuptools.find_namespace_packages( # required - that's how pollination find the package + include=['pollination.*'], exclude=['tests', '.github'] + ), + install_requires=requirements, + use_scm_version=True, + setup_requires=['setuptools_scm'], + url='https://github.com/pollination/pmv-comfort-map', # will be translated to home + project_urls={ + 'icon': 'https://raw.githubusercontent.com/ladybug-tools/artwork/master/icons_components/honeybee/png/pmvmap.png' + }, + description='PMV thermal comfort map for Pollination.', # will be used as package description + long_description=long_description, # will be translated to ReadMe content on Pollination + long_description_content_type="text/markdown", + maintainer='chris, ladybug-tools', # Package maintainers. For multiple maintainers use comma + maintainer_email='chris@ladybug.tools, info@ladybug.tools', + keywords='honeybee, ladybug-tools, thermal, comfort, pmv',# will be used as keywords + license='PolyForm Shield License 1.0.0, https://polyformproject.org/wp-content/uploads/2020/06/PolyForm-Shield-1.0.0.txt', # the license link should be separated by a comma + zip_safe=False +)