Overview

Fython is Fortran with a Python syntax. If performance requirements periodically forces you out of Python, with Fython you won’t feel it

hello.fy

real offset = 0

def fast_sum:
  real in v(1e9)
  real res r

  r = 0
  for i in [1, 1e9]:
    r += v[i] + offset

  print 'The sum is {:sum(v)}'

The loop above is automatically parallelized by the Fortran compiler that powers Fython.

Usage in Python is as simple as

hello.py

import fython
import numpy as np

hello = fython.load('.hello')

offset = hello.offset()
v = fython.Real(shape=[1e9])

offset = 10
v[:] = np.random.uniform(0, 1, 1e9)

hello.fast_sum(v)

Features

Performance

The Fython language is a syntaxtic binding for the Fortran language. Any instruction you write in Fython is translated to Fortran, with the exact same meaning. The translated Fortran code is then compiled to machine instructions. There is no boiler plate code generation or LLVM in between. What you code is what you run (WYCIWYR).

Python Syntax

Fython allows to write Fortran code with Python syntax. No messy end xyz everywhere. No more % for object attributes, enjoy the car.color notation. No more concatenation puzzle, use multiline string """xyz""". No more & for multiline instruction, simply use parenthesis

many(
  arguments,
  and,
  more,
)

Write class with class, enjoying attribute getter and setter.

class Nucleon(Atom):
  real position = 1
  int weight = 10

  def pget fission:
    self inout
    self.weight /= 2

  Nucleon n
  n.fission
  print 'weight {:n.weight}'

Python Comfort

Fython import system is modeled after Python. You can define Fython modules accross several folders. They will all correctly compile and link.

import .variance = var
import stat.mean(*)
import .svm(vapnik, least_square_svm=lsq_svm)

Fython has a stack trace for fast error spotting. Say goodbye to those uninformative runtime errors.

fython sigsegv: segmentation fault

module stat.buggy
function subboom
line 7

stack trace (lineno function module) (most recent first)

7 subboom   stat.buggy
3 boom      stat.buggy

Fython print and read function are intuitive. The format mini-language is that of Fortran plus several improvements. Interpolating variables can be specified directly in the string like Perl.

print './out' 'x is {:x}'

print .file 'column 2 is {v:x[:,2]}

read .data: x y z

Pycessor

Use Python to easily do your preprocessing magic

import numpy

real pi = |numpy.pi|

# lazy initialization of exotic random variate
real v(1e3)

|
  for i in range(1000):
    write('v[i] = {:f}'.format(numpy.random.uniform()))

|

Any expression enclosed in bars is evaluated against the imported Python module. The return value of a pycession can be any valid Fython code.

Template

Overload of a function or a class can be created with template interpolation

def temp f:
  T in x
  T res r
  r = x + 10

def g = f(T=real)

When this is not sufficient, a whole package can be templatized

quicksort.fy

import type_provider(target_class=T)

def quicksort(x):
  T x(:)
  int i
  int res r

  r = 0
  for i in [1, size(x)]:
    r += x[i].less_than(x[i+1])

consumer.fy

import quicksort(*)
||type_provider = maxwell, target_class = Atom ||

int r
Atom a(10)
r = quicksort(a)

FyTypes

You can send Python object by reference to Fython using the intuitive fytypes

from fython import *

m = load('stat.mean')

nb_element = Int(value=3)
x = Real(value=[1, 2, 3])
result = Real()

m.mean(nb_element, x, result)

print(result[:])

When using a fytype, you always access or modify its value with a slice. Wheter the value is a scalar or an array.

x[:2] += 10

result[:] *= 10

Changes made by Fython are propagated back to Python

m.moving_average_in_place(x)

print('moving average is:', x[:])

You can also access and modify global Fython variables

n = m.roundoff_tolerance()
n[:] = 1e-5

All the above works for real, integer and string variables of any dimension

cities = Char(size=30, value=['montreal', 'tokyo', 'santiago'])
result = Char(size=30)

m.word_mean(nb_element, cities, result)

Wrap Fortran code and shared library

All intrinsic Fortran module are avalaible in Fython. Other Fortran modules are avalaible once they are in your Python path

import iso_c_binding(*)
import fftw.discrete_cosine_transform(*)

real x(10)
fftw_dct(x)

Putting a Fortran module in the Python path is usually done with

import sys
sys.path.append('~/fftw')

A Shared library can be imported once you have a Fortran or a Fython interface for it

import mkl.include.mkl_vsl(*)
import mkl.lib.intel64.libmkl_intel_lp64(*)

The first import is the fortran interface mkl_vsl.f90. The second import is for the shared object library libmkl_intel_lp64.so.

The requirement for these imports to work is that the mkl root directory must be in your Python path. This is usually achieve with

import sys
sys.path.append('/opt/intel')

