0.1.33
Loading...
Searching...
No Matches
File Input/Output

File Input/Output

Note
File paths can be given as absolute paths or relative paths. Helios automatically resolves asset file paths (models, textures, spectra, etc.) so executables can be run from any directory. The resolution system searches for assets relative to the build directory. You can set the HELIOS_BUILD environment variable to specify a custom build directory location if needed.

XML File Structure

File input and output is handled using .xml files. See the Wikipedia page for xml for more information on xml files: en.wikipedia.org/wiki/XML.

Within Helios, xml files can be used to execute any Context commands that would be issued within a program, for example, adding geometry, adding timeseries, or adding primitive data.

All xml files start with a header indicating the version to be used, and the tag "<helios>...</helios>" should encapsulate the whole file:

<?xml version="1.0"?>
<helios>
</helios>

Comments are given in xml files by:

<!-- this is a comment -->

Adding Primitives

Primitives are added by giving a tag with the primitive type, followed by elements that specify necessary inputs. The table below gives examples of adding each primitive type in an .xml file. Note that the parameters used to specify the primitives are the same as when adding them to the Context via 'add[*]' commands (e.g., addPatch()).

Primitive Type Code Sample
Patch
 <patch>
<center>0 0 0</center>
<size>1 1</size>
<rotation>0 0</rotation> <!-- OPTIONAL -->
<color>1 0 0</color> <!-- OPTIONAL -->
<texture>"grass.jpg"</texture> <!-- OPTIONAL -->
</patch>
Triangle
 <triangle>
<vertex>0 0 0</vertex>
<vertex>1 0 0</vertex>
<vertex>1 1 0</vertex>
<color>1 0 0</color>
</triangle>
Disk
 <disk>
<center>0 0 0</center>
<size>1 1</size>
<rotation>0 0</rotation>
<color>1 0 0</color>
<texture>"grass.jpg"</texture>
</disk>
Voxel
 <voxel>
<center>0 0 0</center>
<size>1 1 1</size>
<color>1 0 0</color>
</voxel>

Adding Timeseries Data

Data timeseries are specified by the tag 'timeseries' with the attribute giving the label for the timeseries (e.g., temperature). The 'datapoint' tag is used to specify data points, with elements specifying the time, date, and value. The sample below gives an example of creating a timeseries in an xml file.

<?xml version="1.0"?>
<helios>
<timeseries label="temperature" >
<datapoint>
<date>2 1 2000</date> <!-- 2 Jan. 2000 -->
<time>13 0 0</time> <!-- 13:00:00 -->
<value>301.23</value>
</datapoint>
<datapoint>
<date>2 1 2000</date> <!-- 2 Jan. 2000 -->
<time>13 15 00</time> <!-- 13:15:00 -->
<value>301.92</value>
</datapoint>
<datapoint>
<date>2 1 2000</date> <!-- 2 Jan. 2000 -->
<time>13 30 00</time> <!-- 13:30:00 -->
<value>302.56</value>
</datapoint>
<datapoint>
<date>2 1 2000</date> <!-- 2 Jan. 2000 -->
<time>13 45 00</time> <!-- 13:45:00 -->
<value>303.05</value>
</datapoint>
</timeseries>
</helios>

Note that the date can alternatively be specified as a Julian day of year (1-366) using the <dateJulian> tag:

<dateJulian>2 2000</dateJulian> <!-- 2 Jan. 2000, Julian Day = 2 -->

Adding Timeseries (Weather) Data from Tabular Text Files

If timeseries/weather data is available in a tabular ASCII text file, this can be read directly into the Context to create a timeseries using the loadTabularTimeseriesData() method. The general format for these files is that each column should correspond to each variable (e.g., time, date, temperature, wind speed) and each row should be a different time point. There should be an equal number of columns on every line.

At a minimum, there should be a column specifying the date, which could either be a date string (e.g., 03/10/2023) or Julian day and the year, as well as columns specifying the hour and at least one corresponding data value. The date string can separate values by the '/' or '-' characters (03/10/2023 or 03-10-2023, respectively), and can have different ordering (e.g., YYYYMMDD) which is specified based on an argument to the loadTabularTimeseriesData() method.

There are two ways of specifying the ordering and labels of columns. The first is to specify them in the file header (first line). The second is to specify them manually as an argument to the loadTabularTimeseriesData() method. This can be useful if you want to customize the names of data values without modifying the actual text file.

