FEMlium 0.1.0__py3-none-any.whl
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.
- femlium/__init__.py +33 -0
- femlium/base_mesh_plotter.py +263 -0
- femlium/base_plotter.py +64 -0
- femlium/base_solution_plotter.py +384 -0
- femlium/dolfinx_plotter.py +206 -0
- femlium/domain_plotter.py +151 -0
- femlium/firedrake_plotter.py +200 -0
- femlium/meshio_plotter.py +90 -0
- femlium/numpy_plotter.py +14 -0
- femlium/utils/__init__.py +10 -0
- femlium/utils/colorbar_wrapper.py +34 -0
- femlium/utils/geojson_with_arrows.py +90 -0
- femlium/utils/transformer_wrapper.py +52 -0
- femlium-0.1.0.dist-info/METADATA +79 -0
- femlium-0.1.0.dist-info/RECORD +19 -0
- femlium-0.1.0.dist-info/WHEEL +5 -0
- femlium-0.1.0.dist-info/licenses/AUTHORS +1 -0
- femlium-0.1.0.dist-info/licenses/LICENSE +7 -0
- femlium-0.1.0.dist-info/top_level.txt +1 -0
femlium/__init__.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Copyright (C) 2021-2025 by the FEMlium authors
|
|
2
|
+
#
|
|
3
|
+
# This file is part of FEMlium.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: MIT
|
|
6
|
+
"""FEMlium main module."""
|
|
7
|
+
|
|
8
|
+
from femlium.base_mesh_plotter import BaseMeshPlotter
|
|
9
|
+
from femlium.base_plotter import BasePlotter
|
|
10
|
+
from femlium.base_solution_plotter import BaseSolutionPlotter
|
|
11
|
+
from femlium.domain_plotter import DomainPlotter
|
|
12
|
+
from femlium.numpy_plotter import NumpyPlotter
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
import meshio
|
|
16
|
+
except ImportError: # pragma: no cover
|
|
17
|
+
pass
|
|
18
|
+
else:
|
|
19
|
+
from femlium.meshio_plotter import MeshioPlotter
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import dolfinx
|
|
23
|
+
except ImportError:
|
|
24
|
+
pass
|
|
25
|
+
else:
|
|
26
|
+
from femlium.dolfinx_plotter import DolfinxPlotter
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
import firedrake
|
|
30
|
+
except ImportError:
|
|
31
|
+
pass
|
|
32
|
+
else:
|
|
33
|
+
from femlium.firedrake_plotter import FiredrakePlotter
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Copyright (C) 2021-2025 by the FEMlium authors
|
|
2
|
+
#
|
|
3
|
+
# This file is part of FEMlium.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: MIT
|
|
6
|
+
"""Base interface of a geographic plotter for mesh-related plots."""
|
|
7
|
+
|
|
8
|
+
import typing
|
|
9
|
+
|
|
10
|
+
import folium
|
|
11
|
+
import geojson
|
|
12
|
+
import numpy as np
|
|
13
|
+
import numpy.typing as npt
|
|
14
|
+
|
|
15
|
+
from femlium.base_plotter import BasePlotter
|
|
16
|
+
from femlium.utils import ColorbarWrapper
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BaseMeshPlotter(BasePlotter):
|
|
20
|
+
"""Base interface of a geographic plotter for mesh-related plots."""
|
|
21
|
+
|
|
22
|
+
def add_mesh_to(
|
|
23
|
+
self, geo_map: folium.Map, vertices: npt.NDArray[np.float64], cells: npt.NDArray[np.int64],
|
|
24
|
+
cell_markers: typing.Optional[npt.NDArray[np.int64]] = None,
|
|
25
|
+
face_markers: typing.Optional[npt.NDArray[np.int64]] = None,
|
|
26
|
+
cell_colors: typing.Optional[typing.Union[str, dict[int, str]]] = None,
|
|
27
|
+
face_colors: typing.Optional[typing.Union[str, dict[int, str]]] = None,
|
|
28
|
+
face_weights: typing.Optional[typing.Union[int, dict[int, int]]] = None
|
|
29
|
+
) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Add a triangular mesh to a folium map.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
geo_map
|
|
36
|
+
Map to which the mesh plot should be added.
|
|
37
|
+
vertices
|
|
38
|
+
Matrix containing the coordinates of the vertices.
|
|
39
|
+
The matrix should have as many rows as vertices in the mesh, and two columns.
|
|
40
|
+
cells
|
|
41
|
+
Matrix containing the connectivity of the cells.
|
|
42
|
+
The matrix should have as many rows as cells in the mesh, and three columns.
|
|
43
|
+
cell_markers
|
|
44
|
+
Vector containing a marker (i.e., an integer number) for each cell.
|
|
45
|
+
The vector should have as many entries as cells in the mesh.
|
|
46
|
+
If not provided, the marker will be set to 0 everywhere.
|
|
47
|
+
face_markers
|
|
48
|
+
Matrix containing a marker (i.e., an integer number) for each face.
|
|
49
|
+
The matrix should have the same shape of the cells argument.
|
|
50
|
+
Given a row index r, the entry face_markers[r, 0] is the marker of the
|
|
51
|
+
face connecting the first and second vertex of the r-th cell.
|
|
52
|
+
Similarly, face_markers[r, 1] is the marker associated to the face connecting
|
|
53
|
+
the second and third vertex of the r-th cell. Finally, face_markers[r, 2] is
|
|
54
|
+
the marker associated to the face connecting the first and third vertex of the
|
|
55
|
+
r-th cell.
|
|
56
|
+
If not provided, the marker will be set to 0 everywhere.
|
|
57
|
+
cell_colors
|
|
58
|
+
If a dictionary is provided, it should contain key: value pairs defining the mapping
|
|
59
|
+
marker: color for cells.
|
|
60
|
+
If a string is provided instead of a dictionary, the same color will be used for all
|
|
61
|
+
cell markers.
|
|
62
|
+
If not provided, the cells will not be colored.
|
|
63
|
+
face_colors
|
|
64
|
+
If a dictionary is provided, it should contain key: value pairs defining the mapping
|
|
65
|
+
marker: color for faces.
|
|
66
|
+
If a string is provided instead of a dictionary, the same color will be used for all
|
|
67
|
+
face markers.
|
|
68
|
+
If not provided, a default black color will be used for faces.
|
|
69
|
+
face_weights
|
|
70
|
+
Line weight of each face. Input should be provided following a similar convention for
|
|
71
|
+
the face_colors argument.
|
|
72
|
+
If not provided, a unit weight will be used.
|
|
73
|
+
"""
|
|
74
|
+
if cell_markers is None:
|
|
75
|
+
cell_markers = np.zeros((cells.shape[0], ), dtype=np.int64)
|
|
76
|
+
else:
|
|
77
|
+
assert cell_markers.shape[0] == cells.shape[0]
|
|
78
|
+
|
|
79
|
+
if face_markers is None:
|
|
80
|
+
face_markers = np.zeros(cells.shape, dtype=np.int64)
|
|
81
|
+
else:
|
|
82
|
+
assert face_markers.shape == cells.shape
|
|
83
|
+
|
|
84
|
+
unique_cell_markers = np.unique(cell_markers).astype(int)
|
|
85
|
+
unique_face_markers = np.unique(face_markers).astype(int)
|
|
86
|
+
cell_colors = self._process_optional_argument_on_markers(cell_colors, "none", unique_cell_markers)
|
|
87
|
+
face_colors = self._process_optional_argument_on_markers(face_colors, "black", unique_face_markers)
|
|
88
|
+
face_weights = self._process_optional_argument_on_markers(face_weights, 1, unique_face_markers)
|
|
89
|
+
|
|
90
|
+
def style_function(x: dict[str, dict[str, typing.Any]]) -> dict[str, typing.Any]:
|
|
91
|
+
if x["geometry"]["type"] == "MultiPolygon":
|
|
92
|
+
return {
|
|
93
|
+
# Boundary properties
|
|
94
|
+
"stroke": x["properties"]["stroke"],
|
|
95
|
+
"color": x["properties"]["color"],
|
|
96
|
+
"weight": x["properties"]["weight"],
|
|
97
|
+
# Interior properties
|
|
98
|
+
"fill": x["properties"]["fill"],
|
|
99
|
+
"fillColor": x["properties"]["fillColor"],
|
|
100
|
+
"fillOpacity": x["properties"]["fillOpacity"]
|
|
101
|
+
}
|
|
102
|
+
elif x["geometry"]["type"] == "MultiLineString":
|
|
103
|
+
return {
|
|
104
|
+
"stroke": x["properties"]["stroke"],
|
|
105
|
+
"color": x["properties"]["color"],
|
|
106
|
+
"weight": x["properties"]["weight"]
|
|
107
|
+
}
|
|
108
|
+
else: # pragma: no cover
|
|
109
|
+
raise ValueError("Invalid type")
|
|
110
|
+
|
|
111
|
+
json = self._convert_mesh_to_geojson(
|
|
112
|
+
vertices, cells, cell_markers, face_markers, cell_colors, face_colors, face_weights)
|
|
113
|
+
folium.GeoJson(json, style_function=style_function).add_to(geo_map)
|
|
114
|
+
|
|
115
|
+
cell_colors_where_none = np.argwhere(cell_colors == "none")
|
|
116
|
+
cell_colors_not_none = np.delete(cell_colors, cell_colors_where_none)
|
|
117
|
+
cell_colors_values = np.arange(0, np.max(unique_cell_markers) + 1)
|
|
118
|
+
cell_colors_values_not_none = np.delete(cell_colors_values, cell_colors_where_none)
|
|
119
|
+
assert cell_colors_not_none.shape == cell_colors_values_not_none.shape
|
|
120
|
+
cell_colors_in_figure = np.delete(
|
|
121
|
+
cell_colors_not_none, np.setdiff1d(cell_colors_values_not_none, unique_cell_markers))
|
|
122
|
+
cell_colors_values_in_figure = np.delete(
|
|
123
|
+
cell_colors_values_not_none, np.setdiff1d(cell_colors_values_not_none, unique_cell_markers))
|
|
124
|
+
if np.unique(cell_colors_in_figure).shape[0] > 1:
|
|
125
|
+
colorbar = ColorbarWrapper(
|
|
126
|
+
colors=cell_colors_in_figure, values=cell_colors_values_in_figure, caption="Cell markers")
|
|
127
|
+
colorbar.add_to(geo_map)
|
|
128
|
+
|
|
129
|
+
face_colors_values = np.arange(0, np.max(unique_face_markers) + 1)
|
|
130
|
+
assert face_colors.shape == face_colors_values.shape
|
|
131
|
+
face_colors_in_figure = np.delete(
|
|
132
|
+
face_colors, np.setdiff1d(face_colors_values, unique_face_markers))
|
|
133
|
+
face_colors_values_in_figure = np.delete(
|
|
134
|
+
face_colors_values, np.setdiff1d(face_colors_values, unique_face_markers))
|
|
135
|
+
if np.unique(face_colors_in_figure).shape[0] > 1:
|
|
136
|
+
colorbar = ColorbarWrapper(
|
|
137
|
+
colors=face_colors_in_figure, values=face_colors_values_in_figure, caption="Face markers")
|
|
138
|
+
colorbar.add_to(geo_map)
|
|
139
|
+
|
|
140
|
+
def _convert_mesh_to_geojson(
|
|
141
|
+
self, vertices: npt.NDArray[np.float64], cells: npt.NDArray[np.int64],
|
|
142
|
+
cell_markers: npt.NDArray[np.int64], face_markers: npt.NDArray[np.int64],
|
|
143
|
+
cell_colors: typing.Union[str, dict[int, str]], face_colors: typing.Union[str, dict[int, str]],
|
|
144
|
+
face_weights: typing.Union[int, dict[int, int]]
|
|
145
|
+
) -> geojson.FeatureCollection:
|
|
146
|
+
"""
|
|
147
|
+
Convert a mesh to a geojson FeatureCollection.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
vertices
|
|
152
|
+
Matrix containing the coordinates of the vertices.
|
|
153
|
+
The matrix should have as many rows as vertices in the mesh, and two columns.
|
|
154
|
+
cells
|
|
155
|
+
Matrix containing the connectivity of the cells.
|
|
156
|
+
The matrix should have as many rows as cells in the mesh, and three columns.
|
|
157
|
+
cell_markers
|
|
158
|
+
Vector containing a marker (i.e., an integer number) for each cell.
|
|
159
|
+
The vector should have as many entries as cells in the mesh.
|
|
160
|
+
face_markers
|
|
161
|
+
Vector containing a marker (i.e., an integer number) for each face.
|
|
162
|
+
The matrix should have the same shape of the cells argument.
|
|
163
|
+
cell_colors
|
|
164
|
+
Vector associating a cell marker to its color (i.e., a string).
|
|
165
|
+
The vector should have as many entries as the number of cell markers.
|
|
166
|
+
face_colors
|
|
167
|
+
Vector associating a face marker to its color (i.e., a string).
|
|
168
|
+
The vector should have as many entries as the number of face markers.
|
|
169
|
+
face_weights
|
|
170
|
+
Vector associating a face marker to its weight (i.e., a int).
|
|
171
|
+
The vector should have as many entries as the number of face markers.
|
|
172
|
+
|
|
173
|
+
Returns
|
|
174
|
+
-------
|
|
175
|
+
:
|
|
176
|
+
A geojson FeatureCollection representing the mesh.
|
|
177
|
+
"""
|
|
178
|
+
multipolygon_coordinates = dict()
|
|
179
|
+
multipolygon_properties = dict()
|
|
180
|
+
multiline_coordinates = dict()
|
|
181
|
+
multiline_properties = dict()
|
|
182
|
+
for c in range(cells.shape[0]):
|
|
183
|
+
coordinates = [self.transformer(*vertices[cells[c, v], :]) for v in range(3)]
|
|
184
|
+
coordinates.append(coordinates[0])
|
|
185
|
+
cell_face_markers = np.unique([face_markers[c, f] for f in range(3)]).astype(np.int64)
|
|
186
|
+
if cell_face_markers.shape[0] == 1:
|
|
187
|
+
cell_key = (cell_markers[c], True)
|
|
188
|
+
cell_properties = {
|
|
189
|
+
# Boundary properties
|
|
190
|
+
"stroke": True,
|
|
191
|
+
"color": face_colors[cell_face_markers[0]],
|
|
192
|
+
"weight": int(face_weights[cell_face_markers[0]]),
|
|
193
|
+
}
|
|
194
|
+
else:
|
|
195
|
+
cell_key = (cell_markers[c], False)
|
|
196
|
+
cell_properties = {
|
|
197
|
+
# Boundary properties
|
|
198
|
+
"stroke": False,
|
|
199
|
+
"color": None,
|
|
200
|
+
"weight": None
|
|
201
|
+
}
|
|
202
|
+
if cell_colors[cell_key[0]] != "none":
|
|
203
|
+
cell_properties.update({
|
|
204
|
+
# Interior properties
|
|
205
|
+
"fill": True,
|
|
206
|
+
"fillColor": cell_colors[cell_key[0]],
|
|
207
|
+
"fillOpacity": 1
|
|
208
|
+
})
|
|
209
|
+
else:
|
|
210
|
+
cell_properties.update({
|
|
211
|
+
# Interior properties
|
|
212
|
+
"fill": False,
|
|
213
|
+
"fillColor": None,
|
|
214
|
+
"fillOpacity": None
|
|
215
|
+
})
|
|
216
|
+
# Store current cell
|
|
217
|
+
if cell_key not in multipolygon_coordinates:
|
|
218
|
+
multipolygon_coordinates[cell_key] = list()
|
|
219
|
+
multipolygon_coordinates[cell_key].append([coordinates])
|
|
220
|
+
# Store current cell properties
|
|
221
|
+
if cell_key not in multipolygon_properties:
|
|
222
|
+
multipolygon_properties[cell_key] = cell_properties
|
|
223
|
+
else:
|
|
224
|
+
assert multipolygon_properties[cell_key] == cell_properties
|
|
225
|
+
# Store faces only if there are multiple face markers in this cell,
|
|
226
|
+
# otherwise the boundary representation of the cell is sufficient.
|
|
227
|
+
if not cell_key[1]:
|
|
228
|
+
for (f, pair) in enumerate(((0, 1), (1, 2), (0, 2))):
|
|
229
|
+
face_key = face_markers[c, f]
|
|
230
|
+
# Store current face
|
|
231
|
+
if face_key not in multiline_coordinates:
|
|
232
|
+
multiline_coordinates[face_key] = list()
|
|
233
|
+
multiline_coordinates[face_key].append([coordinates[pair[0]], coordinates[pair[1]]])
|
|
234
|
+
# Store current face properties
|
|
235
|
+
face_properties = {
|
|
236
|
+
"stroke": True,
|
|
237
|
+
"color": face_colors[face_markers[c, f]],
|
|
238
|
+
"weight": int(face_weights[face_markers[c, f]]),
|
|
239
|
+
}
|
|
240
|
+
if face_key not in multiline_properties:
|
|
241
|
+
multiline_properties[face_key] = face_properties
|
|
242
|
+
else:
|
|
243
|
+
assert multiline_properties[face_key] == face_properties
|
|
244
|
+
|
|
245
|
+
multipolygon_features = list()
|
|
246
|
+
for cell_key in multipolygon_coordinates.keys():
|
|
247
|
+
multipolygon = geojson.MultiPolygon(coordinates=multipolygon_coordinates[cell_key])
|
|
248
|
+
feature = geojson.Feature(
|
|
249
|
+
geometry=multipolygon,
|
|
250
|
+
properties=multipolygon_properties[cell_key]
|
|
251
|
+
)
|
|
252
|
+
multipolygon_features.append(feature)
|
|
253
|
+
|
|
254
|
+
multiline_features = list()
|
|
255
|
+
for face_key in multiline_coordinates.keys():
|
|
256
|
+
multiline = geojson.MultiLineString(coordinates=multiline_coordinates[face_key])
|
|
257
|
+
feature = geojson.Feature(
|
|
258
|
+
geometry=multiline,
|
|
259
|
+
properties=multiline_properties[face_key]
|
|
260
|
+
)
|
|
261
|
+
multiline_features.append(feature)
|
|
262
|
+
|
|
263
|
+
return geojson.FeatureCollection(multipolygon_features + multiline_features)
|
femlium/base_plotter.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Copyright (C) 2021-2025 by the FEMlium authors
|
|
2
|
+
#
|
|
3
|
+
# This file is part of FEMlium.
|
|
4
|
+
#
|
|
5
|
+
# SPDX-License-Identifier: MIT
|
|
6
|
+
"""Interface of a geographic plotter."""
|
|
7
|
+
|
|
8
|
+
import typing
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import numpy.typing as npt
|
|
12
|
+
import pyproj
|
|
13
|
+
|
|
14
|
+
from femlium.utils import TransformerWrapper
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BasePlotter:
|
|
18
|
+
"""
|
|
19
|
+
Interface of a geographic plotter.
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
transformer
|
|
24
|
+
Defines an optional transformation between coordinate reference systems (CRS) if
|
|
25
|
+
the input data use a different CRS than the output plot.
|
|
26
|
+
If not provided, the identity map is used.
|
|
27
|
+
|
|
28
|
+
Attributes
|
|
29
|
+
----------
|
|
30
|
+
transformer
|
|
31
|
+
Wrapper to the transformer object provided as first input parameter.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, transformer: typing.Optional[pyproj.Transformer] = None) -> None:
|
|
35
|
+
self.transformer = TransformerWrapper(transformer)
|
|
36
|
+
|
|
37
|
+
@staticmethod
|
|
38
|
+
def _process_optional_argument_on_markers(
|
|
39
|
+
argument: typing.Any, default: typing.Any, unique_markers: npt.NDArray[typing.Any] # noqa: ANN401
|
|
40
|
+
) -> npt.NDArray[typing.Any]:
|
|
41
|
+
"""Fill optinal arguments related to markers with default value."""
|
|
42
|
+
expected_type = type(default)
|
|
43
|
+
assert isinstance(argument, (expected_type, dict)) or argument is None
|
|
44
|
+
if isinstance(argument, dict):
|
|
45
|
+
assert all(isinstance(value, expected_type) for (_, value) in argument.items())
|
|
46
|
+
|
|
47
|
+
if isinstance(default, str):
|
|
48
|
+
dtype = np.dtype(object) # otherwise np.dtype(str) only allows a single character
|
|
49
|
+
else:
|
|
50
|
+
dtype = np.dtype(expected_type)
|
|
51
|
+
|
|
52
|
+
assert np.min(unique_markers) >= 0
|
|
53
|
+
output = np.full(np.max(unique_markers) + 1, default, dtype=dtype)
|
|
54
|
+
for m in unique_markers:
|
|
55
|
+
if argument is None:
|
|
56
|
+
pass
|
|
57
|
+
elif isinstance(argument, expected_type):
|
|
58
|
+
output[m] = argument
|
|
59
|
+
elif isinstance(argument, dict):
|
|
60
|
+
if m in argument:
|
|
61
|
+
output[m] = argument[m]
|
|
62
|
+
else: # pragma: no cover
|
|
63
|
+
raise ValueError("Invalid argument provided")
|
|
64
|
+
return output
|