Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Discussion for redesign #159

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft

Discussion for redesign #159

wants to merge 2 commits into from

Conversation

srosenbu
Copy link
Member

To continue the discussion from #158:

Everything is currently very WIP and I am not happy with everything, so please give feedback;)

The code does not really work as a package, but you can build the conda env and then run the experiment.py and the problem.py which currently just print a serialized JSON of the classes.

Copy link
Member

@joergfunger joergfunger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, this looks already very good, only the definition of the Problem is IMO too specific. In addition, I would put the virtual sensors in a class (Postprocessing?) that is one the one hand storing/measuring the data, and then also able to visualize them.

class DirichletBCDefinition:
"""
Definition of a time- and position-dependent Dirichlet boundary condition.
Note that functions cannot be serialized to JSON!
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using the eval function, you could define a string and then convert that to a python function. The string can be stored in json

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can do that. To me, it screams security risk but then again, so would be importing any code you haven't written yourself

value: conlist(float, min_length=1, max_length=3) | Callable[[np.ndarray, float], np.ndarray]

@dataclass(config=dict(arbitrary_types_allowed=True))
class BodyForceDefinition:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

body force is very much mechanics driven, for the heat equation,this would be different (volumetric heat flux). I guess this would also just be a function of x and t and returns the corresponding term.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I wasn't sure what to do here. What would be the general term for body force? So any integral

$$ \int_\Omega \mathbf b \rho \mathrm d x $$

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

source term?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure how we should generalize this. As far as I see it, the body-force definition is constant because gravity is constant. But how is it with heat flux? From this doc https://www.simscale.com/docs/simulation-setup/boundary-conditions/volume-heat-flux/ I get that it could vary with time and position

Copy link
Member

@joergfunger joergfunger Oct 9, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wouldn't that be the same as the functional representation of boundary conditions being a function of space and time? So there could certainly be constant or linear functions as standard (both relevant for body forces or linearly increasing boundary conditions) and then have the functional representation as additional option.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but my question is more related to what should be allowed. In an ideal world, the data structure would ensure that only valid values (like constant gravity) are allowed, but I guess that should then be the responsibility of the user.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I guess it will be very hard to be able to define problems and test the validity beforehand (just from the syntax). And for Dirichlet BC I see now way around that, otherwise we are very limited, so then it makes no difference it that is added here again. But we could define two functions, one for BodyForce = constant and SpaceTimeDependentBodyForce(x,t).

dirichlet_bcs: list[DirichletBCDefinition] | None
neumann_bcs: list[NeumannBCDefinition] | None
initial_conditions: list[InitialConditionDefinition] | None
body_forces: list[BodyForceDefinition] | None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is only mechanics

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only because of the body forces. Currently, each BC knows on which variable it should be applied, so the list of DirichletBCDefinition can contain bcs for any problem. I am however not sure about this design decision. Either the BC knows where it belongs because of some property, or alternatively it could know this because of its type. This would mean that for each physical problem we would introduce another BC type like TemperatureBC

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when you provide the dof (T, d) as additional parameter, then it is completely clear. Just have to make sure that the two have the same dimension.

body_forces: list[BodyForceDefinition] | None
geometry: MeshGenerator
solution_fields: list[str]
time: tuple[float, float]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the time tuple, beginning and end?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. We could also do a list of timesteps for the minimal amount of load steps and then the solver decides if any in-between steps should be taken.

geometry: MeshGenerator
solution_fields: list[str]
time: tuple[float, float]
material: MaterialDefinition
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the material definition somehow has to be related to the geometry or physical components in case more than one material is used, which I think is something we should be able to cover.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True. Then the question is how we define a material. Is it just a string and then the solver needs to interpret that?

@@ -0,0 +1,73 @@
from pydantic.dataclasses import dataclass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my original idea, there was no general problem definition class apart from a very basic on with a solve functionality (to be coupled with probeye or other sensor readings) and potential parameters that are read in from a json file in addition to the experiment. The option to store sensor data in the simulation could IMO provided by a completely separate class that is initialized with an expeiment (thus potentially has all information and names from the sensors) and is called with a FEniCS solution and the time and then knows how to store the virtual sensor readings. Everything else is very user-specific. Or is there an advantage of adding more functionality (quadrature, interpolation order, ..) as general features?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem.py is more an example for trying out how a full json would look like.

I personally see benefits for storing everything that I added because in a workflow, I would could then edit a json that is read by the simulation script instead of changing the script.

As for what should always be contained in a problem, I would say that it should always be able to return sensor readings and it should always be able to plot all variables that are stored. The input can be an experiment and a dictionary of parameters. But apart from that, I am fine with not restricting what a problem can do

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering what the advantage of the plotting capabilities in the problem is, instead of just adding that (and that is something everyone could decide himself) when creating the model. Then this class can be used afterwards to do the plotting (and does not even has to know the FE-problem), just the mesh, which is provided in the experiment. Then you can use any tool to visualize your results, potentially even in a postprocessing step (when having serialized the results) with a different software tool (visualizing ansys results with pyvista)

variable: The variable to be measured

"""
location: tuple[float,float,float]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what are the units of the location? Always meter?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not take care of units yet


