From bf6b2b4d872558810451c5772d084927bc096320 Mon Sep 17 00:00:00 2001 From: benflexcompute Date: Thu, 20 Jun 2024 20:54:04 +0000 Subject: [PATCH 1/7] Fix simulation/framework --- .github/workflows/codestyle.yml | 2 +- .pylintrc | 2 +- .../simulation/framework/base_model.py | 36 ++++--- .../simulation/framework/cached_model_base.py | 9 +- .../simulation/framework/entity_base.py | 94 +++++++++++-------- .../simulation/framework/entity_registry.py | 29 ++++-- .../simulation/framework/expressions.py | 3 + .../framework/single_attribute_base.py | 4 + .../simulation/framework/unique_list.py | 42 +++++---- .../simulation/outputs/output_entities.py | 2 +- .../simulation/framework/test_entities_v2.py | 2 +- 11 files changed, 144 insertions(+), 81 deletions(-) diff --git a/.github/workflows/codestyle.yml b/.github/workflows/codestyle.yml index 4e5558cde..15e1b3308 100644 --- a/.github/workflows/codestyle.yml +++ b/.github/workflows/codestyle.yml @@ -58,4 +58,4 @@ jobs: - name: Install dependencies run: poetry install - name: Run pylint - run: poetry run pylint flow360 --rcfile .pylintrc + run: poetry run pylint $(git ls-files "flow360/*.py") --rcfile .pylintrc diff --git a/.pylintrc b/.pylintrc index 7c952dbc4..85a2e2f02 100644 --- a/.pylintrc +++ b/.pylintrc @@ -46,7 +46,7 @@ ignore=tests, flow360/examples # Add files or directories matching the regex patterns to the ignore-list. The # regex matches against paths and can be in Posix or Windows format. -ignore-paths=tests +ignore-paths=tests, flow360/examples, flow360/component/simulation/user_story_examples, flow360/component/results # Files or directories matching the regex patterns are skipped. The regex # matches against base names, not paths. The default value ignores Emacs file diff --git a/flow360/component/simulation/framework/base_model.py b/flow360/component/simulation/framework/base_model.py index 4e64ad5b8..aca43dc4b 100644 --- a/flow360/component/simulation/framework/base_model.py +++ b/flow360/component/simulation/framework/base_model.py @@ -1,9 +1,11 @@ +"""Flow360BaseModel class definition.""" + from __future__ import annotations import hashlib import json from copy import deepcopy -from typing import Any, List, Literal +from typing import Any, List import pydantic as pd import rich @@ -23,6 +25,23 @@ def custom_to_camel(string: str) -> str: + """ + Convert a snake_case string to camelCase. + + This function takes a snake_case string as input and converts it to camelCase. + It splits the input string by underscores, capitalizes the first letter of + each subsequent component (after the first one), and joins them together. + + Parameters: + string (str): The input string in snake_case format. + + Returns: + str: The converted string in camelCase format. + + Example: + >>> custom_to_camel("example_snake_case") + 'exampleSnakeCase' + """ components = string.split("_") camel_case_string = components[0] @@ -81,15 +100,6 @@ def __pydantic_init_subclass__(cls, **kwargs) -> None: super().__pydantic_init_subclass__(**kwargs) # Correct use of super cls._generate_docstring() - """Sets config for all :class:`Flow360BaseModel` objects. - - Custom Configuration Options - --------------------- - require_one_of: Optional[List[str]] = [] - conflicting_fields: Optional[List[Conflicts]] = [] - include_hash: bool = False - include_defaults_in_schema: bool = True - """ model_config = ConfigDict( ##:: Pydantic kwargs arbitrary_types_allowed=True, # ? @@ -117,6 +127,7 @@ def __setattr__(self, name, value): super().__setattr__(name, value) @pd.model_validator(mode="before") + @classmethod def one_of(cls, values): """ root validator for require one of @@ -429,14 +440,14 @@ def append(self, params: Flow360BaseModel, overwrite: bool = False): """ additional_config = params.model_dump(exclude_unset=True, exclude_none=True) for key, value in additional_config.items(): - if self.__getattribute__(key) and not overwrite: + if self.key and not overwrite: log.warning( f'"{key}" already exist in the original model, skipping. Use overwrite=True to overwrite values.' ) continue is_frozen = self.model_fields[key].frozen if is_frozen is None or is_frozen is False: - self.__setattr__(key, value) + setattr(self, key, value) @classmethod def _generate_docstring(cls) -> str: @@ -506,6 +517,7 @@ def _generate_docstring(cls) -> str: doc += "\n" cls.__doc__ = doc + # pylint: disable=too-many-arguments def _convert_dimensions_to_solver( self, params, diff --git a/flow360/component/simulation/framework/cached_model_base.py b/flow360/component/simulation/framework/cached_model_base.py index 2a97c9abc..dfc05a3be 100644 --- a/flow360/component/simulation/framework/cached_model_base.py +++ b/flow360/component/simulation/framework/cached_model_base.py @@ -1,3 +1,5 @@ +""" Base class for cached models. """ + import abc import inspect from functools import wraps @@ -8,7 +10,10 @@ from flow360.component.simulation.framework.base_model import Flow360BaseModel +# pylint: disable=missing-function-docstring class CachedModelBase(Flow360BaseModel, metaclass=abc.ABCMeta): + """Base class for cached models.""" + @classmethod def model_constructor(cls, func: Callable) -> Callable: @classmethod @@ -21,9 +26,11 @@ def wrapper(cls, *args, **kwargs): for k, v in sig.parameters.items() if v.default is not inspect.Parameter.empty } + # pylint: disable=protected-access result._cached = result.__annotations__["_cached"]( **{**result._cached.model_dump(), **defaults, **kwargs} ) + # pylint: disable=protected-access result._cached.constructor = func.__name__ return result @@ -35,7 +42,7 @@ def __init__(self, **data): if cached: try: self._cached = self.__annotations__["_cached"].model_validate(cached) - except: + except pd.ValidationError: pass else: defaults = {name: field.default for name, field in self.model_fields.items()} diff --git a/flow360/component/simulation/framework/entity_base.py b/flow360/component/simulation/framework/entity_base.py index 64c16fed6..bb024b0ba 100644 --- a/flow360/component/simulation/framework/entity_base.py +++ b/flow360/component/simulation/framework/entity_base.py @@ -1,3 +1,5 @@ +"""Base classes for entity types.""" + from __future__ import annotations import copy @@ -12,7 +14,7 @@ class MergeConflictError(Exception): - pass + """Raised when a merge conflict is detected.""" class EntityBase(Flow360BaseModel, metaclass=ABCMeta): @@ -24,7 +26,8 @@ class EntityBase(Flow360BaseModel, metaclass=ABCMeta): A string representing the specific type of the entity. This should be set in subclasses to differentiate between entity types. Warning: - This controls the granularity of the registry and must be unique for each entity type and it is **strongly recommended NOT** to change it as it will bring up compatability problems. + This controls the granularity of the registry and must be unique for each entity type + and it is **strongly recommended NOT** to change it as it will bring up compatability problems. name (str): The name of the entity instance, used for identification and retrieval. @@ -64,11 +67,13 @@ def copy(self, update=None, **kwargs) -> EntityBase: """ if update is None: raise ValueError( - "Change is necessary when calling .copy() as there cannot be two identical entities at the same time. Please use update parameter to change the entity attributes." + "Change is necessary when calling .copy() as there cannot be two identical entities at the same time. " + "Please use update parameter to change the entity attributes." ) if "name" not in update or update["name"] == self.name: raise ValueError( - "Copying an entity requires a new name to be specified. Please provide a new name in the update dictionary." + "Copying an entity requires a new name to be specified. " + "Please provide a new name in the update dictionary." ) return super().copy(update=update, **kwargs) @@ -82,14 +87,17 @@ def __eq__(self, other): @property def entity_bucket(self) -> str: + """returns the bucket to which the entity belongs.""" return self.private_attribute_registry_bucket_name @entity_bucket.setter def entity_bucket(self, value: str): + """disallow modification of the bucket to which the entity belongs.""" raise AttributeError("Cannot modify the bucket to which the entity belongs.") @property def entity_type(self) -> str: + """returns the entity class name.""" return self.private_attribute_entity_type_name @entity_type.setter @@ -131,7 +139,7 @@ def __combine_bools(input_data): if isinstance(input_data, bool): return input_data # If the input is a numpy ndarray, flatten it - elif isinstance(input_data, np.ndarray): + if isinstance(input_data, np.ndarray): input_data = input_data.ravel() # If the input is not a boolean or an ndarray, assume it's an iterable of booleans return all(input_data) @@ -151,15 +159,18 @@ def _merge_objects(obj_old: EntityBase, obj_new: EntityBase) -> EntityBase: "Make sure merge is intended as the names of two entities are different." ) - if obj_new._is_generic() == False and obj_old._is_generic() == True: + # pylint: disable=protected-access + if obj_new._is_generic() is False and obj_old._is_generic() is True: # swap so that obj_old is **non-generic** and obj_new is **generic** obj_new, obj_old = obj_old, obj_new # Check the two objects are mergeable - if obj_new._is_generic() == False and obj_old._is_generic() == False: + # pylint: disable=protected-access + if obj_new._is_generic() is False and obj_old._is_generic() is False: if obj_new.__class__ != obj_old.__class__: raise MergeConflictError( - f"Cannot merge objects of different class: {obj_old.__class__.__name__} and {obj_new.__class__.__name__}" + f"Cannot merge objects of different class: {obj_old.__class__.__name__} " + f"and {obj_new.__class__.__name__}" ) for attr, value in obj_new.__dict__.items(): @@ -207,8 +218,10 @@ def _remove_duplicate_entities(expanded_entities: List[EntityBase]): for name, entity_list in all_entities.items(): if len(entity_list) > 1: # step 1: find one instance that is non-generic if any + base_index = 0 for base_index, entity in enumerate(entity_list): - if entity._is_generic() == False: + # pylint: disable=protected-access + if entity._is_generic() is False: break for index, entity in enumerate(entity_list): if index == base_index: @@ -271,18 +284,19 @@ def _get_valid_entity_types(cls): raise TypeError("Internal error, the metaclass for EntityList is not properly set.") @classmethod - def _valid_individual_input(cls, input): + def _valid_individual_input(cls, input_data): """Validate each individual element in a list or as standalone entity.""" - if isinstance(input, str) or isinstance(input, EntityBase): - return input - else: - raise ValueError( - f"Type({type(input)}) of input to `entities` ({input}) is not valid. Expected str or entity instance." - ) + if isinstance(input_data, (str, EntityBase)): + return input_data + + raise ValueError( + f"Type({type(input_data)}) of input to `entities` ({input_data}) is not valid. " + "Expected str or entity instance." + ) @pd.model_validator(mode="before") @classmethod - def _format_input_to_list(cls, input: Union[dict, list]): + def _format_input_to_list(cls, input_data: Union[dict, list]): """ Flatten List[EntityBase] and put into stored_entities. """ @@ -293,12 +307,12 @@ def _format_input_to_list(cls, input: Union[dict, list]): # 3. EntityBase comes from direct specification of entity in the list. formated_input = [] valid_types = cls._get_valid_entity_types() - if isinstance(input, list): - if input == []: + if isinstance(input_data, list): + if input_data == []: raise ValueError("Invalid input type to `entities`, list is empty.") - for item in input: + for item in input_data: if isinstance(item, list): # Nested list comes from assets - [cls._valid_individual_input(individual) for individual in item] + _ = [cls._valid_individual_input(individual) for individual in item] formated_input.extend( [ individual @@ -310,16 +324,17 @@ def _format_input_to_list(cls, input: Union[dict, list]): cls._valid_individual_input(item) if isinstance(item, tuple(valid_types)): formated_input.append(item) - elif isinstance(input, dict): - return dict(stored_entities=input["stored_entities"]) + elif isinstance(input_data, dict): + return {"stored_entities": input_data["stored_entities"]} + # pylint: disable=no-else-return else: # Single reference to an entity - if input is None: - return dict(stored_entities=None) + if input_data is None: + return {"stored_entities": None} else: - cls._valid_individual_input(input) - if isinstance(input, tuple(valid_types)): - formated_input.append(input) - return dict(stored_entities=formated_input) + cls._valid_individual_input(input_data) + if isinstance(input_data, tuple(valid_types)): + formated_input.append(input_data) + return {"stored_entities": formated_input} @pd.field_validator("stored_entities", mode="after") @classmethod @@ -364,20 +379,21 @@ def _get_expanded_entities( if entities is None: return None + # pylint: disable=protected-access valid_types = self.__class__._get_valid_entity_types() expanded_entities = [] + # pylint: disable=not-an-iterable for entity in entities: if isinstance(entity, str): # Expand from supplied registry if supplied_registry is None: - if expect_supplied_registry == False: + if expect_supplied_registry is False: continue - else: - raise ValueError( - f"Internal error, registry is not supplied for entity ({entity}) expansion." - ) + raise ValueError( + f"Internal error, registry is not supplied for entity ({entity}) expansion." + ) # Expand based on naming pattern registered in the Registry pattern_matched_entities = supplied_registry.find_by_naming_pattern(entity) # Filter pattern matched entities by valid types @@ -398,12 +414,14 @@ def _get_expanded_entities( raise ValueError( f"Failed to find any matching entity with {entities}. Please check the input to entities." ) - # TODO: As suggested by Runda. We better prompt user what entities are actually used/expanded to avoid user input error. We need a switch to turn it on or off. - if create_hard_copy == True: + # pylint: disable=fixme + # TODO: As suggested by Runda. We better prompt user what entities are actually used/expanded to + # TODO: avoid user input error. We need a switch to turn it on or off. + if create_hard_copy is True: return copy.deepcopy(expanded_entities) - else: - return expanded_entities + return expanded_entities + # pylint: disable=arguments-differ def preprocess(self, supplied_registry=None, **kwargs): """ Expand and overwrite self.stored_entities in preparation for submissin/serialization. diff --git a/flow360/component/simulation/framework/entity_registry.py b/flow360/component/simulation/framework/entity_registry.py index 6d039441b..ebad28204 100644 --- a/flow360/component/simulation/framework/entity_registry.py +++ b/flow360/component/simulation/framework/entity_registry.py @@ -1,3 +1,5 @@ +"""Registry for managing and storing instances of various entity types.""" + import re from typing import Any @@ -35,9 +37,12 @@ def register(self, entity: EntityBase): Parameters: entity (EntityBase): The entity instance to register. """ + # pylint: disable=unsupported-membership-test if entity.entity_bucket not in self.internal_registry: + # pylint: disable=unsupported-assignment-operation self.internal_registry[entity.entity_bucket] = [] + # pylint: disable=unsubscriptable-object for existing_entity in self.internal_registry[entity.entity_bucket]: if existing_entity.name == entity.name: # Same type and same name. Now we try to update existing entity with new values. @@ -45,14 +50,14 @@ def register(self, entity: EntityBase): existing_entity = _merge_objects(existing_entity, entity) return except MergeConflictError as e: - log.debug("Merge conflict: %s", e) - raise ValueError( - f"Entity with name '{entity.name}' and type '{entity.entity_bucket}' already exists and have different definition." - ) + raise MergeConflictError( + f"Entity with name '{entity.name}' and type '{entity.entity_bucket}' " + "already exists and have different definition." + ) from e except Exception as e: log.debug("Merge failed unexpectly: %s", e) raise - + # pylint: disable=unsubscriptable-object self.internal_registry[entity.entity_bucket].append(entity) def get_all_entities_of_given_bucket(self, entity_bucket): @@ -65,6 +70,7 @@ def get_all_entities_of_given_bucket(self, entity_bucket): Returns: List[EntityBase]: A list of registered entities of the specified type. """ + # pylint: disable=no-member return self.internal_registry.get( entity_bucket.model_fields["private_attribute_registry_bucket_name"].default, [] ) @@ -86,6 +92,7 @@ def find_by_naming_pattern(self, pattern: str) -> list[EntityBase]: regex_pattern = f"^{pattern}$" # Exact match if no '*' regex = re.compile(regex_pattern) + # pylint: disable=no-member for entity_list in self.internal_registry.values(): matched_entities.extend(filter(lambda x: regex.match(x.name), entity_list)) return matched_entities @@ -93,7 +100,7 @@ def find_by_naming_pattern(self, pattern: str) -> list[EntityBase]: def find_by_name(self, name: str): """Retrieve the entity with the given name from the registry.""" entities = self.find_by_naming_pattern(name) - if entities == []: + if not entities: raise ValueError(f"No entity found in registry with given name: '{name}'.") if len(entities) > 1: raise ValueError(f"Multiple entities found in registry with given name: '{name}'.") @@ -105,6 +112,7 @@ def show(self): """ index = 0 print("---- Content of the registry ----") + # pylint: disable=no-member for entity_bucket, entities in self.internal_registry.items(): print(f" Entities of type '{entity_bucket}':") for entity in entities: @@ -116,13 +124,16 @@ def clear(self): """ Clears all entities from the registry. """ + # pylint: disable=no-member self.internal_registry.clear() def contains(self, entity: EntityBase) -> bool: """ Returns True if the registry contains any entities, False otherwise. """ + # pylint: disable=unsupported-membership-test if entity.entity_bucket in self.internal_registry: + # pylint: disable=unsubscriptable-object if entity in self.internal_registry[entity.entity_bucket]: return True return False @@ -130,6 +141,7 @@ def contains(self, entity: EntityBase) -> bool: def entity_count(self) -> int: """Return total number of entities in the registry.""" count = 0 + # pylint: disable=no-member for list_of_entities in self.internal_registry.values(): count += len(list_of_entities) return count @@ -142,9 +154,11 @@ def replace_existing_with(self, new_entity: EntityBase): new_entity (EntityBase): The new entity to replace the existing entity with. """ bucket_to_find = new_entity.entity_bucket + # pylint: disable=unsupported-membership-test if bucket_to_find not in self.internal_registry: return + # pylint: disable=unsubscriptable-object for entity in self.internal_registry[bucket_to_find]: if entity.name == new_entity.name: self.internal_registry[bucket_to_find].remove(entity) @@ -155,4 +169,5 @@ def replace_existing_with(self, new_entity: EntityBase): @property def is_empty(self): - return self.internal_registry == {} + """Return True if the registry is empty, False otherwise.""" + return not self.internal_registry diff --git a/flow360/component/simulation/framework/expressions.py b/flow360/component/simulation/framework/expressions.py index 8b4405c33..a3f684206 100644 --- a/flow360/component/simulation/framework/expressions.py +++ b/flow360/component/simulation/framework/expressions.py @@ -1,8 +1,11 @@ +"""String expression type for simulation framework.""" + from pydantic.functional_validators import AfterValidator from typing_extensions import Annotated from flow360.component.utils import process_expressions +# pylint: disable=fixme # TODO: Add units to expression? # TODO: Add variable existence check? StringExpression = Annotated[str, AfterValidator(process_expressions)] diff --git a/flow360/component/simulation/framework/single_attribute_base.py b/flow360/component/simulation/framework/single_attribute_base.py index 963d8afb8..fd6d6f689 100644 --- a/flow360/component/simulation/framework/single_attribute_base.py +++ b/flow360/component/simulation/framework/single_attribute_base.py @@ -1,3 +1,5 @@ +"""Single attribute base model.""" + import abc from typing import Any @@ -7,6 +9,8 @@ class SingleAttributeModel(Flow360BaseModel, metaclass=abc.ABCMeta): + """Base class for single attribute models.""" + value: Any = pd.Field() def __init__(self, value: Any): diff --git a/flow360/component/simulation/framework/unique_list.py b/flow360/component/simulation/framework/unique_list.py index add40714f..e78f94087 100644 --- a/flow360/component/simulation/framework/unique_list.py +++ b/flow360/component/simulation/framework/unique_list.py @@ -1,3 +1,5 @@ +"""Unique list classes for Simulation framework.""" + from collections import Counter from typing import Annotated, List, Union @@ -31,14 +33,16 @@ def __getitem__(cls, item_types): def _validate_unique_list(v: List) -> List: if len(v) != len(set(v)): raise ValueError( - f"Input item to this list must be unique but {[str(item) for item, count in Counter(v).items() if count > 1]} appears multiple times." + "Input item to this list must be unique " + f"but {[str(item) for item, count in Counter(v).items() if count > 1]} appears multiple times." ) return v class UniqueItemList(Flow360BaseModel, metaclass=_UniqueListMeta): """ - A list of general type items that must be unique (uniqueness is determined by the item's __eq__ and __hash__ method). + A list of general type items that must be unique + (uniqueness is determined by the item's __eq__ and __hash__ method). We will **not** try to remove duplicate items as choice is user's preference. """ @@ -46,19 +50,20 @@ class UniqueItemList(Flow360BaseModel, metaclass=_UniqueListMeta): items: Annotated[List, {"uniqueItems": True}] @pd.field_validator("items", mode="after") + @classmethod def check_unique(cls, v): """Check if the items are unique after type checking""" return _validate_unique_list(v) @pd.model_validator(mode="before") @classmethod - def _format_input_to_list(cls, input: Union[dict, list]): - if isinstance(input, list): - return dict(items=input) - elif isinstance(input, dict): - return dict(items=input["items"]) - else: # Single reference to an entity - return dict(items=[input]) + def _format_input_to_list(cls, input_data: Union[dict, list]): + if isinstance(input_data, list): + return {"items": input_data} + if isinstance(input_data, dict): + return {"items": input_data["items"]} + # Single reference to an entity + return {"items": [input_data]} def _validate_unique_aliased_item(v: List[str]) -> List[str]: @@ -78,18 +83,17 @@ class UniqueAliasedStringList(Flow360BaseModel, metaclass=_UniqueListMeta): items: Annotated[List[str], {"uniqueItems": True}] @pd.field_validator("items", mode="after") + @classmethod def deduplicate(cls, v): - # for item in v: - # if isinstance(item, str) == False: - # raise ValueError(f"Expected string for the list but got {item.__class__.__name__}.") + """Deduplicate the list by original name or aliased name.""" return _validate_unique_aliased_item(v) @pd.model_validator(mode="before") @classmethod - def _format_input_to_list(cls, input: Union[dict, list]): - if isinstance(input, list): - return dict(items=input) - elif isinstance(input, dict): - return dict(items=input["items"]) - else: # Single reference to an entity - return dict(items=[input]) + def _format_input_to_list(cls, input_data: Union[dict, list]): + if isinstance(input_data, list): + return {"items": input_data} + if isinstance(input_data, dict): + return {"items": input_data["items"]} + # Single reference to an entity + return {"items": [input_data]} diff --git a/flow360/component/simulation/outputs/output_entities.py b/flow360/component/simulation/outputs/output_entities.py index 8c01db755..7116c4520 100644 --- a/flow360/component/simulation/outputs/output_entities.py +++ b/flow360/component/simulation/outputs/output_entities.py @@ -7,7 +7,7 @@ IsoSurfaceFieldNamesFull, ) from flow360.component.simulation.framework.base_model import Flow360BaseModel -from flow360.component.simulation.framework.entity_base import EntityBase, EntityList +from flow360.component.simulation.framework.entity_base import EntityList from flow360.component.simulation.primitives import Surface from flow360.component.simulation.unit_system import LengthType from flow360.component.types import Axis diff --git a/tests/simulation/framework/test_entities_v2.py b/tests/simulation/framework/test_entities_v2.py index 1e29d4547..36a4eccb0 100644 --- a/tests/simulation/framework/test_entities_v2.py +++ b/tests/simulation/framework/test_entities_v2.py @@ -224,7 +224,7 @@ def preprocess(self): """ _supplementary_registry = _get_supplementary_registry(self.far_field_type) for model in self.models: - model.entities.preprocess(_supplementary_registry, mesh_unit=1 * u.m) + model.entities.preprocess(supplied_registry=_supplementary_registry, mesh_unit=1 * u.m) return self From 824f5019a5c9af625accbb6deee94f2e9ce43a88 Mon Sep 17 00:00:00 2001 From: benflexcompute Date: Thu, 20 Jun 2024 22:47:05 +0000 Subject: [PATCH 2/7] Fixing meshing_param --- .../simulation/meshing_param/edge_params.py | 5 +++- .../simulation/meshing_param/face_params.py | 16 ++++++------ .../simulation/meshing_param/params.py | 13 +++++++--- .../simulation/meshing_param/volume_params.py | 25 +++++++++++++------ 4 files changed, 40 insertions(+), 19 deletions(-) diff --git a/flow360/component/simulation/meshing_param/edge_params.py b/flow360/component/simulation/meshing_param/edge_params.py index bbeab52e3..b7245b910 100644 --- a/flow360/component/simulation/meshing_param/edge_params.py +++ b/flow360/component/simulation/meshing_param/edge_params.py @@ -1,4 +1,6 @@ -from typing import List, Literal, Optional, Union +"""Edge based meshing parameters for meshing.""" + +from typing import Literal, Optional, Union import pydantic as pd @@ -19,6 +21,7 @@ class HeightBasedRefinement(Flow360BaseModel): """Surface edge refinement by specifying first layer height of the anisotropic layers""" type: Literal["height"] = pd.Field("height", frozen=True) + # pylint: disable=no-member value: LengthType.Positive = pd.Field() diff --git a/flow360/component/simulation/meshing_param/face_params.py b/flow360/component/simulation/meshing_param/face_params.py index d793bbbe5..dd343eda7 100644 --- a/flow360/component/simulation/meshing_param/face_params.py +++ b/flow360/component/simulation/meshing_param/face_params.py @@ -1,3 +1,5 @@ +"""Face based meshing parameters for meshing.""" + from typing import Literal, Optional, Union import pydantic as pd @@ -7,10 +9,6 @@ from flow360.component.simulation.primitives import Surface from flow360.component.simulation.unit_system import AngleType, LengthType -""" -Meshing settings that applies to faces. -""" - class SurfaceRefinement(Flow360BaseModel): """ @@ -27,9 +25,11 @@ class SurfaceRefinement(Flow360BaseModel): refinement_type: Literal["SurfaceRefinement"] = pd.Field("SurfaceRefinement", frozen=True) entities: Optional[EntityList[Surface]] = pd.Field(None, alias="faces") + # pylint: disable=no-member max_edge_length: LengthType.Positive = pd.Field( description="Local maximum edge length for surface cells." ) + # pylint: disable=no-member curvature_resolution_angle: AngleType.Positive = pd.Field( description=""" Global maximum angular deviation in degrees. This value will restrict: @@ -46,17 +46,19 @@ class BoundaryLayer(Flow360BaseModel): - We do not support per volume specification of these settings so the entities will be **obsolete** for now. Should we have it at all in the release? - - `None` entities will be expanded (or just ignored and convert to global default, depending on implementation) before - submission. This is supposed to be applied to all the matching entities. We allow this so that we do not need to - have dedicated field for global settings. This is also consistent with the `FluidDynamics` class' design. + - `None` entities will be expanded (or just ignored and convert to global default, depending on implementation) + before submission. This is supposed to be applied to all the matching entities. We allow this so that we do not + need to have dedicated field for global settings. This is also consistent with the `FluidDynamics` class' design. """ refinement_type: Literal["BoundaryLayer"] = pd.Field("BoundaryLayer", frozen=True) type: Literal["aniso", "projectAnisoSpacing", "none"] = pd.Field() entities: Optional[EntityList[Surface]] = pd.Field(None, alias="faces") + # pylint: disable=no-member first_layer_thickness: LengthType.Positive = pd.Field( description="First layer thickness for volumetric anisotropic layers." ) + # pylint: disable=no-member growth_rate: pd.PositiveFloat = pd.Field( description="Growth rate for volume prism layers.", ge=1 ) # Note: Per face specification is actually not supported. This is a global setting in mesher. diff --git a/flow360/component/simulation/meshing_param/params.py b/flow360/component/simulation/meshing_param/params.py index da20cb3dc..0bd700267 100644 --- a/flow360/component/simulation/meshing_param/params.py +++ b/flow360/component/simulation/meshing_param/params.py @@ -1,3 +1,5 @@ +"""Meshing related parameters for volume and surface mesher.""" + from typing import Annotated, List, Literal, Optional, Union import pydantic as pd @@ -22,7 +24,8 @@ class MeshingParams(Flow360BaseModel): """ Meshing parameters for volume and/or surface mesher. - In `Simulation` this only contains what the user specifies. `Simulation` can derive and add more items according to other aspects of simulation. (E.g. BETDisk volume -> ZoneRefinement) + In `Simulation` this only contains what the user specifies. `Simulation` can derive and add more items according + to other aspects of simulation. (E.g. BETDisk volume -> ZoneRefinement) Meshing related but may and maynot (user specified) need info from `Simulation`: 1. Add rotational zones. @@ -49,13 +52,17 @@ class MeshingParams(Flow360BaseModel): ) refinement_factor: Optional[pd.PositiveFloat] = pd.Field( None, - description="If refinementFactor=r is provided all spacings in refinement regions and first layer thickness will be adjusted to generate r-times finer mesh.", + description="""If refinementFactor=r is provided all spacings in refinement regions and first layer + thickness will be adjusted to generate r-times finer mesh.""", ) gap_treatment_strength: Optional[float] = pd.Field( None, ge=0, le=1, - description="Narrow gap treatment strength used when two surfaces are in close proximity. Use a value between 0 and 1, where 0 is no treatment and 1 is the most conservative treatment. This parameter has a global impact where the anisotropic transition into the isotropic mesh. However, the impact on regions without close proximity is negligible.", + description="""Narrow gap treatment strength used when two surfaces are in close proximity. + Use a value between 0 and 1, where 0 is no treatment and 1 is the most conservative treatment. + This parameter has a global impact where the anisotropic transition into the isotropic mesh. + However the impact on regions without close proximity is negligible.""", ) surface_layer_growth_rate: Optional[float] = pd.Field( diff --git a/flow360/component/simulation/meshing_param/volume_params.py b/flow360/component/simulation/meshing_param/volume_params.py index 7cfe98462..6503757b8 100644 --- a/flow360/component/simulation/meshing_param/volume_params.py +++ b/flow360/component/simulation/meshing_param/volume_params.py @@ -1,3 +1,7 @@ +""" +Meshing settings that applies to volumes. +""" + from typing import Literal, Optional, Union import pydantic as pd @@ -7,14 +11,13 @@ from flow360.component.simulation.primitives import Box, Cylinder, Surface from flow360.component.simulation.unit_system import LengthType -""" -Meshing settings that applies to volumes. -""" - class UniformRefinement(Flow360BaseModel): + """Uniform spacing refinement.""" + refinement_type: Literal["UniformRefinement"] = pd.Field("UniformRefinement", frozen=True) entities: EntityList[Box, Cylinder] = pd.Field() + # pylint: disable=no-member spacing: LengthType.Positive = pd.Field() @@ -23,15 +26,19 @@ class AxisymmetricRefinement(Flow360BaseModel): Note: - This basically creates the "rotorDisks" type of volume refinement that we used to have. - - `enclosed_objects` is actually just a way of specifying the enclosing patches of a volume zone. Therefore in the future when supporting arbitrary-axisymmetric shaped sliding interface, we may not need this attribute at all. For example if the new class already has an entry to list all the enclosing patches. + - `enclosed_objects` is actually just a way of specifying the enclosing patches of a volume zone. + Therefore in the future when supporting arbitrary-axisymmetric shaped sliding interface, we may not need this + attribute at all. For example if the new class already has an entry to list all the enclosing patches. - - We may provide a helper function to automatically determine what is inside the encloeud_objects list based on the mesh data. But this currently is out of scope due to the estimated efforts. + - We may provide a helper function to automatically determine what is inside the encloeud_objects list based on + the mesh data. But this currently is out of scope due to the estimated efforts. """ refinement_type: Literal["AxisymmetricRefinement"] = pd.Field( "AxisymmetricRefinement", frozen=True ) entities: EntityList[Cylinder] = pd.Field() + # pylint: disable=no-member spacing_axial: LengthType.Positive = pd.Field() spacing_radial: LengthType.Positive = pd.Field() spacing_circumferential: LengthType.Positive = pd.Field() @@ -41,12 +48,14 @@ class RotationCylinder(AxisymmetricRefinement): """This is the original SlidingInterface. This will create new volume zones Will add RotationSphere class in the future. Please refer to - https://www.notion.so/flexcompute/Python-model-design-document-78d442233fa944e6af8eed4de9541bb1?pvs=4#c2de0b822b844a12aa2c00349d1f68a3 + https://www.notion.so/flexcompute/Python-model-design-document- + 78d442233fa944e6af8eed4de9541bb1?pvs=4#c2de0b822b844a12aa2c00349d1f68a3 """ enclosed_objects: Optional[EntityList[Cylinder, Surface]] = pd.Field( None, - description="Entities enclosed by this sliding interface. Can be faces, boxes and/or other cylinders etc. This helps determining the volume zone boundary.", + description="""Entities enclosed by this sliding interface. Can be faces, boxes and/or other cylinders etc. + This helps determining the volume zone boundary.""", ) From 35da225d8719a072f8379f84706c83412c195691 Mon Sep 17 00:00:00 2001 From: benflexcompute Date: Thu, 20 Jun 2024 22:55:15 +0000 Subject: [PATCH 3/7] Fix outputs --- .../simulation/outputs/output_entities.py | 15 +++++- .../component/simulation/outputs/outputs.py | 54 +++++++++++++------ 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/flow360/component/simulation/outputs/output_entities.py b/flow360/component/simulation/outputs/output_entities.py index 7116c4520..b04921c9c 100644 --- a/flow360/component/simulation/outputs/output_entities.py +++ b/flow360/component/simulation/outputs/output_entities.py @@ -1,4 +1,6 @@ -from typing import List, Literal, Tuple, final +"""Output for simulation.""" + +from typing import List, Literal import pydantic as pd @@ -31,19 +33,30 @@ def __str__(self): class Slice(_OutputItemBase): + """Slice output item.""" + slice_normal: Axis = pd.Field() + # pylint: disable=no-member slice_origin: LengthType.Point = pd.Field() class Isosurface(_OutputItemBase): + """Isosurface output item.""" + field: Literal[IsoSurfaceFieldNames, IsoSurfaceFieldNamesFull] = pd.Field() + # pylint: disable=fixme # TODO: Maybe we need some unit helper function to help user figure out what is the value to use here? iso_value: float = pd.Field(description="Expect non-dimensional value.") class SurfaceList(_OutputItemBase): + """List of surfaces for integrals.""" + entities: EntityList[Surface] = pd.Field(alias="surfaces") class Probe(_OutputItemBase): + """Probe monitor""" + + # pylint: disable=no-member locations: List[LengthType.Point] = pd.Field() diff --git a/flow360/component/simulation/outputs/outputs.py b/flow360/component/simulation/outputs/outputs.py index 0926ec7ad..caba37c48 100644 --- a/flow360/component/simulation/outputs/outputs.py +++ b/flow360/component/simulation/outputs/outputs.py @@ -1,3 +1,10 @@ +"""Mostly the same as Flow360Param counterparts. +Caveats: +1. Check if we support non-average and average output specified at the same time in solver. +(Yes but they share the same output_fields) +2. We do not support mulitple output frequencies/file format for the same type of output. +""" + from typing import Annotated, List, Literal, Optional, Union import pydantic as pd @@ -23,12 +30,6 @@ from flow360.component.simulation.primitives import Surface from flow360.component.simulation.unit_system import LengthType -"""Mostly the same as Flow360Param counterparts. -Caveats: -1. Check if we support non-average and average output specified at the same time in solver. (Yes but they share the same output_fields) -2. We do not support mulitple output frequencies/file format for the same type of output. -""" - class _AnimationSettings(Flow360BaseModel): """ @@ -38,12 +39,14 @@ class _AnimationSettings(Flow360BaseModel): frequency: int = pd.Field( default=-1, ge=-1, - description="Frequency (in number of physical time steps) at which output is saved. -1 is at end of simulation.", + description="""Frequency (in number of physical time steps) at which output is saved. + -1 is at end of simulation.""", ) frequency_offset: int = pd.Field( default=0, ge=0, - description="Offset (in number of physical time steps) at which output animation is started. 0 is at beginning of simulation.", + description="""Offset (in number of physical time steps) at which output animation is started. + 0 is at beginning of simulation.""", ) @@ -56,11 +59,17 @@ class _AnimationAndFileFormatSettings(_AnimationSettings): class SurfaceOutput(_AnimationAndFileFormatSettings): + """Surface output settings.""" + + # pylint: disable=fixme # TODO: entities is None --> use all surfaces. This is not implemented yet. entities: Optional[EntityList[Surface]] = pd.Field(None, alias="surfaces") write_single_file: bool = pd.Field( default=False, - description="Enable writing all surface outputs into a single file instead of one file per surface. This option currently only supports Tecplot output format. Will choose the value of the last instance of this option of the same output type (SurfaceOutput or TimeAverageSurfaceOutput) in the `output` list.", + description="""Enable writing all surface outputs into a single file instead of one file per surface. + This option currently only supports Tecplot output format. + Will choose the value of the last instance of this option of the same output type + (SurfaceOutput or TimeAverageSurfaceOutput) in the `output` list.""", ) output_fields: UniqueAliasedStringList[SurfaceFieldNames] = pd.Field() output_type: Literal["SurfaceOutput"] = pd.Field("SurfaceOutput", frozen=True) @@ -69,11 +78,13 @@ class SurfaceOutput(_AnimationAndFileFormatSettings): class TimeAverageSurfaceOutput(SurfaceOutput): """ Caveats: - Solver side only accept exactly the same set of output_fields (is shared) between VolumeOutput and TimeAverageVolumeOutput. + Solver side only accept exactly the same set of output_fields (is shared) between + VolumeOutput and TimeAverageVolumeOutput. Notes ----- - Old `computeTimeAverages` can be infered when user is explicitly using for example `TimeAverageSurfaceOutput`. + Old `computeTimeAverages` can be infered when user is explicitly using for + example `TimeAverageSurfaceOutput`. """ start_step: Union[pd.NonNegativeInt, Literal[-1]] = pd.Field( @@ -85,6 +96,8 @@ class TimeAverageSurfaceOutput(SurfaceOutput): class VolumeOutput(_AnimationAndFileFormatSettings): + """Volume output settings.""" + output_fields: UniqueAliasedStringList[VolumeFieldNames] = pd.Field() output_type: Literal["VolumeOutput"] = pd.Field("VolumeOutput", frozen=True) @@ -92,12 +105,14 @@ class VolumeOutput(_AnimationAndFileFormatSettings): class TimeAverageVolumeOutput(VolumeOutput): """ Caveats: - Solver side only accept exactly the same set of output_fields (is shared) between VolumeOutput and TimeAverageVolumeOutput. + Solver side only accept exactly the same set of output_fields (is shared) + between VolumeOutput and TimeAverageVolumeOutput. Also let's not worry about allowing entities here as it is not supported by solver anyway. Notes ----- - Old `computeTimeAverages` can be infered when user is explicitly using for example `TimeAverageSurfaceOutput`. + Old `computeTimeAverages` can be infered when user is explicitly using for example + `TimeAverageSurfaceOutput`. """ start_step: Union[pd.NonNegativeInt, Literal[-1]] = pd.Field( @@ -109,31 +124,42 @@ class TimeAverageVolumeOutput(VolumeOutput): class SliceOutput(_AnimationAndFileFormatSettings): + """Slice output settings.""" + entities: UniqueItemList[Slice] = pd.Field(alias="slices") output_fields: UniqueAliasedStringList[SliceFieldNames] = pd.Field() output_type: Literal["SliceOutput"] = pd.Field("SliceOutput", frozen=True) class IsosurfaceOutput(_AnimationAndFileFormatSettings): + """Isosurface output settings.""" + entities: UniqueItemList[Isosurface] = pd.Field(alias="isosurfaces") output_fields: UniqueAliasedStringList[CommonFieldNames] = pd.Field() output_type: Literal["IsosurfaceOutput"] = pd.Field("IsosurfaceOutput", frozen=True) class SurfaceIntegralOutput(_AnimationSettings): + """Surface integral output settings.""" + entities: UniqueItemList[SurfaceList] = pd.Field(alias="monitors") output_fields: UniqueAliasedStringList[CommonFieldNames] = pd.Field() output_type: Literal["SurfaceIntegralOutput"] = pd.Field("SurfaceIntegralOutput", frozen=True) class ProbeOutput(_AnimationSettings): + """Probe monitor output settings.""" + entities: UniqueItemList[Probe] = pd.Field(alias="probes") output_fields: UniqueAliasedStringList[CommonFieldNames] = pd.Field() output_type: Literal["ProbeOutput"] = pd.Field("ProbeOutput", frozen=True) class AeroAcousticOutput(Flow360BaseModel): + """AeroAcoustic output settings.""" + patch_type: str = pd.Field("solid", frozen=True) + # pylint: disable=no-member observers: List[LengthType.Point] = pd.Field() write_per_surface_output: bool = pd.Field(False) output_type: Literal["AeroAcousticOutput"] = pd.Field("AeroAcousticOutput", frozen=True) @@ -142,8 +168,6 @@ class AeroAcousticOutput(Flow360BaseModel): class UserDefinedFields(Flow360BaseModel): """Ignore this for now""" - pass - OutputTypes = Annotated[ Union[ From 4bc1fa333aedb18fde7af9efa5048ec7e5f0473f Mon Sep 17 00:00:00 2001 From: benflexcompute Date: Thu, 20 Jun 2024 22:58:54 +0000 Subject: [PATCH 4/7] Fix time stepping --- .../simulation/time_stepping/time_stepping.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/flow360/component/simulation/time_stepping/time_stepping.py b/flow360/component/simulation/time_stepping/time_stepping.py index 52c8dc85a..e01fffd2d 100644 --- a/flow360/component/simulation/time_stepping/time_stepping.py +++ b/flow360/component/simulation/time_stepping/time_stepping.py @@ -1,3 +1,5 @@ +"""Time stepping setting for simulation""" + from abc import ABCMeta from typing import Literal, Optional, Union @@ -72,14 +74,6 @@ class BaseTimeStepping(Flow360BaseModel, metaclass=ABCMeta): order_of_accuracy: Literal[1, 2] = pd.Field(2) - # TODO: - # # pylint: disable=arguments-differ - # def to_solver(self, params, **kwargs) -> BaseTimeStepping: - # """ - # returns configuration object in flow360 units system - # """ - # return super().to_solver(params, **kwargs) - class Steady(BaseTimeStepping): """ @@ -88,6 +82,7 @@ class Steady(BaseTimeStepping): type_name: Literal["Steady"] = pd.Field("Steady", frozen=True) max_steps: int = pd.Field(2000, gt=0, le=100000, description="Maximum number of pseudo steps.") + # pylint: disable=duplicate-code CFL: Union[RampCFL, AdaptiveCFL] = pd.Field( default=AdaptiveCFL.default_steady(), ) @@ -118,7 +113,9 @@ class Unsteady(BaseTimeStepping): 100, gt=0, le=100000, description="Maximum pseudo steps within one physical step." ) steps: pd.PositiveInt = pd.Field(description="Number of physical steps.") + # pylint: disable=no-member step_size: TimeType.Positive = pd.Field(description="Time step size in physical step marching,") + # pylint: disable=duplicate-code CFL: Union[RampCFL, AdaptiveCFL] = pd.Field( default=AdaptiveCFL.default_unsteady(), ) From b7e4c7f731c475651261b5c84ffd6cf04ef856d0 Mon Sep 17 00:00:00 2001 From: benflexcompute Date: Thu, 20 Jun 2024 23:21:27 +0000 Subject: [PATCH 5/7] Fix translators --- .../translator/solver_translator.py | 37 ++++++++------ .../translator/surface_meshing_translator.py | 6 +++ .../component/simulation/translator/utils.py | 48 +++++++++++++++---- .../translator/volume_meshing_translator.py | 8 +++- 4 files changed, 75 insertions(+), 24 deletions(-) diff --git a/flow360/component/simulation/translator/solver_translator.py b/flow360/component/simulation/translator/solver_translator.py index c83f5b06d..b733a3b51 100644 --- a/flow360/component/simulation/translator/solver_translator.py +++ b/flow360/component/simulation/translator/solver_translator.py @@ -1,3 +1,5 @@ +"""Flow360 solver setting parameter translator.""" + from copy import deepcopy from typing import Union @@ -7,11 +9,7 @@ SymmetryPlane, Wall, ) -from flow360.component.simulation.models.volume_models import ( - ActuatorDisk, - BETDisk, - Fluid, -) +from flow360.component.simulation.models.volume_models import BETDisk, Fluid from flow360.component.simulation.outputs.outputs import ( SliceOutput, SurfaceOutput, @@ -33,15 +31,19 @@ def dump_dict(input_params): + """Dumping param/model to dictionary.""" return input_params.model_dump(by_alias=True, exclude_none=True) def remove_empty_keys(input_dict): + """I do not know what this is for --- Ben""" + # pylint: disable=fixme # TODO: implement return input_dict def init_output_attr_dict(obj_list, class_type): + """Initialize the common output attribute.""" return { "animationFrequency": get_attribute_from_first_instance(obj_list, class_type, "frequency"), "animationFrequencyOffset": get_attribute_from_first_instance( @@ -51,14 +53,19 @@ def init_output_attr_dict(obj_list, class_type): } +# pylint: disable=too-many-statements +# pylint: disable=too-many-branches +# pylint: disable=too-many-locals @preprocess_input def get_solver_json( input_params: Union[str, dict, SimulationParams], + # pylint: disable=no-member mesh_unit: LengthType.Positive, ): """ Get the solver json from the simulation parameters. """ + translated = {} ##:: Step 1: Get geometry: geometry = remove_units_in_dict(dump_dict(input_params.reference_geometry)) @@ -84,16 +91,17 @@ def get_solver_json( translated["boundaries"] = {} for model in input_params.models: if isinstance(model, (Freestream, SlipWall, SymmetryPlane, Wall)): - for surface in model.entities.stored_entities: - spec = dump_dict(model) - spec.pop("surfaces") + spec = dump_dict(model) + spec.pop("surfaces") if isinstance(model, Wall): spec.pop("useWallFunction") spec["type"] = "WallFunction" if model.use_wall_function else "NoSlipWall" if model.heat_spec: spec.pop("heat_spec") + # pylint: disable=fixme # TODO: implement - translated["boundaries"][surface.name] = spec + for surface in model.entities.stored_entities: + translated["boundaries"][surface.name] = spec ##:: Step 4: Get outputs outputs = input_params.outputs @@ -138,6 +146,7 @@ def get_solver_json( for output in input_params.outputs: # validation: no more than one VolumeOutput, Slice and Surface cannot have difference format etc. if isinstance(output, TimeAverageVolumeOutput): + # pylint: disable=fixme # TODO: update time average entries translated["volumeOutput"]["computeTimeAverages"] = True @@ -152,14 +161,14 @@ def get_solver_json( } elif isinstance(output, SliceOutput): slices = translated["sliceOutput"]["slices"] - for slice in output.entities.items: - slices[slice.name] = { + for slice_item in output.entities.items: + slices[slice_item.name] = { "outputFields": merge_unique_item_lists( - slices.get(slice.name, {}).get("outputFields", []), + slices.get(slice_item.name, {}).get("outputFields", []), output.output_fields.model_dump()["items"], ), - "sliceOrigin": list(remove_units_in_dict(dump_dict(slice))["sliceOrigin"]), - "sliceNormal": list(remove_units_in_dict(dump_dict(slice))["sliceNormal"]), + "sliceOrigin": list(remove_units_in_dict(dump_dict(slice_item))["sliceOrigin"]), + "sliceNormal": list(remove_units_in_dict(dump_dict(slice_item))["sliceNormal"]), } ##:: Step 5: Get timeStepping diff --git a/flow360/component/simulation/translator/surface_meshing_translator.py b/flow360/component/simulation/translator/surface_meshing_translator.py index 2f80315d0..8e93d3857 100644 --- a/flow360/component/simulation/translator/surface_meshing_translator.py +++ b/flow360/component/simulation/translator/surface_meshing_translator.py @@ -1,3 +1,5 @@ +"""Surface meshing parameter translator.""" + from flow360.component.simulation.meshing_param.edge_params import SurfaceEdgeRefinement from flow360.component.simulation.meshing_param.face_params import SurfaceRefinement from flow360.component.simulation.simulation_params import SimulationParams @@ -8,6 +10,7 @@ ) +# pylint: disable=invalid-name def SurfaceEdgeRefinement_to_edges(obj: SurfaceEdgeRefinement): """ Translate SurfaceEdgeRefinement to edges. @@ -17,8 +20,10 @@ def SurfaceEdgeRefinement_to_edges(obj: SurfaceEdgeRefinement): return {"type": "aniso", "method": "height", "value": obj.method.value.value.item()} if obj.method.type == "projectAnisoSpacing": return {"type": "projectAnisoSpacing"} + return None +# pylint: disable=invalid-name def SurfaceRefinement_to_faces(obj: SurfaceRefinement): """ Translate SurfaceRefinement to faces. @@ -30,6 +35,7 @@ def SurfaceRefinement_to_faces(obj: SurfaceRefinement): @preprocess_input +# pylint: disable=unused-argument def get_surface_meshing_json(input_params: SimulationParams, mesh_units): """ Get JSON for surface meshing. diff --git a/flow360/component/simulation/translator/utils.py b/flow360/component/simulation/translator/utils.py index f606e2277..75effdb37 100644 --- a/flow360/component/simulation/translator/utils.py +++ b/flow360/component/simulation/translator/utils.py @@ -1,3 +1,5 @@ +"""Utilities for the translator module.""" + from __future__ import annotations import functools @@ -11,8 +13,11 @@ def preprocess_input(func): + """Call param preprocess() method before calling the translator.""" + @functools.wraps(func) def wrapper(input_params, mesh_unit, *args, **kwargs): + # pylint: disable=no-member validated_mesh_unit = LengthType.validate(mesh_unit) processed_input = get_simulation_param_dict(input_params, validated_mesh_unit) return func(processed_input, validated_mesh_unit, *args, **kwargs) @@ -36,7 +41,7 @@ def get_simulation_param_dict( param_dict = json.loads(input_params) except json.JSONDecodeError: # If input is a file path - with open(input_params, "r") as file: + with open(input_params, "r", encoding="utf-8") as file: param_dict = json.load(file) if param_dict is None: raise ValueError(f"Invalid input <{input_params}> for translator. ") @@ -50,27 +55,53 @@ def get_simulation_param_dict( def replace_dict_key(input_dict: dict, key_to_replace: str, replacement_key: str): + """Replace a key in a dictionary.""" if key_to_replace in input_dict: input_dict[replacement_key] = input_dict.pop(key_to_replace) def replace_dict_value(input_dict: dict, key: str, value_to_replace, replacement_value): + """Replace a value in a dictionary.""" if key in input_dict and input_dict[key] == value_to_replace: input_dict[key] = replacement_value def convert_tuples_to_lists(input_dict): + """ + Recursively convert all tuples to lists in a nested dictionary. + + This function traverses through all the elements in the input dictionary and + converts any tuples it encounters into lists. It also handles nested dictionaries + and lists by applying the conversion recursively. + + Args: + input_dict (dict): The input dictionary that may contain tuples. + + Returns: + dict: A new dictionary with all tuples converted to lists. + + Examples: + >>> input_dict = { + ... 'a': (1, 2, 3), + ... 'b': { + ... 'c': (4, 5), + ... 'd': [6, (7, 8)] + ... } + ... } + >>> convert_tuples_to_lists(input_dict) + {'a': [1, 2, 3], 'b': {'c': [4, 5], 'd': [6, [7, 8]]}} + """ if isinstance(input_dict, dict): return {k: convert_tuples_to_lists(v) for k, v in input_dict.items()} - elif isinstance(input_dict, tuple): + if isinstance(input_dict, tuple): return list(input_dict) - elif isinstance(input_dict, list): + if isinstance(input_dict, list): return [convert_tuples_to_lists(item) for item in input_dict] - else: - return input_dict + return input_dict def remove_units_in_dict(input_dict): + """Remove units from a dimensioned value.""" unit_keys = {"value", "units"} if isinstance(input_dict, dict): new_dict = {} @@ -83,13 +114,13 @@ def remove_units_in_dict(input_dict): else: new_dict[key] = remove_units_in_dict(value) return new_dict - elif isinstance(input_dict, list): + if isinstance(input_dict, list): return [remove_units_in_dict(item) for item in input_dict] - else: - return input_dict + return input_dict def has_instance_in_list(obj_list: list, class_type): + """Check if a list contains an instance of a given type.""" for obj in obj_list: if isinstance(obj, class_type): return True @@ -149,5 +180,6 @@ def translate_setting_and_apply_to_all_entities( def merge_unique_item_lists(list1: list[str], list2: list[str]) -> list: + """Merge two lists and remove duplicates.""" combined = list1 + list2 return list(OrderedDict.fromkeys(combined)) diff --git a/flow360/component/simulation/translator/volume_meshing_translator.py b/flow360/component/simulation/translator/volume_meshing_translator.py index 853f5cad3..46676b6f6 100644 --- a/flow360/component/simulation/translator/volume_meshing_translator.py +++ b/flow360/component/simulation/translator/volume_meshing_translator.py @@ -1,3 +1,5 @@ +"""Volume meshing parameter translator.""" + from flow360.component.simulation.meshing_param.edge_params import SurfaceEdgeRefinement from flow360.component.simulation.meshing_param.face_params import BoundaryLayer from flow360.component.simulation.meshing_param.volume_params import UniformRefinement @@ -18,7 +20,7 @@ def unifrom_refinement_translator(obj: SurfaceEdgeRefinement): return {"spacing": obj.spacing.value.item()} -def entitity_info_seralizer(entity_obj): +def _entitity_info_seralizer(entity_obj): if isinstance(entity_obj, Cylinder): return { "type": "cylinder", @@ -27,9 +29,11 @@ def entitity_info_seralizer(entity_obj): "axis": list(entity_obj.axis), "center": list(entity_obj.center.value), } + return {} @preprocess_input +# pylint: disable=unused-argument def get_volume_meshing_json(input_params: SimulationParams, mesh_units): """ Get JSON for surface meshing. @@ -46,7 +50,7 @@ def get_volume_meshing_json(input_params: SimulationParams, mesh_units): UniformRefinement, unifrom_refinement_translator, to_list=True, - entity_injection_func=entitity_info_seralizer, + entity_injection_func=_entitity_info_seralizer, ) # >> Step 3: Get volumetric global settings From 593c9d109ac4669207fefc980b654ae49723874b Mon Sep 17 00:00:00 2001 From: benflexcompute Date: Thu, 20 Jun 2024 23:22:51 +0000 Subject: [PATCH 6/7] Fix user_defined_dynamics --- .../simulation/user_defined_dynamics/user_defined_dynamics.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/flow360/component/simulation/user_defined_dynamics/user_defined_dynamics.py b/flow360/component/simulation/user_defined_dynamics/user_defined_dynamics.py index a08d27452..7d4187d27 100644 --- a/flow360/component/simulation/user_defined_dynamics/user_defined_dynamics.py +++ b/flow360/component/simulation/user_defined_dynamics/user_defined_dynamics.py @@ -1,4 +1,6 @@ -from typing import Dict, List, Optional, Union +"""User defined dynamic model for SimulationParams""" + +from typing import Dict, List, Optional import pydantic as pd From 4e1ae98411f48fb4a4880cc433ebbdda5328ee64 Mon Sep 17 00:00:00 2001 From: Yifan Bai Date: Fri, 21 Jun 2024 02:19:53 +0000 Subject: [PATCH 7/7] Fix pylint --- .../simulation/framework/base_model.py | 1 + .../component/simulation/models/material.py | 25 +++++++++-- .../simulation/models/solver_numerics.py | 4 +- .../simulation/models/surface_models.py | 42 ++++++++++++++++++- .../models/turbulence_quantities.py | 5 +++ .../simulation/models/volume_models.py | 12 +++++- 6 files changed, 80 insertions(+), 9 deletions(-) diff --git a/flow360/component/simulation/framework/base_model.py b/flow360/component/simulation/framework/base_model.py index aca43dc4b..b03edb9da 100644 --- a/flow360/component/simulation/framework/base_model.py +++ b/flow360/component/simulation/framework/base_model.py @@ -144,6 +144,7 @@ def one_of(cls, values): return values # pylint: disable=no-self-argument + # pylint: disable=duplicate-code @pd.model_validator(mode="before") def handle_conflicting_fields(cls, values): """ diff --git a/flow360/component/simulation/models/material.py b/flow360/component/simulation/models/material.py index f9e985d91..fd15f54ed 100644 --- a/flow360/component/simulation/models/material.py +++ b/flow360/component/simulation/models/material.py @@ -1,3 +1,5 @@ +"""Material classes for the simulation framework.""" + from typing import Literal, Optional, Union import pydantic as pd @@ -16,14 +18,21 @@ class MaterialBase(Flow360BaseModel): - # Basic properties required to define a material. - # For example: young's modulus, viscosity as an expression of temperature, etc. - ... + """ + Basic properties required to define a material. + For example: young's modulus, viscosity as an expression of temperature, etc. + """ + type: str = pd.Field() name: str = pd.Field() class Sutherland(Flow360BaseModel): + """ + Sutherland's law + """ + + # pylint: disable=no-member reference_viscosity: ViscosityType.Positive = pd.Field() reference_temperature: TemperatureType.Positive = pd.Field() effective_temperature: TemperatureType.Positive = pd.Field() @@ -32,6 +41,7 @@ class Sutherland(Flow360BaseModel): def dynamic_viscosity_from_temperature( self, temperature: TemperatureType.Positive ) -> ViscosityType.Positive: + """dynamic viscosity""" return ( self.reference_viscosity * pow(temperature / self.reference_temperature, 1.5) @@ -40,7 +50,12 @@ def dynamic_viscosity_from_temperature( ) +# pylint: disable=no-member, missing-function-docstring class Air(MaterialBase): + """ + Material properties for Air + """ + type: Literal["air"] = pd.Field("air", frozen=True) name: str = pd.Field("air") dynamic_viscosity: Union[Sutherland, ViscosityType.Positive] = pd.Field( @@ -81,6 +96,10 @@ def dynamic_viscosity_from_temperature( class SolidMaterial(MaterialBase): + """ + Solid material base + """ + type: Literal["solid"] = pd.Field("solid", frozen=True) name: str = pd.Field(frozen=True) thermal_conductivity: ThermalConductivityType.Positive = pd.Field(frozen=True) diff --git a/flow360/component/simulation/models/solver_numerics.py b/flow360/component/simulation/models/solver_numerics.py index 097d743d7..36556f477 100644 --- a/flow360/component/simulation/models/solver_numerics.py +++ b/flow360/component/simulation/models/solver_numerics.py @@ -1,5 +1,6 @@ """ -Contains basic components(solvers) that composes the `volume` type models. Each volume model represents a physical phenomena that require a combination of solver features to model. +Contains basic components(solvers) that composes the `volume` type models. +Each volume model represents a physical phenomena that require a combination of solver features to model. E.g. NavierStokes, turbulence and transition composes FluidDynamics `volume` type @@ -11,7 +12,6 @@ from abc import ABCMeta from typing import Literal, Optional, Union -import numpy as np import pydantic as pd from pydantic import NonNegativeFloat, NonNegativeInt, PositiveFloat, PositiveInt diff --git a/flow360/component/simulation/models/surface_models.py b/flow360/component/simulation/models/surface_models.py index e1bab63f9..a5482129a 100644 --- a/flow360/component/simulation/models/surface_models.py +++ b/flow360/component/simulation/models/surface_models.py @@ -13,6 +13,9 @@ SingleAttributeModel, ) from flow360.component.simulation.framework.unique_list import UniqueItemList +from flow360.component.simulation.models.turbulence_quantities import ( + TurbulenceQuantitiesType, +) from flow360.component.simulation.operating_condition import VelocityVectorType from flow360.component.simulation.primitives import Surface, SurfacePair from flow360.component.simulation.unit_system import ( @@ -22,42 +25,61 @@ TemperatureType, ) +# pylint: disable=fixme # TODO: Warning: Pydantic V1 import from flow360.component.types import Axis -from .turbulence_quantities import TurbulenceQuantitiesType - class BoundaryBase(Flow360BaseModel, metaclass=ABCMeta): + """Boundary base""" + type: str = pd.Field() entities: EntityList[Surface] = pd.Field(alias="surfaces") class BoundaryBaseWithTurbulenceQuantities(BoundaryBase, metaclass=ABCMeta): + """Boundary base with turbulence quantities""" + turbulence_quantities: Optional[TurbulenceQuantitiesType] = pd.Field(None) class HeatFlux(SingleAttributeModel): + """Heat flux""" + value: Union[HeatFluxType, pd.StrictStr] = pd.Field() class Temperature(SingleAttributeModel): + """Temperature""" + + # pylint: disable=no-member value: Union[TemperatureType.Positive, pd.StrictStr] = pd.Field() class TotalPressure(SingleAttributeModel): + """Total pressure""" + + # pylint: disable=no-member value: PressureType.Positive = pd.Field() class Pressure(SingleAttributeModel): + """Pressure""" + + # pylint: disable=no-member value: PressureType.Positive = pd.Field() class MassFlowRate(SingleAttributeModel): + """Mass flow rate""" + + # pylint: disable=no-member value: MassFlowRateType.NonNegative = pd.Field() class Mach(SingleAttributeModel): + """Mach""" + value: pd.NonNegativeFloat = pd.Field() @@ -79,6 +101,8 @@ class Wall(BoundaryBase): class Freestream(BoundaryBaseWithTurbulenceQuantities): + """Freestream""" + type: Literal["Freestream"] = pd.Field("Freestream", frozen=True) velocity: Optional[VelocityVectorType] = pd.Field(None) velocity_type: Literal["absolute", "relative"] = pd.Field("relative") @@ -102,24 +126,33 @@ class Inflow(BoundaryBaseWithTurbulenceQuantities): """ type: Literal["Inflow"] = pd.Field("Inflow", frozen=True) + # pylint: disable=no-member total_temperature: TemperatureType.Positive = pd.Field() velocity_direction: Optional[Axis] = pd.Field(None) spec: Union[TotalPressure, MassFlowRate] = pd.Field() class SlipWall(BoundaryBase): + """Slip wall""" + type: Literal["SlipWall"] = pd.Field("SlipWall", frozen=True) class SymmetryPlane(BoundaryBase): + """Symmetry plane""" + type: Literal["SymmetryPlane"] = pd.Field("SymmetryPlane", frozen=True) class Translational(Flow360BaseModel): + """Translational""" + type: Literal["Translational"] = pd.Field("Translational", frozen=True) class Rotational(Flow360BaseModel): + """Rotational""" + type: Literal["Rotational"] = pd.Field("Rotational", frozen=True) # pylint: disable=fixme # TODO: Maybe we need more precision when serializeing this one? @@ -127,6 +160,11 @@ class Rotational(Flow360BaseModel): class Periodic(Flow360BaseModel): + """Replace Flow360Param: + - TranslationallyPeriodic + - RotationallyPeriodic + """ + type: Literal["Periodic"] = pd.Field("Periodic", frozen=True) entity_pairs: UniqueItemList[SurfacePair] = pd.Field(alias="surface_pairs") spec: Union[Translational, Rotational] = pd.Field(discriminator="type") diff --git a/flow360/component/simulation/models/turbulence_quantities.py b/flow360/component/simulation/models/turbulence_quantities.py index 1263dc729..e84f3650f 100644 --- a/flow360/component/simulation/models/turbulence_quantities.py +++ b/flow360/component/simulation/models/turbulence_quantities.py @@ -24,6 +24,7 @@ class TurbulentKineticEnergy(Flow360BaseModel): """ type_name: Literal["TurbulentKineticEnergy"] = pd.Field("TurbulentKineticEnergy", frozen=True) + # pylint: disable=no-member turbulent_kinetic_energy: SpecificEnergyType.NonNegative = pd.Field() @@ -47,6 +48,7 @@ class _SpecificDissipationRate(Flow360BaseModel, metaclass=ABCMeta): """ type_name: Literal["SpecificDissipationRate"] = pd.Field("SpecificDissipationRate", frozen=True) + # pylint: disable=no-member specific_dissipation_rate: FrequencyType.NonNegative = pd.Field() @@ -71,6 +73,7 @@ class TurbulentLengthScale(Flow360BaseModel, metaclass=ABCMeta): """ type_name: Literal["TurbulentLengthScale"] = pd.Field("TurbulentLengthScale", frozen=True) + # pylint: disable=no-member turbulent_length_scale: LengthType.Positive = pd.Field() @@ -96,6 +99,7 @@ class ModifiedTurbulentViscosity(Flow360BaseModel): type_name: Literal["ModifiedTurbulentViscosity"] = pd.Field( "ModifiedTurbulentViscosity", frozen=True ) + # pylint: disable=no-member modified_turbulent_viscosity: Optional[ViscosityType.Positive] = pd.Field() @@ -163,6 +167,7 @@ class TurbulentViscosityRatioAndTurbulentLengthScale(TurbulentViscosityRatio, Tu # pylint: enable=missing-class-docstring +# pylint: disable=duplicate-code TurbulenceQuantitiesType = Union[ TurbulentViscosityRatio, diff --git a/flow360/component/simulation/models/volume_models.py b/flow360/component/simulation/models/volume_models.py index 14018f33c..b72e5fc76 100644 --- a/flow360/component/simulation/models/volume_models.py +++ b/flow360/component/simulation/models/volume_models.py @@ -1,3 +1,5 @@ +"""Volume models for the simulation framework.""" + from typing import Dict, List, Literal, Optional, Union import pydantic as pd @@ -30,10 +32,12 @@ LengthType, ) +# pylint: disable=fixme # TODO: Warning: Pydantic V1 import from flow360.component.types import Axis +# pylint: disable=missing-class-docstring class AngularVelocity(SingleAttributeModel): value: AngularVelocityType = pd.Field() @@ -86,6 +90,7 @@ class Fluid(PDEModelBase): Union[NavierStokesModifiedRestartSolution, NavierStokesInitialCondition] ] = pd.Field(None) + # pylint: disable=fixme # fixme: Add support for other initial conditions @@ -104,6 +109,7 @@ class Solid(PDEModelBase): initial_condition: Optional[HeatEquationInitialCondition] = pd.Field(None) +# pylint: disable=duplicate-code class ForcePerArea(Flow360BaseModel): """:class:`ForcePerArea` class for setting up force per area for Actuator Disk @@ -137,7 +143,7 @@ class ForcePerArea(Flow360BaseModel): thrust: List[float] circumferential: List[float] - # pylint: disable=no-self-argument + # pylint: disable=no-self-argument, missing-function-docstring @pd.model_validator(mode="before") @classmethod def check_len(cls, values): @@ -190,9 +196,11 @@ class BETDiskSectionalPolar(Flow360BaseModel): drag_coeffs: Optional[List[List[List[float]]]] = pd.Field() +# pylint: disable=no-member class BETDisk(Flow360BaseModel): """Same as Flow360Param BETDisk. - Note that `center_of_rotation`, `axis_of_rotation`, `radius`, `thickness` can be acquired from `entity` so they are not required anymore. + Note that `center_of_rotation`, `axis_of_rotation`, `radius`, `thickness` can be acquired from `entity` + so they are not required anymore. """ entities: Optional[EntityList[Cylinder]] = pd.Field(None, alias="volumes")