├── .gitignore ├── LICENSE.txt ├── MANIFEST.in ├── fmodpy ├── __init__.py ├── __main__.py ├── about │ ├── author.txt │ ├── classifiers.txt │ ├── description.txt │ ├── keywords.txt │ ├── on_pypi.txt │ ├── requirements.txt │ ├── version.txt │ └── version_history.md ├── config.py ├── development │ ├── strange_type_example.f90 │ ├── todo.py │ └── type.py ├── exceptions.py ├── fmodpy.py ├── parsing │ ├── __init__.py │ ├── _old_code.txt │ ├── argument.py │ ├── character.py │ ├── code.py │ ├── complex.py │ ├── file.py │ ├── function.py │ ├── implicit_none.py │ ├── integer.py │ ├── interface.py │ ├── logical.py │ ├── module.py │ ├── procedure.py │ ├── real.py │ ├── subroutine.py │ ├── type.py │ ├── use.py │ └── util.py └── test │ ├── .coverage │ ├── character │ ├── __init__.py │ └── test_character.f03 │ ├── complex128 │ ├── __init__.py │ └── test_complex128.f03 │ ├── complex256 │ ├── __init__.py │ ├── test_complex256.f03 │ └── test_complex256 │ │ ├── test_complex256_c_wrapper.f90 │ │ └── test_complex256_python_wrapper.py │ ├── complex64 │ ├── __init__.py │ └── test_complex64.f03 │ ├── double_precision │ ├── __init__.py │ └── test_double_precision.f03 │ ├── fixed_format │ ├── __init__.py │ └── test_fixed_format.f │ ├── int32 │ ├── __init__.py │ └── test_int32.f03 │ ├── int64 │ ├── __init__.py │ └── test_int64.f03 │ ├── logical │ ├── __init__.py │ └── test_logical.f03 │ ├── misc │ ├── __init__.py │ ├── implicit_shape.f90 │ ├── simple.f03 │ └── subroutine_with_type.f90 │ ├── module │ ├── __init__.py │ ├── extra_module.f03 │ └── test_module.f03 │ ├── procedure │ ├── __init__.py │ ├── og_procedure │ │ ├── procedure.f03 │ │ ├── procedure.pyx │ │ └── procedure_c_to_f.f90 │ └── procedure.f03 │ ├── real32 │ ├── __init__.py │ └── test_real32.f03 │ ├── real64 │ ├── __init__.py │ └── test_real64.f03 │ ├── test.py │ └── type │ ├── __init__.py │ ├── derived_type_error.f03 │ ├── test_type.f03 │ └── test_type_aux.f03 ├── readme.md ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | __pycache__ 3 | *.pyc 4 | *.o 5 | *.so 6 | *.mod -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Thomas C.H. Lux 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include fmodpy * 2 | -------------------------------------------------------------------------------- /fmodpy/__init__.py: -------------------------------------------------------------------------------- 1 | # Set up some package-related things. 2 | import os 3 | DIRECTORY = os.path.dirname(os.path.abspath(__file__)) 4 | ABOUT_DIR = os.path.join(DIRECTORY, "about") 5 | with open(os.path.join(ABOUT_DIR,"version.txt")) as f: 6 | __version__ = f.read().strip() 7 | del f 8 | 9 | # Steal the documentation from .fmodpy, instead of duplicating here. 10 | from .fmodpy import __doc__ 11 | 12 | # Import the main configuration (needed to initialize the configuration). 13 | import fmodpy.config 14 | 15 | # Import the main features of this package. 16 | from .fmodpy import fimport, configure, load_symbol 17 | 18 | # Set "__all__" so that "from fmodpy import *" returns expected stuff. 19 | __all__ = ["fimport", "configure", "load_symbol"] 20 | 21 | -------------------------------------------------------------------------------- /fmodpy/__main__.py: -------------------------------------------------------------------------------- 1 | # Making this module accessible by being called directly from command line. 2 | import os, sys, traceback 3 | 4 | # Import all of the fmodpy module 5 | from .fmodpy import __doc__, fimport, configure 6 | 7 | if len(sys.argv) < 2: 8 | import fmodpy 9 | help(fmodpy) 10 | exit() 11 | else: 12 | file_path = os.path.abspath(sys.argv[1]) 13 | 14 | # Pretty error handling when this file is executed directly 15 | def custom_excepthook(exc_type, value, tb): 16 | l = ''.join(traceback.format_exception(exc_type, value, tb)) 17 | print(l) 18 | sys.excepthook = custom_excepthook 19 | 20 | # Read command line arguments after the path to the source file. 21 | command_line_args = {} 22 | for arg in sys.argv[2:]: 23 | if ("=" not in arg): 24 | from fmodpy.exceptions import UnrecognizedConfiguration 25 | raise(UnrecognizedConfiguration( 26 | f"Command line argument {str([arg])[1:-1]} must be formatted"+ 27 | " as '=', but no '=' found.")) 28 | first_equals = arg.index('=') 29 | setting, value = arg[:first_equals], arg[first_equals+1:] 30 | command_line_args[setting] = value 31 | 32 | # Call "fimport" providing the given arguments as configurations. 33 | fimport(file_path, **command_line_args) 34 | -------------------------------------------------------------------------------- /fmodpy/about/author.txt: -------------------------------------------------------------------------------- 1 | Thomas C.H. Lux 2 | thomas.ch.lux@gmail.com 3 | tchlux 4 | -------------------------------------------------------------------------------- /fmodpy/about/classifiers.txt: -------------------------------------------------------------------------------- 1 | % How mature is this project? Common values are 2 | % 3 - Alpha 3 | % 4 - Beta 4 | % 5 - Production/Stable 5 | Development Status :: 4 - Beta 6 | % Indicate who your project is intended for 7 | Intended Audience :: Developers 8 | Topic :: Software Development :: Build Tools 9 | % Pick your license as you wish (should match "license" above) 10 | License :: OSI Approved :: MIT License 11 | % Specify the Python versions you support here. In particular, ensure 12 | % that you indicate whether you support Python 2, Python 3 or both. 13 | Programming Language :: Python :: 3.6 14 | Programming Language :: Python :: 3.7 15 | Programming Language :: Python :: 3.8 16 | -------------------------------------------------------------------------------- /fmodpy/about/description.txt: -------------------------------------------------------------------------------- 1 | A lightweight, efficient, highly automated, fortran wrapper for python. 2 | -------------------------------------------------------------------------------- /fmodpy/about/keywords.txt: -------------------------------------------------------------------------------- 1 | python 2 | python3 3 | fortran 4 | wrapper 5 | -------------------------------------------------------------------------------- /fmodpy/about/on_pypi.txt: -------------------------------------------------------------------------------- 1 | True 2 | -------------------------------------------------------------------------------- /fmodpy/about/requirements.txt: -------------------------------------------------------------------------------- 1 | numpy>=1.13 2 | -------------------------------------------------------------------------------- /fmodpy/about/version.txt: -------------------------------------------------------------------------------- 1 | 1.7.5 2 | -------------------------------------------------------------------------------- /fmodpy/about/version_history.md: -------------------------------------------------------------------------------- 1 | | 1.1.0
December 2020 | Supports MODULE wrapping and basic types. | 2 | | 1.2.0
February 2021 | Fixing array size and dimension checks for optional
array arguments. Working towards support of types. | 3 | | 1.2.1
February 2021 | Fixing install issue with setup.py | 4 | | 1.2.2
May 2021 | Added support for complex types. | 5 | | 1.3.0
May 2021 | Removed OG fmodpy, added complex256 tests work in
progress. | 6 | | 1.3.1
June 2021 | Fixing minor bug in argument parsing. | 7 | | 1.3.2
June 2021 | Adding ability to parse Fortran files with renamed
types. | 8 | | 1.3.3
September 2021 | Fixed numerous minor bugs and typos. Added basic
support for characters, as long as they have no
length. | 9 | | 1.3.4
October 2021 | Refactored parsing of SIZE(...) in assumed shape
array allocation during wrapping. Now supports
slightly more complicated arguments to the SIZE
function. | 10 | | 1.4.0
January 2022 | Updated documentation parsing to not clear comments
on new lines after subroutine definition. Fixed
character array tests. Added basic support for
Fortran derived types as long as they are BIND(C)
already. | 11 | | 1.4.1
February 2022 | Fixed usability bugs with derived types, added final
directory cleaning, fixed bug in module dependencies
that relied on .mod file, found bug in gfortran when
trying to support parameterized types. | 12 | | 1.4.2
February 2022 | Derived type struct fields in Python are lower case
to match python-wrapped behaviors in the rest of the
package. | 13 | | 1.4.3
March 2022 | Added warnings for long compilation, many optional
arguments, and included architecture in compiled
file names. | 14 | | 1.4.3
March 2022 | Logical byte size adjustment from 03-27 that
supports C-bool types with only one byte. | 15 | | 1.4.4
March 2022 | Logical byte size adjustment from 03-27 that
supports C-bool types with only one byte. | 16 | | 1.4.5
April 2022 | Updated size measurements for arrays to 64 bit
integers. | 17 | | 1.4.6
April 2022 | Name change of depends_files to dependencies. Missed
this upload last weekend. | 18 | | 1.4.7
April 2022 | Updated dependency handling, automatic compilation
now starts with dependencies in order, source code
modification times are checked for all dependencies. | 19 | | 1.4.8
May 2022 | Added support for types defined in subroutines in
standalone files. Updated test case to reflect new
addition. Added 'verbose_module' parameter for
setting the default verbosity of generated modules. | 20 | | 1.5.0
July 2022 | Added the ability to load symbols from system
libraries, improves automatic dependency resolution. | 21 | | 1.5.1
July 2022 | Minor modifications to fix some compilation and
import bugs when supporting generic search for
symbols. Symbol dependencies are now loaded within
the generated wrapper, to ensure correct operation
after the initial build. | 22 | | 1.5.2
July 2022 | Added some additional standard library search paths
and renamed 'delete_destination' as 'overwrite'.
Also changed default configurations that were
strings being split into proper Python lists. | 23 | | 1.5.3
March 2023 | Minor update. Changed some logic in fimport to more
correctly support different system configurations.
Added comments drescribing most configurable
settings. Added support for TARGET and POINTER
types. | 24 | | 1.5.4
March 2023 | Added support for '$' as a line continuation
character in the fifth column of fixed format files.
Switched 'LOGICAL' types to be 'C_BOOL' | 25 | | 1.5.5
March 2023 | Updated error message for unnamed END statements to
include configuration suggestion. Made fixed format
Fortran files set 'end_is_named=False' by default. | 26 | | 1.5.6
March 2023 | Making some 'os.remove' operations safer by checking
for path existence. Refactored 'f_compiler_args'
into two more parts, 'optimization_level' and
'shared_object_args', so that the typical '-fPIC
-shared' do not need to be included when adding
custom compilation arguments. | 27 | | 1.5.6
March 2023 | Making some 'os.remove' operations safer by checking
for path existence. Refactored 'f_compiler_args'
into two more parts, 'optimization_level' and
'shared_object_args', so that the typical '-fPIC
-shared' do not need to be included when adding
custom compilation arguments. | 28 | | 1.5.7
April 2023 | Delayed warning message about LOGICAL arrays until
those arguments actually have a python interface
generated. This is to prevent parsed-but-unused
subroutine arguments from creating noise. | 29 | | 1.6.0
June 2023 | Added support for Fortran strings. Updated the
generated codes to automatically check for source
code modifications when loading and recompile if the
source code was modified. | 30 | | 1.7.0
November 2023 | Added support for parsing PARAMETER lines in fixed
format Fortran. | 31 | | 1.7.1
January 2024 | Fixed bug in type of return argument for allocatable
arrays. Updated messaging about implicit typing to
be more useful. | 32 | | 1.7.2
March 2024 | Updating command execution to use a list of
arguments instead of string join and 'shell=True'.
Moved compile options to the end of the argument
list to ensure that libraries are included correctly
in compiled shared object. | 33 | | 1.7.3
March 2024 | A commensurate update to size check compilations
that was made in the previous update. Moving library
specification to the back of the list of arguments
for the compiler to ensure dependencies are included
correctly. | 34 | | 1.7.4
May 2024 | Add support for 'size(a,1,kind=int64)' style size
specifications of inputs. | 35 | -------------------------------------------------------------------------------- /fmodpy/config.py: -------------------------------------------------------------------------------- 1 | import os, sysconfig 2 | 3 | # Try to include the NumPy path, ignore if it doesn't exist. 4 | try: 5 | import numpy as NP 6 | numpy_path = NP.__path__ 7 | except: 8 | numpy_path = [] 9 | 10 | # Default configurable variables. 11 | omp = False # True if OpenMP libraries should be linked for the given program. 12 | blas = False # True if BLAS should be linked for the given program. 13 | lapack = False # True if LAPACK should be linked for the given program. 14 | verbose = False # True if fmodpy should show logs during wrapper construction. 15 | verbose_module = True # True if the generated wrapper module should print to stdout when it is compiling. 16 | autocompile = True # True if fmodpy should attempt to automatically compile Fortran source files while creating the wrapper. 17 | wrap = True # True if fmodpy should build the wrapper (Fortran file and Python file that constitutes the translation layer). 18 | rebuild = False # True if the source file should be parse and wrapper built regardless of file modification times. 19 | show_warnings = True # 20 | debug_line_numbers = False # True if fmodpy source line numbers should be prepended to printed logs. 21 | implicit_typing = False # True if types of undeclared arguments should be inferred. 22 | end_is_named = True # True if the END of code blocks includes the name of the block. 23 | overwrite = False # hard-delete destination directory if it already exists. 24 | log_file = os.devnull # The "file" object that logging statements should be directed. 25 | f_compiler = "gfortran" # The command used to execute the fortran program. 26 | f_compiler_args = [] # Any special arguments for compiling. 27 | optimization_level = "-O3" # The level of compiler optimization to use for the final module. 28 | shared_object_args = ["-fPIC", "-shared"] # All arguments required for a functioning shared object. 29 | link_omp = ["-fopenmp"] # The arguments for enabling OpenMP support at compile and link times. 30 | link_blas = ["-lblas"] # The argument(s) for enabling BLAS routines at link time. 31 | link_lapack = ["-lblas", "-llapack"] # The argument(s) for enabling LAPACK routines at link time. 32 | home_directory = os.path.expanduser("~") # The user home directory to search for a global configuration file. 33 | libraries = numpy_path + [ # Paths to be checked for dependent "symbols" / routine defintions. 34 | "/usr/lib", 35 | "/lib", 36 | "/usr/lib64", 37 | "/lib64", 38 | "/usr/lib32", 39 | "/lib32", 40 | "/opt/homebrew/Cellar/openblas", 41 | "/opt/homebrew/Cellar/libomp" 42 | ] 43 | library_recursion = 2 # Maximum recursion depth when searching libraries for symbol definitions. 44 | library_extensions = ["so", "dylib", "dll"] # Files to be checked as linkable shared libraries. 45 | symbol_command = 'nm -gU "{path}" 2> /dev/null || nm -gD "{path}" 2> /dev/null' # Commad used to enumerate available symbols in a shared object. 46 | config_file = ".fmodpy.py" # Name of the fmodpy configuration file to look for. 47 | wait_warning_sec = 5 # Number of seconds to wait before warning about automatic compilation. 48 | 49 | # -------------------------------------------------------------------- 50 | # Development globals, not intended to be changed by users. 51 | # 52 | # All of these variables should have expected types. 53 | BOOL_CONFIG_VARS = ["omp", "blas", "lapack", "verbose", "autocompile", 54 | "wrap", "rebuild", "show_warnings", 55 | "debug_line_numbers", "implicit_typing", 56 | "end_is_named", "overwrite"] 57 | LIST_CONFIG_VARS = ["f_compiler_args", "shared_object_args", "link_omp", "link_blas", 58 | "link_lapack", "libraries", "library_extensions"] 59 | # File related maniplation arguments 60 | PY_EXT = ".py" 61 | FORT_EXT = ".f90" 62 | PYTHON_WRAPPER_EXT = "_python_wrapper" 63 | FORT_WRAPPER_EXT = "_c_wrapper"+FORT_EXT 64 | GET_SIZE_PROG_FILE = "fmodpy_get_size"+FORT_EXT 65 | GET_SIZE_EXEC_FILE = "fmodpy_get_size" 66 | GET_SIZE_VARIABLE_PREFIX = "SIZE_OF_" 67 | # -------------------------------------------------------------------- 68 | 69 | # Automatically handle printing for status updates. 70 | # WARNING: Do not use this function for warnings. Use `warnings.warn`. 71 | # WARNING: Do not use this function for errors. Use `raise(...)`. 72 | # 73 | # Custom print function (allows for line numbers to be automatically 74 | # added to all print statements, easily controls verbosity level). 75 | def fmodpy_print(*args, **kwargs): 76 | # Skip all print statements if verbosity is off. 77 | global verbose 78 | if (not verbose): return 79 | # Set the log file. 80 | global log_file 81 | if (log_file == os.devnull): 82 | import sys 83 | log_file = sys.stdout 84 | # Add information about where the bit is printed from, if turned on. 85 | global debug_line_numbers 86 | if debug_line_numbers: 87 | import inspect 88 | # Add the calling file and line to the print statement 89 | calling_function = inspect.stack()[1] 90 | calling_file = calling_function.filename 91 | calling_file = os.path.basename(calling_file) 92 | calling_line = calling_function.lineno 93 | args += (f'({calling_file} line {calling_line})',) 94 | # For all print operations, force a "flush" output. 95 | kwargs["file"] = log_file 96 | kwargs["flush"] = True 97 | print(*args, **kwargs) 98 | 99 | 100 | # Execute a blocking command with a subprocess, on completion provide 101 | # the return code, stdout as string, and stderr as string. This should 102 | # work across all Python3.x as well as cross-platform. 103 | # INPUT: 104 | # command -- A list of strings or string (space separated) describing 105 | # a standard command as would be given to subprocess.Popen 106 | # 107 | # OUTPUT: 108 | # return_code -- Straight from the subprocess returncode 109 | # stdout -- A list of strings that are the lines of stdout 110 | # produced by the execution of 111 | # stderr -- A list of strings that are the lines of stderr 112 | # produced by the execution of 113 | def run(command, **popen_kwargs): 114 | import sys, subprocess 115 | # For Python 3.x and x < 6, the encoding is a string by default 116 | # For Python 3.6 and later the encoding can be given as an arguemnt 117 | if sys.version_info >= (3,6): 118 | popen_kwargs.update( dict(encoding="UTF-8") ) 119 | proc = subprocess.Popen(command, stdout=subprocess.PIPE, 120 | stderr=subprocess.PIPE, **popen_kwargs) 121 | stdout, stderr = proc.communicate() 122 | # Before python 3.6, the encoding had to be handled after collection 123 | if (sys.version_info < (3,6)): 124 | if (type(stdout) != str): stdout = str(stdout, encoding="UTF-8") 125 | if (type(stderr) != str): stderr = str(stderr, encoding="UTF-8") 126 | # Remove Windows specific characters and split by the new line. 127 | if stdout: stdout = stdout.replace("\r","").split("\n") 128 | else: stdout = "" 129 | if stderr: stderr = stderr.replace("\r","").split("\n") 130 | else: stderr = "" 131 | # Return the exit code, standard out, and standard error. 132 | return proc.returncode, stdout, stderr 133 | 134 | 135 | # Configure the current 'fmodpy' runtime. Start with default values in 136 | # this file, load any global defaults over those, and finally override 137 | # again with provided keyword arguments. 138 | def load_config(**kwargs): 139 | # Get the "globals" for this file, this is the configuration of fmodpy. 140 | fmodpy_config = globals() 141 | 142 | # Identify those elements of "fmodpy.config" that should not be 143 | # set or considered when printing out configuration. 144 | modules = {"os", "sysconfig"} 145 | func_type = type(lambda:None) 146 | functions = {k for k in fmodpy_config if 147 | type(fmodpy_config[k]) == func_type} 148 | # Set the default configuration as the current configuration. 149 | config = { k:fmodpy_config[k] for k in fmodpy_config 150 | if (k[0].isalpha() and k.islower()) and 151 | (k not in modules) and (k not in functions) } 152 | 153 | # Check to make sure that all variables given specify only those 154 | # that are allowed to be specified. 155 | for var in kwargs: 156 | if (var not in config): 157 | from fmodpy.exceptions import UnrecognizedConfiguration 158 | raise(UnrecognizedConfiguration(f"Configuration for '{var}' is given, but that variable does not exist.")) 159 | if (var[0].isupper() or (not var[0].isalpha())): 160 | from fmodpy.exceptions import IllegalConfiguration 161 | raise(IllegalConfiguration(f"The variable '{var}' is not allowed to be configured.")) 162 | 163 | # Update the 'config' dictionary with the provided keyword arguments. 164 | config.update( kwargs ) 165 | 166 | # Make sure the path names do not have spaces. 167 | if any(' ' in str(config[k]) for k in ('f_compiler',)): 168 | from fmodpy.exceptions import NotAllowedPath 169 | raise(NotAllowedPath("Spaces cannot be included in compiler or linker paths.")) 170 | 171 | # Convert list-type configurations into lists (from strings). 172 | for var in LIST_CONFIG_VARS: 173 | if (type(config[var]) is str): 174 | config[var] = config[var].split() 175 | elif (type(config[var]) is not list): 176 | from fmodpy.exceptions import IllegalConfiguration 177 | raise(IllegalConfiguration(f"The variable '{var}' is supposed to be a list or string, but is neither.")) 178 | 179 | # Make sure that boolean-type configurations are booleans (from strings). 180 | for var in BOOL_CONFIG_VARS: 181 | if (type(config[var]) is str): 182 | config[var] = (config[var].lower().strip() == 'true') 183 | elif (type(config[var]) is not bool): 184 | from fmodpy.exceptions import IllegalConfiguration 185 | raise(IllegalConfiguration(f"The variable '{var}' is supposed to be a bool or string, but is neither.")) 186 | 187 | # If 'lblas' is True, then add BLAS compilation and 188 | # link arguments to the list of arguments already. 189 | if config["blas"]: 190 | for l in config["link_blas"]: 191 | if (l not in config["f_compiler_args"]): 192 | config["f_compiler_args"] += [l] 193 | 194 | # If 'llapack' is True, then add LAPACK compilation and 195 | # link arguments to the list of arguments already. 196 | if config["lapack"]: 197 | for l in config["link_lapack"]: 198 | if (l not in config["f_compiler_args"]): 199 | config["f_compiler_args"] += [l] 200 | 201 | # If 'omp' is True, then add OpenMP compilation and link arguments 202 | # to the list of arguments already. 203 | if config["omp"]: 204 | for l in config["link_omp"]: 205 | if (l not in config["f_compiler_args"]): 206 | config["f_compiler_args"] += [l] 207 | 208 | # Make sure any 'f_compiler_args' are not redundant with other args. 209 | config["f_compiler_args"] = [ 210 | a for a in config["f_compiler_args"] 211 | if a not in [config["optimization_level"]] + config["shared_object_args"] 212 | ] 213 | 214 | # Set all of the configuration variables as module-wide globals. 215 | for var in config: fmodpy_config[var] = config[var] 216 | 217 | # Return the current configuration. 218 | return config 219 | 220 | 221 | # -------------------------------------------------------------------- 222 | # -------------------------------------------------------------------- 223 | 224 | 225 | # Read a configuration file into a Python dictionary. 226 | def read_config_file(path): 227 | with open(path) as f: 228 | # Get all lines that have one "=" split and stripped into nice strings. 229 | # Strip quote characters from edges of strings (when user assigned a string). 230 | lines = ((' '.join(var.strip().strip('\'"').split()) for var in l.strip().split('=')) 231 | for l in f.readlines() if (l.count('=') == 1)) 232 | return dict(lines) 233 | 234 | # Load local settings if they are available. 235 | if os.path.exists(os.path.join(home_directory,config_file)): 236 | global_config = read_config_file( 237 | os.path.join(home_directory,config_file)) 238 | else: global_config = {} 239 | # Make sure all provided settings are recognized (and allowable). 240 | for var in global_config: 241 | if (var.strip()[:1] == "#"): continue # Skip commented lines. 242 | if var not in globals(): 243 | from fmodpy.exceptions import UnrecognizedConfiguration 244 | raise(UnrecognizedConfiguration(f"Configuration for '{var}' is given in config file, but that variable does not exist.")) 245 | # Update the configuration of fmodpy. 246 | globals().update(global_config) 247 | # Delete the local variables (so they don't hang around). 248 | if (len(global_config) > 0): del var 249 | del read_config_file 250 | del global_config 251 | # Load in the global configuration file. 252 | load_config() 253 | -------------------------------------------------------------------------------- /fmodpy/development/strange_type_example.f90: -------------------------------------------------------------------------------- 1 | 2 | MODULE HOLDER 3 | IMPLICIT NONE 4 | 5 | TYPE INTERNAL 6 | REAL :: R 7 | INTEGER :: I, J 8 | END TYPE INTERNAL 9 | 10 | CONTAINS 11 | 12 | SUBROUTINE TEST(A, B) 13 | REAL, INTENT(OUT), OPTIONAL :: A 14 | REAL, INTENT(IN) :: B 15 | IF (PRESENT(A)) THEN 16 | A = TEST_INTERNAL() 17 | END IF 18 | CONTAINS 19 | FUNCTION TEST_INTERNAL() 20 | REAL :: TEST_INTERNAL 21 | TEST_INTERNAL = 2.0 * B 22 | END FUNCTION TEST_INTERNAL 23 | END SUBROUTINE TEST 24 | 25 | SUBROUTINE PROC_TEST(A) 26 | ABSTRACT INTERFACE 27 | FUNCTION PTINT(B) 28 | TYPE USEFUL 29 | REAL :: R, S 30 | END TYPE USEFUL 31 | TYPE(USEFUL), INTENT(IN) :: B 32 | REAL PTINT 33 | END FUNCTION PTINT 34 | END INTERFACE 35 | PROCEDURE(PTINT) :: A 36 | 37 | END SUBROUTINE PROC_TEST 38 | 39 | END MODULE HOLDER 40 | 41 | 42 | 43 | 44 | 45 | 46 | PROGRAM TRY_SOMETHING 47 | USE HOLDER 48 | IMPLICIT NONE 49 | REAL :: FIRST, SECOND 50 | TYPE(INTERNAL), DIMENSION(2,2) :: CUSTOM_TYPE 51 | 52 | CUSTOM_TYPE%I = 1 53 | CUSTOM_TYPE%J = 0 54 | CUSTOM_TYPE(:,:)%R = 1.0 55 | PRINT *, CUSTOM_TYPE(:,:) 56 | 57 | ! FIRST = 1.0 58 | ! SECOND = 2.0 59 | 60 | ! PRINT *, FIRST 61 | ! PRINT *, SECOND 62 | ! CALL TEST(A=FIRST, B=SECOND) 63 | ! PRINT *, FIRST 64 | ! PRINT *, SECOND 65 | 66 | END PROGRAM TRY_SOMETHING 67 | -------------------------------------------------------------------------------- /fmodpy/development/todo.py: -------------------------------------------------------------------------------- 1 | 2 | # Custom types: 3 | # - read in the "arguments" for the type, those are the sizes 4 | # - allow "LEN" arguments 5 | # - prohibit "KIND" arguments 6 | # - define a "struct generator" in python that creates a struct 7 | # of the same shape as in Fortran given the same arguments 8 | # - check for "issubclass" instead of exact type match for structs 9 | # 10 | 11 | 12 | 13 | # -------------------------------------------------------------------- 14 | # 15 | # DEVELOPMENT 16 | # PLAN 17 | # 18 | # - support custom types defined in Fortran 19 | # --- parse the custom type and determine its contents in terms of basic types 20 | # --- generate a matching C struct using ctypes to the custom type 21 | # --- identify proper allocation schema in numpy to make sure memory 22 | # maps correctly into Fortran 23 | # 24 | # - support procedures as arguments 25 | # --- create a registry of "fmodpy wrapped functions" with the memory 26 | # addresses of all functions wrapped by fmodpy 27 | # --- map the memory address to the actual function in the Fortran 28 | # wrapper 29 | # 30 | # --- construct C function in Python (with ctypes) that translates C 31 | # style data into Python compatible data, calls the Python 32 | # function that was passed in as an argument, then translates the 33 | # results back into C style data for return 34 | # --- construct a BIND(C) Fortran wrapper function that translates 35 | # arguments from Fortran style into C style and calls the C 36 | # (Python) function from Fortran, nested in a MODULE with PUBLIC 37 | # variables that assign the address of the C function 38 | # --- pass the memory address of the C function in Python to Fortran 39 | # wrapper, use that address to assign a local Fortran function 40 | # in the BIND(C) wrapper module that calls Python C function 41 | # 42 | # - extend test cases to achieve (near) 100% coverage of all lines 43 | # 44 | # 45 | # -------------------------------------------------------------------- 46 | # TODO LISTS 47 | # 48 | # FMODPY 49 | # - Create WARNING for when link files are "not found" in compiled 50 | # projects, asking if the links are still alive. 51 | # - Make __init__.py the source file and the _python_wrapper.py the 52 | # link file, so that projects put on GitHub will work better. 53 | # - Write a code that "recovers links" instituted by GitHub, insert 54 | # that into the generated fmodpy wrappers. 55 | # - Create an archive of the source files when the wrapper was originally 56 | # built, so that those can be recovered even if the links are lost. 57 | # - add option to continue wrapping on failures, and to simply skip 58 | # wrappers for the things that cannot be wrapped (when possible) 59 | # - assign implicit_typing to True and named_end to False for '.f' files 60 | # - assign correct shape (from shape[0] ...) for implicit array arguments 61 | # - check the memory contiguity of OPTIONAL input arrays 62 | # - LOGICAL array warning should only happen *after* non-arguments are deleted 63 | # - compile dependencies first when 'dependencies' are given and 'autocompile=True' 64 | # - remove automatic size option in wrapper for optional arguments with unknown shape 65 | # - handle automatic shapes that have a ":" inside of them 66 | # - check for existence of Fortran compiler on install, offer instruction 67 | # - support lists of compilation commands to run before making final object 68 | # - support list of files to include in final module compilation (like objects) 69 | # - support multiple file input, for specifying all files that are part 70 | # of a module or may contain important types or constants 71 | # - empty classes should not be added into wrappers, they should be skipped 72 | # (i.e., a class that only defines interfaces) 73 | # - automatic documentation of the types of inputs in Python, included 74 | # in addition to original Fortran documentation. 75 | # - replace names of builtins like "global" and "lambda" in the 76 | # python wrapper (when the original Fortran uses these names). 77 | # - IMPLICIT NONE should be added to interfaces for functions 78 | # - integer arguments that are used as sizes should be declared before 79 | # the array arguments that use them as sizes, regardless of their 80 | # order in the actual list of arguments 81 | # 82 | # TYPE 83 | # - find the definition of this Type, and all of its contents. 84 | # - the type could be declared in a module that is "USED" in 85 | # another file, will have to scan other files for modules 86 | # with type definitions? 87 | # in the same subroutine or function 88 | # in the same module 89 | # in a USED module, potentially in another file 90 | # - define a C struct in the appropriate context 91 | # - cycle all arugments in type, initializing them correctly 92 | # - store all the initialized values into a C struct 93 | # 94 | # 95 | # PROCEDURE 96 | # - The interface being used by a subroutine-as-an-argument could 97 | # be defined in another module. Need to check for that. 98 | # Need to disallow "EXTERNAL" subroutines-as-arguments. 99 | # - Need to write fort_to_py versions of interface functions in 100 | # fortran wrapper and in python wrapper 101 | # - Have a way to bypass Python when a fmodpy wrapped fortran routine 102 | # is given. Maybe define a get_address function for all source 103 | # functions in Fortran wrappers? 104 | # 105 | -------------------------------------------------------------------------------- /fmodpy/development/type.py: -------------------------------------------------------------------------------- 1 | # "util" should perform compilation of fortran codes in setup, saving requires admin privaleges. 2 | # Passed functions store python functions associated with random integer 3 | # identifier, that is used to look up correct function in cython. 4 | 5 | # An example of how to create containers of structures in Numpy 6 | # https://stackoverflow.com/questions/50822183/efficient-conversion-from-c-array-of-struct-to-numpy-array-with-ctypes 7 | # https://stackoverflow.com/questions/40063080/casting-an-array-of-c-structs-to-a-numpy-array 8 | 9 | from ctypes import Structure, c_char_p, c_int, c_double, POINTER 10 | import numpy as np 11 | 12 | def c_func(): pass 13 | 14 | class info(Structure): 15 | _fields_ = [ ("name", c_char_p), 16 | ("arr_len", c_int), 17 | ("real_data", POINTER(c_double)) ] 18 | 19 | c_func.restype = info 20 | ret_val = c_func() 21 | # data = np.ctypeslib.as_array(ret_val.contents.real_data 22 | -------------------------------------------------------------------------------- /fmodpy/exceptions.py: -------------------------------------------------------------------------------- 1 | # Custom errors that can be raised during the fmodpy process 2 | 3 | class BadKeywordArgument(Exception): pass 4 | class BuildAndLinkError(Exception): pass 5 | class CompileError(Exception): pass 6 | class FortranError(Exception): pass 7 | class IllegalConfiguration(Exception): pass 8 | class LinkError(Exception): pass 9 | class NameError(Exception): pass 10 | class NotAllowedPath(Exception): pass 11 | class NotSupportedError(Exception): pass 12 | class ParseError(Exception): pass 13 | class SizeError(Exception): pass 14 | class UnrecognizedConfiguration(Exception): pass 15 | -------------------------------------------------------------------------------- /fmodpy/parsing/__init__.py: -------------------------------------------------------------------------------- 1 | # Add spaces to syntax in lines (for easier splitting by white-space) 2 | FORT_TEXT_REPLACEMENTS = { 3 | "DOUBLE PRECISION": "REAL ( KIND ( 0.0D0 ) )", 4 | "END DO": "ENDDO", 5 | "END IF": "ENDIF", 6 | ",":" , ", 7 | "(":" ( ", 8 | ")":" ) ", 9 | ":":" : ", 10 | "+":" + ", 11 | "-":" - ", 12 | "*":" * ", 13 | "/":" / ", 14 | "=":" = ", 15 | } 16 | 17 | # Some spaces that were created above were misplaced, so this fixes them. 18 | FORT_TEXT_FIXES = { 19 | "= >":"=>", 20 | } 21 | 22 | # Keep all comment lines and all lines from a file that start with the 23 | # following expressions (the file is converted to all upper case). 24 | ACCEPTABLE_LINE_STARTS = {'ABSTRACT', 'CHARACTER', 'END', # 'EXTERNAL', 25 | 'INTEGER', 'LOGICAL', 'REAL', 'COMPLEX', 26 | 'IMPLICIT', 'INTERFACE', 'MODULE', 'FUNCTION', 27 | 'OPTIONAL', 'PRIVATE', 'PROCEDURE', 28 | 'PUBLIC', 'PURE', 'RECURSIVE', 29 | 'SUBROUTINE', 'TYPE', 'USE', 'PARAMETER'} 30 | LINE_STARTS_TO_REMOVE = {'PURE', 'RECURSIVE'} 31 | 32 | # Immediately exclude a file from automatic compilation if it has a 33 | # line starting with the following. 34 | IMMEDIATELY_EXCLUDE = {"PROGRAM"} 35 | 36 | please_report_to = f"\nIf this is syntactically correct Fortran, please report this\n"+\ 37 | f"as an issue with the relevant Fortran code at\n\n"+\ 38 | f" https://github.com/tchlux/fmodpy\n" 39 | 40 | 41 | from .util import after_dot, before_dot, class_name, \ 42 | legal_module_name, simplify_fortran_file, pop_group, \ 43 | wrap_long_lines 44 | 45 | # -------------------------------------------------------------------- 46 | 47 | # Define a function for parsing an Interface. Returns a list of instances 48 | def parse_interface(list_of_lines, comments, parent): 49 | from .interface import Interface 50 | return parse_code(Interface, list_of_lines, comments, parent) 51 | 52 | # Define a function for parsing a Module. Returns a list of instances 53 | def parse_module(list_of_lines, comments, parent): 54 | from .module import Module 55 | return parse_code(Module, list_of_lines, comments, parent) 56 | 57 | # Define a function for parsing a Subroutine. Returns a list of instances 58 | def parse_subroutine(list_of_lines, comments, parent): 59 | from .subroutine import Subroutine 60 | return parse_code(Subroutine, list_of_lines, comments, parent) 61 | 62 | # Define a function for parsing a Function. Returns a list of instances 63 | def parse_function(list_of_lines, comments, parent): 64 | from .function import Function 65 | return parse_code(Function, list_of_lines, comments, parent) 66 | 67 | # Define a function for parsing a Type. Returns a list of instances 68 | def parse_type(list_of_lines, comments, parent): 69 | from .type import TypeDeclaration 70 | return parse_code(TypeDeclaration, list_of_lines, comments, parent) 71 | 72 | # Parse a PUBLIC line, identifying routines that are public. 73 | def parse_public(list_of_lines, comments, parent, keyword="PUBLIC"): 74 | # Skip if there are no lines to process. 75 | if (len(list_of_lines) == 0): return [] 76 | line = list_of_lines[0].strip().split() 77 | # Remove any comments on the line that may exist. 78 | if ("!" in line): line = line[:line.index("!")] 79 | # Skip empty lines. 80 | if (len(line) == 0): 81 | list_of_lines.pop(0) 82 | return [] 83 | # Check to see if the first element is 'PUBLIC'. 84 | if (line[0] == keyword): 85 | # If there is only the word 'PUBLIC', then it is a status. 86 | if (len(line) == 1): 87 | parent.status = keyword 88 | # Remove this line from the list (because it has been parsed). 89 | list_of_lines.pop(0) 90 | return [] 91 | # Strip out the double colon if it exists (it's optional). 92 | if (''.join(line[1:3]) == "::"): line = line[3:] 93 | # Remove all commas from the line. 94 | commas = [i for i in range(len(line)) if (line[i] == ",")] 95 | for i in reversed(commas): line.pop(i) 96 | # Remove this line from the list (because it has been parsed). 97 | list_of_lines.pop(0) 98 | # Only variable and function names remain, they are the instances. 99 | return line 100 | # Otherwise, no 'PUBLIC' line found, return empty list. 101 | return [] 102 | 103 | # Parse a PRIVATE line, identifying routines that are private. 104 | def parse_private(list_of_lines, comments, parent): 105 | return parse_public(list_of_lines, comments, parent, keyword="PRIVATE") 106 | 107 | # Parse a USE line, return a string in a list. 108 | def parse_use(list_of_lines, comments, parent): 109 | # Skip if there are no lines to process. 110 | if (len(list_of_lines) == 0): return [] 111 | line = list_of_lines[0].strip().split() 112 | # Skip empty lines. 113 | if (len(line) == 0): return [] 114 | # Check to see if the first element is "USE". 115 | # If it is, then return a list with one string (the line). 116 | if (line[0] == "USE"): return [list_of_lines.pop(0).replace(": :","::")] 117 | # Otherwise, no USE line found, return empty list. 118 | else: return [] 119 | 120 | # Parse a line with IMPLICIT NONE, return string in a list. 121 | def parse_implicit(list_of_lines, comments, parent): 122 | # Skip if there are no lines to process. 123 | if (len(list_of_lines) == 0): return [] 124 | line = list_of_lines[0].strip().split() 125 | # Skip empty lines. 126 | if (len(line) == 0): return [] 127 | # Check to see if the first element is "IMPLICIT". 128 | # If it is, then return a list with one string (the line). 129 | if (line[0] == "IMPLICIT"): 130 | if (len(line) > 1): 131 | if (line[1] == "NONE"): return [list_of_lines.pop(0)] 132 | else: 133 | from fmodpy.exceptions import ParseError 134 | raise(ParseError("Found phrase 'IMPLICIT' but it was not followed by 'NONE'.")) 135 | # Otherwise, no IMPLICIT line found, return empty list. 136 | else: return [] 137 | 138 | # Only initialize a "code" object if the line is not empty and the 139 | # type is matched (after removing any prefixes). 140 | def parse_code(code, list_of_lines, comments, parent): 141 | # Skip if there are no lines to process. 142 | if (len(list_of_lines) == 0): return [] 143 | line = list_of_lines[0].strip().split() 144 | # Skip empty lines. 145 | if (len(line) == 0): return [] 146 | # Remove a prefix if it is at the front of the line. 147 | for p in code.prefixes: 148 | if (line[0] == p): 149 | line = line[1:] 150 | break 151 | # Check for an empty line (this shouldn't happen). 152 | if (len(line) == 0): 153 | import warnings 154 | text = f"\nAn enexpected thing just happened when parsing.\n"+\ 155 | f"After removing {class_name(code)} prefix '{p}', the line was empty.\n" 156 | warnings.warn(text+please_report_to) 157 | # Check for a match, if it matches complete instance initialization. 158 | elif (line[0] == code.type): 159 | parsed_code = code(list_of_lines, comments, parent) 160 | if (parsed_code.lines > 0): return [parsed_code] 161 | else: return [] 162 | # No objects were found, return empty list of instances. 163 | return [] 164 | 165 | # Given a list of strings that represent lines of a file, determine if 166 | # it is a recognizable declaration. If so, define Argument object(s), 167 | # return the list of Argument(s). 168 | def parse_argument(list_of_lines, comments, parent): 169 | line = list_of_lines[0].strip().split() 170 | success = False 171 | if (len(line) == 0): return [] 172 | # Try out all possible declarations. 173 | from .real import Real 174 | from .integer import Integer 175 | from .logical import Logical 176 | from .character import Character 177 | from .complex import Complex 178 | from .type import TypeArgument 179 | for arg_type in [Real, Integer, Logical, Character, Complex, TypeArgument]: 180 | if (line[0] == arg_type.type): 181 | success = True 182 | break 183 | else: return [] 184 | # If an argument type was identified, then finish parsing. 185 | double_colon = [i for i in range(len(line)-1) 186 | if line[i] == line[i+1] == ":"] 187 | # If there is no "::", then variable names will immediately follow type(kind). 188 | # TODO: This might be a parsing error, or a FortranLanguageError. 189 | if (len(double_colon) > 1): raise(NotImplementedError) 190 | elif (len(double_colon) == 1): 191 | # There is a double colon in this line. 192 | double_colon = double_colon[0] 193 | base = line[:double_colon] 194 | tail = line[double_colon+2:] 195 | else: 196 | # If there is a KIND, then include that in the base. 197 | if ((len(line) > 2) and (line[1]) == '('): 198 | # Check to see of there is a paranthetical group after 199 | # the arugment type (this would be for a KIND). 200 | kind, tail = pop_group(line[1:], open_with="(", close_with=")") 201 | base = [line[0]] + ["("] + kind + [")"] 202 | else: 203 | base = line[:1] 204 | tail = line[1:] 205 | # Check to make sure there are variable names after the TYPE. 206 | if (len(tail) == 0): raise(NotImplementedError) 207 | # Now we are guaranteed to use this line to define an argument. 208 | list_of_lines.pop(0) 209 | # Get all names from the tail (with their dimensions, if given). 210 | names = [tail.pop(0)] 211 | while (len(tail) > 0): 212 | # First, check to see of there is a paranthetical group after 213 | # the previous argument name (this would be for a size, like 214 | # ARG(10,2), but it could have expresssions inside as well). 215 | group, tail = pop_group(tail, open_with="(", close_with=")") 216 | if (len(group) > 0): names[-1] += " ( "+" ".join(group)+" )" 217 | # If there is a "," it indicates more arguments follow. 218 | elif (tail[0] == ","): tail.pop(0) 219 | # If this is a PARAMETER assignment, there could be an "= value" 220 | # after the name. There will only be other arguments after 221 | # this one if there is a "," in the line. If no "," then stop. 222 | elif (tail[0] == "="): 223 | if (("," in tail) and ("=" in tail[1:])): 224 | tail = tail[tail.index(",")+1:] 225 | else: break 226 | # Finally, if it is not a comma, group, or value, it must be an argument name. 227 | else: names.append(tail.pop(0)) 228 | # Cycle the names, converting all into proper Argument objects. 229 | args = [] 230 | for n in names: 231 | args.append( arg_type(base[:]) ) 232 | args[-1].parent = parent 233 | # Split the dimension out of the name, if it is there. 234 | if "(" in n: 235 | begin_dim = n.index("(") 236 | n, group = n[:begin_dim].strip(), n[begin_dim:].strip().split() 237 | group, _ = pop_group(group) 238 | dimension = [""] 239 | num_open = 0 240 | while (len(group) > 0): 241 | next_value = group.pop(0) 242 | if (next_value == ",") and (num_open == 0): 243 | dimension.append("") 244 | else: dimension[-1] += next_value 245 | if (next_value == "("): num_open += 1 246 | if (next_value == ")"): num_open -= 1 247 | # Overwrite the dimension. 248 | args[-1].dimension = dimension 249 | # Overwrite the name. 250 | args[-1].name = n 251 | # Return the final list of declared arguments, and success. 252 | return args 253 | -------------------------------------------------------------------------------- /fmodpy/parsing/_old_code.txt: -------------------------------------------------------------------------------- 1 | # TEMPORARY. 2 | _ = ', '.join([a.fort_call_name() for a in present]) 3 | lines += [f"{indent}PRINT *, 'PRESENT: {_}'"] 4 | 5 | 6 | # TEMPORARY. 7 | _ = ', '.join([a.fort_call_name() for a in present]) 8 | 9 | 10 | 11 | module = line_str_list[1] 12 | if (len(line_str_list) > 2): 13 | if (tuple(line_str_list[2:5]) != (",","ONLY",":")): 14 | from fmodpy.exceptions import FortranError 15 | raise(FortranError("Expected long 'USE' statment to have structure 'USE , ONLY: '.\n Recieved instead '{' '.join(line_str_list)}'.")) 16 | names = line_str_list[5:] 17 | while "," in names: names.remove(",") 18 | 19 | 20 | 21 | # The string that will construct a corresponding Parameter object 22 | # as part of a Python signature. 23 | def py_signature(self): 24 | param = [f"'{self.name.lower()}'","inspect.Parameter.POSITIONAL_OR_KEYWORD"] 25 | # Get annotation based on size. 26 | if (self.size in self.np_types): 27 | if (self.dimension is None): dim = "" 28 | else: dim = str(self.dimension).lower().replace(' ','').replace("'",'') 29 | annotation = f"{self.np_types[self.size]}{dim}" 30 | param += [f"annotation='{annotation}'"] 31 | # Add the default value (if it is applicable). 32 | if self._is_optional(): param += ["default=None"] 33 | # Return the Parameter string that assumes "inspect" is imported. 34 | return [f"inspect.Parameter({', '.join(param)})"] 35 | 36 | 37 | # Given a simplified fortran file (list of strings, where each 38 | # element is a line from a file with only the necessary Fortran 39 | # constructs to define the interface with python), return a Fortran 40 | # object (a python-object abstraction of the Fortran file). 41 | def parse_fortran_file(fortran_file): 42 | from fmodpy.parsing.file import Fortran 43 | raise(NotImplementedError) 44 | return 45 | 46 | 47 | # Argument() 48 | # .name = "" 49 | # .type = "" 50 | # .size = "" 51 | # .kind = "" 52 | # .intent = "" 53 | # .message = "" 54 | # .defined = True / False 55 | # .allocatable = True / False 56 | # .optional = True / False 57 | # .dim = ["", ...] 58 | # ._is_optional() 59 | # ._is_present() 60 | # ._to_copy_args() 61 | # ._to_needed_dim() 62 | # .to_py_input_doc_string() 63 | # .to_py_output_doc_string() 64 | # .to_py_input() 65 | # .to_py_prepare() 66 | # .to_py_call() 67 | # .to_py_after() 68 | # .to_py_return() 69 | # .to_c_input() 70 | # .to_c_prepare() 71 | # .to_c_call() 72 | # .to_c_after() 73 | # .to_fort_input() 74 | # .to_fort_declare() 75 | # .to_fort_prepare() 76 | # .to_fort_call() 77 | # .to_fort_after() 78 | # 79 | # Real(Argument) 80 | # 81 | # Integer(Argument) 82 | # 83 | # Logical(Argument) 84 | # 85 | # Character(Argument) 86 | # .len = "" 87 | # 88 | # Procedure(Argument) 89 | # .interface = Interface() 90 | # 91 | # Type(Argument) 92 | # .contains = [, ...] 93 | # 94 | 95 | 96 | -------------------------------------------------------------------------------- /fmodpy/parsing/character.py: -------------------------------------------------------------------------------- 1 | from .argument import Argument 2 | 3 | class Character(Argument): 4 | type = "CHARACTER" 5 | c_types = {"1":"ctypes.c_char"} 6 | default_singleton = "0" 7 | 8 | # Add a warning about logical arrays. 9 | def __init__(self, *args, **kwargs): 10 | super().__init__(*args, **kwargs) 11 | if (self.dimension is not None): 12 | # TODO: Change the C-type of this character be a "uint8" for arrays. 13 | # self.c_types = self.c_types.copy() 14 | # self.c_types["1"] = "ctypes.c_char" # Needs to be uint8 in numpy. 15 | # Show a warning to the user. 16 | from fmodpy.config import show_warnings 17 | if show_warnings: 18 | import warnings 19 | warnings.warn("Fortran CHARACTER arrays are not formally supported yet. Raise an issue with an example to encourage development.") 20 | # If there is a "len", make it the first dimension. 21 | if (self.kind_prefix.startswith("LEN")): 22 | self.dimension = [self.kind] + (self.dimension if self.dimension is not None else []) 23 | -------------------------------------------------------------------------------- /fmodpy/parsing/complex.py: -------------------------------------------------------------------------------- 1 | from .argument import Argument 2 | 3 | COMPLEX_STRUCT = """ 4 | # This defines a C structure that can be used to hold the complex C type. 5 | class c_complex_{type}(ctypes.Structure): 6 | _fields_ = [("real", ctypes.c_{type}), ("imag", ctypes.c_{type})] 7 | # Define an "__init__" that can take a complex number as input. 8 | def __init__(self, real=0.0, imag=None): 9 | # If the provided first value was a complex number, expand it out. 10 | if (hasattr(real, "imag") and hasattr(real, "real") and (imag is None)): 11 | imag = real.imag 12 | real = real.real 13 | # Assign the internal fields. 14 | self.real = real 15 | self.imag = imag 16 | # Define a "value" so that it can be cleanly retreived as a single complex number. 17 | @property 18 | def value(self): 19 | return complex(self.real, self.imag) 20 | """ 21 | 22 | COMPLEX_FLOAT = COMPLEX_STRUCT.format(type="float") 23 | COMPLEX_DOUBLE = COMPLEX_STRUCT.format(type="double") 24 | COMPLEX_LONG_DOUBLE = COMPLEX_STRUCT.format(type="longdouble") 25 | 26 | class Complex(Argument): 27 | type = "COMPLEX" 28 | default_singleton = "1" 29 | c_types = {"32":"c_complex_double", "16":"c_complex_double", "8":"c_complex_float"} 30 | c_types_arrays = {"32":"numpy.complex128", "16":"numpy.complex128", "8":"numpy.complex64"} 31 | py_types = {"32":COMPLEX_DOUBLE, "16":COMPLEX_DOUBLE, "8":COMPLEX_FLOAT} 32 | 33 | @property 34 | def py_type(self): 35 | if (self.size not in self.py_types): 36 | raise(NotImplementedError(f"\n\nUnrecognized size '{self.size}' for argument '{self.name}', no known corresponding Pythonx type.")) 37 | return self.py_types[self.size] 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /fmodpy/parsing/file.py: -------------------------------------------------------------------------------- 1 | from .code import Code 2 | from . import parse_subroutine, parse_function, parse_module, parse_type 3 | 4 | # -------------------------------------------------------------------- 5 | # Class for holding all relevant information in a Fortran file. 6 | class Fortran(Code): 7 | docs = "" 8 | type = "FILE" 9 | needs_end = False 10 | allowed_unknown = True 11 | can_contain = [(parse_subroutine, "subroutines"), 12 | (parse_function, "functions"), 13 | (parse_module, "modules"), 14 | (parse_type, "types"), 15 | ] 16 | 17 | # Generate a string describing this file. 18 | def __str__(self): 19 | out = f"{self.type} {self.name}" 20 | for (_, name) in self.can_contain: 21 | if (len(getattr(self, name)) > 0): out += "\n" 22 | for obj in getattr(self,name): 23 | for line in str(obj).split("\n"): 24 | out += " "+line+"\n" 25 | return out 26 | 27 | 28 | # Create Fortran Subroutines with "BIND(C)" that are 29 | # accessible from C, translate arguments to Fortran, call the 30 | # Fortran code, and translates arguments back (if necessary). 31 | def generate_fortran(self): 32 | lines = ["! This automatically generated Fortran wrapper file allows codes", 33 | "! written in Fortran to be called directly from C and translates all", 34 | "! C-style arguments into expected Fortran-style arguments (with", 35 | "! assumed size, local type declarations, etc.).", 36 | ""] 37 | lines += super().generate_fortran() 38 | # Return the list of lines for this object (add an extra newline). 39 | from . import wrap_long_lines 40 | # Return the final Fortran file as a string. 41 | return "\n".join(wrap_long_lines(lines+[''])) 42 | 43 | # Generate a string representation of the python code for this wrapper. 44 | def generate_python(self): 45 | # Define the header to this file. Some necessary variables are 46 | # left to be filled by formatting, namely: 47 | # f_compiler -- string 48 | # shared_object_name -- string 49 | # f_compiler_args -- string 50 | # dependencies -- list of strings 51 | # module_name -- string 52 | lines = ["'''This Python code is an automatically generated wrapper", 53 | "for Fortran code made by 'fmodpy'. The original documentation", 54 | "for the Fortran source code follows.", 55 | "", 56 | self.docs, 57 | "'''", 58 | '', 59 | 'import os', 60 | 'import ctypes', 61 | 'import platform', 62 | 'import numpy', 63 | '', 64 | '# --------------------------------------------------------------------', 65 | '# CONFIGURATION', 66 | '# ', 67 | '_verbose = {verbose_module}', 68 | '_fort_compiler = "{f_compiler}"', 69 | '_shared_object_name = "{shared_object_name}." + platform.machine() + ".so"', 70 | '_this_directory = os.path.dirname(os.path.abspath(__file__))', 71 | '_path_to_lib = os.path.join(_this_directory, _shared_object_name)', 72 | '_compile_options = {f_compiler_args}', 73 | '_ordered_dependencies = {dependencies}', 74 | '_symbol_files = {symbol_files}' 75 | '# ', 76 | '# --------------------------------------------------------------------', 77 | '# AUTO-COMPILING', 78 | '#', 79 | '# Try to import the prerequisite symbols for the compiled code.', 80 | 'for _ in _symbol_files:', 81 | ' _ = ctypes.CDLL(os.path.join(_this_directory, _), mode=ctypes.RTLD_GLOBAL)', 82 | '# Try to import the existing object. If that fails, recompile and then try.', 83 | 'try:', 84 | ' # Check to see if the source files have been modified and a recompilation is needed.', 85 | ' if (max(max([0]+[os.path.getmtime(os.path.realpath(os.path.join(_this_directory,_))) for _ in _symbol_files]),', 86 | ' max([0]+[os.path.getmtime(os.path.realpath(os.path.join(_this_directory,_))) for _ in _ordered_dependencies]))', 87 | ' > os.path.getmtime(_path_to_lib)):', 88 | ' print()', 89 | ' print("WARNING: Recompiling because the modification time of a source file is newer than the library.", flush=True)', 90 | ' print()', 91 | ' if os.path.exists(_path_to_lib):', 92 | ' os.remove(_path_to_lib)', 93 | ' raise NotImplementedError(f"The newest library code has not been compiled.")', 94 | ' # Import the library.', 95 | ' clib = ctypes.CDLL(_path_to_lib)', 96 | 'except:', 97 | ' # Remove the shared object if it exists, because it is faulty.', 98 | ' if os.path.exists(_shared_object_name):', 99 | ' os.remove(_shared_object_name)', 100 | ' # Compile a new shared object.', 101 | ' _command = [_fort_compiler] + _ordered_dependencies + _compile_options + ["-o", _shared_object_name]', 102 | ' if _verbose:', 103 | ' print("Running system command with arguments")', 104 | ' print(" ", " ".join(_command))', 105 | ' # Run the compilation command.', 106 | ' import subprocess', 107 | ' subprocess.check_call(_command, cwd=_this_directory)', 108 | ' # Import the shared object file as a C library with ctypes.', 109 | ' clib = ctypes.CDLL(_path_to_lib)', 110 | '# --------------------------------------------------------------------', 111 | ''] 112 | # Add all the Python lines for the children of this Code. 113 | lines += super().generate_python() 114 | # Return the Python file as a string. 115 | return "\n".join(lines + ['']) 116 | -------------------------------------------------------------------------------- /fmodpy/parsing/function.py: -------------------------------------------------------------------------------- 1 | from .subroutine import Subroutine 2 | 3 | # -------------------------------------------------------------------- 4 | class Function(Subroutine): 5 | type = "FUNCTION" 6 | result = None 7 | 8 | # Produce a string representation of this Subroutine object. 9 | def __str__(self): 10 | # Print out header line. 11 | arg_names = [a.name for a in self.arguments if (a.name != self.result)] 12 | out = f"{self.type} {self.name}({', '.join(arg_names)})" 13 | if (self.result != self.name): out += f" RESULT({self.result})\n" 14 | else: out += "\n" 15 | # Use SUBROUTINE string method for the rest (strip SUBROUTINE). 16 | out += "\n".join(super().__str__().split("\n")[1:]) 17 | return out 18 | 19 | # Given a list of lines (of a source Fortran file), parse out this 20 | # Function (assuming the first line is the first line *inside* of 21 | # this Function). 22 | def parse(self, list_of_lines): 23 | # Pre-fetch the "result" and modify the first line so that it 24 | # can be parsed correctly by the Subroutine.parse function. 25 | declaration_line = list_of_lines[0].strip().split() 26 | arg_start = declaration_line.index("(") 27 | arg_end = declaration_line.index(")") 28 | argument_names = declaration_line[arg_start+1:arg_end] 29 | while "," in argument_names: argument_names.remove(",") 30 | name = declaration_line[:arg_start][-1] 31 | rest_of_line = declaration_line[arg_end+1:] 32 | # Get the result from the end of the line. 33 | if ("RESULT" in rest_of_line): 34 | if ("(" not in rest_of_line) or (")" not in rest_of_line): 35 | from fmodpy.exceptions import ParseError 36 | raise(ParseError(f"Found 'RESULT' but no '()' on line.\n{list_of_lines[0].strip()}")) 37 | self.result = ''.join(rest_of_line[rest_of_line.index("(")+1: 38 | rest_of_line.index(")")]) 39 | else: self.result = name 40 | argument_names.append( self.result ) 41 | list_of_lines[0] = f"SUBROUTINE {name} ( {' , '.join(argument_names)} )" 42 | # ------------------------------------------------------------ 43 | # Use Subroutine parse code. 44 | super().parse(list_of_lines) 45 | # ------------------------------------------------------------ 46 | # Find the result in the identified arguments, make sure its 47 | # intent is not declared (it will make Fortran compilers angry). 48 | for arg in self.arguments: 49 | if (arg.name == self.result): 50 | arg.intent = "OUT" 51 | arg.show_intent = False 52 | break 53 | else: 54 | from fmodpy.config import fmodpy_print as print 55 | print() 56 | print("Did not find output argument in parsed list..") 57 | print(self.result) 58 | print() 59 | raise(NotImplementedError) 60 | 61 | -------------------------------------------------------------------------------- /fmodpy/parsing/implicit_none.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchlux/fmodpy/88002dc980ef368d200ed6c114aeae5f0aaab297/fmodpy/parsing/implicit_none.py -------------------------------------------------------------------------------- /fmodpy/parsing/integer.py: -------------------------------------------------------------------------------- 1 | from .argument import Argument 2 | 3 | class Integer(Argument): 4 | type = "INTEGER" 5 | c_types = {"8":"ctypes.c_long", "4":"ctypes.c_int"} 6 | default_singleton = "1" 7 | -------------------------------------------------------------------------------- /fmodpy/parsing/interface.py: -------------------------------------------------------------------------------- 1 | from . import parse_subroutine, parse_function 2 | from .code import Code 3 | 4 | # PROCEDURE cannot have "INTENT". 5 | # 6 | # All passed procedure arguments, will be defined as 7 | # 'ABSTRACT INTERFACE's and declared at the front of the 8 | # definitions for a routine. Since they are passed, they must 9 | # be procedures with the BIND(C) attribute enabled. 10 | 11 | 12 | # For parsing purposes, an Interface looks like a Module. 13 | class Interface(Code): 14 | type = "INTERFACE" 15 | named_end = False 16 | # prefixes = ["ABSTRACT"] # <- need another type for abstrac ones. 17 | can_contain = [(parse_subroutine, "subroutines"), 18 | (parse_function, "functions") 19 | ] 20 | 21 | # Produce a string representation of this Subroutine object. 22 | def __str__(self): 23 | # Print out header line. 24 | out = "INTERFACE\n" 25 | for f in self.subroutines: 26 | for line in str(f).split("\n"): 27 | out += " "+line+"\n" 28 | if (len(self.subroutines) > 0): out += "\n" 29 | for f in self.functions: 30 | for line in str(f).split("\n"): 31 | out += " "+line+"\n" 32 | out += "END INTERFACE" 33 | # Return the whole interface. 34 | return out 35 | 36 | # Given a list of lines (of a source Fortran file), parse out this 37 | # Function (assuming the first line is the first line *inside* of 38 | # this Function). 39 | def parse(self, list_of_lines): 40 | # Pop out the first line, it's not needed. 41 | list_of_lines.pop(0) 42 | # Use standard parsing code (looks for subroutines and functions). 43 | super().parse(list_of_lines) 44 | -------------------------------------------------------------------------------- /fmodpy/parsing/logical.py: -------------------------------------------------------------------------------- 1 | from .argument import Argument 2 | 3 | class Logical(Argument): 4 | type = "LOGICAL" 5 | c_types = {"4":"ctypes.c_int", "1":"ctypes.c_bool"} 6 | default_singleton = "0" 7 | 8 | # For array inputs of logicals, change the C-type to be an "int". 9 | def __init__(self, *args, **kwargs): 10 | super().__init__(*args, **kwargs) 11 | if (self.dimension is not None): 12 | self.c_types = self.c_types.copy() 13 | self.c_types["4"] = "ctypes.c_int" 14 | 15 | # Add a warning about logical arrays. 16 | def py_declare(self): 17 | # Show a warning to the user for passing in logical array types. 18 | if ((self.dimension is not None) and (self._allowed_input())): 19 | from fmodpy.config import show_warnings 20 | if show_warnings: 21 | import warnings 22 | warnings.warn("Fortran LOGICAL arrays must be given as 32-bit integers.") 23 | # Return the usual function. 24 | return super().py_declare() 25 | 26 | -------------------------------------------------------------------------------- /fmodpy/parsing/module.py: -------------------------------------------------------------------------------- 1 | from .code import Code 2 | from . import parse_use, parse_implicit, parse_public, parse_private, \ 3 | parse_argument, parse_interface, parse_type, parse_subroutine, \ 4 | parse_function 5 | from .subroutine import Subroutine 6 | from .function import Function 7 | 8 | 9 | # -------------------------------------------------------------------- 10 | class Module(Code): 11 | type = "MODULE" 12 | status = "PUBLIC" 13 | can_contain = [(parse_use, "uses"), 14 | (parse_implicit, "implicit_none"), 15 | (parse_public, "public"), 16 | (parse_private, "private"), 17 | (parse_type, "types"), 18 | (parse_interface, "interfaces"), 19 | (parse_argument, "arguments"), 20 | (parse_subroutine, "subroutines"), 21 | (parse_function, "functions"), 22 | ] 23 | 24 | 25 | # Produce a string representation of this code object. 26 | def __str__(self): 27 | out = f"{self.type} {self.name}\n" 28 | # Add documentation. 29 | if (len(self.docs.strip()) > 0): 30 | doc_lines = self.docs.strip().split("\n") 31 | for line in doc_lines: out += f" {line}\n" 32 | # Add used modules. 33 | for line in sorted(self.uses): out += f" {line}\n" 34 | for line in sorted(self.implicit_none): out += f" {line}\n" 35 | # Add types. 36 | for t in self.types: 37 | for line in str(t).split("\n"): 38 | out += f" {line}\n" 39 | # Add interfaces. 40 | if (len(self.interfaces) > 0): out += "\n" 41 | for i in self.interfaces: 42 | for line in str(i).split("\n"): 43 | out += f" {line}\n" 44 | # Only add the space before arguments if types or 45 | # interfaces came before them. 46 | if ((max(len(self.interfaces),len(self.types)) > 0) and 47 | (len(self.arguments) > 0)): out += "\n" 48 | # Add arguments. 49 | for a in self.arguments: 50 | out += f" {a}\n" 51 | # Add subroutines. 52 | for obj in self.subroutines: 53 | out += "\n" 54 | for line in str(obj).split("\n"): 55 | out += f" {line}\n" 56 | # Add functions. 57 | for obj in self.functions: 58 | out += "\n" 59 | for line in str(obj).split("\n"): 60 | out += f" {line}\n" 61 | # End the module. 62 | out += f"END {self.type} {self.name}\n" 63 | return out 64 | 65 | # Generate Fortran wrapper code that can call this Module's source code. 66 | def generate_fortran(self): 67 | # Generate the input signature. 68 | fortran_arguments = [] 69 | for arg in self.arguments: fortran_arguments += arg.fort_input() 70 | lines = ['',f'{self.type} C_{self.name}'] 71 | # Add the "USE" line. 72 | lines += self.uses 73 | if any((a.allocatable and a._is_output()) for a in self.arguments): 74 | lines += [' USE ISO_FORTRAN_ENV, ONLY: INT64'] 75 | if any(a._is_optional() for a in self.arguments): 76 | lines += [' USE ISO_C_BINDING, ONLY: C_BOOL'] 77 | # Enforce no implicit typing (within this code). 78 | lines += [f' IMPLICIT NONE'] 79 | lines += [''] 80 | 81 | # Declare all interfaces. 82 | for i in self.interfaces: 83 | if (len(i.subroutines) + len(i.functions) > 0): 84 | lines += [''] 85 | lines += [' '+l for l in str(i).split("\n")] 86 | 87 | lines += [''] 88 | lines += ['CONTAINS'] 89 | lines += [''] 90 | 91 | # Define all "getter" and "setter" subroutines for accessing 92 | # internal module (PUBLIC) attributes. 93 | for arg in self.arguments: 94 | lines += ['',f' ! Getter and setter for {arg.name}.'] 95 | lines += [' '+l for l in arg.fort_getter()] 96 | lines += [' '+l for l in arg.fort_setter()] 97 | 98 | # Declare all subroutines and functions. 99 | for code in self.subroutines + self.functions: 100 | lines += [''] 101 | lines += [' ' + l for l in code.generate_fortran()] 102 | 103 | # Add the END line. 104 | lines += [f"END {self.type} C_{self.name}",''] 105 | 106 | return lines 107 | 108 | # Generate Python code. 109 | def generate_python(self, *args): 110 | # Add access points for the contained subroutines and functions. 111 | lines = [ '', 112 | f'class {self.name.lower()}:', 113 | f" '''{self.docs}'''"] 114 | # Generate internal types. 115 | for t in self.types: 116 | lines += [ " "+l for l in t.py_declare()] 117 | # Generate getter and setter for arguments. 118 | for arg in self.arguments: 119 | py_name = arg.name.lower() 120 | lines += [''] 121 | lines += [f" # Declare '{py_name}'"] 122 | # Declare the "getter" for this module variable. 123 | lines += [ " "+l for l in arg.py_getter() ] 124 | # Declare the "setter" for this module variable. 125 | lines += [ " "+l for l in arg.py_setter() ] 126 | # Declare this as a property that has a getter and a setter. 127 | lines += [ " "+l for l in arg.py_property() ] 128 | # Generate static methods for all internal routines. 129 | for code in self.subroutines + self.functions: 130 | lines += [''] 131 | lines += [' '+l for l in code.generate_python(*args)] 132 | # Replace the class definition with a single instance of the class. 133 | lines += [ "", 134 | f"{self.name.lower()} = {self.name.lower()}()"] 135 | return lines 136 | 137 | # Given a list of lines (of a source Fortran file), parse out this 138 | # module (assuming the first line is the first line *inside* of 139 | # this module). 140 | def parse(self, list_of_lines): 141 | self.lines += 1 142 | line = list_of_lines.pop(0).strip().split() 143 | # Remove any comments on the line that may exist. 144 | if ("!" in line): line = line[:line.index("!")] 145 | # Catch a potential parsing error. 146 | if (len(line) <= 1): 147 | from fmodpy.exceptions import ParseError 148 | raise(ParseError(f"Expected 'MODULE ', but the line did not contain the name of module.\n {list_of_lines[0]}")) 149 | # Get the name of this module. 150 | self.name = line[1] 151 | # ------- Default parsing operations ------- 152 | super().parse(list_of_lines) 153 | # ------------------------------------------ 154 | # Get the set of things declared private. 155 | private = set(self.private) 156 | # Remove any instances of things that are declared private. 157 | if (len(private) > 0): 158 | for attr in ("arguments","interfaces","types","subroutines","functions"): 159 | codes = getattr(self,attr) 160 | to_remove = [i for (i,c) in enumerate(codes) if (c.name in private)] 161 | for i in reversed(to_remove): codes.pop(i) 162 | if (len(to_remove) > 0): 163 | from fmodpy.config import fmodpy_print as print 164 | print(f" removed {len(to_remove)} from '{attr}' declared PRIVATE..") 165 | # Remove any things not declared public if this MODULE is private. 166 | if (self.status == "PRIVATE"): 167 | public = set(self.public) 168 | for attr in ("interfaces","types","subroutines","functions","arguments"): 169 | codes = getattr(self,attr) 170 | to_remove = [i for (i,c) in enumerate(codes) if (c.name not in public)] 171 | for i in reversed(to_remove): codes.pop(i) 172 | if (len(to_remove) > 0): 173 | from fmodpy.config import fmodpy_print as print 174 | print(f" removed {len(to_remove)} from '{attr}' because this MODULE is PRIVATE..") 175 | # Make sure all "arguments" don't show intent. 176 | for a in self.arguments: a.show_intent = False 177 | -------------------------------------------------------------------------------- /fmodpy/parsing/procedure.py: -------------------------------------------------------------------------------- 1 | from .argument import Argument 2 | 3 | class Procedure(Argument): 4 | type = "PROCEDURE" 5 | interface = None # Interface() 6 | 7 | -------------------------------------------------------------------------------- /fmodpy/parsing/real.py: -------------------------------------------------------------------------------- 1 | from .argument import Argument 2 | 3 | class Real(Argument): 4 | type = "REAL" 5 | c_types = {"8":"ctypes.c_double", "4":"ctypes.c_float"} 6 | default_singleton = "1" 7 | -------------------------------------------------------------------------------- /fmodpy/parsing/type.py: -------------------------------------------------------------------------------- 1 | from .code import Code 2 | from .argument import Argument 3 | from . import parse_argument 4 | 5 | STRUCT_DEFAULT_VALUE = 0 6 | CONTENTS_DECLARATION = '("{n}", {t})' 7 | FIELD_DECLARATION = 'self.{n} = kwargs.get("{n}", getattr(value, "{n}", value))' 8 | STRUCTURE_DEFINITION = ''' 9 | # This defines a C structure that can be used to hold this defined type. 10 | class {name}(ctypes.Structure): 11 | # (name, ctype) fields for this structure. 12 | _fields_ = [{fields}] 13 | # Define an "__init__" that can take a class or keyword arguments as input. 14 | def __init__(self, value={default}, **kwargs): 15 | # From whatever object (or dictionary) was given, assign internal values. 16 | {field_declarations} 17 | # Define a "__str__" that produces a legible summary of this type. 18 | def __str__(self): 19 | s = [] 20 | for (n, t) in self._fields_: 21 | s.append( n + "=" + str(getattr(self,n)) ) 22 | return "{name}[" + ", ".join(s) + "]" 23 | # Define an "__eq__" method that checks equality of all fields. 24 | def __eq__(self, other): 25 | for (n, t) in self._fields_: 26 | if (getattr(self, n) != getattr(other, n, None)): 27 | return False 28 | return True 29 | ''' 30 | 31 | 32 | class TypeDeclaration(Code): 33 | type = "TYPE" 34 | can_contain = [(parse_argument, "arguments")] 35 | 36 | # Given a list of lines (of a source Fortran file), parse out this 37 | # TYPE (assuming the first line is the line declaring this 38 | # Subroutine). 39 | def parse(self, list_of_lines): 40 | # If there is "TYPE (" then this doesn't declare a type, it is an argument. 41 | if ("(" == list_of_lines[0][5:6]): return 42 | # Otherwise, this declares a type. 43 | self.lines += 1 44 | if ('BIND ( C )' not in list_of_lines[0]): 45 | from fmodpy.exceptions import NotSupportedError 46 | raise NotSupportedError(f"\n Fortran derived types must be C compatible and include 'BIND(C)' to be wrapped.\n Encountered this issue when parsing the following line:\n {list_of_lines[0]}") 47 | # Parse the name of this subroutine out of the argument list. 48 | declaration_line = list_of_lines.pop(0).strip().split() 49 | assert (':' in declaration_line), "Expected ':' to exist in TYPE declaration line:\n {' '.join(declaration_line)}" 50 | colon_index = -declaration_line[::-1].index(':')-1 51 | declaration_line = declaration_line[colon_index+1:] 52 | # Extract any "arguments" to the type. 53 | argument_names = set() 54 | # Assume the format "TYPE :: name" 55 | # or "TYPE :: name(arg1, arg2, ...)" 56 | if ('(' in declaration_line): 57 | self.name = declaration_line.pop(0) 58 | argument_names = { 59 | token for token in declaration_line 60 | if token not in {'(', ',', ')'} 61 | } 62 | else: 63 | self.name = declaration_line[-1] 64 | # ------- Default parsing operations ------- 65 | super().parse(list_of_lines) 66 | # ------------------------------------------ 67 | # Disable showing "INTENT" on all arguments of this TYPE. 68 | for arg in self.arguments: 69 | arg.show_intent = False 70 | # Verify that all arguments are defined and permissable. 71 | if (len(argument_names) > 0): 72 | for arg in self.arguments: 73 | if (arg.name in argument_names): 74 | if (arg.type_kind): 75 | from fmodpy.exceptions import NotSupportedError 76 | raise(NotSupportedError( 77 | "\nDerived types with KIND parameters are not supported yet."+ 78 | "\n Consider posting a simplified example with your motivation to"+ 79 | "\n https://github.com/tchlux/fmodpy/issues" 80 | )) 81 | elif (not arg.type_len): 82 | raise(NotImplemented()) 83 | argument_names.remove(arg.name) 84 | assert (len(argument_names) == 0), f'TYPE arguments {argument_names} were not defined within the type.' 85 | 86 | 87 | # Declare the lines that create the corresponding Struct for this type. 88 | # TODO: Treat the type like it is a c function being called, include 89 | # size parameters for arrays (when it is not C compatible). 90 | def py_declare(self): 91 | # Get the names and types necessary to declare a C-compatible struct. 92 | name = self.name 93 | names = [arg.name for arg in self.arguments] 94 | types = [arg.c_type for arg in self.arguments] 95 | struct = STRUCTURE_DEFINITION.format( 96 | name = name, 97 | default = STRUCT_DEFAULT_VALUE, 98 | fields = ", ".join([CONTENTS_DECLARATION.format(n=n.lower(), t=t) 99 | for (n,t) in zip(names, types)]), 100 | field_declarations = "\n ".join([FIELD_DECLARATION.format(n=n.lower()) 101 | for n in names]) 102 | ) 103 | # Return the list of lines that defines this struct. 104 | return struct.split("\n") 105 | 106 | # Declare the lines that create this type in Fortran. 107 | def fort_declare(self): 108 | return str(self).split("\n") 109 | 110 | def __str__(self): 111 | # Define the arguments to the type (for constructing its internals). 112 | type_args = [a.name for a in self.arguments if a.type_len] 113 | if (len(type_args) > 0): 114 | args = f"(" + ", ".join(type_args) + ")" 115 | else: 116 | args = "" 117 | # Define the type. 118 | out = f"{self.type}, BIND(C) :: {self.name}{args}\n" 119 | # Add arguments. 120 | for a in self.arguments: 121 | temp = a.copy() 122 | temp.show_intent = False 123 | out += f" {temp}\n" 124 | # End type. 125 | out += f"END {self.type} {self.name}" 126 | return out 127 | 128 | 129 | class TypeArgument(Argument): 130 | type = "TYPE" 131 | c_types = {} 132 | default_singleton = 1 133 | kind_prefix = "" 134 | 135 | # For a type, the "c_type" in python is the type name (assuming 136 | # the struct was defined in an accessible scope). 137 | @property 138 | def c_type(self): 139 | return self.kind 140 | 141 | # TODO: Make both the type and a C compatible equivalent. 142 | # def fort_declare(self): 143 | 144 | # TODO: Copy the input into the true Fortran type. 145 | # def fort_before(self): 146 | 147 | # TODO: Copy the result from the call outputs into the C inputs. 148 | # def fort_after(self): 149 | -------------------------------------------------------------------------------- /fmodpy/parsing/use.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchlux/fmodpy/88002dc980ef368d200ed6c114aeae5f0aaab297/fmodpy/parsing/use.py -------------------------------------------------------------------------------- /fmodpy/parsing/util.py: -------------------------------------------------------------------------------- 1 | from fmodpy.parsing import FORT_TEXT_REPLACEMENTS, FORT_TEXT_FIXES 2 | 3 | # Short functions. 4 | # identifying the strings before and after the last "." 5 | def before_dot(name): return name[:len(name) - 1 - name[::-1].find(".")]; 6 | def after_dot(name): return name[min(-name[::-1].find("."),0):] 7 | # getting the class name of something, stripping module prefix 8 | def class_name(cls): return str(type(cls)).split(".")[-1].split("'")[0] 9 | # identifying legal python module names 10 | def legal_module_name(name): 11 | return (name.replace("_","")[0].isalpha() and 12 | name.replace("_","")[1:].isalnum() and 13 | (len(name) > 0)) 14 | 15 | # Shorten all strings to be 132 characters at maximum given a list of str. 16 | def wrap_long_lines(list_of_lines, max_len=132): 17 | i = -1 18 | while (i+1 < len(list_of_lines)): 19 | i += 1 20 | # Get the line (strip off any comments for length checks). 21 | line = list_of_lines[i] 22 | if "!" in line: line = line[:line.index("!")] 23 | # Check the length of the (uncommented) line. 24 | if (len(line) > max_len-1): 25 | # Break up this line if it is too long. 26 | keep, rest = list_of_lines[i][:max_len-1], list_of_lines[i][max_len-1:] 27 | list_of_lines[i] = keep+"&" 28 | list_of_lines.insert(i+1, "&"+rest) 29 | return list_of_lines 30 | 31 | # Function for efficiently performing a series of replacements on a line of text 32 | def clean_text(text, replacements=FORT_TEXT_REPLACEMENTS, fixes=FORT_TEXT_FIXES): 33 | import re 34 | # Create a proper reg-exp dictionary of replacements 35 | replacements = {re.escape(k):v for (k,v) in replacements.items()} 36 | # Generate a regular expression pattern with that dictionary 37 | pattern = re.compile("|".join(replacements.keys())) 38 | # Perform the replacement using python reg-exp search 39 | cleaned_file = pattern.sub(lambda m: replacements[re.escape(m.group(0))], text) 40 | # Now repeat the above steps undoing any broken elements. 41 | fixes = {re.escape(k):v for (k,v) in fixes.items()} 42 | pattern = re.compile("|".join(fixes.keys())) 43 | fixed_file = pattern.sub(lambda m: fixes[re.escape(m.group(0))], cleaned_file) 44 | return fixed_file 45 | 46 | # Read a fortran file, store it as single lines 47 | # (without leading and trailing whitespace) and return 48 | def simplify_fortran_file(in_file, fixed_format=False): 49 | from fmodpy.parsing import ACCEPTABLE_LINE_STARTS, \ 50 | LINE_STARTS_TO_REMOVE, IMMEDIATELY_EXCLUDE 51 | with open(in_file) as f: 52 | fort_file = [] 53 | curr_line = "" 54 | for line in f.readlines(): 55 | if (fixed_format) and (len(line) >0) and (line[0].upper() == "C"): 56 | line = "!" + line[1:] 57 | # Split out the comments from the line 58 | comment_start = line.find("!") if (line.find("!") != -1) else len(line) 59 | line, comment = line[:comment_start], line[comment_start:].strip() 60 | # Keep lines that are strictly comments (might be documentation) 61 | if len(line.strip()) == 0: 62 | fort_file.append(comment) 63 | continue 64 | # Make all fortran upper case 65 | line = line.upper() 66 | # Break the line by the colon character 67 | lines = line.split(";") 68 | for line in lines: 69 | if len(line.strip()) == 0: continue 70 | if (fixed_format): 71 | line = line[:72] 72 | # Line continuation character in column 5 73 | if (len(line) > 5) and (not line[5].isspace()): 74 | line = "&" + line[6:] 75 | # Retro-actively pop the previous line for continuation 76 | if len(curr_line) == 0: curr_line = fort_file.pop(-1) 77 | # Remove any numeric labels from the front of the line 78 | line = line.strip().split() 79 | if (len(line) > 0) and (line[0].isnumeric()): line = line[1:] 80 | line = " ".join(line) 81 | # After processing old fortran, properly strip the line of whitespace 82 | line = line.strip() 83 | # Remove a leading ampersand if necessary 84 | if (line[0] == "&"): 85 | line = line[1:] 86 | curr_line += line 87 | if ((len(line) > 0) and (line[-1] == "&")): 88 | curr_line = curr_line[:-1] 89 | else: 90 | # Process the line into a common format 91 | # (upper case, space separated, no commas) 92 | clean_line = clean_text(curr_line) 93 | line = [v.strip() for v in clean_line.split()] 94 | # Only take lines that are acceptable 95 | acceptable = (len(line) > 0) and ( 96 | ("!" in line[0]) or (line[0] in ACCEPTABLE_LINE_STARTS)) 97 | # Check for certain exclusions (like "PROGRAM"). 98 | if (len(line) > 0) and (line[0] in IMMEDIATELY_EXCLUDE): 99 | from fmodpy.exceptions import FortranError 100 | raise(FortranError(("A valid fortran python module cannot"+ 101 | " contain '%s'.")%(line[0]))) 102 | # Store the line if it was acceptable. 103 | if acceptable: 104 | # Remove line starts that can be ignored safely. 105 | while ((len(line) > 0) and (line[0] in LINE_STARTS_TO_REMOVE)): 106 | line.pop(0) 107 | # Add the line to the simplified Fortran file. 108 | fort_file.append( " ".join(line) ) 109 | curr_line = "" 110 | return fort_file 111 | 112 | 113 | # Given a list of strings, return the group of elements between 114 | # and . This is initially designed to extract 115 | # the elements within an open-close parenthesis while allowing for 116 | # nested parenthetical groups. Returns two lists, one containing the 117 | # group (if it starts at the beginning of , else empty), the 118 | # other containing the remainder of the list of strings. 119 | def pop_group(list_str, open_with="(", close_with=")"): 120 | group = [] 121 | # Get the first element of the string (open the group, if matched). 122 | if ((len(list_str) > 0) and (list_str[0] == open_with)): 123 | list_str.pop(0) 124 | num_open = 1 125 | else: num_open = 0 126 | # Search until the started group is closed. 127 | while (num_open > 0): 128 | # TOOD: Might need to raise parsing error when this happens. 129 | if (len(list_str) == 0): raise(NotImplementedError) 130 | next_value = list_str.pop(0) 131 | if (next_value == open_with): num_open += 1 132 | elif (next_value == close_with): num_open -= 1 133 | if (num_open != 0): group.append(next_value) 134 | # Return the captured portion and the remaining. 135 | return group, list_str 136 | 137 | -------------------------------------------------------------------------------- /fmodpy/test/.coverage: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tchlux/fmodpy/88002dc980ef368d200ed6c114aeae5f0aaab297/fmodpy/test/.coverage -------------------------------------------------------------------------------- /fmodpy/test/character/__init__.py: -------------------------------------------------------------------------------- 1 | # Standardized testing interface. 2 | def test(): 3 | import os 4 | dir_name = os.path.dirname(os.path.abspath(__file__)) 5 | test_name = os.path.basename(dir_name) 6 | fort_file = os.path.join(dir_name, f"test_{test_name}.f03") 7 | build_dir = os.path.join(dir_name, f"fmodpy_{test_name}") 8 | print(f" {test_name}..", end=" ", flush=True) 9 | import fmodpy 10 | fort = fmodpy.fimport(fort_file, build_dir=build_dir, 11 | output_dir=dir_name, verbose=False, wrap=True, 12 | rebuild=True, show_warnings=False) 13 | # --------------------------------------------------------------- 14 | # Begin specific testing code. 15 | 16 | import ctypes 17 | import numpy as np 18 | a = np.array(list(map(ord,['1', '0', '1', '0', '1', '0', '1', '0'])), 19 | dtype=ctypes.c_char, order='F') 20 | out = a.copy() 21 | # Check for successful copy. 22 | result = fort.test_simple_character(a, b=out, c=ord('1')) 23 | assert(all(result.view('uint8') == a.view('uint8'))) 24 | # Check for successful overwrite. 25 | result = fort.test_simple_character(a, b=out, c=ord('0')) 26 | assert(all(result.view('uint8') == [1, 2, 0, 1, 2, 0, 1, 2])) 27 | 28 | # Check for string copy in and out correctly. 29 | a = "abc" 30 | b = fort.test_string(a) 31 | assert(a == b) 32 | 33 | # End specific testing code. 34 | # --------------------------------------------------------------- 35 | print("passed", flush=True) 36 | import shutil 37 | shutil.rmtree(os.path.join(dir_name,f"test_{test_name}")) 38 | 39 | if __name__ == "__main__": 40 | test() 41 | -------------------------------------------------------------------------------- /fmodpy/test/character/test_character.f03: -------------------------------------------------------------------------------- 1 | 2 | SUBROUTINE TEST_SIMPLE_CHARACTER(A,C,B) 3 | CHARACTER, INTENT(IN), DIMENSION(:) :: A 4 | CHARACTER, INTENT(IN), OPTIONAL :: C 5 | CHARACTER, INTENT(OUT), DIMENSION(SIZE(A)) :: B 6 | ! Local 7 | INTEGER :: I 8 | B(:) = 'A' 9 | DO I = 1, SIZE(B) 10 | IF (PRESENT(C) .AND. (C .EQ. '1')) THEN 11 | B(I) = A(I) 12 | ELSE 13 | B(I) = CHAR(MOD(I,3)) 14 | END IF 15 | END DO 16 | END SUBROUTINE TEST_SIMPLE_CHARACTER 17 | 18 | 19 | SUBROUTINE TEST_STRING(A,B) 20 | CHARACTER(LEN=*), INTENT(IN) :: A 21 | CHARACTER(LEN=LEN(A)), INTENT(OUT) :: B 22 | INTEGER :: I 23 | DO I = 1, LEN(A) 24 | B(I:I) = A(I:I) 25 | END DO 26 | END SUBROUTINE TEST_STRING 27 | 28 | 29 | ! python3 -c "import fmodpy as f; f.fimport('simple_character.f03', build_dir='.', verbose=True)" 30 | ! python3 -c "import og_fmodpy as f; f.wrap('simple_character.f03', working_directory='og_fmodpy', verbose=True)" 31 | -------------------------------------------------------------------------------- /fmodpy/test/complex128/__init__.py: -------------------------------------------------------------------------------- 1 | # Standardized testing interface. 2 | def test(): 3 | import os 4 | dir_name = os.path.dirname(os.path.abspath(__file__)) 5 | test_name = os.path.basename(dir_name) 6 | fort_file = os.path.join(dir_name, f"test_{test_name}.f03") 7 | build_dir = os.path.join(dir_name, f"fmodpy_{test_name}") 8 | print(f" {test_name}..", end=" ", flush=True) 9 | import fmodpy 10 | fort = fmodpy.fimport(fort_file, build_dir=build_dir, 11 | output_dir=dir_name) 12 | # --------------------------------------------------------------- 13 | # Begin specific testing code. 14 | 15 | import numpy as np 16 | n = 10 17 | array_in = np.asarray(np.arange(n), dtype=np.complex128, order='F') 18 | array_out = np.zeros(n//2 - 1, dtype=np.complex128, order='F') 19 | sing_in = complex(7,0) 20 | out = fort.test_standard(sing_in, array_in, array_out) 21 | # Test the standard functionality. 22 | assert(out[0] == 8), out[0] 23 | assert(all(out[1] == [0,1,2,3])) 24 | assert(all(out[2] == [1,2,3,4])) 25 | assert(np.all(out[3] == [[2,3,4,5], 26 | [3,4,5,6], 27 | [4,5,6,7]])) 28 | assert(out[4] == None) 29 | # Test the extended functionality. 30 | out = fort.test_extended(4) 31 | assert(out[0] == None) 32 | assert(out[1] == None) 33 | assert(all(out[2] == [4, 3, 2, 1])) 34 | # WARNING: In the current code, the memory associated with out[2a] 35 | # will be freed by Fortran on subsequent calls to the 36 | # function "test_extended". Copy for object permanance. 37 | out2 = fort.test_extended(10, known_opt_array_out=True) 38 | assert(all(out2[0] == [1,2,3])) 39 | assert(out2[1] == None) 40 | assert(all(out2[2] == list(reversed(range(11)[1:])))) 41 | # WARNING: Similar to before, the memory at out2[0] and out2[2] 42 | # will be freed by Fortran on subsequent calls to 43 | # "test_extended". Copies should be made for permanence. 44 | out3 = fort.test_extended(6, opt_alloc_array_out=True) 45 | assert(out3[0] == None) 46 | assert(all(out3[1] == [3,2,1])) 47 | assert(all(out3[2] == list(reversed(range(7)[1:])))) 48 | 49 | # End specific testing code. 50 | # --------------------------------------------------------------- 51 | print("passed", flush=True) 52 | import shutil 53 | shutil.rmtree(os.path.join(dir_name,f"test_{test_name}")) 54 | 55 | if __name__ == "__main__": 56 | test() 57 | -------------------------------------------------------------------------------- /fmodpy/test/complex128/test_complex128.f03: -------------------------------------------------------------------------------- 1 | ! Test Fortran COMPLEX wrapping and usage from Python with fmodpy. 2 | 3 | SUBROUTINE TEST_STANDARD(SING_IN, SING_OUT, ARRAY_IN, ARRAY_OUT,& 4 | KNOWN_ARRAY_OUT, KNOWN_MATRIX_OUT, OPT_SING_IN, OPT_SING_OUT) 5 | ! Test the basic functionaly of the 'COMPLEX' type and its 6 | ! interoperability with Python. This includes, inputs, outputs, 7 | ! array inputs with known and unknown size, optional inputs, and 8 | ! optional outputs. 9 | USE ISO_FORTRAN_ENV, ONLY: REAL64 10 | USE ISO_FORTRAN_ENV, ONLY: REAL64 11 | IMPLICIT NONE 12 | ! Argument definitions. 13 | COMPLEX(KIND=REAL64), INTENT(IN) :: SING_IN 14 | COMPLEX(KIND=REAL64), INTENT(OUT) :: SING_OUT 15 | COMPLEX(KIND=REAL64), DIMENSION(:), INTENT(IN) :: ARRAY_IN 16 | COMPLEX(KIND=REAL64), DIMENSION(:), INTENT(OUT) :: ARRAY_OUT 17 | COMPLEX(KIND=REAL64), DIMENSION(SIZE(ARRAY_OUT)), INTENT(OUT) :: KNOWN_ARRAY_OUT 18 | COMPLEX(KIND=REAL64), DIMENSION(3,SIZE(ARRAY_OUT)), INTENT(OUT) :: KNOWN_MATRIX_OUT 19 | COMPLEX(KIND=REAL64), INTENT(IN), OPTIONAL :: OPT_SING_IN 20 | COMPLEX(KIND=REAL64), INTENT(OUT), OPTIONAL :: OPT_SING_OUT 21 | ! Local variable. 22 | INTEGER :: I 23 | ! Copy the single input value to the single output value. 24 | SING_OUT = SING_IN + 1 25 | ! Copy as much of the input array as possible to the output array. 26 | ARRAY_OUT(1:MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))) = & 27 | &ARRAY_IN(1:MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))) 28 | DO I = MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))+1, SIZE(ARRAY_OUT) 29 | ARRAY_OUT(I) = I 30 | END DO 31 | DO I = 1, SIZE(KNOWN_MATRIX_OUT, 1) 32 | KNOWN_MATRIX_OUT(I,:) = I 33 | END DO 34 | ! Set the KNOWN_ARRAY and the KNOWN_MATRIX values to be identifiabl. 35 | DO I = 1,SIZE(ARRAY_OUT) 36 | KNOWN_ARRAY_OUT(I) = I 37 | KNOWN_MATRIX_OUT(:,I) = KNOWN_MATRIX_OUT(:,I) + I 38 | END DO 39 | ! Do some operations on the optional inputs / outputs. 40 | IF (PRESENT(OPT_SING_OUT)) THEN 41 | IF (PRESENT(OPT_SING_IN)) THEN 42 | OPT_SING_OUT = OPT_SING_IN 43 | ELSE 44 | OPT_SING_OUT = SING_IN 45 | END IF 46 | ELSE IF (PRESENT(OPT_SING_IN)) THEN 47 | SING_OUT = OPT_SING_IN 48 | END IF 49 | ! End of this subroutine. 50 | END SUBROUTINE TEST_STANDARD 51 | 52 | 53 | FUNCTION TEST_EXTENDED(OPT_ARRAY_IN, KNOWN_OPT_ARRAY_OUT,& 54 | & OPT_ALLOC_ARRAY_OUT, N ) RESULT(ALLOC_ARRAY_OUT) 55 | ! Test the extended functionaly of the 'COMPLEX' type and its 56 | ! interoperability with Python. This includes, optional array 57 | ! inputs, optional array outputs, and allocatable array outputs. 58 | USE ISO_FORTRAN_ENV, ONLY: REAL64 59 | IMPLICIT NONE 60 | COMPLEX(REAL64), INTENT(IN), OPTIONAL, DIMENSION(:) :: OPT_ARRAY_IN 61 | COMPLEX(REAL64), INTENT(OUT), OPTIONAL :: KNOWN_OPT_ARRAY_OUT(3) 62 | COMPLEX(REAL64), INTENT(OUT), OPTIONAL, ALLOCATABLE :: OPT_ALLOC_ARRAY_OUT(:) 63 | COMPLEX(REAL64), DIMENSION(:), ALLOCATABLE :: ALLOC_ARRAY_OUT 64 | INTEGER, INTENT(IN) :: N 65 | ! Local variable. 66 | INTEGER :: I 67 | 68 | ! Assign the optional array output values. 69 | IF (PRESENT(KNOWN_OPT_ARRAY_OUT)) THEN 70 | IF (PRESENT(OPT_ARRAY_IN)) THEN 71 | DO I = 1, MIN(SIZE(OPT_ARRAY_IN), SIZE(KNOWN_OPT_ARRAY_OUT)) 72 | KNOWN_OPT_ARRAY_OUT(I) = I 73 | END DO 74 | ELSE 75 | DO I = 1, SIZE(KNOWN_OPT_ARRAY_OUT) 76 | KNOWN_OPT_ARRAY_OUT(I) = I 77 | END DO 78 | END IF 79 | END IF 80 | 81 | ! Allocate the optional array output and assign its values. 82 | IF (PRESENT(OPT_ALLOC_ARRAY_OUT)) THEN 83 | ALLOCATE(OPT_ALLOC_ARRAY_OUT(1:N/2)) 84 | DO I = 1, SIZE(OPT_ALLOC_ARRAY_OUT) 85 | OPT_ALLOC_ARRAY_OUT(I) = SIZE(OPT_ALLOC_ARRAY_OUT) - (I-1) 86 | END DO 87 | END IF 88 | 89 | ! Allocate the required array output to the specified size. 90 | ALLOCATE(ALLOC_ARRAY_OUT(1:N)) 91 | DO I = 1, SIZE(ALLOC_ARRAY_OUT) 92 | ALLOC_ARRAY_OUT(I) = SIZE(ALLOC_ARRAY_OUT) - (I-1) 93 | END DO 94 | 95 | ! End of function. 96 | END FUNCTION TEST_EXTENDED 97 | -------------------------------------------------------------------------------- /fmodpy/test/complex256/__init__.py: -------------------------------------------------------------------------------- 1 | # Standardized testing interface. 2 | def test(): 3 | import os 4 | dir_name = os.path.dirname(os.path.abspath(__file__)) 5 | test_name = os.path.basename(dir_name) 6 | fort_file = os.path.join(dir_name, f"test_{test_name}.f03") 7 | build_dir = os.path.join(dir_name, f"fmodpy_{test_name}") 8 | print(f" {test_name}..", end=" ", flush=True) 9 | import fmodpy 10 | fort = fmodpy.fimport(fort_file, build_dir=build_dir, 11 | rebuild=True, wrap=False, verbose=True, 12 | output_dir=dir_name) 13 | # --------------------------------------------------------------- 14 | # Begin specific testing code. 15 | 16 | import numpy as np 17 | n = 10 18 | array_in = np.asarray(np.arange(n), dtype=np.complex256, order='F') 19 | array_out = np.zeros(n//2 - 1, dtype=np.complex256, order='F') 20 | sing_in = complex(7,0) 21 | out = fort.test_standard(sing_in, array_in, array_out) 22 | # Test the standard functionality. 23 | assert(out[0] == 8), out[0] 24 | assert(all(out[1] == [0,1,2,3])) 25 | assert(all(out[2] == [1,2,3,4])) 26 | assert(np.all(out[3] == [[2,3,4,5], 27 | [3,4,5,6], 28 | [4,5,6,7]])) 29 | assert(out[4] == None) 30 | # Test the extended functionality. 31 | out = fort.test_extended(4) 32 | assert(out[0] == None) 33 | assert(out[1] == None) 34 | assert(all(out[2] == [4, 3, 2, 1])) 35 | # WARNING: In the current code, the memory associated with out[2a] 36 | # will be freed by Fortran on subsequent calls to the 37 | # function "test_extended". Copy for object permanance. 38 | out2 = fort.test_extended(10, known_opt_array_out=True) 39 | assert(all(out2[0] == [1,2,3])) 40 | assert(out2[1] == None) 41 | assert(all(out2[2] == list(reversed(range(11)[1:])))) 42 | # WARNING: Similar to before, the memory at out2[0] and out2[2] 43 | # will be freed by Fortran on subsequent calls to 44 | # "test_extended". Copies should be made for permanence. 45 | out3 = fort.test_extended(6, opt_alloc_array_out=True) 46 | assert(out3[0] == None) 47 | assert(all(out3[1] == [3,2,1])) 48 | assert(all(out3[2] == list(reversed(range(7)[1:])))) 49 | 50 | # End specific testing code. 51 | # --------------------------------------------------------------- 52 | print("passed", flush=True) 53 | import shutil 54 | shutil.rmtree(os.path.join(dir_name,f"test_{test_name}")) 55 | 56 | if __name__ == "__main__": 57 | test() 58 | -------------------------------------------------------------------------------- /fmodpy/test/complex256/test_complex256.f03: -------------------------------------------------------------------------------- 1 | ! Test Fortran COMPLEX wrapping and usage from Python with fmodpy. 2 | 3 | SUBROUTINE TEST_STANDARD(SING_IN, SING_OUT, ARRAY_IN, ARRAY_OUT,& 4 | KNOWN_ARRAY_OUT, KNOWN_MATRIX_OUT, OPT_SING_IN, OPT_SING_OUT) 5 | ! Test the basic functionaly of the 'COMPLEX' type and its 6 | ! interoperability with Python. This includes, inputs, outputs, 7 | ! array inputs with known and unknown size, optional inputs, and 8 | ! optional outputs. 9 | USE ISO_FORTRAN_ENV, ONLY: REAL128 10 | USE ISO_FORTRAN_ENV, ONLY: REAL128 11 | IMPLICIT NONE 12 | ! Argument definitions. 13 | COMPLEX(KIND=REAL128), INTENT(IN) :: SING_IN 14 | COMPLEX(KIND=REAL128), INTENT(OUT) :: SING_OUT 15 | COMPLEX(KIND=REAL128), DIMENSION(:), INTENT(IN) :: ARRAY_IN 16 | COMPLEX(KIND=REAL128), DIMENSION(:), INTENT(OUT) :: ARRAY_OUT 17 | COMPLEX(KIND=REAL128), DIMENSION(SIZE(ARRAY_OUT)), INTENT(OUT) :: KNOWN_ARRAY_OUT 18 | COMPLEX(KIND=REAL128), DIMENSION(3,SIZE(ARRAY_OUT)), INTENT(OUT) :: KNOWN_MATRIX_OUT 19 | COMPLEX(KIND=REAL128), INTENT(IN), OPTIONAL :: OPT_SING_IN 20 | COMPLEX(KIND=REAL128), INTENT(OUT), OPTIONAL :: OPT_SING_OUT 21 | ! Local variable. 22 | INTEGER :: I 23 | PRINT *, '' 24 | PRINT *, 'SING_IN ', SING_IN 25 | PRINT *, 'SING_OUT', SING_OUT 26 | PRINT *, 'ARRAY_IN ', ARRAY_IN 27 | PRINT *, 'ARRAY_OUT', ARRAY_OUT 28 | ! Copy the single input value to the single output value. 29 | SING_OUT = SING_IN + 1 30 | ! Copy as much of the input array as possible to the output array. 31 | ARRAY_OUT(1:MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))) = & 32 | &ARRAY_IN(1:MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))) 33 | DO I = MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))+1, SIZE(ARRAY_OUT) 34 | ARRAY_OUT(I) = I 35 | END DO 36 | DO I = 1, SIZE(KNOWN_MATRIX_OUT, 1) 37 | KNOWN_MATRIX_OUT(I,:) = I 38 | END DO 39 | ! Set the KNOWN_ARRAY and the KNOWN_MATRIX values to be identifiabl. 40 | DO I = 1,SIZE(ARRAY_OUT) 41 | KNOWN_ARRAY_OUT(I) = I 42 | KNOWN_MATRIX_OUT(:,I) = KNOWN_MATRIX_OUT(:,I) + I 43 | END DO 44 | ! Do some operations on the optional inputs / outputs. 45 | IF (PRESENT(OPT_SING_OUT)) THEN 46 | IF (PRESENT(OPT_SING_IN)) THEN 47 | OPT_SING_OUT = OPT_SING_IN 48 | ELSE 49 | OPT_SING_OUT = SING_IN 50 | END IF 51 | ELSE IF (PRESENT(OPT_SING_IN)) THEN 52 | SING_OUT = OPT_SING_IN 53 | END IF 54 | ! End of this subroutine. 55 | PRINT *, '' 56 | PRINT *, 'SING_IN ', SING_IN 57 | PRINT *, 'SING_OUT', SING_OUT 58 | PRINT *, 'ARRAY_IN ', ARRAY_IN 59 | PRINT *, 'ARRAY_OUT', ARRAY_OUT 60 | END SUBROUTINE TEST_STANDARD 61 | 62 | 63 | FUNCTION TEST_EXTENDED(OPT_ARRAY_IN, KNOWN_OPT_ARRAY_OUT,& 64 | & OPT_ALLOC_ARRAY_OUT, N ) RESULT(ALLOC_ARRAY_OUT) 65 | ! Test the extended functionaly of the 'COMPLEX' type and its 66 | ! interoperability with Python. This includes, optional array 67 | ! inputs, optional array outputs, and allocatable array outputs. 68 | USE ISO_FORTRAN_ENV, ONLY: REAL128 69 | IMPLICIT NONE 70 | COMPLEX(KIND=REAL128), INTENT(IN), OPTIONAL, DIMENSION(:) :: OPT_ARRAY_IN 71 | COMPLEX(KIND=REAL128), INTENT(OUT), OPTIONAL :: KNOWN_OPT_ARRAY_OUT(3) 72 | COMPLEX(KIND=REAL128), INTENT(OUT), OPTIONAL, ALLOCATABLE :: OPT_ALLOC_ARRAY_OUT(:) 73 | COMPLEX(KIND=REAL128), DIMENSION(:), ALLOCATABLE :: ALLOC_ARRAY_OUT 74 | INTEGER, INTENT(IN) :: N 75 | ! Local variable. 76 | INTEGER :: I 77 | 78 | ! Assign the optional array output values. 79 | IF (PRESENT(KNOWN_OPT_ARRAY_OUT)) THEN 80 | IF (PRESENT(OPT_ARRAY_IN)) THEN 81 | DO I = 1, MIN(SIZE(OPT_ARRAY_IN), SIZE(KNOWN_OPT_ARRAY_OUT)) 82 | KNOWN_OPT_ARRAY_OUT(I) = I 83 | END DO 84 | ELSE 85 | DO I = 1, SIZE(KNOWN_OPT_ARRAY_OUT) 86 | KNOWN_OPT_ARRAY_OUT(I) = I 87 | END DO 88 | END IF 89 | END IF 90 | 91 | ! Allocate the optional array output and assign its values. 92 | IF (PRESENT(OPT_ALLOC_ARRAY_OUT)) THEN 93 | ALLOCATE(OPT_ALLOC_ARRAY_OUT(1:N/2)) 94 | DO I = 1, SIZE(OPT_ALLOC_ARRAY_OUT) 95 | OPT_ALLOC_ARRAY_OUT(I) = SIZE(OPT_ALLOC_ARRAY_OUT) - (I-1) 96 | END DO 97 | END IF 98 | 99 | ! Allocate the required array output to the specified size. 100 | ALLOCATE(ALLOC_ARRAY_OUT(1:N)) 101 | DO I = 1, SIZE(ALLOC_ARRAY_OUT) 102 | ALLOC_ARRAY_OUT(I) = SIZE(ALLOC_ARRAY_OUT) - (I-1) 103 | END DO 104 | 105 | ! End of function. 106 | END FUNCTION TEST_EXTENDED 107 | -------------------------------------------------------------------------------- /fmodpy/test/complex256/test_complex256/test_complex256_c_wrapper.f90: -------------------------------------------------------------------------------- 1 | ! This automatically generated Fortran wrapper file allows codes 2 | ! written in Fortran to be called directly from C and translates all 3 | ! C-style arguments into expected Fortran-style arguments (with 4 | ! assumed size, local type declarations, etc.). 5 | 6 | 7 | SUBROUTINE C_TEST_STANDARD(SING_IN, SING_OUT, ARRAY_IN_DIM_1, ARRAY_IN, ARRAY_OUT_DIM_1, ARRAY_OUT, KNOWN_ARRAY_OUT_DIM_1, KNOWN_AR& 8 | &RAY_OUT, KNOWN_MATRIX_OUT_DIM_1, KNOWN_MATRIX_OUT_DIM_2, KNOWN_MATRIX_OUT, OPT_SING_IN_PRESENT, OPT_SING_IN, OPT_SING_OUT_PRESENT,& 9 | & OPT_SING_OUT) BIND(C) 10 | USE ISO_FORTRAN_ENV , ONLY : REAL128 11 | USE ISO_FORTRAN_ENV , ONLY : REAL128 12 | IMPLICIT NONE 13 | COMPLEX(KIND=REAL128), INTENT(IN) :: SING_IN 14 | COMPLEX(KIND=REAL128), INTENT(OUT) :: SING_OUT 15 | INTEGER, INTENT(IN) :: ARRAY_IN_DIM_1 16 | COMPLEX(KIND=REAL128), INTENT(IN), DIMENSION(ARRAY_IN_DIM_1) :: ARRAY_IN 17 | INTEGER, INTENT(IN) :: ARRAY_OUT_DIM_1 18 | COMPLEX(KIND=REAL128), INTENT(OUT), DIMENSION(ARRAY_OUT_DIM_1) :: ARRAY_OUT 19 | INTEGER, INTENT(IN) :: KNOWN_ARRAY_OUT_DIM_1 20 | COMPLEX(KIND=REAL128), INTENT(OUT), DIMENSION(KNOWN_ARRAY_OUT_DIM_1) :: KNOWN_ARRAY_OUT 21 | INTEGER, INTENT(IN) :: KNOWN_MATRIX_OUT_DIM_1 22 | INTEGER, INTENT(IN) :: KNOWN_MATRIX_OUT_DIM_2 23 | COMPLEX(KIND=REAL128), INTENT(OUT), DIMENSION(KNOWN_MATRIX_OUT_DIM_1,KNOWN_MATRIX_OUT_DIM_2) :: KNOWN_MATRIX_OUT 24 | LOGICAL, INTENT(IN) :: OPT_SING_IN_PRESENT 25 | COMPLEX(KIND=REAL128), INTENT(IN) :: OPT_SING_IN 26 | LOGICAL, INTENT(IN) :: OPT_SING_OUT_PRESENT 27 | COMPLEX(KIND=REAL128), INTENT(OUT) :: OPT_SING_OUT 28 | 29 | INTERFACE 30 | SUBROUTINE TEST_STANDARD(SING_IN, SING_OUT, ARRAY_IN, ARRAY_OUT, KNOWN_ARRAY_OUT, KNOWN_MATRIX_OUT, OPT_SING_IN, OPT_SING_OUT) 31 | ! Test the basic functionaly of the 'COMPLEX' type and its 32 | ! interoperability with Python. This includes, inputs, outputs, 33 | ! array inputs with known and unknown size, optional inputs, and 34 | ! optional outputs. 35 | USE ISO_FORTRAN_ENV , ONLY : REAL128 36 | USE ISO_FORTRAN_ENV , ONLY : REAL128 37 | IMPLICIT NONE 38 | COMPLEX(KIND=REAL128), INTENT(IN) :: SING_IN 39 | COMPLEX(KIND=REAL128), INTENT(OUT) :: SING_OUT 40 | COMPLEX(KIND=REAL128), INTENT(IN), DIMENSION(:) :: ARRAY_IN 41 | COMPLEX(KIND=REAL128), INTENT(OUT), DIMENSION(:) :: ARRAY_OUT 42 | COMPLEX(KIND=REAL128), INTENT(OUT), DIMENSION(SIZE(ARRAY_OUT)) :: KNOWN_ARRAY_OUT 43 | COMPLEX(KIND=REAL128), INTENT(OUT), DIMENSION(3,SIZE(ARRAY_OUT)) :: KNOWN_MATRIX_OUT 44 | COMPLEX(KIND=REAL128), INTENT(IN), OPTIONAL :: OPT_SING_IN 45 | COMPLEX(KIND=REAL128), INTENT(OUT), OPTIONAL :: OPT_SING_OUT 46 | END SUBROUTINE TEST_STANDARD 47 | END INTERFACE 48 | 49 | IF (OPT_SING_IN_PRESENT) THEN 50 | IF (OPT_SING_OUT_PRESENT) THEN 51 | CALL TEST_STANDARD(SING_IN=SING_IN, SING_OUT=SING_OUT, ARRAY_IN=ARRAY_IN, ARRAY_OUT=ARRAY_OUT, KNOWN_ARRAY_OUT=KNOWN_ARRAY_OU& 52 | &T, KNOWN_MATRIX_OUT=KNOWN_MATRIX_OUT, OPT_SING_IN=OPT_SING_IN, OPT_SING_OUT=OPT_SING_OUT) 53 | ELSE 54 | CALL TEST_STANDARD(SING_IN=SING_IN, SING_OUT=SING_OUT, ARRAY_IN=ARRAY_IN, ARRAY_OUT=ARRAY_OUT, KNOWN_ARRAY_OUT=KNOWN_ARRAY_OU& 55 | &T, KNOWN_MATRIX_OUT=KNOWN_MATRIX_OUT, OPT_SING_IN=OPT_SING_IN) 56 | END IF 57 | ELSE 58 | IF (OPT_SING_OUT_PRESENT) THEN 59 | CALL TEST_STANDARD(SING_IN=SING_IN, SING_OUT=SING_OUT, ARRAY_IN=ARRAY_IN, ARRAY_OUT=ARRAY_OUT, KNOWN_ARRAY_OUT=KNOWN_ARRAY_OU& 60 | &T, KNOWN_MATRIX_OUT=KNOWN_MATRIX_OUT, OPT_SING_OUT=OPT_SING_OUT) 61 | ELSE 62 | CALL TEST_STANDARD(SING_IN=SING_IN, SING_OUT=SING_OUT, ARRAY_IN=ARRAY_IN, ARRAY_OUT=ARRAY_OUT, KNOWN_ARRAY_OUT=KNOWN_ARRAY_OU& 63 | &T, KNOWN_MATRIX_OUT=KNOWN_MATRIX_OUT) 64 | END IF 65 | END IF 66 | END SUBROUTINE C_TEST_STANDARD 67 | 68 | 69 | SUBROUTINE C_TEST_EXTENDED(OPT_ARRAY_IN_PRESENT, OPT_ARRAY_IN_DIM_1, OPT_ARRAY_IN, KNOWN_OPT_ARRAY_OUT_PRESENT, KNOWN_OPT_ARRAY_OUT& 70 | &_DIM_1, KNOWN_OPT_ARRAY_OUT, OPT_ALLOC_ARRAY_OUT_PRESENT, OPT_ALLOC_ARRAY_OUT_DIM_1, OPT_ALLOC_ARRAY_OUT, N, ALLOC_ARRAY_OUT_DIM_1& 71 | &, ALLOC_ARRAY_OUT) BIND(C) 72 | USE ISO_FORTRAN_ENV , ONLY : REAL128 73 | USE ISO_FORTRAN_ENV, ONLY: INT64 74 | IMPLICIT NONE 75 | LOGICAL, INTENT(IN) :: OPT_ARRAY_IN_PRESENT 76 | INTEGER, INTENT(IN) :: OPT_ARRAY_IN_DIM_1 77 | COMPLEX(KIND=REAL128), INTENT(IN), DIMENSION(OPT_ARRAY_IN_DIM_1) :: OPT_ARRAY_IN 78 | LOGICAL, INTENT(IN) :: KNOWN_OPT_ARRAY_OUT_PRESENT 79 | INTEGER, INTENT(IN) :: KNOWN_OPT_ARRAY_OUT_DIM_1 80 | COMPLEX(KIND=REAL128), INTENT(OUT), DIMENSION(KNOWN_OPT_ARRAY_OUT_DIM_1) :: KNOWN_OPT_ARRAY_OUT 81 | LOGICAL, INTENT(IN) :: OPT_ALLOC_ARRAY_OUT_PRESENT 82 | INTEGER, INTENT(OUT) :: OPT_ALLOC_ARRAY_OUT_DIM_1 83 | COMPLEX(KIND=REAL128), ALLOCATABLE, SAVE, DIMENSION(:) :: OPT_ALLOC_ARRAY_OUT_LOCAL 84 | INTEGER(KIND=INT64), INTENT(OUT) :: OPT_ALLOC_ARRAY_OUT 85 | INTEGER, INTENT(IN) :: N 86 | INTEGER, INTENT(OUT) :: ALLOC_ARRAY_OUT_DIM_1 87 | COMPLEX(KIND=REAL128), ALLOCATABLE, SAVE, DIMENSION(:) :: ALLOC_ARRAY_OUT_LOCAL 88 | INTEGER(KIND=INT64), INTENT(OUT) :: ALLOC_ARRAY_OUT 89 | 90 | INTERFACE 91 | FUNCTION TEST_EXTENDED(OPT_ARRAY_IN, KNOWN_OPT_ARRAY_OUT, OPT_ALLOC_ARRAY_OUT, N) RESULT(ALLOC_ARRAY_OUT) 92 | ! Test the extended functionaly of the 'COMPLEX' type and its 93 | ! interoperability with Python. This includes, optional array 94 | ! inputs, optional array outputs, and allocatable array outputs. 95 | USE ISO_FORTRAN_ENV , ONLY : REAL128 96 | IMPLICIT NONE 97 | COMPLEX(KIND=REAL128), INTENT(IN), OPTIONAL, DIMENSION(:) :: OPT_ARRAY_IN 98 | COMPLEX(KIND=REAL128), INTENT(OUT), OPTIONAL, DIMENSION(3) :: KNOWN_OPT_ARRAY_OUT 99 | COMPLEX(KIND=REAL128), INTENT(OUT), OPTIONAL, ALLOCATABLE, DIMENSION(:) :: OPT_ALLOC_ARRAY_OUT 100 | INTEGER, INTENT(IN) :: N 101 | COMPLEX(KIND=REAL128), ALLOCATABLE, DIMENSION(:) :: ALLOC_ARRAY_OUT 102 | END FUNCTION TEST_EXTENDED 103 | END INTERFACE 104 | 105 | IF (OPT_ARRAY_IN_PRESENT) THEN 106 | IF (KNOWN_OPT_ARRAY_OUT_PRESENT) THEN 107 | IF (OPT_ALLOC_ARRAY_OUT_PRESENT) THEN 108 | ALLOC_ARRAY_OUT_LOCAL = TEST_EXTENDED(N=N, OPT_ARRAY_IN=OPT_ARRAY_IN, KNOWN_OPT_ARRAY_OUT=KNOWN_OPT_ARRAY_OUT, OPT_ALLOC_AR& 109 | &RAY_OUT=OPT_ALLOC_ARRAY_OUT_LOCAL) 110 | OPT_ALLOC_ARRAY_OUT_DIM_1 = SIZE(OPT_ALLOC_ARRAY_OUT_LOCAL,1) 111 | OPT_ALLOC_ARRAY_OUT = LOC(OPT_ALLOC_ARRAY_OUT_LOCAL(1)) 112 | ELSE 113 | ALLOC_ARRAY_OUT_LOCAL = TEST_EXTENDED(N=N, OPT_ARRAY_IN=OPT_ARRAY_IN, KNOWN_OPT_ARRAY_OUT=KNOWN_OPT_ARRAY_OUT) 114 | OPT_ALLOC_ARRAY_OUT_DIM_1 = 0 115 | END IF 116 | ELSE 117 | IF (OPT_ALLOC_ARRAY_OUT_PRESENT) THEN 118 | ALLOC_ARRAY_OUT_LOCAL = TEST_EXTENDED(N=N, OPT_ARRAY_IN=OPT_ARRAY_IN, OPT_ALLOC_ARRAY_OUT=OPT_ALLOC_ARRAY_OUT_LOCAL) 119 | OPT_ALLOC_ARRAY_OUT_DIM_1 = SIZE(OPT_ALLOC_ARRAY_OUT_LOCAL,1) 120 | OPT_ALLOC_ARRAY_OUT = LOC(OPT_ALLOC_ARRAY_OUT_LOCAL(1)) 121 | ELSE 122 | ALLOC_ARRAY_OUT_LOCAL = TEST_EXTENDED(N=N, OPT_ARRAY_IN=OPT_ARRAY_IN) 123 | OPT_ALLOC_ARRAY_OUT_DIM_1 = 0 124 | END IF 125 | END IF 126 | ELSE 127 | IF (KNOWN_OPT_ARRAY_OUT_PRESENT) THEN 128 | IF (OPT_ALLOC_ARRAY_OUT_PRESENT) THEN 129 | ALLOC_ARRAY_OUT_LOCAL = TEST_EXTENDED(N=N, KNOWN_OPT_ARRAY_OUT=KNOWN_OPT_ARRAY_OUT, OPT_ALLOC_ARRAY_OUT=OPT_ALLOC_ARRAY_OUT& 130 | &_LOCAL) 131 | OPT_ALLOC_ARRAY_OUT_DIM_1 = SIZE(OPT_ALLOC_ARRAY_OUT_LOCAL,1) 132 | OPT_ALLOC_ARRAY_OUT = LOC(OPT_ALLOC_ARRAY_OUT_LOCAL(1)) 133 | ELSE 134 | ALLOC_ARRAY_OUT_LOCAL = TEST_EXTENDED(N=N, KNOWN_OPT_ARRAY_OUT=KNOWN_OPT_ARRAY_OUT) 135 | OPT_ALLOC_ARRAY_OUT_DIM_1 = 0 136 | END IF 137 | ELSE 138 | IF (OPT_ALLOC_ARRAY_OUT_PRESENT) THEN 139 | ALLOC_ARRAY_OUT_LOCAL = TEST_EXTENDED(N=N, OPT_ALLOC_ARRAY_OUT=OPT_ALLOC_ARRAY_OUT_LOCAL) 140 | OPT_ALLOC_ARRAY_OUT_DIM_1 = SIZE(OPT_ALLOC_ARRAY_OUT_LOCAL,1) 141 | OPT_ALLOC_ARRAY_OUT = LOC(OPT_ALLOC_ARRAY_OUT_LOCAL(1)) 142 | ELSE 143 | ALLOC_ARRAY_OUT_LOCAL = TEST_EXTENDED(N=N) 144 | OPT_ALLOC_ARRAY_OUT_DIM_1 = 0 145 | END IF 146 | END IF 147 | END IF 148 | 149 | ALLOC_ARRAY_OUT_DIM_1 = SIZE(ALLOC_ARRAY_OUT_LOCAL,1) 150 | ALLOC_ARRAY_OUT = LOC(ALLOC_ARRAY_OUT_LOCAL(1)) 151 | END SUBROUTINE C_TEST_EXTENDED 152 | 153 | -------------------------------------------------------------------------------- /fmodpy/test/complex256/test_complex256/test_complex256_python_wrapper.py: -------------------------------------------------------------------------------- 1 | '''This Python code is an automatically generated wrapper 2 | for Fortran code made by 'fmodpy'. The original documentation 3 | for the Fortran source code follows. 4 | 5 | ! Test Fortran COMPLEX wrapping and usage from Python with fmodpy. 6 | ''' 7 | 8 | import os 9 | import ctypes 10 | import numpy 11 | 12 | # -------------------------------------------------------------------- 13 | # CONFIGURATION 14 | # 15 | _verbose = True 16 | _fort_compiler = "gfortran" 17 | _shared_object_name = "test_complex256.so" 18 | _this_directory = os.path.dirname(os.path.abspath(__file__)) 19 | _path_to_lib = os.path.join(_this_directory, _shared_object_name) 20 | _compile_options = ['-fPIC', '-shared', '-O3'] 21 | _ordered_dependencies = ['test_complex256.f03', 'test_complex256_c_wrapper.f90'] 22 | # 23 | # -------------------------------------------------------------------- 24 | # AUTO-COMPILING 25 | # 26 | # Try to import the existing object. If that fails, recompile and then try. 27 | try: 28 | clib = ctypes.CDLL(_path_to_lib) 29 | except: 30 | # Remove the shared object if it exists, because it is faulty. 31 | if os.path.exists(_shared_object_name): 32 | os.remove(_shared_object_name) 33 | # Compile a new shared object. 34 | _command = " ".join([_fort_compiler] + _compile_options + ["-o", _shared_object_name] + _ordered_dependencies) 35 | if _verbose: 36 | print("Running system command with arguments") 37 | print(" ", _command) 38 | # Run the compilation command. 39 | import subprocess 40 | subprocess.run(_command, shell=True, cwd=_this_directory) 41 | # Import the shared object file as a C library with ctypes. 42 | clib = ctypes.CDLL(_path_to_lib) 43 | # -------------------------------------------------------------------- 44 | 45 | 46 | # This defines a C structure that can be used to hold the complex C type. 47 | class c_complex_double(ctypes.Structure): 48 | _fields_ = [("real", ctypes.c_longdouble), ("imag", ctypes.c_longdouble)] 49 | # Define an "__init__" that can take a complex number as input. 50 | def __init__(self, real=0.0, imag=None): 51 | # If the provided first value was a complex number, expand it out. 52 | if (hasattr(real, "imag") and hasattr(real, "real") and (imag is None)): 53 | imag = real.imag 54 | real = real.real 55 | # Assign the internal fields. 56 | self.real = real 57 | self.imag = imag 58 | # Define a "value" so that it can be cleanly retreived as a single complex number. 59 | @property 60 | def value(self): 61 | return complex(self.real, self.imag) 62 | 63 | 64 | 65 | # ---------------------------------------------- 66 | # Wrapper for the Fortran subroutine TEST_STANDARD 67 | 68 | def test_standard(sing_in, array_in, array_out, known_array_out=None, known_matrix_out=None, opt_sing_in=None, opt_sing_out=None): 69 | '''! Test the basic functionaly of the 'COMPLEX' type and its 70 | ! interoperability with Python. This includes, inputs, outputs, 71 | ! array inputs with known and unknown size, optional inputs, and 72 | ! optional outputs.''' 73 | 74 | # Setting up "sing_in" 75 | if (type(sing_in) is not c_complex_double): sing_in = c_complex_double(sing_in) 76 | 77 | # Setting up "sing_out" 78 | sing_out = c_complex_double() 79 | 80 | # Setting up "array_in" 81 | if ((not issubclass(type(array_in), numpy.ndarray)) or 82 | (not numpy.asarray(array_in).flags.f_contiguous) or 83 | (not (array_in.dtype == numpy.dtype(numpy.complex128)))): 84 | import warnings 85 | warnings.warn("The provided argument 'array_in' was not an f_contiguous NumPy array of type 'numpy.complex128' (or equivalent). Automatically converting (probably creating a full copy).") 86 | array_in = numpy.asarray(array_in, dtype=numpy.complex128, order='F') 87 | array_in_dim_1 = ctypes.c_int(array_in.shape[0]) 88 | 89 | # Setting up "array_out" 90 | if ((not issubclass(type(array_out), numpy.ndarray)) or 91 | (not numpy.asarray(array_out).flags.f_contiguous) or 92 | (not (array_out.dtype == numpy.dtype(numpy.complex128)))): 93 | import warnings 94 | warnings.warn("The provided argument 'array_out' was not an f_contiguous NumPy array of type 'numpy.complex128' (or equivalent). Automatically converting (probably creating a full copy).") 95 | array_out = numpy.asarray(array_out, dtype=numpy.complex128, order='F') 96 | array_out_dim_1 = ctypes.c_int(array_out.shape[0]) 97 | 98 | # Setting up "known_array_out" 99 | if (known_array_out is None): 100 | known_array_out = numpy.zeros(shape=(array_out.size), dtype=numpy.complex128, order='F') 101 | elif ((not issubclass(type(known_array_out), numpy.ndarray)) or 102 | (not numpy.asarray(known_array_out).flags.f_contiguous) or 103 | (not (known_array_out.dtype == numpy.dtype(numpy.complex128)))): 104 | import warnings 105 | warnings.warn("The provided argument 'known_array_out' was not an f_contiguous NumPy array of type 'numpy.complex128' (or equivalent). Automatically converting (probably creating a full copy).") 106 | known_array_out = numpy.asarray(known_array_out, dtype=numpy.complex128, order='F') 107 | known_array_out_dim_1 = ctypes.c_int(known_array_out.shape[0]) 108 | 109 | # Setting up "known_matrix_out" 110 | if (known_matrix_out is None): 111 | known_matrix_out = numpy.zeros(shape=(3, array_out.size), dtype=numpy.complex128, order='F') 112 | elif ((not issubclass(type(known_matrix_out), numpy.ndarray)) or 113 | (not numpy.asarray(known_matrix_out).flags.f_contiguous) or 114 | (not (known_matrix_out.dtype == numpy.dtype(numpy.complex128)))): 115 | import warnings 116 | warnings.warn("The provided argument 'known_matrix_out' was not an f_contiguous NumPy array of type 'numpy.complex128' (or equivalent). Automatically converting (probably creating a full copy).") 117 | known_matrix_out = numpy.asarray(known_matrix_out, dtype=numpy.complex128, order='F') 118 | known_matrix_out_dim_1 = ctypes.c_int(known_matrix_out.shape[0]) 119 | known_matrix_out_dim_2 = ctypes.c_int(known_matrix_out.shape[1]) 120 | 121 | # Setting up "opt_sing_in" 122 | opt_sing_in_present = ctypes.c_bool(True) 123 | if (opt_sing_in is None): 124 | opt_sing_in_present = ctypes.c_bool(False) 125 | opt_sing_in = c_complex_double() 126 | if (type(opt_sing_in) is not c_complex_double): opt_sing_in = c_complex_double(opt_sing_in) 127 | 128 | # Setting up "opt_sing_out" 129 | opt_sing_out_present = ctypes.c_bool(True) 130 | if (opt_sing_out is None): 131 | opt_sing_out_present = ctypes.c_bool(False) 132 | opt_sing_out = c_complex_double() 133 | 134 | print() 135 | print("sing_in ", sing_in) 136 | print("sing_out", sing_out) 137 | print("array_in ", array_in) 138 | print("array_out", array_out) 139 | 140 | # Call C-accessible Fortran wrapper. 141 | clib.c_test_standard(ctypes.byref(sing_in), ctypes.byref(sing_out), ctypes.byref(array_in_dim_1), ctypes.c_void_p(array_in.ctypes.data), ctypes.byref(array_out_dim_1), ctypes.c_void_p(array_out.ctypes.data), ctypes.byref(known_array_out_dim_1), ctypes.c_void_p(known_array_out.ctypes.data), ctypes.byref(known_matrix_out_dim_1), ctypes.byref(known_matrix_out_dim_2), ctypes.c_void_p(known_matrix_out.ctypes.data), ctypes.byref(opt_sing_in_present), ctypes.byref(opt_sing_in), ctypes.byref(opt_sing_out_present), ctypes.byref(opt_sing_out)) 142 | 143 | print() 144 | print("sing_in ", sing_in) 145 | print("sing_out", sing_out) 146 | print("array_in ", array_in) 147 | print("array_out", array_out) 148 | 149 | # Return final results, 'INTENT(OUT)' arguments only. 150 | return sing_out.value, array_out, known_array_out, known_matrix_out, (opt_sing_out.value if opt_sing_out_present else None) 151 | 152 | 153 | # ---------------------------------------------- 154 | # Wrapper for the Fortran subroutine TEST_EXTENDED 155 | 156 | def test_extended(n, opt_array_in=None, known_opt_array_out=None, opt_alloc_array_out=None): 157 | '''! Test the extended functionaly of the 'COMPLEX' type and its 158 | ! interoperability with Python. This includes, optional array 159 | ! inputs, optional array outputs, and allocatable array outputs.''' 160 | 161 | # Setting up "opt_array_in" 162 | opt_array_in_present = ctypes.c_bool(True) 163 | if (opt_array_in is None): 164 | opt_array_in_present = ctypes.c_bool(False) 165 | opt_array_in = numpy.zeros(shape=(1), dtype=numpy.complex128, order='F') 166 | elif (type(opt_array_in) == bool) and (opt_array_in): 167 | opt_array_in = numpy.zeros(shape=(1), dtype=numpy.complex128, order='F') 168 | elif ((not issubclass(type(opt_array_in), numpy.ndarray)) or 169 | (not numpy.asarray(opt_array_in).flags.f_contiguous) or 170 | (not (opt_array_in.dtype == numpy.dtype(numpy.complex128)))): 171 | import warnings 172 | warnings.warn("The provided argument 'opt_array_in' was not an f_contiguous NumPy array of type 'numpy.complex128' (or equivalent). Automatically converting (probably creating a full copy).") 173 | opt_array_in = numpy.asarray(opt_array_in, dtype=numpy.complex128, order='F') 174 | if (opt_array_in_present): 175 | opt_array_in_dim_1 = ctypes.c_int(opt_array_in.shape[0]) 176 | else: 177 | opt_array_in_dim_1 = ctypes.c_int() 178 | 179 | # Setting up "known_opt_array_out" 180 | known_opt_array_out_present = ctypes.c_bool(True) 181 | if (known_opt_array_out is None): 182 | known_opt_array_out_present = ctypes.c_bool(False) 183 | known_opt_array_out = numpy.zeros(shape=(1), dtype=numpy.complex128, order='F') 184 | elif (type(known_opt_array_out) == bool) and (known_opt_array_out): 185 | known_opt_array_out = numpy.zeros(shape=(3), dtype=numpy.complex128, order='F') 186 | elif ((not issubclass(type(known_opt_array_out), numpy.ndarray)) or 187 | (not numpy.asarray(known_opt_array_out).flags.f_contiguous) or 188 | (not (known_opt_array_out.dtype == numpy.dtype(numpy.complex128)))): 189 | import warnings 190 | warnings.warn("The provided argument 'known_opt_array_out' was not an f_contiguous NumPy array of type 'numpy.complex128' (or equivalent). Automatically converting (probably creating a full copy).") 191 | known_opt_array_out = numpy.asarray(known_opt_array_out, dtype=numpy.complex128, order='F') 192 | if (known_opt_array_out_present): 193 | known_opt_array_out_dim_1 = ctypes.c_int(known_opt_array_out.shape[0]) 194 | else: 195 | known_opt_array_out_dim_1 = ctypes.c_int() 196 | 197 | # Setting up "opt_alloc_array_out" 198 | opt_alloc_array_out_present = ctypes.c_bool(True) 199 | if (opt_alloc_array_out is None): 200 | opt_alloc_array_out_present = ctypes.c_bool(False) 201 | opt_alloc_array_out = numpy.zeros(shape=(0), dtype=numpy.complex128, order='F') 202 | elif (type(opt_alloc_array_out) == bool) and (opt_alloc_array_out): 203 | opt_alloc_array_out = numpy.zeros(shape=(1), dtype=numpy.complex128, order='F') 204 | elif ((not issubclass(type(opt_alloc_array_out), numpy.ndarray)) or 205 | (not numpy.asarray(opt_alloc_array_out).flags.f_contiguous) or 206 | (not (opt_alloc_array_out.dtype == numpy.dtype(numpy.complex128)))): 207 | import warnings 208 | warnings.warn("The provided argument 'opt_alloc_array_out' was not an f_contiguous NumPy array of type 'numpy.complex128' (or equivalent). Automatically converting (probably creating a full copy).") 209 | opt_alloc_array_out = numpy.asarray(opt_alloc_array_out, dtype=numpy.complex128, order='F') 210 | if (opt_alloc_array_out_present): 211 | opt_alloc_array_out_dim_1 = ctypes.c_int(opt_alloc_array_out.shape[0]) 212 | else: 213 | opt_alloc_array_out_dim_1 = ctypes.c_int() 214 | opt_alloc_array_out = ctypes.c_void_p(opt_alloc_array_out.ctypes.data) 215 | 216 | # Setting up "n" 217 | if (type(n) is not ctypes.c_int): n = ctypes.c_int(n) 218 | 219 | # Setting up "alloc_array_out" 220 | alloc_array_out = ctypes.c_void_p() 221 | alloc_array_out_dim_1 = ctypes.c_int() 222 | 223 | # Call C-accessible Fortran wrapper. 224 | clib.c_test_extended(ctypes.byref(opt_array_in_present), ctypes.byref(opt_array_in_dim_1), ctypes.c_void_p(opt_array_in.ctypes.data), ctypes.byref(known_opt_array_out_present), ctypes.byref(known_opt_array_out_dim_1), ctypes.c_void_p(known_opt_array_out.ctypes.data), ctypes.byref(opt_alloc_array_out_present), ctypes.byref(opt_alloc_array_out_dim_1), ctypes.byref(opt_alloc_array_out), ctypes.byref(n), ctypes.byref(alloc_array_out_dim_1), ctypes.byref(alloc_array_out)) 225 | 226 | # Post-processing "opt_alloc_array_out" 227 | opt_alloc_array_out_size = (opt_alloc_array_out_dim_1.value) 228 | if (opt_alloc_array_out_present) and (opt_alloc_array_out_size > 0): 229 | opt_alloc_array_out = numpy.array(ctypes.cast(opt_alloc_array_out, ctypes.POINTER(c_complex_double*opt_alloc_array_out_size)).contents, copy=False).view(numpy.complex128) 230 | elif (opt_alloc_array_out_size == 0): 231 | opt_alloc_array_out = numpy.zeros(opt_alloc_array_out_dim_1.value, dtype=numpy.complex128, order='F') 232 | else: 233 | opt_alloc_array_out = None 234 | 235 | # Post-processing "alloc_array_out" 236 | alloc_array_out_size = (alloc_array_out_dim_1.value) 237 | if (alloc_array_out_size > 0): 238 | alloc_array_out = numpy.array(ctypes.cast(alloc_array_out, ctypes.POINTER(c_complex_double*alloc_array_out_size)).contents, copy=False).view(numpy.complex128) 239 | elif (alloc_array_out_size == 0): 240 | alloc_array_out = numpy.zeros(alloc_array_out_dim_1.value, dtype=numpy.complex128, order='F') 241 | else: 242 | alloc_array_out = None 243 | 244 | # Return final results, 'INTENT(OUT)' arguments only. 245 | return (known_opt_array_out if known_opt_array_out_present else None), (opt_alloc_array_out if opt_alloc_array_out_present else None), alloc_array_out 246 | 247 | -------------------------------------------------------------------------------- /fmodpy/test/complex64/__init__.py: -------------------------------------------------------------------------------- 1 | # Standardized testing interface. 2 | def test(): 3 | import os 4 | dir_name = os.path.dirname(os.path.abspath(__file__)) 5 | test_name = os.path.basename(dir_name) 6 | fort_file = os.path.join(dir_name, f"test_{test_name}.f03") 7 | build_dir = os.path.join(dir_name, f"fmodpy_{test_name}") 8 | print(f" {test_name}..", end=" ", flush=True) 9 | import fmodpy 10 | fort = fmodpy.fimport(fort_file, build_dir=build_dir, 11 | output_dir=dir_name) 12 | # --------------------------------------------------------------- 13 | # Begin specific testing code. 14 | 15 | import numpy as np 16 | n = 10 17 | array_in = np.asarray(np.arange(n), dtype=np.complex64, order='F') 18 | array_out = np.zeros(n//2 - 1, dtype=np.complex64, order='F') 19 | sing_in = complex(7,0) 20 | out = fort.test_standard(sing_in, array_in, array_out) 21 | # Test the standard functionality. 22 | assert(out[0] == 8), out[0] 23 | assert(all(out[1] == [0,1,2,3])) 24 | assert(all(out[2] == [1,2,3,4])) 25 | assert(np.all(out[3] == [[2,3,4,5], 26 | [3,4,5,6], 27 | [4,5,6,7]])) 28 | assert(out[4] == None) 29 | # Test the extended functionality. 30 | out = fort.test_extended(4) 31 | assert(out[0] == None) 32 | assert(out[1] == None) 33 | assert(all(out[2] == [4, 3, 2, 1])) 34 | # WARNING: In the current code, the memory associated with out[2a] 35 | # will be freed by Fortran on subsequent calls to the 36 | # function "test_extended". Copy for object permanance. 37 | out2 = fort.test_extended(10, known_opt_array_out=True) 38 | assert(all(out2[0] == [1,2,3])) 39 | assert(out2[1] == None) 40 | assert(all(out2[2] == list(reversed(range(11)[1:])))) 41 | # WARNING: Similar to before, the memory at out2[0] and out2[2] 42 | # will be freed by Fortran on subsequent calls to 43 | # "test_extended". Copies should be made for permanence. 44 | out3 = fort.test_extended(6, opt_alloc_array_out=True) 45 | assert(out3[0] == None) 46 | assert(all(out3[1] == [3,2,1])) 47 | assert(all(out3[2] == list(reversed(range(7)[1:])))) 48 | 49 | # End specific testing code. 50 | # --------------------------------------------------------------- 51 | print("passed", flush=True) 52 | import shutil 53 | shutil.rmtree(os.path.join(dir_name,f"test_{test_name}")) 54 | 55 | if __name__ == "__main__": 56 | test() 57 | -------------------------------------------------------------------------------- /fmodpy/test/complex64/test_complex64.f03: -------------------------------------------------------------------------------- 1 | ! Test Fortran COMPLEX wrapping and usage from Python with fmodpy. 2 | 3 | SUBROUTINE TEST_STANDARD(SING_IN, SING_OUT, ARRAY_IN, ARRAY_OUT,& 4 | KNOWN_ARRAY_OUT, KNOWN_MATRIX_OUT, OPT_SING_IN, OPT_SING_OUT) 5 | ! Test the basic functionaly of the 'COMPLEX' type and its 6 | ! interoperability with Python. This includes, inputs, outputs, 7 | ! array inputs with known and unknown size, optional inputs, and 8 | ! optional outputs. 9 | USE ISO_FORTRAN_ENV, ONLY: REAL32 10 | USE ISO_FORTRAN_ENV, ONLY: REAL32 11 | IMPLICIT NONE 12 | ! Argument definitions. 13 | COMPLEX(KIND=REAL32), INTENT(IN) :: SING_IN 14 | COMPLEX(KIND=REAL32), INTENT(OUT) :: SING_OUT 15 | COMPLEX(KIND=REAL32), DIMENSION(:), INTENT(IN) :: ARRAY_IN 16 | COMPLEX(KIND=REAL32), DIMENSION(:), INTENT(OUT) :: ARRAY_OUT 17 | COMPLEX(KIND=REAL32), DIMENSION(SIZE(ARRAY_OUT)), INTENT(OUT) :: KNOWN_ARRAY_OUT 18 | COMPLEX(KIND=REAL32), DIMENSION(3,SIZE(ARRAY_OUT)), INTENT(OUT) :: KNOWN_MATRIX_OUT 19 | COMPLEX(KIND=REAL32), INTENT(IN), OPTIONAL :: OPT_SING_IN 20 | COMPLEX(KIND=REAL32), INTENT(OUT), OPTIONAL :: OPT_SING_OUT 21 | ! Local variable. 22 | INTEGER :: I 23 | ! Copy the single input value to the single output value. 24 | SING_OUT = SING_IN + 1 25 | ! Copy as much of the input array as possible to the output array. 26 | ARRAY_OUT(1:MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))) = & 27 | &ARRAY_IN(1:MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))) 28 | DO I = MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))+1, SIZE(ARRAY_OUT) 29 | ARRAY_OUT(I) = I 30 | END DO 31 | DO I = 1, SIZE(KNOWN_MATRIX_OUT, 1) 32 | KNOWN_MATRIX_OUT(I,:) = I 33 | END DO 34 | ! Set the KNOWN_ARRAY and the KNOWN_MATRIX values to be identifiabl. 35 | DO I = 1,SIZE(ARRAY_OUT) 36 | KNOWN_ARRAY_OUT(I) = I 37 | KNOWN_MATRIX_OUT(:,I) = KNOWN_MATRIX_OUT(:,I) + I 38 | END DO 39 | ! Do some operations on the optional inputs / outputs. 40 | IF (PRESENT(OPT_SING_OUT)) THEN 41 | IF (PRESENT(OPT_SING_IN)) THEN 42 | OPT_SING_OUT = OPT_SING_IN 43 | ELSE 44 | OPT_SING_OUT = SING_IN 45 | END IF 46 | ELSE IF (PRESENT(OPT_SING_IN)) THEN 47 | SING_OUT = OPT_SING_IN 48 | END IF 49 | ! End of this subroutine. 50 | END SUBROUTINE TEST_STANDARD 51 | 52 | 53 | FUNCTION TEST_EXTENDED(OPT_ARRAY_IN, KNOWN_OPT_ARRAY_OUT,& 54 | & OPT_ALLOC_ARRAY_OUT, N ) RESULT(ALLOC_ARRAY_OUT) 55 | ! Test the extended functionaly of the 'COMPLEX' type and its 56 | ! interoperability with Python. This includes, optional array 57 | ! inputs, optional array outputs, and allocatable array outputs. 58 | USE ISO_FORTRAN_ENV, ONLY: REAL32 59 | IMPLICIT NONE 60 | COMPLEX(REAL32), INTENT(IN), OPTIONAL, DIMENSION(:) :: OPT_ARRAY_IN 61 | COMPLEX(REAL32), INTENT(OUT), OPTIONAL :: KNOWN_OPT_ARRAY_OUT(3) 62 | COMPLEX(REAL32), INTENT(OUT), OPTIONAL, ALLOCATABLE :: OPT_ALLOC_ARRAY_OUT(:) 63 | COMPLEX(REAL32), DIMENSION(:), ALLOCATABLE :: ALLOC_ARRAY_OUT 64 | INTEGER, INTENT(IN) :: N 65 | ! Local variable. 66 | INTEGER :: I 67 | 68 | ! Assign the optional array output values. 69 | IF (PRESENT(KNOWN_OPT_ARRAY_OUT)) THEN 70 | IF (PRESENT(OPT_ARRAY_IN)) THEN 71 | DO I = 1, MIN(SIZE(OPT_ARRAY_IN), SIZE(KNOWN_OPT_ARRAY_OUT)) 72 | KNOWN_OPT_ARRAY_OUT(I) = I 73 | END DO 74 | ELSE 75 | DO I = 1, SIZE(KNOWN_OPT_ARRAY_OUT) 76 | KNOWN_OPT_ARRAY_OUT(I) = I 77 | END DO 78 | END IF 79 | END IF 80 | 81 | ! Allocate the optional array output and assign its values. 82 | IF (PRESENT(OPT_ALLOC_ARRAY_OUT)) THEN 83 | ALLOCATE(OPT_ALLOC_ARRAY_OUT(1:N/2)) 84 | DO I = 1, SIZE(OPT_ALLOC_ARRAY_OUT) 85 | OPT_ALLOC_ARRAY_OUT(I) = SIZE(OPT_ALLOC_ARRAY_OUT) - (I-1) 86 | END DO 87 | END IF 88 | 89 | ! Allocate the required array output to the specified size. 90 | ALLOCATE(ALLOC_ARRAY_OUT(1:N)) 91 | DO I = 1, SIZE(ALLOC_ARRAY_OUT) 92 | ALLOC_ARRAY_OUT(I) = SIZE(ALLOC_ARRAY_OUT) - (I-1) 93 | END DO 94 | 95 | ! End of function. 96 | END FUNCTION TEST_EXTENDED 97 | -------------------------------------------------------------------------------- /fmodpy/test/double_precision/__init__.py: -------------------------------------------------------------------------------- 1 | # Standardized testing interface. 2 | def test(): 3 | import os 4 | dir_name = os.path.dirname(os.path.abspath(__file__)) 5 | test_name = os.path.basename(dir_name) 6 | fort_file = os.path.join(dir_name, f"test_{test_name}.f03") 7 | build_dir = os.path.join(dir_name, f"fmodpy_{test_name}") 8 | print(f" {test_name}..", end=" ", flush=True) 9 | import fmodpy 10 | fort = fmodpy.fimport(fort_file, build_dir=build_dir, 11 | output_dir=dir_name) 12 | # --------------------------------------------------------------- 13 | # Begin specific testing code. 14 | 15 | import numpy as np 16 | n = 10 17 | array_in = np.asarray(np.arange(n), dtype=float, order='F') 18 | array_out = np.zeros(n//2 - 1, dtype=float, order='F') 19 | out = fort.test_standard(7, array_in, array_out) 20 | # Test the standard functionality. 21 | assert(out[0] == 8) 22 | assert(all(out[1] == [0,1,2,3])) 23 | assert(all(out[2] == [1,2,3,4])) 24 | assert(np.all(out[3] == [[2,3,4,5], 25 | [3,4,5,6], 26 | [4,5,6,7]])) 27 | assert(out[4] == None) 28 | # Test the extended functionality. 29 | out = fort.test_extended(4) 30 | assert(out[0] == None) 31 | assert(out[1] == None) 32 | assert(all(out[2] == [4, 3, 2, 1])) 33 | # WARNING: In the current code, the memory associated with out[2] 34 | # will be freed by Fortran on subsequent calls to the 35 | # function "test_extended". Copy for object permanance. 36 | out2 = fort.test_extended(10, known_opt_array_out=True) 37 | assert(all(out2[0] == [1,2,3])) 38 | assert(out2[1] == None) 39 | assert(all(out2[2] == list(reversed(range(11)[1:])))) 40 | # WARNING: Similar to before, the memory at out2[0] and out2[2] 41 | # will be freed by Fortran on subsequent calls to 42 | # "test_extended". Copies should be made for permanence. 43 | out3 = fort.test_extended(6, opt_alloc_array_out=True) 44 | assert(out3[0] == None) 45 | assert(all(out3[1] == [3,2,1])) 46 | assert(all(out3[2] == list(reversed(range(7)[1:])))) 47 | 48 | # End specific testing code. 49 | # --------------------------------------------------------------- 50 | print("passed", flush=True) 51 | import shutil 52 | shutil.rmtree(os.path.join(dir_name,f"test_{test_name}")) 53 | 54 | if __name__ == "__main__": 55 | test() 56 | -------------------------------------------------------------------------------- /fmodpy/test/double_precision/test_double_precision.f03: -------------------------------------------------------------------------------- 1 | ! Test Fortran REAL wrapping and usage from Python with fmodpy. 2 | 3 | SUBROUTINE TEST_STANDARD(SING_IN, SING_OUT, ARRAY_IN, ARRAY_OUT,& 4 | KNOWN_ARRAY_OUT, KNOWN_MATRIX_OUT, OPT_SING_IN, OPT_SING_OUT) 5 | ! Test the basic functionaly of the 'REAL' type and its 6 | ! interoperability with Python. This includes, inputs, outputs, 7 | ! array inputs with known and unknown size, optional inputs, and 8 | ! optional outputs. 9 | USE, INTRINSIC :: ISO_C_BINDING, ONLY: C_DOUBLE 10 | IMPLICIT NONE 11 | ! Argument definitions. 12 | REAL(KIND=C_DOUBLE), INTENT(IN) :: SING_IN 13 | REAL(KIND=C_DOUBLE), INTENT(OUT) :: SING_OUT 14 | REAL(KIND=C_DOUBLE), DIMENSION(:), INTENT(IN) :: ARRAY_IN 15 | REAL(KIND=C_DOUBLE), DIMENSION(:), INTENT(OUT) :: ARRAY_OUT 16 | REAL(KIND=C_DOUBLE), DIMENSION(SIZE(ARRAY_OUT)), INTENT(OUT) :: KNOWN_ARRAY_OUT 17 | REAL(KIND=C_DOUBLE), DIMENSION(3,SIZE(ARRAY_OUT)), INTENT(OUT) :: KNOWN_MATRIX_OUT 18 | REAL(KIND=C_DOUBLE), INTENT(IN), OPTIONAL :: OPT_SING_IN 19 | REAL(KIND=C_DOUBLE), INTENT(OUT), OPTIONAL :: OPT_SING_OUT 20 | ! Local variable. 21 | INTEGER :: I 22 | ! Copy the single input value to the single output value. 23 | SING_OUT = SING_IN + 1 24 | ! Copy as much of the input array as possible to the output array. 25 | ARRAY_OUT(1:MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))) = & 26 | &ARRAY_IN(1:MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))) 27 | DO I = MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))+1, SIZE(ARRAY_OUT) 28 | ARRAY_OUT(I) = I 29 | END DO 30 | DO I = 1, SIZE(KNOWN_MATRIX_OUT, 1) 31 | KNOWN_MATRIX_OUT(I,:) = I 32 | END DO 33 | ! Set the KNOWN_ARRAY and the KNOWN_MATRIX values to be identifiabl. 34 | DO I = 1,SIZE(ARRAY_OUT) 35 | KNOWN_ARRAY_OUT(I) = I 36 | KNOWN_MATRIX_OUT(:,I) = KNOWN_MATRIX_OUT(:,I) + I 37 | END DO 38 | ! Do some operations on the optional inputs / outputs. 39 | IF (PRESENT(OPT_SING_OUT)) THEN 40 | IF (PRESENT(OPT_SING_IN)) THEN 41 | OPT_SING_OUT = OPT_SING_IN 42 | ELSE 43 | OPT_SING_OUT = SING_IN 44 | END IF 45 | ELSE IF (PRESENT(OPT_SING_IN)) THEN 46 | SING_OUT = OPT_SING_IN 47 | END IF 48 | ! End of this subroutine. 49 | END SUBROUTINE TEST_STANDARD 50 | 51 | 52 | FUNCTION TEST_EXTENDED(OPT_ARRAY_IN, KNOWN_OPT_ARRAY_OUT,& 53 | & OPT_ALLOC_ARRAY_OUT, N ) RESULT(ALLOC_ARRAY_OUT) 54 | ! Test the extended functionaly of the 'REAL' type and its 55 | ! interoperability with Python. This includes, optional array 56 | ! inputs, optional array outputs, and allocatable array outputs. 57 | USE, INTRINSIC :: ISO_C_BINDING, ONLY: C_DOUBLE 58 | IMPLICIT NONE 59 | REAL(C_DOUBLE), INTENT(IN), OPTIONAL, DIMENSION(:) :: OPT_ARRAY_IN 60 | REAL(C_DOUBLE), INTENT(OUT), OPTIONAL :: KNOWN_OPT_ARRAY_OUT(3) 61 | REAL(C_DOUBLE), INTENT(OUT), OPTIONAL, ALLOCATABLE :: OPT_ALLOC_ARRAY_OUT(:) 62 | REAL(C_DOUBLE), DIMENSION(:), ALLOCATABLE :: ALLOC_ARRAY_OUT 63 | INTEGER, INTENT(IN) :: N 64 | ! Local variable. 65 | INTEGER :: I 66 | 67 | ! Assign the optional array output values. 68 | IF (PRESENT(KNOWN_OPT_ARRAY_OUT)) THEN 69 | IF (PRESENT(OPT_ARRAY_IN)) THEN 70 | DO I = 1, MIN(SIZE(OPT_ARRAY_IN), SIZE(KNOWN_OPT_ARRAY_OUT)) 71 | KNOWN_OPT_ARRAY_OUT(I) = I 72 | END DO 73 | ELSE 74 | DO I = 1, SIZE(KNOWN_OPT_ARRAY_OUT) 75 | KNOWN_OPT_ARRAY_OUT(I) = I 76 | END DO 77 | END IF 78 | END IF 79 | 80 | ! Allocate the optional array output and assign its values. 81 | IF (PRESENT(OPT_ALLOC_ARRAY_OUT)) THEN 82 | ALLOCATE(OPT_ALLOC_ARRAY_OUT(1:N/2)) 83 | DO I = 1, SIZE(OPT_ALLOC_ARRAY_OUT) 84 | OPT_ALLOC_ARRAY_OUT(I) = SIZE(OPT_ALLOC_ARRAY_OUT) - (I-1) 85 | END DO 86 | END IF 87 | 88 | ! Allocate the required array output to the specified size. 89 | ALLOCATE(ALLOC_ARRAY_OUT(1:N)) 90 | DO I = 1, SIZE(ALLOC_ARRAY_OUT) 91 | ALLOC_ARRAY_OUT(I) = SIZE(ALLOC_ARRAY_OUT) - (I-1) 92 | END DO 93 | 94 | ! End of function. 95 | END FUNCTION TEST_EXTENDED 96 | -------------------------------------------------------------------------------- /fmodpy/test/fixed_format/__init__.py: -------------------------------------------------------------------------------- 1 | # Standardized testing interface. 2 | def test(): 3 | import os 4 | dir_name = os.path.dirname(os.path.abspath(__file__)) 5 | test_name = os.path.basename(dir_name) 6 | fort_file = os.path.join(dir_name, f"test_{test_name}.f") 7 | build_dir = os.path.join(dir_name, f"fmodpy_{test_name}") 8 | print(f" {test_name}..", end=" ", flush=True) 9 | import fmodpy 10 | fort = fmodpy.fimport(fort_file, build_dir=build_dir, 11 | output_dir=dir_name) 12 | # --------------------------------------------------------------- 13 | # Begin specific testing code. 14 | import numpy as np 15 | a = 1 16 | b = 2 17 | c = 3 18 | d = 4 19 | aa = np.zeros((2,10), dtype="int32", order="F") 20 | ab = np.zeros((3,11), dtype="int32", order="F") 21 | ac = np.zeros((12), dtype="int32", order="F") 22 | ad = np.zeros((13), dtype="int32", order="F") 23 | ba = np.zeros((c,d), dtype="int32", order="F") 24 | bb = np.zeros((6,14), dtype="int32", order="F") 25 | # Call the function. 26 | a, b, c, d, aa, ab, ac, ad, ba, bb = fort.test_standard(a, b, c, d, aa, ab, ac, ad, ba, bb) 27 | # Test the standard functionality. 28 | assert(a == 1) 29 | assert(b == 2) 30 | assert(c == 3) 31 | assert(d == 4) 32 | assert(aa[1,2] == 1) 33 | assert(ab[1,2] == 2) 34 | assert(ac[1] == 3) 35 | assert(ad[1] == 4) 36 | assert(ba[1,2] == 5) 37 | # End specific testing code. 38 | # --------------------------------------------------------------- 39 | print("passed", flush=True) 40 | import shutil 41 | shutil.rmtree(os.path.join(dir_name,f"test_{test_name}")) 42 | 43 | if __name__ == "__main__": 44 | test() 45 | -------------------------------------------------------------------------------- /fmodpy/test/fixed_format/test_fixed_format.f: -------------------------------------------------------------------------------- 1 | SUBROUTINE TEST_STANDARD(A,B,C,D,AA,AB,AC,AD,BA,BB) 2 | * Comments. 3 | * Parameters 4 | INTEGER PA,PB 5 | PARAMETER (PA=0,PB=6) 6 | * .. Scalar Arguments 7 | INTEGER A,B,C,D 8 | * Array Arguments .. 9 | INTEGER AA(2,*),AB(3,*),AC(*),AD(*),BA(C,D), 10 | + BB(PB,*) 11 | * Code. 12 | IF (A.NE.1) RETURN 13 | AA(2,3) = 1 14 | AB(2,3) = 2 15 | AC(2) = 3 16 | AD(2) = 4 17 | BA(2,3) = 5 18 | BB(2,3) = 6 19 | RETURN 20 | END 21 | -------------------------------------------------------------------------------- /fmodpy/test/int32/__init__.py: -------------------------------------------------------------------------------- 1 | # Standardized testing interface. 2 | def test(): 3 | import os 4 | dir_name = os.path.dirname(os.path.abspath(__file__)) 5 | test_name = os.path.basename(dir_name) 6 | fort_file = os.path.join(dir_name, f"test_{test_name}.f03") 7 | build_dir = os.path.join(dir_name, f"fmodpy_{test_name}") 8 | print(f" {test_name}..", end=" ", flush=True) 9 | import fmodpy 10 | fort = fmodpy.fimport(fort_file, build_dir=build_dir, 11 | output_dir=dir_name) 12 | # --------------------------------------------------------------- 13 | # Begin specific testing code. 14 | 15 | import numpy as np 16 | n = 10 17 | array_in = np.asarray(np.arange(n), dtype="int32", order='F') 18 | array_out = np.zeros(n//2 - 1, dtype="int32", order='F') 19 | out = fort.test_standard(7, array_in, array_out) 20 | # Test the standard functionality. 21 | assert(out[0] == 8) 22 | assert(all(out[1] == [0,1,2,3])) 23 | assert(all(out[2] == [1,2,3,4])) 24 | assert(np.all(out[3] == [[2,3,4,5], 25 | [3,4,5,6], 26 | [4,5,6,7]])) 27 | assert(out[4] == None) 28 | # Test the extended functionality. 29 | out = fort.test_extended(4) 30 | assert(out[0] == None) 31 | assert(out[1] == None) 32 | assert(all(out[2] == [4, 3, 2, 1])) 33 | # WARNING: In the current code, the memory associated with out[2] 34 | # will be freed by Fortran on subsequent calls to the 35 | # function "test_extended". Copy for object permanance. 36 | out2 = fort.test_extended(10, known_opt_array_out=True) 37 | assert(all(out2[0] == [1,2,3])) 38 | assert(out2[1] == None) 39 | assert(all(out2[2] == list(reversed(range(11)[1:])))) 40 | # WARNING: Similar to before, the memory at out2[0] and out2[2] 41 | # will be freed by Fortran on subsequent calls to 42 | # "test_extended". Copies should be made for permanence. 43 | out3 = fort.test_extended(6, opt_alloc_array_out=True) 44 | assert(out3[0] == None) 45 | assert(all(out3[1] == [3,2,1])) 46 | assert(all(out3[2] == list(reversed(range(7)[1:])))) 47 | 48 | # End specific testing code. 49 | # --------------------------------------------------------------- 50 | print("passed", flush=True) 51 | import shutil 52 | shutil.rmtree(os.path.join(dir_name,f"test_{test_name}")) 53 | 54 | if __name__ == "__main__": 55 | test() 56 | -------------------------------------------------------------------------------- /fmodpy/test/int32/test_int32.f03: -------------------------------------------------------------------------------- 1 | ! Test Fortran INTEGER wrapping and usage from Python with fmodpy. 2 | 3 | SUBROUTINE TEST_STANDARD(SING_IN, SING_OUT, ARRAY_IN, ARRAY_OUT,& 4 | KNOWN_ARRAY_OUT, KNOWN_MATRIX_OUT, OPT_SING_IN, OPT_SING_OUT) 5 | ! Test the basic functionaly of the 'INTEGER' type and its 6 | ! interoperability with Python. This includes, inputs, outputs, 7 | ! array inputs with known and unknown size, optional inputs, and 8 | ! optional outputs. 9 | IMPLICIT NONE 10 | ! Argument definitions. 11 | INTEGER, INTENT(IN) :: SING_IN 12 | INTEGER, INTENT(OUT) :: SING_OUT 13 | INTEGER, DIMENSION(:), INTENT(IN) :: ARRAY_IN 14 | INTEGER, DIMENSION(:), INTENT(OUT) :: ARRAY_OUT 15 | INTEGER, DIMENSION(SIZE(ARRAY_OUT)), INTENT(OUT) :: KNOWN_ARRAY_OUT 16 | INTEGER, DIMENSION(3,SIZE(ARRAY_OUT)), INTENT(OUT) :: KNOWN_MATRIX_OUT 17 | INTEGER, INTENT(IN), OPTIONAL :: OPT_SING_IN 18 | INTEGER, INTENT(OUT), OPTIONAL :: OPT_SING_OUT 19 | ! Local variable. 20 | INTEGER :: I 21 | ! Copy the single input value to the single output value. 22 | SING_OUT = SING_IN + 1 23 | ! Copy as much of the input array as possible to the output array. 24 | ARRAY_OUT(1:MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))) = & 25 | &ARRAY_IN(1:MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))) 26 | DO I = MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))+1, SIZE(ARRAY_OUT) 27 | ARRAY_OUT(I) = I 28 | END DO 29 | DO I = 1, SIZE(KNOWN_MATRIX_OUT, 1) 30 | KNOWN_MATRIX_OUT(I,:) = I 31 | END DO 32 | ! Set the KNOWN_ARRAY and the KNOWN_MATRIX values to be identifiabl. 33 | DO I = 1,SIZE(ARRAY_OUT) 34 | KNOWN_ARRAY_OUT(I) = I 35 | KNOWN_MATRIX_OUT(:,I) = KNOWN_MATRIX_OUT(:,I) + I 36 | END DO 37 | ! Do some operations on the optional inputs / outputs. 38 | IF (PRESENT(OPT_SING_OUT)) THEN 39 | IF (PRESENT(OPT_SING_IN)) THEN 40 | OPT_SING_OUT = OPT_SING_IN 41 | ELSE 42 | OPT_SING_OUT = SING_IN 43 | END IF 44 | ELSE IF (PRESENT(OPT_SING_IN)) THEN 45 | SING_OUT = OPT_SING_IN 46 | END IF 47 | ! End of this subroutine. 48 | END SUBROUTINE TEST_STANDARD 49 | 50 | 51 | FUNCTION TEST_EXTENDED(OPT_ARRAY_IN, KNOWN_OPT_ARRAY_OUT,& 52 | & OPT_ALLOC_ARRAY_OUT, N ) RESULT(ALLOC_ARRAY_OUT) 53 | ! Test the extended functionaly of the 'INTEGER' type and its 54 | ! interoperability with Python. This includes, optional array 55 | ! inputs, optional array outputs, and allocatable array outputs. 56 | IMPLICIT NONE 57 | INTEGER, INTENT(IN), OPTIONAL, DIMENSION(:) :: OPT_ARRAY_IN 58 | INTEGER, INTENT(OUT), OPTIONAL :: KNOWN_OPT_ARRAY_OUT(3) 59 | INTEGER, INTENT(OUT), OPTIONAL, ALLOCATABLE :: OPT_ALLOC_ARRAY_OUT(:) 60 | INTEGER, DIMENSION(:), ALLOCATABLE :: ALLOC_ARRAY_OUT 61 | INTEGER, INTENT(IN) :: N 62 | ! Local variable. 63 | INTEGER :: I 64 | 65 | ! Assign the optional array output values. 66 | IF (PRESENT(KNOWN_OPT_ARRAY_OUT)) THEN 67 | IF (PRESENT(OPT_ARRAY_IN)) THEN 68 | DO I = 1, MIN(SIZE(OPT_ARRAY_IN), SIZE(KNOWN_OPT_ARRAY_OUT)) 69 | KNOWN_OPT_ARRAY_OUT(I) = I 70 | END DO 71 | ELSE 72 | DO I = 1, SIZE(KNOWN_OPT_ARRAY_OUT) 73 | KNOWN_OPT_ARRAY_OUT(I) = I 74 | END DO 75 | END IF 76 | END IF 77 | 78 | ! Allocate the optional array output and assign its values. 79 | IF (PRESENT(OPT_ALLOC_ARRAY_OUT)) THEN 80 | ALLOCATE(OPT_ALLOC_ARRAY_OUT(1:N/2)) 81 | DO I = 1, SIZE(OPT_ALLOC_ARRAY_OUT) 82 | OPT_ALLOC_ARRAY_OUT(I) = SIZE(OPT_ALLOC_ARRAY_OUT) - (I-1) 83 | END DO 84 | END IF 85 | 86 | ! Allocate the required array output to the specified size. 87 | ALLOCATE(ALLOC_ARRAY_OUT(1:N)) 88 | DO I = 1, SIZE(ALLOC_ARRAY_OUT) 89 | ALLOC_ARRAY_OUT(I) = SIZE(ALLOC_ARRAY_OUT) - (I-1) 90 | END DO 91 | 92 | ! End of function. 93 | END FUNCTION TEST_EXTENDED 94 | -------------------------------------------------------------------------------- /fmodpy/test/int64/__init__.py: -------------------------------------------------------------------------------- 1 | # Standardized testing interface. 2 | def test(): 3 | import os 4 | dir_name = os.path.dirname(os.path.abspath(__file__)) 5 | test_name = os.path.basename(dir_name) 6 | fort_file = os.path.join(dir_name, f"test_{test_name}.f03") 7 | build_dir = os.path.join(dir_name, f"fmodpy_{test_name}") 8 | print(f" {test_name}..", end=" ", flush=True) 9 | import fmodpy 10 | fort = fmodpy.fimport(fort_file, build_dir=build_dir, 11 | output_dir=dir_name) 12 | # --------------------------------------------------------------- 13 | # Begin specific testing code. 14 | 15 | import numpy as np 16 | n = 10 17 | array_in = np.asarray(np.arange(n), dtype=int, order='F') 18 | array_out = np.zeros(n//2 - 1, dtype=int, order='F') 19 | out = fort.test_standard(7, array_in, array_out) 20 | # Test the standard functionality. 21 | assert(out[0] == 8) 22 | assert(all(out[1] == [0,1,2,3])) 23 | assert(all(out[2] == [1,2,3,4])) 24 | assert(np.all(out[3] == [[2,3,4,5], 25 | [3,4,5,6], 26 | [4,5,6,7]])) 27 | assert(out[4] == None) 28 | # Test the extended functionality. 29 | out = fort.test_extended(4) 30 | assert(out[0] == None) 31 | assert(out[1] == None) 32 | assert(all(out[2] == [4, 3, 2, 1])) 33 | # WARNING: In the current code, the memory associated with out[2] 34 | # will be freed by Fortran on subsequent calls to the 35 | # function "test_extended". Copy for object permanance. 36 | out2 = fort.test_extended(10, known_opt_array_out=True) 37 | assert(all(out2[0] == [1,2,3])) 38 | assert(out2[1] == None) 39 | assert(all(out2[2] == list(reversed(range(11)[1:])))) 40 | # WARNING: Similar to before, the memory at out2[0] and out2[2] 41 | # will be freed by Fortran on subsequent calls to 42 | # "test_extended". Copies should be made for permanence. 43 | out3 = fort.test_extended(6, opt_alloc_array_out=True) 44 | assert(out3[0] == None) 45 | assert(all(out3[1] == [3,2,1])) 46 | assert(all(out3[2] == list(reversed(range(7)[1:])))) 47 | 48 | # Check that a function with an allocatable return works correctly. 49 | n = 13 50 | assert(tuple(fort.test_allocatable_return(n).tolist()) == tuple(reversed(list(range(n))))) 51 | 52 | # End specific testing code. 53 | # --------------------------------------------------------------- 54 | print("passed", flush=True) 55 | import shutil 56 | shutil.rmtree(os.path.join(dir_name,f"test_{test_name}")) 57 | 58 | if __name__ == "__main__": 59 | test() 60 | -------------------------------------------------------------------------------- /fmodpy/test/int64/test_int64.f03: -------------------------------------------------------------------------------- 1 | ! Test Fortran REAL wrapping and usage from Python with fmodpy. 2 | 3 | SUBROUTINE TEST_STANDARD(SING_IN, SING_OUT, ARRAY_IN, ARRAY_OUT,& 4 | KNOWN_ARRAY_OUT, KNOWN_MATRIX_OUT, OPT_SING_IN, OPT_SING_OUT) 5 | ! Test the basic functionaly of the 'REAL' type and its 6 | ! interoperability with Python. This includes, inputs, outputs, 7 | ! array inputs with known and unknown size, optional inputs, and 8 | ! optional outputs. 9 | USE ISO_FORTRAN_ENV, ONLY: INT64 10 | IMPLICIT NONE 11 | ! Argument definitions. 12 | INTEGER(KIND=INT64), INTENT(IN) :: SING_IN 13 | INTEGER(KIND=INT64), INTENT(OUT) :: SING_OUT 14 | INTEGER(KIND=INT64), DIMENSION(:), INTENT(IN) :: ARRAY_IN 15 | INTEGER(KIND=INT64), DIMENSION(:), INTENT(OUT) :: ARRAY_OUT 16 | INTEGER(KIND=INT64), DIMENSION(SIZE(ARRAY_OUT)), INTENT(OUT) :: KNOWN_ARRAY_OUT 17 | INTEGER(KIND=INT64), DIMENSION(3,SIZE(ARRAY_OUT)), INTENT(OUT) :: KNOWN_MATRIX_OUT 18 | INTEGER(KIND=INT64), INTENT(IN), OPTIONAL :: OPT_SING_IN 19 | INTEGER(KIND=INT64), INTENT(OUT), OPTIONAL :: OPT_SING_OUT 20 | ! Local variable. 21 | INTEGER :: I 22 | ! Copy the single input value to the single output value. 23 | SING_OUT = SING_IN + 1 24 | ! Copy as much of the input array as possible to the output array. 25 | ARRAY_OUT(1:MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))) = & 26 | &ARRAY_IN(1:MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))) 27 | DO I = MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))+1, SIZE(ARRAY_OUT) 28 | ARRAY_OUT(I) = I 29 | END DO 30 | DO I = 1, SIZE(KNOWN_MATRIX_OUT, 1) 31 | KNOWN_MATRIX_OUT(I,:) = I 32 | END DO 33 | ! Set the KNOWN_ARRAY and the KNOWN_MATRIX values to be identifiabl. 34 | DO I = 1,SIZE(ARRAY_OUT) 35 | KNOWN_ARRAY_OUT(I) = I 36 | KNOWN_MATRIX_OUT(:,I) = KNOWN_MATRIX_OUT(:,I) + I 37 | END DO 38 | ! Do some operations on the optional inputs / outputs. 39 | IF (PRESENT(OPT_SING_OUT)) THEN 40 | IF (PRESENT(OPT_SING_IN)) THEN 41 | OPT_SING_OUT = OPT_SING_IN 42 | ELSE 43 | OPT_SING_OUT = SING_IN 44 | END IF 45 | ELSE IF (PRESENT(OPT_SING_IN)) THEN 46 | SING_OUT = OPT_SING_IN 47 | END IF 48 | ! End of this subroutine. 49 | END SUBROUTINE TEST_STANDARD 50 | 51 | 52 | FUNCTION TEST_EXTENDED(OPT_ARRAY_IN, KNOWN_OPT_ARRAY_OUT,& 53 | & OPT_ALLOC_ARRAY_OUT, N ) RESULT(ALLOC_ARRAY_OUT) 54 | ! Test the extended functionaly of the 'REAL' type and its 55 | ! interoperability with Python. This includes, optional array 56 | ! inputs, optional array outputs, and allocatable array outputs. 57 | USE, INTRINSIC :: ISO_C_BINDING, ONLY: C_LONG 58 | IMPLICIT NONE 59 | INTEGER(C_LONG), INTENT(IN), OPTIONAL, DIMENSION(:) :: OPT_ARRAY_IN 60 | INTEGER(C_LONG), INTENT(OUT), OPTIONAL :: KNOWN_OPT_ARRAY_OUT(3) 61 | INTEGER(C_LONG), INTENT(OUT), OPTIONAL, ALLOCATABLE :: OPT_ALLOC_ARRAY_OUT(:) 62 | INTEGER(C_LONG), DIMENSION(:), ALLOCATABLE :: ALLOC_ARRAY_OUT 63 | INTEGER, INTENT(IN) :: N 64 | ! Local variable. 65 | INTEGER :: I 66 | 67 | ! Assign the optional array output values. 68 | IF (PRESENT(KNOWN_OPT_ARRAY_OUT)) THEN 69 | IF (PRESENT(OPT_ARRAY_IN)) THEN 70 | DO I = 1, MIN(SIZE(OPT_ARRAY_IN), SIZE(KNOWN_OPT_ARRAY_OUT)) 71 | KNOWN_OPT_ARRAY_OUT(I) = REAL(I) 72 | END DO 73 | ELSE 74 | DO I = 1, SIZE(KNOWN_OPT_ARRAY_OUT) 75 | KNOWN_OPT_ARRAY_OUT(I) = REAL(I) 76 | END DO 77 | END IF 78 | END IF 79 | 80 | ! Allocate the optional array output and assign its values. 81 | IF (PRESENT(OPT_ALLOC_ARRAY_OUT)) THEN 82 | ALLOCATE(OPT_ALLOC_ARRAY_OUT(1:N/2)) 83 | DO I = 1, SIZE(OPT_ALLOC_ARRAY_OUT) 84 | OPT_ALLOC_ARRAY_OUT(I) = SIZE(OPT_ALLOC_ARRAY_OUT) - (I-1) 85 | END DO 86 | END IF 87 | 88 | ! Allocate the required array output to the specified size. 89 | ALLOCATE(ALLOC_ARRAY_OUT(1:N)) 90 | DO I = 1, SIZE(ALLOC_ARRAY_OUT) 91 | ALLOC_ARRAY_OUT(I) = SIZE(ALLOC_ARRAY_OUT) - (I-1) 92 | END DO 93 | 94 | ! End of function. 95 | END FUNCTION TEST_EXTENDED 96 | 97 | 98 | ! Function returning allocatable. 99 | FUNCTION TEST_ALLOCATABLE_RETURN(N) 100 | USE ISO_FORTRAN_ENV, ONLY: INT64 101 | IMPLICIT NONE 102 | INTEGER(KIND=INT64), INTENT(IN) :: N 103 | INTEGER(KIND=INT64), ALLOCATABLE :: TEST_ALLOCATABLE_RETURN(:) 104 | INTEGER :: I 105 | ALLOCATE(TEST_ALLOCATABLE_RETURN(1:N)) 106 | DO I = 1, N 107 | TEST_ALLOCATABLE_RETURN(I) = N - I 108 | END DO 109 | END FUNCTION TEST_ALLOCATABLE_RETURN 110 | -------------------------------------------------------------------------------- /fmodpy/test/logical/__init__.py: -------------------------------------------------------------------------------- 1 | # Standardized testing interface. 2 | def test(): 3 | import os 4 | dir_name = os.path.dirname(os.path.abspath(__file__)) 5 | test_name = os.path.basename(dir_name) 6 | fort_file = os.path.join(dir_name, f"test_{test_name}.f03") 7 | build_dir = os.path.join(dir_name, f"fmodpy_{test_name}") 8 | print(f" {test_name}..", end=" ", flush=True) 9 | import fmodpy 10 | fort = fmodpy.fimport(fort_file, build_dir=build_dir, 11 | output_dir=dir_name, verbose=False, wrap=True, 12 | rebuild=True, show_warnings=False) 13 | # --------------------------------------------------------------- 14 | # Begin specific testing code. 15 | 16 | import ctypes 17 | import numpy as np 18 | a = np.array([True, False, True, False, True, False, True, False], dtype="int32") 19 | out = a.copy().astype(ctypes.c_bool) 20 | # Construct the answers (that are expected). 21 | b12 = np.array([(i+1)%3 == 0 for i in range(len(a))]) 22 | b3 = np.array([(i+1)%2 == 0 for i in range(len(a))]) 23 | assert(all(b12 == fort.test_simple_logical(a, b=out))) 24 | assert(all(b12 == fort.test_simple_logical(a, b=out, c=False))) 25 | assert(all(b3 == fort.test_simple_logical(a, b=out, c=True))) 26 | # End specific testing code. 27 | # --------------------------------------------------------------- 28 | print("passed", flush=True) 29 | import shutil 30 | shutil.rmtree(os.path.join(dir_name,f"test_{test_name}")) 31 | 32 | if __name__ == "__main__": 33 | test() 34 | -------------------------------------------------------------------------------- /fmodpy/test/logical/test_logical.f03: -------------------------------------------------------------------------------- 1 | 2 | SUBROUTINE TEST_SIMPLE_LOGICAL(A,C,B) 3 | USE ISO_C_BINDING, ONLY: C_BOOL 4 | LOGICAL, INTENT(IN), DIMENSION(:) :: A 5 | LOGICAL(KIND=C_BOOL), INTENT(IN), OPTIONAL :: C 6 | LOGICAL(KIND=C_BOOL), INTENT(OUT), DIMENSION(SIZE(A)) :: B 7 | ! Local 8 | INTEGER :: I 9 | B(:) = .TRUE. 10 | DO I = 1, SIZE(B) 11 | IF (PRESENT(C) .AND. (C)) THEN 12 | B(I) = (MOD(I,2) .EQ. 0) 13 | ELSE 14 | B(I) = (MOD(I,3) .EQ. 0) 15 | END IF 16 | END DO 17 | END SUBROUTINE TEST_SIMPLE_LOGICAL 18 | 19 | ! python3 -c "import fmodpy as f; f.fimport('simple_logical.f03', build_dir='.', verbose=True)" 20 | ! python3 -c "import og_fmodpy as f; f.wrap('simple_logical.f03', working_directory='og_fmodpy', verbose=True)" 21 | -------------------------------------------------------------------------------- /fmodpy/test/misc/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Standardized testing interface. 3 | def test(): 4 | import os 5 | import shutil 6 | dir_name = os.path.dirname(os.path.abspath(__file__)) 7 | test_name = os.path.basename(dir_name) 8 | build_dir = os.path.join(dir_name, f"fmodpy_{test_name}") 9 | import fmodpy 10 | # --------------------------------------------------------------- 11 | # Begin specific testing code. 12 | fort_file = os.path.join(dir_name, f"subroutine_with_type.f90") 13 | print(f" {os.path.basename(fort_file)}..", end=" ", flush=True) 14 | fort = fmodpy.fimport(fort_file, build_dir=build_dir, 15 | output_dir=dir_name, rebuild=True, 16 | ) 17 | assert (fort.one_up(1) == fort.FANCY(shirt=2.0, pants=3)), "Failed!" 18 | print("passed", flush=True) 19 | shutil.rmtree(os.path.join(dir_name,fort_file[:-4])) 20 | # End specific testing code. 21 | # --------------------------------------------------------------- 22 | 23 | 24 | if __name__ == "__main__": 25 | test() 26 | -------------------------------------------------------------------------------- /fmodpy/test/misc/implicit_shape.f90: -------------------------------------------------------------------------------- 1 | 2 | PURE FUNCTION TEST_SHAPES(a) RESULT(b) 3 | REAL, INTENT(IN), DIMENSION(:,:) :: a 4 | INTEGER :: b(SIZE(a,1), SIZE(a(1,:))) 5 | ! INTEGER :: b(SIZE(a,1), SIZE(a(1,3:4))) 6 | b(:,:) = INT(a(:,:)) 7 | END FUNCTION TEST_SHAPES 8 | -------------------------------------------------------------------------------- /fmodpy/test/misc/simple.f03: -------------------------------------------------------------------------------- 1 | SUBROUTINE TEST(N, OUT) 2 | ! Simple allocation test, allocate a real array of size N. 3 | INTEGER, INTENT(IN) :: N 4 | REAL, INTENT(OUT), ALLOCATABLE :: OUT(:) 5 | ! Local variables. 6 | INTEGER :: I 7 | ALLOCATE(OUT(1:I)) 8 | DO I = 1, N 9 | OUT(I) = I 10 | END DO 11 | END SUBROUTINE TEST 12 | -------------------------------------------------------------------------------- /fmodpy/test/misc/subroutine_with_type.f90: -------------------------------------------------------------------------------- 1 | 2 | subroutine one_up(person) bind(c) 3 | use iso_c_binding, only: c_double, c_int 4 | type, bind(c) :: fancy 5 | real(c_double) :: shirt 6 | integer(c_int) :: pants 7 | end type fancy 8 | type(fancy), intent(inout) :: person 9 | person%shirt = person%shirt + 1 10 | person%pants = person%pants + 2 11 | end subroutine one_up 12 | -------------------------------------------------------------------------------- /fmodpy/test/module/__init__.py: -------------------------------------------------------------------------------- 1 | # Standardized testing interface. 2 | def test(): 3 | import os 4 | dir_name = os.path.dirname(os.path.abspath(__file__)) 5 | test_name = os.path.basename(dir_name) 6 | fort_file = os.path.join(dir_name, f"test_{test_name}.f03") 7 | build_dir = os.path.join(dir_name, f"fmodpy_{test_name}") 8 | print(f" {test_name}..", end=" ", flush=True) 9 | import fmodpy 10 | fort = fmodpy.fimport(fort_file, build_dir=build_dir, 11 | output_dir=dir_name, rebuild=True, 12 | ) 13 | # --------------------------------------------------------------- 14 | # Begin specific testing code. 15 | 16 | import numpy as np 17 | n = 10 18 | array_test = np.asarray(np.arange(n), dtype="float32", order='F') 19 | 20 | # Assign some internal variables. 21 | # a_pub (float) 22 | fort.addition.a_pub = 1.5 23 | assert(fort.addition.a_pub == 1.5) 24 | fort.addition.a_pub = 2.5 25 | assert(fort.addition.a_pub == 2.5) 26 | 27 | # b_pub (integer) 28 | fort.addition.b_pub = 2 29 | assert(fort.addition.b_pub == 2) 30 | fort.addition.b_pub = 1 31 | assert(fort.addition.b_pub == 1) 32 | 33 | # Verify that the public subtraction routine works correctly. 34 | assert(3.5 == fort.addition.add_ab()) 35 | 36 | # a_vec_pub (real vector of size 10) 37 | import numpy as np 38 | a = np.asarray(np.random.random(size=(10,)), dtype=np.float32) 39 | # testing using the "get" before assignment 40 | fort.addition.a_vec_pub 41 | # test assignment. 42 | fort.addition.a_vec_pub = a 43 | assert(all(a == fort.addition.a_vec_pub)) 44 | # try assigning with a vector that is too small.. 45 | a = np.asarray(np.random.random(size=(8,)), dtype=np.float32) 46 | fort.addition.a_vec_pub = a 47 | assert(all(a == fort.addition.a_vec_pub[:len(a)])) 48 | # try assigning with a vector that is too big, might seg-fault.. 49 | a = np.asarray(np.random.random(size=(80,)), dtype=np.float32) 50 | fort.addition.a_vec_pub = a 51 | assert(all(a[:len(fort.addition.a_vec_pub)] == fort.addition.a_vec_pub)) 52 | # reassign a normal value. 53 | a = np.asarray(np.random.random(size=(10,)), dtype=np.float32) 54 | fort.addition.a_vec_pub = a 55 | 56 | # Make sure the vector is correctly negated internally and returned. 57 | result = fort.addition.sub_vecs() 58 | assert(all(result == -a)) 59 | 60 | # b_vec_pub (real vector of size 10) 61 | b = np.asarray(np.arange(4), dtype=np.float32) 62 | # testing using the "get" before assignment 63 | # test assignment. 64 | fort.addition.b_vec_pub = b 65 | assert(all(b == fort.addition.b_vec_pub)) 66 | # try assigning over top with a new vector 67 | b = np.asarray(np.arange(10), dtype=np.float32) 68 | fort.addition.b_vec_pub = b 69 | assert(all(b == fort.addition.b_vec_pub)) 70 | 71 | # Make sure the vectors are correctly subtracted internally and returned. 72 | result = fort.addition.sub_vecs() 73 | assert(all(b == result + a)) 74 | 75 | # TODO: check size of array, if large enough, warn about copy in SETTER 76 | # TODO: provide some protection against incorrectly sized array assignments 77 | 78 | # End specific testing code. 79 | # --------------------------------------------------------------- 80 | print("passed", flush=True) 81 | import shutil 82 | shutil.rmtree(os.path.join(dir_name,f"test_{test_name}")) 83 | 84 | if __name__ == "__main__": 85 | test() 86 | -------------------------------------------------------------------------------- /fmodpy/test/module/extra_module.f03: -------------------------------------------------------------------------------- 1 | MODULE EXTRA_CONSTANT 2 | USE CONSTANT, ONLY: C 3 | REAL, PARAMETER :: B = 1.0 4 | END MODULE EXTRA_CONSTANT 5 | 6 | MODULE REFERENCE_ADDITION 7 | USE ADDITION 8 | END MODULE REFERENCE_ADDITION 9 | -------------------------------------------------------------------------------- /fmodpy/test/module/test_module.f03: -------------------------------------------------------------------------------- 1 | ! This file contains a few modules, only ADDITION is relevant. 2 | 3 | MODULE NOTHING 4 | END MODULE NOTHING 5 | 6 | 7 | ! This file contains a single module ADDITION. 8 | MODULE CONSTANT 9 | REAL, PARAMETER :: C = 1.0 ! Comment should not mess this up 10 | INTEGER, PARAMETER :: D = 2, E = 3 ! Comment should not mess this up 11 | END MODULE CONSTANT 12 | 13 | 14 | ! This module tests out private and public attributes and subroutines. 15 | MODULE ADDITION 16 | USE EXTRA_CONSTANT, ONLY: B 17 | 18 | PRIVATE :: A_PR 19 | PRIVATE B_PR, NOTHING_PR 20 | REAL :: A_PR 21 | INTEGER :: B_PR 22 | 23 | PUBLIC 24 | REAL :: A_PUB 25 | INTEGER :: B_PUB 26 | REAL, DIMENSION(10) :: A_VEC_PUB 27 | REAL, DIMENSION(:), ALLOCATABLE :: B_VEC_PUB 28 | REAL, DIMENSION(:,:), ALLOCATABLE :: C_MAT_PUB 29 | 30 | CONTAINS 31 | 32 | SUBROUTINE NOTHING_PR() 33 | ! Private subroutine that does nothing. 34 | END SUBROUTINE NOTHING_PR 35 | 36 | SUBROUTINE ADD_AB(C) 37 | ! Compute C = A_PUB + B_PUB. 38 | REAL, INTENT(OUT) :: C 39 | C = A_PUB + REAL(B_PUB) 40 | END SUBROUTINE ADD_AB 41 | 42 | SUBROUTINE SUB_VECS(C_VEC) 43 | ! Compute C_VEC = B_VEC_PUB - A_VEC_PUB. 44 | REAL, INTENT(OUT), DIMENSION(SIZE(A_VEC_PUB)) :: C_VEC 45 | IF (ALLOCATED(B_VEC_PUB)) THEN 46 | C_VEC(:) = B_VEC_PUB(:) - A_VEC_PUB(:) 47 | ELSE 48 | C_VEC(:) = -A_VEC_PUB(:) 49 | END IF 50 | END SUBROUTINE SUB_VECS 51 | 52 | SUBROUTINE RETURN_ALLOC(A) 53 | REAL, INTENT(OUT), DIMENSION(:), ALLOCATABLE :: A 54 | END SUBROUTINE RETURN_ALLOC 55 | 56 | END MODULE ADDITION 57 | -------------------------------------------------------------------------------- /fmodpy/test/procedure/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Standardized testing interface. 4 | def test(): 5 | import os 6 | dir_name = os.path.dirname(os.path.abspath(__file__)) 7 | test_name = os.path.basename(dir_name) 8 | fort_file = os.path.join(dir_name, f"{test_name}.f03") 9 | build_dir = os.path.join(dir_name, f"fmodpy_{test_name}") 10 | print(f" {test_name}..", end=" ", flush=True) 11 | import fmodpy 12 | fort = fmodpy.fimport(fort_file, build_dir=build_dir, 13 | output_dir=dir_name, verbose=False, wrap=True, 14 | rebuild=True, show_warnings=False) 15 | # --------------------------------------------------------------- 16 | # Begin specific testing code. 17 | 18 | import numpy as np 19 | a = np.array([True, False, True, False, True, False, True, False], dtype=bool) 20 | temp = np.asarray(a, dtype='int32') 21 | out = temp.copy() 22 | assert(all(out == fort.test_simple_logical(temp, b=out, c=False))) 23 | assert(not any(fort.test_simple_logical(temp, b=out, c=True))) 24 | 25 | # End specific testing code. 26 | # --------------------------------------------------------------- 27 | print("passed", flush=True) 28 | 29 | 30 | if __name__ == "__main__": 31 | test() 32 | 33 | -------------------------------------------------------------------------------- /fmodpy/test/procedure/og_procedure/procedure.f03: -------------------------------------------------------------------------------- 1 | SUBROUTINE TEST_STANDARD(QUADRATIC, STEPS, SOLUTION, MAX_WINDOW) 2 | IMPLICIT NONE 3 | ! Minimize a quadratic by taking random 4 | INTEGER, INTENT(IN) :: STEPS 5 | REAL, INTENT(INOUT) :: SOLUTION 6 | REAL, INTENT(IN), OPTIONAL :: MAX_WINDOW 7 | INTERFACE 8 | FUNCTION QUADRATIC(X) RESULT(Y) 9 | REAL, INTENT(IN) :: X 10 | REAL :: Y 11 | END FUNCTION QUADRATIC 12 | END INTERFACE 13 | ! Local variables. 14 | REAL :: DEFAULT_WINDOW 15 | REAL :: W, MOVEMENT, QVALUE, UVALUE 16 | INTEGER :: I 17 | ! Set the default window. 18 | IF (PRESENT(MAX_WINDOW)) THEN ; DEFAULT_WINDOW = MAX_WINDOW 19 | ELSE ; DEFAULT_WINDOW = 8.0 20 | END IF 21 | ! Initialize the value of the function. 22 | QVALUE = QUADRATIC(SOLUTION) 23 | W = DEFAULT_WINDOW 24 | ! Search randomly. 25 | DO I = 1, STEPS 26 | ! Generate a random movement. 27 | CALL RANDOM_NUMBER(MOVEMENT) 28 | MOVEMENT = W * (MOVEMENT - 0.5) 29 | UVALUE = QUADRATIC(SOLUTION+MOVEMENT) 30 | ! If this is better, take the step and reset the search window. 31 | IF (UVALUE < QVALUE) THEN 32 | SOLUTION = SOLUTION + MOVEMENT 33 | QVALUE = UVALUE 34 | W = DEFAULT_WINDOW 35 | ! This is not better, shrink the search window (increase likely of success). 36 | ELSE ; W = W / 2.0 37 | END IF 38 | END DO 39 | END SUBROUTINE TEST_STANDARD 40 | -------------------------------------------------------------------------------- /fmodpy/test/procedure/og_procedure/procedure.pyx: -------------------------------------------------------------------------------- 1 | '''''' 2 | 3 | 4 | import cython 5 | import numpy 6 | 7 | class NotFortranCompatible(Exception): pass 8 | 9 | # Interface for fortran function quadratic 10 | # ================================================= 11 | 12 | cdef void c_quadratic(float* x, float* y): 13 | # Recall the python function from global 14 | # (this was assigned by cython during python wrapper call) 15 | global quadratic_global 16 | # Define any variables needed to copy arrays 17 | 18 | # Create local variables that are python types 19 | 20 | # Call function with python variables in and out 21 | y[0] = quadratic_global(x[0]) 22 | # Copy out from python variables back into c-typed (fortran) variables 23 | 24 | 25 | # Wrapper for fortran function test_standard 26 | # ================================================= 27 | 28 | cdef extern: 29 | void c_test_standard( void (*quadratic)(float*, float*), int* steps, float* solution, float* max_window, bint* max_window_present ) 30 | 31 | @cython.boundscheck(False) 32 | @cython.wraparound(False) 33 | def test_standard( quadratic, int steps, float solution, max_window=None ): 34 | '''! Minimize a quadratic by taking random''' 35 | # Prepare for fortran function call (initialize optionals) 36 | global quadratic_global 37 | quadratic_global = quadratic 38 | cdef bint max_window_present = True 39 | if (type(max_window) == type(None)): 40 | max_window_present = False 41 | max_window = 1 42 | cdef float local_max_window = max_window 43 | 44 | 45 | # Make fortran function call 46 | c_test_standard(&c_quadratic, &steps, &solution, &local_max_window, &max_window_present) 47 | # Return appropriate values based on "INTENT" from fortran code 48 | 49 | return solution 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /fmodpy/test/procedure/og_procedure/procedure_c_to_f.f90: -------------------------------------------------------------------------------- 1 | ! Module generated by fmodpy for fortran file named 'PROCEDURE' 2 | !===================================================================== 3 | MODULE PROCEDURE_WRAPPER 4 | ! Any necessary "USE" statements 5 | 6 | ! Make sure it is clear that no implicit types are used in this wrapper 7 | IMPLICIT NONE 8 | ! Any interfaces defined for "PROCEDURE" declarations 9 | 10 | 11 | ABSTRACT INTERFACE 12 | ! Fortran interface for declaring necessary "PROCEDURE" typed arguments 13 | SUBROUTINE QUADRATIC_INTERFACE( X, Y ) BIND(C) 14 | 15 | REAL, INTENT(IN) :: X 16 | REAL, INTENT(OUT) :: Y 17 | END SUBROUTINE QUADRATIC_INTERFACE 18 | END INTERFACE 19 | 20 | 21 | 22 | INTERFACE 23 | ! Fortran interface for passing assumed shapes to non-moduled functions. 24 | SUBROUTINE TEST_STANDARD( QUADRATIC, STEPS, SOLUTION, MAX_WINDOW & 25 | &) 26 | 27 | PROCEDURE(QUADRATIC_INTERFACE) :: QUADRATIC 28 | INTEGER, INTENT(IN) :: STEPS 29 | REAL, INTENT(INOUT) :: SOLUTION 30 | REAL, INTENT(IN), OPTIONAL :: MAX_WINDOW 31 | END SUBROUTINE TEST_STANDARD 32 | END INTERFACE 33 | 34 | 35 | 36 | CONTAINS 37 | ! Fortran wrapper for TEST_STANDARD, callable from C 38 | !======================================================== 39 | 40 | SUBROUTINE c_TEST_STANDARD( QUADRATIC, STEPS, SOLUTION, MAX_WINDOW,& 41 | & MAX_WINDOW_PRESENT ) BIND(c) 42 | ! This subroutine's only purpose is to call the source fortran 43 | ! code while also being accessible from C. 44 | PROCEDURE(QUADRATIC_INTERFACE) :: QUADRATIC 45 | INTEGER, INTENT(IN) :: STEPS 46 | REAL, INTENT(INOUT) :: SOLUTION 47 | LOGICAL, INTENT(IN) :: MAX_WINDOW_PRESENT 48 | REAL, INTENT(IN), OPTIONAL :: MAX_WINDOW 49 | ! Preparation code before function call (copying character arrays) 50 | 51 | ! Calling source fortran function 52 | IF (MAX_WINDOW_PRESENT) THEN 53 | CALL TEST_STANDARD(QUADRATIC, STEPS, SOLUTION, MAX_WINDOW=MAX_W& 54 | &INDOW) 55 | ELSE 56 | CALL TEST_STANDARD(QUADRATIC, STEPS, SOLUTION) 57 | ENDIF 58 | ! Copying out any necessary values for return (character arrays) 59 | 60 | END SUBROUTINE c_TEST_STANDARD 61 | 62 | END MODULE PROCEDURE_WRAPPER 63 | -------------------------------------------------------------------------------- /fmodpy/test/procedure/procedure.f03: -------------------------------------------------------------------------------- 1 | SUBROUTINE TEST_STANDARD(QUADRATIC, STEPS, SOLUTION, MAX_WINDOW) 2 | IMPLICIT NONE 3 | ! Minimize a quadratic by taking random 4 | INTEGER, INTENT(IN) :: STEPS 5 | REAL, INTENT(INOUT) :: SOLUTION 6 | REAL, INTENT(IN), OPTIONAL :: MAX_WINDOW 7 | INTERFACE 8 | FUNCTION QUADRATIC(X) RESULT(Y) 9 | REAL, INTENT(IN) :: X 10 | REAL :: Y 11 | END FUNCTION QUADRATIC 12 | END INTERFACE 13 | ! Local variables. 14 | REAL :: DEFAULT_WINDOW 15 | REAL :: W, MOVEMENT, QVALUE, UVALUE 16 | INTEGER :: I 17 | ! Set the default window. 18 | IF (PRESENT(MAX_WINDOW)) THEN ; DEFAULT_WINDOW = MAX_WINDOW 19 | ELSE ; DEFAULT_WINDOW = 8.0 20 | END IF 21 | ! Initialize the value of the function. 22 | QVALUE = QUADRATIC(SOLUTION) 23 | W = DEFAULT_WINDOW 24 | ! Search randomly. 25 | DO I = 1, STEPS 26 | ! Generate a random movement. 27 | CALL RANDOM_NUMBER(MOVEMENT) 28 | MOVEMENT = W * (MOVEMENT - 0.5) 29 | UVALUE = QUADRATIC(SOLUTION+MOVEMENT) 30 | ! If this is better, take the step and reset the search window. 31 | IF (UVALUE < QVALUE) THEN 32 | SOLUTION = SOLUTION + MOVEMENT 33 | QVALUE = UVALUE 34 | W = DEFAULT_WINDOW 35 | ! This is not better, shrink the search window (increase likely of success). 36 | ELSE ; W = W / 2.0 37 | END IF 38 | END DO 39 | END SUBROUTINE TEST_STANDARD 40 | -------------------------------------------------------------------------------- /fmodpy/test/real32/__init__.py: -------------------------------------------------------------------------------- 1 | # Standardized testing interface. 2 | def test(): 3 | import os 4 | dir_name = os.path.dirname(os.path.abspath(__file__)) 5 | test_name = os.path.basename(dir_name) 6 | fort_file = os.path.join(dir_name, f"test_{test_name}.f03") 7 | build_dir = os.path.join(dir_name, f"fmodpy_{test_name}") 8 | print(f" {test_name}..", end=" ", flush=True) 9 | import fmodpy 10 | fort = fmodpy.fimport(fort_file, build_dir=build_dir, 11 | output_dir=dir_name) 12 | # --------------------------------------------------------------- 13 | # Begin specific testing code. 14 | 15 | import numpy as np 16 | n = 10 17 | array_in = np.asarray(np.arange(n), dtype="float32", order='F') 18 | array_out = np.zeros(n//2 - 1, dtype="float32", order='F') 19 | out = fort.test_standard(7, array_in, array_out) 20 | # Test the standard functionality. 21 | assert(out[0] == 8) 22 | assert(all(out[1] == [0,1,2,3])) 23 | assert(all(out[2] == [1,2,3,4])) 24 | assert(np.all(out[3] == [[2,3,4,5], 25 | [3,4,5,6], 26 | [4,5,6,7]])) 27 | assert(out[4] == None) 28 | # Test the extended functionality. 29 | out = fort.test_extended(4) 30 | assert(out[0] == None) 31 | assert(out[1] == None) 32 | assert(all(out[2] == [4, 3, 2, 1])) 33 | # WARNING: In the current code, the memory associated with out[2] 34 | # will be freed by Fortran on subsequent calls to the 35 | # function "test_extended". Copy for object permanance. 36 | out2 = fort.test_extended(10, known_opt_array_out=True) 37 | assert(all(out2[0] == [1,2,3])) 38 | assert(out2[1] == None) 39 | assert(all(out2[2] == list(reversed(range(11)[1:])))) 40 | # WARNING: Similar to before, the memory at out2[0] and out2[2] 41 | # will be freed by Fortran on subsequent calls to 42 | # "test_extended". Copies should be made for permanence. 43 | out3 = fort.test_extended(6, opt_alloc_array_out=True) 44 | assert(out3[0] == None) 45 | assert(all(out3[1] == [3,2,1])) 46 | assert(all(out3[2] == list(reversed(range(7)[1:])))) 47 | 48 | # End specific testing code. 49 | # --------------------------------------------------------------- 50 | print("passed", flush=True) 51 | import shutil 52 | shutil.rmtree(os.path.join(dir_name,f"test_{test_name}")) 53 | 54 | if __name__ == "__main__": 55 | test() 56 | 57 | -------------------------------------------------------------------------------- /fmodpy/test/real32/test_real32.f03: -------------------------------------------------------------------------------- 1 | ! Test Fortran REAL wrapping and usage from Python with fmodpy. 2 | 3 | SUBROUTINE TEST_STANDARD(SING_IN, SING_OUT, ARRAY_IN, ARRAY_OUT,& 4 | KNOWN_ARRAY_OUT, KNOWN_MATRIX_OUT, OPT_SING_IN, OPT_SING_OUT) 5 | ! Test the basic functionaly of the 'REAL' type and its 6 | ! interoperability with Python. This includes, inputs, outputs, 7 | ! array inputs with known and unknown size, optional inputs, and 8 | ! optional outputs. 9 | IMPLICIT NONE 10 | ! Argument definitions. 11 | REAL, INTENT(IN) :: SING_IN 12 | REAL, INTENT(OUT) :: SING_OUT 13 | REAL, DIMENSION(:), INTENT(IN) :: ARRAY_IN 14 | REAL, DIMENSION(:), INTENT(OUT) :: ARRAY_OUT 15 | REAL, DIMENSION(SIZE(ARRAY_OUT)), INTENT(OUT) :: KNOWN_ARRAY_OUT 16 | REAL, DIMENSION(3,SIZE(ARRAY_OUT)), INTENT(OUT) :: KNOWN_MATRIX_OUT 17 | REAL, INTENT(IN), OPTIONAL :: OPT_SING_IN 18 | REAL, INTENT(OUT), OPTIONAL :: OPT_SING_OUT 19 | ! Local variable. 20 | INTEGER :: I 21 | ! Copy the single input value to the single output value. 22 | SING_OUT = SING_IN + 1 23 | ! Copy as much of the input array as possible to the output array. 24 | ARRAY_OUT(1:MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))) = & 25 | &ARRAY_IN(1:MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))) 26 | DO I = MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))+1, SIZE(ARRAY_OUT) 27 | ARRAY_OUT(I) = I 28 | END DO 29 | DO I = 1, SIZE(KNOWN_MATRIX_OUT, 1) 30 | KNOWN_MATRIX_OUT(I,:) = I 31 | END DO 32 | ! Set the KNOWN_ARRAY and the KNOWN_MATRIX values to be identifiabl. 33 | DO I = 1,SIZE(ARRAY_OUT) 34 | KNOWN_ARRAY_OUT(I) = I 35 | KNOWN_MATRIX_OUT(:,I) = KNOWN_MATRIX_OUT(:,I) + I 36 | END DO 37 | ! Do some operations on the optional inputs / outputs. 38 | IF (PRESENT(OPT_SING_OUT)) THEN 39 | IF (PRESENT(OPT_SING_IN)) THEN 40 | OPT_SING_OUT = OPT_SING_IN 41 | ELSE 42 | OPT_SING_OUT = SING_IN 43 | END IF 44 | ELSE IF (PRESENT(OPT_SING_IN)) THEN 45 | SING_OUT = OPT_SING_IN 46 | END IF 47 | ! End of this subroutine. 48 | END SUBROUTINE TEST_STANDARD 49 | 50 | 51 | FUNCTION TEST_EXTENDED(OPT_ARRAY_IN, KNOWN_OPT_ARRAY_OUT,& 52 | & OPT_ALLOC_ARRAY_OUT, N ) RESULT(ALLOC_ARRAY_OUT) 53 | ! Test the extended functionaly of the 'REAL' type and its 54 | ! interoperability with Python. This includes, optional array 55 | ! inputs, optional array outputs, and allocatable array outputs. 56 | IMPLICIT NONE 57 | REAL, INTENT(IN), OPTIONAL, DIMENSION(:) :: OPT_ARRAY_IN 58 | REAL, INTENT(OUT), OPTIONAL :: KNOWN_OPT_ARRAY_OUT(3) 59 | REAL, INTENT(OUT), OPTIONAL, ALLOCATABLE :: OPT_ALLOC_ARRAY_OUT(:) 60 | REAL, DIMENSION(:), ALLOCATABLE :: ALLOC_ARRAY_OUT 61 | INTEGER, INTENT(IN) :: N 62 | ! Local variable. 63 | INTEGER :: I 64 | 65 | ! Assign the optional array output values. 66 | IF (PRESENT(KNOWN_OPT_ARRAY_OUT)) THEN 67 | IF (PRESENT(OPT_ARRAY_IN)) THEN 68 | DO I = 1, MIN(SIZE(OPT_ARRAY_IN), SIZE(KNOWN_OPT_ARRAY_OUT)) 69 | KNOWN_OPT_ARRAY_OUT(I) = I 70 | END DO 71 | ELSE 72 | DO I = 1, SIZE(KNOWN_OPT_ARRAY_OUT) 73 | KNOWN_OPT_ARRAY_OUT(I) = I 74 | END DO 75 | END IF 76 | 77 | END IF 78 | 79 | ! Allocate the optional array output and assign its values. 80 | IF (PRESENT(OPT_ALLOC_ARRAY_OUT)) THEN 81 | ALLOCATE(OPT_ALLOC_ARRAY_OUT(1:N/2)) 82 | DO I = 1, SIZE(OPT_ALLOC_ARRAY_OUT) 83 | OPT_ALLOC_ARRAY_OUT(I) = SIZE(OPT_ALLOC_ARRAY_OUT) - (I-1) 84 | END DO 85 | 86 | END IF 87 | 88 | ! Allocate the required array output to the specified size. 89 | ALLOCATE(ALLOC_ARRAY_OUT(1:N)) 90 | DO I = 1, SIZE(ALLOC_ARRAY_OUT) 91 | ALLOC_ARRAY_OUT(I) = SIZE(ALLOC_ARRAY_OUT) - (I-1) 92 | END DO 93 | 94 | ! End of function. 95 | END FUNCTION TEST_EXTENDED 96 | -------------------------------------------------------------------------------- /fmodpy/test/real64/__init__.py: -------------------------------------------------------------------------------- 1 | # Standardized testing interface. 2 | def test(): 3 | import os 4 | dir_name = os.path.dirname(os.path.abspath(__file__)) 5 | test_name = os.path.basename(dir_name) 6 | fort_file = os.path.join(dir_name, f"test_{test_name}.f03") 7 | build_dir = os.path.join(dir_name, f"fmodpy_{test_name}") 8 | print(f" {test_name}..", end=" ", flush=True) 9 | import fmodpy 10 | fort = fmodpy.fimport(fort_file, build_dir=build_dir, 11 | output_dir=dir_name) 12 | # --------------------------------------------------------------- 13 | # Begin specific testing code. 14 | 15 | import numpy as np 16 | n = 10 17 | array_in = np.asarray(np.arange(n), dtype=float, order='F') 18 | array_out = np.zeros(n//2 - 1, dtype=float, order='F') 19 | out = fort.test_standard(7, array_in, array_out) 20 | # Test the standard functionality. 21 | assert(out[0] == 8) 22 | assert(all(out[1] == [0,1,2,3])) 23 | assert(all(out[2] == [1,2,3,4])) 24 | assert(np.all(out[3] == [[2,3,4,5], 25 | [3,4,5,6], 26 | [4,5,6,7]])) 27 | assert(out[4] == None) 28 | # Test the extended functionality. 29 | out = fort.test_extended(4) 30 | assert(out[0] == None) 31 | assert(out[1] == None) 32 | assert(all(out[2] == [4, 3, 2, 1])) 33 | # WARNING: In the current code, the memory associated with out[2] 34 | # will be freed by Fortran on subsequent calls to the 35 | # function "test_extended". Copy for object permanance. 36 | out2 = fort.test_extended(10, known_opt_array_out=True) 37 | assert(all(out2[0] == [1,2,3])) 38 | assert(out2[1] == None) 39 | assert(all(out2[2] == list(reversed(range(11)[1:])))) 40 | # WARNING: Similar to before, the memory at out2[0] and out2[2] 41 | # will be freed by Fortran on subsequent calls to 42 | # "test_extended". Copies should be made for permanence. 43 | out3 = fort.test_extended(6, opt_alloc_array_out=True) 44 | assert(out3[0] == None) 45 | assert(all(out3[1] == [3,2,1])) 46 | assert(all(out3[2] == list(reversed(range(7)[1:])))) 47 | 48 | # Ensure that the output type of the subroutine is as expected. 49 | out = fort.test_allocated_output_type() 50 | assert(out.dtype.itemsize == 8) 51 | assert(out.dtype == "float64") 52 | assert(np.allclose(out / np.arange(1,out.size+1), 3.14)) 53 | 54 | # End specific testing code. 55 | # --------------------------------------------------------------- 56 | print("passed", flush=True) 57 | import shutil 58 | shutil.rmtree(os.path.join(dir_name,f"test_{test_name}")) 59 | 60 | if __name__ == "__main__": 61 | test() 62 | -------------------------------------------------------------------------------- /fmodpy/test/real64/test_real64.f03: -------------------------------------------------------------------------------- 1 | ! Test Fortran REAL wrapping and usage from Python with fmodpy. 2 | 3 | SUBROUTINE TEST_STANDARD(SING_IN, SING_OUT, ARRAY_IN, ARRAY_OUT,& 4 | KNOWN_ARRAY_OUT, KNOWN_MATRIX_OUT, OPT_SING_IN, OPT_SING_OUT) 5 | ! Test the basic functionaly of the 'REAL' type and its 6 | ! interoperability with Python. This includes, inputs, outputs, 7 | ! array inputs with known and unknown size, optional inputs, and 8 | ! optional outputs. 9 | USE ISO_FORTRAN_ENV, ONLY: REAL64 10 | IMPLICIT NONE 11 | ! Argument definitions. 12 | REAL(KIND=REAL64), INTENT(IN) :: SING_IN 13 | REAL(KIND=REAL64), INTENT(OUT) :: SING_OUT 14 | REAL(KIND=REAL64), DIMENSION(:), INTENT(IN) :: ARRAY_IN 15 | REAL(KIND=REAL64), DIMENSION(:), INTENT(OUT) :: ARRAY_OUT 16 | REAL(KIND=REAL64), DIMENSION(SIZE(ARRAY_OUT)), INTENT(OUT) :: KNOWN_ARRAY_OUT 17 | REAL(KIND=REAL64), DIMENSION(3,SIZE(ARRAY_OUT)), INTENT(OUT) :: KNOWN_MATRIX_OUT 18 | REAL(KIND=REAL64), INTENT(IN), OPTIONAL :: OPT_SING_IN 19 | REAL(KIND=REAL64), INTENT(OUT), OPTIONAL :: OPT_SING_OUT 20 | ! Local variable. 21 | INTEGER :: I 22 | ! Copy the single input value to the single output value. 23 | SING_OUT = SING_IN + 1 24 | ! Copy as much of the input array as possible to the output array. 25 | ARRAY_OUT(1:MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))) = & 26 | &ARRAY_IN(1:MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))) 27 | DO I = MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))+1, SIZE(ARRAY_OUT) 28 | ARRAY_OUT(I) = I 29 | END DO 30 | DO I = 1, SIZE(KNOWN_MATRIX_OUT, 1) 31 | KNOWN_MATRIX_OUT(I,:) = I 32 | END DO 33 | ! Set the KNOWN_ARRAY and the KNOWN_MATRIX values to be identifiabl. 34 | DO I = 1,SIZE(ARRAY_OUT) 35 | KNOWN_ARRAY_OUT(I) = I 36 | KNOWN_MATRIX_OUT(:,I) = KNOWN_MATRIX_OUT(:,I) + I 37 | END DO 38 | ! Do some operations on the optional inputs / outputs. 39 | IF (PRESENT(OPT_SING_OUT)) THEN 40 | IF (PRESENT(OPT_SING_IN)) THEN 41 | OPT_SING_OUT = OPT_SING_IN 42 | ELSE 43 | OPT_SING_OUT = SING_IN 44 | END IF 45 | ELSE IF (PRESENT(OPT_SING_IN)) THEN 46 | SING_OUT = OPT_SING_IN 47 | END IF 48 | ! End of this subroutine. 49 | END SUBROUTINE TEST_STANDARD 50 | 51 | 52 | FUNCTION TEST_EXTENDED(OPT_ARRAY_IN, KNOWN_OPT_ARRAY_OUT,& 53 | & OPT_ALLOC_ARRAY_OUT, N ) RESULT(ALLOC_ARRAY_OUT) 54 | ! Test the extended functionaly of the 'REAL' type and its 55 | ! interoperability with Python. This includes, optional array 56 | ! inputs, optional array outputs, and allocatable array outputs. 57 | USE ISO_FORTRAN_ENV, ONLY: REAL64 58 | IMPLICIT NONE 59 | REAL(REAL64), INTENT(IN), OPTIONAL, DIMENSION(:) :: OPT_ARRAY_IN 60 | REAL(REAL64), INTENT(OUT), OPTIONAL :: KNOWN_OPT_ARRAY_OUT(3) 61 | REAL(REAL64), INTENT(OUT), OPTIONAL, ALLOCATABLE :: OPT_ALLOC_ARRAY_OUT(:) 62 | REAL(REAL64), DIMENSION(:), ALLOCATABLE :: ALLOC_ARRAY_OUT 63 | INTEGER, INTENT(IN) :: N 64 | ! Local variable. 65 | INTEGER :: I 66 | 67 | ! Assign the optional array output values. 68 | IF (PRESENT(KNOWN_OPT_ARRAY_OUT)) THEN 69 | IF (PRESENT(OPT_ARRAY_IN)) THEN 70 | DO I = 1, MIN(SIZE(OPT_ARRAY_IN), SIZE(KNOWN_OPT_ARRAY_OUT)) 71 | KNOWN_OPT_ARRAY_OUT(I) = I 72 | END DO 73 | ELSE 74 | DO I = 1, SIZE(KNOWN_OPT_ARRAY_OUT) 75 | KNOWN_OPT_ARRAY_OUT(I) = I 76 | END DO 77 | END IF 78 | END IF 79 | 80 | ! Allocate the optional array output and assign its values. 81 | IF (PRESENT(OPT_ALLOC_ARRAY_OUT)) THEN 82 | ALLOCATE(OPT_ALLOC_ARRAY_OUT(1:N/2)) 83 | DO I = 1, SIZE(OPT_ALLOC_ARRAY_OUT) 84 | OPT_ALLOC_ARRAY_OUT(I) = SIZE(OPT_ALLOC_ARRAY_OUT) - (I-1) 85 | END DO 86 | END IF 87 | 88 | ! Allocate the required array output to the specified size. 89 | ALLOCATE(ALLOC_ARRAY_OUT(1:N)) 90 | DO I = 1, SIZE(ALLOC_ARRAY_OUT) 91 | ALLOC_ARRAY_OUT(I) = SIZE(ALLOC_ARRAY_OUT) - (I-1) 92 | END DO 93 | 94 | ! End of function. 95 | END FUNCTION TEST_EXTENDED 96 | 97 | 98 | ! Test added for ISSUE#9 99 | ! https://github.com/tchlux/fmodpy/issues/9 100 | SUBROUTINE TEST_ALLOCATED_OUTPUT_TYPE(ARRAY_REAL64) 101 | USE ISO_FORTRAN_ENV, ONLY: REAL64 102 | implicit none 103 | INTEGER :: I 104 | REAL(KIND=REAL64), DIMENSION(:), ALLOCATABLE, INTENT(OUT) :: ARRAY_REAL64 105 | ALLOCATE (ARRAY_REAL64(4)) 106 | DO I = 1, SIZE(ARRAY_REAL64) 107 | ARRAY_REAL64(I) = 3.14*I 108 | END DO 109 | END SUBROUTINE TEST_ALLOCATED_OUTPUT_TYPE 110 | -------------------------------------------------------------------------------- /fmodpy/test/test.py: -------------------------------------------------------------------------------- 1 | # - make immutable INTENT(OUT) objects not part of input. 2 | # - make test cases (and necessary code) for: 3 | # real 4 | # inputs 5 | # outputs 6 | # array inputs 7 | # array outputs (known size) 8 | # array outputs (unknown size) 9 | # optional inputs 10 | # 11 | # optional array inputs 12 | # optional array outputs (known size) 13 | # optional array outputs (unknown size) 14 | # allocatable outputs 15 | # optional allocatable outputs 16 | # double precision 17 | # ^^ 18 | # integer 19 | # ^^ 20 | # logical 21 | # ^^ 22 | # character 23 | # ^^ 24 | # type 25 | # ^^ 26 | # undefined inputs 27 | # procedure inputs 28 | # POINTER attribute 29 | # modules with interfaces 30 | # modules with internal variables (define getter / setter functions) 31 | # modules with custom type declarations 32 | # type EXTENDS (copy the other type, add more definitions) 33 | # fixed format Fortran inputs (Fortran 77) 34 | # 35 | # 36 | # "Module" should be put inside a Python class. Then I can define 37 | # proper "getter" and "setter" behaviors for internal attributes. 38 | # 39 | 40 | 41 | def run_tests(): 42 | from fmodpy.config import load_config 43 | load_config(verbose=False, verbose_module=False, wrap=True, rebuild=True) 44 | 45 | # Import all of the tests. 46 | from fmodpy.test.character import test as test_character 47 | from fmodpy.test.real32 import test as test_real32 48 | from fmodpy.test.real64 import test as test_real64 49 | from fmodpy.test.double_precision import test as test_double_precision 50 | from fmodpy.test.int32 import test as test_int32 51 | from fmodpy.test.int64 import test as test_int64 52 | from fmodpy.test.logical import test as test_logical 53 | from fmodpy.test.module import test as test_module 54 | from fmodpy.test.complex64 import test as test_complex64 55 | from fmodpy.test.complex128 import test as test_complex128 56 | from fmodpy.test.type import test as test_type 57 | from fmodpy.test.misc import test as test_misc 58 | from fmodpy.test.fixed_format import test as test_fixed_format 59 | 60 | # Run all of the tests. 61 | test_character() 62 | test_real32() 63 | test_real64() 64 | test_double_precision() 65 | test_int32() 66 | test_int64() 67 | test_logical() 68 | test_module() 69 | test_complex64() 70 | test_complex128() 71 | test_type() 72 | test_misc() 73 | test_fixed_format() 74 | 75 | 76 | if __name__ == "__main__": 77 | # /Users/thomaslux/Library/Python/3.7/bin/pprofile 78 | # 79 | # Activate a code coverage module. 80 | # import coverage, os 81 | # this_dir = os.path.dirname(os.path.abspath(__file__)) 82 | # module_dir = os.path.dirname(this_dir) 83 | # omitted = ([this_dir] + [os.path.join(this_dir,f) for f in os.listdir(this_dir)] 84 | # + [os.path.join(this_dir,f)+"*" for f in os.listdir(this_dir) 85 | # if os.path.isdir(os.path.join(this_dir,f))]) 86 | # print("Omitted:") 87 | # print(omitted) 88 | # cov = coverage.Coverage(source=[module_dir], omit=omitted) 89 | # cov.start() 90 | 91 | # Run the tests on `fmodpy`. 92 | run_tests() 93 | 94 | exit() 95 | 96 | print() 97 | print("Generating code coverage report..", flush=True) 98 | # Save data from the coverage tracking (for report). 99 | cov.stop() 100 | cov.save() 101 | print(" (will host local webserver for 10 minutes or until killed)", flush=True) 102 | # Create a temporary directory for holding test results. 103 | from tempfile import TemporaryDirectory 104 | temp_dir = TemporaryDirectory() 105 | results_dir = temp_dir.name 106 | cov.html_report(directory=results_dir) 107 | # Open the results file. 108 | import webbrowser 109 | webbrowser.open("file://"+os.path.join(results_dir, "index.html")) 110 | # Wait for load, then delete the temporary directory. 111 | # (user might decide to kill this process early). 112 | import time 113 | time.sleep(60*10) # <- Default to 10 minutes of waiting. 114 | temp_dir.cleanup() 115 | del temp_dir 116 | -------------------------------------------------------------------------------- /fmodpy/test/type/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | # Standardized testing interface. 3 | def test(): 4 | import os 5 | dir_name = os.path.dirname(os.path.abspath(__file__)) 6 | test_name = os.path.basename(dir_name) 7 | fort_file = os.path.join(dir_name, f"test_{test_name}.f03") 8 | build_dir = os.path.join(dir_name, f"fmodpy_{test_name}") 9 | print(f" {test_name}..", end=" ", flush=True) 10 | import fmodpy 11 | fort = fmodpy.fimport(fort_file, build_dir=build_dir, 12 | dependencies=["test_type_aux.f03"], 13 | output_dir=dir_name, rebuild=True, 14 | verbose=False 15 | ) 16 | # --------------------------------------------------------------- 17 | # Begin specific testing code. 18 | 19 | t1 = fort.ftypes.T1() 20 | t2 = fort.ftypes.copy_t1_to_t2(t1) 21 | assert((t1.a == t2.a) and (t1.b == t2.b)) 22 | 23 | import numpy as np 24 | n = 10 25 | array_in = np.asarray(np.arange(n), dtype=fort.ftypes.T1, order='F') 26 | array_out = np.zeros(n//2 - 1, dtype=fort.T2, order='F') 27 | out = fort.test_standard(7, array_in, array_out) 28 | # Test the standard functionality. 29 | assert(out[0].a == out[0].b == 8) 30 | assert(all(out[1]["a"] == [0,1,2,3])) 31 | assert(all(out[1]["b"] == [0,1,2,3])) 32 | assert(all(out[2]["a"] == [1,2,3,4])) 33 | assert(all(out[2]["b"] == [1,2,3,4])) 34 | assert(np.all(out[3]["a"] == [[2,3,4,5], 35 | [3,4,5,6], 36 | [4,5,6,7]])) 37 | assert(np.all(out[3]["b"] == [[2,3,4,5], 38 | [3,4,5,6], 39 | [4,5,6,7]])) 40 | assert(out[4] == None) 41 | # Test the extended functionality. 42 | out = fort.test_extended(4) 43 | assert(out[0] == None) 44 | assert(out[1] == None) 45 | assert(all(out[2]["a"] == [4, 3, 2, 1])) 46 | assert(all(out[2]["b"] == [4, 3, 2, 1])) 47 | # WARNING: In the current code, the memory associated with out[2] 48 | # will be freed by Fortran on subsequent calls to the 49 | # function "test_extended". Copy for object permanance. 50 | out2 = fort.test_extended(10, known_opt_array_out=True) 51 | assert(all(out2[0]["a"] == [1,2,3])) 52 | assert(all(out2[0]["b"] == [1,2,3])) 53 | assert(out2[1] == None) 54 | assert(all(out2[2]["a"] == list(reversed(range(11)[1:])))) 55 | assert(all(out2[2]["b"] == list(reversed(range(11)[1:])))) 56 | # WARNING: Similar to before, the memory at out2[0] and out2[2] 57 | # will be freed by Fortran on subsequent calls to 58 | # "test_extended". Copies should be made for permanence. 59 | out3 = fort.test_extended(6, opt_alloc_array_out=True) 60 | assert(out3[0] == None) 61 | assert(all(out3[1]["a"] == [3,2,1])) 62 | assert(all(out3[1]["b"] == [3,2,1])) 63 | assert(all(out3[2]["a"] == list(reversed(range(7)[1:])))) 64 | assert(all(out3[2]["b"] == list(reversed(range(7)[1:])))) 65 | 66 | 67 | # Test a code that uses an auxilliary type (in another file). 68 | res = fort.test_return_aux() 69 | assert(res.a == 1) 70 | assert(res.b == 2.0) 71 | assert(res.c == '3'.encode()) 72 | 73 | # End specific testing code. 74 | # --------------------------------------------------------------- 75 | print("passed", flush=True) 76 | import shutil 77 | shutil.rmtree(os.path.join(dir_name,f"test_{test_name}")) 78 | 79 | 80 | if __name__ == "__main__": 81 | test() 82 | -------------------------------------------------------------------------------- /fmodpy/test/type/derived_type_error.f03: -------------------------------------------------------------------------------- 1 | ! Define a simple subroutine that copies a derived type from A to B. 2 | SUBROUTINE COPY_TYPE(N, A, B) 3 | IMPLICIT NONE 4 | TYPE :: TP(NX) 5 | INTEGER, LEN :: NX 6 | REAL, DIMENSION(NX) :: X 7 | END TYPE TP 8 | INTEGER, INTENT(IN) :: N 9 | TYPE(TP(N)), INTENT(IN) :: A 10 | TYPE(TP(N)), INTENT(OUT) :: B 11 | B = A 12 | END SUBROUTINE COPY_TYPE 13 | 14 | ! Run a program that tests derived type functionality. 15 | PROGRAM TEST_DERIVED_TYPES 16 | IMPLICIT NONE 17 | ! Configuration. 18 | TYPE :: TP(NX) 19 | INTEGER, LEN :: NX 20 | REAL, DIMENSION(NX) :: X 21 | END TYPE TP 22 | INTEGER, PARAMETER :: N = 10 23 | ! Locals for testing. 24 | INTEGER :: I 25 | TYPE(TP(N)) :: A 26 | TYPE(TP(N)) :: B 27 | 28 | ! ----------------------------------------------------------------- 29 | ! ! Define an interface to the externally defined subroutine above. 30 | ! INTERFACE 31 | ! SUBROUTINE COPY_TYPE(N, A, B) 32 | ! IMPLICIT NONE 33 | ! TYPE :: TP(NX) 34 | ! INTEGER, LEN :: NX 35 | ! REAL, DIMENSION(NX) :: X 36 | ! END TYPE TP 37 | ! INTEGER, INTENT(IN) :: N 38 | ! TYPE(TP(N)), INTENT(IN) :: A 39 | ! TYPE(TP(N)), INTENT(OUT) :: B 40 | ! END SUBROUTINE COPY_TYPE 41 | ! END INTERFACE 42 | 43 | EXTERNAL :: COPY_TYPE 44 | ! ----------------------------------------------------------------- 45 | 46 | ! Initialize A. 47 | DO I = 1, N 48 | A%X(I) = I 49 | END DO 50 | ! Call the test-running procedure. 51 | CALL COPY_TYPE(N, A, B) 52 | 53 | END PROGRAM TEST_DERIVED_TYPES 54 | -------------------------------------------------------------------------------- /fmodpy/test/type/test_type.f03: -------------------------------------------------------------------------------- 1 | MODULE FTYPES 2 | TYPE, BIND(C) :: SIZE_TYPE 3 | INTEGER :: N 4 | END TYPE SIZE_TYPE 5 | 6 | TYPE, BIND(C) :: T1 7 | INTEGER :: A 8 | REAL :: B 9 | END TYPE T1 10 | 11 | TYPE, BIND(C) :: T2 12 | INTEGER :: A 13 | REAL :: B 14 | END TYPE T2 15 | 16 | CONTAINS 17 | 18 | SUBROUTINE COPY_T1_TO_T2(A, B) 19 | TYPE(T1), INTENT(IN) :: A 20 | TYPE(T2), INTENT(OUT) :: B 21 | B%A = A%A 22 | B%B = A%B 23 | END SUBROUTINE COPY_T1_TO_T2 24 | 25 | END MODULE FTYPES 26 | 27 | ! Test Fortran REAL wrapping and usage from Python with fmodpy. 28 | 29 | SUBROUTINE TEST_STANDARD(SING_IN, SING_OUT, ARRAY_IN, ARRAY_OUT,& 30 | KNOWN_ARRAY_OUT, KNOWN_MATRIX_OUT, OPT_SING_IN, OPT_SING_OUT) 31 | ! Test the basic functionaly of the 'TYPE(T1)' type and its 32 | ! interoperability with Python. This includes, inputs, outputs, 33 | ! array inputs with known and unknown size, optional inputs, and 34 | ! optional outputs. 35 | IMPLICIT NONE 36 | TYPE, BIND(C) :: T2 37 | INTEGER :: A 38 | REAL :: B 39 | END TYPE T2 40 | ! Argument definitions. 41 | TYPE(T2), INTENT(IN) :: SING_IN 42 | TYPE(T2), INTENT(OUT) :: SING_OUT 43 | TYPE(T2), DIMENSION(:), INTENT(IN) :: ARRAY_IN 44 | TYPE(T2), DIMENSION(:), INTENT(OUT) :: ARRAY_OUT 45 | TYPE(T2), DIMENSION(SIZE(ARRAY_OUT)), INTENT(OUT) :: KNOWN_ARRAY_OUT 46 | TYPE(T2), DIMENSION(3,SIZE(ARRAY_OUT)), INTENT(OUT) :: KNOWN_MATRIX_OUT 47 | TYPE(T2), INTENT(IN), OPTIONAL :: OPT_SING_IN 48 | TYPE(T2), INTENT(OUT), OPTIONAL :: OPT_SING_OUT 49 | ! Local variable. 50 | INTEGER :: I 51 | ! Copy the single input value to the single output value. 52 | SING_OUT%A = SING_IN%A + 1 53 | SING_OUT%B = SING_IN%B + 1.0 54 | ! Copy as much of the input array as possible to the output array. 55 | ARRAY_OUT(1:MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))) = & 56 | &ARRAY_IN(1:MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))) 57 | DO I = MIN(SIZE(ARRAY_IN),SIZE(ARRAY_OUT))+1, SIZE(ARRAY_OUT) 58 | ARRAY_OUT(I)%A = I 59 | ARRAY_OUT(I)%B = REAL(I) 60 | END DO 61 | DO I = 1, SIZE(KNOWN_MATRIX_OUT, 1) 62 | KNOWN_MATRIX_OUT(I,:)%A = I 63 | KNOWN_MATRIX_OUT(I,:)%B = REAL(I) 64 | END DO 65 | ! Set the KNOWN_ARRAY and the KNOWN_MATRIX values to be identifiabl. 66 | DO I = 1,SIZE(ARRAY_OUT) 67 | KNOWN_ARRAY_OUT(I)%A = I 68 | KNOWN_ARRAY_OUT(I)%B = REAL(I) 69 | KNOWN_MATRIX_OUT(:,I)%A = KNOWN_MATRIX_OUT(:,I)%A + I 70 | KNOWN_MATRIX_OUT(:,I)%B = KNOWN_MATRIX_OUT(:,I)%B + REAL(I) 71 | END DO 72 | ! Do some operations on the optional inputs / outputs. 73 | IF (PRESENT(OPT_SING_OUT)) THEN 74 | IF (PRESENT(OPT_SING_IN)) THEN 75 | OPT_SING_OUT = OPT_SING_IN 76 | ELSE 77 | OPT_SING_OUT = SING_IN 78 | END IF 79 | ELSE IF (PRESENT(OPT_SING_IN)) THEN 80 | SING_OUT = OPT_SING_IN 81 | END IF 82 | ! End of this subroutine. 83 | END SUBROUTINE TEST_STANDARD 84 | 85 | 86 | FUNCTION TEST_EXTENDED(OPT_ARRAY_IN, KNOWN_OPT_ARRAY_OUT,& 87 | & OPT_ALLOC_ARRAY_OUT, N ) RESULT(ALLOC_ARRAY_OUT) 88 | ! Test the extended functionaly of the 'TYPE(T1)' type and its 89 | ! interoperability with Python. This includes, optional array 90 | ! inputs, optional array outputs, and allocatable array outputs. 91 | USE FTYPES, ONLY: T1 92 | IMPLICIT NONE 93 | TYPE(T1), INTENT(IN), OPTIONAL, DIMENSION(:) :: OPT_ARRAY_IN 94 | TYPE(T1), INTENT(OUT), OPTIONAL :: KNOWN_OPT_ARRAY_OUT(3) 95 | TYPE(T1), INTENT(OUT), OPTIONAL, ALLOCATABLE :: OPT_ALLOC_ARRAY_OUT(:) 96 | TYPE(T1), DIMENSION(:), ALLOCATABLE :: ALLOC_ARRAY_OUT 97 | INTEGER, INTENT(IN) :: N 98 | ! Local variable. 99 | INTEGER :: I 100 | 101 | ! Assign the optional array output values. 102 | IF (PRESENT(KNOWN_OPT_ARRAY_OUT)) THEN 103 | IF (PRESENT(OPT_ARRAY_IN)) THEN 104 | DO I = 1, MIN(SIZE(OPT_ARRAY_IN), SIZE(KNOWN_OPT_ARRAY_OUT)) 105 | KNOWN_OPT_ARRAY_OUT(I)%A = I 106 | KNOWN_OPT_ARRAY_OUT(I)%B = REAL(I) 107 | END DO 108 | ELSE 109 | DO I = 1, SIZE(KNOWN_OPT_ARRAY_OUT) 110 | KNOWN_OPT_ARRAY_OUT(I)%A = I 111 | KNOWN_OPT_ARRAY_OUT(I)%B = REAL(I) 112 | END DO 113 | END IF 114 | 115 | END IF 116 | 117 | ! Allocate the optional array output and assign its values. 118 | IF (PRESENT(OPT_ALLOC_ARRAY_OUT)) THEN 119 | ALLOCATE(OPT_ALLOC_ARRAY_OUT(1:N/2)) 120 | DO I = 1, SIZE(OPT_ALLOC_ARRAY_OUT) 121 | OPT_ALLOC_ARRAY_OUT(I)%A = SIZE(OPT_ALLOC_ARRAY_OUT) - (I-1) 122 | OPT_ALLOC_ARRAY_OUT(I)%B = REAL(SIZE(OPT_ALLOC_ARRAY_OUT) - (I-1)) 123 | END DO 124 | 125 | END IF 126 | 127 | ! Allocate the required array output to the specified size. 128 | ALLOCATE(ALLOC_ARRAY_OUT(1:N)) 129 | DO I = 1, SIZE(ALLOC_ARRAY_OUT) 130 | ALLOC_ARRAY_OUT(I)%A = SIZE(ALLOC_ARRAY_OUT) - (I-1) 131 | ALLOC_ARRAY_OUT(I)%B = REAL(SIZE(ALLOC_ARRAY_OUT) - (I-1)) 132 | END DO 133 | 134 | ! End of function. 135 | END FUNCTION TEST_EXTENDED 136 | 137 | 138 | ! Create a test that pulls from a type defined in another file. This will 139 | ! require that the Fortran wrapper USE's the other module, and that 140 | ! that the Python wrapper has a definition of the type from the other file. 141 | FUNCTION TEST_RETURN_AUX() RESULT(ANSWER) 142 | TYPE, BIND(C) :: T3 143 | INTEGER :: A 144 | REAL :: B 145 | CHARACTER :: C 146 | END TYPE T3 147 | TYPE(T3) :: ANSWER 148 | ANSWER%A = 1 149 | ANSWER%B = 2.0 150 | ANSWER%C = '3' 151 | END FUNCTION TEST_RETURN_AUX 152 | -------------------------------------------------------------------------------- /fmodpy/test/type/test_type_aux.f03: -------------------------------------------------------------------------------- 1 | 2 | MODULE EXTRA_TYPE 3 | 4 | TYPE, BIND(C) :: T3 5 | INTEGER :: A 6 | REAL :: B 7 | CHARACTER :: C 8 | END TYPE T3 9 | 10 | END MODULE EXTRA_TYPE 11 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 |

