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