Installation

git clone https://github.com/nicolasessisbreton/fython
cd fython
python3 setup.py install

Dependencies:

Platform:

Linux

Contribute

Any contribution is welcomed. The source is avalaible on github.

Support

Questions, suggestions, improvements or bug fixes? Join the discussion here.

License

Fython is released under an Apache License 2.0.

Overview

Installation

Fython can be installed with

git clone https://github.com/nicolasessisbreton/fython
cd fython
python3 setup.py install

To use Fython, a Fortran compiler must be present on your system. By default, Fython will try to use gfortran or ifort. If you want to explicitly specify one of these you can do

import fython

fython.use_ifort()
fython.use_gfortran()

To use another compiler do

import fython

fython.set_compiler(
    cmd = 'gfortran'
    prefix = '__',
    infix = '_MOD_',
    suffix = '',
    debug = '-O0',
    release = '-O3',
    error_regex = '(Error:|ld:)'
)

Assuming

nm -D gfortran_produced.so

show names as

__modname_MOD_varname

that is the pattern

prefix modname infix varname suffix

Testing it

That’s it. Your Fython rocket is ready

>>> import fython
>>> fython.hello()
'Welcome to Fython. See the build products in ./__fycache__'

Syntax Highlighting in Sublime

To get syntax highlighting in Sublime Text, you can use these files.

Getting Started

Any Fython package must be part of a Python package. This way Fython can leverage Python automatic package detection system.

In this tutorial, we will create a Fython package stat that illustrates most of Fython Functionalities.

The code for this tutorial is in fython/example/stat.

The Host Python package

First, we create the folder structure for the Python package where our Fython package will reside

stat/

  setup.py

  stat/

    mean/
      mean.fy
      mean_test.py

    variance/
      variance.fy
      variance_spec.fy
      variance_test.py

    newspaper/
      print.fy
      print_test.py
      read.fy
      read_test.fy

    class/
      class.py
      class_test.fy

    imports/
      __init__.fy
      imports.fy
      imports_test.fy

    pycessor/
      pycessor.fy
      pycessor_test.py
  
    baygon/
      baygon.fy      
      baygon_test.py
      force_test.py
      release_test.py
      verbose_test.py
      
    backus/
      fortran.py
      so.py
language:shell

The content of setup.py is

from setuptools import setup, find_packages

setup(
	packages = find_packages(),
	name = 'stat',
) 
language:python

We register the package in Python with

python3 setup.py develop

Mean

The Mean module illustrate the basic semantic of Fython. We write three kind of mean functions.

In Fython, the arguments of a function must have the intent attribute in, out or inout.

mean.fy

real cons boltzman = 10 # a constant

int plank = 8 # a global variable

def cube_mean:
  real in: x y z
  real out r

  r = x + y + z
  r /= 3 
  r **= 3

  ## 
    Yes, Fython has all the augmented assignment operators (+=, -=, *=, /=, **=).
    Yes, this is a multiline comment.
  ## 

def moving_mean:
  int in n 
  real inout x(n)
  int i 

  # let's forget about the edges
  for i in [2, n-1]:
    x[i] = sum( x[i-1:i+1] ) / 3

  if x[1] > 5:
    print 'x[1] is bigger than 5: x[1]={:x[1]}'
    print """
      All the values in x are:  
      {v:x}
    """

  ## 
    The Python vector x will be modified in-place.

    The print format mini-language is that of Python, plus a few additions.
    The v directive eases the printing of vector.

  ## 

def string_mean:
  char(3) in x(3)
  real out r
  int: i j 

  # a mean on strings? creative.
  for i in [1, 3]:
    for j in [1, 3]:
      r += ichar( x[i][j:j] )

  r /= 10

  r += boltzman + plank # essential constants for any calculation, aren't they?

  ##
    The ichar function gives the ascii code of a character.

    The ichar function is an intrinsic Fortran function.
    Yes, you have access to all of them.
  ##

Testing Mean

Let’s test drive our mean module

mean.py

from fython import *

m = load('.mean')

# accessing a global variable
p = m.plank()

# setting a global variable
p[:] = 6

# cube mean
print('cube mean')
x = Real(value=1)
y = Real(value=2)
z = Real(value=3)
r = Real()

m.cube_mean(x, y, z, r)

print(' result in python', r[:])

# moving mean
print('\nmoving mean 1')

n = Int(value=5)
x = Real(value=[1, 2, 3, 4, 5])
m.moving_mean(n, x)

print('  result in python', x[:])

# an other time
print('\nmoving mean 2')
x[0] = 10
x[1] = 20
m.moving_mean(n, x)

print('  result in python', x[:])

# string mean
x = Char(size=3, value=['abc', 'xyz', 'ijk'])
m.string_mean(x, r)

print('string mean', r[:])
language:python

