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 |
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'
|
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 |
required |
strict
|
bool
|
raise :class: |
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 |
True
|
comments
|
bool
|
write the |
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]
|
|
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:
- Every physical line is read into 256 characters; the rest is lost (:49, :58).
**starts a comment anywhere in a line; blank lines are dropped (:64-68, :134-153).- A line whose upper-cased text contains
INCLUDEis an include: the text after the first=(leading and trailing blanks removed) plus.inpis copied in; includes are not nested (:70-97). - Lines are upper-cased (ASCII letters only) (:127).
- All blanks are deleted, except between the first and the last
$of a line (:156-160). - A comma at the end of a line is removed (:162-174).
- The line
*AMPLITUDE,NAME=DEFAULT,TYPE=RAMPis 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 |
pos |
SourcePos | None
|
source position, |
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 |
None
|
preprocess(path, workdir=None)
¶
Preprocess an input file like numgeo.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | PathLike[str]
|
the master |
required |
workdir
|
str | PathLike[str] | None
|
folder in which numgeo would run (include names are relative to it). Defaults to the
folder of |
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. |
value |
str | None
|
text after |
value_cased |
str | None
|
|
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')