fmodpy

3 |

4 | 5 |

6 | An easy-to-use Fortran wrapper for Python. 7 |
8 | No source code changes, no manual specification files, just import like it's native. 9 |

10 | 11 | ```python 12 | # Automatically generate a wrapper, compile, link, and import into Python. 13 | fortran_module = fmodpy.fimport('MODULE.f08', dependencies=['LIB.f90', 'BLAS.f']) 14 | # - handles any Fortran source, modern, or fixed format F77 15 | # - reads the source to determine the interface specification 16 | # - includes Fortran documentation in the wrapped code 17 | # - produces a stand-alone and distrutable Python module with only numpy as dependency 18 | ``` 19 | 20 | 21 | Modern Fortran is capable of being integrated with Python near 22 | seamlessly allowing for rapid transition between prototype and 23 | optimized production-ready code. This packages aims to make Fortran 24 | code as easy to import and use as native Python. This combines the 25 | performance of Fortran with the convenience and accessibility of 26 | Python, allowing for a productive and exciting development pipeline. 27 | 28 | This package is compiler independent. The generated wrappers are 29 | self contained, written purely in Python, and are immediately sharable 30 | to any other POSIX platform with a Fortran compiler installed. The only 31 | python dependency is `numpy` (no need to hassle with installing `cython`). 32 | After generating the python wrapper module, even `fmodpy` itself is no longer 33 | a dependency! This makes `fmodpy` particularly suitable to writing and sharing 34 | performant mathematical software with a broader audience, since 35 | complex dependencies can often be difficult to configure on cutting 36 | edge high performance computing platforms. 37 | 38 | 39 | ## INSTALLATION: 40 | 41 | python3 -m pip install fmodpy 42 | 43 | This code expects that you already have a Fortran compiler 44 | installed. By default most machines do not have a Fortran compiler 45 | installed, but most package managers support installation of 46 | `gfortran` (a GNU compiler). In addition, there are popular 47 | commercial Fortran compilers such as `pgifortran` (the [PGI 48 | compiler](https://www.pgroup.com/products/index.htm) that uniquely 49 | supports OpenACC), `ifort` (the [Intel 50 | compiler](https://software.intel.com/content/www/us/en/develop/tools/oneapi/components/fortran-compiler.html)), 51 | and `f90` (the [Oracle Sun 52 | compiler](https://www.oracle.com/application-development/technologies/developerstudio-features.html)). 53 | 54 | The easiest setup is to install `gfortran` with your preferred 55 | package manager, then the default behaviors of `fmodpy` will work 56 | correctly. 57 | 58 | 59 | ## USAGE: 60 | 61 | ### PYTHON: 62 | 63 | ```python 64 | import fmodpy 65 | 66 | # Compile and import the Fortran code. (This will automatically 67 | # recompile the module if the Fortran source has been saved 68 | # more recently than the last time the module was imported.) 69 | module = fmodpy.fimport("") 70 | ``` 71 | 72 | For more details, see the `help(fmodpy.fimport)` documentation. 73 | Notably, global configurations (i.e., the default Fortran compiler) 74 | can be viewed and edited with `fmodpy.configure`. 75 | 76 | ### COMMAND LINE: 77 | 78 | Run `fmodpy` from the command line with: 79 | 80 | ```shell 81 | python3 -m fmodpy "" [setting1=""] [setting2=""] ... 82 | ``` 83 | 84 | The result will be a directory containing a Python package that 85 | wraps and calls the underlying Fortran code. 86 | 87 | Execute with no arguments to get help documentation. For a list of 88 | the different configuration options, run the command: 89 | ```shell 90 | python3 -c "import fmodpy; fmodpy.configure()" 91 | ``` 92 | 93 | 94 | ## SUPPORTED FORTRAN: 95 | 96 | - `INTEGER` 32 and 64 bit, (allocatable / assumed shape) arrays, optionals, pointers 97 | - `REAL` 32 and 64 bit, (allocatable / assumed shape) arrays, optionals, pointers 98 | - `CHARACTER` singletons and assumed-shape arrays, but *no* support for `LEN` behaviors 99 | - `COMPLEX` 64 and 128 bit, (allocatable / assumed shape) arrays, optionals, pointers 100 | - `LOGICAL` singletons, (allocatable / assumed shape) arrays, optionals, pointers 101 | - `TYPE` singletons, (allocatable / assumed shape) arrays, optionals, and pointers (type must have `BIND(C)` attribute) 102 | - `SUBROUTINE` standard behaviors (automatically drops `PURE` and `RECURSIVE` prefixes) 103 | - `FUNCTION` standard behaviors (wrapped with a standard subroutine call) 104 | - `MODULE` wrapper that behaves like an instantiated Python class with property-based accessors for internal attributes 105 | 106 | It is a goal to eventually allow for the following: 107 | - passing a `PROCEDURE` as an argument to Fortran code 108 | 109 | This code base is entirely driven by concrete examples and use-cases. If you want to see something supported that is not currently, please consider posting a minimum example of the Fortran code you'd like to wrap under the [Issues](https://github.com/tchlux/fmodpy/issues) page. 110 | 111 | 112 | ## HOW IT WORKS: 113 | 114 | Reads the fortran file, abstracting out the modules, subroutines, 115 | functions. Identifies the type-description of each argument for 116 | module variables, subroutines, and functions. Uses type-descriptors 117 | to generate a Fortran wrapper with `BIND(C)` enabled, as well as a 118 | matching Python wrapper using `ctypes` to pass data from Python into 119 | the Fortran wrapper. The constructed Python wrapper contains 120 | compilation settings that will automatically recompile a shared object 121 | file containing the underlying original Fortran source code. 122 | Overall, the call sequence at runtime looks like: 123 | 124 | Python code 125 | -> Python wrapper converting to C types 126 | -> Fortran wrapper bound to C (transferring characters, implicit shapes, etc.) 127 | -> Original Fortran code 128 | 129 | 130 | This uses the specifications from the fortran file to determine how 131 | the interface for each subroutine / function should behave. (I.e., 132 | `INTENT(IN)` does not return, `INTENT(OUT)` is optional as input.) 133 | 134 | 135 | ## EXAMPLE CODE 136 | 137 | Here is a simple python code that compares a `fmodpy`-wrapped Fortran 138 | code with standard NumPy. This example performs a matrix multiply 139 | operation using Fortran. 140 | 141 | ```python 142 | # This is a Python file named whatever you like. It demonstrates 143 | # automatic `fmodpy` wrapping. 144 | 145 | import fmodpy 146 | import numpy as np 147 | 148 | code = fmodpy.fimport("code.f03") 149 | 150 | a = np.array([ 151 | [1,2,3,4,5], 152 | [1,2,3,4,5] 153 | ], dtype=float, order='F') 154 | 155 | b = np.array([ 156 | [1,2], 157 | [3,4], 158 | [5,6], 159 | [7,8], 160 | [9,10] 161 | ], dtype=float, order='F') 162 | 163 | print() 164 | print("a:") 165 | print(a) 166 | 167 | print() 168 | print("b:") 169 | print(b) 170 | 171 | print() 172 | print("Numpy result") 173 | print(np.matmul(a,b)) 174 | 175 | print() 176 | print("Fortran result") 177 | print(code.matrix_multiply(a,b)) 178 | 179 | 180 | print() 181 | help(code.matrix_multiply) 182 | ``` 183 | 184 | Here is the associated Fortran file `code.f03` (no specific reason for 185 | the name, any of `.f95`, `.f03`, and `.f08` extensions could be used). 186 | 187 | ```fortran 188 | ! This module provides various testbed routines for demonstrating 189 | ! the simplicity of Fortran code usage with `fmodpy`. 190 | ! 191 | ! Contains: 192 | ! 193 | ! MATRIX_MULTIPLY -- A routine for multiplying two matrices of floats. 194 | ! 195 | 196 | SUBROUTINE MATRIX_MULTIPLY(A,B,OUT) 197 | ! This subroutine multiplies the matrices A and B. 198 | ! 199 | ! INPUT: 200 | ! A(M,N) -- A 2D matrix of 64 bit floats. 201 | ! B(N,P) -- A 2D matrix of 64 bit floats, 202 | ! 203 | ! OUTPUT: 204 | ! OUT(M,P) -- The matrix that is the result of (AB). 205 | ! 206 | USE ISO_FORTRAN_ENV, ONLY: REAL64 ! <- Get a float64 type. 207 | IMPLICIT NONE ! <- Make undefined variable usage raise errors. 208 | REAL(KIND=REAL64), INTENT(IN), DIMENSION(:,:) :: A, B 209 | REAL(KIND=REAL64), INTENT(OUT), DIMENSION(SIZE(A,1),SIZE(B,2)) :: OUT 210 | 211 | ! Compute the matrix multiplication of A and B. 212 | OUT(:,:) = MATMUL(A,B) 213 | 214 | END SUBROUTINE MATRIX_MULTIPLY 215 | ``` 216 | 217 | Now run the python program and see the output! 218 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = readme.md 3 | 4 | [bdist_wheel] 5 | universal=1 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Custom error (in case user does not have setuptools) 2 | class DependencyError(Exception): pass 3 | 4 | # Try to import setuptools (if it fails, the user needs that package) 5 | try: 6 | from setuptools import setup, find_packages 7 | except: 8 | raise(DependencyError("Missing python package 'setuptools'.\n python3 -m pip install --user setuptools")) 9 | 10 | import os 11 | # Go to the "about" directory in the package directory 12 | PACKAGE_NAME = "fmodpy" 13 | DEFAULT_DIRECTORY = os.path.join(os.path.dirname(os.path.abspath(__file__)),PACKAGE_NAME,"about") 14 | # Convenience function for reading information files 15 | def read(f_name, dir_name=DEFAULT_DIRECTORY, processed=True): 16 | text = [] 17 | # Check for directory existence. 18 | if (not os.path.exists(dir_name)): 19 | print(f"ERROR: No directory found '{dir_name}'") 20 | while (not os.path.exists(dir_name)): 21 | dir_name = os.path.dirname(dir_name) 22 | print(f" only found {os.listdir(dir_name)}.") 23 | return [""] if processed else "" 24 | # Check for path existence. 25 | path = os.path.join(dir_name, f_name) 26 | if (not os.path.exists(path)): 27 | print(f"ERROR: No path found '{path}'") 28 | print(f" only found {os.listdir(dir_name)}.") 29 | return [""] if processed else "" 30 | # Process the contents and return. 31 | with open(path) as f: 32 | if processed: 33 | for line in f: 34 | line = line.strip() 35 | if (len(line) > 0) and (line[0] != "%"): 36 | text.append(line) 37 | else: 38 | text = f.read() 39 | # Return the text. 40 | return text 41 | 42 | if __name__ == "__main__": 43 | # Read in the package description files 44 | # =============================================== 45 | package = PACKAGE_NAME 46 | version =read("version.txt")[0] 47 | description = read("description.txt")[0] 48 | requirements = read("requirements.txt") 49 | keywords = read("keywords.txt") 50 | classifiers = read("classifiers.txt") 51 | name, email, git_username = read("author.txt") 52 | 53 | setup( 54 | author=name, 55 | author_email=email, 56 | name=package, 57 | packages=find_packages(exclude=[]), 58 | include_package_data=True, 59 | install_requires=requirements, 60 | version=version, 61 | url = f"https://github.com/{git_username}/{package}", 62 | download_url = f"https://github.com/{git_username}/{package}/archive/{version}.tar.gz", 63 | description = description, 64 | keywords = keywords, 65 | python_requires = ">=3.6", 66 | license="MIT", 67 | classifiers=classifiers 68 | ) 69 | 70 | # Check to see if the user has a fortran compiler. Create a 71 | # warning if there is no Fortran compiler installed. 72 | 73 | --------------------------------------------------------------------------------