We did not attempt to access the constant bolztman. This is because variable defined with as constant are not accessible from fython.

That’s it for the mean module. Now you can write a standalone Fython module. In preparation for those days where you need more, let’s see the variance module.

Variance

In Fython the specification and the implementation of a function or a class can be separated. Similar to Python, we start by working on our implementation of the variance function, deferring the spec to the variance_spec import

variance.fy

import .variance_spec(*)

def variance:
  cube_mean(x, y, z, u)
  r = x - u + maxwell # this is the official formula on planet kawabunga

When we are satisfied with our algorithm, we write the specification

variance_spec.fy

import stat.mean.mean(*)

def variance:
  real in: x y z
  real out r
  real u
  int cons maxwell = 8

The specification contains all the imports and the definitions we need in variance.fy. We test with

variance_test.py

from fython import *

m = load('.variance')

# cube mean
print('calling variance')
x = Real(value=1)
y = Real(value=2)
z = Real(value=3)
r = Real()

m.variance(x, y, z, r)

print(' result in python', r[:])

What we did above is called an implicit spec interpolation. We can also do explicit spec interpolation with the spec modifier

explicit_spec_interpolation.fy


def f:
  real in x
  real out y
  y = x + 2

def spec(f) g:
  y = x * 10

We test with

explicit_spec_interpolation_test.fy

from fython import *

m = load('.explicit_spec_interpolation')

print('calling f')
x = Real(value=1)
y = Real()
m.f(x, y)

print(' result in python', y[:])

print('calling g')
x = Real(value=1)
y = Real()
m.g(x, y)

print(' result in python', y[:])

Newspaper

Any good bug is remove by several usage of the print statement. With Fython the print statement output can be standard out, a file on the Python path, or a path on the file system

print.fy

int i
real x(10)

for i in [1, 10]:
  x[i] = i
  print 'x({:i}) = {:x[i]}'

print .x_final 'x is {v:x}'

# string to specify the path
print './x_transformed.out' """
  x+10 is: {vc:x+10}
  x-10 is : {vc:x-10}
  """

# Yes, Python multiline string

The Python url .x_final tells Fython to create a file ‘x_final.out’ in the same directory than the Fython module. A string can also be used to specify the path.

print_test.fy

import os
from fython import *

os.system('rm *.out') # cleaning any previous run

m = load('.print', force=1)

print('\nfinal x')
print(open('./x_final.out', 'r').read())

print('\nx transformed')
print(open('./x_transformed.out', 'r').read())

Since all bugs originates from data, we use the read statement to keep ourselves busy

read.fy

int: x y z
int u(3)

print .data mode(w) '1, 2, 3' # explicitly creating a new file with the mode modifier

read .data: x y z

print 'x {:x}'
print 'y {:y}'
print 'z {:z}'

# vectors too
read .data u
print 'u is {v:u}'

The possible mode for printing to files are mode(a) for appending, and mode(w) for overwriting. The default mode is appending. We test with

read_test.py

from fython import *

m = load('.read')

Class

Similar to Python, the first argument to any class method must be self. You can use any name for the self argument, the only rule is that the first argument is the self argument.

class.fy

class Magnetism:
  real maxwell = 8
  real pointer tesla(:) => null()
  real allocatable bohr(:)

  def energy:
    self in # first argument is always self
    real res r
    r = self.maxwell + sum(self.tesla)
      
  def pget courant:
    self in
    real res r
    r = self.maxwell + sum(self.bohr)

  def pset courant:
    s inout # any name is allow for the self argument
    real in value
    s.bohr = value

real target x(10) = 1
Magnetism m

m.tesla => x # pointer assignment
print 'energy {:m.energy()}'

# happy allocation
alloc m.bohr(8) 
print 'bohr {v:m.bohr}'

# getter/setter
m.courant = 4.
print 'courant {:m.courant}'

# happy deallocation
dealloc m.bohr

Inheritance and spec interpolation are also possible. See the language section for more details.

class_test.py

from fython import *

m = load('.class')

Imports

Three kinds of imports are possible in Fython. With a star import all the names are imported. With an aliased namespace import, the names of the target module are acessible through the alias. With a slice import, only the stated names are imported, possibly aliased.

imports.fy

import ..mean.mean(*) 

import ..variance.variance = v

import ..newspaper.print(
    i = counter, # aliasing name i 
    x, # no alias 
)

import ..imports = package_import # when the directory as a __init__.fy file

print 'mean boltzman {:boltzman}'

counter = 1
v.variance(1., 2., 3., x[counter])
print 'variance {:x[counter]}'

# triggering package main code
package_import.main()

imports_test.py

from fython import *

load('.imports')

It is possible to import a directory when it contains a __init__.fy file. This is usefull as your package grow. The content of our __init__.fy for the imports directory is