"""
location: tuple[float,float,float]
variable: str
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should the variables be restricted to the predefined enum instead of general strings?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know yet. The enum would have the benefit that the input can very easily be checked, but it would also be restrictive. If you have a new material model with weird history variables, it would be easier to use the sting

unit: str

@dataclass(config=dict(arbitrary_types_allowed=True))
class PlotDefinition:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure what that class represents, a fieldSensor?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically yes. It is different from a sensor because, it needs to write to a file with each measurement


def measure(self, t: float):
for plot_function, function in zip(self.plot_functions.values(), self.functions.values()):
if plot_function is not None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see benefits in making that an external class (not within the standard problem). Thus, you would have to pass the fenics data structure to be able to measure from that. However, I also see that it has benefits to integrate that into BaseProblem, in particular the passing of the solution fields. However, the latter is somehow more difficult to read, since the variables are defined in a different class than where they are used.

initial = InitialConditionDefinition(value=[42.24], variable='density')
body_force = BodyForceDefinition(value=[0.,0., 9.81], variable='displacement')
mat = LinearElasticMaterial(name='steel', mu=1., lam=2.)
geo = MeshGenerator(parameters={'length': (1, 'm')}, mesh_tags={'left': 0, 'right': 1, 'top': 2, 'bottom': 3})
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can we cover the case that the mesh is given from other software (abaqus ...) or someone send me the mesh fiel and I have to use that? MeshGenerator = reading existing file and defining boundary groups (this would then be fenics stuff, or?)

bc = DirichletBCDefinition(marker=1, value=[0.,0.], subspace=0, variable='displacement')
neumann = NeumannBCDefinition(marker=2, value=[-42.0], variable='displacement')
initial = InitialConditionDefinition(value=[42.24], variable='density')
body_force = BodyForceDefinition(value=[0.,0., 9.81], variable='displacement')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How can we ensure that the body force is applied in the correct direction? We had some problems on that in the current version.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the users coordinate system and the three components of the body force I would expect that to be unique, what was the problem in the current version?

Stress = Quantity("Pa")
Density = Quantity("kg/m^3")
Time = Quantity("s")

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would I use that if I have other, additional quantities? Would I create my own class AnnikaQuantity(Enum) or do we add that here? Some question for the SolutionFields Class ...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you have something that is general, I would add that here, if that is something really user specific, I would just define that in your own problem definition.

plots=[plot],
element_type=[FiniteElementDefinition(name="P", geometry_order=1, function_order=1, variable="displacement")],
quadrature_rule=[QuadratureRuleDefinition(name="Gauss", order=2, variable="displacement")],
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't get where the real computation happens

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think exactly here, but I guess this is currently missing in the proposal. Maybe having some linearElastic computation here would help to better understand the idea and how the poblem definition is then used. And I'm not yet sure what these inputs would be, and in particular, if FEMProblemDefinition is a BaseClass or just a class everyone is reimplementing according to their needs.


from typing import Callable
from pydantic.dataclasses import dataclass

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only a general definition of the material, right? At which point do we implement what happens if you choose e.g linear elasticity?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the level of your solver (FEMProblemDefinition -though the name is IMO too long). You would then built your weak form and have an additional configuration file (but that does not have to be general but is really depending on your problem) that includes the material parameters (if you always use linearElastic), or the constitutive law and the parameters (if you decide to be able to allow on the configuration file level different material laws).

@dataclass(config=dict(arbitrary_types_allowed=True))
class DirichletBCDefinition:
"""
Definition of a time- and position-dependent Dirichlet boundary condition.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would the Dirichlet boundary condition be defined in time? Just passing a np.ndarray or a function?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would need to be a function $f(x,t)\mapsto y,\quad x\in\partial\Omega$, so in python a Callable[[np.ndarray, float],np.ndarray] . For simplicity, a numpy array would be interpreted as a constant BC at every point in time

plots: list[PlotDefinition]
element_type: list[FiniteElementDefinition]
quadrature_rule: list[QuadratureRuleDefinition]

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here, the different Experiment and Definition objects would only be used for initializing the problem and then can be forgotten, right? That would work with the model updating schemes, as then I can access directly the variables from FEMProblem (or the derive class) and update their values.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say, that most attributes could be forgotten after initialization. However, I haven't really considered the implementation part of the problems yet.

groups: dict[str, list[PointSensorDefinition | GlobalSensorDefinition | PlotDefinition]]
#plot_functions: dict[str, df.fem.Function | None]
#functions: dict[str, df.fem.Function]

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One functionality that is important for me is to be able to create sensors from metadata and export the sensors metadata as well. To export it, that would probably be just a new method here for the class Sensors. To import a sensor from metadata, I guess we would need to write a new function that adds it to the sensors list. That should not be a problem, but means keeping and updating the json schema to the dataclass structure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of my design goals was that all classes have a very obvious representation as a dictionary/json, and you can just create a dataclass from a dictionary

sensor = Sensor(*dictionary)

Since the classes here only describe the sensors and have no methods, serializing and deserializing them should be what you are looking for

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants