diff --git a/.travis.yml b/.travis.yml index ba29cf6..f9c4448 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,16 +11,16 @@ language: generic matrix: include: - os: linux - env: PYTHON="3.5" + env: PYTHON="3.7" - os: linux - env: PYTHON="3.6" + env: PYTHON="3.9" - os: osx - env: PYTHON="3.5" + env: PYTHON="3.7" - os: osx - env: PYTHON="3.6" + env: PYTHON="3.9" # Install packages install: @@ -43,7 +43,7 @@ install: conda env create -qf test-environment.yaml; conda activate py${PYTHON}; else - if [[ "$PYTHON" == "3.6" && "$TRAVIS_OS_NAME" == "linux" ]]; then + if [[ "$PYTHON" == "3.9" && "$TRAVIS_OS_NAME" == "linux" ]]; then conda env update -qn base -f build-environment.yaml; fi fi @@ -56,7 +56,7 @@ script: - set -e - if [[ -z "$TRAVIS_TAG" ]]; then pytest -vv --cov=./; - if [[ "$PYTHON" == "3.6" && "$TRAVIS_OS_NAME" == "linux" ]]; then + if [[ "$PYTHON" == "3.9" && "$TRAVIS_OS_NAME" == "linux" ]]; then git checkout -- test-environment.yaml; conda install -q sphinx doctr nbsphinx ipython; python setup.py install; @@ -66,7 +66,7 @@ script: doctr deploy devel; fi else - if [[ "$PYTHON" == "3.6" && "$TRAVIS_OS_NAME" == "linux" ]]; then + if [[ "$PYTHON" == "3.9" && "$TRAVIS_OS_NAME" == "linux" ]]; then conda build conda.recipe; anaconda -t $ANACONDA_TOKEN upload $HOME/miniconda/conda-bld/*/pyteck*.tar.bz2; python setup.py install; @@ -91,6 +91,6 @@ deploy: secure: "vaCCwVyZ8m+PDWub0kpPWYwwAUrtgnFQxU9qKc6VwIp3cCtYyEAZjTbAUzBfc/DsqmpCoLI62hYrQWqq0PwFfs4/1Ho+ppJuuiPvcIs0Syh8iv37YKnQG5Yjc9OjEGOY3ZScIUz57X67dst8HBiNHZnovwh5yxoGCTYrP0HdPlQbMkU6byzpXJ/+gzZaRiXXCPElEMf7HRuOZCkoDUIxypcBAvRHjyqI5TLecfkZBllkA3MFfEMT2mDurU4fmSsS7ndcKONaW9OKWSpSEItGle1Yq68qLN3xJIDSVfWg+vAUANuNAZJwo3CyfwPdxfWq4zth9+fY56ZJg3rz2QhC4kVJIOyNfyHyLdKXCQpCg2A0zcTMItnHafKm8eN2IPKNkO+HN1mpE6+G0UTnGfQtsA3FWQvedvA/WH5f226JJ1RNlJAsMDpf08WK7tTtc4rQ0Rm9TtKiblcZ+AxQMyZCdO3UxKCzqpusZmueIbr4koPFczaQYDsldWihboWp6ToAymI1XwcsSQTQTTcqGqt5ADuI9Ae0egWqpsAe72aiTYUR/FrIXD2ylSQl0E3UobjkYl+5kT4pJtF3TknJqN4VZDAbRP1IxPGcpNXLuZXekLpNaaDxJFepAnytWtIGd/W2vt+Afv5vt5VvGLFS56uuxOkLlIFebw0NfXnT1citLLM=" on: tags: true - condition: $TRAVIS_OS_NAME == "linux" && $PYTHON == "3.6" + condition: $TRAVIS_OS_NAME == "linux" && $PYTHON == "3.9" distributions: "sdist bdist_wheel" skip_upload_docs: true diff --git a/appveyor.yml b/appveyor.yml index 902ea7a..ab2b305 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,6 @@ version: 1.0.{build} environment: - PYTHON_LOC: "C:\\Miniconda36-x64" + PYTHON_LOC: "C:\\Miniconda3-x64" skip_tags: true @@ -10,20 +10,20 @@ install: - cmd: conda config --append channels conda-forge - cmd: conda update conda - cmd: conda update -q --all - - ps: (get-content test-environment.yaml) | %{$_ -replace "\$\{PYTHON\}","3.5"} | set-content test-environment.yaml + - ps: (get-content test-environment.yaml) | %{$_ -replace "\$\{PYTHON\}","3.7"} | set-content test-environment.yaml - cmd: conda env create -qf test-environment.yaml - cmd: git checkout -- test-environment.yaml - - ps: (get-content test-environment.yaml) | %{$_ -replace "\$\{PYTHON\}","3.6"} | set-content test-environment.yaml + - ps: (get-content test-environment.yaml) | %{$_ -replace "\$\{PYTHON\}","3.9"} | set-content test-environment.yaml - cmd: conda env create -qf test-environment.yaml - cmd: conda info -a - - ps: Write-Host "Packages in py3.5 Environment:" - - cmd: conda list -n py3.5 - - ps: Write-Host "Packages in py3.6 Environment:" - - cmd: conda list -n py3.6 + - ps: Write-Host "Packages in py3.7 Environment:" + - cmd: conda list -n py3.7 + - ps: Write-Host "Packages in py3.9 Environment:" + - cmd: conda list -n py3.9 build_script: - - cmd: call %PYTHON_LOC%\Scripts\activate.bat py3.5 + - cmd: call %PYTHON_LOC%\Scripts\activate.bat py3.7 - cmd: pytest -vv --cov=./ --cov-append - - cmd: call %PYTHON_LOC%\Scripts\activate.bat py3.6 + - cmd: call %PYTHON_LOC%\Scripts\activate.bat py3.9 - cmd: pytest -vv --cov=./ --cov-append - cmd: codecov -X gcov diff --git a/conda.recipe/meta.yaml b/conda.recipe/meta.yaml index ebd1c47..86bf923 100644 --- a/conda.recipe/meta.yaml +++ b/conda.recipe/meta.yaml @@ -14,12 +14,12 @@ build: requirements: build: - - python >=3.5 + - python >=3.7,<3.10 - setuptools - pip run: - - python + - python >=3.7,<3.10 - pyyaml >=3.12,<4.0 - numpy >=1.13.1 - scipy >=1.0.0 diff --git a/docs/conf.py b/docs/conf.py index 651774f..6dd6fcd 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,7 +51,7 @@ napoleon_numpy_docstring = True napoleon_google_docstring = False intersphinx_mapping = { - 'python': ('https://docs.python.org/3.6', None), + 'python': ('https://docs.python.org/3', None), 'numpy': ('http://docs.scipy.org/doc/numpy/', None), } diff --git a/pyteck/simulation.py b/pyteck/simulation.py index ec37087..ea8779c 100644 --- a/pyteck/simulation.py +++ b/pyteck/simulation.py @@ -96,16 +96,18 @@ def create_volume_history(mech, temp, pres, reactants, pres_rise, time_end): def get_ignition_delay(time, target, target_name, ignition_type): """Identify ignition delay based on time, target, and type of detection. """ + no_ignition_delay = np.array([0.0]) + if ignition_type == 'max': # Get indices of peaks peak_inds = detect_peaks(target, edge=None, mph=1.e-9*np.max(target)) - # Get index of largest peak (overall ignition delay) - max_ind = peak_inds[np.argmax(target[peak_inds])] - - #ign_delays = time[peak_inds[np.where((time[peak_inds[peak_inds <= max_ind]]) > 0.0)]] - - ign_delays = time[peak_inds[peak_inds <= max_ind]] + if peak_inds.size == 0: + return no_ignition_delay + else: + # Get index of largest peak (overall ignition delay) + max_ind = peak_inds[np.argmax(target[peak_inds])] + ign_delays = time[peak_inds[peak_inds <= max_ind]] elif ignition_type == 'd/dt max': target = first_derivative(time, target) @@ -113,34 +115,48 @@ def get_ignition_delay(time, target, target_name, ignition_type): # maximum value to avoid noise peaks. peak_inds = detect_peaks(target, edge=None, mph=1.e-9*np.max(target)) - # Get index of largest peak (overall ignition delay) - max_ind = peak_inds[np.argmax(target[peak_inds])] - - ign_delays = time[peak_inds[np.where((time[peak_inds[peak_inds <= max_ind]]) > 0.0)]] + if peak_inds.size == 0: + return no_ignition_delay + else: + # Get index of largest peak (overall ignition delay) + max_ind = peak_inds[np.argmax(target[peak_inds])] + ign_delays = time[peak_inds[np.where((time[peak_inds[peak_inds <= max_ind]]) > 0.0)]] elif ignition_type == '1/2 max': # maximum value, and associated index max_val = np.max(target) peak_inds = detect_peaks(target, edge=None, mph=1.e-9*np.max(target)) - max_ind = peak_inds[np.argmax(target[peak_inds])] - # TODO: interpolate for actual half-max value - # Find index associated with the 1/2 max value, but only consider - # points before the peak - half_idx = (np.abs(target[0:max_ind] - 0.5 * max_val)).argmin() - ign_delays = np.array([time[half_idx]]) + if peak_inds.size == 0: + return no_ignition_delay + else: + max_ind = peak_inds[np.argmax(target[peak_inds])] + # TODO: interpolate for actual half-max value + # Find index associated with the 1/2 max value, but only consider + # points before the peak + half_idx = (np.abs(target[0:max_ind] - 0.5 * max_val)).argmin() + ign_delays = np.array([time[half_idx]]) elif ignition_type == 'd/dt max extrapolated': # First need to evaluate derivative of the target target_derivative = first_derivative(time, target) + max_derivative = np.max(target_derivative) + + if not np.isfinite(max_derivative) or max_derivative <= 0.0: + return no_ignition_delay # Get indices of peaks, and index of largest peak, which corresponds to - # the point of maximum deriative - peak_inds = detect_peaks(target_derivative, edge=None, mph=1.e-9*np.max(target)) - max_ind = peak_inds[np.argmax(target_derivative[peak_inds])] + # the point of maximum derivative + peak_inds = detect_peaks(target_derivative, edge=None, mph=1.e-9*max_derivative) - # use slope to extrapolate to intercept with baseline value (0 by default) - ign_delays = np.array([time[max_ind] - (target[max_ind] / target_derivative[max_ind])]) + if peak_inds.size == 0: + return no_ignition_delay + else: + max_ind = peak_inds[np.argmax(target_derivative[peak_inds])] + if target_derivative[max_ind] <= 0.0: + return no_ignition_delay + # use slope to extrapolate to intercept with baseline value (0 by default) + ign_delays = np.array([time[max_ind] - (target[max_ind] / target_derivative[max_ind])]) # TODO: handle target with nonzero baseline? else: diff --git a/pyteck/tests/test_simulation.py b/pyteck/tests/test_simulation.py index 660bc29..6c04057 100644 --- a/pyteck/tests/test_simulation.py +++ b/pyteck/tests/test_simulation.py @@ -301,6 +301,53 @@ def test_derivative_max_extrapolated(self): rtol=1e-4 ) + def test_derivative_max_extrapolated_nonpositive_derivative_returns_zero(self, monkeypatch): + """Do not extrapolate when the derivative maximum is not positive. + """ + times = np.linspace(0, 1, 3) + target = np.ones_like(times) + derivative = np.array([-1.0, 0.0, -1.0]) + + monkeypatch.setattr( + simulation, 'first_derivative', + lambda time, target: derivative + ) + + ignition_delays = simulation.get_ignition_delay( + times, target, 'species', 'd/dt max extrapolated' + ) + + assert ignition_delays[0] == 0.0 + + @pytest.mark.parametrize('ignition_type', [ + 'max', + 'd/dt max', + '1/2 max', + 'd/dt max extrapolated', + ]) + def test_flat_signal_returns_zero_without_dumping_target_data(self, ignition_type): + """Flat signals are expected non-ignitions, not debug-dump cases. + """ + times = np.linspace(0, 1, 100) + target = np.zeros_like(times) + + cwd = os.getcwd() + with TemporaryDirectory() as temp_dir: + os.chdir(temp_dir) + try: + ignition_delays = simulation.get_ignition_delay( + times, target, 'species', ignition_type + ) + dumped_files = [ + filename for filename in os.listdir(temp_dir) + if filename.startswith('target-data-') and filename.endswith('.out') + ] + finally: + os.chdir(cwd) + + assert ignition_delays[0] == 0.0 + assert dumped_files == [] + def test_not_supported_type(self): """Test that a non-supported type raises a warning and returns zero. """ diff --git a/setup.py b/setup.py index ccd8c42..e9cc941 100644 --- a/setup.py +++ b/setup.py @@ -26,9 +26,9 @@ long_description = readme + '\n\n' + changelog + '\n\n' + citation install_requires = [ - 'pyyaml>=3.12,<4.0', - 'pint>=0.7.2,<0.9', - 'numpy>=1.13.0,<2.0', + 'pyyaml>=3.12', + 'pint>=0.7.2', + 'numpy>=1.13.0', 'tables', 'pyked>=0.4.1', 'scipy>=1.0.0', @@ -60,6 +60,7 @@ package_data={'pyteck': ['tests/*.xml', 'tests/*.yaml', 'tests/dataset_file.txt', 'tests/*.cti']}, install_requires=install_requires, zip_safe=False, + python_requires='>=3.7,<3.10', license='MIT License', @@ -76,8 +77,9 @@ 'License :: OSI Approved :: MIT License', 'Natural Language :: English', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Topic :: Scientific/Engineering :: Chemistry', ], keywords='chemical_kinetics',