There are certain column labels that are keywords used by Helios to specify required values such as the date. These are given in the table below. Any other label not contained in the table below will be treated as a variable, whose label in the timeseries will be the given column label.

Column Label Description Required?
year Year (YYYY) year+DOY -OR- date
DOY or Jul Day of year starting at Jan. 1 (DOY=1) year+DOY -OR- date
date Date string delimited by '/' or '-' character (default format YYYY-MM-DD) year+DOY -OR- date
hour Hour of the day. Can be specified either as 13 or 1300 (for 1PM example). yes
minute Minute of the hour. If 'hour' above is specified with minute included (e.g., 1315), the minute field will be automatically added. no
second Second of the minute. no

Arguments to the loadTabularTimeseriesData() method:

  1. A string specifying the file to be loaded, with path relative to the current working directory or as an absolute path.
  2. A list of strings specifying labels for the columns in the file. If an empty list is specified ([]), the labels given in the first line of the file will be used. Otherwise, the length of the list must match the number of columns in the file.
  3. The delimiter character separating values in each row (e.g., ',').
  4. (optional) The format of the date string used to specify date values (e.g., 'YYYYMMDD', 'MMDDYYYY'; month and day should always be two values, and year should be four values). This is not used if dates are specified using the year and DOY. The default format is 'YYYYMMDD'.
  5. (optional) Number of header lines in the file. By default, headerlines=0.

There is a special shortcut for reading CIMIS weather station files. To read these files, simply specify the second argument as a list containing the string "CIMIS" (e.g., ["CIMIS"]), which will automatically set column labels as: ["", "", "", "date", "hour", "DOY", "ETo", "", "precipitation", "", "net_radiation", "", "vapor_pressure", "", "air_temperature", "", "air_humidity", "", "dew_point", "", "wind_speed", "", "wind_direction", "", "soil_temperature", ""]. You can specify any delimiter in this case - it will be overridden to be comma delimited.

A code example for generic weather data is given below.

from pyhelios import Context
from pyhelios.types import Date, Time
context = Context()
context.loadTabularTimeseriesData("../input/weatherfile.csv", ["date", "hour", "temperature"], ",", "MMDDYYYY", 1)
date = Date(2020, 1, 2)
time = Time(13, 0, 0)
T = context.queryTimeseriesData("temperature", date, time)

The above example would read the comma-delimited file "../input/weatherfile.csv". Timeseries data would be added to the Context for values of "temperature", which could be queried based on this label.

Reading XML Files

XML files can be read by the Context via the function loadXML(). This function parses the XML file and adds all specified structures (see above) to the Context. Below is an example of how to load an XML file into the Context.

from pyhelios import Context
context = Context()
context.loadXML("file.xml")

Reading Standard Polygon File Formats

Reading PLY (Stanford Polygon) Files

The Context can automatically import Stanford .ply files en.wikipedia.org/wiki/PLY_(file_format). This is accomplished via the loadPLY() command, as illustrated below. There are several overloaded versions of this function, which allow for various modifications to the PLY model.

The nx, ny and nz vertex properties are read as per-vertex normals, and are given the same up-axis permutation as the vertex coordinates.

from pyhelios import Context
from pyhelios.types import vec3, SphericalCoord, RGBcolor, RGBcolor
import math
context = Context()
# location where PLY model will be centered
origin = vec3(0, 0, 0)
# scaling factor to apply to model
scale = 1.0
# rotation to apply to model (optional argument to loadPLY)
rotation = SphericalCoord(0., math.pi)
# default color for model (optional argument to loadPLY)
color = RGBcolor(1, 0, 0)
context.loadPLY("file.ply", origin, scale, rotation, color)

There are other forms of the loadPLY function that allow for translation, rotation, and scaling of the entire PLY model.

Different applications may utilize different coordinate axes in .ply files. In computer graphics applications, it is common to define the y-axis as the up direction. Helios uses a z-up coordinate system. By default loadPLY() assumes that the coordinate system used when creating the .ply file is y-up. There is an optional argument loadPLY() that can allow you to easily define the up-axis.

The Blender software package (www.blender.org) can easily modify and convert most polygon file formats to .ply format.

Reading OBJ (Wavefront) Files

