Metadata-Version: 2.5
Name: fem-post
Version: 0.1.2
Summary: VTK post-processing API for fem-core results: views of meshes and fields, exported to .vtu/.png or shown interactively
Author-email: Gustavo Martins <gucmartins@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: fea,fem,ngsolve,post-processing,visualization,vtk
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Visualization
Requires-Python: >=3.10
Requires-Dist: ngsolve>=6.2.2404
Requires-Dist: numpy>=1.24.0
Requires-Dist: vtk>=9.3.0
Description-Content-Type: text/markdown

# fem-post

A Python API for post-processing [fem-core](https://github.com/gcmartins/fem-core) results with VTK.
You create **views** of a mesh and the fields solved on it, update them, and export them to `.vtu`
(for [ParaView](https://www.paraview.org/)) or `.png`, or open them in an interactive window.

> fem-post lives in the fem-core repository for now (`packages/fem-post/`). It is self-contained
> (its own `pyproject.toml`, sources and tests, no `fem_core` import), so it can move to its own
> repository unchanged.

## Installation

```bash
pip install ./packages/fem-post       # standalone
uv sync --extra viz                   # from the fem-core repository root (uv workspace member)
```

Dependencies: `ngsolve`, `numpy`, `vtk`. fem-post does **not** depend on fem-core. Solver results
are accepted by shape (see `fem_post.results`), so fem-core's `StaticResult`, `ModalResult` and
`FrequencySweepResult` work directly, and so does any object with the same attributes.

## Usage

```python
from fem_post import PostProcessor

post = PostProcessor(mesh)  # samples the mesh once; every view shares it

mesh_view = post.add_mesh_view("mesh")  # colored by material region
mesh_view.export_png("out/mesh.png")

pressure = post.add_field_view("pressure", static.result)  # GridFunction, CoefficientFunction or StaticResult
pressure.export_vtu("out/pressure")  # out/pressure.vtu
pressure.export_png("out/pressure.png")

modes = post.add_modal_view("modes", modal.result, deformed=True)
modes.export_vtu("out/modes")  # every mode in one file
modes.export_pngs("out")  # out/mode1.png, out/mode2.png, ...

post.add_frequency_sweep_view("pressure_sweep", sweep.result)
post.export_vtu("out/everything")  # several views' fields in one .vtu
```

### Views

| Factory | View | Steps |
|---|---|---|
| `add_mesh_view(name="mesh", color_by_material=True)` | `MeshView` | one: the geometry |
| `add_field_view(name, field)` | `FieldView` | one: the field |
| `add_frequency_sweep_view(name, result)` | `FrequencySweepView` | one per frequency: `pressure_0`, `pressure_1`, … |
| `add_modal_view(name, result)` | `ModalView` | one per mode: `mode1`, `mode2`, … |

View names are unique within a `PostProcessor`: `post["modes"]`, `"modes" in post`,
`post.views`, `post.remove_view("modes")`.

Step labels are built from indices, never from frequency values, so code can rebuild them
exactly. A sweep view named `pressure` has steps `pressure_0`, `pressure_1`, … in the same order
as `result.frequencies` (0-based, like `select_step(i)`). A modal view has steps `mode1`, `mode2`,
… (1-based, like mode numbers). A step's label is also its `.vtu` array name and its PNG file name.
The frequencies are in `FrequencySweepView.frequencies`, `ModalView.frequencies_hz` and the
colorbar titles (e.g. "Mode 1 (41.93 Hz)").

### Updating a view

Every view can be changed after it is created, and every change method returns the view, so calls
chain:

- **Field data.** `view.update(new_field_or_result)` re-evaluates the view with new data (e.g.
  after a re-solve) and keeps its settings. On a `MeshView`, `set_color_by_material(bool)`
  plays this role.
- **Display settings.** `view.configure(**settings)` changes how the view is drawn. See the
  settings below.
- **Current step.** `view.select_step(i)` or `view.select_step("mode2")` sets the step
  that `export_png()` and `show()` use. `view.steps` lists the step labels and `view.step` gives
  the current index.

```python
modes.update(new_modal.result).configure(part="real", warp_scale=50.0).select_step(2).export_png("out/mode3.png")
```

### Display settings

Pass these to any `add_*_view()` call or to `configure()`. They are stored as an immutable
`DisplaySettings`, available as `view.settings`.

| Setting | Default | Meaning |
|---|---|---|
| `part` | `"abs"` | For a complex field: `"abs"` (the pointwise norm), `"real"` or `"imag"` |
| `edges` | `True` | Draw element edges |
| `deformed` | `False` | Warp the geometry by the shown vector field |
| `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 |
| `value_range` | `None` | Colormap `(min, max)`. `None` uses the data range |
| `colorbar_title` | `None` | Override the default title, e.g. `Mode 1 (41.93 Hz)` |
| `background` | white | RGB tuple |
| `azimuth`, `elevation` | `None` | Camera rotation in degrees. `None` gives 30°/20° for 3D and a head-on view for a flat 2D mesh |

### Exporting and viewing

- `view.export_vtu(path)` writes the mesh and every step's arrays to one file. `.vtu` is appended
  if missing.
- `post.export_vtu(path, views=None)` writes the arrays of several views (default: all) to one file.
- `view.export_png(path, step=None, width=1200, height=900)` renders one step off-screen.
- `view.export_pngs(directory)` writes `<step label>.png` for every step.
- `view.show()` opens an interactive window. `n`/Right and `p`/Left flip between steps.
- `view.point_array(step=None)` returns the shown values as a numpy array, and `view.grid` is the
  underlying `vtkUnstructuredGrid` for custom VTK work.

PNG export needs an OpenGL context but no display. On headless Linux, run under `xvfb-run -a`.
`show()` needs a real display. Do not run it under `xvfb-run`, which gives it an invisible display.

### Embedding in your own window

`show()` opens a standalone window. To draw a view in a window you own, such
as a Qt-embedded `QVTKRenderWindowInteractor`, build a `Scene` from the view's
grid, attach it to your render window and draw a step:

```python
from fem_post.scene import Scene

view = post.add_mesh_view(edges=True)
scene = Scene(view.grid)
scene.attach(render_window)            # adds the surface and colorbar renderers
view.draw(scene)                       # current step; or view.draw(scene, "mode2")
render_window.Render()

view.draw(scene, 1, fit_camera=False)  # switch step, keep the camera
scene.detach(render_window)            # before attaching a different scene
```

A `Scene` is bound to one grid: after `update()` or `set_color_by_material()`
replace `view.grid`, build a new `Scene`.

### How fields are sampled

Each volume element is refined `subdivision` times on its reference element (2^subdivision cells
per edge, default `PostProcessor(mesh, subdivision=2)`). The refined points are mapped through the
element transformation with `ngsolve.Mesh.MapToAllElements`, and each field is evaluated there in
one vectorized call. As a result:

- **curved elements** (`curvature_order > 1`) render with their true curved geometry;
- **higher-order fields** render smoothly rather than piecewise-linear on coarse elements;
- **discontinuous fields** (material index, L2 spaces) stay sharp, because points are per element.

The geometry is sampled once per `PostProcessor` and shared by all its views, so adding or updating
a view only evaluates its fields. Supported element types are TRIG and QUAD (2D) and TET, HEX and
PRISM (3D). PYRAMID is supported but always unrefined.

A complex field `name` is stored as `name_re`, `name_im` and `name_abs`, and a 2D vector field is
padded to 3 components.

The lower-level `MeshSampler` and `mesh_to_grid()` build grids directly, for custom VTK pipelines.

## Development

```bash
xvfb-run -a uv run pytest packages/fem-post/tests    # from the fem-core repository root
```
