diff --git a/.github/linters/.ruff b/.github/linters/.ruff new file mode 100644 index 0000000..7861522 --- /dev/null +++ b/.github/linters/.ruff @@ -0,0 +1,3 @@ +[ruff] +exclude = .git,__pycache__,docs/source/conf.py,old,build,dist,bin/tmp/* +max-complexity = 20 \ No newline at end of file diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml old mode 100644 new mode 100755 index 29835fc..2b48ad5 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,31 +1,19 @@ name: Lint Code Base on: + workflow_dispatch: push: branches-ignore: - 'gh-pages' jobs: - build: - name: Lint Code Base + ruff: + name: Ruff runs-on: ubuntu-latest steps: - - name: Checkout Code - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + - uses: chartboost/ruff-action@v1 with: - fetch-depth: 0 - - - name: Lint Code Base - uses: docker://ghcr.io/github/super-linter:slim-v4 - env: - VALIDATE_ALL_CODEBASE: false - VALIDATE_PYTHON_BLACK: false - VALIDATE_PYTHON_ISORT: false - VALIDATE_PYTHON_MYPY: false - VALIDATE_DOCKERFILE_HADOLINT: false - VALIDATE_JSCPD: false - VALIDATE_JSON: false - VALIDATE_MARKDOWN: false - VALIDATE_YAML: false - DEFAULT_BRANCH: main - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + version: 0.4.10 + args: check --output-format=github + src: "./farms ./tests" diff --git a/.github/workflows/pull_request_tests.yml b/.github/workflows/pull_request_tests.yml index 16c0fdc..5c36b87 100644 --- a/.github/workflows/pull_request_tests.yml +++ b/.github/workflows/pull_request_tests.yml @@ -8,14 +8,16 @@ jobs: strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.11'] + python-version: ['3.13'] include: + - os: ubuntu-latest + python-version: '3.12' + - os: ubuntu-latest + python-version: '3.11' - os: ubuntu-latest python-version: '3.10' - os: ubuntu-latest python-version: '3.9' - - os: ubuntu-latest - python-version: '3.8' steps: - uses: actions/checkout@v2 diff --git a/farms/utilities.py b/farms/utilities.py index 0e5b045..cbf62c3 100644 --- a/farms/utilities.py +++ b/farms/utilities.py @@ -10,8 +10,10 @@ from farms import CLEAR_TYPES, CLOUD_TYPES, RADIUS, SZA_LIM +RANDOM_GENERATOR = np.random.default_rng(seed=42) -def execute_pytest(file, capture="all", flags="-rapP"): + +def execute_pytest(file, capture='all', flags='-rapP'): """Execute module as pytest with detailed summary report. Parameters @@ -26,7 +28,7 @@ def execute_pytest(file, capture="all", flags="-rapP"): """ fname = os.path.basename(file) - pytest.main(["-q", "--show-capture={}".format(capture), fname, flags]) + pytest.main(['-q', '--show-capture={}'.format(capture), fname, flags]) def check_range(data, name, rang=(0, 1)): @@ -34,10 +36,10 @@ def check_range(data, name, rang=(0, 1)): if np.nanmin(data) < rang[0] or np.nanmax(data) > rang[1]: raise ValueError( 'Variable "{n}" is out of expected ' - "transmittance/reflectance range. Recommend checking " - "solar zenith angle to ensure cos(sza) is " - "non-negative and non-zero. " - "Max/min of {n} = {mx}/{mn}".format( + 'transmittance/reflectance range. Recommend checking ' + 'solar zenith angle to ensure cos(sza) is ' + 'non-negative and non-zero. ' + 'Max/min of {n} = {mx}/{mn}'.format( n=name, mx=np.nanmax(data), mn=np.nanmin(data) ) ) @@ -91,34 +93,34 @@ def ti_to_radius(time_index, n_cols=1): """ # load earth periodic table path = os.path.dirname(os.path.realpath(__file__)) - df = pd.read_csv(os.path.join(path, "earth_periodic_terms.csv")) - df["key"] = 1 + df = pd.read_csv(os.path.join(path, 'earth_periodic_terms.csv')) + df['key'] = 1 # 3.1.1 (4). Julian Date. j = time_index.to_julian_date().values # 3.1.2 (5). Julian Ephermeris Date - j = j + 64.797 / 86400 + j += 64.797 / 86400 # 3.1.3 (7). Julian Century Ephemeris j = (j - 2451545) / 36525 # 3.1.4 (8). Julian Ephemeris Millennium - j = j / 10 - df_jme = pd.DataFrame({"uid": range(len(j)), "jme": j, "key": 1}) + j /= 10 + df_jme = pd.DataFrame({'uid': range(len(j)), 'jme': j, 'key': 1}) # Merge JME with Periodic Table - df_merge = pd.merge(df_jme, df, on="key") + df_merge = pd.merge(df_jme, df, on='key') # 3.2.1 (9). Heliocentric radius vector. - df_merge["r"] = df_merge["a"] * np.cos( - df_merge["b"] + df_merge["c"] * df_merge["jme"] + df_merge['r'] = df_merge['a'] * np.cos( + df_merge['b'] + df_merge['c'] * df_merge['jme'] ) # 3.2.2 (10). - dfs = df_merge.groupby(by=["uid", "term"])["r"].sum().unstack() + dfs = df_merge.groupby(by=['uid', 'term'])['r'].sum().unstack() # 3.2.4 (11). Earth Heliocentric radius vector radius = ( ( - dfs["R0"] - + dfs["R1"] * j - + dfs["R2"] * np.power(j, 2) - + dfs["R3"] * np.power(j, 3) - + dfs["R4"] * np.power(j, 4) - + dfs["R5"] * np.power(j, 5) + dfs['R0'] + + dfs['R1'] * j + + dfs['R2'] * np.power(j, 2) + + dfs['R3'] * np.power(j, 3) + + dfs['R4'] * np.power(j, 4) + + dfs['R5'] * np.power(j, 5) ) / np.power(10, 8) ).values @@ -147,8 +149,8 @@ def calc_beta(aod, alpha): """ if aod.shape != alpha.shape: raise ValueError( - "To calculate beta, aod and alpha inputs must be of " - "the same shape. Received arrays of shape {} and {}".format( + 'To calculate beta, aod and alpha inputs must be of ' + 'the same shape. Received arrays of shape {} and {}'.format( aod.shape, alpha.shape ) ) @@ -156,8 +158,8 @@ def calc_beta(aod, alpha): beta = aod * np.power(0.55, alpha) if np.max(beta) > 2.2 or np.min(beta) < 0: warn( - "Calculation of beta resulted in values outside of " - "expected range [0, 2.2]. Min/max of beta are: {}/{}".format( + 'Calculation of beta resulted in values outside of ' + 'expected range [0, 2.2]. Min/max of beta are: {}/{}'.format( np.min(beta), np.max(beta) ) ) @@ -249,7 +251,7 @@ def merge_rest_farms(clearsky_irrad, cloudy_irrad, cloud_type): FARMS and REST. """ # disable nan warnings - np.seterr(divide="ignore", invalid="ignore") + np.seterr(divide='ignore', invalid='ignore') # combine clearsky and farms according to the cloud types. all_sky_irrad = np.where( @@ -330,8 +332,8 @@ def cloud_variability( cs_irrad, cloud_type, var_frac=0.05, - distribution="uniform", - option="tri", + distribution='uniform', + option='tri', tri_center=0.9, random_seed=123, ): @@ -365,28 +367,29 @@ def cloud_variability( to cloudy timesteps. """ # disable divide by zero warnings - np.seterr(divide="ignore", invalid="ignore") + np.seterr(divide='ignore', invalid='ignore') if var_frac: # set a seed for psuedo-random but repeatable results - np.random.seed(seed=random_seed) + state = np.random.default_rng(random_seed).bit_generator.state + RANDOM_GENERATOR.bit_generator.state = state # update the clearsky ratio (1 is clear, 0 is cloudy or dark) csr = irrad / cs_irrad # Set the cloud/clear ratio to zero when it's nighttime csr[(cs_irrad == 0)] = 0 - if distribution == "uniform": + if distribution == 'uniform': variability_scalar = uniform_variability( csr, cloud_type, var_frac, option=option, tri_center=tri_center ) - elif distribution == "normal": + elif distribution == 'normal': variability_scalar = normal_variability( csr, cloud_type, var_frac, option=option, tri_center=tri_center ) else: raise ValueError( - "Did not recognize distribution: {}".format(distribution) + 'Did not recognize distribution: {}'.format(distribution) ) irrad *= variability_scalar @@ -395,7 +398,7 @@ def cloud_variability( def uniform_variability( - csr, cloud_type, var_frac, option="tri", tri_center=0.9 + csr, cloud_type, var_frac, option='tri', tri_center=0.9 ): """Get an array with uniform variability scalars centered at 1 that can be multiplied by a irradiance array with the same shape as csr. @@ -422,17 +425,17 @@ def uniform_variability( 1 with range (1 - var_frac) to (1 + var_frac). This array can be multiplied by an irradiance array with the same shape as csr """ - if option == "linear": + if option == 'linear': var_frac_arr = linear_variability(csr, var_frac) - elif option == "tri": + elif option == 'tri': var_frac_arr = tri_variability(csr, var_frac, tri_center=tri_center) else: raise ValueError( - "Did not recognize variability option: {}".format(option) + 'Did not recognize variability option: {}'.format(option) ) # get a uniform random scalar array 0 to 1 with data shape - rand_arr = np.random.rand(csr.shape[0], csr.shape[1]) + rand_arr = RANDOM_GENERATOR.uniform(size=(csr.shape[0], csr.shape[1])) # Center the random array at 1 +/- var_frac_arr (with csr scaling) variability_scalar = 1 + var_frac_arr * (rand_arr * 2 - 1) @@ -446,7 +449,7 @@ def uniform_variability( def normal_variability( - csr, cloud_type, var_frac, option="tri", tri_center=0.9 + csr, cloud_type, var_frac, option='tri', tri_center=0.9 ): """Get an array with a normal distribution of variability scalars centered at 1 that can be multiplied by a irradiance array with the same shape as @@ -475,17 +478,17 @@ def normal_variability( centered at 1 with range (1 - var_frac) to (1 + var_frac). This array can be multiplied by an irradiance array with the same shape as csr """ - if option == "linear": + if option == 'linear': var_frac_arr = linear_variability(csr, var_frac) - elif option == "tri": + elif option == 'tri': var_frac_arr = tri_variability(csr, var_frac, tri_center=tri_center) else: raise ValueError( - "Did not recognize variability option: {}".format(option) + 'Did not recognize variability option: {}'.format(option) ) # get a normal distribution of data centered at 0 with stdev 1 - rand_arr = np.random.normal(loc=0.0, scale=1.0, size=csr.shape) + rand_arr = RANDOM_GENERATOR.normal(loc=0.0, scale=1.0, size=csr.shape) # Center the random array at 1 +/- var_frac_arr (with csr scaling) variability_scalar = 1 + var_frac_arr * rand_arr diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7b6c371 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,170 @@ +[build-system] +requires = [ + "setuptools >= 61.0", + "setuptools_scm[toml] >= 8", +] +build-backend = "setuptools.build_meta" + +[project] +name = "NREL-farms" +dynamic = ["version"] +description = "The Fast All-sky Radiation Model for Solar applications (FARMS)" +keywords = ["farms", "NREL"] +readme = "README.rst" +authors = [ + {name = "Brandon Benton", email = "brandon.benton@nrel.gov"}, + {name = "Grant Buster", email = "grant.buster@nrel.gov"} +] +license = {text = "BSD-3-Clause"} +requires-python = ">= 3.9" +classifiers=[ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: BSD License", + "Natural Language :: English", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +dependencies = [ + "numpy>=1.17", + "pandas>=0.25" +] + +[project.optional-dependencies] +dev = [ + "build>=0.5", + "pre-commit", + "ruff>=0.5.0" +] +doc = [ + "sphinx>=7.0", + "sphinx_rtd_theme>=2.0", + "sphinx-click>=4.0", +] +test = [ + "pytest>=5.2", +] + +[project.urls] +homepage = "https://github.com/NREL/farms" +documentation = "https://nrel.github.io/farms/" +repository = "https://github.com/NREL/farms" + +[tool.ruff] +line-length = 79 +indent-width = 4 + +target-version = "py39" + +[tool.ruff.lint] +fixable = [] +# preview = true +# logger-objects = [] +task-tags = ["TODO", "FIXME", "XXX"] +select = [ + "A", # flake8-builtins + "ARG", # flake8-unused-arguments + "C", + "C4", # flake8-comprehensions + "C90", # mccabe + "COM", # flake8-commas + "D", # pydocstyle + "E", # pycodestyle + "F", # Pyflakes + "G", # flake8-logging-format + "I", # isort + "LOG", # flake8-logging + "N", # pep8-naming + "NPY", # numpy-specific + "PERF", # Perflint + "PL", # Pylint + "Q", # flake8-quotes + "SIM", # flake8-simplify + "UP", # pyupgrade + "W", # Warning +] + +ignore = [ + # Currently don't conform but we might want to reconsider + "A001", # builtin-variable-shadowing + "A002", # builtin-argument-shadowing + # Currently don't conform but we might want to reconsider + "ARG005", # unused-lambda-argument + "C408", # unnecessary-collection-call + "C414", # unnecessary-double-cast-or-process + "COM812", # missing-trailing-comma + "D105", # undocumented-magic-method + "D200", # fits-on-one-line + "D202", # no-blank-line-after-function + "D204", # one-blank-line-after-class + "D205", # blank-line-after-summary + "D207", # under-indentation + "D209", # new-line-after-last-paragraph + "D400", # ends-in-period + "D401", # non-imperative-mood + "D404", # docstring-starts-with-this + "E402", # import not at top of file + "FIX001", # line-contains-fixme + "G004", # f-string logging + "G001", # str.format logging + "N802", # invalid-function-name + "N803", # invalid-argument-name + "N806", # non-lowercase-variable-in-function + "N811", # constant imported as non constant + "N817", # imported as acronym + "PERF102", # incorrect-dict-iterator + "PERF203", # try-except-in-loop + "PERF401", # manual-list-comprehension + "PLC0415", # import not at top of file + "PLR0904", # too-many-public-methods + "PLR0912", # too-many-branches + "PLR0913", # too-many-arguments + "PLR0914", # too-many-locals + "PLR0915", # too-many-statements + "PLR1702", # too-many-nested-blocks + "PLR1704", # redefined-argument-from-local + "PLR2004", # magic-value-comparison + "PLW1514", # unspecified-encoding + "PLW2901", # redefined-loop-name + "Q000", # bad-quotes-inline-string + "Q004", # unnecessary-escaped-quote + "SIM108", # if-else-block-instead-of-if-exp + "SIM117", #multiple-with-statements + "SIM118", # in-dict-keys + "SIM211", # if-expr-with-false-true + "UP009", # utf8-encoding-declaration + "UP015", # redundant-open-modes + "UP032", # f-string +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] # unused imports + +[tool.ruff.lint.pylint] +max-args = 5 # (PLR0913) Maximum number of arguments for function / method +max-bool-expr = 5 # ( PLR0916) Boolean in a single if statement +max-branches=12 # (PLR0912) branches allowed for a function or method body +max-locals=15 # (PLR0912) local variables allowed for a function or method body +max-nested-blocks = 5 # (PLR1702) nested blocks within a function or method body +max-public-methods=20 # (R0904) public methods allowed for a class +max-returns=6 # (PLR0911) return statements for a function or method body +max-statements=50 # (PLR0915) statements allowed for a function or method body + +[tool.ruff.format] +quote-style = "single" +indent-style = "space" +# Consider adopting "lf" instead +line-ending = "auto" + +[tool.ruff.lint.pydocstyle] +convention = "numpy" + +[tool.setuptools] +include-package-data = true +packages = ["farms"] + +[tool.setuptools_scm] +version_file = "farms/_version.py" diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index d09663c..62430da --- a/setup.py +++ b/setup.py @@ -2,13 +2,11 @@ setup.py """ -import os import shlex -from codecs import open from subprocess import check_call from warnings import warn -from setuptools import find_packages, setup +from setuptools import setup from setuptools.command.develop import develop @@ -22,61 +20,17 @@ def run(self): Run method that tries to install pre-commit hooks """ try: - check_call(shlex.split("pre-commit install")) + check_call(shlex.split('pre-commit install')) except Exception as e: warn("Unable to run 'pre-commit install': {}".format(e)) develop.run(self) -here = os.path.abspath(os.path.dirname(__file__)) - -with open(os.path.join(here, "README.rst"), encoding="utf-8") as f: - readme = f.read() - -with open("requirements.txt") as f: - install_requires = f.readlines() - -with open(os.path.join(here, "farms", "version.py"), encoding="utf-8") as f: - version = f.read() - -version = version.split("=")[-1].strip().strip('"').strip("'") - -test_requires = ["pytest>=5.2"] -description = "The Fast All-sky Radiation Model for Solar applications (FARMS)" - setup( - name="NREL-farms", - version=version, - description=description, - long_description=readme, - author="Grant Buster", - author_email="grant.buster@nrel.gov", - url="https://github.com/NREL/farms", - packages=find_packages(), - package_dir={"farms": "farms"}, package_data={ - "farms": ["earth_periodic_terms.csv", "sun_earth_radius_vector.csv"] - }, - include_package_data=True, - license="BSD 3-Clause", - zip_safe=False, - keywords="farms", - python_requires=">=3.8", - classifiers=[ - "Development Status :: 4 - Beta", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: BSD License", - "Natural Language :: English", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - ], - test_suite="tests", - install_requires=install_requires, - extras_require={ - "test": test_requires, - "dev": test_requires + ["flake8", "pre-commit", "pylint"], + 'farms': ['earth_periodic_terms.csv', 'sun_earth_radius_vector.csv'] }, - cmdclass={"develop": PostDevelopCommand}, + test_suite='tests', + cmdclass={'develop': PostDevelopCommand}, )