Skip to content

Commit cd0452b

Browse files
authored
Merge pull request #81 from swryan/numpyscipy
Ensure NumPy/Scipy are installed from conda-forge if using conda
2 parents 181b4d8 + 8a242c9 commit cd0452b

File tree

2 files changed

+131
-46
lines changed

2 files changed

+131
-46
lines changed

.github/workflows/test_workflow.yml

Lines changed: 58 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ on:
2727
- 'Ubuntu Default'
2828
- 'Ubuntu Default, no MPI'
2929
- 'Ubuntu Default, Numpy 2.x'
30-
- 'MacOS Default (Intel)'
31-
- 'MacOS Default (ARM), NumPy 2.x'
30+
- 'MacOS Default, NumPy 2.x'
3231
- 'Ubuntu Latest'
32+
- 'Ubuntu Default, no SciPy'
33+
- 'MacOS Default, SciPy from PyPI'
3334
- 'Ubuntu Oldest'
3435
- 'Ubuntu Default, no MPI, forced build'
35-
- 'MacOS Default, no MPI, forced build'
3636
- 'Ubuntu Latest, no MPI, forced build'
3737
required: false
3838
default: ''
@@ -86,19 +86,8 @@ jobs:
8686
# PAROPT: true
8787
SNOPT: 7.7
8888

89-
# test default pyoptsparse on MacOS Legacy (Intel), NumPy 1.x
90-
- NAME: MacOS Default (Intel)
91-
OS: macos-13
92-
PY: '3.11'
93-
NUMPY: '1.26'
94-
SCIPY: '1.13'
95-
MPI: true
96-
PYOPTSPARSE: 'default'
97-
# PAROPT: true
98-
SNOPT: 7.7
99-
10089
# test default pyoptsparse on MacOS latest (ARM), NumPy 2.x
101-
- NAME: MacOS Default (ARM), NumPy 2.x
90+
- NAME: MacOS Default, NumPy 2.x
10291
OS: macos-latest
10392
PY: '3.12'
10493
NUMPY: '1.26'
@@ -112,12 +101,34 @@ jobs:
112101
OS: ubuntu-latest
113102
PY: 3.13
114103
NUMPY: 2.2
115-
SCIPY: 1.15
104+
SCIPY: 1.16
116105
MPI: true
117106
PYOPTSPARSE: 'latest'
118107
# PAROPT: true
119108
SNOPT: 7.7
120109

110+
# test default release of pyoptsparse, NumPy 2.x, no scipy
111+
- NAME: Ubuntu Default, no SciPy
112+
OS: ubuntu-latest
113+
PY: 3.13
114+
NUMPY: 2.2
115+
MPI: true
116+
PYOPTSPARSE: 'default'
117+
# PAROPT: true
118+
SNOPT: 7.7
119+
120+
# test default release of pyoptsparse, NumPy 2.x, scipy from pypi
121+
- NAME: MacOS Default, SciPy from PyPI
122+
OS: macos-latest
123+
PY: '3.12'
124+
NUMPY: '1.26'
125+
SCIPY: '1.13'
126+
SCIPY_FROM_PYPI: true
127+
MPI: true
128+
PYOPTSPARSE: 'default'
129+
# PAROPT: true
130+
SNOPT: 7.7
131+
121132
# test oldest supported version of pyoptsparse
122133
- NAME: Ubuntu Oldest
123134
OS: ubuntu-latest
@@ -138,17 +149,6 @@ jobs:
138149
SNOPT: 7.7
139150
FORCE_BUILD: true
140151

141-
# test default pyoptsparse on MacOS without MPI with forced build
142-
- NAME: MacOS Default, no MPI, forced build
143-
OS: macos-13
144-
PY: '3.11'
145-
NUMPY: '1.26'
146-
SCIPY: '1.13'
147-
PYOPTSPARSE: 'default'
148-
SNOPT: 7.7
149-
FORCE_BUILD: true
150-
XCODE: '14.2'
151-
152152
# test latest pyoptsparse without MPI with forced build
153153
- NAME: Ubuntu Latest, no MPI, forced build
154154
OS: ubuntu-latest
@@ -207,7 +207,33 @@ jobs:
207207

