# -*- coding: utf-8 -*-# vim: tabstop=4 shiftwidth=4 softtabstop=4## Copyright (C) 2015-2025 GEM Foundation## OpenQuake is free software: you can redistribute it and/or modify it# under the terms of the GNU Affero General Public License as published# by the Free Software Foundation, either version 3 of the License, or# (at your option) any later version.## OpenQuake is distributed in the hope that it will be useful,# but WITHOUT ANY WARRANTY; without even the implied warranty of# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the# GNU Affero General Public License for more details.## You should have received a copy of the GNU Affero General Public License# along with OpenQuake. If not, see <http://www.gnu.org/licenses/>."""Source model XML Writer"""importosimporttomlimportoperatorimportnumpyfromopenquake.baselibimporthdf5fromopenquake.baselib.generalimportCallableDict,groupbyfromopenquake.baselib.nodeimportNode,node_to_dictfromopenquake.hazardlibimportnrml,sourceconverter,pmf,validfromopenquake.hazardlib.sourceimport(NonParametricSeismicSource,check_complex_fault,PointSource)fromopenquake.hazardlib.tomimportNegativeBinomialTOMobj_to_node=CallableDict(lambdaobj:obj.__class__.__name__)
[docs]defr4(x):""" Round lon, lat to 4 digits (11 meters) """returnround(x,4)
[docs]defbuild_area_source_geometry(area_source):""" Returns the area source geometry as a Node :param area_source: Area source model as an instance of the :class: `openquake.hazardlib.source.area.AreaSource` :returns: Instance of :class:`openquake.baselib.node.Node` """geom=[]forlon,latinzip(area_source.polygon.lons,area_source.polygon.lats):# NB: converting numpy.float64 -> float is good for TOMLgeom.extend((float(lon),float(lat)))poslist_node=Node("gml:posList",text=geom)linear_ring_node=Node("gml:LinearRing",nodes=[poslist_node])exterior_node=Node("gml:exterior",nodes=[linear_ring_node])polygon_node=Node("gml:Polygon",nodes=[exterior_node])upper_depth_node=Node("upperSeismoDepth",text=area_source.upper_seismogenic_depth)lower_depth_node=Node("lowerSeismoDepth",text=area_source.lower_seismogenic_depth)returnNode("areaGeometry",nodes=[polygon_node,upper_depth_node,lower_depth_node])
[docs]defbuild_point_source_geometry(point_source):""" Returns the poing source geometry as a Node :param point_source: Point source model as an instance of the :class: `openquake.hazardlib.source.point.PointSource` :returns: Instance of :class:`openquake.baselib.node.Node` """xy=point_source.location.x,point_source.location.ypos_node=Node("gml:pos",text=xy)point_node=Node("gml:Point",nodes=[pos_node])upper_depth_node=Node("upperSeismoDepth",text=point_source.upper_seismogenic_depth)lower_depth_node=Node("lowerSeismoDepth",text=point_source.lower_seismogenic_depth)returnNode("pointGeometry",nodes=[point_node,upper_depth_node,lower_depth_node])
[docs]defbuild_linestring_node(line,with_depth=False):""" Parses a line to a Node class :param line: Line as instance of :class:`openquake.hazardlib.geo.line.Line` :param bool with_depth: Include the depth values (True) or not (False): :returns: Instance of :class:`openquake.baselib.node.Node` """geom=[]forpinline.points:ifwith_depth:geom.extend((r4(p.x),r4(p.y),r4(p.z)))else:geom.extend((r4(p.x),r4(p.y)))poslist_node=Node("gml:posList",text=geom)returnNode("gml:LineString",nodes=[poslist_node])
[docs]defbuild_simple_fault_geometry(fault_source):""" Returns the simple fault source geometry as a Node :param fault_source: Simple fault source model as an instance of the :class: `openquake.hazardlib.source.simple_fault.SimpleFaultSource` :returns: Instance of :class:`openquake.baselib.node.Node` """linestring_node=build_linestring_node(fault_source.fault_trace,with_depth=False)dip_node=Node("dip",text=fault_source.dip)upper_depth_node=Node("upperSeismoDepth",text=fault_source.upper_seismogenic_depth)lower_depth_node=Node("lowerSeismoDepth",text=fault_source.lower_seismogenic_depth)returnNode("simpleFaultGeometry",nodes=[linestring_node,dip_node,upper_depth_node,lower_depth_node])
[docs]defbuild_complex_fault_geometry(fault_source):""" Returns the complex fault source geometry as a Node :param fault_source: Complex fault source model as an instance of the :class: `openquake.hazardlib.source.complex_fault.ComplexFaultSource` :returns: Instance of :class:`openquake.baselib.node.Node` """num_edges=len(fault_source.edges)edge_nodes=[]foriloc,edgeinenumerate(fault_source.edges):ifiloc==0:# Top Edgenode_name="faultTopEdge"elifiloc==(num_edges-1):# Bottom edgenode_name="faultBottomEdge"else:# Intermediate edgenode_name="intermediateEdge"edge_nodes.append(Node(node_name,nodes=[build_linestring_node(edge,with_depth=True)]))returnNode("complexFaultGeometry",nodes=edge_nodes)
[docs]@obj_to_node.add('KiteSurface')defbuild_kite_surface(ksurface):""" Returns the KiteSurface instance as a Node :param ksurface: Kite fault source model as an instance of the :class: `openquake.hazardlib.source.kite_fault.KiteFaultSource` :returns: Instance of :class:`openquake.baselib.node.Node` """profile_nodes=[]forprofileinksurface.profiles:node=build_linestring_node(profile,with_depth=True)profile_nodes.append(Node("profile",nodes=[node]))returnNode("kiteSurface",nodes=profile_nodes)
[docs]@obj_to_node.add('EvenlyDiscretizedMFD')defbuild_evenly_discretised_mfd(mfd):""" Returns the evenly discretized MFD as a Node :param mfd: MFD as instance of :class: `openquake.hazardlib.mfd.evenly_discretized.EvenlyDiscretizedMFD` :returns: Instance of :class:`openquake.baselib.node.Node` """occur_rates=Node("occurRates",text=mfd.occurrence_rates)returnNode("incrementalMFD",{"binWidth":mfd.bin_width,"minMag":mfd.min_mag},nodes=[occur_rates])
[docs]@obj_to_node.add('TruncatedGRMFD')defbuild_truncated_gr_mfd(mfd):""" Parses the truncated Gutenberg Richter MFD as a Node :param mfd: MFD as instance of :class: `openquake.hazardlib.mfd.truncated_gr.TruncatedGRMFD` :returns: Instance of :class:`openquake.baselib.node.Node` """ifhasattr(mfd,'slip_rate'):returnNode("truncGutenbergRichterMFD",{"bValue":mfd.b_val,"slipRate":mfd.slip_rate,"rigidity":mfd.rigidity,"minMag":mfd.min_mag,"maxMag":mfd.max_mag})returnNode("truncGutenbergRichterMFD",{"aValue":mfd.a_val,"bValue":mfd.b_val,"minMag":mfd.min_mag,"maxMag":mfd.max_mag})
[docs]@obj_to_node.add('TaperedGRMFD')defbuild_tapered_gr_mfd(mfd):""" Parses the truncated Gutenberg Richter MFD as a Node :param mfd: MFD as instance of :class: `openquake.hazardlib.mfd.tapered_gr_mfd.TaperedGRMFD` :returns: Instance of :class:`openquake.baselib.node.Node` """returnNode("taperedGutenbergRichterMFD",{"aValue":mfd.a_val,"bValue":mfd.b_val,"cornerMag":mfd.corner_mag,"minMag":mfd.min_mag,"maxMag":mfd.max_mag})
[docs]@obj_to_node.add('ArbitraryMFD')defbuild_arbitrary_mfd(mfd):""" Parses the arbitrary MFD as a Node :param mfd: MFD as instance of :class: `openquake.hazardlib.mfd.arbitrary.ArbitraryMFD` :returns: Instance of :class:`openquake.baselib.node.Node` """magnitudes=Node("magnitudes",text=mfd.magnitudes)occur_rates=Node("occurRates",text=mfd.occurrence_rates)returnNode("arbitraryMFD",nodes=[magnitudes,occur_rates])
[docs]@obj_to_node.add("YoungsCoppersmith1985MFD")defbuild_youngs_coppersmith_mfd(mfd):""" Parses the Youngs & Coppersmith MFD as a node. Note that the MFD does not hold the total moment rate, but only the characteristic rate. Therefore the node is written to the characteristic rate version regardless of whether or not it was originally created from total moment rate :param mfd: MFD as instance of :class: `openquake.hazardlib.mfd.youngs_coppersmith_1985. YoungsCoppersmith1985MFD` :returns: Instance of :class:`openquake.baselib.node.Node` """returnNode("YoungsCoppersmithMFD",{"minMag":mfd.min_mag,"bValue":mfd.b_val,"characteristicMag":mfd.char_mag,"characteristicRate":mfd.char_rate,"binWidth":mfd.bin_width})
[docs]@obj_to_node.add('MultiMFD')defbuild_multi_mfd(mfd):""" Parses the MultiMFD as a Node :param mfd: MFD as instance of :class: `openquake.hazardlib.mfd.multi_mfd.MultiMFD` :returns: Instance of :class:`openquake.baselib.node.Node` """node=Node("multiMFD",dict(kind=mfd.kind,size=mfd.size))fornameinsorted(mfd.kwargs):values=mfd.kwargs[name]ifnamein('magnitudes','occurRates'):values=sum(values,[])node.append(Node(name,text=values))if'occurRates'inmfd.kwargs:lengths=[len(rates)forratesinmfd.kwargs['occurRates']]node.append(Node('lengths',text=lengths))returnnode
[docs]defbuild_nodal_plane_dist(npd):""" Returns the nodal plane distribution as a Node instance :param npd: Nodal plane distribution as instance of :class: `openquake.hazardlib.pmf.PMF` :returns: Instance of :class:`openquake.baselib.node.Node` """npds=[]dist=[]forprob,npdinnpd.data:dist.append((prob,(npd.dip,npd.strike,npd.rake)))nodal_plane=Node("nodalPlane",{"dip":npd.dip,"probability":prob,"strike":npd.strike,"rake":npd.rake})npds.append(nodal_plane)sourceconverter.fix_dupl(dist)returnNode("nodalPlaneDist",nodes=npds)
[docs]defbuild_hypo_depth_dist(hdd):""" Returns the hypocentral depth distribution as a Node instance :param hdd: Hypocentral depth distribution as an instance of :class: `openquake.hzardlib.pmf.PMF` :returns: Instance of :class:`openquake.baselib.node.Node` """hdds=[]dist=[]for(prob,depth)inhdd.data:dist.append((prob,depth))hdds.append(Node("hypoDepth",{"depth":depth,"probability":prob}))sourceconverter.fix_dupl(dist)returnNode("hypoDepthDist",nodes=hdds)
[docs]defget_distributed_seismicity_source_nodes(source):""" Returns list of nodes of attributes common to all distributed seismicity source classes :param source: Seismic source as instance of :class: `openquake.hazardlib.source.area.AreaSource` or :class: `openquake.hazardlib.source.point.PointSource` :returns: List of instances of :class:`openquake.baselib.node.Node` """source_nodes=[]# parse msrsource_nodes.append(Node("magScaleRel",text=source.magnitude_scaling_relationship.__class__.__name__))# Parse aspect ratiosource_nodes.append(Node("ruptAspectRatio",text=source.rupture_aspect_ratio))# Parse MFDsource_nodes.append(obj_to_node(source.mfd))# Parse nodal plane distributionsource_nodes.append(build_nodal_plane_dist(source.nodal_plane_distribution))# Parse hypocentral depth distributionsource_nodes.append(build_hypo_depth_dist(source.hypocenter_distribution))returnsource_nodes
[docs]defbuild_hypo_list_node(hypo_list):""" :param hypo_list: an array of shape (N, 3) with columns (alongStrike, downDip, weight) :returns: a hypoList node containing N hypo nodes """hypolist=Node('hypoList',{})forrowinhypo_list:n=Node('hypo',dict(alongStrike=row[0],downDip=row[1],weight=row[2]))hypolist.append(n)returnhypolist
[docs]defbuild_slip_list_node(slip_list):""" :param slip_list: an array of shape (N, 2) with columns (slip, weight) :returns: a hypoList node containing N slip nodes """sliplist=Node('slipList',{})forrowinslip_list:sliplist.append(Node('slip',dict(weight=row[1]),row[0]))returnsliplist
[docs]defget_fault_source_nodes(source):""" Returns list of nodes of attributes common to all fault source classes :param source: Fault source as instance of :class: `openquake.hazardlib.source.simple_fault.SimpleFaultSource` or :class: `openquake.hazardlib.source.complex_fault.ComplexFaultSource` :returns: List of instances of :class:`openquake.baselib.node.Node` """source_nodes=[]# parse msrsource_nodes.append(Node("magScaleRel",text=source.magnitude_scaling_relationship.__class__.__name__))# Parse aspect ratiosource_nodes.append(Node("ruptAspectRatio",text=source.rupture_aspect_ratio))# Parse MFDsource_nodes.append(obj_to_node(source.mfd))# Parse Rakesource_nodes.append(Node("rake",text=source.rake))iflen(getattr(source,'hypo_list',[])):source_nodes.append(build_hypo_list_node(source.hypo_list))iflen(getattr(source,'slip_list',[])):source_nodes.append(build_slip_list_node(source.slip_list))returnsource_nodes
[docs]defget_source_attributes(source):""" Retreives a dictionary of source attributes from the source class :param source: Seismic source as instance of :class: `openquake.hazardlib.source.base.BaseSeismicSource` :returns: Dictionary of source attributes """attrs={"id":source.source_id,"name":source.name}ifisinstance(source,NonParametricSeismicSource):rup=source.data[0][0]# from [(rup, pmf), ...] pairsifnothasattr(rup,'weight'):# happens in test_non_parametric_srcreturnattrselifrup.weightisnotNone:weights=[rup.weightforrup,pmfinsource.data]attrs['rup_weights']=numpy.array(weights)elifisinstance(source,PointSource):tom=source.temporal_occurrence_modelifisinstance(tom,NegativeBinomialTOM):attrs['tom']='NegativeBinomialTOM'attrs['mu']=tom.muattrs['alpha']=tom.alphareturnattrs
[docs]@obj_to_node.add('AreaSource')defbuild_area_source_node(area_source):""" Parses an area source to a Node class :param area_source: Area source as instance of :class: `openquake.hazardlib.source.area.AreaSource` :returns: Instance of :class:`openquake.baselib.node.Node` """# parse geometrysource_nodes=[build_area_source_geometry(area_source)]# parse common distributed attributessource_nodes.extend(get_distributed_seismicity_source_nodes(area_source))returnNode("areaSource",get_source_attributes(area_source),nodes=source_nodes)
[docs]defbuild_rupture_node(rupt,probs_occur):""" :param rupt: a hazardlib rupture object :param probs_occur: a list of floats with sum 1 """s=sum(probs_occur)ifabs(s-1)>pmf.PRECISION:raiseValueError('The sum of %s is not 1: %s'%(probs_occur,s))h=rupt.hypocenterhp_dict=dict(lon=h.longitude,lat=h.latitude,depth=h.depth)rupt_nodes=[Node('magnitude',{},rupt.mag),Node('rake',{},rupt.rake),Node('hypocenter',hp_dict)]rupt_nodes.extend(rupt.surface.surface_nodes)geom=rupt.surface.surface_nodes[0].tagiflen(rupt.surface.surface_nodes)>1:name='multiPlanesRupture'elifgeom=='planarSurface':name='singlePlaneRupture'elifgeom=='simpleFaultGeometry':name='simpleFaultRupture'elifgeom=='complexFaultGeometry':name='complexFaultRupture'elifgeom=='griddedSurface':name='griddedRupture'elifgeom=='kiteSurface':name='kiteSurface'returnNode(name,{'probs_occur':probs_occur},nodes=rupt_nodes)
[docs]@obj_to_node.add('MultiPointSource')defbuild_multi_point_source_node(multi_point_source):""" Parses a point source to a Node class :param point_source: MultiPoint source as instance of :class: `openquake.hazardlib.source.point.MultiPointSource` :returns: Instance of :class:`openquake.baselib.node.Node` """# parse geometrypos=[]forpinmulti_point_source.mesh:# converting numpy.float64 -> float is good for TOMLpos.append(float(p.x))pos.append(float(p.y))mesh_node=Node('gml:posList',text=pos)upper_depth_node=Node("upperSeismoDepth",text=multi_point_source.upper_seismogenic_depth)lower_depth_node=Node("lowerSeismoDepth",text=multi_point_source.lower_seismogenic_depth)source_nodes=[Node("multiPointGeometry",nodes=[mesh_node,upper_depth_node,lower_depth_node])]# parse common distributed attributessource_nodes.extend(get_distributed_seismicity_source_nodes(multi_point_source))returnNode("multiPointSource",get_source_attributes(multi_point_source),nodes=source_nodes)
[docs]@obj_to_node.add('PointSource')defbuild_point_source_node(point_source):""" Parses a point source to a Node class :param point_source: Point source as instance of :class: `openquake.hazardlib.source.point.PointSource` :returns: Instance of :class:`openquake.baselib.node.Node` """# parse geometrysource_nodes=[build_point_source_geometry(point_source)]# parse common distributed attributessource_nodes.extend(get_distributed_seismicity_source_nodes(point_source))returnNode("pointSource",get_source_attributes(point_source),nodes=source_nodes)
[docs]@obj_to_node.add('SimpleFaultSource')defbuild_simple_fault_source_node(fault_source):""" Parses a simple fault source to a Node class :param fault_source: Simple fault source as instance of :class: `openquake.hazardlib.source.simple_fault.SimpleFaultSource` :returns: Instance of :class:`openquake.baselib.node.Node` """# Parse geometrysource_nodes=[build_simple_fault_geometry(fault_source)]# Parse common fault source attributessource_nodes.extend(get_fault_source_nodes(fault_source))returnNode("simpleFaultSource",get_source_attributes(fault_source),nodes=source_nodes)
[docs]@obj_to_node.add('ComplexFaultSource')defbuild_complex_fault_source_node(fault_source):""" Parses a complex fault source to a Node class :param fault_source: Complex fault source as instance of :class: `openquake.hazardlib.source.complex_fault.ComplexFaultSource` :returns: Instance of :class:`openquake.baselib.node.Node` """list(check_complex_fault(fault_source))# check get_dip# Parse geometrysource_nodes=[build_complex_fault_geometry(fault_source)]# Parse common fault source attributessource_nodes.extend(get_fault_source_nodes(fault_source))returnNode("complexFaultSource",get_source_attributes(fault_source),nodes=source_nodes)
[docs]@obj_to_node.add('KiteFaultSource')defbuild_kite_fault_source_node(fault_source):""" Parses a kite fault source to a Node class :param fault_source: Kite fault source as instance of :class: `openquake.hazardlib.source.kite_fault.KiteFaultSource` :returns: Instance of :class:`openquake.baselib.node.Node` """# Parse geometrysource_nodes=[build_kite_surface(fault_source)]# Parse common fault source attributessource_nodes.extend(get_fault_source_nodes(fault_source))returnNode("kiteFaultSource",get_source_attributes(fault_source),nodes=source_nodes)
# tested in case_65
[docs]@obj_to_node.add('MultiFaultSource')defbuild_multi_fault_source_node(multi_fault_source):""" Parses a MultiFaultSource to a Node class :param multi_fault_source: Multi fault source as instance of :class: `openquake.hazardlib.source.multi_fault.MultiFaultSource` :returns: Instance of :class:`openquake.baselib.node.Node` """rup_nodes=[]# multiPlanesRuptureforrup_idxs,prbs,mag,rakeinzip(multi_fault_source._rupture_idxs,multi_fault_source.probs_occur,multi_fault_source.mags,multi_fault_source.rakes):probs=' '.join(map(str,prbs))nodes=[Node('magnitude',text=str(mag)),Node('sectionIndexes',{'indexes':' '.join(map(str,rup_idxs))}),Node('rake',text=str(rake))]rup_node=Node('multiPlanesRupture',{'probs_occur':probs},nodes=nodes)rup_nodes.append(rup_node)ifmulti_fault_source.faults:faults=[Node('fault',{'tag':tag,'indexes':','.join(indexes)})fortag,indexesinmulti_fault_source.faults.items()]rup_nodes.insert(0,Node('faults',nodes=faults))returnNode("multiFaultSource",get_source_attributes(multi_fault_source),nodes=rup_nodes)
[docs]@obj_to_node.add('SourceGroup')defbuild_source_group(source_group):source_nodes=[obj_to_node(src)forsrcinsource_group.sources]attrs=dict(tectonicRegion=source_group.trt)ifsource_group.name:attrs['name']=source_group.nameifsource_group.src_interdep:attrs['src_interdep']=source_group.src_interdepifsource_group.rup_interdep:attrs['rup_interdep']=source_group.rup_interdepifsource_group.grp_probability!=1.0:attrs['grp_probability']=source_group.grp_probabilityifsource_group.cluster:attrs['cluster']='true'ifsource_group.temporal_occurrence_modelisnotNone:tom=source_group.temporal_occurrence_modelifhasattr(tom,'occurrence_rate'):attrs['tom']='ClusterPoissonTOM'attrs['occurrence_rate']=tom.occurrence_rateifsource_group.src_interdep=='mutex':# tested in multi_fault_testattrs['srcs_weights']=' '.join('%.7f'%getattr(src,'mutex_weight',1/len(source_group))forsrcinsource_group)returnNode('sourceGroup',attrs,nodes=source_nodes)
# ##################### generic source model writer ####################### #
[docs]defextract_gridded_attrs(src_groups):""" Extract the attributes of nonparametric/multifault sources. The attributes are arrays or a list of strings for rupture_idxs. :returns: a dictionary source_id -> attr -> value """ddict={}forsrc_groupinsrc_groups:forsrcinsrc_group:ifsrc.is_gridded():ddict[src.source_id]=src.todict()returnddict
[docs]defwrite_source_model(dest,sources_or_groups,name=None,investigation_time=None,prefix=''):""" Writes a source model to XML. :param dest: Destination path :param sources_or_groups: Source model in different formats :param name: Name of the source model (if missing, extracted from the filename) :param investigation_time: Investigation time (for time-dependent sources) :param prefix: Add a prefix to the rupture_idxs, if given :returns: the list of generated filenames """# first a sanity check, only a SourceModel or a sequence are acceptedassertisinstance(sources_or_groups,(nrml.SourceModel,list,tuple))ifisinstance(sources_or_groups,nrml.SourceModel):groups=sources_or_groups.src_groupsattrs=dict(name=sources_or_groups.name,investigation_time=sources_or_groups.investigation_time)elifisinstance(sources_or_groups[0],sourceconverter.SourceGroup):groups=sources_or_groupsattrs=dict(investigation_time=investigation_time)else:# passed a list of sourcessrcs_by_trt=groupby(sources_or_groups,operator.attrgetter('tectonic_region_type'))groups=[sourceconverter.SourceGroup(trt,srcs_by_trt[trt])fortrtinsrcs_by_trt]attrs=dict(investigation_time=investigation_time)ifnameor'name'notinattrs:attrs['name']=nameoros.path.splitext(os.path.basename(dest))[0]ifattrs['investigation_time']isNone:delattrs['investigation_time']nodes=list(map(obj_to_node,groups))gridded_attrs=extract_gridded_attrs(groups)out=[dest]ifgridded_attrs:# for nonparametric and multifault sources save attrs on HDF5 file# NB: this is tested in the oq-mbtk, in rupture_smooth_testdest5=os.path.splitext(dest)[0]+'.hdf5'withhdf5.File(dest5,'w')ash:forsrc_id,dicingridded_attrs.items():fork,vindic.items():key='%s/%s'%(src_id,k)dset=h.create_dataset(key,v.shape,v.dtype,compression='gzip',compression_opts=9)ifkey=='rupture_idxs'andprefix:dset[:]=[prefix+xforxinv]else:dset[:]=v# remove duplicate content from nodesforgrp_nodeinnodes:forsrc_nodeingrp_node:ifsrc_node["id"]ingridded_attrs:src_node.nodes=[nforninsrc_node.nodesifn.tag=='faults']out.append(dest5)# produce a geometryModel if there are MultiFaultSourcessections=[]forgroupingroups:forsrcingroup:ifhasattr(src,'sections'):sections.extend(src.sections)smodel=Node("sourceModel",attrs,nodes=nodes)withopen(dest,'wb')asf:nrml.write([smodel],f,'%s')ifsections:# surfaces have no 'id', so we use sections instead, with an 'id'# starting from 0; this is necessary for conversion to hdf5secnodes=[Node('section',{'id':str(i)},nodes=[obj_to_node(sec)])fori,secinenumerate(sections)]gmodel=Node("geometryModel",attrs,nodes=secnodes)withopen(dest[:-4]+'_sections.xml','wb')asf:nrml.write([gmodel],f,'%s')out.append(f.name)returnout
[docs]deftomldump(obj,fileobj=None):""" Write a generic serializable object in TOML format """dic=valid._fix_toml(node_to_dict(obj_to_node(obj)))iffileobjisNone:returntoml.dumps(dic)toml.dump(dic,fileobj)