__init__.fy

import iso_c_binding(*)
print 'package export'

When importing a package, the main code of the package needs to be triggered manually. The main code of package is any code that is not part of a function and that is not a specification (variable, class and interface specification).

Pycessor

A Pycession is the fythonic term for Python preprocessor interpolation.

Pycessions can be used to define compile time constant or to avoid writing lines of code that are similar.

Since any Python code is allowed in pycession, a more exotic usage can be to run a Makefile script that produce a shared library used in your module.

For clarity, we write python imports together with the other fython imports at the top of a module.

pycessor.fy

import os
import numpy 
# or any python module on your path

real pi = |numpy.pi| # atan what? no more

# lazy initialization of exotic random variate
real v(1e3)

|
  x = numpy.random.uniform(0, 1, 1000) 

  for i in range(1000):
    write('v[i] = {:f}'.format(x[i]))
|

# running a make file
|os.system("echo 'compiling boost ... done'")| # for real? isn't it 2 hours

pycessor_test.py

from fython import *

load('.pycessor')

When a pycession is an expression such as 1+2 or f(1), its returned value is inserted in your fython code. When a pycession contains several lines, you need to explicitly state wich string to include in your code. The special pycessor function write serves this purpose.

Baygon

When an error occurs in your code, Fython will usually detect it and produce a stack trace.

baygon.fy

def boom:
	real x
	subboom(x)	

def subboom:
	real inout x
	x = 1 / 0

baygon_test.py

from fython import *

m = load('.baygon')

# shocking hazard
m.boom()

If Fython error detection system is overriden by your compiler or simply fails, you can use the verbose function of a Fython module, The verbose function tells Fython to print the location of every line of code that are run. You can then easily spot that wonderfull bug.

verbose_test.py

from fython import *

m = load('.baygon')

m.verbose()

m.boom()

Sometimes Fython may fails to detect changes in your code since the last compilation. If that happens, simply load your module with the force option to trigger a refresh of the Fython cache

force_test.py

from fython import *

m = load('.baygon', force=1)

m.boom()

When your code is bug free, the release keyword tells Fython to run your code at full Fortran speed, with all optimizations enables

release_test.py

from fython import *

m = load('.baygon', release=1)

m.boom()

print('a bad idea here as the division by zero will go unseen')

Template

Function template are usefull to create overload of a function

function.fy

def temp f:
  T in x
  print 'x is {:x}'

def f_real = f(T=real)
def f_int = f(T=int)


f_real(1.5)
f_int(1)

function_test.py

from fython import *
load('.function')

The template function needs to me marked with the modifier temp. The principle is the same for class

class.fy

class temp Atom:
  T x

  def lt:
    self in
    self in other
    bool res r
    r = self.x > other.x

def Electron = Atom(T = real)

Electron e
Electron p

e.x = 10
p.x = 1

print '{:e.lt(p)}'

class_test.py

from fython import *
load('.class')

You can also templatize a whole package

package.fy

import .class(*)
import .quicksort(*)
||type_provider=.class, target_class=Electron||

Electron e(10)
int i

for i in [10, 1, -1]:
	e.x = i

quicksort(e)


When the module quicksort and all of its dependency is imported any occurence of type_provider and target_class will be replaced by the package interpolation provided at the import statement.

The content of quicksort is

quicksort.fy

import asis type_provider(target_class=T)

def quicksort:
  T dimension(10) in x
  int: i r
  for i in [1, 9]:
    r = x[i].lt( x[i+1] )
    print 'i {:r}'

To prevent any package interpolation to happen during the import of type_provider, the import has the asis modifier.

We test with

package_test.py

from fython import *
load('.package')

Backus

You can import a Fortran module in Fython. For this example, we use the writer function of Fython.

fortran.py

from fython import *

writer("""
.brent.f90
  module brent
    integer, parameter :: tolerance = 1e-3

  contains

    function root(x) result(r)
      real, dimension(10) :: x
      real :: r
      integer :: i

      r = 0
      
      do i = 1, 10
        r = r + x(i)
      end do

    end function

  end module


.consummer.fy

  import .brent(*)

  real x(10) = 1

  print 'brent says {:root(x)}'
""")

load('.consummer')

The writer function turns a Python script into a playground for languages. The function creates the file .brent.f90 in the current directory. The content of the file is the indented content after the file name.

What we did is that we created the file brent.f90 and the file consummer.fy. The Fython consummer module imports the Fortran brent module. We then test the Fython consummer module with the load function.

we can also imports shared library in Fython

so.py

import os
from fython import *

writer("""
.ritchie.f90
  module ritchie
    real, bind(c) :: c = 10

    contains

      subroutine compile() bind(c)
        write(*, *) 'c is ', c
      end subroutine

  end module

.backus.fy
  import .ritchie(*)

  c = 20

  compile()
""")

