Skip to content

Reading and writing

pynumgeo.io.reader

Read a numgeo input file into a :class:~pynumgeo.deck.Deck.

The reader interprets the input the way numgeo does (:mod:pynumgeo.io.lexer for preprocessing, the solver's option rules for the keywords modelled here) and reports everything that numgeo would reject or read differently than a user would expect as :class:~pynumgeo.validation.issue.Issue objects. Keywords that pynumgeo does not model are kept as generic :class:~pynumgeo.deck.keyword.Keyword objects.

Models with *Part / *Instance / *Assembly are flattened with numgeo's rules: instance-scoped set names become <instance>.<set>, node and element labels are offset per instance (each part must be numbered 1..n in input order, which is what numgeo needs anyway), instance translations and rotations are applied to the coordinates, and find queries inside an instance are evaluated in the instance's scope.

Examples:

>>> import pathlib, tempfile
>>> p = pathlib.Path(tempfile.mkdtemp()) / "job.inp"
>>> _ = p.write_text("*Node\n1, 0., 0.\n2, 1., 0.\n*Element, type=u2-truss\n1, 1, 2\n"
...                  "*Nset, nset=Fix\n1\n*Step, name=S1\n*Static\n1., 1., 1., 1.\n"
...                  "*Boundary\nFix, u1, 0.\n*End step\n")
>>> res = parse(p)
>>> res.deck.mesh.node_count, res.deck.steps[0].items[0].data
(2, [['Fix', 'u1', 0.0]])
>>> [i.id for i in res.issues]
[]

ReadError(issues)

Bases: ValueError

Raised by :func:read with strict=True when the input has errors.

ReadResult(deck, issues=list(), preprocessed=None) dataclass

Deck plus everything worth reporting about the input.

ok property

True if no issue has severity error.

FragmentResult(keywords, deck, issues=list()) dataclass

What :func:parse_fragment read: keywords of one context, or a partial deck (context="model").

ok property

True if no issue has severity error.

to_token(text)

Data token: int for integers, float for reals (Fortran syntax), else the text unchanged.

This is how the reader turns the fields of a data line into tokens; :class:~pynumgeo.deck.Keyword applies it to text tokens as well, so that "2.0" and 2.0 are the same token.

to_token("12"), to_token("0.0d0"), to_token("Soil.all") (12, 0.0, 'Soil.all')

data_tokens(line)

The tokens numgeo reads from one data line, None for a line it skips (blank or comment).

numgeo cuts the line at **, deletes the blanks (not between $ signs) and a trailing comma and splits it at the commas; numbers become int / float (:func:to_token), text keeps its spelling. A builder can give a line in any spelling and gets the tokens the reader would produce from it.

data_tokens("Base, u1, 0 ** fixed") ['Base', 'u1', 0] data_tokens("2.0d6, 0.3,"), data_tokens(" ** note") ([2000000.0, 0.3], None)

parse(path, workdir=None)

Read an input file; never raises for input errors, they are in issues.

Raises:

Type Description
FileNotFoundError

if the file or an include is missing.

parse_text(text, name='<string>', *, includes=None)

Like :func:parse for a string.

Parameters:

Name Type Description Default
includes Mapping[str, str] | None

include name (the text after = of the include line) -> content of that file, e.g. the result of :func:~pynumgeo.io.writer.write_texts keyed by include name. Without it, includes raise :class:FileNotFoundError.

None

parse_fragment(text, context='model', name='<fragment>')

Read keyword lines that belong inside a deck, e.g. lines a user types for a model, a step or a material.

The lines are preprocessed like numgeo input (upper case, blanks, **) and grouped like the reader groups them. Includes are not allowed.

Parameters:

Name Type Description Default
context str

"model" - model definitions; they may define sets, amplitudes, materials, assignments and other model keywords, all of which are in deck; keywords are the deck's model keywords. "step" - keywords inside a *Step (no procedure); "material" - sub-keywords of a *Material. For these two, keywords holds them and deck is empty.

'model'

r = parse_fragment("Controls, u, modify\n1e-3, 1e-3, 1e-2, 1e-5, 1e-6, 1.0", "step") [(k.keyword, k.data) for k in r.keywords], r.ok ([('CONTROLS', [[0.001, 0.001, 0.01, 1e-05, 1e-06, 1.0]])], True)

read(path, *, strict=False)

Read an input file into a :class:~pynumgeo.deck.Deck.

Parameters:

Name Type Description Default
path str | PathLike[str]

the master .inp file (includes are resolved relative to its folder).

required
strict bool

raise :class:ReadError if the input has errors (see :func:parse for all issues).

False

pynumgeo.io.writer

Write a :class:~pynumgeo.deck.Deck as a numgeo input file.

Spelling: never all capitals. Keywords are written in sentence case with the docs' word breaks (*Step, *Initial conditions, *Node output), option keys and enumerated values in lower case, names defined in the deck (sets, surfaces, materials, amplitudes, steps, ...) exactly as given. numgeo upper-cases every line and deletes blanks, so this is what it reads.

Canonical order: nodes, elements, sets, surfaces, amplitudes, materials, material assignments, other model keywords (in their original order), steps, *End input. Options are written in the order the solver's readers require (name= first and scalefactor= second on *Step, type= first on *Element). Reals are written as Python's shortest round-trip text. Lines never exceed 256 characters (:class:WriteError otherwise); element records that would are split over two lines with different field counts, which is what numgeo's pre-scan needs.

The output is byte-stable: the same deck always gives the same text.

Comments: the comment of a keyword, element block, set, surface, material, amplitude, section or step is written above it as ** lines (comments=False leaves them out). Include files: :func:write_texts and write(..., includes=...) put the mesh and the materials into files of their own that the main file includes, the numgeo-GiD layout.

Examples:

>>> from pynumgeo.deck import Deck, Keyword, Step
>>> d = Deck()
>>> _ = d.mesh.add_nodes([[0, 0], [1, 0]]); _ = d.mesh.add_elements("u2-truss", [[1, 2]])
>>> _ = d.mesh.add_nset("Fix", [1])
>>> d.steps.append(Step(name="Load", procedure=Keyword.make("*Static", data=[[0.1, 1.0, 1e-5, 0.5]]),
...     items=[Keyword.make("*Boundary", data=[["Fix", "U1", 0.0]])]))
>>> print(write_text(d, header=False))
*Node
1, 0.0, 0.0
2, 1.0, 0.0
*Element, type=u2-truss
1, 1, 2
*Nset, nset=Fix
1
*Step, name=Load
*Static
0.1, 1.0, 1e-05, 0.5
*Boundary
Fix, u1, 0.0
*End step
*End input

WriteError

Bases: ValueError

The deck cannot be written as valid numgeo input (e.g. a line would exceed 256 characters).

display_keyword(keyword, context=None)

Normal-case spelling of a solver keyword (sentence case, docs' word breaks).

display_keyword("INITIALCONDITIONS"), display_keyword("NODEOUTPUT", "output-field") ('Initial conditions', 'Node output')

write_text(deck, *, header=True, comments=True)

The input file text of deck (see module docstring).

Parameters:

Name Type Description Default
header bool

write the provenance header and deck.comments.

True
comments bool

write the comment of the deck's objects as ** lines above them.

True

write_texts(deck, *, includes, header=True, comments=True)

The text of the main file and of include files, the numgeo-GiD layout of an input deck.

includes maps parts of the deck (:data:PARTS) to include names: {"mesh": "model-mesh", "materials": "material-definitions"} moves the mesh to model-mesh.inp and the materials to material-definitions.inp; the main file includes them (*Include, input=model-mesh) where they stood. numgeo copies an include into the input before it reads it, so the files mean the same as the single file of :func:write_text, and :func:~pynumgeo.parse reads them back.

Returns:

Type Description
dict[str, str]

{"main": text, "<part>": text, ...}; included files carry the header but no *End input.

from pynumgeo.deck import Deck d = Deck() _ = d.mesh.add_nodes([[0, 0], [1, 0]]); _ = d.mesh.add_elements("u2-truss", [[1, 2]]) texts = write_texts(d, includes={"mesh": "model-mesh"}, header=False) print(texts["main"] + texts["mesh"]) *Include, input=model-mesh *End input *Node 1, 0.0, 0.0 2, 1.0, 0.0 *Element, type=u2-truss 1, 1, 2

write(deck, path, *, header=True, comments=True, includes=None)

Write deck to path (UTF-8 without BOM, \n line ends) and return the path.

With includes (see :func:write_texts) the parts are written next to path as <include name>.inp. Missing parent folders are created.

format_keyword(kw, context=None, *, names=frozenset())

One keyword (with its data lines and children) as the writer spells it, e.g. for a preview.

names are kept in their spelling (upper-case keys are matched); other words are written in lower case.

print(format_keyword(Keyword.make("*Boundary", data=[["Fix", "U1", 0.0]]), names={"FIX"}), end="") *Boundary Fix, u1, 0.0

format_material(m, *, names=frozenset())

A *Material block as the writer spells it (preview of a material).

pynumgeo.io.lexer

Emulation of numgeo's preprocessing of an input file.

numgeo first copies the input file into an internal file (<job>_internal.inp) and parses only that copy (src/input/input_create_internal_input_.f90). The rules, reproduced here one by one:

  1. Every physical line is read into 256 characters; the rest is lost (:49, :58).
  2. ** starts a comment anywhere in a line; blank lines are dropped (:64-68, :134-153).
  3. A line whose upper-cased text contains INCLUDE is an include: the text after the first = (leading and trailing blanks removed) plus .inp is copied in; includes are not nested (:70-97).
  4. Lines are upper-cased (ASCII letters only) (:127).
  5. All blanks are deleted, except between the first and the last $ of a line (:156-160).
  6. A comma at the end of a line is removed (:162-174).
  7. The line *AMPLITUDE,NAME=DEFAULT,TYPE=RAMP is prepended (:55).

Every resulting :class:Line keeps the solver's text (text), the same text without upper-casing (cased, used to keep the user's spelling of names) and its source position. Deviations from what a careful user would expect (truncated lines, tabs, a line that is an include only because it contains the word INCLUDE, ...) are reported as :class:Quirk objects, never silently corrected.

Examples:

>>> pre = preprocess_text("*Step, name = Load 1  ** first step\n*Static\n0.1, 1., 1e-5, 1.,\n")
>>> [ln.text for ln in pre.lines][1:]
['*STEP,NAME=LOAD1', '*STATIC', '0.1,1.,1E-5,1.']
>>> pre.lines[1].cased
'*Step,name=Load1'

IncludeResolver = Callable[[str], tuple[str, str]] module-attribute

Maps an include name (text after =) to (display name, file content).

SourcePos(file, line) dataclass

Position of a physical line: file and 1-based line number.

Line(text, cased, pos, raw) dataclass

One line of the internal input file as numgeo sees it.

Attributes:

Name Type Description
text str

upper-cased text with blanks removed (what the solver parses).

cased str

the same text without upper-casing (same length as text).

pos SourcePos | None

source position, None for the prepended default amplitude.

raw str

the physical line as read (after truncation to 256 characters).

is_keyword property

True if the line starts with *.

Quirk(code, message, pos=None) dataclass

Something in the input that numgeo treats differently than a reader might expect.

Preprocessed(lines, quirks=list(), includes=list()) dataclass

Result of :func:preprocess: the internal lines plus everything worth reporting.

internal_text property

The internal file numgeo would write (<job>_internal.inp).

upper_ascii(text)

Upper-case ASCII letters only, like numgeo's convert_to_uppercase.

upper_ascii("Grün-1") 'GRüN-1'

preprocess_text(text, name='<string>', include_resolver=None)

Preprocess input text like numgeo (see module docstring).

Parameters:

Name Type Description Default
text str

content of the master input file.

required
name str

file name used in source positions.

'<string>'
include_resolver IncludeResolver | None

returns (display name, content) for an include name; without it, includes raise :class:FileNotFoundError.

None

preprocess(path, workdir=None)

Preprocess an input file like numgeo.

Parameters:

Name Type Description Default
path str | PathLike[str]

the master .inp file.

required
workdir str | PathLike[str] | None

folder in which numgeo would run (include names are relative to it). Defaults to the folder of path — the way pynumgeo runs jobs.

None

Raises:

Type Description
FileNotFoundError

if the file or an include is missing (numgeo stops in that case too).

pynumgeo.io.syntax

Keyword blocks: the internal lines grouped the way numgeo's readers consume them.

A :class:KeywordBlock is a keyword line, its data lines (the following lines that do not start with *) and, for container keywords, its child blocks:

===================== ============================================================================== container children ===================== ============================================================================== *STEP every block up to *ENDSTEP (src/input/read_step_.f90) *PART blocks up to *ENDPART; *INSTANCE / *ASSEMBLY likewise *MATERIAL the following material sub-keywords (src/input/read_material_.f90) *INTERACTION *NORMAL / *FRICTION lines that follow *OUTPUT *FREQUENCY, *NODE[OUTPUT], *ELEMENT[OUTPUT], *INTEGRATIONPOINT *DYNAMIC *HILBER-HUGHES-TAYLOR / *HHT / *BACKWARDEULER / *BWE *EXPLICITDYNAMIC *MASSSCALING *EQUATION *PENALTY (also under *RIGIDBODY) ===================== ==============================================================================

The grouping is purely syntactic; whether numgeo accepts a block is decided by the reader and the validator.

Examples:

>>> from pynumgeo.io.lexer import preprocess_text
>>> blocks = parse_blocks(preprocess_text(
...     "*Material, name=Soil\n*Density\n2.0\n*Step, name=S1\n*Static\n1,1,1,1\n*End step\n").lines)
>>> [(b.keyword, [c.keyword for c in b.children]) for b in blocks]
[('*AMPLITUDE', []), ('*MATERIAL', ['*DENSITY']), ('*STEP', ['*STATIC', '*ENDSTEP'])]
>>> blocks[1].option("NAME").value_cased
'Soil'

Option(key, value, key_cased, value_cased) dataclass

One comma-separated option of a keyword line.

key is the text before the first = (upper-case), value the text after it (None for a flag). The *_cased variants keep the user's spelling.

token property

The whole option as numgeo sees it.

KeywordBlock(line, keyword, value, value_cased, options, data=list(), children=list()) dataclass

A keyword line with its data lines and child blocks.

Attributes:

Name Type Description
line Line

the keyword line.

keyword str

upper-case keyword without options, e.g. *STEP or *MECHANICAL.

value str | None

text after = in the keyword itself (*MECHANICAL=MOHR-COULOMB), upper-case.

value_cased str | None

value in the user's spelling.

options list[Option]

the comma-separated options after the keyword.

data list[Line]

data lines (lines not starting with *).

children list[KeywordBlock]

child blocks of a container keyword.

pos property

Source position of the keyword line.

option_text property

Upper-case text after the first comma (what many numgeo readers search with index).

option(key)

First option whose key equals key exactly (upper-case comparison).

has_flag(key)

True if an option with exactly this key is present (with or without value).

data_tokens()

Data lines split at commas: (line, tokens, cased tokens).

walk()

This block and all descendants, depth first.

split_options(text)

Split at commas that are not inside a $...$ string.

split_options("SOLVER,EXTERNAL,ARG=$-A,-B$,MPI=2") ['SOLVER', 'EXTERNAL', 'ARG=$-A,-B$', 'MPI=2']

parse_blocks(lines)

Group internal lines into a tree of :class:KeywordBlock (see the module docstring).

Raises:

Type Description
ValueError

if a data line comes before the first keyword.

pynumgeo.io.numbers

Numbers as numgeo reads and writes them.

numgeo reads values with Fortran list-directed input (read(line, *)). That accepts more spellings than Python's float: 210d6, 1.0D-3, 1.0+5 (exponent without letter), .5 and 5.. Result files contain numbers such as 0.123456-100 (three-digit exponent without E).

pynumgeo writes reals as Python's shortest round-trip representation, which numgeo reads back exactly, and never writes inf or nan.

Examples:

>>> parse_real("210d6")
210000000.0
>>> parse_real("1.0+5")
100000.0
>>> parse_real(".5")
0.5
>>> format_real(1e-5), format_real(210e6), format_real(-0.0)
('1e-05', '210000000.0', '-0.0')
>>> is_number("FIX")
False

is_number(token)

True if token is a number in Fortran list-directed syntax (no inf/nan).

parse_real(token)

Parse a real the way Fortran list-directed input does.

Raises:

Type Description
ValueError

if the token is not a Fortran real.

parse_int(token)

Parse an integer (list-directed input; no decimal point allowed).

Raises:

Type Description
ValueError

if the token is not an integer.

format_real(value)

Shortest text that numgeo reads back as exactly value.

Raises:

Type Description
ValueError

for inf and nan, which numgeo input must not contain.

format_int(value)

Integer as text.

to_token(text)

Data token: int for integers, float for reals (Fortran syntax), else the text unchanged.

This is how the reader turns the fields of a data line into tokens; :class:~pynumgeo.deck.Keyword applies it to text tokens as well, so that "2.0" and 2.0 are the same token.

to_token("12"), to_token("0.0d0"), to_token("Soil.all") (12, 0.0, 'Soil.all')