Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 43 additions & 15 deletions .github/workflows/test_workflow_pixi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ on:
required: false
default: false

concurrency:
# Cancel any existing CI runs if we push to this branch
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

run-name: ${{ inputs.run_name }}

jobs:
Expand All @@ -36,11 +41,16 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest, macos-latest, windows-latest]
pixi-environment: ['default', 'py311', 'py312', 'py314']

runs-on: ${{ matrix.os }}

defaults:
run:
# Note this isn't passed to the composite action
shell: bash -l {0}

name: ${{ matrix.os }} / Python ${{ matrix.pixi-environment }}

env:
Expand All @@ -67,7 +77,7 @@ jobs:
run: |
mkdir -p ~/.ssh/
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
sudo chmod 600 ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
echo "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts

- name: Checkout code
Expand All @@ -84,7 +94,14 @@ jobs:
echo "============================================================="
echo "Install build_pyoptsparse in pixi environment"
echo "============================================================="
pixi run -e ${{ matrix.pixi-environment }} pip install -e .
pixi run --frozen -e ${{ matrix.pixi-environment }} pip install -e .

- uses: fortran-lang/setup-fortran@v1
id: setup-fortran
if: env.HAS_SNOPT_ACCESS == 'true' && runner.os == 'Windows'
with:
compiler: intel
version: 2025.2

- name: Display environment
run: |
Expand All @@ -94,13 +111,6 @@ jobs:
pixi info
pixi list -e ${{ matrix.pixi-environment }}

echo "============================================================="
echo "Check Python, NumPy versions"
echo "============================================================="
pixi run -e ${{ matrix.pixi-environment }} python -c "import sys; print(f'Python: {sys.version}')"
pixi run -e ${{ matrix.pixi-environment }} python -c "import numpy; print(f'NumPy: {numpy.__version__}')"
pixi run -e ${{ matrix.pixi-environment }} python -c "import pyoptsparse; print(f'pyoptsparse: {pyoptsparse.__version__}')"

- name: Copy SNOPT source files
if: env.HAS_SNOPT_ACCESS == 'true'
run: |
Expand All @@ -113,27 +123,45 @@ jobs:

- name: Build SNOPT module
if: env.HAS_SNOPT_ACCESS == 'true'
env:
FC: ${{ runner.os == 'Windows' && 'ifx' || 'gfortran' }}
CC: ${{ runner.os == 'Windows' && 'cl' || 'gcc' }}
run: |
echo "============================================================="
echo "Build SNOPT module with build_pyoptsparse"
echo "============================================================="
pixi run -e ${{ matrix.pixi-environment }} python -m build_pyoptsparse.snopt_module SNOPT/src
pixi run --frozen -e ${{ matrix.pixi-environment }} python -m build_pyoptsparse.snopt_module SNOPT/src

- name: Test SNOPT module
if: env.HAS_SNOPT_ACCESS == 'true'
- name: Test SNOPT module (Unix)
if: env.HAS_SNOPT_ACCESS == 'true' && runner.os != 'Windows'
run: |
echo "============================================================="
echo "Test that SNOPT module can be imported"
echo "============================================================="
pixi run -e ${{ matrix.pixi-environment }} python -c "from pyoptsparse import SNOPT; print('SNOPT loaded successfully!')"
pixi run --frozen -e ${{ matrix.pixi-environment }} python -c "from pyoptsparse import SNOPT; print('SNOPT loaded successfully!')"

echo "============================================================="
echo "Test SNOPT optimization"
echo "============================================================="
export SNOPT_SRC_PATH="SNOPT/src"
echo $PWD
ls -la
pixi run -e ${{ matrix.pixi-environment }} python -m unittest build_pyoptsparse/test/test_snopt_module.py
pixi run --frozen -e ${{ matrix.pixi-environment }} python -m unittest build_pyoptsparse/test/test_snopt_module.py

- name: Test SNOPT module (Windows)
if: env.HAS_SNOPT_ACCESS == 'true' && runner.os == 'Windows'
shell: cmd
run: |
echo =============================================================
echo Test that SNOPT module can be imported
echo =============================================================
echo Attempting to load snopt.pyd with detailed error info
pixi run --frozen -e ${{ matrix.pixi-environment }} python -c "from pyoptsparse import OPT; opt = OPT('SNOPT'); print('Success!')"

echo =============================================================
echo Test SNOPT optimization
echo =============================================================
pixi run --frozen -e ${{ matrix.pixi-environment }} python -m unittest build_pyoptsparse/test/test_snopt_module.py

# Enable tmate debugging of manually-triggered workflows if the input option was provided
- name: Setup tmate session
Expand Down
89 changes: 87 additions & 2 deletions build_pyoptsparse/snopt_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,15 +303,36 @@ def create_meson_build_file(build_dir: Path | str,
command: [py3, '-m', 'numpy.f2py', '@INPUT@', '--lower', '--build-dir', '.']
)

