pyFormex file formats

Date:

Nov 21, 2023

Version:

3.4

Author:

Benedict Verhegghe <bverheg@gmail.com>

Introduction

pyFormex can export geometrical data to many well-known file formats. However, objects in pyFormex often contain a lot more information, which can not be saved in these formats. For storing complete object information on a persistent medium pyFormex has three native file formats: the pyFormex Zip File Format (PZF), the pyFormex Project File Format (PYF) and the pyFormex Geometry File Format (PGF).

The PZF format is the most versatile: it can store any pyFormex Geometry object and most other data in an open, versatile and efficient format: that of a ZIP archive. It allows partial read, append, edit and remove operations. It is the most recent of the three pyFormex formats and is the prefered way to store your data for reuse in pyFormex or plain Python.

The PYF format can store any data supported by the Python pickle protocol (which means almost everything). It is fast and compact, but is bound to the implementation of classes in pyFormex. Reading back with a very different version of pyFormex may require extra work. Therefore it is mostly recommended for short term storage.

The PGF format can only store Geometry objects. The format is stable and well-defined and has a binary and ascii version. Since the introduction of the PZF format, there is no longer any advantage of using this format, except maybe that it is easier to write a reader in other programming languages but Python. There will likely be no further developments of the format, but it will continue to be supported.

The following table gives an overview of the different capabilities of the three formats.

Overview of capabilities

PZF

PYF

PGF

Can save Geometry objects

yes

yes

yes

Can save Geometry object’s Attributes

yes

yes

[1]

Can save Geometry object’s Fields

yes

yes

yes

Can save other objects

[2]

yes

no

Can save Canvas layout and Camera

yes

[3]

no

Can load Geometry objects

yes

yes

yes

Can load Geometry object’s Attributes

yes

yes

yes

Can load Geometry object’s Fields

yes

yes

yes

Can load other objects

yes

yes

no

Can restore Canvas layout and Camera

yes

[3]

no

Supports storing multiple objects

yes

yes

yes

Supports adding objects to the file

yes

no

no

Supports removing objects from the file

yes

no

no

Supports listing contents without loading

yes

no

no

Backwards compatible (load, not save, old versions)

yes

[4]

yes

Compatibility guaranteed on pyFormex upgrades

yes

[4]

yes

Compatibility guaranteed on Python upgrades

yes

[4]

yes

Supports loading in Python (outside of pyFormex)

[5]

no

[6]

Supports loading outside of Python

[7]

no

[8]

pyFormex Zip File Format (PZF)

A pyFormex Zip File (PZF) is actually a ZIP archive, written with the standard Python zipfile.ZipFile class. Clearly, the user can insert any file in such an archive. But the pyFormex pzffile.PzfFile class provides tools for storing pyFormex objects in such an archive, and for restoring the pyFormex objects from the stored data. The API is very general and extensible and allows any pyFormex class to be saved in PZF format and restored from it. The PZF format can therefore replace nearly all use cases of both the pyFormex Geometry File Format (PGF) and the pyFormex Project File Format (PYF).

The PZF format is very robust, is easy to implement and extend, provides easy ways to upgrade without losing contents, and guarantees openness to other softwares and portability to other OSes and architectures. PZF files can be opened with most modern file managers (if they can open a ZIP archive), allowing the user a view on what’s inside, and even to remove or edit parts of it or to add more contents. The format offers compression by default, and even password-protection might be added in future. It is recommended (but not enforced) to use file names with a suffix .pzf rather than .zip, to better recognize the specialized PZF format.

Being a ZIP archive, the contents of the PZF file are individual files. Creating a PZF file is normally done using the save() method of the pzffile.PzfFile class:

PzfFile(filename).save(name1=obj1, name2=obj2, ...)

Only keyword parameters are allowed and thus each object has a name that will be stored in the PZF file. The object’s class is stored as well, to enable restoring the original objects upon reading the PZF file.

In order for an object to be saveable, it should have a method pzf_dict, returning a dict with all the object data to be saved. Each item in the dict will cause a file to be added in the PZF archive. Full details are given in API for saving objects to PZF.

In order for an object of some class to be loadable from a PZF format file, the class has to be registered with the pzffile module:

pzffile.register(class, name)

This tells the PzfFile reader that class should be used for objects with the specified class name in the PZF file. The utils.pzf_register() decorator can conveniently be used to automatically register the class with its own name (see example below).