os.system('gfortran ritchie.f90 -shared -fpic -o ritchie.so')

load('.backus')

In ritchie, we use the bind(c) attribute to emulate the standard naming convention in shared library. We then compile ritchie into a shared library with gfortran. After that we load the Fython module backus into Python. The backus module then places a class to compile.

Language Reference

This section describes in details the Fython language.

In code examples

  • shell code is indicated with a dollar $
  • Python code is indicated with triple quote >>>
  • Fortran code is indicated with triple bangs !!!

When no mention of the language is made, assume it is Fython.

The term shared library is often simply referenced by so.

Syntax

In Fython a statement is formed by a keyword, modifiers and target

keyword modifier* target

The keyword is the action performed by the statement. The modifiers mutate the default behavior of the actions, The action dictated by the keyword and modifiers is then applied to all the target.

real pointer x

real pointer: x y z
real pointer: x, y, z

real pointer:
  x
  y
  x

real pointer:
  x, y, z
  a, b, c

The comma is necessary for target that are non atomic

real cons: x=1, y=2, z=3

The only exception to the statement construction are in-place assignment operation

real: x y

x = y + 1
x += 10

Modifiers are also called attributes.

Any modifier that is allowed in Fortran can also be used in Fython

real pointer x
int allocatable contiguous y

Arrays are defined by indicating their dimensions

real:
  x(10)
  y(1:10, 0:5)

Array elements are accessed with the slice notation

x[1:6] => y[2, :]

You can initialize an array or use an array in place with the bracket notation

real x(3) = [1, 2, 3]

f([1, 2, 3])

Imports

Three kinds of imports are possible in Fython. Aliased namespace import, star import and slice import.

import pca
import stat.mean = m

import stat.variance(*)
import stat.newspaper(x, y=z)

When the module url is composed of only one name, such as the pca imports. The statement is equivalent to

import pca = pca

With an aliased namespace import, the object in the module are access with a dot .

import stat.mean = m
m.cube_mean(1, 2, 3)

With a star import all the object of the imported module are avalaible

import stat.mean(*)
cube_mean(1, 2, 3)

With a slice import only the stated names are imported, optionally aliased

import stat.mean(cube_mean, char_mean= cm)
cube_mean(1, 2, 3)
char_mean('abc', 'def', 'ijk')

For all imports it is necessary that you have write permission to the directoty that contains the imported module. This is because Fython needs to maintain build products in the same directory. The only exception to this rule are shared library import, as no build product needs to be maintained in these case.

The url of a module is its qualified Python name

import numpy.random.uniform

This implies that for a file to be importable, it must be on your Python path.

The first way to put a file on your Python is to create a host Python package and registering it to Python with a setup script

$ python3 setup.py develop

See the Getting Started section for the details.

The other method is to modify your path directly

#>>>
import sys
import fython
sys.path.append('/opt/nag')
m = fython.load('random.sobolev')

In a Python url, the file extension cannot be part of the url. You should then take into account the following resolution order. In a compile time import, Fython first search for

  • a Fython file (.fy, __init__.fy)
  • a Fortran file (.f90, .f03, ... and many other)
  • a So File (.so)

In a pycessor time import, Fython search for

  • a Python file (.py, __init__.py).

For print and read statement that uses an url, the assumed extension is .out.

For Fortran, only star and slice imports are allowed. For So only star imports are allowed.

Usefull modifiers of the import statements are

import asis payoff_defs_provider(*)

import noforce mkl.include.mkl_vsl(*)

The asis modifier prevents any package interpolation to happen during the import. This is usefull when designing a packaged meant to be a template. See the Template section.