Helios can read Wavefront (.obj) files and associated material (.mtl) files. Not all features of .obj files are applicable to Helios, and thus some features are not supported. Supported features are:

  • Geometric vertex coordinates (x,y,z)
  • Vertex texture coordinates (u,v)
  • Vertex normals (vn)
  • All four face vertex reference forms (v, v/vt, v//vn, v/vt/vn), including negative (relative) indices
  • Texture map image files (.jpg, .png) associated with materials
  • Object groups. When the 'o groupname' is added before a set of vertices, primitive data called 'object_label' is written for the resulting set of primitives.

Wavefront files are loaded via the loadOBJ() function, which takes the name of an .obj file. If the file defines materials and lists an associated material (.mtl) file, the code looks in one of two places for the file. First, if an absolute or relative path is given for the .mtl file name, the file is simply loaded from that location. If only a file name is given for the .mtl file, the directory where the original .obj file was found is searched for the .mtl file.

All aspects of material files are not supported. Helios simply searches for the "Kd" texture map, and uses that to color the primitives.

The loadOBJ() function requires inputs that scale the model to achieve a certain size of the model, and apply a rotation to the model. There are multiple overloaded versions of this function (listed below) that allow specification of these aspects differently.

Function Description
loadOBJ(filename, origin, height, rotation, default_color, silent=False) Load the model and translate it to the location 'origin', scale to have a height of 'height' (set height=0 to apply no scaling), and apply spherical rotation of 'rotation'.
loadOBJ(filename, origin, height, rotation, default_color, upaxis, silent=False) Same as above, except specify the 'up axis' of the model ('XUP', 'YUP', or 'ZUP'). Some 3D modeling software uses a different default axis corresponding to the up direction (e.g., computer graphics software commonly uses 'y' as the up direction).
loadOBJ(filename, origin, scale, rotation, default_color, upaxis, silent=False) Same as above, except a scaling factor is applied to the model in the x-, y-, and z-directions based on the value of 'scale' (if scale.x=scale.y=scale.z=0, no scaling is applied).

The last optional argument to the loadOBJ() function allows the user to disable any messages output from the function while loading by setting silent=True.

Example code is given below to load an OBJ model at the origin with no scaling (height=0) or rotation (rotation=SphericalCoord(1, 0, 0)) applied. Note that upaxis defaults to ‘'YUP’; passupaxis='ZUP'‘ explicitly if your model already uses Helios’ z-up convention.

from pyhelios import Context
from pyhelios.types import vec3, RGBcolor, SphericalCoord
context = Context()
context.loadOBJ("relative/path/to/someobjfile.obj", origin=vec3(0, 0, 0), height=0,
rotation=SphericalCoord(1, 0, 0), color=RGBcolor(1, 0, 0))

Writing PLY (Stanford Polygon) Files

The Context has analogous files for writing PLY files based on the geometry currently loaded in the Context. This is accomplished via the writePLY() command, as illustrated below.

Geometry belonging to a Polymesh with retained connectivity (see Mesh Topology) is written against the mesh's shared vertices rather than three independent vertices per triangle, and nx, ny and nz vertex properties are emitted when every vertex in the file carries a normal. This is what allows a closed solid to reload as a closed solid whose volume can still be computed. Output for geometry that is not part of such a mesh is unchanged.

from pyhelios import Context
context = Context()
context.addPatch()
context.writePLY("file.ply")

Writing OBJ (Wavefront) Files

Writing Wavefront OBJ files is similar to writing a PLY file, except that it will produce both a .obj file containing the geometry, and a .mtl file defining materials (colors, texture masks). For this reason, only the base file name with no extension is provided to the function.

As with PLY, geometry belonging to a Polymesh with retained connectivity is written against the mesh's shared vertices, emitting each vertex once and referencing it by index from every face that uses it. With write_normals=True one true per-vertex normal is written per shared vertex, rather than one flattened face normal per primitive, so a smooth mesh survives a load–write–load round trip instead of degrading to a faceted one.

from pyhelios import Context
context = Context()
context.addPatch()
# This will produce two files: "file.obj" and "file.mtl"
context.writeOBJ("file")

Mesh Topology

Triangles read from an OBJ or PLY file are grouped into a Polymesh compound object that retains the connectivity of the source file: a deduplicated vertex list, a face table, and optional per-vertex normals and texture coordinates. The vector of UUIDs returned by loadOBJ() and loadPLY() is unchanged; the object ID is obtained with getPrimitiveParentObjectID().

Retained connectivity is what makes a loaded mesh a surface rather than a triangle soup. It is used to compute an enclosed volume, to render the mesh smoothly, and to write it back out against shared vertices so a smooth mesh survives a load–write–load round trip.

Function Description
getPolymeshObjectVertices(objID) Shared vertex positions
getPolymeshObjectFaces(objID) Vertex index triples defining each face
getPolymeshObjectVertexNormals(objID) Per-vertex normals, or an empty list if the mesh has none
getPolymeshObjectVertexUV(objID) Per-vertex texture coordinates
getPolymeshObjectVertexCount(objID) Number of shared vertices
getPolymeshObjectFaceCount(objID) Number of faces
doesPolymeshObjectHaveVertexNormals(objID) Whether the mesh carries vertex normals
getPolymeshObjectVertexNormalSource(objID) Where those normals came from
getPolymeshObjectFaceIndexForPrimitive(objID, uuid) Face index of a member primitive
getPolymeshObjectPrimitiveUUIDForFace(objID, face_index) UUID of the primitive making up a face
setPolymeshObjectTopology(objID, vertices, faces, face_UUIDs, ...) Attach a face set to a mesh built programmatically

A mesh assembled with addPolymeshObject() has no topology until one is attached: its face count is zero, it cannot report a volume, and it is written out as independent per-triangle vertices.

Vertex Normals

A mesh loaded by loadOBJ() or loadPLY() always carries vertex normals. Normals authored in the file are kept as they are; a file that supplies none (most exporters omit them unless asked) has them generated from the mesh connectivity at load time (helios-core 1.3.85+), so a curved imported surface shades smoothly without any further call. The generated normals are blended across every edge, which leaves the shared-vertex topology intact, so isPolymeshObjectClosed() and getPolymeshObjectVolume() are unaffected.

Only a mesh assembled programmatically through setPolymeshObjectTopology() without normals has none. computePolymeshObjectVertexNormals() generates them for that case, or regenerates them at a different crease angle or after the mesh has been deformed. It averages face normals weighted by area, and splits vertices across edges whose dihedral angle exceeds crease_angle_degrees so that hard edges stay hard.

getPolymeshObjectVertexNormalSource() reports the provenance as a VertexNormalSource: AUTHORED (read from the file), COMPUTED (generated, by the loader or by computePolymeshObjectVertexNormals()) or NONE (a programmatic mesh with no normals).

from pyhelios import Context
from pyhelios.wrappers.DataTypes import VertexNormalSource
context = Context()
uuids = context.loadOBJ("mesh.obj")
objID = context.getPrimitiveParentObjectID(uuids[0])
# AUTHORED if mesh.obj carried "vn" records, otherwise COMPUTED by the loader.
print(context.getPolymeshObjectVertexNormalSource(objID))
# Regenerate at a sharper crease angle, e.g. for a mechanical part with hard edges.
context.computePolymeshObjectVertexNormals(objID, crease_angle_degrees=15)

Topological Queries

Function Description
isPolymeshObjectClosed(objID) Whether the mesh is watertight
getPolymeshObjectBoundaryEdges(objID) Vertex index pairs for edges referenced by only one face
getPolymeshObjectConnectedComponents(objID) Face indices grouped into disjoint pieces
getPolymeshObjectSurfaceArea(objID) Total area of every face
getPolymeshObjectVolume(objID) Enclosed volume, summed over the closed pieces

Only a closed mesh has a well-defined enclosed volume. getPolymeshObjectVolume() separates a mesh into its connected pieces and sums the volume of those that are closed, so a solid shape modelled with an open stalk reports the shape's volume; an error naming the number of unmatched edges is raised only when no piece is closed. A mesh carrying no face table has its closure checked by matching facets on coincident corners. Use getPolymeshObjectSurfaceArea() for a wholly open mesh.

if context.isPolymeshObjectClosed(objID):
volume = context.getPolymeshObjectVolume(objID)
else:
print(f"open surface: {len(context.getPolymeshObjectBoundaryEdges(objID))} boundary edges")

Analytic Vertex Normals

Sphere, Tube and Cone objects can report the true surface normal of the curved shape they approximate, rather than the flat normal of the triangle that approximates it. doesObjectHaveAnalyticVertexNormals() reports which object types can, and getObjectPrimitiveVertexNormals() returns the normals at each vertex of a member primitive. Pass a list of UUIDs instead of one to evaluate many primitives at once, which prepares the per-object quantities only once.

Normals are evaluated from each shape's own definition rather than stored, so they account for taper and remain correct after the object is transformed or its nodes and radii are changed. Object types built from genuinely flat faces, such as a tile or box, report that they have no analytic normals and return an empty list.

Shared Vertex Topology

A compound object can report which of its member primitives meet at each mesh vertex. This is what lets a per-face quantity — an outgoing flux, a colour, a scalar field — be averaged onto the vertices that neighbouring faces have in common, and then interpolated back across each face, so a tessellated curve reads as a curve rather than as a set of flat panels.

doesObjectHaveSharedVertexTopology() reports whether an object exposes this. Tile, AdaptiveTile, Sphere, Tube and Cone objects always do; a Polymesh does when it carries a face table (see Mesh Topology). A Box or Disk object, and a polymesh assembled from loose primitives by addPolymeshObject(), does not.

getObjectSharedVertexCount() gives the number of distinct shared vertices — one greater than the largest index the accessors can return. getObjectPrimitiveSharedVertexIndices() returns, for one primitive, the shared vertex each of its corners belongs to, in the same order as getPrimitiveVertices(). Two primitives meeting at a corner report the same index there.

Both take a VertexWeldMode, which sets the granularity at which coincident vertices count as the same shared vertex:

Value Meaning
VertexWeldMode.WELD_FULL Treat every coincident vertex of the object as one shared vertex
VertexWeldMode.WELD_CROSS_SECTION_ONLY Weld only within a cross-section, keeping vertices at the same cross-sectional position on different segments distinct

The distinction matters only for object types with a distinguished axis. For a Tube, welding fully smooths along the axis as well as around it, which erases detail the tessellation actually captured; welding only around the cross-section removes the faceting without touching the axial direction. A Polymesh or Tile has only one sensible answer and reports the same topology for either mode.

from pyhelios.types import VertexWeldMode
if context.doesObjectHaveSharedVertexTopology(objID):
n = context.getObjectSharedVertexCount(objID, VertexWeldMode.WELD_FULL)
uuids = context.getObjectPrimitiveUUIDs(objID)
# Batched: prepares the per-object quantities once for the whole walk.
indices = context.getObjectPrimitiveSharedVertexIndicesMulti(objID, uuids)
accum = [0.0] * n
counts = [0] * n
for uuid, corners in zip(uuids, indices):
value = context.getPrimitiveData(uuid, "some_flux")
for i in corners:
accum[i] += value
counts[i] += 1
per_vertex = [a / c if c else 0.0 for a, c in zip(accum, counts)]

Prefer getObjectPrimitiveSharedVertexIndicesMulti() when walking a whole object: the per-primitive form locates the primitive in the object's list on every call, so a full walk of a Sphere, Tube or Cone is quadratic. getPrimitiveSharedVertexIndices() is a convenience form that resolves the primitive's parent object itself, and returns an empty list for a primitive that belongs to no object.

Deforming a Mesh

setPolymeshObjectVertices() moves every shared vertex of a polymesh in one call and pushes the new positions out to the member primitives, so faces that meet at a vertex stay welded. This is the supported way to deform a mesh: transforming the member primitives individually leaves each shared vertex wherever the last facet processed put it, tearing the surface apart.

The topology is unchanged, so the list must be parallel to and the same length as getPolymeshObjectVertices(). Texture coordinates are not touched, and neither is the solid fraction of the member primitives — which is a function of the (u,v) coordinates rather than the vertex positions — so deforming a textured mesh does not re-rasterize its alpha mask.

verts = context.getPolymeshObjectVertices(objID)
context.setPolymeshObjectVertices(objID, [vec3(v.x, v.y, v.z * 1.5) for v in verts])
Note
Vertex normals are not recomputed and no longer describe the deformed surface. Call computePolymeshObjectVertexNormals() again if exact normals are needed.

Exporting Project to XML File Format

All geometry and global/primitive data loaded into the Context can be written to an XML file using the writeXML() function, which can be later read back in using the loadXML(). This functionality can be used to save progress during a simulation run, or to ensure that consistent geometry is always used across simulation runs, among other things.

An XML file can be written as follows:

from pyhelios import Context
context = Context()
UUID = context.addPatch()
context.setPrimitiveDataFloat(UUID, "somedata", 10.2)
context.writeXML("file.xml")

and later read back into a new simulation:

from pyhelios import Context
context = Context()
# The context will now contain a default patch, with primitive data "somedata" equal to 10.2
context.loadXML("file.xml")

To export only a subset of primitives, pass a list of UUIDs to writeXML():

context.writeXML("subset.xml", uuids=[uuid1, uuid2], quiet=True)

To export a list of compound objects (and the primitives they contain), use writeXML_byobject():

context.writeXML_byobject("objects.xml", [objID1, objID2], quiet=True)

The quiet flag (defaults to False) suppresses informational console output during export.