fem-post 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ .venv/
5
+ .pytest_cache/
6
+ .ty_cache/
7
+ .ruff_cache/
8
+ .coverage
9
+ htmlcov/
10
+ dist/
11
+ build/
12
+ examples/vtk_output/
fem_post-0.1.2/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Gustavo Martins
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,177 @@
1
+ Metadata-Version: 2.5
2
+ Name: fem-post
3
+ Version: 0.1.2
4
+ Summary: VTK post-processing API for fem-core results: views of meshes and fields, exported to .vtu/.png or shown interactively
5
+ Author-email: Gustavo Martins <gucmartins@gmail.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: fea,fem,ngsolve,post-processing,visualization,vtk
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Scientific/Engineering :: Visualization
15
+ Requires-Python: >=3.10
16
+ Requires-Dist: ngsolve>=6.2.2404
17
+ Requires-Dist: numpy>=1.24.0
18
+ Requires-Dist: vtk>=9.3.0
19
+ Description-Content-Type: text/markdown
20
+
21
+ # fem-post
22
+
23
+ A Python API for post-processing [fem-core](https://github.com/gcmartins/fem-core) results with VTK.
24
+ You create **views** of a mesh and the fields solved on it, update them, and export them to `.vtu`
25
+ (for [ParaView](https://www.paraview.org/)) or `.png`, or open them in an interactive window.
26
+
27
+ > fem-post lives in the fem-core repository for now (`packages/fem-post/`). It is self-contained
28
+ > (its own `pyproject.toml`, sources and tests, no `fem_core` import), so it can move to its own
29
+ > repository unchanged.
30
+
31
+ ## Installation
32
+
33
+ ```bash
34
+ pip install ./packages/fem-post # standalone
35
+ uv sync --extra viz # from the fem-core repository root (uv workspace member)
36
+ ```
37
+
38
+ Dependencies: `ngsolve`, `numpy`, `vtk`. fem-post does **not** depend on fem-core. Solver results
39
+ are accepted by shape (see `fem_post.results`), so fem-core's `StaticResult`, `ModalResult` and
40
+ `FrequencySweepResult` work directly, and so does any object with the same attributes.
41
+
42
+ ## Usage
43
+
44
+ ```python
45
+ from fem_post import PostProcessor
46
+
47
+ post = PostProcessor(mesh) # samples the mesh once; every view shares it
48
+
49
+ mesh_view = post.add_mesh_view("mesh") # colored by material region
50
+ mesh_view.export_png("out/mesh.png")
51
+
52
+ pressure = post.add_field_view("pressure", static.result) # GridFunction, CoefficientFunction or StaticResult
53
+ pressure.export_vtu("out/pressure") # out/pressure.vtu
54
+ pressure.export_png("out/pressure.png")
55
+
56
+ modes = post.add_modal_view("modes", modal.result, deformed=True)
57
+ modes.export_vtu("out/modes") # every mode in one file
58
+ modes.export_pngs("out") # out/mode1.png, out/mode2.png, ...
59
+
60
+ post.add_frequency_sweep_view("pressure_sweep", sweep.result)
61
+ post.export_vtu("out/everything") # several views' fields in one .vtu
62
+ ```
63
+
64
+ ### Views
65
+
66
+ | Factory | View | Steps |
67
+ |---|---|---|
68
+ | `add_mesh_view(name="mesh", color_by_material=True)` | `MeshView` | one: the geometry |
69
+ | `add_field_view(name, field)` | `FieldView` | one: the field |
70
+ | `add_frequency_sweep_view(name, result)` | `FrequencySweepView` | one per frequency: `pressure_0`, `pressure_1`, … |
71
+ | `add_modal_view(name, result)` | `ModalView` | one per mode: `mode1`, `mode2`, … |
72
+
73
+ View names are unique within a `PostProcessor`: `post["modes"]`, `"modes" in post`,
74
+ `post.views`, `post.remove_view("modes")`.
75
+
76
+ Step labels are built from indices, never from frequency values, so code can rebuild them
77
+ exactly. A sweep view named `pressure` has steps `pressure_0`, `pressure_1`, … in the same order
78
+ as `result.frequencies` (0-based, like `select_step(i)`). A modal view has steps `mode1`, `mode2`,
79
+ … (1-based, like mode numbers). A step's label is also its `.vtu` array name and its PNG file name.
80
+ The frequencies are in `FrequencySweepView.frequencies`, `ModalView.frequencies_hz` and the
81
+ colorbar titles (e.g. "Mode 1 (41.93 Hz)").
82
+
83
+ ### Updating a view
84
+
85
+ Every view can be changed after it is created, and every change method returns the view, so calls
86
+ chain:
87
+
88
+ - **Field data.** `view.update(new_field_or_result)` re-evaluates the view with new data (e.g.
89
+ after a re-solve) and keeps its settings. On a `MeshView`, `set_color_by_material(bool)`
90
+ plays this role.
91
+ - **Display settings.** `view.configure(**settings)` changes how the view is drawn. See the
92
+ settings below.
93
+ - **Current step.** `view.select_step(i)` or `view.select_step("mode2")` sets the step
94
+ that `export_png()` and `show()` use. `view.steps` lists the step labels and `view.step` gives
95
+ the current index.
96
+
97
+ ```python
98
+ modes.update(new_modal.result).configure(part="real", warp_scale=50.0).select_step(2).export_png("out/mode3.png")
99
+ ```
100
+
101
+ ### Display settings
102
+
103
+ Pass these to any `add_*_view()` call or to `configure()`. They are stored as an immutable
104
+ `DisplaySettings`, available as `view.settings`.
105
+
106
+ | Setting | Default | Meaning |
107
+ |---|---|---|
108
+ | `part` | `"abs"` | For a complex field: `"abs"` (the pointwise norm), `"real"` or `"imag"` |
109
+ | `edges` | `True` | Draw element edges |
110
+ | `deformed` | `False` | Warp the geometry by the shown vector field |
111
+ | `warp_scale` | `None` | Fixed deformation factor. `None` auto-scales the largest displacement to 10% of the geometry's size, and the scale used is printed on the image |
112
+ | `value_range` | `None` | Colormap `(min, max)`. `None` uses the data range |
113
+ | `colorbar_title` | `None` | Override the default title, e.g. `Mode 1 (41.93 Hz)` |
114
+ | `background` | white | RGB tuple |
115
+ | `azimuth`, `elevation` | `None` | Camera rotation in degrees. `None` gives 30°/20° for 3D and a head-on view for a flat 2D mesh |
116
+
117
+ ### Exporting and viewing
118
+
119
+ - `view.export_vtu(path)` writes the mesh and every step's arrays to one file. `.vtu` is appended
120
+ if missing.
121
+ - `post.export_vtu(path, views=None)` writes the arrays of several views (default: all) to one file.
122
+ - `view.export_png(path, step=None, width=1200, height=900)` renders one step off-screen.
123
+ - `view.export_pngs(directory)` writes `<step label>.png` for every step.
124
+ - `view.show()` opens an interactive window. `n`/Right and `p`/Left flip between steps.
125
+ - `view.point_array(step=None)` returns the shown values as a numpy array, and `view.grid` is the
126
+ underlying `vtkUnstructuredGrid` for custom VTK work.
127
+
128
+ PNG export needs an OpenGL context but no display. On headless Linux, run under `xvfb-run -a`.
129
+ `show()` needs a real display. Do not run it under `xvfb-run`, which gives it an invisible display.
130
+
131
+ ### Embedding in your own window
132
+
133
+ `show()` opens a standalone window. To draw a view in a window you own, such
134
+ as a Qt-embedded `QVTKRenderWindowInteractor`, build a `Scene` from the view's
135
+ grid, attach it to your render window and draw a step:
136
+
137
+ ```python
138
+ from fem_post.scene import Scene
139
+
140
+ view = post.add_mesh_view(edges=True)
141
+ scene = Scene(view.grid)
142
+ scene.attach(render_window) # adds the surface and colorbar renderers
143
+ view.draw(scene) # current step; or view.draw(scene, "mode2")
144
+ render_window.Render()
145
+
146
+ view.draw(scene, 1, fit_camera=False) # switch step, keep the camera
147
+ scene.detach(render_window) # before attaching a different scene
148
+ ```
149
+
150
+ A `Scene` is bound to one grid: after `update()` or `set_color_by_material()`
151
+ replace `view.grid`, build a new `Scene`.
152
+
153
+ ### How fields are sampled
154
+
155
+ Each volume element is refined `subdivision` times on its reference element (2^subdivision cells
156
+ per edge, default `PostProcessor(mesh, subdivision=2)`). The refined points are mapped through the
157
+ element transformation with `ngsolve.Mesh.MapToAllElements`, and each field is evaluated there in
158
+ one vectorized call. As a result:
159
+
160
+ - **curved elements** (`curvature_order > 1`) render with their true curved geometry;
161
+ - **higher-order fields** render smoothly rather than piecewise-linear on coarse elements;
162
+ - **discontinuous fields** (material index, L2 spaces) stay sharp, because points are per element.
163
+
164
+ The geometry is sampled once per `PostProcessor` and shared by all its views, so adding or updating
165
+ a view only evaluates its fields. Supported element types are TRIG and QUAD (2D) and TET, HEX and
166
+ PRISM (3D). PYRAMID is supported but always unrefined.
167
+
168
+ A complex field `name` is stored as `name_re`, `name_im` and `name_abs`, and a 2D vector field is
169
+ padded to 3 components.
170
+
171
+ The lower-level `MeshSampler` and `mesh_to_grid()` build grids directly, for custom VTK pipelines.
172
+
173
+ ## Development
174
+
175
+ ```bash
176
+ xvfb-run -a uv run pytest packages/fem-post/tests # from the fem-core repository root
177
+ ```
@@ -0,0 +1,157 @@
1
+ # fem-post
2
+
3
+ A Python API for post-processing [fem-core](https://github.com/gcmartins/fem-core) results with VTK.
4
+ You create **views** of a mesh and the fields solved on it, update them, and export them to `.vtu`
5
+ (for [ParaView](https://www.paraview.org/)) or `.png`, or open them in an interactive window.
6
+
7
+ > fem-post lives in the fem-core repository for now (`packages/fem-post/`). It is self-contained
8
+ > (its own `pyproject.toml`, sources and tests, no `fem_core` import), so it can move to its own
9
+ > repository unchanged.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pip install ./packages/fem-post # standalone
15
+ uv sync --extra viz # from the fem-core repository root (uv workspace member)
16
+ ```
17
+
18
+ Dependencies: `ngsolve`, `numpy`, `vtk`. fem-post does **not** depend on fem-core. Solver results
19
+ are accepted by shape (see `fem_post.results`), so fem-core's `StaticResult`, `ModalResult` and
20
+ `FrequencySweepResult` work directly, and so does any object with the same attributes.
21
+
22
+ ## Usage
23
+
24
+ ```python
25
+ from fem_post import PostProcessor
26
+
27
+ post = PostProcessor(mesh) # samples the mesh once; every view shares it
28
+
29
+ mesh_view = post.add_mesh_view("mesh") # colored by material region
30
+ mesh_view.export_png("out/mesh.png")
31
+
32
+ pressure = post.add_field_view("pressure", static.result) # GridFunction, CoefficientFunction or StaticResult
33
+ pressure.export_vtu("out/pressure") # out/pressure.vtu
34
+ pressure.export_png("out/pressure.png")
35
+
36
+ modes = post.add_modal_view("modes", modal.result, deformed=True)
37
+ modes.export_vtu("out/modes") # every mode in one file
38
+ modes.export_pngs("out") # out/mode1.png, out/mode2.png, ...
39
+
40
+ post.add_frequency_sweep_view("pressure_sweep", sweep.result)
41
+ post.export_vtu("out/everything") # several views' fields in one .vtu
42
+ ```
43
+
44
+ ### Views
45
+
46
+ | Factory | View | Steps |
47
+ |---|---|---|
48
+ | `add_mesh_view(name="mesh", color_by_material=True)` | `MeshView` | one: the geometry |
49
+ | `add_field_view(name, field)` | `FieldView` | one: the field |
50
+ | `add_frequency_sweep_view(name, result)` | `FrequencySweepView` | one per frequency: `pressure_0`, `pressure_1`, … |
51
+ | `add_modal_view(name, result)` | `ModalView` | one per mode: `mode1`, `mode2`, … |
52
+
53
+ View names are unique within a `PostProcessor`: `post["modes"]`, `"modes" in post`,
54
+ `post.views`, `post.remove_view("modes")`.
55
+
56
+ Step labels are built from indices, never from frequency values, so code can rebuild them
57
+ exactly. A sweep view named `pressure` has steps `pressure_0`, `pressure_1`, … in the same order
58
+ as `result.frequencies` (0-based, like `select_step(i)`). A modal view has steps `mode1`, `mode2`,
59
+ … (1-based, like mode numbers). A step's label is also its `.vtu` array name and its PNG file name.
60
+ The frequencies are in `FrequencySweepView.frequencies`, `ModalView.frequencies_hz` and the
61
+ colorbar titles (e.g. "Mode 1 (41.93 Hz)").
62
+
63
+ ### Updating a view
64
+
65
+ Every view can be changed after it is created, and every change method returns the view, so calls
66
+ chain:
67
+
68
+ - **Field data.** `view.update(new_field_or_result)` re-evaluates the view with new data (e.g.
69
+ after a re-solve) and keeps its settings. On a `MeshView`, `set_color_by_material(bool)`
70
+ plays this role.
71
+ - **Display settings.** `view.configure(**settings)` changes how the view is drawn. See the
72
+ settings below.
73
+ - **Current step.** `view.select_step(i)` or `view.select_step("mode2")` sets the step
74
+ that `export_png()` and `show()` use. `view.steps` lists the step labels and `view.step` gives
75
+ the current index.
76
+
77
+ ```python
78
+ modes.update(new_modal.result).configure(part="real", warp_scale=50.0).select_step(2).export_png("out/mode3.png")
79
+ ```
80
+
81
+ ### Display settings
82
+
83
+ Pass these to any `add_*_view()` call or to `configure()`. They are stored as an immutable
84
+ `DisplaySettings`, available as `view.settings`.
85
+
86
+ | Setting | Default | Meaning |
87
+ |---|---|---|
88
+ | `part` | `"abs"` | For a complex field: `"abs"` (the pointwise norm), `"real"` or `"imag"` |
89
+ | `edges` | `True` | Draw element edges |
90
+ | `deformed` | `False` | Warp the geometry by the shown vector field |
91
+ | `warp_scale` | `None` | Fixed deformation factor. `None` auto-scales the largest displacement to 10% of the geometry's size, and the scale used is printed on the image |
92
+ | `value_range` | `None` | Colormap `(min, max)`. `None` uses the data range |
93
+ | `colorbar_title` | `None` | Override the default title, e.g. `Mode 1 (41.93 Hz)` |
94
+ | `background` | white | RGB tuple |
95
+ | `azimuth`, `elevation` | `None` | Camera rotation in degrees. `None` gives 30°/20° for 3D and a head-on view for a flat 2D mesh |
96
+
97
+ ### Exporting and viewing
98
+
99
+ - `view.export_vtu(path)` writes the mesh and every step's arrays to one file. `.vtu` is appended
100
+ if missing.
101
+ - `post.export_vtu(path, views=None)` writes the arrays of several views (default: all) to one file.
102
+ - `view.export_png(path, step=None, width=1200, height=900)` renders one step off-screen.
103
+ - `view.export_pngs(directory)` writes `<step label>.png` for every step.
104
+ - `view.show()` opens an interactive window. `n`/Right and `p`/Left flip between steps.
105
+ - `view.point_array(step=None)` returns the shown values as a numpy array, and `view.grid` is the
106
+ underlying `vtkUnstructuredGrid` for custom VTK work.
107
+
108
+ PNG export needs an OpenGL context but no display. On headless Linux, run under `xvfb-run -a`.
109
+ `show()` needs a real display. Do not run it under `xvfb-run`, which gives it an invisible display.
110
+
111
+ ### Embedding in your own window
112
+
113
+ `show()` opens a standalone window. To draw a view in a window you own, such
114
+ as a Qt-embedded `QVTKRenderWindowInteractor`, build a `Scene` from the view's
115
+ grid, attach it to your render window and draw a step:
116
+
117
+ ```python
118
+ from fem_post.scene import Scene
119
+
120
+ view = post.add_mesh_view(edges=True)
121
+ scene = Scene(view.grid)
122
+ scene.attach(render_window) # adds the surface and colorbar renderers
123
+ view.draw(scene) # current step; or view.draw(scene, "mode2")
124
+ render_window.Render()
125
+
126
+ view.draw(scene, 1, fit_camera=False) # switch step, keep the camera
127
+ scene.detach(render_window) # before attaching a different scene
128
+ ```
129
+
130
+ A `Scene` is bound to one grid: after `update()` or `set_color_by_material()`
131
+ replace `view.grid`, build a new `Scene`.
132
+
133
+ ### How fields are sampled
134
+
135
+ Each volume element is refined `subdivision` times on its reference element (2^subdivision cells
136
+ per edge, default `PostProcessor(mesh, subdivision=2)`). The refined points are mapped through the
137
+ element transformation with `ngsolve.Mesh.MapToAllElements`, and each field is evaluated there in
138
+ one vectorized call. As a result:
139
+
140
+ - **curved elements** (`curvature_order > 1`) render with their true curved geometry;
141
+ - **higher-order fields** render smoothly rather than piecewise-linear on coarse elements;
142
+ - **discontinuous fields** (material index, L2 spaces) stay sharp, because points are per element.
143
+
144
+ The geometry is sampled once per `PostProcessor` and shared by all its views, so adding or updating
145
+ a view only evaluates its fields. Supported element types are TRIG and QUAD (2D) and TET, HEX and
146
+ PRISM (3D). PYRAMID is supported but always unrefined.
147
+
148
+ A complex field `name` is stored as `name_re`, `name_im` and `name_abs`, and a 2D vector field is
149
+ padded to 3 components.
150
+
151
+ The lower-level `MeshSampler` and `mesh_to_grid()` build grids directly, for custom VTK pipelines.
152
+
153
+ ## Development
154
+
155
+ ```bash
156
+ xvfb-run -a uv run pytest packages/fem-post/tests # from the fem-core repository root
157
+ ```
@@ -0,0 +1,44 @@
1
+ [project]
2
+ name = "fem-post"
3
+ version = "0.1.2"
4
+ description = "VTK post-processing API for fem-core results: views of meshes and fields, exported to .vtu/.png or shown interactively"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ dependencies = [
10
+ "ngsolve>=6.2.2404",
11
+ "numpy>=1.24.0",
12
+ "vtk>=9.3.0",
13
+ ]
14
+ authors = [{ name = "Gustavo Martins", email = "gucmartins@gmail.com" }]
15
+ keywords = ["fem", "fea", "ngsolve", "vtk", "post-processing", "visualization"]
16
+ classifiers = [
17
+ "Development Status :: 3 - Alpha",
18
+ "Intended Audience :: Developers",
19
+ "Operating System :: OS Independent",
20
+ "Topic :: Scientific/Engineering :: Visualization",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.12",
23
+ ]
24
+
25
+ [build-system]
26
+ requires = ["hatchling>=1.27"]
27
+ build-backend = "hatchling.build"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/fem_post"]
31
+
32
+ [tool.pytest.ini_options]
33
+ testpaths = ["tests"]
34
+ pythonpath = ["src"]
35
+
36
+ [tool.ty.environment]
37
+ python-version = "3.11"
38
+
39
+ [tool.ruff]
40
+ line-length = 120
41
+
42
+ [tool.ruff.lint]
43
+ select = ["E", "F", "PLC0415"]
44
+ ignore = ["F401"]
@@ -0,0 +1,41 @@
1
+ """
2
+ fem-post: VTK post-processing for fem-core results.
3
+
4
+ Create a PostProcessor for a mesh, add views to it -- the mesh, a field, a
5
+ frequency sweep, mode shapes -- then update them (new data, display settings,
6
+ current step) and export them (.vtu, .png) or show them interactively:
7
+
8
+ from fem_post import PostProcessor
9
+
10
+ post = PostProcessor(mesh)
11
+ post.add_mesh_view("mesh").export_png("out/mesh.png")
12
+ sweep = post.add_frequency_sweep_view("pressure", sweep_result)
13
+ sweep.export_vtu("out/sweep")
14
+ sweep.configure(part="real", value_range=(-1.0, 1.0)).export_pngs("out")
15
+
16
+ Solver results are consumed by shape (fem_post.results), so fem_post does not
17
+ depend on fem_core itself.
18
+ """
19
+
20
+ from fem_post.conversion import MATERIAL_FIELD, MeshSampler, mesh_to_grid
21
+ from fem_post.manager import PostProcessor
22
+ from fem_post.naming import complex_field_names, frequency_sweep_field_name, modal_field_name, modal_frequency_hz
23
+ from fem_post.settings import DisplaySettings
24
+ from fem_post.views import FieldView, FrequencySweepView, MeshView, ModalView, View
25
+
26
+ __all__ = [
27
+ "MATERIAL_FIELD",
28
+ "DisplaySettings",
29
+ "FieldView",
30
+ "FrequencySweepView",
31
+ "MeshSampler",
32
+ "MeshView",
33
+ "ModalView",
34
+ "PostProcessor",
35
+ "View",
36
+ "complex_field_names",
37
+ "frequency_sweep_field_name",
38
+ "mesh_to_grid",
39
+ "modal_field_name",
40
+ "modal_frequency_hz",
41
+ ]