Skip to content

User expression support [POC] (#789) #841

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

Draft
wants to merge 46 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
ec01e74
User expression support [POC] (#789)
andrzej-krupka Mar 19, 2025
de228bc
Make core blueprint package framework-agnostic, add support for parti…
andrzej-krupka Apr 10, 2025
d257d5b
Fix Python 3.9 compatibility
Apr 10, 2025
622b587
Added complete list of usable solver variables (#888)
andrzej-krupka Apr 15, 2025
eedb1a7
Ensure global scope variables are visible within validation service
Apr 16, 2025
49318b0
Fix validation ordering bug
Apr 18, 2025
b0a9d06
Allow extra fields in variable objects
Apr 22, 2025
858413d
Improved error messages (#945)
maciej-flexcompute Apr 24, 2025
c580137
Merge branch 'develop' into expressions
benflexcompute Apr 30, 2025
d86b2dd
Nested expression support + expression validation endpoints (#946)
andrzej-krupka May 8, 2025
65667a4
Merge branch 'develop' into expressions
benflexcompute May 8, 2025
6f41bf4
Reorganized solver variables into target namespaces (#986)
andrzej-krupka May 8, 2025
dfd430c
Merge branch 'develop' into expressions
benflexcompute May 9, 2025
e0404d7
Merge branch 'develop' into expressions
benflexcompute May 12, 2025
ea3e9fa
Add dependency cycle checking and add non-dimensioned array handling …
andrzej-krupka May 14, 2025
fb136c1
Validation service fixes, better error messages (#1030)
andrzej-krupka May 20, 2025
ec50929
Added unit handling to solver code converter (#1049)
andrzej-krupka May 20, 2025
63be368
Expressions fixes, demonstrating E2E capability for user-variable exp…
andrzej-krupka May 20, 2025
1cecaac
Merge branch 'develop' into expressions
benflexcompute May 20, 2025
2a12453
Pylint Fix for `expression` branch (#1083)
benflexcompute May 23, 2025
ed45d58
Merge remote-tracking branch 'origin/develop' into expressions
benflexcompute May 29, 2025
eda9dfa
Rolled back to python list types, no numpy interop as of now because …
andrzej-krupka May 29, 2025
61beaf5
Partial expression evaluation, example of a builtin function (#1115)
andrzej-krupka Jun 5, 2025
eab8c8a
Merge remote-tracking branch 'origin/develop' into expressions
benflexcompute Jun 5, 2025
b923311
Fixed merging
benflexcompute Jun 6, 2025
b700bbf
Fixed V1 tests
benflexcompute Jun 6, 2025
a6b2cf4
Merge branch 'develop' into expressions
benflexcompute Jun 9, 2025
4a1db3e
[FL-729] [FLPY-7] Dimensioned Volume Output (#1012)
benflexcompute Jun 9, 2025
a613806
Enabled all output types to use UserVariable (#1148)
benflexcompute Jun 10, 2025
7f2eb02
Added unit test for project_variables and also simplified the transla…
benflexcompute Jun 11, 2025
b7b06d6
Merge branch 'develop' into expressions
benflexcompute Jun 11, 2025
e02336c
Added util function to get the unit from expression (#1157)
benflexcompute Jun 11, 2025
18904e2
Disables vector arithmetics for variables (#1158)
benflexcompute Jun 12, 2025
6a337af
List all solver variables (#1150)
angranl-flex Jun 12, 2025
1888483
Merge branch 'develop' into expressions
benflexcompute Jun 12, 2025
14f1c0f
Separate prepending code to declaration and computation parts (#1165)
angranl-flex Jun 17, 2025
d7c3a43
Merge remote-tracking branch 'origin/develop' into expressions
benflexcompute Jun 17, 2025
cb9171c
UserVariable as Token and value from context (#1161)
benflexcompute Jun 17, 2025
14a9e91
Enabled timestepping->step size to be expression too (#1166)
benflexcompute Jun 17, 2025
f88c3f7
Handles NaN desearilization (#1168)
benflexcompute Jun 17, 2025
258ed85
Added proper base for surface probe output
benflexcompute Jun 17, 2025
6c4922e
Ben y/expression front end feedback (#1169)
benflexcompute Jun 18, 2025
e987fbd
Added translator for ValurOrExpression object (#1175)
benflexcompute Jun 18, 2025
49b57b1
Added postProcessing flag setter (#1176)
benflexcompute Jun 19, 2025
096b3f4
Decouple solver variable's solver name with user variable name (#1170)
angranl-flex Jun 19, 2025
78fb058
Merge branch 'develop' into expressions
benflexcompute Jun 19, 2025
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
8 changes: 0 additions & 8 deletions .github/workflows/codestyle.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
# This workflow will upload a Python Package using Twine when a release is created
# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries

# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.

name: Codestyle checking

on:
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ on:
branches: [ develop, release-candidate/* ]
pull_request:
types: [ opened, synchronize, reopened, ready_for_review ]
branches: [ develop, release-candidate/* ]
workflow_call:

jobs:
Expand Down
7 changes: 7 additions & 0 deletions flow360/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@
SI_unit_system,
imperial_unit_system,
)
from flow360.component.simulation.user_code.core.types import UserVariable
from flow360.component.simulation.user_code.functions import math
from flow360.component.simulation.user_code.variables import control, solution
from flow360.component.simulation.user_defined_dynamics.user_defined_dynamics import (
UserDefinedDynamic,
)
Expand Down Expand Up @@ -274,5 +277,9 @@
"StreamlineOutput",
"Transformation",
"WallRotation",
"UserVariable",
"math",
"control",
"solution",
"report",
]
4 changes: 4 additions & 0 deletions flow360/component/project_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from flow360.component.simulation.primitives import Box, Cylinder, GhostSurface
from flow360.component.simulation.simulation_params import SimulationParams
from flow360.component.simulation.unit_system import LengthType
from flow360.component.simulation.user_code.core.types import save_user_variables
from flow360.component.simulation.utils import model_attribute_unlock
from flow360.component.simulation.web.asset_base import AssetBase
from flow360.component.utils import parse_datetime
Expand Down Expand Up @@ -281,6 +282,9 @@ def set_up_params_for_uploading(

params = _set_up_default_reference_geometry(params, length_unit)

# Convert all reference of UserVariables to VariableToken
params = save_user_variables(params)

return params


Expand Down
18 changes: 18 additions & 0 deletions flow360/component/simulation/blueprint/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Blueprint: Safe function serialization and visual programming integration."""

from flow360.component.simulation.blueprint.core.generator import model_to_function
from flow360.component.simulation.blueprint.core.parser import (
expr_to_model,
function_to_model,
)

from .core.function import FunctionNode
from .core.types import Evaluable

__all__ = [
"FunctionNode",
"Evaluable",
"function_to_model",
"model_to_function",
"expr_to_model",
]
111 changes: 111 additions & 0 deletions flow360/component/simulation/blueprint/core/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""Core blueprint functionality."""

from .context import EvaluationContext, ReturnValue
from .expressions import (
BinOpNode,
CallModelNode,
ConstantNode,
ExpressionNode,
ExpressionNodeType,
ListCompNode,
ListNode,
NameNode,
RangeCallNode,
SubscriptNode,
TupleNode,
)
from .function import FunctionNode
from .generator import expr_to_code, model_to_function, stmt_to_code
from .parser import function_to_model
from .statements import (
AssignNode,
AugAssignNode,
ForLoopNode,
IfElseNode,
ReturnNode,
StatementNode,
StatementNodeType,
TupleUnpackNode,
)
from .types import Evaluable, TargetSyntax


def _model_rebuild() -> None:
"""Update forward references in the correct order."""
namespace = {
# Expression types
"NameNode": NameNode,
"ConstantNode": ConstantNode,
"BinOpNode": BinOpNode,
"RangeCallNode": RangeCallNode,
"CallModelNode": CallModelNode,
"TupleNode": TupleNode,
"ListNode": ListNode,
"ListCompNode": ListCompNode,
"SubscriptNode": SubscriptNode,
"ExpressionNodeType": ExpressionNodeType,
# Statement types
"AssignNode": AssignNode,
"AugAssignNode": AugAssignNode,
"IfElseNode": IfElseNode,
"ForLoopNode": ForLoopNode,
"ReturnNode": ReturnNode,
"TupleUnpackNode": TupleUnpackNode,
"StatementNodeType": StatementNodeType,
# Function type
"FunctionNode": FunctionNode,
}

# First update expression classes that only depend on ExpressionType
BinOpNode.model_rebuild(_types_namespace=namespace)
RangeCallNode.model_rebuild(_types_namespace=namespace)
CallModelNode.model_rebuild(_types_namespace=namespace)
TupleNode.model_rebuild(_types_namespace=namespace)
ListNode.model_rebuild(_types_namespace=namespace)
ListCompNode.model_rebuild(_types_namespace=namespace)
SubscriptNode.model_rebuild(_types_namespace=namespace)

# Then update statement classes that depend on both types
AssignNode.model_rebuild(_types_namespace=namespace)
AugAssignNode.model_rebuild(_types_namespace=namespace)
IfElseNode.model_rebuild(_types_namespace=namespace)
ForLoopNode.model_rebuild(_types_namespace=namespace)
ReturnNode.model_rebuild(_types_namespace=namespace)
TupleUnpackNode.model_rebuild(_types_namespace=namespace)

# Finally update Function class
FunctionNode.model_rebuild(_types_namespace=namespace)


# Update forward references
_model_rebuild()


__all__ = [
"ExpressionNode",
"NameNode",
"ConstantNode",
"BinOpNode",
"RangeCallNode",
"CallModelNode",
"TupleNode",
"ListNode",
"ListCompNode",
"ExpressionNodeType",
"StatementNode",
"AssignNode",
"AugAssignNode",
"IfElseNode",
"ForLoopNode",
"ReturnNode",
"TupleUnpackNode",
"StatementNodeType",
"FunctionNode",
"EvaluationContext",
"ReturnValue",
"Evaluable",
"expr_to_code",
"stmt_to_code",
"model_to_function",
"function_to_model",
]
154 changes: 154 additions & 0 deletions flow360/component/simulation/blueprint/core/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"""Evaluation context that contains references to known symbols"""

from typing import Any, Optional

import pydantic as pd

from flow360.component.simulation.blueprint.core.resolver import CallableResolver


class ReturnValue(Exception):
"""
Custom exception to signal a 'return' during the evaluation
of a function model.
"""

def __init__(self, value: Any):
super().__init__("Function returned.")
self.value = value


class EvaluationContext:
"""
Manages variable scope and access during function evaluation.

This class stores named values and optionally resolves names through a
`CallableResolver` when not already defined in the context.
"""

def __init__(
self, resolver: CallableResolver, initial_values: Optional[dict[str, Any]] = None
) -> None:
"""
Initialize the evaluation context.

Args:
resolver (CallableResolver): A resolver used to look up callable names
and constants if not explicitly defined.
initial_values (Optional[dict[str, Any]]): Initial variable values to populate
the context with.
"""
self._values = initial_values or {}
self._data_models = {}
self._resolver = resolver
self._aliases: dict[str, str] = {}

def get(self, name: str, resolve: bool = True) -> Any:
"""
Retrieve a value by name from the context.

If the name is not explicitly defined and `resolve` is True,
attempt to resolve it using the resolver.

Args:
name (str): The variable or callable name to retrieve.
resolve (bool): Whether to attempt to resolve the name if it's undefined.

Returns:
Any: The corresponding value.

Raises:
NameError: If the name is not found and cannot be resolved.
ValueError: If resolution is disabled and the name is undefined.
"""
if name not in self._values:
# Try loading from builtin callables/constants if possible
try:
if not resolve:
raise ValueError(f"{name} was not defined explicitly in the context")
val = self.resolve(name)
# If successful, store it so we don't need to import again
self._values[name] = val
except ValueError as err:
raise NameError(f"Name '{name}' is not defined") from err
return self._values[name]

def get_data_model(self, name: str) -> Optional[pd.BaseModel]:
"""Get the Validation model for the given name."""
if name not in self._data_models:
return None
return self._data_models[name]

def set_alias(self, name, alias) -> None:
"""
Set alias used for code generation.
This is meant for non-user variables.
"""
self._aliases[name] = alias

def get_alias(self, name) -> Optional[str]:
"""
Get alias used for code generation.
This is meant for non-user variables.
"""
return self._aliases.get(name)

def set(self, name: str, value: Any, data_model: pd.BaseModel = None) -> None:
"""
Assign a value to a name in the context.

Args:
name (str): The variable name to set.
value (Any): The value to assign.
data_model (BaseModel, optional): The type of the associate with this entry (for non-user variables)
"""
self._values[name] = value

if data_model:
self._data_models[name] = data_model

def resolve(self, name):
"""
Resolve a name using the provided resolver.

Args:
name (str): The name to resolve.

Returns:
Any: The resolved callable or constant.

Raises:
ValueError: If the name cannot be resolved by the resolver.
"""
return self._resolver.get_allowed_callable(name)

def can_evaluate(self, name) -> bool:
"""
Check if the name can be evaluated via the resolver.

Args:
name (str): The name to check.

Returns:
bool: True if the name is allowed and resolvable, False otherwise.
"""
return self._resolver.can_evaluate(name)

def copy(self) -> "EvaluationContext":
"""
Create a copy of the current context.

Returns:
EvaluationContext: A new context instance with the same resolver and a copy
of the current variable values.
"""
return EvaluationContext(self._resolver, dict(self._values))

@property
def user_variable_names(self):
"""Get the set of user variables in the context."""
return {name for name in self._values.keys() if "." not in name}

def clear(self):
"""Clear user variables from the context."""
self._values = {name: value for name, value in self._values.items() if "." in name}
Loading