Finally, reading is done with:

d = PzfFile(filename).read()

The returned dict has the object names as keys and the restored objects as values. Only objects with a registered class name are restored.

Here’s an example of a simple class that can be saved to PZF and restored:

@utils.pzf_register
class Person:
    def __init__(self, first_name, last_name):
        self.first = first_name
        self.last = last_name
    def pzf_dict(self):
        return { 'first_name': self.first, 'last_name: self.last }

API for saving objects to PZF

pzf_dict

An object can be saved to PZF if it has a pzf_dict method returning a dict with the object data that should be saved. Each item in the dict causes a file to be written into the PZF archive. The key becomes part of the filename and the value is stored inside the file.

The key is often an attribute of the object, though it doesn’t have to be. In most cases it is just the keyword parameter that will be passed to the object’s loader function when reading the PZF file. The default loader function is the object’s __init__ method. That’s why in the above example we made the keys in the pzf_dict match the keyword parameters of the __init__ method. Note that if in the above example we use the same names for object attributes and __init__ arguments (as is often done), we can simply return the object’s __dict__ as pzf_dict:

@utils.pzf_register
class Person:
    def __init__(self, first_name, last_name):
        self.first_name = first_name
        self.last_name = last_name
    def pzf_dict(self):
        return self.__dict__

This hasn’t been made the default because objects often have a lot of computed attributes that are unneeded or even unwanted for restoring the object and because the pzf_dict items should be carefully crafted to allow storage in the PZF format.

The value of the item should be one of these types:

numpy.ndarray

The value is written with numpy.lib.format.write_array() to a file with suffix .npy. This is the format as created by numpy.save().

str

The value is written as text to a file with suffix .txt and utf-8 encoding.

dict

The dict is converted to a string and then written to a file with suffix .txt like str type above. The key is required to hold a conversion specifier (see Converting dict to string).

None

The value is part of the key (see Encoding value in filename). An empty file is created and the filename doesn’t get a suffix.

File names

From the above, it follows that only three types of files are written into a PZF archive. They are marked by the filename suffix:

.npy

A file containing a single numpy.ndarray in NumPy’s .npy format.

.txt

A file containing text in a utf-8 encoding.

no suffix

An empty file: the info is in the file name.

The file name is formed as follows: the object’s name and its class name are joined together with a colon as separator to form a directory entry, and the key from the pzf_dict with the appropriate suffix appended becomes the file name. In other words, the full file names in the PZF archive become one of these:

name:class/key.npy
name:class/key.txt
name:class/key

This file name structure makes it easy to recognize the objects stored in a PZF and conveniently groups all the files belonging to that object in a subdirectory.

Valid keys

A key in the pzf_dict must be a str. It cannot start or end with an underscore and cannot contain double underscores excpet for the specific purposes described in

It should also not contain a colon except for the purposes described in the following cases, where it is required:

The part of the key before the (first) double underscore or colon (or the whole key if it doesn’t contain any of them) will be passed as keyword argument to the loader function when reading a PZF file. We call (that part of) the key the karg. Obviously, the karg has to be a valid Python identifier. It is recommended to only use literals, numbers and underscore.

The following karg values are reserved for special purposes:

Converting dict to string

Storing a dict on a file involves converting the dict to a string, and then writing the string to a text file. The PZF implementation provides a number of dict to str conversion methods, identified by a single character. The pzf_dict key for a dict value should specify this method and be of the form:

karg:M

where M is the character identifying the conversion method and karg is the keyword that will be pass the decoded dict to the loader function on readback. Currently the following values for M are available:

  • c: use the pzffile.Config class

  • j: use Python’s json module

  • r: use Python’s repr function

  • p: use Python’s pprint function

  • P: use Python’s pickle module

It is important to understand the limitations of each of these methods. The method should be choosen such that the whole dict can be converted to a string and restored from it. Another consideration is whether the resulting file should be easily editable or not (the P method is clearly not). If in doubt, use ‘r’ or ‘p’.

One can also use any custom method, by pre-converting the dict to a string and passing the string as value in the pzf_dict method. The key doesn’t have a :M part in this case, as the item’s value is a str. For readback, a custom loader function should be provided, taking the string as input and properly initializing the object from it (see API for loading objects from PZF).

Reserved key attrib*

pyFormex objects that are instances of a Geometry subclass can have an attribute attrib that is a dict-like object storing mostly drawing options (such as color) to be used with the rendering of the object. The Geometry.pzf_dict() method contains this item, and subclasses using that method inherit it. On readback the attrib dict is not passed as an keyword parameter to the loader function, but the contained attributes are set on the loaded objects using the special attrib method. Therefore, this key should not be used for any other purpose.

Reserved key kargs*

The pzf_dict from the example above has two items, and thus creates two files in the PZF archive. If we run the following example:

somebody = Person('John', 'Doe')
PzfFile('test_api.pzf').save(johndoe=somebody)

the PZF archive will contain two files:

  • johndoe:Person/first_name.txt: a text file with contents ‘John’

  • johndoe:Person/last_name.txt: a text file with contents ‘Doe’

With simple attributes like this, the use of two files is clearly overshoot. However, most of the pyFormex classes contain attributes which are large NumPy arrays, and the PZF format was specifically created to store those in an effective way.

Simple attributes like the above can better be collected in a dict and stored on a single file. On readback, a special loader function could be used to restore the individual values and argument names from the loaded dict. To make this process more easy (and to avoid the use of a special loader function), the reserved key name kargs can be used. Just collect all simple attributes in a dict and put that as value in the pzf_dict and use as key kargs:M, where M is again one of the methods from Converting dict to string. On readback, the kargs dict will not be passed to the loader function (with kargs as keyword argument), but rather all individual items from the dict will be passed as keyword arguments. See API for loading objects from PZF.

Thus, in the example above we can simply implement the pzf_dict as follows:

def pzf_dict(self):
    return { 'kargs:c': self.__dict__ }

and the PZF file then has a single file johndoe:Person/kargs:c.txt with the contents:

first_name = 'John'
last_name = 'Doe'

Encoding value in filename

Information can also be encoded directly in the file name, instead of the file contents. It is normally only done when the following conditions are met:

  • the string representation of the value is simple and short,

  • only a few object attributes are encoded in filenames,

  • it is interesting for the user to see the value from inspecting the contents of the PZF archive, without having to open and read a file.

In order to encode a value into the filename, the pzf_dict should pass an item with value None and a key that contains the encoded value, in the following format:

karg:N__value

where N is one of the following characters identifying the stored value type:

  • b: bool

  • i: int

  • f: float

  • s: str

This generates an empty file and the filename has no suffix.

Continuing on the example above, if we can implement the pzf_dict like this:

def pzf_dict(self):
      return {f'fullname:s__{self.first_name}_{self.last_name}': None}

the PZF archive will contain an empty file named:

johndoe:Person/fullname:s__John_Doe

In this case, readback will require a special load function accepting the argument (see API for loading objects from PZF):

fullname='John_Doe'

Reserved key field*

pyFormex objects that are instances of a Geometry subclass have an attribute fields that stores one or more Field instances defined over the geometry. These objects are stored using the reserved key field*. The key needs two extra parts of information: the Field type and the Field name. The Field data are a numpy array, and will be stored in a .npy file. This results in filenames like:

name:class/field__fieldtype__fieldname.npy``.

There can be any number of such files for the same object.

Geometry subclasses do not have to add these field items to the pzf_dict. The Geometry.pzf_dict() method provides the proper pzf_dict items. The subclasses can just initialize their pzf_dict from it and add their specific items.

Summary of the file name structure

The filenames below have the following variable parts:

name

the name of the object

class

the name of the class of the restored object, which is usually (but not necessarily) the class of the object written

attr

the name of the attribute, which is not necessarily an attribute of the object written:

value

a value directly stored in the filename

M

a modifier character, specifying the way to store some value in the archive

Some attribute names are reserved and are used in a special way on loading:

kargs

Defines a dict of keyword arguments to be passed to the loader. This is convenient when many simple attributes have to be stored. The ‘kargs’ attribute can be combined with normal named attributes, but will overwrite those in case of name clashes.

attrib

Defines a dict of values that will be loaded via the ‘attrib’ method of the object. This usually contains drawing options for a Geometry object.

field

Defines a single Field value that will be attached to the Geometry object using the addField method.

If the value of an attribute is a dict, the attribute name should have one of the following modifiers to specify what method is used to convert the dict to a string:

  • ‘:c’ use the pzffile.Config class,

  • ‘:j’ use the json module to,

  • ‘:r’ use Python’s repr function.

  • ‘:p’ use Python’s pprint function.

If a value is to be stored inside the file name, the attribute name should have one of the following modifiers attached:

  • ‘:b’ if the value is a boolean,

  • ‘:i’ if the value is an int,

  • ‘:f’ if the value is a float,

  • ‘:s’ if the value is a string.

If an attribute name does not have a modifier attached, then its value is stored in numpy’s .npy format if the value is a numpy.ndarray, or as utf-8 text in a .txt file if the value os a string. Other values are invalid.

Object, class or attribute should not start or end with an underscore or have a double underscore inside. Also, ‘class’ can not be use as attribute name, and ‘field’ and ‘attrib’ are reserved attribute names with a specific meaning for pyFormex Geometry classes. Likewise, object names ‘_camera’ and ‘_canvas’ are reserved.

Here’s a list of the valid file name formats and their use:

  • name:class/attr.npy: attr is a numpy ndarray stored on .npy file

  • name:class/attr.txt: attr is a str stored on .txt file

  • name:class/attr:M.txt: attr is a dict stored on .txt file

  • name:class/attr:M__value: attr is stored in filename

  • name:class/kargs:M.txt: attr is a dict stored on .txt file

  • name:class/attrib:M.txt: attr is an attrib dict stored on .txt file

  • name:class/field__fieldtype__fieldname.npy: an object’s Field data is stored on the .npy file, the filename contains the Field’s type and name.

The reserved attribute name ‘kargs’ is handled differently than other names. Its purpose is to store multiple attributes on a single file using one of the dict modifiers. But while an other attribute with a dict value will be passed as ‘attr=dict_value’ argument to the object loader, the ‘kargs’ attribute will be passed as **kargs, thus making the contents of the dict individual items.

As an example, the list of files in the ‘saveload.pzf’ archive in the pyformex/data folder is:

__FORMAT__PZF__2.0
__METADATA
F:Formex/coords.npy
F:Formex/prop.npy
M:Mesh/coords.npy
M:Mesh/field__node__dist.npy
M:Mesh/field__node__dist3n.npy
M:Mesh/field__elemc__dist3c.npy
M:Mesh/attrib:j.txt
M:Mesh/elems.npy
M:Mesh/eltype:s__quad4
T:TriSurface/coords.npy
T:TriSurface/attrib:j.txt
T:TriSurface/elems.npy
spiral:PolyLine/coords.npy
spiral:PolyLine/attrib:j.txt
spiral:PolyLine/closed:b__False
CS:CoordSys/rot.npy
CS:CoordSys/trl.npy
curve:BezierSpline/attrib:j.txt
curve:BezierSpline/closed:b__True
curve:BezierSpline/control.npy
curve:BezierSpline/degree:i__3
X:Coords/data.npy
_canvas:MultiCanvas/kargs:p.txt

From this list it is immediately obvious that the file is a PZF version 2.0 archive and that it contains a Formex named ‘F’, a Mesh named ‘M’, a TriSurface ‘T’, a PolyLine ‘spiral’, and some more objects (among which there is one with a reserved object name: ‘_canvas’. We can also see that the Mesh ‘M’ has an element type ‘quad4’ and that the BezierSpline ‘curve’ is closed and of the third degree. Furthermore, the Mesh object has three Fields defined on it.

This info can not only be got from the files() method, but can also be seen outside of pyFormex by opening the pzf file in your file manager: the PZF file is a valid ZIP file and most modern file managers know how to open zuch an archive and list its contents. Opening the PZF will likely only show the top level:

__FORMAT__PZF__2.0
__METADATA
F:Formex
M:Mesh
T:TriSurface
spiral:PolyLine
CS:CoordSys
curve:BezierSpline
X:Coords
_canvas:MultiCanvas

and clicking on any of the subdirectories would show its contents. You can also use the file manager to delete some objects, extract the archive, rename the objects, edit some text files, zip some extracted files to a new pzf file. Just be careful to observe the file naming rules. Using the PzfFile methods is of course more secure.

Hey, but what are these files that do not obey the above given file name rules: __FORMAT__PZF__2.0 and __METADATA? Filenames starting with double underscores are system files and should not be meddled with by the user. As you can guess, the __FORMAT__PZF__2.0 declares this file to be a PZF version 2.0 format. Likewise, __METADATA contains some metadata about the archive. You can open it and read it. It may look like this:

format = 'PZF'
version = '2.0'
creator = 'pyFormex 3.1.dev0'
datetime = (2022, 2, 13, 13, 41, 18)

The format info is repeated in the __METADATA file. We keep the __FORMAT… file to recognize the format immediately without the need to read __METADATA file from the archive.

API for loading objects from PZF

Loading objects from a PZF file using PzfFile.load() processes as follows:

  • File names are decomposed into object name, class name, keyword and possibly extra items such as modifier, value, suffix. The subdirectory name defines the object name and class, the filename the other items. See filename_structure for the full set of valid filenames.

  • If the file name has no suffix, the value is set from the filename (see value_in_filename). If the file name has a suffix .npy, the file is read into a numpy array using numpy.lib.format.read_array() and this becomes the value. If the suffix is .txt, the file is read as text, and if a modifier was used, the resulting string is transformed into a dict. Either way, we now have a keyword and a value, which are added to the object dict.

  • The class name should be a registered class for restoring PZF objects. This can have been registered by calling the pzffile.register() function or by using the utils.pzf_register() decorator on a class definition. Note that the registered class for some class name does not have to be the same class as the object’s class on storing the PZF (though it usually is). It is thus possibly to load a PZF into other objects than they were stored from.

  • The registered class is used to create a Python object from the object dict. If the class does not have a pzf_load method, the class is instantiated with the object dict as keyword arguments and the object is an instance of the registered class. If a class method pzf_load exists, this method is called with the object dict as keyword args, and the resulting object is whatever this returns. See pzf_load.

  • The created objects are collected in a dict with the object names as keys and the resulting dict is returned.

As an example, take the first PZF from reserved_key_kargs, containing two files:

  • johndoe:Person/first_name.txt: a text file with contents ‘John’

  • johndoe:Person/last_name.txt: a text file with contents ‘Doe’

After reading these files, there will be object named johndoe with class name Person and object dict {'first_name':'John', 'last_name':'Doe'}. The object will be created as:

Person(first_name='John', last_name='Doe')

Handling reserved keywords

The following reserved keywords are not put into the object dict like the others, but are handled in a special way: kargs, attrib, field.

kargs

The kargs keyword requires a dict as value. This dict is used to update the object dict (after all normal keywords for the object were added). Thus the contents of the kargs dict are keyword parameters passed to the object creation. Thus, in the second example from reserved_key_kargs, the single file johndoe:Person/kargs:c.txt will lead to exactly the same object dict as above.

attrib

The attrib keyword requires a dict as value. This dict is not used in the creation of the object. Rather, after the object has been initialized, the objects attrib method will be called with this dict as the keyword arguments. Obviously, this requires a class that has an attrib method (such as all the Geometry subclasses in pyFormex).

field

The field keyword requires a field type and field name encoded in the file name, and a numpy array as value. All the field values are collected and after the object has been created, the corresponding data are applied to the object by calling its addField() method.

Custom pzf_load

In some cases the __init__ method of the registered object class is not fit to reconstruct the object from the stored data. Therefore, a special method pzf_load may be defined in the class to process the object dict and produce whatever result is required. If an object’s registered class has such a method, it will be called with the contents of the object dict as keyword parameters, and whatever the method returns will be set as the object.

In the example from value_in_filename there was one file with all information encoded in the filename:

johndoe:Person/fullname:s__John_Doe

After reading the file, the object dict for johndoe will look like:

{'fullname': 'John_Doe'}

Obviously, we can not instantiate the Person class with these keyword parameters. Therefore, we add a custom loader method to the Person class. The method accepts the object dict as keyword parameters, and transforms the info into the proper arguments for the class initialization. Note that this has to be a class method:

@classmethod
def pzf_load(clas, fullname):
    first, last = fullname.split('_')
    return clas(first, last)

Positional arguments

In some cases the object class __init__ or pzf_load method may require the use of positional arguments. This can be achieved by declaring an attribute pzf_args containing a list of the keywords from the object dict that should be passed as positional arguments, in the order of that list.

As an example, the TriSurface initialization signature is:

def __init__(self, *args, prop=None)

It accepts up to 3 positional arguments, covering these cases:

  • none creates an empty object,

  • 1: convert from a Coords, Formex or Mesh object,

  • 2: coords, elems

  • 3: coords, edges, faces

Internally the data are stored as (coords, elems), both being numpy arrays. It also has an optional prop keyword argument. The PZF storage mirrors this and stores coords, elems (and optionally prop). When the object is restored from PZF, we can not pass the coords and elems as keyword arguments: they should be passed as two positional arguments. This is achieved by declaring in the TriSurface class:

pzf_args = ['coords', 'elems']

An alternative would be to use a pzf_load function:

@classmethod
def pzf_load(clas, coords, elems, prop=None):
    return TriSurface(coords, elems, prop=prop)

But obviously, in cases like this, using pzf_args is simpler.

Examples

This section presents some cases from important pyFormex classes. For clarity they are shown here slightly different from the actual implementation, where many classes inherit part of their pzf_dict from a parent class.

Coords

The Coords class is a subclass of a numpy.ndarray and does not contain other data, so we only have to store itself. The Coords __init__ method gets the data in an argument named data. Thus we just need to define this pzf_dict in the Coords class:

def pzf_dict(self):
    return {'data': self}

and register the Coords class:

@utils.pzf_register
class Coords:
    ...

Formex

A Formex has an attribute coords, which is a Coords (and thus an ndarray), and has an optional second data attribute, prop, which is also an ndarray. The pzf_dict looks like:

def pzf_dict(self):
    d = {'coords': self.coords}
    if self.prop:
        d['prop'] = self.prop
    return d

Mesh

A Mesh has attributes coords and elems that are ndarrays, an optional prop like in the Formex class and eltype, which is an ElementType, but can be specified by the ElementType’s name (a string) in the __init__ method. This name can be simply encoded in the file name. The pzf_dict then looks like this:

def pzf_dict(self):
    d = {
        'coords': self.coords,
        'elems': self.elems,
        f"{eltype}:s__{self.eltype.name}": None,
        }
    if self.prop:
        d['prop'] = self.prop
    return d

TriSurface

TriSurface is a subclass of Mesh with a fixed ElementType (‘tri3’). Its pzf_dict is therefore the same as that of Mesh, but without the eltype entry. However, TriSurface.__init__ has a different signature. It does not have coords, elems arguments, but rather a list of positional arguments *args. Therefore it needs a pzf_args as discussed in pzf_args.

Polygons

Polygons is like a Mesh, but without eltype and the elems attribute is a Varray, which itself has two ndarray attributes: data and ind. The pzf_dict becomes:

def pzf_dict(self):
    d = {
        'coords': self.coords,
        'elems': self.elems.data,
        'ind': self.elems.ind,
    }
    if self.prop:
        d['prop'] = self.prop
    return d

A pzf_load method is required to restore the Varray before passing it to the Polygons.__init__:

@classmethod
def pzf_load(clas, coords, elems, ind, **kargs):
    return clas(coords, Varray(elems, ind), **kargs)

BezierSpline

A BezierSpline stores three attributes: an ndarray coords, an int degree and a bool closed. The latter two are encoded in the filename. The coords attribute has to be passed to the control argument when creating a new BezierSpline:

def pzf_dict(self):
    return {
        'control': self.coords,
        f'degree:i__{self.degree}': None,
        f'closed:b__{self.closed}': None,
    }

Camera

The Camera class has a method Camera.settings() which returns a dict with all the parameters from which an identical Camera instance may be restored. All the parameters are simple enough to be restored from a string version of the dict. So the pzf_dict can be just:

def pzf_dict(self):
    return { 'kargs:p': self.settings() }

pyFormex Project File Format (PYF)

A pyFormex project file is just a pickled Python dictionary stored on file, possibly with compression. Any pyFormex objects can be exported and stored on the project file. The resulting file is normally not readable for humans and because all the class definitions of the exported data have to be present, the file can only be read back by pyFormex itself.

The format of the project file is therefore currently not further documented. See Using Projects for the use of project files from within pyFormex.

pyFormex Geometry File Format (PGF)

This describes the pyFormex Geometry File Format (PGF) version 1.6 as drafted on 2013-03-10 and being used in pyFormex 0.9.0. The version numbering is such that implementations of a later version are able to read an older version with the same major numbering. Thus, the 1.6 version can still read version 1.5 files.

The prefered filename extension for pyFormex geometry files is ‘.pgf’, though this is not a requirement.

General principles

The PGF format consists of a sequence of records of two types: comment lines and data blocks. A record always ends with a newline character, but not all newline characters are record separators: data blocks may include multiple newlines as part of the data.

Comment records are ascii and start with a ‘#’ character. Comment records are mostly used to announce the type and amount of data in the following data block(s). This is done by comment line containing a sequence of ‘key=value’ statements, separated by semicolons (‘;’).

Data blocks can be either ascii or binary, and are always announced by specially crafted comment lines preceding them. Note that even binary data blocks get a newline character at the end, to mark the end of the record.

Detailed layout

The pyFormex Geometry File starts with a header comment line identify the file type and version, and possibly specifying some global variables. For the version 1.6 format the first line may look like:

# pyFormex Geometry File (http://pyformex.org) version='1.6'; sep=' '

The version number is used to read back legacy formats in newer versions of pyFormex. The sep = ‘ ‘ defines the default data separator for data blocks that do not specify it (see below).

The remainder of the file is a sequence of comment lines announcing data blocks, followed by those data blocks. The announcement line provides information about the number, type and size of data blocks that follow. This makes it possible to write and read the data using high speed functions (like numpy.tofile and numpy.fromfile) and without having to test any contents of the data. The data block information in the announcement line is provided by a number of ‘key=value’ strings separated with a semicolon and optional whitespace.

Object type specific fields

For each object type that can be stored, there are some required fields and data blocks. In the examples below, <int> stands for an integer number, <str> for a string, and <bool> for either True or False.

  • Formex: the announcement provides at least:

    # objtype='Formex'; nelems=<int>; nplex=<int>
    

    The data block following this line should contain exactly nelems*nplex*3 floating point values: the 3 coordinates of the nplex points of the nelems elements of the Formex.

  • Mesh: the announcement contains at least:

    # objtype='Mesh'; ncoords=<int>; nelems=<int>; nplex=<int>
    

    In this case two data blocks will follow: first ncoords*3 float values with the coordinates of the nodes; then a block with nelems*nplex integer values: the connectivity table of the mesh.

  • Curve:

Optional fields

The announcement line may contain other fields, usually to define extra attributes for the object:

  • props=<bool> : If the value is True, another data block with nelems integer values follows. These are the property numbers of the object.

  • eltype=<str> : Can also have the special value None. If specified and not None, it will be used to set the element type of the object.

  • name=<str> : Name of the object. If specified, pyFormex will use this value as a key when returning the restored object.

  • sep=<str> : This field defines how the data are stored. If it is not defined, the value from the file header is used.

    • An empty string means that the data blocks are written in binary. Floating point values are stored as little-endian 4byte floats, while integer values are stored as 4 byte integers.

    • Any other string makes the data being written in ascii mode, with the specified string used as a separator between any two values. When reading a PGF file, extra whitespace and newlines appearing around the separator are silently ignored.

Example

The following pyFormex script creates a PGF file containing two objects, a Formex with one square, and a Mesh with two triangles:

F = Formex('4:0123')
M = Formex('3:112.34').setProp(1).toMesh()
writeGeomFile('test.pgf',[F,M],sep=', ')

The Mesh has property numbers defined on it, the Formex doesn’t. The data are written in ascii mode with ‘, ‘ as separator. Here is the resulting contents of the file ‘test.pgf’:

# pyFormex Geometry File (http://pyformex.org) version='1.6'; sep=', '
# objtype='Formex'; nelems=1; nplex=4; props=False; eltype=None; sep=', '
0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 1.0, 0.0
# objtype='Mesh'; ncoords=4; nelems=2; nplex=3; props=True; eltype='tri3'; sep=', '
1.0, 0.0, 0.0, 2.0, 0.0, 0.0, 1.0, 1.0, 0.0, 2.0, 1.0, 0.0
0, 1, 3, 3, 2, 0
1, 1

This file contains two objects: a Formex and a Mesh. The Formex has 1 element of plexitude 4 and no property numbers. Following its announcement is a single data block with 1x4x3 = 12 coordinate values. The Mesh contains 2 elements of plexitude 3, has element type ‘tri3’ and contains property numbers. Following the announcement are three data blocks: first the 4*3 nodal coordinates, then the 2*3 = 6 entries in the connectivity table, and finally 2 property numbers.