208208
- name: Install
209209
run: |
210-
conda install numpy=${{ matrix.NUMPY }} scipy=${{ matrix.SCIPY }} -q -y
210+
if [[ "${{ matrix.NUMPY}}" ]]; then
211+
if [[ "${{ matrix.SCIPY_FROM_PYPI }}" ]]; then
212+
echo "============================================================="
213+
echo "Install NumPy from PyPI"
214+
echo "============================================================="
215+
python -m pip install numpy==${{ matrix.NUMPY }}
216+
else
217+
echo "============================================================="
218+
echo "Install NumPy from conda-forge"
219+
echo "============================================================="
220+
conda install numpy=${{ matrix.NUMPY }} -c conda-forge -q -y
221+
fi
222+
fi
223+
224+
if [[ "${{ matrix.SCIPY}}" ]]; then
225+
if [[ "${{ matrix.SCIPY_FROM_PYPI }}" ]]; then
226+
echo "============================================================="
227+
echo "Install SciPy from PyPI"
228+
echo "============================================================="
229+
python -m pip install scipy==${{ matrix.SCIPY }}
230+
else
231+
echo "============================================================="
232+
echo "Install SciPy from conda-forge"
233+
echo "============================================================="
234+
conda install scipy=${{ matrix.SCIPY }} -c conda-forge -q -y
235+
fi
236+
fi
211237
212238
conda install cython swig compilers cmake meson liblapack openblas -q -y
213239
@@ -246,8 +272,10 @@ jobs:
246272
python -c "import numpy; assert str(numpy.__version__).startswith(str(${{ matrix.NUMPY }})), \
247273
f'Numpy version {numpy.__version__} is not the requested version (${{ matrix.NUMPY }})'"
248274
249-
python -c "import scipy; assert str(scipy.__version__).startswith(str(${{ matrix.SCIPY }})), \
250-
f'Scipy version {scipy.__version__} is not the requested version (${{ matrix.SCIPY }})'"
275+
if [[ "${{ matrix.SCIPY}}" ]]; then
276+
python -c "import scipy; assert str(scipy.__version__).startswith(str(${{ matrix.SCIPY }})), \
277+
f'Scipy version {scipy.__version__} is not the requested version (${{ matrix.SCIPY }})'"
278+
fi
251279
252280
- name: Build pyOptSparse
253281
run: |

build_pyoptsparse.py