# Build extension module
fc = meson.get_compiler('fortran')
fc_id = fc.get_id()
host_system = host_machine.system()

# Set compiler-specific flags for fixed-form Fortran
if host_system == 'windows' and (fc_id == 'intel' or fc_id == 'intel-cl' or fc_id == 'intel-llvm-cl')
# Intel on Windows - use Windows-style flags
fortran_flags = ['/fixed', '/extend-source:80', '/names:lowercase', '/assume:underscore']
elif fc_id == 'intel' or fc_id == 'intel-cl'
# Intel on Linux/macOS - use Unix-style flags
fortran_flags = ['-fixed', '-extend-source', '80']
elif fc_id == 'gcc'
fortran_flags = ['-ffixed-form', '-ffixed-line-length-80']
else
# Default to gfortran-style flags
fortran_flags = ['-ffixed-form', '-ffixed-line-length-80']
endif

message('Fortran compiler ID: ' + fc_id)
message('Host system: ' + host_system)
message('Fortran flags: ' + ' '.join(fortran_flags))

py3.extension_module('snopt',
snopt_source,
fortranobject_c,
snopt_source_files,
include_directories: [inc_np, inc_f2py],
dependencies: py3_dep,
install: false,
fortran_args: '-ffixed-line-length-80'
fortran_args: fortran_flags
)

message('SNOPT module will be built')
Expand Down Expand Up @@ -396,6 +417,64 @@ def find_built_module(build_dir):

return None

def copy_intel_runtime_dlls(output_dir):
"""Copy Intel Fortran runtime DLLs to output directory on Windows."""
# Intel runtime DLLs needed by ifx-compiled code
required_dlls = [
'libifcoremd.dll',
'libmmd.dll',
'svml_dispmd.dll',
'libiompstubs5md.dll',
]

# Find Intel compiler bin directory from PATH or known locations
intel_bin_dir = None

# Check PATH first
path_dirs = os.environ.get('PATH', '').split(os.pathsep)
for path_dir in path_dirs:
if 'Intel' in path_dir and 'compiler' in path_dir and 'bin' in path_dir:
test_path = Path(path_dir)
if test_path.exists() and (test_path / 'libifcoremd.dll').exists():
intel_bin_dir = test_path
break

# Fallback to known locations
if not intel_bin_dir:
known_paths = [
Path(r"C:\Program Files (x86)\Intel\oneAPI\compiler\latest\bin"),
Path(r"C:\Program Files (x86)\Intel\oneAPI\compiler\2025.2\bin"),
Path(r"C:\Program Files (x86)\Intel\oneAPI\compiler\2024.2\bin"),
]
for path in known_paths:
if path.exists() and (path / 'libifcoremd.dll').exists():
intel_bin_dir = path
break

if not intel_bin_dir:
print("\nWarning: Could not find Intel runtime DLLs.")
print("The SNOPT module may not work unless Intel oneAPI is in the system PATH.")
return

print(f"\nCopying Intel runtime DLLs from {intel_bin_dir}")
copied = []
for dll in required_dlls:
src = intel_bin_dir / dll
if src.exists():
dest = output_dir / dll
try:
shutil.copy2(src, dest)
copied.append(dll)
print(f" Copied {dll}")
except Exception as e:
print(f" Warning: Could not copy {dll}: {e}")

if copied:
print(f"\nSuccessfully copied {len(copied)} Intel runtime DLLs")
print("SNOPT module should now work without requiring Intel oneAPI in PATH")
else:
print("\nWarning: No Intel runtime DLLs were copied")


def install_module(module_path, output_dir):
"""Copy the built module to the output directory."""
Expand All @@ -405,6 +484,12 @@ def install_module(module_path, output_dir):
dest_file = output_path / module_path.name
shutil.copy2(module_path, dest_file)

# On Windows with Intel compiler, copy runtime DLLs next to the .pyd
if platform.system() == 'Windows':
fc_compiler = os.environ.get('FC', '').lower()
if 'ifx' in fc_compiler or 'ifort' in fc_compiler:
copy_intel_runtime_dlls(output_path)

print(f"\nInstalled SNOPT module to: {dest_file}")
return dest_file

Expand Down
5 changes: 1 addition & 4 deletions build_pyoptsparse/test/test_snopt_module.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,14 @@ def sens(xdict, funcs):
# Objective
optProb.addObj("obj")

# Check optimization problem:
print(optProb)

# Optimizer
opt = OPT('SNOPT')

# Solution
sol = opt(optProb, sens=sens)

self.assertEqual(sol.optInform.value, 1)

assert_almost_equal(sol.fStar, 17.014, decimal=2)

if __name__ == '__main__':
unittest.main()
Loading
Loading