The noforce modifier prevents a forcefull recompilation. If the module alreasy exists, it is not recompile, even if it was loaded with load(url, force=1. This is usefull to avoid recompilation of heavy module, that anyway never changes.

Declaration

The declarations order follows Fortran convention. Variables, Classes and interface declaration shoud appear first within a scoping unit, then functions.

A scoping unit is the entire module, the body of a class, or the body of a function.

For this release, nested class or nested function are not supported.

Operator

Fython has the augmented assignment operators, the logical operator, the bitwise operators, and the pointer operator.

x += 1
x -= 1
x *= 2
x /= 2

x <<= 1
x &= 1
x ^= 1
x |= 1
x >>= 1


x < <= == != => > y # this is an invalid syntax

x and y or b not c

x >> 1 + y << 4

x => y # pointer

The min and max operator are often convenient.

x ++= y # x = max(x, y)
x --= y # x = min(x, y)

Variable Declaration

In Fython the elementary types have a Python flavor

real x
int y
char z
bool a
complex b

Constant are declared with the attribute cons.

real cons x

Classes are instantiated by using there name

class A:
  pass

A a

String variable can be assign a value with a multiline string

char(100) x

x = """
  extra leading space
  at the beggining remove
"""

x = '''
  triple quote
'''

x = 'single line'
x = "double quote"

Any newline or tab character in the string will be honored.

For procedure argument, use the proc modifier

Coarray

A coarray is defined by specifying its codimension in bracket

real x[*]
real y(10)[*]

A coarray is accessed with the slice notation

x = 1 # this_image() x
x[2] = 2 # x on image 2

y[1] = 1 # this_image() y
y[:][2] = 1 # y on image 2

To use coarray in Fython, you need to set the compiler to use with the set_compiler function

#>>>
from fython import *

set_compiler(
  cmd = 'ifort',
  prefix = '',
  infix = '_mp_',
  suffix = '_',
  debug = '-coarray -coarray-num-images=5',
  release = '-coarray -coarray-num-images=5',
  error_regex = 'error:',
)

m = load('.coarray_test')

Function

A function is declared with the keyword def

def f:
  real in x
  real res r

For a variable to be recognized as an argument, it must have one of the intent modifier

in
out
inout

The return value of a function must be indicated with the modifier

res

When no argument has the modifier res, the function has no return value.

You can separate the implementation and the specification of you function with spec interpolation

def pure f:
  real in x
  real res r


def f:
  r = x + 3

The spec for f can be in the same module or originate from an import. You can also explicitlye specify the spec to use with the spec.

def elemental f:
  real in x
  real res r

def spec(f) g:
  r = x + 10

  def spec(f, g) h:
    pass # multiple spec parent

You can use the inline instruction to include verbatim the definition of one function into another

def f:
  x += 1

def g:
  inline f

The modifier debug or release can be use to specify in which mode to include the code. This is usefull for conditional inclusion of logging code for example.

inline debug f
inline release f

When no modifier is specified, the code is inlined in all compilation mode.

Automatic argument completion dispense for the need to write all the arguments of a function provided the name of the argument is the same than a name in the current scope.

real x = 1
real y = 10

def f:
  real in x
  real in y

f(y=1.) # x added automatically
f() # both x and y added

Automatic arguments completion works for keyword arguments call only. It cannot be mixed with positional argument code.

# with f as above

f(y) # not supported
f(y=1.) # supported

If a function should not be compiled, used the noprod modifier. This is usefull when the function is only used as a spec provider, and that the function should not be compiled.

def noprod f:
  real x(n) # n is not defined, this would give an error if compiled

def spec(f) g:
  int in n

  x += 1 # definition of x is provided by the spec of f

The noprod modifier is not inherited during a spec interpolation. So, only f is not compiled. To not compile g, explicitly use the modifier noprod.

to help distinguish between pure and non-pure function used the modifiers pure and sidef

def pure f:
  pass

def sidef g:
  pass

The modifier sidef has no effect during compilation. The modifier clearly states the intent of the coder: that the function g has side-effects, and cannot be marked as pure.

Interface

An interface is declared with the interface keyword

interface:
  def f:
    real in x
    real res r

To facilitate the definition of C procedure the modifier iso(c) can be used

interface:
  def iso(c) f:
    real in x

The iso(c) modifier can be used on any function declaration and is not restricted to interface declaration. The effect of the modifier is to produce

!!!
subroutine f(x) bind(c)
  use iso_c_binding
  real, intent(in) :: x
end subroutine

Class

A class is defined with the class keyword.

class A:
  real x

  def f:
    self in
    real res r
    r = self.x

  def pget y:
    s in
    real res r
    r = s.x

  def pset y:
    s inout
    real in value
    s.x = value

The first argument of any class method must be the self argument. The name used for the self argument can be anything. Above we used s instead of self for the getter and setter.

Getter and Setter are defined with the pget and pset modifiers.

Inheritance is indicated with parenthesis after the class name

class C(B, A):
  pass

You can separate the specification and the implementation of a class with the spec interpolation

# spec.fy

class A:
  real x
  def pure f:
    self in
    real res r

# code.fy
import spec(*)

class A:
  def f:
    r = self.x + 10

You can explicitly state the spec to use with the spec modifier

class A:
  real x

class spec(A) B:
  pass

You can use the inline statement to include verbatim the definition of a class or a function into your class

class A:
  real x

class B:
  inline A

Allocation

Memory is allocated and deallocated with the alloc and dealloc keyword

alloc: x y(n) z

alloc(n):
  x
  y
  z(m)

dealloc: x y z

When the keyword alloc has an argument, it is used as the default size for any variable where no size is specified.

Control Flow

Fython has if, for, fop, while and where statement

if x < 1:
  y += 1

elif x < 1:
  y += 2

else:
  y = 0

The third argument in the bracket of a for statement is the step size

for i in [1, 2]:
  r += x[i]

for i in [0, 10, 2]:
  r += x[i] # 0, 2, 4, ...

The fop loop is a parallel for loop. The Fortran equivalent is a do concurrent loop.

fop i in [1, 2]:
  r += x[i]

The while loop is

while x < 10:
  x += 1

The where statement is

where x > 1:
  x = y

elwhere x < 1:
  x -= 1

else:
  x = 0

Print

Printing to the console needs no modifier

print 'x {:x}'

When an url is used the file extension is assumed to be .out

print .simulation 'x {:x}'

A file system path can also be used

print './simulation.out' 'x {:x}'

You can then choose any extension you want.

You can print to a character variable when its name does not contain a dot

char(100) r

print r 'x {:x}'

If the name contains a dot use the unit modifier

print unit(atom.name) 'proton'

The unit modifier can also be used if you opened a file by yourself

int u = 10
open(unit=u, file='./simulation.out')
print unit(u) 'x {:x}'

If you use a number, the unit modifier is not necessary

print 10 'x {:x}'

You can control the mode in which the file is written to during a print statment with the mode modifier

print mode(a) 'x {:x}'

print mode(w) 'overwrite any previous content'

The default mode is a.

To continue printing on the same line, use the c modifier

print c 'start '
print c ' on same line'
print ' ending the line'
print 'this one on a new line'

The format mini-language is that of Fortran plus several additions

print """
  {:x} : general format used
  {f5.2:x} : float format
  {i5:x} : int format

  {v:y} : vector format: [1, 2, 3, ]

  {vc:y} : vector content format: 1, 2, 3,

  {va:y} : vector format: array([1, 2, 3, ]) ; usefull for python post-processing
"""

The additions are the v, vc and va formats that facilitates the printing of vectors.

Format that helps printing to the JSON format are also avalaible. The JSON formats avoid typing the name of a variable twice, and helps to deal with comma.

print """
  {jn:x} : json no comma before: "x": x

  {j:x} : json with comma before: ,"x":x

  {jv:x} : json vector: "x":[1, 2, 3]

  {jvn:x} : json vector no comma before: ,"x":[1,2,3]

  {j_tag:x} : json with specified tag: ,"tag":x

  {jv_tag:x} : json vector with specified tag: ,"tag":[1,2,3]

  {jn_tag:x} : json no comma before with specified tag: "tag":x

  {jvn_tag:x} : json no comma before vector with specified tag: "tag":[1,2,3]

"""

In a Typical printing with JSON format, the first element is explicitly specified without leading comma, then the remaining elements are added, prepended by a comma.

print """
  [
    { } # first element no comma

    ,{ } # any addition prepended by a comma

    ,{
      {jn:x} # no comma
      {j:y} # prepended by a comma

    }

  ]
"""

If a print statement is used only in debug mode, use the xip instruction

xip 'printed only in debug mode'
print 'printed in both debug and release mode'

The xip takes the same modifiers than the print instruction. The xip instruction is usefull for debugging.

Read

You can read a file by specifying its url. The extension is then assumed to be .out

read .data: x y z

You can specify a path in the file system with a string

read './data.out': x y z

You are then free to use any extension you want.

The read statement in Fython supports csv-like formats automatically. In Fortran, this is a called a list-directed read. For this release, the other kind of read statement are not supported.

You can use the name of variable that does not contains a dot for the read source

char(100) data

read data: x y z

If the name of the variable contains a dot, use the unit modifier

read unit(mendeleiv.table): x y z

You can read into a vector or any other variable

read .data:
  x[:, i]
  atom.name

FyTypes

Only three kinds of data type can travel back and forth between Python and Fython

#>>>
from fython import *
x = Real()
y = Int()
z = Char(size=100)

m = load('.mean')

m.f(x, y, z)

Only function that have no return value can be called from Python. In Fortran term, f must be a subroutine.

Fython can modify in-place the element send by Python. The change will be seen in Python. The same is true in Python. In change made to a fytype in Python, will be seen in Fython.

The value of a fytype is always accesses with a slice, wheter the fytype is a scalar or a vector

#>>>
x = Real()
y = Real(value=[1, 2, 3])

x[:] = 9
y[:1] = 10 + x[:]

The three Fytypes all have the optional arguments size, shape and ``value. They are shown below with their default value

#>>>
x = Real(size=4, shape=[], value=None)
y = Int(size=4, shape=[], value=None)
z = Char(size=100, shape=[], value=None)

An empty list indicates a scalar. A vector is defined by specifying the number of element in each dimension.

#>>>
x = Real(shape=[10, 10])

The argument value is used to connect a fytype to a Python object. Any change made to the fytype will be reflected in the Python object

#>>>
x = [1, 2, 3]
y = Real(value=x)

To access a global variable, use its name

#>>>
 from fython import *
 m = load('.mean')
 tolerance = m.tolerance()

Fython will automatically detects the type of the variable and use the default fytype initializer above. You can specify the variable specification yourself

#>>>
x = m.x(size=8, shape=[10])

Once setted the shape of fytype cannot change. This limitation can be overcome by letting Python and Fython share informations

#>>>
m = load('.mean')
m.compute()
n = m.result_size()
result = m.result(size=n[:])

Callback

In Fython, it is possible to call a Python function. Such callable function are called callback. The trick is to pass the address of the callback. This address is a simple integer, so, in fython, we cast it to a function pointer.

The code below shows how to transfer integer, real and arrays of these two.

The Fython code goes as follows.

import:
  iso_c_binding(*)

interface:
  def iso(c) py_fct:
    int(c_int) value in:
      xint
    real(c_float) value in:
      xreal
    int(c_int) value in:
      nintv
      nrealv
    int(c_int) in:
      xintv(*)
    real(c_float) in:
      xrealv(*)

def f:
  int(8) in:
    py_fct_pointer_int
  c_funptr:
    py_fct_pointer
  proc(py_fct) pointer:
    pyf
  int:
    x
    xv(2)
  real:
    y
    yv(2)

  print 'fython start: {:py_fct_pointer_int}'
  py_fct_pointer = transfer(py_fct_pointer_int, py_fct_pointer)
  c_f_procpointer(py_fct_pointer, pyf)
  print 'pyf called'
  x = 1
  y = 0.5
  xv = 10
  yv = 5.5
  pyf(x, y, 2, 2, xv, yv)
  print 'fython exit'

The Python goes like this

#>>>
from mttc import *
from ctypes import *
import numpy as np

m = load('.pycall', force = 2)

def f(xint, xreal, nintv, nrealv, xintv, xrealv):
  array_type = c_int*nintv
  addr = addressof(xintv.contents)
  xintv = np.array(array_type.from_address(addr), copy=0)

  array_type = c_float*nrealv
  addr = addressof(xrealv.contents)
  xrealv = np.array(array_type.from_address(addr), copy=0)

  print('hello from python', xint, xreal, nintv, nrealv, xintv, xrealv)

c_fun_dec = CFUNCTYPE(None, c_int, c_float, c_int, c_int, POINTER(c_int), POINTER(c_float))

c_fun = c_fun_dec(f)

c_fun_int = cast(c_fun, c_void_p).value

m.f(
  py_fct_pointer_int = Int8(c_fun_int),
)

Load Function

The python api side load function has the following default arguments

#>>>
load(
  fython_module_url,
  force = 0,
  release = 0,
)

The force argument is used to force a refresh of the Fython dependency cache.

The release argument indicates wheter to run in debugging mode (0), or release mode (1). In debug mode, Fython tries to detect errors. In release mode, all the compiler optimization are enabled.

When a Fython module is loaded, you access its objects with there name.

#>>>
from fython import *
m = load('.mean')

x = m.global_variable()
y = m.global_variable_with_explicit_shape(shape=[10])

# function call
m.mean(x, y)

# keyword call
m.mean(
  offset = x,
  vec = y,
)

When a global variable is accessed, Fython will automatically determine its fytype. For function however, only minimal arguments consistency is made. You should make sure that you invoke your Fython function with the right argument order and the right fytype. The argument order consistency can be alleviated by using a keyword argument call. Fython will then call your Fython function with the argument in the right order.

Pycessor

Pycession instruction are specified within bars. The Python imports necessary for the pycession at the top of your Fython module

import numpy = np

pi = |np.py|

A pycession can be multiline

|
  for T in ['real', 'int']:
    write('def f_{T:s} = g(T = {T:s})'.format(T)
|

The signature of the write function is

#>>>
write(
  string,
  *args,
  end = '\n',
  **kwargs,
)

The positional and keyword arguments are used to format the string argument.

When a pycession is a Python expression, its value is directly inserted in your Fython code. The write function is necessary when the pycession is not an expression. Each string passed to the write function is inserted in your Fython code. The end argument is appended to the string.

Any valid Python code is possible in a Pycession.

The Pycessor is a preprocessor. Do not use it to pass arguments to Fython because the dependency system will not see any post-compilation change in your Python module. The Pycessor is meant to facilitate the generation of tedious code, or to trigger any kind of necessary preparation before compilation.

Template

A function template is defined with the def statement

def f:
  T in x
  T res r
  r += x

def g = f(T=real)

To templatize a whole package use the import statement

import quicksort(*)
||type_provide=stat.mean, target_class=KMean||

Package interpolation can also be multilines

import quicksort(*)
||
  type_provide = stat.mean
  target_class = KMean

||

To disable package interpolation during a package import, use the asis modifier.

import asis random

This is usefull to avoid further interpolation during an ongoing package interpolation.