Lines changed: 73 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import tarfile
1010
from pathlib import Path, PurePath
1111
import tempfile
12-
from colors import *
12+
from colors import yellow, red, green, cyan, color
1313
from shutil import which
1414
from packaging.version import parse
1515
import numpy
@@ -369,7 +369,8 @@ def subst_env_for_path(path:str)->str:
369369
The possibly updated path.
370370
"""
371371

372-
if opts['verbose'] is True: return path
372+
if opts['verbose'] is True:
373+
return path
373374

374375
for testvar in ['TMPDIR', 'TMP_DIR', 'TEMP_DIR', 'CONDA_PREFIX', 'VIRTUAL_ENV']:
375376
if testvar in os.environ and re.match(os.environ[testvar], path) is not None:
@@ -500,7 +501,7 @@ def pip_install(pip_install_args, pkg_desc='packages'):
500501
run_cmd(cmd_list)
501502
note_ok()
502503

503-
def install_conda_pkg(pkg_name:str):
504+
def install_conda_pkg(pkg_name:str, version:str=None):
504505
"""
505506
Shorthand for performing a 'conda install' operation for a single package.
506507
@@ -510,6 +511,8 @@ def install_conda_pkg(pkg_name:str):
510511
The name of the package to install.
511512
"""
512513
note(f'Installing {pkg_name.upper()} with conda')
514+
if version is not None:
515+
pkg_name += f'={version}'
513516
install_args = ['install', '-q', '-y', pkg_name]
514517
run_conda_cmd(cmd_args=install_args)
515518
note_ok()
@@ -707,7 +710,7 @@ def install_mumps_from_src():
707710
if not allow_build('mumps'):
708711
return
709712

710-
build_dir = git_clone('mumps')
713+
git_clone('mumps')
711714
run_cmd(['./get.Mumps'])
712715

713716
cnf_cmd_list = get_common_solver_config_cmd()
@@ -723,7 +726,7 @@ def install_paropt_from_src():
723726
"""
724727
Git clone the PAROPT repo, build the library, and install it and the include files.
725728
"""
726-
build_dir = git_clone('paropt')
729+
git_clone('paropt')
727730

728731
# Use build defaults as per ParOpt instructions:
729732
Path('Makefile.in.info').rename('Makefile.in')
@@ -763,13 +766,16 @@ def install_ipopt_from_src(config_opts:list=None):
763766
if not allow_build('ipopt') or opts['include_ipopt'] is False:
764767
return
765768

766-
build_dir = git_clone('ipopt')
769+
git_clone('ipopt')
767770
cnf_cmd_list = ['./configure', f'--prefix={opts["prefix"]}', '--disable-java']
768771

769772
# Don't accidentally use PARDISO if it wasn't selected:
770-
if opts['linear_solver'] != 'pardiso': cnf_cmd_list.append('--disable-pardisomkl')
773+
if opts['linear_solver'] != 'pardiso':
774+
cnf_cmd_list.append('--disable-pardisomkl')
775+
776+
if config_opts is not None:
777+
cnf_cmd_list.extend(config_opts)
771778

772-
if config_opts is not None: cnf_cmd_list.extend(config_opts)
773779
note("Running configure")
774780
run_cmd(cmd_list=cnf_cmd_list)
775781
note_ok()
@@ -841,7 +847,7 @@ def install_hsl_from_src():
841847
if not allow_build('hsl'):
842848
return
843849

844-
build_dir = git_clone('hsl')
850+
git_clone('hsl')
845851

846852
# Extract the HSL tar file and rename the folder to 'coinhsl'
847853
# First, determine the name of the top-level folder:
@@ -982,7 +988,7 @@ def uninstall_built_item(build_key:str):
982988

983989
try:
984990
inc_dir.rmdir()
985-
except:
991+
except Exception:
986992
pass
987993

988994
note_ok()
@@ -1021,10 +1027,13 @@ def remove_conda_scripts():
10211027
if conda_is_active() and opts['ignore_conda'] is False:
10221028
note("Removing conda activate/deactivate scripts")
10231029
act_path = Path(sys_info['conda_activate_dir']) / sys_info['conda_env_script']
1024-
if act_path.is_file(): act_path.unlink()
1030+
if act_path.is_file():
1031+
act_path.unlink()
10251032

10261033
deact_path = Path(sys_info['conda_deactivate_dir']) / sys_info['conda_env_script']
1027-
if deact_path.is_file(): deact_path.unlink()
1034+
if deact_path.is_file():
1035+
deact_path.unlink()
1036+
10281037
note_ok()
10291038

10301039
def uninstall_built():
@@ -1034,7 +1043,8 @@ def uninstall_built():
10341043
for build_key in ['ipopt', 'hsl', 'mumps', 'metis']:
10351044
uninstall_built_item(build_key)
10361045

1037-
if opts['ignore_conda'] is False: remove_conda_scripts()
1046+
if opts['ignore_conda'] is False:
1047+
remove_conda_scripts()
10381048

10391049
def uninstall_conda_pkgs():
10401050
""" Attempt to remove packages previously installed by conda. """
@@ -1105,7 +1115,7 @@ def check_compiler_sanity():
11051115
note_ok()
11061116

11071117
if opts['include_paropt']:
1108-
note(f'Testing mpicxx')
1118+
note('Testing mpicxx')
11091119
run_cmd(cmd_list=['mpicxx', '-o', 'hello_cxx_mpi', 'hello.cc'])
11101120
run_cmd(cmd_list=['./hello_cxx_mpi'])
11111121
note_ok()
@@ -1232,7 +1242,8 @@ def finish_setup():
12321242

12331243
# Set an option with the parsed pyOptSparse version
12341244
pos_ver_str = build_info['pyoptsparse']['branch']
1235-
if pos_ver_str[:1] == 'v': pos_ver_str = pos_ver_str[1:] # Drop the initial v
1245+
if pos_ver_str[:1] == 'v':
1246+
pos_ver_str = pos_ver_str[1:] # Drop the initial v
12361247
opts['pyoptsparse_version'] = parse(pos_ver_str)
12371248

12381249
# Change snopt_dir to an absolute path
@@ -1325,22 +1336,68 @@ def post_build_success():
13251336
announce('SUCCESS!')
13261337
exit(0)
13271338

1339+
1340+
def get_package_info(pkgname:str) -> dict:
1341+
info = {
1342+
'installed': False,
1343+
'version': None,
1344+
'origin': None # 'conda-forge', 'pypi', or 'unknown'
1345+
}
1346+
try:
1347+
result = subprocess.run(['conda', 'list', pkgname], capture_output=True, text=True, check=True)
1348+
lines = result.stdout.splitlines()
1349+
for line in lines:
1350+
if line.startswith(f'{pkgname} '):
1351+
parts = line.split()
1352+
info['installed'] = True
1353+
info['version'] = parts[1]
1354+
info['origin'] = parts[-1] # channel name, e.g. 'conda-forge'
1355+
break
1356+
except Exception:
1357+
pass
1358+
return info
1359+
1360+
13281361
def perform_install():
13291362
""" Initiate all the required actions in the script. """
1363+
13301364
process_command_line()
13311365
initialize()
13321366

13331367
if opts['uninstall']:
13341368
announce('Uninstalling pyOptSparse and related packages')
13351369
print(f'{yellow("NOTE:")} Some items may be listed even if not installed.')
1336-
if opts['ignore_conda'] is False: uninstall_conda_pkgs()
1370+
if opts['ignore_conda'] is False:
1371+
uninstall_conda_pkgs()
13371372
uninstall_built()
13381373
exit(0)
13391374

13401375
finish_setup()
13411376

13421377
announce('Beginning installation')
13431378

1379+
# if using conda, we want numpy and scipy to be installed from conda-forge
1380+
if allow_install_with_conda():
1381+
numpy_info = get_package_info('numpy')
1382+
if numpy_info['installed'] is False or numpy_info['origin'] != 'conda-forge':
1383+
numpy_version = numpy_info['version']
1384+
if numpy_version is not None:
1385+
print(f"{yellow('NOTE:')} NumPy {numpy_version} is not installed from conda-forge, reinstalling it now.")
1386+
install_conda_pkg('numpy', version=numpy_version)
1387+
else:
1388+
print(f"{yellow('NOTE:')} NumPy is not installed from conda-forge, installing it now.")
1389+
install_conda_pkg('numpy')
1390+
1391+
scipy_info = get_package_info('scipy')
1392+
if scipy_info['installed'] is False or scipy_info['origin'] != 'conda-forge':
1393+
scipy_version = scipy_info['version']
1394+
if scipy_version is not None:
1395+
print(f"{yellow('NOTE:')} Scipy {scipy_version} is not installed from conda-forge, reinstalling it now.")
1396+
install_conda_pkg('scipy', version=scipy_version)
1397+
else:
1398+
print(f"{yellow('NOTE:')} Scipy is not installed from conda-forge, installing it now.")
1399+
install_conda_pkg('scipy')
1400+
13441401
if opts['linear_solver'] == 'mumps':
13451402
install_with_mumps()
13461403
install_pyoptsparse_from_src()

0 commit comments

Comments
 (0)