Skip to content

[ServiceIntegration] Add operating condition unit test for the implementation of custom cache constructor #324

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

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -328,4 +328,4 @@ tmp/
/.vscode

# test residual
flow360/examples/cylinder/flow360mesh.json
flow360/examples/cylinder2D/flow360mesh.json
115 changes: 115 additions & 0 deletions examples/simulation_e2e.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import os

import flow360 as fl
from flow360.component.simulation.meshing_param.face_params import (
BoundaryLayer,
SurfaceRefinement,
)
from flow360.component.simulation.models.surface_models import Freestream, Wall
from flow360.component.simulation.models.volume_models import Fluid
from flow360.component.simulation.operating_condition import AerospaceCondition
from flow360.component.simulation.primitives import ReferenceGeometry, Surface
from flow360.component.simulation.services import (
simulation_to_case_json,
simulation_to_surface_meshing_json,
simulation_to_volume_meshing_json,
)
from flow360.component.simulation.simulation_params import (
MeshingParams,
SimulationParams,
)
from flow360.component.simulation.time_stepping.time_stepping import Steady
from flow360.component.simulation.unit_system import SI_unit_system, u

fl.UserConfig.set_profile("auto_test_1")
fl.Env.dev.active()

from flow360.component.geometry import Geometry
from flow360.examples import Airplane

SOLVER_VERSION = "workbench-24.6.0"


with SI_unit_system:
meshing = MeshingParams(
surface_layer_growth_rate=1.5,
refinements=[
BoundaryLayer(first_layer_thickness=0.001),
SurfaceRefinement(
entities=[Surface(name="wing")],
max_edge_length=15 * u.cm,
curvature_resolution_angle=10 * u.deg,
),
],
)
param = SimulationParams(
meshing=meshing,
reference_geometry=ReferenceGeometry(
moment_center=(1, 2, 3), moment_length=1.0 * u.m, area=1.0 * u.cm**2
),
operating_condition=AerospaceCondition(velocity_magnitude=100),
models=[
Fluid(),
Wall(
entities=[
Surface(name="fluid/rightWing"),
Surface(name="fluid/leftWing"),
Surface(name="fluid/fuselage"),
],
),
Freestream(entities=[Surface(name="fluid/farfield")]),
],
time_stepping=Steady(max_steps=700),
)

params_as_dict = param.model_dump()
surface_json, hash = simulation_to_surface_meshing_json(
params_as_dict, "SI", {"value": 100.0, "units": "cm"}
)
print(surface_json)
volume_json, hash = simulation_to_volume_meshing_json(
params_as_dict, "SI", {"value": 100.0, "units": "cm"}
)
print(volume_json)
case_json, hash = simulation_to_case_json(params_as_dict, "SI", {"value": 100.0, "units": "cm"})
print(case_json)


prefix = "testing-workbench-integration-airplane-csm"

# geometry
geometry_draft = Geometry.from_file(
Airplane.geometry, name=f"{prefix}-geometry", solver_version=SOLVER_VERSION
)
geometry = geometry_draft.submit()
print(geometry)

# surface mesh
params = fl.SurfaceMeshingParams(**surface_json)

surface_mesh_draft = fl.SurfaceMesh.create(
geometry_id=geometry.id,
params=params,
name=f"{prefix}-surface-mesh",
solver_version=SOLVER_VERSION,
)
surface_mesh = surface_mesh_draft.submit()

print(surface_mesh)

# volume mesh
params = fl.VolumeMeshingParams(**volume_json)

volume_mesh_draft = fl.VolumeMesh.create(
surface_mesh_id=surface_mesh.id,
name=f"{prefix}-volume-mesh",
params=params,
solver_version=SOLVER_VERSION,
)
volume_mesh = volume_mesh_draft.submit()
print(volume_mesh)

# case
params = fl.Flow360Params(**case_json, legacy_fallback=True)
case_draft = volume_mesh.create_case(f"{prefix}-case", params, solver_version=SOLVER_VERSION)
case = case_draft.submit()
1 change: 1 addition & 0 deletions flow360/component/flow360_params/updater.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def _no_update(params_as_dict):
("23.3.0", "23.3.*", _no_update),
("23.3.*", "24.2.*", _no_update),
("24.2.*", "24.2.*", _no_update),
("24.2.*", "24.3.*", _no_update), # we should not allow to submit Flow360Params to version 24.3
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

So why we add an updater here?

Copy link
Collaborator

@maciej-flexcompute maciej-flexcompute Jun 18, 2024

Choose a reason for hiding this comment

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

otherwise it will fail tests. There is only single concept of version and is used by both Flow360Params and SimulationParams, we would need to separate these

]


Expand Down
6 changes: 6 additions & 0 deletions flow360/component/simulation/exposed_units.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,10 @@
"thermal_conductivity": [],
"inverse_area": [],
"inverse_length": [],
"angle": [],
"specific_energy": [],
"frequency": [],
"mass_flow_rate": [],
"power": [],
"moment": [],
}
59 changes: 0 additions & 59 deletions flow360/component/simulation/framework/cached_model_base.py

This file was deleted.

181 changes: 181 additions & 0 deletions flow360/component/simulation/framework/multi_constructor_model_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
"""MultiConstructorModelBase class for Pydantic models with multiple constructors."""

import abc
import inspect
from contextlib import contextmanager
from functools import wraps
from typing import Any, Callable, Literal, Optional

import pydantic as pd

from flow360.component.simulation.framework.base_model import Flow360BaseModel

