Mod: improved mesh class
Mod: mesh subclass for each sample New: foamfile, reader and writer
This commit is contained in:
parent
77647b3370
commit
b45954d153
@ -291,104 +291,39 @@ class Anisotropy(object):
|
||||
setupLogger(logger, logging.INFO, self.env["LOG"])
|
||||
p = self.params
|
||||
|
||||
###
|
||||
# Shape
|
||||
##
|
||||
logger.info("Constructing shape ...")
|
||||
|
||||
geompy = salomepl.geometry.getGeom()
|
||||
structure = dict(
|
||||
simple = Simple,
|
||||
bodyCentered = BodyCentered,
|
||||
faceCentered = FaceCentered
|
||||
sGeometry, sMesh = dict(
|
||||
simple = (Simple, SimpleMesh),
|
||||
bodyCentered = (BodyCentered, BodyCenteredMesh),
|
||||
faceCentered = (FaceCentered, FaceCenteredMesh)
|
||||
)[p["structure"]["type"]]
|
||||
shapeGeometry = structure(**p["structure"])
|
||||
shapeGeometry.build()
|
||||
|
||||
[length, surfaceArea, volume] = geompy.BasicProperties(shapeGeometry.shape, theTolerance = 1e-06)
|
||||
# Shape
|
||||
logger.info("Constructing shape ...")
|
||||
geometry = sGeometry(**p["structure"])
|
||||
geometry.build()
|
||||
|
||||
|
||||
###
|
||||
# Mesh
|
||||
##
|
||||
logger.info("Prepairing mesh ...")
|
||||
mesh = sMesh(geometry)
|
||||
mesh.build()
|
||||
|
||||
params = db.Mesh()
|
||||
mesh = mesh.Mesh(shapeGeometry)
|
||||
algo3d = mesh.algo3d(Netgen3D)
|
||||
algo3d.apply(**params)
|
||||
|
||||
mesh = smesh.Mesh(shape)
|
||||
algo3d = mesh.Tetrahedron(algo = smeshBuilder.NETGEN_3D)
|
||||
algo2d = mesh.Triangle(algo = smeshBuilder.NETGEN_2D)
|
||||
hypo2d = algo2d.MaxElementArea(0.197375)
|
||||
algo1d = mesh.Segment()
|
||||
hypo1d = algo1d.AutomaticLength(1)
|
||||
|
||||
algo2d = mesh.Triangle(algo = smeshBuilder.NETGEN_2D, geom = strips)
|
||||
hypo2d = algo2d.LengthFromEdges()
|
||||
algo1d = mesh.Segment(algo = smeshBuilder.COMPOSITE, geom = strips)
|
||||
hypo1d = algo1d.AutomaticLength(0.633882)
|
||||
hypo1d.SetFineness( 1 )
|
||||
|
||||
mp = p["mesh"]
|
||||
|
||||
lengths = [
|
||||
geompy.BasicProperties(edge)[0] for edge in geompy.SubShapeAll(shapeGeometry.shape, geompy.ShapeType["EDGE"])
|
||||
]
|
||||
meanSize = sum(lengths) / len(lengths)
|
||||
mp["maxSize"] = meanSize
|
||||
mp["minSize"] = meanSize * 1e-1
|
||||
mp["chordalError"] = mp["maxSize"] / 2
|
||||
|
||||
faces = []
|
||||
for group in shapeGeometry.groups:
|
||||
if group.GetName() in mp["facesToIgnore"]:
|
||||
faces.append(group)
|
||||
|
||||
|
||||
mesh = salomepl.mesh.Mesh(shapeGeometry.shape)
|
||||
mesh.Tetrahedron(**mp)
|
||||
|
||||
if mp["viscousLayers"]:
|
||||
mesh.ViscousLayers(**mp, faces = faces)
|
||||
|
||||
# Submesh
|
||||
smp = p["submesh"]
|
||||
|
||||
for submesh in smp:
|
||||
for group in shapeGeometry.groups:
|
||||
if submesh["name"] == group.GetName():
|
||||
subshape = group
|
||||
|
||||
submesh["maxSize"] = meanSize * 1e-1
|
||||
submesh["minSize"] = meanSize * 1e-3
|
||||
submesh["chordalError"] = submesh["minSize"] * 1e+1
|
||||
|
||||
mesh.Triangle(subshape, **submesh)
|
||||
|
||||
|
||||
self.update()
|
||||
logger.info("Computing mesh ...")
|
||||
out, err, returncode = mesh.compute()
|
||||
|
||||
###
|
||||
# Results
|
||||
##
|
||||
#p["meshresult"] = dict()
|
||||
|
||||
if not returncode:
|
||||
mesh.removePyramids()
|
||||
mesh.assignGroups()
|
||||
mesh.createGroups()
|
||||
|
||||
casePath = self.getCasePath(path)
|
||||
os.makedirs(casePath, exist_ok = True)
|
||||
logger.info("Exporting mesh ...")
|
||||
returncode, err = mesh.exportUNV(os.path.join(casePath, "mesh.unv"))
|
||||
out, err, returncode = mesh.export(os.path.join(casePath, "mesh.unv"))
|
||||
|
||||
if returncode:
|
||||
logger.error(err)
|
||||
|
||||
# NOTE: edit from here
|
||||
meshStats = mesh.stats()
|
||||
p["meshresult"].update(
|
||||
surfaceArea = surfaceArea,
|
||||
|
30
anisotropy/openfoam/foamcase.py
Normal file
30
anisotropy/openfoam/foamcase.py
Normal file
@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# This file is part of anisotropy.
|
||||
# License: GNU GPL version 3, see the file "LICENSE" for details.
|
||||
|
||||
from anisotropy.openfoam.foamfile import FoamFile
|
||||
|
||||
class ControlDict(FoamFile):
|
||||
def __init__(self):
|
||||
ff = FoamFile(
|
||||
"system/controlDict",
|
||||
_location = "system"
|
||||
)
|
||||
self.header = ff.header
|
||||
self.content = {
|
||||
"application": "simpleFoam",
|
||||
"startFrom": "startTime",
|
||||
"startTime": 0,
|
||||
"stopAt": "endTime",
|
||||
"endTime": 2000,
|
||||
"deltaT": 1,
|
||||
"writeControl": "timeStep",
|
||||
"writeInterval": 100,
|
||||
"purgeWrite": 0,
|
||||
"writeFormat": "ascii",
|
||||
"writePrecision": 6,
|
||||
"writeCompression": "off",
|
||||
"timeFormat": "general",
|
||||
"timePrecision": 6,
|
||||
"runTimeModifiable": True
|
||||
}
|
91
anisotropy/openfoam/foamfile.py
Normal file
91
anisotropy/openfoam/foamfile.py
Normal file
@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# This file is part of anisotropy.
|
||||
# License: GNU GPL version 3, see the file "LICENSE" for details.
|
||||
|
||||
from anisotropy.openfoam.utils import version
|
||||
from PyFoam.RunDictionary.ParsedParameterFile import ParsedParameterFile
|
||||
from PyFoam.Basics.FoamFileGenerator import FoamFileGenerator
|
||||
import os
|
||||
|
||||
class FoamFile(object):
|
||||
def __init__(self,
|
||||
filename,
|
||||
_version = 2.0,
|
||||
_format = "ascii",
|
||||
_class = "dictionary",
|
||||
_location = None,
|
||||
_object = None
|
||||
):
|
||||
|
||||
self.path = os.path.abspath(filename)
|
||||
self.header = {
|
||||
"version": _version,
|
||||
"format": _format,
|
||||
"class": _class,
|
||||
"object": _object or os.path.split(filename)[1]
|
||||
}
|
||||
self.content = {}
|
||||
|
||||
if _location:
|
||||
self.header["location"] = f'"{ _location }"'
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.content[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.content[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
del self.content[key]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.content)
|
||||
|
||||
def __iter__(self):
|
||||
for key in self.content:
|
||||
yield key
|
||||
|
||||
def read(self):
|
||||
ppf = ParsedParameterFile(self.path)
|
||||
|
||||
self.header = ppf.header
|
||||
self.content = ppf.content
|
||||
|
||||
def _template(self, header, content):
|
||||
limit = 78
|
||||
desc = [
|
||||
"/*--------------------------------*- C++ -*----------------------------------*\\",
|
||||
"| ========= | |",
|
||||
"| \\\\ / F ield | OpenFOAM: The Open Source CFD Toolbox |",
|
||||
"| \\\\ / O peration |",
|
||||
"| \\\\ / A nd | |",
|
||||
"| \\\\/ M anipulation | |",
|
||||
"\*---------------------------------------------------------------------------*/"
|
||||
]
|
||||
desc[3] += " Version: {}".format(version() or "missed")
|
||||
desc[3] += " " * (limit - len(desc[3])) + "|"
|
||||
afterheader = "// " + 37 * "* " + "//"
|
||||
endfile = "// " + 73 * "*" + " //"
|
||||
|
||||
return "\n".join([*desc, header, afterheader, content, endfile])
|
||||
|
||||
|
||||
def write(self):
|
||||
header = FoamFileGenerator({}, header = self.header)
|
||||
header = header.makeString()[ :-2]
|
||||
header = header.replace("\n ", "\n" + 4 * " ")
|
||||
|
||||
content = FoamFileGenerator(self.content)
|
||||
content = content.makeString()[ :-1]
|
||||
content = content.replace("\n ", "\n" + 4 * " ").replace(" \t// " + 73 * "*" + " //", "")
|
||||
content = content.replace(" /* empty */ ", "")
|
||||
|
||||
prepared = self._template(header, content)
|
||||
|
||||
os.makedirs(os.path.split(self.path)[0], exist_ok = True)
|
||||
|
||||
with open(self.path, "w") as io:
|
||||
_ = io.write(prepared)
|
||||
|
||||
|
||||
|
@ -1,96 +1,84 @@
|
||||
/*--------------------------------*- C++ -*----------------------------------*\
|
||||
| ========= | |
|
||||
| \\ / F ield | OpenFOAM: The Open Source CFD Toolbox |
|
||||
| \\ / O peration | Version: v2012 |
|
||||
| \\ / A nd | Website: www.openfoam.com |
|
||||
| \\ / O peration | Version: missed |
|
||||
| \\ / A nd | |
|
||||
| \\/ M anipulation | |
|
||||
\*---------------------------------------------------------------------------*/
|
||||
FoamFile
|
||||
{
|
||||
version 2.0;
|
||||
format ascii;
|
||||
class dictionary;
|
||||
location "system";
|
||||
object fvSolution;
|
||||
version 2.0;
|
||||
format ascii;
|
||||
class dictionary;
|
||||
location "system";
|
||||
object fvSolution;
|
||||
}
|
||||
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
|
||||
|
||||
solvers
|
||||
{
|
||||
p
|
||||
{
|
||||
solver GAMG;
|
||||
tolerance 1e-06;
|
||||
relTol 0.1;
|
||||
smoother GaussSeidel;
|
||||
}
|
||||
|
||||
U
|
||||
{
|
||||
solver smoothSolver;
|
||||
smoother GaussSeidel;
|
||||
nSweeps 2;
|
||||
tolerance 1e-08;
|
||||
relTol 0.1;
|
||||
}
|
||||
|
||||
/*Phi
|
||||
{
|
||||
solver GAMG;
|
||||
smoother GaussSeidel;
|
||||
tolerance 1e-08;
|
||||
relTol 0.01;
|
||||
}*/
|
||||
Phi
|
||||
{
|
||||
solver GAMG;
|
||||
smoother DIC;
|
||||
cacheAgglomeration on;
|
||||
agglomerator faceAreaPair;
|
||||
nCellsInCoarsestLevel 10;
|
||||
mergeLevels 1;
|
||||
|
||||
tolerance 1e-06;
|
||||
relTol 0.01;
|
||||
}
|
||||
p
|
||||
{
|
||||
solver GAMG;
|
||||
tolerance 1e-06;
|
||||
relTol 0.1;
|
||||
smoother GaussSeidel;
|
||||
}
|
||||
U
|
||||
{
|
||||
solver smoothSolver;
|
||||
smoother GaussSeidel;
|
||||
nSweeps 2;
|
||||
tolerance 1e-08;
|
||||
relTol 0.1;
|
||||
} /*Phi
|
||||
{
|
||||
solver GAMG;
|
||||
smoother GaussSeidel;
|
||||
tolerance 1e-08;
|
||||
relTol 0.01;
|
||||
}*/
|
||||
Phi
|
||||
{
|
||||
solver GAMG;
|
||||
smoother DIC;
|
||||
cacheAgglomeration yes;
|
||||
agglomerator faceAreaPair;
|
||||
nCellsInCoarsestLevel 10;
|
||||
mergeLevels 1;
|
||||
tolerance 1e-06;
|
||||
relTol 0.01;
|
||||
}
|
||||
}
|
||||
|
||||
potentialFlow
|
||||
{
|
||||
nNonOrthogonalCorrectors 20;
|
||||
PhiRefCell 0;
|
||||
PhiRefPoint 0;
|
||||
PhiRefValue 0;
|
||||
Phi 0;
|
||||
nNonOrthogonalCorrectors 20;
|
||||
PhiRefCell 0;
|
||||
PhiRefPoint 0;
|
||||
PhiRefValue 0;
|
||||
Phi 0;
|
||||
}
|
||||
|
||||
cache
|
||||
{
|
||||
grad(U);
|
||||
}
|
||||
|
||||
grad(p) /* empty */ ;
|
||||
}
|
||||
SIMPLE
|
||||
{
|
||||
nNonOrthogonalCorrectors 10;
|
||||
|
||||
residualControl
|
||||
{
|
||||
p 1e-5;
|
||||
U 1e-5;
|
||||
}
|
||||
nNonOrthogonalCorrectors 10;
|
||||
residualControl
|
||||
{
|
||||
p 1e-05;
|
||||
U 1e-05;
|
||||
}
|
||||
}
|
||||
|
||||
relaxationFactors
|
||||
{
|
||||
fields
|
||||
{
|
||||
p 0.3;
|
||||
}
|
||||
equations
|
||||
{
|
||||
U 0.5;
|
||||
}
|
||||
fields
|
||||
{
|
||||
p 0.3;
|
||||
}
|
||||
equations
|
||||
{
|
||||
U 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ************************************************************************* //
|
||||
// ************************************************************************* //
|
@ -7,7 +7,7 @@ import shutil
|
||||
from .application import application
|
||||
|
||||
def version() -> str:
|
||||
return os.environ["WM_PROJECT_VERSION"]
|
||||
return os.environ.get("WM_PROJECT_VERSION")
|
||||
|
||||
|
||||
def foamCleanCustom(case: str = None):
|
||||
|
@ -58,7 +58,7 @@ class StructureGeometry(object):
|
||||
|
||||
# Geometry module
|
||||
if not GEOM_IMPORTED:
|
||||
raise ImportError("Cannot find the salome modules.")
|
||||
raise ImportError("Cannot find the salome geometry modules.")
|
||||
|
||||
else:
|
||||
self.geo = geomBuilder.New()
|
||||
|
@ -3,124 +3,77 @@
|
||||
# License: GNU GPL version 3, see the file "LICENSE" for details.
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("anisotropy")
|
||||
|
||||
SMESH_IMPORTED = False
|
||||
|
||||
try:
|
||||
import SMESH
|
||||
from salome.smesh import smeshBuilder
|
||||
|
||||
except ImportError:
|
||||
logger.debug("Trying to get SALOME mesh modules outside SALOME environment. Modules won't be imported.")
|
||||
|
||||
if globals().get("smeshBuilder"):
|
||||
smesh = smeshBuilder.New()
|
||||
|
||||
else:
|
||||
smesh = None
|
||||
|
||||
import enum
|
||||
|
||||
class Fineness(enum.Enum):
|
||||
VeryCoarse = 0
|
||||
Coarse = 1
|
||||
Moderate = 2
|
||||
Fine = 3
|
||||
VeryFine = 4
|
||||
Custom = 5
|
||||
|
||||
|
||||
def getSmesh():
|
||||
return smesh
|
||||
|
||||
|
||||
def updateParams(old, new: dict):
|
||||
old.SetMaxSize(new.get("maxSize", old.GetMaxSize()))
|
||||
old.SetMinSize(new.get("minSize", old.GetMinSize()))
|
||||
|
||||
old.SetFineness(new.get("fineness", old.GetFineness()))
|
||||
old.SetGrowthRate(new.get("growthRate", old.GetGrowthRate()))
|
||||
old.SetNbSegPerEdge(new.get("nbSegPerEdge", old.GetNbSegPerEdge()))
|
||||
old.SetNbSegPerRadius(new.get("nbSegPerRadius", old.GetNbSegPerRadius()))
|
||||
|
||||
old.SetChordalErrorEnabled(new.get("chordalErrorEnabled", old.GetChordalErrorEnabled()))
|
||||
old.SetChordalError(new.get("chordalError", old.GetChordalError()))
|
||||
|
||||
old.SetSecondOrder(new.get("secondOrder", old.GetSecondOrder()))
|
||||
old.SetOptimize(new.get("optimize", old.GetOptimize()))
|
||||
old.SetQuadAllowed(new.get("quadAllowed", old.GetQuadAllowed()))
|
||||
old.SetUseSurfaceCurvature(new.get("useSurfaceCurvature", old.GetUseSurfaceCurvature()))
|
||||
old.SetFuseEdges(new.get("fuseEdges", old.GetFuseEdges()))
|
||||
old.SetCheckChartBoundary(new.get("checkChartBoundary", old.GetCheckChartBoundary()))
|
||||
SMESH_IMPORTED = True
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
class Mesh(object):
|
||||
def __init__(self, shape, name = ""):
|
||||
self.name = name if name else shape.GetName()
|
||||
self.mesh = smesh.Mesh(shape, self.name)
|
||||
self.geom = shape
|
||||
self.algo = None
|
||||
self.params = None
|
||||
self.viscousLayers = None
|
||||
def __init__(self, geom):
|
||||
|
||||
# Mesh module
|
||||
if not SMESH_IMPORTED:
|
||||
raise ImportError("Cannot find the salome mesh modules.")
|
||||
|
||||
self.submeshes = []
|
||||
else:
|
||||
self.smesh = smeshBuilder.New()
|
||||
self.smeshBuilder = smeshBuilder
|
||||
|
||||
# General attributes
|
||||
self.geom = geom
|
||||
self.mesh = self.smesh.Mesh(self.geom.shape, self.geom.name)
|
||||
|
||||
def Tetrahedron(self, **kwargs):
|
||||
self.algo = self.mesh.Tetrahedron(algo = smeshBuilder.NETGEN_1D2D3D)
|
||||
self.params = self.algo.Parameters()
|
||||
|
||||
self.params = updateParams(self.params, kwargs)
|
||||
|
||||
def _extrusionMethod(self, key: str):
|
||||
return dict(
|
||||
SURF_OFFSET_SMOOTH = smeshBuilder.SURF_OFFSET_SMOOTH,
|
||||
FACE_OFFSET = smeshBuilder.FACE_OFFSET,
|
||||
NODE_OFFSET = smeshBuilder.NODE_OFFSET,
|
||||
).get(key, smeshBuilder.SURF_OFFSET_SMOOTH)
|
||||
|
||||
def ViscousLayers(self,
|
||||
thickness = 1,
|
||||
numberOfLayers = 1,
|
||||
stretchFactor = 0,
|
||||
faces = [],
|
||||
isFacesToIgnore = True,
|
||||
extrMethod = "SURF_OFFSET_SMOOTH",
|
||||
**kwargs
|
||||
):
|
||||
|
||||
self.viscousLayers = self.algo.ViscousLayers(
|
||||
thickness,
|
||||
numberOfLayers,
|
||||
stretchFactor,
|
||||
faces,
|
||||
isFacesToIgnore,
|
||||
self._extrusionMethod(extrMethod)
|
||||
)
|
||||
|
||||
def Triangle(self, subshape, **kwargs):
|
||||
submesh = Submesh(self.mesh, subshape)
|
||||
submesh.algo = self.mesh.Triangle(algo = smeshBuilder.NETGEN_1D2D, geom = subshape)
|
||||
submesh.mesh = submesh.algo.subm
|
||||
submesh.params = submesh.algo.Parameters()
|
||||
|
||||
submesh.params = updateParams(submesh.params, kwargs)
|
||||
|
||||
self.submeshes.append(submesh)
|
||||
def algo3d(self, algo, type = "tetrahedron"):
|
||||
smeshAlgo = self.mesh.__dict__.get(type.capitalize())
|
||||
self.meshAlgorithm3d = algo()
|
||||
self.meshAlgorithm3d.initialize(smeshAlgo(algo = self.meshAlgorithm3d.key))
|
||||
self.mesh.AddHypothesis(self.meshAlgorithm3d.hypo)
|
||||
|
||||
return self.meshAlgorithm3d
|
||||
|
||||
def algo2d(self, algo, type = "triangle"):
|
||||
smeshAlgo = self.mesh.__dict__.get(type.capitalize())
|
||||
self.meshAlgorithm2d = algo()
|
||||
self.meshAlgorithm2d.initialize(smeshAlgo(algo = self.meshAlgorithm2d.key))
|
||||
self.mesh.AddHypothesis(self.meshAlgorithm2d.hypo)
|
||||
|
||||
return self.meshAlgorithm2d
|
||||
|
||||
def algo1d(self, algo, type = "segment"):
|
||||
smeshAlgo = self.mesh.__dict__.get(type.capitalize())
|
||||
self.meshAlgorithm1d = algo()
|
||||
self.meshAlgorithm1d.initialize(smeshAlgo(algo = self.meshAlgorithm1d.key))
|
||||
self.mesh.AddHypothesis(self.meshAlgorithm1d.hypo)
|
||||
|
||||
return self.meshAlgorithm1d
|
||||
|
||||
def createGroups(self, prefix = None):
|
||||
prefix = prefix or ""
|
||||
|
||||
for group in self.shape.groups:
|
||||
name = group.GetName()
|
||||
|
||||
if name:
|
||||
name = prefix + name
|
||||
self.mesh.GroupOnGeom(group, name, SMESH.FACE)
|
||||
|
||||
def assignGroups(self, withPrefix = True):
|
||||
prefix = "smesh_" if withPrefix else ""
|
||||
|
||||
for group in self.mesh.geompyD.GetGroups(self.geom):
|
||||
if group.GetName():
|
||||
self.mesh.GroupOnGeom(group, f"{ prefix }{ group.GetName() }", SMESH.FACE)
|
||||
|
||||
def compute(self):
|
||||
"""Compute mesh.
|
||||
"""
|
||||
isDone = self.mesh.Compute()
|
||||
returncode = int(not isDone)
|
||||
out = ""
|
||||
err = self.mesh.GetComputeErrors()
|
||||
|
||||
return "", err, returncode
|
||||
|
||||
returncode = int(not isDone)
|
||||
|
||||
return out, err, returncode
|
||||
|
||||
def stats(self):
|
||||
return {
|
||||
"elements": self.mesh.NbElements(),
|
||||
@ -132,19 +85,6 @@ class Mesh(object):
|
||||
"pyramids": self.mesh.NbPyramids()
|
||||
}
|
||||
|
||||
def exportUNV(self, path):
|
||||
returncode = 0
|
||||
error = ""
|
||||
|
||||
try:
|
||||
self.mesh.ExportUNV(path)
|
||||
|
||||
except Exception as e:
|
||||
error = e.details.text
|
||||
returncode = 1
|
||||
|
||||
return returncode, error
|
||||
|
||||
def removePyramids(self):
|
||||
if self.mesh.NbPyramids() > 0:
|
||||
pyramidCriterion = smesh.GetCriterion(
|
||||
@ -159,15 +99,206 @@ class Mesh(object):
|
||||
self.mesh.SplitVolumesIntoTetra(pyramidVolumes, smesh.Hex_5Tet)
|
||||
|
||||
self.mesh.RemoveGroup(pyramidGroup)
|
||||
self.mesh.RenumberElements()
|
||||
self.mesh.RenumberElements()
|
||||
|
||||
def export(
|
||||
filename: str
|
||||
):
|
||||
"""Export a mesh.
|
||||
|
||||
Supported formats: unv.
|
||||
|
||||
:param filename:
|
||||
Name of the file to store the given mesh in.
|
||||
|
||||
:return:
|
||||
Output, error messages and returncode
|
||||
"""
|
||||
out, err, returncode = "", "", 0
|
||||
ext = os.path.splitext(filename)[1][1: ]
|
||||
|
||||
try:
|
||||
if ext == "unv":
|
||||
self.mesh.ExportUNV(self.mesh, filename)
|
||||
|
||||
else:
|
||||
raise NotImplementedError(f"{ ext } is not supported")
|
||||
|
||||
except NotImplementedError as e:
|
||||
err = e
|
||||
returncode = 1
|
||||
|
||||
except Exception as e:
|
||||
err = e.details.text
|
||||
returncode = 1
|
||||
|
||||
return out, err, returncode
|
||||
|
||||
|
||||
class MeshAlgorithm(object):
|
||||
pass
|
||||
|
||||
class Submesh(object):
|
||||
def __init__(self, father, subshape, name = ""):
|
||||
self.name = name if name else subshape.GetName()
|
||||
self.mesh = None
|
||||
self.geom = subshape
|
||||
self.algo = None
|
||||
self.params = None
|
||||
class AlgorithmHypothesis(object):
|
||||
pass
|
||||
|
||||
class Netgen3D(MeshAlgorithm):
|
||||
"""
|
||||
MaxElementVolume
|
||||
Parameters
|
||||
ViscousLayers
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
self.key = smeshBuilder.NETGEN_3D
|
||||
|
||||
def initialize(self, algo, hypo): #thesises: list):
|
||||
self.algo = algo
|
||||
#self.hypo = self.algo.Parameters()
|
||||
|
||||
#for hypo in hypothesises:
|
||||
|
||||
self.hypo = self.__dict__[hypo.__name__]()
|
||||
|
||||
class ViscousLayers(AlgorithmHypothesis):
|
||||
def __init__(self,
|
||||
algo,
|
||||
thickness = 1,
|
||||
numberOfLayers = 1,
|
||||
stretchFactor = 0,
|
||||
faces = [],
|
||||
isFacesToIgnore = True,
|
||||
extrMethod = "SURF_OFFSET_SMOOTH",
|
||||
**kwargs
|
||||
):
|
||||
extrusionMethod = dict(
|
||||
SURF_OFFSET_SMOOTH = smeshBuilder.SURF_OFFSET_SMOOTH,
|
||||
FACE_OFFSET = smeshBuilder.FACE_OFFSET,
|
||||
NODE_OFFSET = smeshBuilder.NODE_OFFSET,
|
||||
).get(extrMethod, smeshBuilder.SURF_OFFSET_SMOOTH)
|
||||
|
||||
self.hypo = self.algo.ViscousLayers(
|
||||
thickness,
|
||||
numberOfLayers,
|
||||
stretchFactor,
|
||||
faces,
|
||||
isFacesToIgnore,
|
||||
extrusionMethod
|
||||
)
|
||||
|
||||
class Parameters(AlgorithmHypothesis):
|
||||
def __init__(self, algo):
|
||||
self.hypo = self.algo.Parameters()
|
||||
|
||||
@property
|
||||
def minSize(self):
|
||||
return self.hypo.GetMinSize()
|
||||
|
||||
@minSize.setter
|
||||
def minSize(self, value):
|
||||
self.hypo.SetMinSize(value)
|
||||
|
||||
@property
|
||||
def maxSize(self):
|
||||
return self.hypo.GetMaxSize()
|
||||
|
||||
@maxSize.setter
|
||||
def maxSize(self, value):
|
||||
self.hypo.SetMaxSize(value)
|
||||
|
||||
@property
|
||||
def fineness(self):
|
||||
return self.hypo.GetFineness()
|
||||
|
||||
@fineness.setter
|
||||
def fineness(self, value):
|
||||
self.hypo.SetFineness(value)
|
||||
|
||||
@property
|
||||
def growthRate(self):
|
||||
return self.hypo.GetGrowthRate()
|
||||
|
||||
@growthRate.setter
|
||||
def growthRate(self, value):
|
||||
self.hypo.SetGrowthRate(value)
|
||||
|
||||
@property
|
||||
def nbSegPerEdge(self):
|
||||
return self.hypo.GetNbSegPerEdge()
|
||||
|
||||
@nbSegPerEdge.setter
|
||||
def nbSegPerEdge(self, value):
|
||||
self.hypo.SetNbSegPerEdge(value)
|
||||
|
||||
@property
|
||||
def nbSegPerRadius(self):
|
||||
return self.hypo.GetNbSegPerRadius()
|
||||
|
||||
@nbSegPerRadius.setter
|
||||
def nbSegPerRadius(self, value):
|
||||
self.hypo.SetNbSegPerRadius(value)
|
||||
|
||||
@property
|
||||
def chordalErrorEnabled(self):
|
||||
return self.hypo.GetChordalErrorEnabled()
|
||||
|
||||
@chordalErrorEnabled.setter
|
||||
def chordalErrorEnabled(self, value):
|
||||
self.hypo.SetChordalErrorEnabled(value)
|
||||
|
||||
@property
|
||||
def chordalError(self):
|
||||
return self.hypo.GetChordalError()
|
||||
|
||||
@chordalError.setter
|
||||
def chordalError(self, value):
|
||||
self.hypo.SetChordalError(value)
|
||||
|
||||
@property
|
||||
def secondOrder(self):
|
||||
return self.hypo.GetSecondOrder()
|
||||
|
||||
@secondOrder.setter
|
||||
def secondOrder(self, value):
|
||||
self.hypo.SetSecondOrder(value)
|
||||
|
||||
@property
|
||||
def optimize(self):
|
||||
return self.hypo.GetOptimize()
|
||||
|
||||
@optimize.setter
|
||||
def optimize(self, value):
|
||||
self.hypo.SetOptimize(value)
|
||||
|
||||
@property
|
||||
def quadAllowed(self):
|
||||
return self.hypo.GetQuadAllowed()
|
||||
|
||||
@quadAllowed.setter
|
||||
def quadAllowed(self, value):
|
||||
self.hypo.SetQuadAllowed(value)
|
||||
|
||||
@property
|
||||
def useSurfaceCurvature(self):
|
||||
return self.hypo.GetUseSurfaceCurvature()
|
||||
|
||||
@useSurfaceCurvature.setter
|
||||
def useSurfaceCurvature(self, value):
|
||||
self.hypo.SetUseSurfaceCurvature(value)
|
||||
|
||||
@property
|
||||
def fuseEdges(self):
|
||||
return self.hypo.GetFuseEdges()
|
||||
|
||||
@fuseEdges.setter
|
||||
def fuseEdges(self, value):
|
||||
self.hypo.SetFuseEdges(value)
|
||||
|
||||
@property
|
||||
def checkChartBoundary(self):
|
||||
return self.hypo.GetCheckChartBoundary()
|
||||
|
||||
@checkChartBoundary.setter
|
||||
def GetCheckChartBoundary(self, value):
|
||||
self.hypo.SetCheckChartBoundary(value)
|
||||
|
||||
class MEFISTO(MeshAlgorithm):
|
||||
pass
|
||||
|
@ -2,6 +2,6 @@
|
||||
# This file is part of anisotropy.
|
||||
# License: GNU GPL version 3, see the file "LICENSE" for details.
|
||||
|
||||
from anisotropy.samples.simple import Simple
|
||||
from anisotropy.samples.bodyCentered import BodyCentered
|
||||
from anisotropy.samples.faceCentered import FaceCentered
|
||||
#from anisotropy.samples.simple import Simple
|
||||
#from anisotropy.samples.bodyCentered import BodyCentered
|
||||
#from anisotropy.samples.faceCentered import FaceCentered
|
||||
|
@ -226,3 +226,26 @@ class BodyCentered(StructureGeometry):
|
||||
self.groups.append(self.geo.CutListOfGroups([groupAll], self.groups, theName = "wall"))
|
||||
|
||||
|
||||
class BodyCenteredMesh(Mesh):
|
||||
def build(self):
|
||||
algo2d = self.mesh.Triangle(algo = self.smeshBuilder.NETGEN_1D2D)
|
||||
hypo2d = algo2d.Parameters()
|
||||
hypo2d.SetMaxSize(0.1)
|
||||
hypo2d.SetMinSize(0.001)
|
||||
hypo2d.SetFineness(5)
|
||||
hypo2d.SetGrowthRate(0.3)
|
||||
hypo2d.SetNbSegPerEdge(2)
|
||||
hypo2d.SetNbSegPerRadius(3)
|
||||
hypo2d.SetChordalErrorEnabled(True)
|
||||
hypo2d.SetChordalError(0.05)
|
||||
hypo2d.SetOptimize(True)
|
||||
hypo2d.SetUseSurfaceCurvature(True)
|
||||
|
||||
algo3d = self.mesh.Tetrahedron(algo = self.smeshBuilder.NETGEN_3D)
|
||||
#hypo3d = algo3d.Parameters()
|
||||
|
||||
#faces = [ group for group in self.geom.groups if group.GetName() in ["inlet", "outlet"] ]
|
||||
#hypo3dVL = algo3d.ViscousLayers(...)
|
||||
|
||||
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
# License: GNU GPL version 3, see the file "LICENSE" for details.
|
||||
|
||||
from anisotropy.salomepl.geometry import StructureGeometry
|
||||
from anisotropy.salomepl.mesh import Mesh
|
||||
from numpy import pi, sqrt, fix
|
||||
import logging
|
||||
|
||||
@ -228,3 +229,27 @@ class FaceCentered(StructureGeometry):
|
||||
self.groups.append(self.createGroup(self.shape, shapeShell, "strips", self.groups + [grainsOrigin]))
|
||||
|
||||
self.groups.append(self.geo.CutListOfGroups([groupAll], self.groups, theName = "wall"))
|
||||
|
||||
class FaceCenteredMesh(Mesh):
|
||||
def build(self):
|
||||
algo2d = self.mesh.Triangle(algo = self.smeshBuilder.NETGEN_1D2D)
|
||||
hypo2d = algo2d.Parameters()
|
||||
hypo2d.SetMaxSize(0.1)
|
||||
hypo2d.SetMinSize(0.001)
|
||||
hypo2d.SetFineness(5)
|
||||
hypo2d.SetGrowthRate(0.3)
|
||||
hypo2d.SetNbSegPerEdge(2)
|
||||
hypo2d.SetNbSegPerRadius(3)
|
||||
hypo2d.SetChordalErrorEnabled(True)
|
||||
hypo2d.SetChordalError(0.05)
|
||||
hypo2d.SetOptimize(True)
|
||||
hypo2d.SetUseSurfaceCurvature(True)
|
||||
|
||||
algo3d = self.mesh.Tetrahedron(algo = self.smeshBuilder.NETGEN_3D)
|
||||
#hypo3d = algo3d.Parameters()
|
||||
|
||||
#faces = [ group for group in self.geom.groups if group.GetName() in ["inlet", "outlet"] ]
|
||||
#hypo3dVL = algo3d.ViscousLayers(...)
|
||||
|
||||
|
||||
|
||||
|
@ -3,6 +3,7 @@
|
||||
# License: GNU GPL version 3, see the file "LICENSE" for details.
|
||||
|
||||
from anisotropy.salomepl.geometry import StructureGeometry
|
||||
from anisotropy.salomepl.mesh import Mesh
|
||||
from numpy import pi, sqrt, fix
|
||||
import logging
|
||||
|
||||
@ -211,3 +212,36 @@ class Simple(StructureGeometry):
|
||||
self.groups.append(self.createGroup(self.shape, shapeShell, "strips", self.groups + [grainsOrigin]))
|
||||
|
||||
self.groups.append(self.geo.CutListOfGroups([groupAll], self.groups, theName = "wall"))
|
||||
|
||||
|
||||
class SimpleMesh(Mesh):
|
||||
def build(self):
|
||||
algo2d = self.mesh.Triangle(algo = self.smeshBuilder.NETGEN_1D2D)
|
||||
hypo2d = algo2d.Parameters()
|
||||
hypo2d.SetMaxSize(0.1)
|
||||
hypo2d.SetMinSize(0.001)
|
||||
hypo2d.SetFineness(5)
|
||||
hypo2d.SetGrowthRate(0.3)
|
||||
hypo2d.SetNbSegPerEdge(2)
|
||||
hypo2d.SetNbSegPerRadius(3)
|
||||
hypo2d.SetChordalErrorEnabled(True)
|
||||
hypo2d.SetChordalError(0.05)
|
||||
hypo2d.SetOptimize(True)
|
||||
hypo2d.SetUseSurfaceCurvature(True)
|
||||
|
||||
algo3d = self.mesh.Tetrahedron(algo = self.smeshBuilder.NETGEN_3D)
|
||||
#hypo3d = algo3d.Parameters()
|
||||
|
||||
#faces = [ group for group in self.geom.groups if group.GetName() in ["inlet", "outlet"] ]
|
||||
#hypo3dVL = algo3d.ViscousLayers(...)
|
||||
|
||||
|
||||
from anisotropy.openfoam.foamcase import ControlDict
|
||||
|
||||
class SimpleFlow(object): # FoamCase
|
||||
def __init__(self):
|
||||
controlDict = ControlDict()
|
||||
controlDict["startFrom"] = "latestTime"
|
||||
controlDict["endTime"] = 5000
|
||||
controlDict["writeInterval"] = 100
|
||||
|
||||
|
@ -1,35 +1,36 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# This file is part of anisotropy.
|
||||
# License: GNU GPL version 3, see the file "LICENSE" for details.
|
||||
|
||||
class MeshAlgorithm(object):
|
||||
import logging
|
||||
|
||||
SMESH_IMPORTED = False
|
||||
|
||||
try:
|
||||
import SMESH
|
||||
from salome.smesh import smeshBuilder
|
||||
|
||||
SMESH_IMPORTED = True
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
class Netgen3D(MeshAlgorithm):
|
||||
def __init__(self, **kwargs):
|
||||
self.key = smeshBuilder.NETGEN_3D
|
||||
|
||||
def initialize(self, algo):
|
||||
self.algo = algo
|
||||
self.hypo = self.algo.Parameters()
|
||||
|
||||
|
||||
@property
|
||||
def minSize(self):
|
||||
return self.hypo.GetMinSize()
|
||||
|
||||
@minSize.setter
|
||||
def minSize(self, value):
|
||||
self.hypo.SetMinSize(value)
|
||||
|
||||
class MEFISTO(MeshAlgorithm):
|
||||
pass
|
||||
|
||||
class Mesh(object):
|
||||
def __init__(self, geom):
|
||||
|
||||
self.smesh = smeshBuilder.New()
|
||||
# Mesh module
|
||||
if not SMESH_IMPORTED:
|
||||
raise ImportError("Cannot find the salome mesh modules.")
|
||||
|
||||
else:
|
||||
self.smesh = smeshBuilder.New()
|
||||
self.smeshBuilder = smeshBuilder
|
||||
|
||||
# General attributes
|
||||
self.geom = geom
|
||||
self.mesh = self.smesh.Mesh(self.geom.shape, self.geom.name)
|
||||
|
||||
def algo3d(self, algo: MeshAlgorithm, type = "tetrahedron"):
|
||||
def algo3d(self, algo, type = "tetrahedron"):
|
||||
smeshAlgo = self.mesh.__dict__.get(type.capitalize())
|
||||
self.meshAlgorithm3d = algo()
|
||||
self.meshAlgorithm3d.initialize(smeshAlgo(algo = self.meshAlgorithm3d.key))
|
||||
@ -37,7 +38,7 @@ class Mesh(object):
|
||||
|
||||
return self.meshAlgorithm3d
|
||||
|
||||
def algo2d(self, algo: MeshAlgorithm, type = "triangle"):
|
||||
def algo2d(self, algo, type = "triangle"):
|
||||
smeshAlgo = self.mesh.__dict__.get(type.capitalize())
|
||||
self.meshAlgorithm2d = algo()
|
||||
self.meshAlgorithm2d.initialize(smeshAlgo(algo = self.meshAlgorithm2d.key))
|
||||
@ -45,7 +46,7 @@ class Mesh(object):
|
||||
|
||||
return self.meshAlgorithm2d
|
||||
|
||||
def algo1d(self, algo: MeshAlgorithm, type = "segment"):
|
||||
def algo1d(self, algo, type = "segment"):
|
||||
smeshAlgo = self.mesh.__dict__.get(type.capitalize())
|
||||
self.meshAlgorithm1d = algo()
|
||||
self.meshAlgorithm1d.initialize(smeshAlgo(algo = self.meshAlgorithm1d.key))
|
||||
@ -83,7 +84,23 @@ class Mesh(object):
|
||||
"prisms": self.mesh.NbPrisms(),
|
||||
"pyramids": self.mesh.NbPyramids()
|
||||
}
|
||||
|
||||
|
||||
def removePyramids(self):
|
||||
if self.mesh.NbPyramids() > 0:
|
||||
pyramidCriterion = smesh.GetCriterion(
|
||||
SMESH.VOLUME,
|
||||
SMESH.FT_ElemGeomType,
|
||||
SMESH.FT_Undefined,
|
||||
SMESH.Geom_PYRAMID
|
||||
)
|
||||
pyramidGroup = self.mesh.MakeGroupByCriterion("pyramids", pyramidCriterion)
|
||||
pyramidVolumes = self.mesh.GetIDSource(pyramidGroup.GetIDs(), SMESH.VOLUME)
|
||||
|
||||
self.mesh.SplitVolumesIntoTetra(pyramidVolumes, smesh.Hex_5Tet)
|
||||
|
||||
self.mesh.RemoveGroup(pyramidGroup)
|
||||
self.mesh.RenumberElements()
|
||||
|
||||
def export(
|
||||
filename: str
|
||||
):
|
||||
@ -117,3 +134,171 @@ class Mesh(object):
|
||||
|
||||
return out, err, returncode
|
||||
|
||||
|
||||
class MeshAlgorithm(object):
|
||||
pass
|
||||
|
||||
class AlgorithmHypothesis(object):
|
||||
pass
|
||||
|
||||
class Netgen3D(MeshAlgorithm):
|
||||
"""
|
||||
MaxElementVolume
|
||||
Parameters
|
||||
ViscousLayers
|
||||
"""
|
||||
def __init__(self, **kwargs):
|
||||
self.key = smeshBuilder.NETGEN_3D
|
||||
|
||||
def initialize(self, algo, hypo): #thesises: list):
|
||||
self.algo = algo
|
||||
#self.hypo = self.algo.Parameters()
|
||||
|
||||
#for hypo in hypothesises:
|
||||
|
||||
self.hypo = self.__dict__[hypo.__name__]()
|
||||
|
||||
class ViscousLayers(AlgorithmHypothesis):
|
||||
def __init__(self,
|
||||
algo,
|
||||
thickness = 1,
|
||||
numberOfLayers = 1,
|
||||
stretchFactor = 0,
|
||||
faces = [],
|
||||
isFacesToIgnore = True,
|
||||
extrMethod = "SURF_OFFSET_SMOOTH",
|
||||
**kwargs
|
||||
):
|
||||
extrusionMethod = dict(
|
||||
SURF_OFFSET_SMOOTH = smeshBuilder.SURF_OFFSET_SMOOTH,
|
||||
FACE_OFFSET = smeshBuilder.FACE_OFFSET,
|
||||
NODE_OFFSET = smeshBuilder.NODE_OFFSET,
|
||||
).get(extrMethod, smeshBuilder.SURF_OFFSET_SMOOTH)
|
||||
|
||||
self.hypo = self.algo.ViscousLayers(
|
||||
thickness,
|
||||
numberOfLayers,
|
||||
stretchFactor,
|
||||
faces,
|
||||
isFacesToIgnore,
|
||||
extrusionMethod
|
||||
)
|
||||
|
||||
class Parameters(AlgorithmHypothesis):
|
||||
def __init__(self, algo):
|
||||
self.hypo = self.algo.Parameters()
|
||||
|
||||
@property
|
||||
def minSize(self):
|
||||
return self.hypo.GetMinSize()
|
||||
|
||||
@minSize.setter
|
||||
def minSize(self, value):
|
||||
self.hypo.SetMinSize(value)
|
||||
|
||||
@property
|
||||
def maxSize(self):
|
||||
return self.hypo.GetMaxSize()
|
||||
|
||||
@maxSize.setter
|
||||
def maxSize(self, value):
|
||||
self.hypo.SetMaxSize(value)
|
||||
|
||||
@property
|
||||
def fineness(self):
|
||||
return self.hypo.GetFineness()
|
||||
|
||||
@fineness.setter
|
||||
def fineness(self, value):
|
||||
self.hypo.SetFineness(value)
|
||||
|
||||
@property
|
||||
def growthRate(self):
|
||||
return self.hypo.GetGrowthRate()
|
||||
|
||||
@growthRate.setter
|
||||
def growthRate(self, value):
|
||||
self.hypo.SetGrowthRate(value)
|
||||
|
||||
@property
|
||||
def nbSegPerEdge(self):
|
||||
return self.hypo.GetNbSegPerEdge()
|
||||
|
||||
@nbSegPerEdge.setter
|
||||
def nbSegPerEdge(self, value):
|
||||
self.hypo.SetNbSegPerEdge(value)
|
||||
|
||||
@property
|
||||
def nbSegPerRadius(self):
|
||||
return self.hypo.GetNbSegPerRadius()
|
||||
|
||||
@nbSegPerRadius.setter
|
||||
def nbSegPerRadius(self, value):
|
||||
self.hypo.SetNbSegPerRadius(value)
|
||||
|
||||
@property
|
||||
def chordalErrorEnabled(self):
|
||||
return self.hypo.GetChordalErrorEnabled()
|
||||
|
||||
@chordalErrorEnabled.setter
|
||||
def chordalErrorEnabled(self, value):
|
||||
self.hypo.SetChordalErrorEnabled(value)
|
||||
|
||||
@property
|
||||
def chordalError(self):
|
||||
return self.hypo.GetChordalError()
|
||||
|
||||
@chordalError.setter
|
||||
def chordalError(self, value):
|
||||
self.hypo.SetChordalError(value)
|
||||
|
||||
@property
|
||||
def secondOrder(self):
|
||||
return self.hypo.GetSecondOrder()
|
||||
|
||||
@secondOrder.setter
|
||||
def secondOrder(self, value):
|
||||
self.hypo.SetSecondOrder(value)
|
||||
|
||||
@property
|
||||
def optimize(self):
|
||||
return self.hypo.GetOptimize()
|
||||
|
||||
@optimize.setter
|
||||
def optimize(self, value):
|
||||
self.hypo.SetOptimize(value)
|
||||
|
||||
@property
|
||||
def quadAllowed(self):
|
||||
return self.hypo.GetQuadAllowed()
|
||||
|
||||
@quadAllowed.setter
|
||||
def quadAllowed(self, value):
|
||||
self.hypo.SetQuadAllowed(value)
|
||||
|
||||
@property
|
||||
def useSurfaceCurvature(self):
|
||||
return self.hypo.GetUseSurfaceCurvature()
|
||||
|
||||
@useSurfaceCurvature.setter
|
||||
def useSurfaceCurvature(self, value):
|
||||
self.hypo.SetUseSurfaceCurvature(value)
|
||||
|
||||
@property
|
||||
def fuseEdges(self):
|
||||
return self.hypo.GetFuseEdges()
|
||||
|
||||
@fuseEdges.setter
|
||||
def fuseEdges(self, value):
|
||||
self.hypo.SetFuseEdges(value)
|
||||
|
||||
@property
|
||||
def checkChartBoundary(self):
|
||||
return self.hypo.GetCheckChartBoundary()
|
||||
|
||||
@checkChartBoundary.setter
|
||||
def GetCheckChartBoundary(self, value):
|
||||
self.hypo.SetCheckChartBoundary(value)
|
||||
|
||||
class MEFISTO(MeshAlgorithm):
|
||||
pass
|
||||
|
@ -1,5 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys; path = "/home/nafaryus/projects/anisotropy"; sys.path.extend([path, path + "/env/lib/python3.10/site-packages"])
|
||||
from anisotropy.samples import Simple, FaceCentered, BodyCentered
|
||||
s = Simple([1, 0, 0], 0.28, filletsEnabled = True)
|
||||
from anisotropy.samples.faceCentered import FaceCentered, FaceCenteredMesh
|
||||
fc = FaceCentered([1, 0, 0], 0.12, filletsEnabled = True)
|
||||
fc.build()
|
||||
fcm = FaceCenteredMesh(fc)
|
||||
fcm.build()
|
||||
|
@ -7,3 +7,4 @@ pandas
|
||||
Click
|
||||
matplotlib
|
||||
pyqt5
|
||||
PyFoam
|
||||
|
Loading…
Reference in New Issue
Block a user