Skip to content

Commit 0e9b6dc

Browse files
committed
Initial commit
1 parent e31503c commit 0e9b6dc

File tree

6 files changed

+1291
-2
lines changed

6 files changed

+1291
-2
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
# sectionproperties-tools
2-
A set of tools for sectionproperties to extend its functionality
1+
# sectionproperties_reporting
2+
A 3rd party tool to aid in extracting results from sectionproperties

pyproject.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[project]
2+
name = "sectionproperties-reporting"
3+
version = "0.1.0"
4+
description = "Add your description here"
5+
readme = "README.md"
6+
authors = [
7+
{ name = "Connor Ferster", email = "[email protected]" }
8+
]
9+
requires-python = ">=3.13"
10+
dependencies = [
11+
"numpydantic>=1.6.9",
12+
"pydantic>=2.11.7",
13+
"sectionproperties>=3.9.0",
14+
]
15+
16+
[project.scripts]
17+
sectionproperties-reporting = "sectionproperties_reporting:main"
18+
19+
[build-system]
20+
requires = ["hatchling"]
21+
build-backend = "hatchling.build"
22+
23+
[dependency-groups]
24+
dev = [
25+
"ipykernel>=6.29.5",
26+
"ipython>=9.3.0",
27+
"sectionproperties>=3.9.0",
28+
]
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""
2+
sectionproperties_reporting is a package of convenience functions
3+
to reduce the work of extracting analysis results from sectionproperties.
4+
5+
Includes functions for extracting both stress results and properties results.
6+
7+
- Connor Ferster, Structural Python, 2025
8+
"""
9+
10+
__version__ = "0.1.0"
11+
12+
from .extraction import *
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from __future__ import annotations
2+
import numpy as np
3+
from typing import Optional
4+
5+
def export_properties(
6+
analysis_section: "sectionproperties.section.Section",
7+
subset_to_extract: Optional[str] = None,
8+
) -> dict[str, float]:
9+
"""
10+
Extracts the properties from the solved 'analysis_section'.
11+
12+
'analysis_section': a solved Section from sectionproperties.
13+
'subset_to_extract': One of {"geometric", "warping", "plastic"}
14+
The results extracted will be from one of these sets
15+
of properties. If None, then all results will be extracted.
16+
'properties_to_extract': If provided, will extract only the
17+
properties from the provided list. If None, will extract
18+
all sectionproperties.
19+
20+
21+
The following properties can be specified when materials
22+
are _not_ added to the section (i.e. a geometric analysis):
23+
[
24+
'area', 'perimeter', 'mass', 'ea', 'ga', 'nu_eff', 'e_eff',
25+
'g_eff', 'qx', 'qy', 'ixx_g', 'iyy_g', 'ixy_g', 'cx', 'cy',
26+
'ixx_c', 'iyy_c', 'ixy_c', 'zxx_plus', 'zxx_minus', 'zyy_plus',
27+
'zyy_minus', 'rx_c', 'ry_c', 'i11_c', 'i22_c', 'phi', 'z11_plus',
28+
'z11_minus', 'z22_plus', 'z22_minus', 'r11_c', 'r22_c', 'j',
29+
'my_xx', 'my_yy', 'my_11', 'my_22', 'delta_s', 'x_se', 'y_se',
30+
'x11_se', 'y22_se', 'x_st', 'y_st', 'gamma', 'a_sx', 'a_sy',
31+
'a_sxy', 'a_s11', 'a_s22', 'beta_x_plus', 'beta_x_minus',
32+
'beta_y_plus', 'beta_y_minus', 'beta_11_plus', 'beta_11_minus',
33+
'beta_22_plus', 'beta_22_minus', 'x_pc', 'y_pc', 'x11_pc',
34+
'y22_pc', 'sxx', 'syy', 'sf_xx_plus', 'sf_xx_minus', 'sf_yy_plus',
35+
'sf_yy_minus', 's11', 's22', 'sf_11_plus', 'sf_11_minus',
36+
'sf_22_plus', 'sf_22_minus'
37+
]
38+
39+
40+
The following properties can be specified when matierals
41+
ARE added to the section (i.e. a composite analysis). This
42+
applies even if it is only one material:
43+
[
44+
'area', 'perimeter', 'mass', 'ea', 'ga', 'nu_eff', 'e_eff',
45+
'g_eff', 'qx', 'qy', 'ixx_g', 'iyy_g', 'ixy_g', 'cx', 'cy',
46+
'ixx_c', 'iyy_c', 'ixy_c', 'zxx_plus', 'zxx_minus', 'zyy_plus',
47+
'zyy_minus', 'rx_c', 'ry_c', 'i11_c', 'i22_c', 'phi', 'z11_plus',
48+
'z11_minus', 'z22_plus', 'z22_minus', 'r11_c', 'r22_c', 'j',
49+
'my_xx', 'my_yy', 'my_11', 'my_22', 'omega', 'psi_shear',
50+
'phi_shear', 'delta_s', 'x_se', 'y_se', 'x11_se', 'y22_se',
51+
'x_st', 'y_st', 'gamma', 'a_sx', 'a_sy', 'a_sxy', 'a_s11',
52+
'a_s22', 'beta_x_plus', 'beta_x_minus', 'beta_y_plus',
53+
'beta_y_minus', 'beta_11_plus', 'beta_11_minus', 'beta_22_plus',
54+
'beta_22_minus', 'x_pc', 'y_pc', 'x11_pc', 'y22_pc', 'sxx', 'syy',
55+
'sf_xx_plus', 'sf_xx_minus', 'sf_yy_plus', 'sf_yy_minus', 's11',
56+
's22', 'sf_11_plus', 'sf_11_minus', 'sf_22_plus', 'sf_22_minus'
57+
]
58+
59+
"""
60+
props = analysis_section.section_props.as_dict()
61+
62+
# These are arrays of intermediate values and are not
63+
# intended to be outputs.
64+
props.pop("omega")
65+
props.pop("psi_shear")
66+
props.pop("phi_shear")
67+
68+
if subset_to_extract is not None:
69+
subset = {}
70+
for prop in subset_to_extract:
71+
if prop in props:
72+
subset.update({prop: props[prop]})
73+
return subset
74+
else:
75+
return props
76+
77+
78+
def envelope_stress_results(stress_post: "sectionproperties.post.stress_post.StressPost") -> dict[str, dict]:
79+
"""
80+
Returns the envelope (min/max/absmax) of the stress results.
81+
"""
82+
stress_results = stress_post.get_stress()[0]
83+
stress_results.pop("material")
84+
stress_envelopes = {}
85+
for stress_dir_name, stress_array in stress_results.items():
86+
trimmed_stress_name = stress_dir_name.replace("sig_", "")
87+
max_stress = np.max(stress_array)
88+
min_stress = np.min(stress_array)
89+
absmax_stress = np.max(np.abs(stress_array))
90+
stress_envelopes.update({"max": max_stress})
91+
stress_envelopes.update({"min": min_stress})
92+
stress_envelopes.update({"absmax": absmax_stress})
93+
return stress_envelopes
94+
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import json
2+
import io
3+
import pathlib
4+
from pydantic import TypeAdapter, BaseModel, RootModel, Field, field_serializer, field_validator
5+
from typing import Optional, Any, Union, ClassVar, TextIO, Annotated
6+
7+
from sectionproperties.pre.geometry import Geometry, CompoundGeometry
8+
from sectionproperties.pre.pre import Material, DEFAULT_MATERIAL
9+
import numpy as np
10+
import shapely
11+
from shapely import wkt
12+
13+
14+
def to_json(section_geometry: Geometry | CompoundGeometry, filepath: str | pathlib.Path) -> None:
15+
"""
16+
Serializes the model to a new JSON file at 'filepath'.
17+
"""
18+
filepath = pathlib.Path(filepath)
19+
with open(filepath, 'w') as file:
20+
dump(section_geometry, file)
21+
22+
23+
def from_json(filepath: str | pathlib.Path) -> Geometry | CompoundGeometry:
24+
"""
25+
Reads the JSON file at 'filepath' and returns the Pynite.FEModel3D.
26+
"""
27+
with open(filepath, 'r') as file:
28+
model = load(file)
29+
return model
30+
31+
32+
def dump(section_geometry: Geometry | CompoundGeometry, file_io: TextIO, indent: int = 2) -> None:
33+
"""
34+
Writes the 'model' as a JSON data to the file-handler object, 'file_io'.
35+
36+
'indent': the number of spaces to indent in the file.
37+
"""
38+
model_dict = dump_dict(section_geometry)
39+
json.dump(model_dict, fp=file_io, indent=indent)
40+
41+
42+
def dumps(section_geometry: Geometry | CompoundGeometry, indent: int = 2) -> str:
43+
"""
44+
Returns the model as JSON string.
45+
"""
46+
model_schema = get_model_schema(section_geometry)
47+
return model_schema.model_dump_json(indent=indent)
48+
49+
50+
def dump_dict(section_geometry: Geometry | CompoundGeometry) -> dict:
51+
"""
52+
Returns a Python dictionary representing the model.
53+
54+
The Python dictionary is serializable to JSON.
55+
"""
56+
model_schema = get_model_schema(section_geometry)
57+
return model_schema.model_dump()
58+
59+
60+
def load(file_io: TextIO) -> Geometry | CompoundGeometry:
61+
"""
62+
Returns an FEModel3D from the json data contained within the file.
63+
"""
64+
json_data = json.load(file_io)
65+
model_adapter = TypeAdapter(SectionPropertiesSchema)
66+
model_schema = model_adapter.validate_python(json_data)
67+
# return model_schema.to_femodel3d()
68+
return model_schema.root.to_sectionproperties()
69+
70+
71+
def loads(model_json: str) -> Geometry | CompoundGeometry:
72+
"""
73+
Returns an FEModel3D based on the provided 'model_json'.
74+
75+
'model_json': a JSON-serialized str representing an FEModel3D
76+
"""
77+
model_adapter = TypeAdapter(SectionPropertiesSchema)
78+
model_schema = model_adapter.validate_json(model_json)
79+
# femodel3d = model_schema.to_femodel3d()
80+
return model_schema.root.to_sectionproperties()
81+
82+
83+
def load_dict(model_dict: dict) -> Geometry | CompoundGeometry:
84+
"""
85+
Returns an FEModel3D based on the provided 'model_dict'.
86+
87+
'model_dict': A JSON-serializable dict representing an FEModel3D
88+
"""
89+
model_adapter = TypeAdapter(SectionPropertiesSchema)
90+
model_schema = model_adapter.validate_python(model_dict)
91+
return model_schema.root.to_sectionproperties()
92+
93+
94+
def get_model_schema(section_geometry: Geometry | CompoundGeometry) -> dict[str, dict]:
95+
"""
96+
Returns an SectionPropertiesSchema based on the supplied model.
97+
"""
98+
model_adapter = TypeAdapter(SectionPropertiesSchema)
99+
model_schema = model_adapter.validate_python(section_geometry, from_attributes=True)
100+
return model_schema
101+
102+
103+
class ExporterMixin:
104+
def to_init_dict(self):
105+
init_dict = {}
106+
if self._init_attrs is None:
107+
return init_dict
108+
for attr_name in self._init_attrs:
109+
attr_value = getattr(self, attr_name)
110+
if hasattr(attr_value, "_sectionproperties_class"):
111+
attr_value = attr_value._sectionproperties_class(**attr_value.to_init_dict())
112+
init_dict.update({attr_name: attr_value})
113+
return init_dict
114+
115+
116+
117+
class MaterialSchema(BaseModel, ExporterMixin):
118+
name: str
119+
elastic_modulus: float
120+
poissons_ratio: float
121+
yield_strength: float
122+
density: float
123+
color: str
124+
_init_attrs: ClassVar[Optional[list[str]]] = [
125+
'name',
126+
'elastic_modulus',
127+
'poissons_ratio',
128+
'yield_strength',
129+
'density',
130+
'color',
131+
]
132+
_sectionproperties_class: ClassVar[type] = Material
133+
134+
135+
class GeometrySchema(BaseModel, ExporterMixin):
136+
geom: str
137+
material: Optional[MaterialSchema]
138+
control_points: Optional[list[str] | list[tuple[float, float]]] = None
139+
tol: int = 12
140+
points: Optional[list[tuple[float, float]]] = None
141+
facets: Optional[list[tuple[int, int]]] = None
142+
holes: Optional[list[tuple[float, float]]] = None
143+
mesh: Optional[dict[str, list | float]] = None
144+
_init_attrs: ClassVar[Optional[list[str]]] = ['geom', 'material', 'control_points', 'tol']
145+
_sectionproperties_class: ClassVar[type] = Geometry
146+
147+
148+
@field_validator("geom", mode="before")
149+
@classmethod
150+
def validate_geom(cls, geom: str | shapely.Polygon):
151+
if isinstance(geom, str):
152+
return geom
153+
return wkt.dumps(geom, trim=True)
154+
155+
156+
@field_validator("control_points", mode="before")
157+
@classmethod
158+
def validate_control_points(cls, ctrl_pts: list[tuple[float, float]] | shapely.Point):
159+
if isinstance(ctrl_pts, shapely.Point):
160+
return list(ctrl_pts.coords)
161+
else:
162+
return ctrl_pts
163+
164+
@field_validator("mesh", mode="before")
165+
@classmethod
166+
def validate_mesh(cls, mesh: dict[str, Any]):
167+
if mesh is not None:
168+
serialized_mesh = {}
169+
170+
serialized_mesh['vertices'] = mesh['vertices']
171+
if isinstance(mesh['vertices'], np.ndarray):
172+
serialized_mesh['vertices'] = mesh['vertices'].tolist()
173+
174+
serialized_mesh['vertex_markers'] = mesh['vertex_markers']
175+
if isinstance(mesh['vertex_markers'], np.ndarray):
176+
serialized_mesh['vertex_markers'] = mesh['vertex_markers'].tolist()
177+
178+
serialized_mesh['triangles'] = mesh['triangles']
179+
if isinstance(mesh['triangles'], np.ndarray):
180+
serialized_mesh['triangles'] = mesh['triangles'].tolist()
181+
182+
serialized_mesh['triangle_attributes'] = mesh['triangle_attributes']
183+
if isinstance(mesh['triangle_attributes'], np.ndarray):
184+
serialized_mesh['triangle_attributes'] = mesh['triangle_attributes'].tolist()
185+
186+
serialized_mesh['segments'] = mesh['segments']
187+
serialized_mesh['segment_markers'] = mesh['segment_markers']
188+
serialized_mesh['regions'] = mesh['regions']
189+
return serialized_mesh
190+
191+
def to_sectionproperties(self):
192+
sec_prop_class = self._sectionproperties_class
193+
geom = wkt.loads(self.geom)
194+
init_dict = self.to_init_dict()
195+
if "geom" in init_dict:
196+
init_dict.update({"geom": geom})
197+
new_inst = sec_prop_class(**init_dict)
198+
199+
for attr_name, attr_value in self:
200+
if attr_name in init_dict: continue
201+
if attr_name == "mesh" and attr_value is not None:
202+
attr_value['vertices'] = np.array(attr_value['vertices'])
203+
attr_value['vertex_markers'] = np.array(attr_value['vertex_markers'])
204+
attr_value['triangles'] = np.array(attr_value['triangles'])
205+
attr_value['triangle_attributes'] = np.array(attr_value['triangle_attributes'])
206+
setattr(new_inst, attr_name, attr_value)
207+
return new_inst
208+
209+
210+
211+
212+
class CompoundGeometrySchema(GeometrySchema):
213+
geoms: list[GeometrySchema] | str
214+
215+
_init_attrs: ClassVar[Optional[list[str]]] = ['geoms']
216+
_sectionproperties_class: ClassVar[type] = CompoundGeometry
217+
218+
def to_sectionproperties(self):
219+
sec_prop_class = self._sectionproperties_class
220+
geom = wkt.loads(self.geom)
221+
init_dict = self.to_init_dict()
222+
if "geoms" in init_dict:
223+
init_dict.update({"geoms": geom})
224+
new_inst = sec_prop_class(**init_dict)
225+
226+
for attr_name, attr_value in self:
227+
if attr_name in init_dict: continue
228+
if attr_name == "mesh" and attr_value is not None:
229+
attr_value['vertices'] = np.array(attr_value['vertices'])
230+
attr_value['vertex_markers'] = np.array(attr_value['vertex_markers'])
231+
attr_value['triangles'] = np.array(attr_value['triangles'])
232+
attr_value['triangle_attributes'] = np.array(attr_value['triangle_attributes'])
233+
setattr(new_inst, attr_name, attr_value)
234+
return new_inst
235+
236+
237+
class SectionPropertiesSchema(RootModel, ExporterMixin):
238+
"""
239+
A container to hold the schema, whether it is Geometry or CompoundGeometry
240+
object
241+
"""
242+
root: GeometrySchema | CompoundGeometrySchema
243+
_init_attrs: ClassVar[Optional[list[str]]] = None
244+
245+
def to_sectionproperties(self):
246+
return self.root.to_sectionproperties()

0 commit comments

Comments
 (0)