# requirements for data models with custom constructors:
# 1. data model can be saved to JSON and read back to pydantic model without problems
# 2. data model can return data provided to custom constructor
# 3. data model can be created from JSON that contains only custom constructor inputs - incomplete JSON
# 4. incomplete JSON is not in a conflict with complete JSON (from point 1), such that there is no need for 2 parsers


@contextmanager
def _model_attribute_unlock(model, attr: str):
try:
# validate_assignment is set to False to allow for the attribute to be modified
# Otherwise, the attribute will STILL be frozen and cannot be modified
model.model_config["validate_assignment"] = False
model.model_fields[attr].frozen = False
yield
finally:
model.model_config["validate_assignment"] = True
model.model_fields[attr].frozen = True


class _MultiConstructorModelBase(Flow360BaseModel, metaclass=abc.ABCMeta):

type_name: Literal["_MultiConstructorModelBase"] = pd.Field(
"_MultiConstructorModelBase", frozen=True
)
private_attribute_constructor: str = pd.Field("default", frozen=True)
private_attribute_input_cache: Optional[Any] = pd.Field(None, frozen=True)

@classmethod
def model_constructor(cls, func: Callable) -> Callable:
"""
[AI-Generated] Decorator for model constructor functions.

This method wraps a constructor function to manage default argument values and cache the inputs.

Args:
func (Callable): The constructor function to wrap.

Returns:
Callable: The wrapped constructor function.
"""

@classmethod
@wraps(func)
def wrapper(cls, **kwargs):
obj = func(cls, **kwargs)
sig = inspect.signature(func)
function_arg_defaults = {
k: v.default
for k, v in sig.parameters.items()
if v.default is not inspect.Parameter.empty
}
with _model_attribute_unlock(obj, "private_attribute_input_cache"):
obj.private_attribute_input_cache = obj.private_attribute_input_cache.__class__(
# Note: obj.private_attribute_input_cache should not be included here
# Note: Because it carries over the previous cached inputs. Whatever the user choose not to specify
# Note: should be using default values rather than the previous cached inputs.
**{
**function_arg_defaults, # Defaults as the base (needed when load in UI)
**kwargs, # User specified inputs (overwrites defaults)
}
)
with _model_attribute_unlock(obj, "private_attribute_constructor"):
obj.private_attribute_constructor = func.__name__
return obj

return wrapper


##:: Utility functions for multi-constructor models


def get_class_method(cls, method_name):
"""
Retrieve a class method by its name.

Parameters
----------
cls : type
The class containing the method.
method_name : str
The name of the method as a string.

Returns
-------
method : callable
The class method corresponding to the method name.

Raises
------
AttributeError
If the method_name is not a callable method of the class.

Examples
--------
>>> class MyClass:
... @classmethod
... def my_class_method(cls):
... return "Hello from class method!"
...
>>> method = get_class_method(MyClass, "my_class_method")
>>> method()
'Hello from class method!'
"""
method = getattr(cls, method_name)
if not callable(method):
raise AttributeError(f"{method_name} is not a callable method of {cls.__name__}")
return method


def get_class_by_name(class_name, global_vars):
"""
Retrieve a class by its name from the global scope.

Parameters
----------
class_name : str
The name of the class as a string.

Returns
-------
cls : type
The class corresponding to the class name.

Raises
------
NameError
If the class_name is not found in the global scope.
TypeError
If the found object is not a class.

Examples
--------
>>> class MyClass:
... pass
...
>>> cls = get_class_by_name("MyClass")
>>> cls
<class '__main__.MyClass'>
"""
cls = global_vars.get(class_name)
if cls is None:
raise NameError(f"Class '{class_name}' not found in the global scope.")
if not isinstance(cls, type):
raise TypeError(f"'{class_name}' found in global scope, but it is not a class.")
return cls


def model_custom_constructor_parser(model_as_dict, global_vars):
"""Parse the dictionary, construct the object and return obj dict."""
constructor_name = model_as_dict.get("private_attribute_constructor", None)
if constructor_name is not None:
model_cls = get_class_by_name(model_as_dict.get("type_name"), global_vars)
input_kwargs = model_as_dict.get("private_attribute_input_cache")
if constructor_name != "default":
constructor = get_class_method(model_cls, constructor_name)
return constructor(**input_kwargs).model_dump(exclude_none=True)
return model_as_dict


def parse_model_dict(model_as_dict, global_vars) -> dict:
"""Recursively parses the model dictionary and attempts to construct the multi-constructor object."""
if isinstance(model_as_dict, dict):
for key, value in model_as_dict.items():
model_as_dict[key] = parse_model_dict(value, global_vars)
model_as_dict = model_custom_constructor_parser(model_as_dict, global_vars)
elif isinstance(model_as_dict, list):
model_as_dict = [parse_model_dict(item, global_vars) for item in model_as_dict]
return model_as_dict
2 changes: 1 addition & 1 deletion flow360/component/simulation/meshing_param/edge_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ class ProjectAnisoSpacing(Flow360BaseModel):
class _BaseEdgeRefinement(Flow360BaseModel):
entities: EntityList[Edge] = pd.Field(alias="edges")
growth_rate: Optional[float] = pd.Field(
None, description="Growth rate for volume prism layers.", ge=1
None, description="Growth rate for surface mesh layers grown from edges.", ge=1
) # Note: Per edge specification is actually not supported. This is a global setting in mesher.


Expand Down
Loading
Loading