Skip to content

Code: basics

William Jussiau edited this page Feb 24, 2025 · 6 revisions

Convention

Use of the _prefix for methods that are not intended to be used outside of the body of a class.

Basic use

Define a new use-case: inherit FlowSolver abstract class

The simulation revolves around the abstract class FlowSolver that implements core features such as loading mesh, defining function spaces, trial/test functions, variational formulations, numerical schemes and solvers, handling the time-stepping and exporting information. The class is abstract as it does not implement a simulation case per se, but only provides utility for doing so. It features two abstract methods, that are redefined for each use-case:

  1. _make_boundaries provides a definition and naming of the boundaries of the mesh in a pandas DataFrame.
    @abstractmethod
    def _make_boundaries(self) -> pd.DataFrame:
        pass
  1. _make_bcs provides a description of the boundary conditions on the boundaries defined above, in a dictionary.
    @abstractmethod
    def _make_bcs(self) -> dict[str, Any]:
        pass

For the two aforementioned examples, these methods are reimplemented in the classes CylinderFlowSolver and CavityFlowSolver that inherit from FlowSolver.

You also need to be able to provide a mesh that is consistent with your definition of boundaries

Attach Sensors and Actuators to an instance of a FlowSolver subclass

In order to perform sensing and actuation (in order to close the loop), dedicated classes Sensor and Actuator are proposed. They are not aimed at being instantiated, but rather inherited.

  • Sensor is an abstract class that provides a method eval(self, up: dolfin.Function) -> float. Classes SensorPoint (point probe) and SensorIntegral (integration on a subdomain) are examples of subclasses that implement the eval method.
  • Likewise, Actuator is an abstract class that encapsulates a dolfin.Expression among other elements, and embeds it in the variational formulations.

The sensors and actuators are attached to a FlowSolver object as a list, embedded in the ParamControl object. The call to Sensors and Actuators is made automatically by FlowSolver. By attaching several sensors or actuators, it is possible to use Multiple-Input, Multiple-Output controllers in the loop.

In the example below (for the cylinder use-case), we are creating an actuator acting on boundary conditions and three point probes at different locations. They are gathered in a ParamControl object, which is passed as an argument when creating a CylinderFlowSolver.

# Actuator
actuator_bc = ActuatorBCParabolicV(angular_size_deg=10)
# Sensors
sensor_feedback = SensorPoint(sensor_type=SENSOR_TYPE.V, position=np.array([3, 0]))
sensor_perf_1 = SensorPoint(sensor_type=SENSOR_TYPE.V, position=np.array([3.1, 1]))
sensor_perf_2 = SensorPoint(sensor_type=SENSOR_TYPE.V, position=np.array([3.1, -1.3]))
# Gather actuators and sensors in ParamControl object
params_control = ParamControl(
    sensor_list=[sensor_feedback, sensor_perf_1, sensor_perf_2],
    actuator_list=[actuator_bc],
)

Run a closed-loop simulation

Once a use-case has been defined by implementing the corresponding class, the basic feedback syntax has the following philosophy:

# Instantiate and initialize FlowSolver object
fs = CylinderFlowSolver(...)
fs.compute_steady_state(...)
fs.initialize_time_stepping(...)

# Instantiate Controller (e.g. load from .mat file)
Kss = Controller.from_file(...)

# Time loop
y_meas = fs.y_meas
for _ in range(fs.params_time.num_steps):
    u_ctrl = Kss.step(y=-y_meas[0], dt=fs.params_time.dt)
    y_meas = fs.step(u_ctrl=u_ctrl)

See examples for a more detailed description.

More details

Initializations in general

Mesh

Mesh as xdmf. User is responsible for mesh coherence with respect to boundary conditions.

Initialization of a FlowSolver

Timeline of function calls at __init__ for example.

Initialize time-stepping

Initial guess for base flow computation

  • good practice is to do Picard iterations, then Newton
_default_steady_state_initial_guess(self) -> dolfin.UserExpression

Perturbation of initial state

_default_initial_perturbation(self, xloc: float = 0.0, yloc: float = 0.0, radius: float = 1.0) -> dolfin.Function

FlowSolverParameters

user_data: dict

Inlet flow profile

FlowField, FlowFieldCollection, self.fields

Actuators

Principle

Defining new actuators

Types of actuators

Their contribution does not go into the same function:

  • FORCE is seamless
  • BC goes somewhere in _make_bcs() and should be handled by the user. An example can be found in examples/cylinder/cylinderflowsolver.py in _make_bcs(), as follows:
def _make_bcs(self):
    ...
    bcu_actuation_up = dolfin.DirichletBC(
        self.W.sub(0),
        self.params_control.actuator_list[0].expression,
        self.get_subdomain["actuator_up"],
        )
    bcu_actuation_lo = dolfin.DirichletBC(
        self.W.sub(0),
        self.params_control.actuator_list[1].expression,
        self.get_subdomain["actuator_lo"],
        )
    ...
    return BoundaryConditions(bcu=bcu, bcp=[])

Sensors

Principle

Defining new sensors

User is responsible for parallel execution when implementing new sensors.

Saving files

save every

Controller

What is this class

control.StateSpace encapsulating a current state x and potentially a file

What is step

Wrapper around control.StateSpace.forced_response

Additional uses of the toolbox

The toolbox provides additional utility related to flow control:

Clone this wiki locally