API Reference

physika.features.classes

class physika.features.classes.ClassFeature[source]

Bases: ELF

Physika classes implemented as an ELF subclass.

ClassFeature injects rules via REGISTRY at lexer, parser, type checker, and code generator.

Lexer rules Adds two new tokens, CLASS reserved keyword ("class") and DOT token (".") for field and method access

Parser rules Sixteen PLY grammar functions (see make_parser_rules) handle class declarations with and without constructor parameters, field declarations, method definitions, and single or two tuple valued returns.

Type rules Registers class_env entries so the type checker can resolve field types, method calls and constructor calls.

Forward rules Three code-generation handlers were defined. class_def emits a complete nn.Module. field_access` emits ``obj.field. method_call``emits ``obj.method(args).

Physika classes are fully differentiable using Pytroch as backend. Scalar and tensor constructor parameters are converted to torch.as_tensor objects. Parameters used inside a forward method are wrapped in nn.Parameter.

Physika syntax example (see examples/physika_class.phyk):

class Vec:
    x : 
    y : 
    def dot(other : Vec) : :
        return this.x * other.x + this.y * other.y
    def norm_sq() : :
        return this.x * this.x + this.y * this.y

a = Vec(3.0, 4.0)
a.norm_sq()

Examples

>>> from physika.lexer import lexer
>>> from physika.parser import parser, symbol_table
>>> from physika.utils.ast_utils import build_unified_ast
>>> from physika.codegen import from_ast_to_torch
>>> def run_phyk(src):
...     symbol_table.clear()
...     lexer.lexer.lineno = 1
...     ast = build_unified_ast(parser.parse(src, lexer=lexer), symbol_table)
...     exec(from_ast_to_torch(ast, print_code=False), {})
>>> # Physika class example
>>> src = '''
... class Vec:
...     x : ℝ
...     y : ℝ
...     def norm_sq() : ℝ:
...         return this.x * this.x + this.y * this.y
... a = Vec(3.0, 4.0)
... a.x
... a.y
... a.norm_sq()
... '''
>>> # Execute code and verify outputs
>>> run_phyk(src)
3.0 ∈ ℝ
4.0 ∈ ℝ
25.0 ∈ ℝ
forward_rules() dict[source]

Three code-generation handlers were defined. class_def emits a complete nn.Module. field_access` emits ``obj.field. method_call``emits ``obj.method(args).

Physika classes are fully differentiable using Pytroch as backend. Scalar and tensor constructor parameters are converted to torch.as_tensor objects. Parameters used inside a forward method are wrapped in nn.Parameter.

Returns:

Dictionary containg code generation handlers.

Return type:

dict

Examples

>>> from physika.features import ClassFeature
>>> from physika.features.classes import build_class
>>> rules = ClassFeature().forward_rules()
>>> class_def = build_class([("mass", "ℝ")], [])
>>> code = rules["class_def"](("class_def", "Particle", class_def), **{})
>>> # Physika code:
>>> # class Particle:
>>> #   mass: ℝ
>>> nl = chr(10) # unicode for
>>> expected = nl.join([
...     "class Particle(nn.Module):",
...     "    def __init__(self, mass):",
...     "        super().__init__()",
...     "        self.mass = torch.as_tensor(mass).float()",
...     "",
...     "    @property",
...     "    def params(self):",
...     "        return list(self.parameters())",
...     "",
...     "    def update(self, lr, grads):",
...     "        with torch.no_grad():",
...     "            for p, g in zip(self.parameters(), grads):",
...     "                if g is not None:",
...     "                    p -= lr * g",
... ])
>>> code == expected
True
lexer_rules() dict[source]

Adds two new tokens, CLASS reserved keyword ("class") and DOT token (".") for field and method access.

Returns:

Dictionary with reserved keywords, tokens and tokens functions

Return type:

dict

Examples

>>> from physika.features import ClassFeature
>>> rules = ClassFeature().lexer_rules()
>>> rules["reserved"]
{'class': 'CLASS'}
>>> rules["tokens"]
['CLASS', 'DOT']
name: str = 'physika-class'
parser_rules() list[source]

Override parser_rules handler for new grammar rules.

Sixteen PLY grammar functions (see make_parser_rules) handle class declarations with and without constructor parameters, field declarations , method definitions, and single or two tuple valued returns.

Returns:

List of PLY grammar functions to be injected into physika.parser.

Return type:

list

Examples

>>> from physika.features import ClassFeature
>>> rules = ClassFeature().parser_rules()
>>> len(rules)
20
>>> rules[0].__name__
'p_statement_class_no_params'
type_rules() dict[source]

Registers two type-checking handlers that validate field access and method calls on class instances. field_access``infers ``obj.field by looking up the field name in the class_env and returns its declared type. Raises an error if the field does not exist or if a class constructor is not instance. method_call infers obj.method(args) by checking the number of arguments and types against the method’s declared parameters and returning its declared return type. Raises an error if the method does not exist or if argument types do not match.

Returns:

Dispatch table mapping "field_access" and "method_call" AST tags to their type inference handlers.

Return type:

dict

Examples

>>> from physika.features import ClassFeature
>>> rules = ClassFeature().type_rules()
>>> sorted(rules.keys())
['field_access', 'method_call']
physika.features.classes.build_class(constructor_params: list | None, body_items: list) dict[source]

Build a dict of the class from parsed body items.

Parameters:
  • constructor_params (Optional[list]) – None when fields are declared in the class body (e.g. class Particle: mass : ). A list of (name, type) pairs when params appear in the header (e.g. class HamiltonianNet(W1: ℝ[M,N], b1: ℝ[M], ...):).

  • body_items (list) – Body expressions and statemetns that could be declarations, assignments, methods, fields, for-loops, if-else, etc.

Returns:

Dictionary with constructor params, fields and methods needed for building a Python class.

Return type:

dict

Examples

class Particle:
    mass : 
    def ke() : : ...
>>> from physika.features.classes import build_class
>>> ke = {"name": "ke", "params": [], "return_type": "ℝ", "statements": [], "body": None}
>>> body_items = [("field_decl", "mass", "ℝ"), ("method_def", ke)]
>>> result = build_class(None, body_items)
>>> result["class_params"]
[('mass', 'ℝ')]
>>> result["fields"]
[]
>>> result
{'class_params': [('mass', 'ℝ')], 'fields': [], 'methods': [{'name': 'ke', 'params': [], 'return_type': 'ℝ', 'statements': [], 'body': None}]}
physika.features.classes.emit_method(method: dict, all_params: list, to_expr: Callable, scalar_only: bool) list[str][source]

Emit code for a class method as an nn.Module class.

Parameters:
  • method (dict) – Dictionary that contains method’s class definition information in order “name”, “params” with declared types, “return_type”, body statements and expressions. # noqa :E501

  • all_params (list) – List of method’s parameters with declared types.

  • to_expr (Callable) – ast_to_torch_expr for getting the associated torch code.

  • scalar_only (bool) – Boolean that indicate how to define if-else blocks when emiting body stmts. # noqa :E501

Returns:

method_lines – Pytorch code lines for a given Physika class method.

Return type:

list[str]

Examples

>>> from physika.features.classes import emit_method
>>> body = ("mul", ("num", 0.5), ("field_access", ("var", "this"), "mass"))
>>> ke = {"name": "ke", "params": [], "return_type": "ℝ", "statements": [], "body": body}
>>> emit_method(ke, [("mass", "ℝ")], lambda _: "0.5 * this.mass", True)
['', '    def ke(self):', '        this = self', '        return 0.5 * self.mass']
physika.features.classes.generate_class(name: str, class_def: dict) str[source]

Emit a nn.Module subclass from a Physika class definition.

Parameters:
  • name (str) – Name of a defined Physika class.

  • class_def (dict) – Dictionary that contains all the information for the defined Physika class. In order, “class_params” and types, methods, statements and body.

Returns:

PyTorch source code for the nn.Module subclass.

Return type:

str

Examples

Physika class:

class Particle:
    mass : 
    def ke() : :
        return 0.5 * this.mass
>>> from physika.features.classes import generate_class, build_class
>>> ke = {"name": "ke", "params": [], "return_type": "ℝ", "statements": [], "body": ("mul", ("num", 0.5), ("field_access", ("var", "this"), "mass"))}
>>> class_def = build_class(None, [("field_decl", "mass", "ℝ"), ("method_def", ke)])  # noqa :E501
>>> print(generate_class("Particle", class_def))
class Particle(nn.Module):
    def __init__(self, mass):
        super().__init__()
        self.mass = torch.as_tensor(mass).float()

    def ke(self):
        this = self
        return (0.5 * self.mass)

    @property
    def params(self):
        return list(self.parameters())

    def update(self, lr, grads):
        with torch.no_grad():
            for p, g in zip(self.parameters(), grads):
                if g is not None:
                    p -= lr * g
physika.features.classes.is_learnable(type_spec: str) bool[source]

Helper functions that returns True for “ℝ” and “ℝ[n]” types that should become nn.Parameter.

Parameters:

type_spec (str) – Physika types converted to strings.

Returns:

Returns True for ℝ and tensor (“ℝ[n]”) types, which are learnable

Return type:

bool

Examples

>>> from physika.features.classes import is_learnable
>>> is_learnable("ℝ")
True
>>> is_learnable(("tensor", [2]))
True
>>> is_learnable("int")
False
physika.features.classes.make_parser_rules()[source]

PLY grammar functions for Physika class syntax.

physika.features.classes.replace_class_params(code: str, all_params: list) str[source]

Add self. prefix to class params to match Python code.

Parameters:
  • code (str) – Converted Python code using emit_body_stmts that contains params as bare param names and needs self. prefix to run in Python.

  • all_params (list)

Returns:

code – Same Pythonic code with prefix added for class params.

Return type:

str

Examples

>>> from physika.features.classes import replace_class_params
>>> replace_class_params("0.5 * mass", [("mass", "ℝ")])
'0.5 * self.mass'
>>> replace_class_params("self.mass * 2", [("mass", "ℝ")])
'self.mass * 2'
physika.features.classes.unwrap_return(ret: tuple | None) tuple | None[source]

Helper function to build a return expression that will be used by the parser.

Parameters:

ret (tuple[str]) – Return expressions from class methods

Returns:

Depending on the case, None return expression, single or tuple return expressions

Return type:

Optional[tuple]

Examples

>>> from physika.features.classes import unwrap_return
>>> # equivalent of: 0.5 * this.mass
>>> expr = ("mul",
... ("num", 0.5), ("field_access", ("var", "this"), "mass"))
>>> unwrap_return(("return_single", expr)) == expr
True
>>> unwrap_return(("return_tuple", ("var", "a"), ("var", "b")))
('tuple_return', ('var', 'a'), ('var', 'b'))
>>> unwrap_return(None) is None
True

physika.features.randomness

class physika.features.randomness.RandomnessFeature[source]

Bases: ELF

Differentiable probabilistic sampling for Physika.

RandomnessFeature, as an ELF subclass, adds support for sampling from Pytorch probability distributions in Physika programs being fully differentiable. Physika random sampling uses tilde syntax ~ to draw from a distribution (e.g. x ~ Normal(0, 1)). Each distribution recieves its own set of parameters (e.g. mean and std for Normal). There are two general arguments: 1) shape parameters to specify the number of samples and their shapes, 2) a string argument to specify the gradient estimator to use ("reparam", "score", or "none").

Supported distributions

  • Normal(µ, σ, n, mode)

  • Uniform(a, b, n, mode)

  • Beta(α, β, n, mode)

  • Gamma(concentration, rate, n, mode)

  • Bernoulli(p, n, mode)

Examples

>>> import torch
>>> from physika.lexer import lexer
>>> from physika.parser import parser, symbol_table
>>> from physika.utils.ast_utils import build_unified_ast
>>> from physika.codegen import from_ast_to_torch
>>> def run_phyk(src):
...     symbol_table.clear()
...     lexer.lexer.lineno = 1
...     ast = build_unified_ast(parser.parse(src, lexer=lexer), symbol_table)  # noqa: E501
...     exec(from_ast_to_torch(ast, print_code=False), {})
>>> # Physika scalar Normal and Bernoulli samples
>>> src = '''
... μ : ℝ = 0.0
... σ : ℝ = 1.0
... x : ℝ ~ Normal(μ, σ)
... coin : ℝ ~ Bernoulli(0.5)
... '''
>>> # Execute code
>>> run_phyk(src)
forward_rules() dict[source]

Code generation handler for emiting random sampling nodes using Pytorch as backend.

sample_stmt_emit emits name = <dist>.rsample(...) for statement-level sample nodes ("sample", "typed_sample", "body_sample", "body_typed_sample", "for_sample", "for_typed_sample"). For inline sample expressions nodes, sample_expr_emit emits call expression ("sample_expr", "typed_sample_expr"). make_call_emit`  wraps each distribution function (e.g. ``normal_dist) so it can be dispatched by "call:Name" key.

Returns:

Dispatch table mapping AST node tags and "call:Name" keys to their code generation emiters.

Return type:

dict

Examples

>>> from physika.features import RandomnessFeature
>>> from physika.utils.ast_utils import ast_to_torch_expr
>>> rules = RandomnessFeature().forward_rules()
>>> # Physika code:
>>> # x ~ Normal(0.0, 1.0)
>>> node = ("sample", "x", ("call", "Normal", [("num", 0.0), ("num", 1.0)]))  # noqa: E501
>>> rules["sample"](node, ast_to_torch_expr)
'x = torch.distributions.Normal(0.0, 1.0).rsample()'
lexer_rules() dict[source]

Adds TILDE token ("~") for stochastic sampling syntax and includes PHYSIKA reserved keyword so that physika.seed(n) parses. Also, includes greek letters aliases for mapping with torch distributions:

  • 𝒩Normal

  • 𝒰Uniform

  • ΓGamma

  • Beta

Returns:

Dictionary with tokens (["TILDE", "PHYSIKA"]) and token_funcs (t_TILDE, t_DIST_NORMAL, t_DIST_GAMMA, t_DIST_BETA, t_DIST_UNIFORM).

Return type:

dict

Examples

>>> from physika.features.randomness import RandomnessFeature
>>> rules = RandomnessFeature().lexer_rules()
>>> rules["tokens"]
['TILDE', 'PHYSIKA']
>>> rules["reserved"]
{'physika': 'PHYSIKA'}
>>> rules["token_funcs"][1].__name__
't_DIST_NORMAL'
>>> rules["token_funcs"][2].__name__
't_DIST_GAMMA'
>>> rules["token_funcs"][3].__name__
't_DIST_BETA'
>>> rules["token_funcs"][4].__name__
't_DIST_UNIFORM'
name: str = 'randomness'
parser_rules() list[source]

Handler for new grammar rules.

Nine new PLY grammar functions:

  • Seven for random sampling at top-level, function/method bodies, and for-loops.

  • Two for physika.seed(n) at top-level and inside function bodies.

Returns:

List of PLY grammar functions to be injected into physika.parser.

Return type:

list

Examples

>>> from physika.features import RandomnessFeature
>>> rules = RandomnessFeature().parser_rules()
>>> len(rules)
9
>>> rules[0].__name__
'p_sample_untyped'
type_rules() dict[source]

Adds two type checker rules that verifies the declared and inferred type of random sampling:

  • typed_sample_type: checks for statements, declarations, and assignments.

  • sample_expr_type: intended for expressions (e.g., inside inline for-loops).

Returns:

Dispatch table mapping "typed_sample_type" and "sample_expr_type" AST tags to their type inference handlers.

Return type:

dict

Examples

>>> from physika.features import RandomnessFeature
>>> rules = RandomnessFeature().type_rules()
>>> sorted(rules.keys())
['sample_expr', 'typed_sample', 'typed_sample_expr']
physika.features.randomness.bernoulli_dist(args: List[Tuple], to_expr: Callable, **ctx) str[source]

Emit Pytorch code for sampling from a Bernoulli distribution based on args (p).

Parameters:
  • args (List[Union[Tuple, Tuple]]) – List that contains the arguments passed to a probability distribution.

  • to_expr (callable) – ast_to_torch_expr to transform AST elements for bernoulli distribution to valid torch code as strings.

Example

>>> from physika.features.randomness import bernoulli_dist
>>> from physika.utils.ast_utils import ast_to_torch_expr
>>> args = [('num', 0.0)]
>>> bernoulli_dist(args, ast_to_torch_expr)
'torch.distributions.Bernoulli(0.0).sample().detach()'
physika.features.randomness.beta_dist(args: List[Tuple], to_expr: Callable, **ctx) str[source]

Emit Pytorch code for sampling from a Beta distribution based on args (alpha, beta).

Parameters:
  • args (List[Union[Tuple, Tuple]]) – List that contains the arguments passed to a probability distribution.

  • to_expr (callable) – ast_to_torch_expr to transform AST elements for beta distribution to valid torch code as strings.

Example

>>> from physika.features.randomness import beta_dist
>>> from physika.utils.ast_utils import ast_to_torch_expr
>>> args = [('num', 0.0), ('num', 1.0)]
>>> beta_dist(args, ast_to_torch_expr)
'torch.distributions.Beta(0.0, 1.0).rsample()'
physika.features.randomness.extract_dist_args(args: List[Tuple], n_params: int) Tuple[List, List, str][source]

Split distribution args into (param_args, shape_args, mode).

Returns (param_args, shape_args, mode) where mode is one of "reparam", "score", or "none". param_args are distribution parameters related to sampling like mean (μ) and standard deviation (σ) for Normal distribution. shape_args are related to size of output sampled vector (empty mean to sample one element).

"reparam" and "score" refers to two estimators used in stochastic graph computation described in [1].

Parameters:

args (List[Union[Tuple, Tuple]]) – List that contains the arguments passed to a probability distribution.

References

Example

>>> from physika.features.randomness import extract_dist_args
>>> # Normal distribution (mean = 0, std = 1),
>>> # 1 sample (ℝ),
>>> # none grad mode
>>> args = [("num", 0.0), ("num", 1.0)]
>>> extract_dist_args(args, n_params=2)
([('num', 0.0), ('num', 1.0)], [], 'none')
>>> # Normal distribution (mean = 0, std = 1),
>>> # 20 samples (ℝ[20]),
>>> # none grad mode
>>> args = [("num", 0.0), ("num", 1.0), ("num", 20.0), ("num", 1.0)]
>>> extract_dist_args(args, n_params=2)
([('num', 0.0), ('num', 1.0)], [('num', 20.0), ('num', 1.0)], 'none')
>>> # Normal distribution (mean = 0, std = 1),
>>> # 1 sample (ℝ),
>>> # 'reparam' as grad mode
>>> args = [("num", 0.0), ("num", 1.0), ("string", "reparam")]
>>> extract_dist_args(args, n_params=2)
([('num', 0.0), ('num', 1.0)], [], 'reparam')
physika.features.randomness.gamma_dist(args: List[Tuple], to_expr: Callable, **ctx) str[source]

Emit Pytorch code for sampling from a Gamma distribution based on args (concentration, rate).

Parameters:
  • args (List[Union[Tuple, Tuple]]) – List that contains the arguments passed to a probability distribution.

  • to_expr (callable) – ast_to_torch_expr to transform AST elements for gamma distribution to valid torch code as strings.

Example

>>> from physika.features.randomness import gamma_dist
>>> from physika.utils.ast_utils import ast_to_torch_expr
>>> args = [('num', 0.0), ('num', 1.0)]
>>> gamma_dist(args, ast_to_torch_expr)
'torch.distributions.Gamma(0.0, 1.0).rsample()'
physika.features.randomness.get_dim(val: Tuple, env: dict) str | int | None[source]

Get the dimension value, int or string, from a distribution shape argument.

Parameters:
  • val (Tuple) – AST node (“num”/”var”) or a dim (int or str) from declared_type.dims[i][0].

  • env (dict) – Enviroment dictionary with variables, classes, functions, and types accumulated so far.

Example

>>> from physika.features.randomness import get_dim
>>> env = {("__val__", "n"): 100}
>>> get_dim(("var", "n"), env)
100
physika.features.randomness.get_shape_args(call_args: List[Tuple], env: dict) List[Tuple][source]

Extract shape arguments from distribution arguments.

Distribution calls in Physika have leading args as distribution parameters, and the optional trailing args specify the output shape. This function is a helper for type checker algorithm to get shape args without requiring explicit knowledge of how many params each distribution takes.

Starts from the end of distribution args and stopping at the first distribution parameter rather than a size.

A ("string", ...) or ("equation_string", ...) arg (a gradient mode hint such as "reparam" or "score") is stripped before looking for shape arguments.

Parameters:
  • call_args (List[Tuple]) – Argument list from a distribution "call" AST node.

  • env (dict) – Type environment accumulated by the type checker. Must include ("__val__", name) entries for variables whose literal integer value was tracked during declaration.

Returns:

Shape arg AST nodes in order. Empty when the call produces a scalar sample.

Return type:

List[Tuple]

Examples

>>> from physika.features.randomness import get_shape_args
>>> env = {}
>>> # Normal(0.0, 1.0) — float params, no size args
>>> get_shape_args([("num", 0.0), ("num", 1.0)], env)
[]
>>> # Normal(0.0, 1.0, 100)
>>> get_shape_args([("num", 0.0), ("num", 1.0), ("num", 100)], env)
[('num', 100)]
>>> # Normal(mu, sigma, n)
>>> env_n = {("__val__", "n"): 100}
>>> get_shape_args([("var", "mu"), ("var", "sigma"), ("var", "n")], env_n)
[('var', 'n')]
>>> get_shape_args([("num", 0.0), ("num", 1.0), ("num", 50), ("string", "reparam")], env)  # noqa: E501
[('num', 50)]
physika.features.randomness.normal_dist(args: List[Tuple], to_expr: Callable, **ctx) str[source]

Emit Pytorch code for sampling from a Normal distribution based on args (mean, std).

Parameters:
  • args (List[Union[Tuple, Tuple]]) – List that contains the arguments passed to a probability distribution.

  • to_expr (callable) – ast_to_torch_expr to transform AST elements for normal distribution to valid torch code as strings.

Example

>>> from physika.features.randomness import normal_dist
>>> from physika.utils.ast_utils import ast_to_torch_expr
>>> args = [('num', 0.0), ('num', 1.0)]
>>> normal_dist(args, ast_to_torch_expr)
'torch.distributions.Normal(0.0, 1.0).rsample()'
physika.features.randomness.sample(dist_expr: str, shape_args: List[Tuple], mode: str, default_mode: str, to_expr: Callable) str[source]

Emit PyTorch source code for a stochastic node in a Physika program.

Physika models probabilistic programs as Stochastic Computation Graphs (SCGs) [1]. In an SCG, random variables are known as stochastic node and other operations, not related with randomness, are deterministic nodes. Gradients flow through deterministic nodes by backpropagation, but stochastic nodes require a dedicated estimator to propagate gradients when sampling from a distribution.

Physika supports two estimators:

  • Pathwise Estimator ("reparam", default for continuous distributions): the sample is written as a deterministic transformation of a noise variable, e.g. z: = μ + σ·ε where ε : ~ N(0,1). PyTorch’s rsample() allows gradients to flow through μ and σ.

  • Score Function Estimator ("score", for non-continous distributions):

    log p(x, θ) is used to estimate the gradient without needing a differentiable sampler. The sample is detached from the tape (sample().detach()) and a differentiable log_prob term in the loss is needed so that the gradient is computed.

Parameters:
  • dist_expr (str) – Emitted PyTorch source code (torch.distributions.Dist(...)) expression.

  • shape_args (list) – Tuple containing the output shape dimensions. Empty values means scalar sample.

  • mode (str) – Explicit grad mode from the source.

  • default_mode (str) – Fallback mode when mode is "none". "reparam" for continuous distributions, "score" for non-continuous distributions such as Bernoulli.

  • to_expr (callable) – ast_to_torch_expr used to emit sub-expression for dims and shapes.

Returns:

A Python code string that evaluates to a sampled tensor.

Return type:

str

Example

>>> from physika.features.randomness import sample
>>> # Scalar reparam sample
>>> sample("torch.distributions.Normal(0.0, 1.0)", [], "none", "reparam", str)
'torch.distributions.Normal(0.0, 1.0).rsample()'
>>> shape_nodes = [("num", 20.0), ("num", 1.0)]
>>> to_expr = lambda node: str(node[1])
>>> # 2D reparam normal sample, shape (20, 1)
>>> sample("torch.distributions.Normal(0.0, 1.0)", shape_nodes, "none", "reparam", to_expr)  # noqa: E501
'torch.distributions.Normal(0.0, 1.0).rsample((int(20.0), int(1.0),))'
>>> # Bernoulli (score function sample)
>>> sample("torch.distributions.Bernoulli(0.3)", [], "score", "score", str)
'torch.distributions.Bernoulli(0.3).sample().detach()'
physika.features.randomness.uniform_dist(args: List[Tuple], to_expr: Callable, **ctx) str[source]

Emit Pytorch code for sampling from a Uniform distribution based on args (lo, hi).

Parameters:
  • args (List[Union[Tuple, Tuple]]) – List that contains the arguments passed to a probability distribution.

  • to_expr (callable) – ast_to_torch_expr to transform AST elements for normal distribution to valid torch code as strings.

Example

>>> from physika.features.randomness import normal_dist
>>> from physika.utils.ast_utils import ast_to_torch_expr
>>> args = [('num', 0.0), ('num', 1.0)]
>>> normal_dist(args, ast_to_torch_expr)
'torch.distributions.Normal(0.0, 1.0).rsample()'

physika.parser

physika.parser.p_args_empty(p)[source]

args :

physika.parser.p_args_multi(p)[source]

args : expr COMMA args

physika.parser.p_args_single(p)[source]

args : expr

physika.parser.p_condition_eq(p)[source]

condition : func_expr EQEQ func_expr

physika.parser.p_condition_geq(p)[source]

condition : func_expr GEQ func_expr

physika.parser.p_condition_gt(p)[source]

condition : func_expr GT func_expr

physika.parser.p_condition_leq(p)[source]

condition : func_expr LEQ func_expr

physika.parser.p_condition_lt(p)[source]

condition : func_expr LT func_expr

physika.parser.p_condition_neq(p)[source]

condition : func_expr NEQ func_expr

physika.parser.p_dimension_contravariant(p)[source]

dimension_spec : PLUS NUMBER

physika.parser.p_dimension_covariant(p)[source]

dimension_spec : MINUS NUMBER

physika.parser.p_dimension_invariant(p)[source]

dimension_spec : NUMBER

physika.parser.p_dimension_invariant_id(p)[source]

dimension_spec : ID

physika.parser.p_dimension_list_multi(p)[source]

dimension_list : dimension_spec COMMA dimension_list

physika.parser.p_dimension_list_single(p)[source]

dimension_list : dimension_spec

physika.parser.p_dimension_type_as_symbol(p)[source]

dimension_spec : TYPE

physika.parser.p_elements_multi(p)[source]

elements : expr COMMA elements

physika.parser.p_elements_newline(p)[source]

elements : NEWLINE elements | elements NEWLINE

physika.parser.p_elements_single(p)[source]

elements : expr

physika.parser.p_error(p)[source]
physika.parser.p_expr_minus(p)[source]

expr : expr MINUS term

physika.parser.p_expr_plus(p)[source]

expr : expr PLUS term

physika.parser.p_expr_term(p)[source]

expr : term

physika.parser.p_factor_array(p)[source]

factor : LBRACKET elements RBRACKET

physika.parser.p_factor_call(p)[source]

factor : ID LPAREN args RPAREN

physika.parser.p_factor_complex(p)[source]

factor : COMPLEX

physika.parser.p_factor_for_expr(p)[source]

factor : FOR ID COLON TYPE LPAREN func_expr RPAREN ARROW func_expr

physika.parser.p_factor_for_expr_auto(p)[source]

factor : FOR ID ARROW func_expr

physika.parser.p_factor_for_expr_range(p)[source]

factor : FOR ID COLON TYPE LPAREN func_expr COMMA func_expr RPAREN ARROW func_expr

physika.parser.p_factor_group(p)[source]

factor : LPAREN expr RPAREN

physika.parser.p_factor_id(p)[source]

factor : ID

physika.parser.p_factor_index(p)[source]

factor : ID LBRACKET NUMBER RBRACKET

physika.parser.p_factor_indexN(p)[source]

factor : ID LBRACKET multi_index_list RBRACKET

physika.parser.p_factor_index_var(p)[source]

factor : ID LBRACKET ID RBRACKET

physika.parser.p_factor_neg(p)[source]

factor : MINUS factor

physika.parser.p_factor_number(p)[source]

factor : NUMBER

physika.parser.p_factor_slice(p)[source]

factor : ID LBRACKET NUMBER COLON NUMBER RBRACKET

physika.parser.p_factor_string(p)[source]

factor : STRING

physika.parser.p_for_body_empty(p)[source]

for_body :

physika.parser.p_for_body_multi(p)[source]

for_body : for_body for_statement

physika.parser.p_for_statement_assign(p)[source]

for_statement : ID EQUALS func_expr NEWLINE

physika.parser.p_for_statement_call(p)[source]

for_statement : ID LPAREN func_args RPAREN NEWLINE

physika.parser.p_for_statement_empty(p)[source]

for_statement : NEWLINE

physika.parser.p_for_statement_for(p)[source]

for_statement : FOR ID COLON NEWLINE INDENT for_body DEDENT

physika.parser.p_for_statement_for_range(p)[source]

for_statement : FOR ID COLON TYPE LPAREN func_expr RPAREN NEWLINE INDENT for_body DEDENT | FOR ID COLON TYPE LPAREN func_expr RPAREN COLON NEWLINE INDENT for_body DEDENT | FOR ID COLON TYPE LPAREN func_expr COMMA func_expr RPAREN NEWLINE INDENT for_body DEDENT | FOR ID COLON TYPE LPAREN func_expr COMMA func_expr RPAREN COLON NEWLINE INDENT for_body DEDENT

physika.parser.p_for_statement_if_else(p)[source]

for_statement : IF condition COLON NEWLINE INDENT for_body DEDENT ELSE COLON NEWLINE INDENT for_body DEDENT

physika.parser.p_for_statement_if_only(p)[source]

for_statement : IF condition COLON NEWLINE INDENT for_body DEDENT

physika.parser.p_for_statement_index_assign_nd(p)[source]

for_statement : ID LBRACKET loop_index_list RBRACKET EQUALS func_expr NEWLINE

physika.parser.p_for_statement_pluseq(p)[source]

for_statement : ID PLUSEQ func_expr NEWLINE

physika.parser.p_func_args_empty(p)[source]

func_args :

physika.parser.p_func_args_multi(p)[source]

func_args : func_expr COMMA func_args

physika.parser.p_func_args_single(p)[source]

func_args : func_expr

physika.parser.p_func_body_stmt_assign(p)[source]

func_body_stmt : ID EQUALS func_expr NEWLINE

physika.parser.p_func_body_stmt_decl(p)[source]

func_body_stmt : ID COLON type_spec EQUALS func_expr NEWLINE

physika.parser.p_func_body_stmt_empty(p)[source]

func_body_stmt : NEWLINE

physika.parser.p_func_body_stmt_for(p)[source]

func_body_stmt : FOR ID COLON NEWLINE INDENT func_loop_body DEDENT

physika.parser.p_func_body_stmt_for_implicit(p)[source]

func_body_stmt : FOR loop_var_list COLON NEWLINE INDENT func_loop_body DEDENT

physika.parser.p_func_body_stmt_for_range(p)[source]

func_body_stmt : FOR ID COLON TYPE LPAREN func_expr RPAREN NEWLINE INDENT func_loop_body DEDENT | FOR ID COLON TYPE LPAREN func_expr RPAREN COLON NEWLINE INDENT func_loop_body DEDENT | FOR ID COLON TYPE LPAREN func_expr COMMA func_expr RPAREN NEWLINE INDENT func_loop_body DEDENT | FOR ID COLON TYPE LPAREN func_expr COMMA func_expr RPAREN COLON NEWLINE INDENT func_loop_body DEDENT

physika.parser.p_func_body_stmt_if_else(p)[source]

func_body_stmt : IF condition COLON NEWLINE INDENT func_body_stmts DEDENT ELSE COLON NEWLINE INDENT func_body_stmts DEDENT

physika.parser.p_func_body_stmt_if_else_return(p)[source]

func_body_stmt : IF condition COLON NEWLINE INDENT RETURN func_expr NEWLINE DEDENT ELSE COLON NEWLINE INDENT RETURN func_expr NEWLINE DEDENT

physika.parser.p_func_body_stmt_if_only(p)[source]

func_body_stmt : IF condition COLON NEWLINE INDENT func_body_stmts DEDENT

physika.parser.p_func_body_stmt_if_return(p)[source]

func_body_stmt : IF condition COLON NEWLINE INDENT RETURN func_expr NEWLINE DEDENT

physika.parser.p_func_body_stmt_index_assign(p)[source]

func_body_stmt : ID LBRACKET func_expr RBRACKET EQUALS func_expr NEWLINE

physika.parser.p_func_body_stmt_index_assign_nd(p)[source]

func_body_stmt : ID LBRACKET multi_index_list RBRACKET EQUALS func_expr NEWLINE

physika.parser.p_func_body_stmt_tuple_unpack(p)[source]

func_body_stmt : ID COMMA ID EQUALS func_expr NEWLINE

physika.parser.p_func_body_stmt_tuple_unpack_three(p)[source]

func_body_stmt : ID COMMA ID COMMA ID EQUALS func_expr NEWLINE

physika.parser.p_func_body_stmt_zeros_decl(p)[source]

func_body_stmt : ID COLON type_spec NEWLINE

physika.parser.p_func_body_stmts_multi(p)[source]

func_body_stmts : func_body_stmts func_body_stmt

physika.parser.p_func_body_stmts_single(p)[source]

func_body_stmts : func_body_stmt

physika.parser.p_func_elements_multi(p)[source]

func_elements : func_expr COMMA func_elements

physika.parser.p_func_elements_single(p)[source]

func_elements : func_expr

physika.parser.p_func_expr_minus(p)[source]

func_expr : func_expr MINUS func_term

physika.parser.p_func_expr_plus(p)[source]

func_expr : func_expr PLUS func_term

physika.parser.p_func_expr_term(p)[source]

func_expr : func_term

physika.parser.p_func_factor_array(p)[source]

func_factor : LBRACKET func_elements RBRACKET

physika.parser.p_func_factor_call(p)[source]

func_factor : ID LPAREN func_args RPAREN

physika.parser.p_func_factor_call_index(p)[source]

func_factor : ID LPAREN func_args RPAREN LBRACKET func_expr RBRACKET

physika.parser.p_func_factor_chain_index(p)[source]

func_factor : func_factor LBRACKET func_expr RBRACKET

physika.parser.p_func_factor_complex(p)[source]

func_factor : COMPLEX

physika.parser.p_func_factor_for_expr(p)[source]

func_factor : FOR ID COLON TYPE LPAREN func_expr RPAREN ARROW func_expr

physika.parser.p_func_factor_for_expr_auto(p)[source]

func_factor : FOR ID ARROW func_expr

physika.parser.p_func_factor_for_expr_range(p)[source]

func_factor : FOR ID COLON TYPE LPAREN func_expr COMMA func_expr RPAREN ARROW func_expr

physika.parser.p_func_factor_group(p)[source]

func_factor : LPAREN func_expr RPAREN

physika.parser.p_func_factor_id(p)[source]

func_factor : ID

physika.parser.p_func_factor_imaginary(p)[source]

func_factor : IMAGINARY

physika.parser.p_func_factor_index(p)[source]

func_factor : ID LBRACKET func_expr RBRACKET

physika.parser.p_func_factor_indexN(p)[source]

func_factor : ID LBRACKET multi_index_list RBRACKET

physika.parser.p_func_factor_number(p)[source]

func_factor : NUMBER

physika.parser.p_func_factor_step_slice(p)[source]

func_factor : ID LBRACKET NUMBER COLON COLON NUMBER RBRACKET

physika.parser.p_func_factor_string(p)[source]

func_factor : STRING

physika.parser.p_func_init_empty(p)[source]

func_init :

physika.parser.p_func_init_multi(p)[source]

func_init : func_init func_init_stmt

physika.parser.p_func_init_stmt_assign(p)[source]

func_init_stmt : ID EQUALS func_expr NEWLINE

physika.parser.p_func_init_stmt_empty(p)[source]

func_init_stmt : NEWLINE

physika.parser.p_func_loop_body_empty(p)[source]

func_loop_body :

physika.parser.p_func_loop_body_multi(p)[source]

func_loop_body : func_loop_body func_loop_stmt

physika.parser.p_func_loop_stmt_assign(p)[source]

func_loop_stmt : ID EQUALS func_expr NEWLINE

physika.parser.p_func_loop_stmt_empty(p)[source]

func_loop_stmt : NEWLINE

physika.parser.p_func_loop_stmt_for_range(p)[source]

func_loop_stmt : FOR ID COLON TYPE LPAREN func_expr RPAREN NEWLINE INDENT func_loop_body DEDENT | FOR ID COLON TYPE LPAREN func_expr RPAREN COLON NEWLINE INDENT func_loop_body DEDENT | FOR ID COLON TYPE LPAREN func_expr COMMA func_expr RPAREN NEWLINE INDENT func_loop_body DEDENT | FOR ID COLON TYPE LPAREN func_expr COMMA func_expr RPAREN COLON NEWLINE INDENT func_loop_body DEDENT

physika.parser.p_func_loop_stmt_if(p)[source]

func_loop_stmt : IF condition COLON NEWLINE INDENT func_loop_body DEDENT

physika.parser.p_func_loop_stmt_if_else(p)[source]

func_loop_stmt : IF condition COLON NEWLINE INDENT func_loop_body DEDENT ELSE COLON NEWLINE INDENT func_loop_body DEDENT

physika.parser.p_func_loop_stmt_index_assign_nd(p)[source]

func_loop_stmt : ID LBRACKET loop_index_list RBRACKET EQUALS func_expr NEWLINE

physika.parser.p_func_loop_stmt_index_pluseq(p)[source]

func_loop_stmt : ID LBRACKET loop_index_list RBRACKET PLUSEQ func_expr NEWLINE

physika.parser.p_func_loop_stmt_pluseq(p)[source]

func_loop_stmt : ID PLUSEQ func_expr NEWLINE

physika.parser.p_func_power_factor(p)[source]

func_power : func_factor

physika.parser.p_func_power_neg(p)[source]

func_power : MINUS func_power

physika.parser.p_func_power_pow(p)[source]

func_power : func_factor POWER func_power

physika.parser.p_func_term_power(p)[source]

func_term : func_power

physika.parser.p_func_term_times(p)[source]

func_term : func_term TIMES func_power | func_term DIVIDE func_power | func_term INTDIV func_power | func_term MATMUL func_power

physika.parser.p_id_list(p)[source]

id_list : ID | id_list COMMA ID

physika.parser.p_import_list_multiple(p)[source]

import_list : import_list COMMA ID

physika.parser.p_import_list_single(p)[source]

import_list : ID

physika.parser.p_loop_index_list_multi(p)[source]

loop_index_list : loop_index_list COMMA func_expr

physika.parser.p_loop_index_list_single(p)[source]

loop_index_list : func_expr

physika.parser.p_loop_var_list_multi(p)[source]

loop_var_list : loop_var_list ID

physika.parser.p_loop_var_list_single(p)[source]

loop_var_list : ID

physika.parser.p_multi_index_list_base(p)[source]

multi_index_list : func_expr COMMA func_expr

physika.parser.p_multi_index_list_extend(p)[source]

multi_index_list : multi_index_list COMMA func_expr

physika.parser.p_params_empty(p)[source]

params :

physika.parser.p_params_multi(p)[source]

params : ID COLON type_spec COMMA params

physika.parser.p_params_single(p)[source]

params : ID COLON type_spec

physika.parser.p_program(p)[source]

program : statements

physika.parser.p_statement_assign(p)[source]

statement : ID EQUALS expr

physika.parser.p_statement_decl(p)[source]

statement : ID COLON type_spec EQUALS expr NEWLINE

physika.parser.p_statement_empty(p)[source]

statement : NEWLINE

physika.parser.p_statement_expr(p)[source]

statement : expr

physika.parser.p_statement_for(p)[source]

statement : FOR ID COLON NEWLINE INDENT for_body DEDENT

physika.parser.p_statement_for_range(p)[source]

statement : FOR ID COLON TYPE LPAREN func_expr RPAREN NEWLINE INDENT for_body DEDENT | FOR ID COLON TYPE LPAREN func_expr RPAREN COLON NEWLINE INDENT for_body DEDENT | FOR ID COLON TYPE LPAREN func_expr COMMA func_expr RPAREN NEWLINE INDENT for_body DEDENT | FOR ID COLON TYPE LPAREN func_expr COMMA func_expr RPAREN COLON NEWLINE INDENT for_body DEDENT

physika.parser.p_statement_function(p)[source]

statement : DEF ID LPAREN params RPAREN COLON type_spec COLON NEWLINE INDENT RETURN func_expr NEWLINE DEDENT

physika.parser.p_statement_function_body_only(p)[source]

statement : DEF ID LPAREN params RPAREN COLON type_spec COLON NEWLINE INDENT func_body_stmts DEDENT

physika.parser.p_statement_function_decl(p)[source]

statement : ID COLON FUNCTION NEWLINE

physika.parser.p_statement_function_multi_decl(p)[source]

statement : id_list COLON FUNCTION NEWLINE

physika.parser.p_statement_function_with_body(p)[source]

statement : DEF ID LPAREN params RPAREN COLON type_spec COLON NEWLINE INDENT func_body_stmts RETURN func_expr NEWLINE DEDENT

physika.parser.p_statement_function_with_loop(p)[source]

statement : DEF ID LPAREN params RPAREN COLON type_spec COLON NEWLINE INDENT func_init FOR ID COLON NEWLINE INDENT func_loop_body DEDENT NEWLINE RETURN func_expr DEDENT

physika.parser.p_statement_if_else(p)[source]

statement : IF condition COLON NEWLINE INDENT for_body DEDENT ELSE COLON NEWLINE INDENT for_body DEDENT

physika.parser.p_statement_if_only(p)[source]

statement : IF condition COLON NEWLINE INDENT for_body DEDENT

physika.parser.p_statement_import(p)[source]

statement : FROM ID IMPORT import_list NEWLINE

physika.parser.p_statement_index_assign(p)[source]

statement : ID LBRACKET ID RBRACKET EQUALS expr NEWLINE | ID LBRACKET NUMBER RBRACKET EQUALS expr NEWLINE

physika.parser.p_statement_index_assign_nd(p)[source]

statement : ID LBRACKET multi_index_list RBRACKET EQUALS expr NEWLINE

physika.parser.p_statement_symbol_decl(p)[source]

statement : ID COLON SYMBOL NEWLINE

physika.parser.p_statement_symbol_multi_decl(p)[source]

statement : id_list COLON SYMBOL NEWLINE

physika.parser.p_statements_multi(p)[source]

statements : statements statement

physika.parser.p_statements_single(p)[source]

statements : statement

physika.parser.p_statemet_equation_decl(p)[source]

statement : ID COLON EQUATION WALRUS func_expr EQUALS func_expr NEWLINE

physika.parser.p_term_binop(p)[source]

term : term TIMES factor | term DIVIDE factor | term MATMUL factor | term POWER factor

physika.parser.p_term_factor(p)[source]

term : factor

physika.parser.p_type_function(p)[source]

type_spec : TYPE ARROW TYPE

physika.parser.p_type_scalar(p)[source]

type_spec : TYPE

physika.parser.p_type_tangent(p)[source]

type_spec : TANGENT ID TYPE

physika.parser.p_type_tensor(p)[source]

type_spec : TYPE LBRACKET dimension_list RBRACKET

physika.type_checker

class physika.type_checker.TypeChecker(unified_ast: dict)[source]

Bases: object

Type checker for Physika programs.

The Hindley-Milner algorithm rests on two main steps:

Substitution performs a mapping {αN: concrete_type} from unknown type variables (TVar αN or TDim δN) to valid types. When unify determines that αN must equal some type T, it extends the substitution with the binding αN T. Calling s.apply(t) replaces every bound type variable in t with its mapped type, following chains of bindings until a concrete type is reached.

Unification (unify(t1, t2, s)) uses the accumulated version of s that makes s.apply(t1) == s.apply(t2). If either side is a free TVar, a new binding is added to s. If both sides are concrete types of the same constructor (arrays, matrices, tensors as TTensor types), their components are unified recursively. If the shapes are incompatible (e.g. vs. ℝ[3]), the mismatch is recorded as a type error.

Physika’s type checker performs three passes over the unified AST:

  1. Signature registration: All function and class signatures are stored in func_env and class_env before any body is examined. Class constructors are stored in func_env as (field_types, TInstance(name)).

  2. Body checking (check_function, check_class): For each def and class, infer_stmts walks statements in order, threading s through every expression to build a local type environment. The return expression is inferred and unified against the declared return type. A mismatch is recorded as an error prefixed with the function or class name.

  3. Program statement checking (check_statement): Top-level stmts nodes are checked in source order. The line number is read from the last element of each statement tuple and prepended to error messages.

Type mismatches are accumulated in self.errors as plain strings.

Parameters:

unified_ast (dict) – The unified AST dict produced by build_unified_ast(), with keys "functions", "classes", and "program".

Examples

>>> # Example 1
>>> # No errors
>>> from physika.type_checker import TypeChecker
>>> ast = {
...     "functions": {},
...     "classes": {},
...     "program": [("decl", "x", "ℝ", ("num", 1.0), 1)],
... }
>>> TypeChecker(ast).run()
[]
>>> # Example 2
>>> # function called with wrong number of arguments:
>>> fdef = {
...     "params": [("x", "ℝ"), ("y", "ℝ")],
...     "statements": [],
...     "body": ("add", ("var", "x"), ("var", "y")),
...     "return_type": "ℝ",
... }
>>> ast = {
...     "functions": {"add2": fdef},
...     "classes": {},
...     "program": [("expr", ("call", "add2", [("num", 1.0)]), 3)],
... }
>>> TypeChecker(ast).run()
["Line 3: Function 'add2' expects 2 args, got 1"]
run() list[str][source]

Run type inference over the full unified AST.

Three passes:

  1. Register all function and class signatures.

  2. Check function bodies

  3. Check class bodies.

  4. Check top-level statements.

Returns:

Accumulated type error messages. Empty if the program is well-typed.

Return type:

list[str]

Examples

>>> # No errors
>>> from physika.type_checker import TypeChecker
>>> ast = {
...     "functions": {},
...     "classes": {},
...     "program": [("decl", "x", "ℝ", ("num", 1.0), 1)],
... }
>>> TypeChecker(ast).run()
[]

physika.codegen

physika.codegen.from_ast_to_torch(unified_ast: Dict[str, Any], print_code: bool = True) str[source]

Convert a unified AST into a complete, executable Python/PyTorch source string.

This conversion is done in two passes:

  1. Analysis pass — walks the AST to determine which runtime.py helpers (solve, train, evaluate, compute_grad, simulate, animate, etc) are referenced, and collects variables used as grad() differentiation targets.

  2. Code-generation pass — uses generate_function, generate_class, and generate_statement (from utils.ast_utils) to emit Python source for each AST entry, preceded by import header.

The returned string is ready to be executed with exec().

Parameters:
  • unified_ast (Dict[str, Any]) –

    The unified AST dict produced by build_unified_ast(), with keys:

    • "functions"Dict[str, dict] mapping function names to their AST definitions (params, body, statements).

    • "classes"Dict[str, dict] mapping class names to their AST definitions (class_params, lambda_params, body, loss_body, …).

    • "program"List[tuple] of top-level statement AST nodes (decl, assign, expr, for_loop, func_def, class_def).

  • print_code (bool, default True) – If True, print the generated code.

Returns:

A complete Python/PyTorch source string containing import statements, function definitions, nn.Module class definitions, and program-level statements. Variables that appear as grad() targets are initialised with requires_grad=True.

Return type:

str

Examples

>>> # Example #1: simple expression
>>> unified_ast = {
...     "functions": {},
...     "classes": {},
...     "program": [("expr", ("num", 42.0), 1)],
... }
>>> code = from_ast_to_torch(unified_ast, print_code=False)
>>> "import torch" in code
True
>>> "physika_print(42.0)" in code
True
>>> print(code)
import torch
import torch.nn as nn
import torch.optim as optim

from physika.runtime import physika_print

# === Program ===
physika_print(42.0)
>>> # Example #2: function definition and call
>>> unified_ast = {
...     "functions": {
...         "f": {"params": [("x", "ℝ")], "body": ("call", "exp",
...         [("var", "x")]), "statements": []},
...     },
...     "classes": {},
...     "program": [("expr", ("call", "f", [("num", 1.0)]), 2)],
... }
>>> code = from_ast_to_torch(unified_ast, print_code=False)
>>> "def f(x):" in code
True
>>> "torch.exp" in code
True
>>> print(code)  # noqa: E501
import torch
import torch.nn as nn
import torch.optim as optim

from physika.runtime import physika_print

# === Functions ===
def f(x):
    return torch.exp(x if isinstance(x, torch.Tensor) else torch.tensor(float(x)))

# === Program ===
physika_print(f(1.0))

physika.runtime

physika.runtime.animate(func: Any, *args: Any) None[source]

Animate a scalar function over a time range.

Evaluates func at n_points evenly-spaced time values between time_min and time_max, numerically differentiates to obtain velocity, and displays an interactive animation.

If PyVista is installed an interactive 3-D scene is used (SPACE to pause, Q to quit). Otherwise falls back to a matplotlib FuncAnimation.

The last two (or three) positional args are interpreted as (time_min, time_max) or (time_min, time_max, n_points); everything before them is forwarded as extra arguments to func.

Parameters:
  • func (callable) – A callable (*extra_args, t) -> scalar where t is the time parameter and extra_args are any fixed arguments.

  • *args (Any) – Positional arguments laid out as [*extra_args, time_min, time_max] or [*extra_args, time_min, time_max, n_points]. n_points defaults to 200 when omitted.

Examples

>>> from physika.runtime import animate
>>> animate(harmonic_oscillator, 1.0, 0.0, 0.0, 10.0)
physika.runtime.compute_grad(f: Callable | torch.Tensor, x: float | torch.Tensor) torch.Tensor[source]

Compute the scalar gradient of a Physika expression with respect to x, where x is the function’s argument.

The first argument is overloaded: pass a callable when Physika uses a simple grad(f(x), x) pattern, or a pre-evaluated tensor when the expression is nested (e.g. grad(real(U(k,m,t,...)), t)).

Parameters:
  • f_or_output (callable or torch.Tensor) – A Physika function f or a computed output tensor f(x) whose gradient is requested.

  • x (float or torch.Tensor) – The scalar point at which to differentiate f. When f is a tensor, x must be a leaf with requires_grad=True that was used when the output was computed.

Returns:

Detached scalar df/dx evaluated at x.

Return type:

torch.Tensor

Examples

>>> from physika.runtime import compute_grad
>>> # callable form for grad(f(x), x)
>>> compute_grad(lambda t: t * t, torch.tensor(3.0))
tensor(6.)
>>> # pre-evaluated form for grad(real(U(..., t, ...)), t)
>>> x = torch.tensor(3.0, requires_grad=True)
>>> compute_grad(x * x, x)
tensor(6.)
physika.runtime.evaluate(model: torch.nn.Module, X: torch.Tensor, y: torch.Tensor) float[source]

Evaluate a trained model and return the mean per-sample loss.

Iterates over every sample (X[i], y[i]), computes the loss using the model’s loss() method (if defined) or MSE, and returns the average.

Parameters:
  • model (nn.Module) – The Physika nn.Module to evaluate.

  • X (torch.Tensor) – Input data of shape (n_samples, ...).

  • y (torch.Tensor) – Target data of shape (n_samples, ...).

Returns:

The mean loss across all samples.

Return type:

float

Examples

>>> from physika.runtime import evaluate
>>> avg_loss = evaluate(trained_model, X_test, y_test)
physika.runtime.physika_print(value: Any) None[source]

Pretty-print a Physika value with its inferred type annotation.

Converts PyTorch tensors, complex numbers, and Python scalars into a readable display form, infers the Physika type (e.g. , ℝ[3], ), and prints <value> <type>.

Parameters:

value (Any) – The value to print. Supported types include torch.Tensor, int, float, complex, list (nested), and nn.Module subclasses.

Examples

>>> physika_print(3.0)
3.0 ∈ ℝ
>>> physika_print(torch.tensor([1.0, 2.0, 3.0]))
[1.0, 2.0, 3.0] ∈ ℝ[3]
>>> physika_print(torch.tensor([[1.0, 2.0], [3.0, 4.0]]))
[[1.0, 2.0], [3.0, 4.0]] ∈ ℝ[2,2]
physika.runtime.simulate(model: torch.nn.Module, x0: Sequence[float] | torch.Tensor, nsteps: int | float, dt: float | torch.Tensor) None[source]

Simulate a dynamical system and visualise the trajectory.

Rolls out model for nsteps discrete time-steps starting from initial state x0 with step size dt:

x_{k+1} = model(x_k)

The resulting trajectory is plotted with matplotlib (time evolution and, for multi-dimensional states, a phase-space plot).

Parameters:
  • model (nn.Module) – A Physika nn.Module whose forward maps a state vector to the next state vector.

  • x0 (Sequence[float] or torch.Tensor) – The initial state, e.g. [theta_0, omega_0].

  • nsteps (int or float) – Number of simulation steps (cast to int internally).

  • dt (float or torch.Tensor) – Time-step size.

Examples

>>> from physika.runtime import simulate
>>> simulate(pendulum_model, [0.5, 0.0], 1000, 0.01)
physika.runtime.solve(*equations: str, **known_vars: float) tuple[torch.Tensor, ...][source]

Solve a system of linear equations for unknown variables.

Parses string equations of the form "var = expr" where expr is a linear combination of unknowns and known variables. Builds the coefficient matrix A and constant vector b, then solves Ax = b using torch.linalg.solve.

Unknowns are automatically detected: any variable on the right-hand side that is not in known_vars and is not a built-in (exp, sin, cos, sqrt, i) is treated as an unknown.

Parameters:
  • *equations (str) – One or more equation strings, each containing exactly one =. Example: "F1 = m * a1 + m * a2".

  • **known_vars (float) – Named values for known variables. Example: m=2.0, F1=10.0.

Returns:

A tuple of solved values, one per unknown, in sorted alphabetical order of the unknown variable names.

Return type:

tuple[torch.Tensor, …]

Examples

>>> from physika.runtime import solve
>>> x, = solve("y = 2 * x", y=6.0)
>>> float(x)
3.0
physika.runtime.train(model: torch.nn.Module, X: torch.Tensor, y: torch.Tensor, epochs: int | float, lr: float) torch.nn.Module[source]

Train a Physika model using SGD on per-sample loss.

Creates a deep copy of model so the original is not mutated, enables gradients on all parameters, then runs epochs of SGD. For each epoch every sample (X[i], y[i]) is passed through the model; if the model defines a loss(pred, target[, input]) method it is used, otherwise MSE (pred - y_i)**2 is the default.

Training progress is printed every epochs // 10 epochs and on the final epoch.

Parameters:
  • model (nn.Module) – The Physika nn.Module to train (will be deep-copied).

  • X (torch.Tensor) – Input data of shape (n_samples, ...).

  • y (torch.Tensor) – Target data of shape (n_samples, ...).

  • epochs (int or float) – Number of training epochs (cast to int internally).

  • lr (float) – Learning rate for optim.SGD.

Returns:

A new, trained copy of the model.

Return type:

nn.Module

Example

>>> from physika.runtime import train
>>> trained = train(model, X, y, 100, 0.01)

physika.utils.ast_utils

physika.utils.ast_utils.ast_to_torch_expr(node: ASTNode, indent: int = 0, current_loop_var: str | set[str] | None = None) str[source]

Convert an AST expression node to a PyTorch source code string.

Recursively translates a Physika AST subtree into a valid Python/PyTorch expression string. Handles arithmetic operators, array construction, indexing, slicing, function calls (mapping builtins like sin to torch.sin), and complex numbers.

This is the core of the string-codegen path used by generate_function, generate_class, and generate_statement.

Parameters:
  • node (ASTNode) – AST expression node (tagged tuple) or a scalar leaf.

  • indent (int, default 0) – Current indentation level. Reserved for future use.

  • current_loop_var (str or None, default None) – When set, an ("imaginary",) node whose loop variable is "i" will emit the loop variable name instead of torch.tensor(1j), disambiguating the complex unit from the loop index.

Returns:

A torch Python expression string corresponding to the given ASTNode.

Return type:

str

Examples

>>> from physika.utils.ast_utils import ast_to_torch_expr
>>> ast_to_torch_expr(("add", ("num", 1.0), ("var", "x")))
'(1.0 + x)'
>>> expected = (
...     "torch.sin(theta if isinstance(theta, torch.Tensor) "
...     "else torch.tensor(float(theta)))"
... )
>>> ast_to_torch_expr(("call", "sin", [("var", "theta")])) == expected
True
>>> ast_to_torch_expr(("array", [("num", 1.0), ("num", 2.0)]))
'torch.tensor([1.0, 2.0])'
physika.utils.ast_utils.ast_uses_func(node: tuple[Any, ...] | list[ASTNode] | str | int | float | None, func_name: str) bool[source]

Check whether an AST subtree contains a call to func_name.

Recursively walks node looking for both ("call", func_name, ...) and ("call_index", func_name, ..., idx) nodes. Used during calling of from_ast_to_torch to decide which runtime helpers need to be imported.

Parameters:
  • node (ASTNode) – A tagged tuple, list, or scalar leaf of an AST.

  • func_name (str) – The function identifier to search for (e.g. "train", "grad", "simulate").

Returns:

True if a matching call node exists anywhere in the subtree, False otherwise.

Return type:

bool

Examples

>>> from physika.utils.ast_utils import ast_uses_func
>>> ast_uses_func(("call", "train", [("var", "model")]), "train")
True
>>> ast_uses_func(("call_index", "grad", [("var", "H")], ("num", 0.0)), "grad")  # noqa: E501
True
>>> ast_uses_func(("add", ("num", 1.0), ("num", 2.0)), "train")
False
physika.utils.ast_utils.ast_uses_solve(node: tuple[Any, ...] | list[ASTNode] | str | int | float | None) bool[source]

Check whether an AST subtree contains a call to solve.

Recursively walks node looking for ("call", "solve", ...).

Parameters:

node (ASTNode) – A tagged tuple, list, or scalar leaf of an AST.

Returns:

True if a ("call", "solve", ...) node exists anywhere in the subtree, False otherwise.

Return type:

bool

Examples

>>> from physika.utils.ast_utils import ast_uses_solve
>>> ast_uses_solve(("call", "solve", [("var", "eq1"), ("var", "eq2")]))
True
>>> ast_uses_solve(("add", ("num", 1.0), ("var", "x")))
False
physika.utils.ast_utils.ast_uses_sympy(node: tuple[Any, ...] | list[ASTNode] | str | int | float | None) bool[source]

Check whether an AST subtree contains a Symbol or Function declaration.

Recursively walks node looking for ("symbol_decl", ...)` or ("function_decl", ...) nodes, which indicate that sympy is needed as a backed for symbolic math

Parameters:

node (ASTNode) – A tagged tuple, list, or scalar leaf of an AST.

Returns:

True if a ("symbol_decl", ...)` or `("function_decl", ...) node exists anywhere in the subtree, False otherwise.

Return type:

bool

Examples

>>> from physika.utils.ast_utils import ast_uses_sympy
>>> ast_uses_sympy(("symbol_decl", "x"))
True
>>> ast_uses_sympy(("function_decl", "u"))
True
>>> ast_uses_sympy(("num", "1.0"))
False
physika.utils.ast_utils.build_unified_ast(program_ast: list[tuple[Any, ...] | list[ASTNode] | str | int | float | None], symbol_table: dict[str, dict[str, Any]], print_ast: bool = False) dict[str, Any][source]

Build a unified AST combining definitions and program statements.

Merges the flat program_ast (list of statement tuples produced by the parser) with the symbol_table (function and class definitions accumulated during parsing) into a single dict with three sections: "functions", "classes", and "program".

Parameters:
  • program_ast (list[ASTNode]) – The list of top-level statement AST tuples returned by parser.parse().

  • symbol_table (dict[str, dict[str, Any]]) – The parser’s symbol table mapping names to {"type": "function"|"class", "value": ...} entries.

  • print_ast (bool, default False) – If True, print the unified AST to stdout for debugging.

Returns:

A dict with keys:

  • "functions"{name: func_def, ...}

  • "classes"{name: class_def, ...}

  • "program"[stmt, ...]

Return type:

dict[str, dict[str, ASTNode] | list[ASTNode]]

Examples

>>> from physika.utils.ast_utils import build_unified_ast
>>> ast = [("expr", ("num", 42.0), 1)]
>>> sym = {}
>>> unified = build_unified_ast(ast, sym)
>>> unified["program"]
[('expr', ('num', 42.0), 1)]
>>> unified["functions"]
{}
physika.utils.ast_utils.collect_grad_targets(node: tuple[Any, ...] | list[ASTNode] | str | int | float | None, targets: set[str]) None[source]

Collect variable names used as differentiation targets in grad() calls.

Recursively walks node looking for ("call", "grad", [output, input]) patterns and extracts the second argument (the differentiation variable) when it is a ("var", name) node. The collected names are added to targets so that generate_statement can initialise those variables with requires_grad=True.

Parameters:
  • node (ASTNode) – A tagged tuple, list, or scalar leaf of an AST.

  • targets (set[str]) – Mutable set to add target variable names into. Modified in place; not returned.

Examples

>>> from physika.utils.ast_utils import collect_grad_targets
>>> targets = set()
>>> stmt = ("expr", ("call", "grad", [("var", "H"), ("var", "t")]))
>>> collect_grad_targets(stmt, targets)
>>> targets
{'t'}
physika.utils.ast_utils.condition_to_expr(cond: ASTNode, current_loop_var: str | set[str] | None = None) str[source]

Convert a condition AST node to a Python boolean expression string.

Parameters:
  • cond (tuple[str, ...]) – A condition tuple like ("cond_eq", left, right).

  • current_loop_var (str or set, optional) – Active loop variable(s) for disambiguating the imaginary token i.

Returns:

A Python boolean expression (e.g. "n == 0.0").

Return type:

str

Examples

>>> from physika.utils.ast_utils import condition_to_expr
>>> condition_to_expr(("cond_eq", ("var", "n"), ("num", 0.0)))
'n == 0.0'
>>> condition_to_expr(("cond_lt", ("var", "x"), ("num", 1.0)))
'x < 1.0'
physika.utils.ast_utils.emit_body_stmts(stmts: list[ASTNode], indent_level: int, lines: list[str], known_vars: list[str], equation_vars: set[str], generate_solve_call: Callable[[ASTNode], str], scalar_only: bool = False, expr_fn=<function ast_to_torch_expr>, _equation_vars: set[str] | None = None) None[source]

Recursively emit Python code lines for a function body.

Converts a sequence of body_decl, body_assign, body_tuple_unpack, body_if_return, body_if_else_return, body_if_else, or body_if AST nodes into indented Python source lines and appends them to lines.

Parameters:
  • stmts (list[ASTNode]) – Sequence of body_decl, body_assign, body_tuple_unpack, body_if_return, body_if_else_return, body_if_else, or body_if AST tuples to emit. None entries are skipped.

  • indent_level (int) – Nesting depth. 1 if directly inside the function body, indent_level is 2 if inside an if/else branch, etc.). Each level adds four spaces.

  • lines (list[str]) – Output list; generated source lines are appended here.

  • known_vars (list[str]) – Running list of variable names in scope. Extended in place when new locals are declared.

  • equation_vars (set[str]) – Set of variable names bound to equation strings (used to exclude them from solve() keyword arguments). Updated in place.

  • generate_solve_call (Callable[[ASTNode], str]) – Callable that converts an expression AST to a Python string, expanding solve(...) calls with the current known_vars.

  • expr_fn (callable, optional) – Expression code-generator; defaults to ast_to_torch_expr.

  • _equation_vars (set, optional) – Internal — tracks variables bound to equation strings so they are excluded from solve() keyword arguments. Pass None (default) to create a fresh set for this call.

Examples

>>> from physika.utils.ast_utils import emit_body_stmts
>>> from physika.utils.ast_utils import ast_to_torch_expr
>>> lines = []
>>> known_vars = ["x"]
>>> equation_vars = set()
>>> emit_body_stmts(
...     [("body_assign", "y", ("mul", ("var", "x"), ("num", 2.0)))],
...     1, lines, known_vars, equation_vars, ast_to_torch_expr,
... )
>>> lines
['    y = (x * 2.0)']
physika.utils.ast_utils.emit_for_stmts(stmts: list[ASTNode], indent: int = 4, loop_var: str | set[str] | None = None) list[str][source]

Emit Python code for a top-level for-loop or if-else branch body.

Handles for_assign, for_pluseq, for_index_assign, for_call, and nested for_loop / for_loop_range nodes. Recurses for nested loops, increasing indentation by 4 spaces per level.

Parameters:
  • stmts (list[ASTNode]) – List of for_assign, for_pluseq, for_index_assign, for_call, for_loop or for_loop_range AST nodes.

  • indent (int) – Integer representing the whitespace in emitted line.

  • loop_var (str or None) – Enclosing loop variable name, forwarded to ast_to_torch_expr.

Returns:

Python code lines .

Return type:

list[str]

Examples

>>> from physika.utils.ast_utils import emit_for_stmts
>>> stmts = [("for_assign", "z", ("mul", ("var", "a"), ("var", "b")))]
>>> emit_for_stmts(stmts, 4)
['    z = (a * b)']
physika.utils.ast_utils.emit_func_loop_body(loop_body: list, indent_level: int, lines: list[str], loop_var) None[source]

Emit code lines for a list of func_loop_stmt AST nodes.

Recurse for nested loop_for_range, loop_if, and loop_if_else nodes, extending loop_var with each new inner variable.

ast_to_torch_expr resolves the imaginary-unit token i to the correct Python name instead of torch.tensor(1j).

Parameters:
  • loop_body (list[ASTNode]) – func_loop_stmt nodes. None entries are skipped. Supported tags: - loop_assign - loop_pluseq - loop_index_pluseq - loop_for_range - loop_if - loop_if_else

  • indent_level (int) – Current indentation depth. Each level adds 4 spaces.

  • lines (list[str]) – Output list. Source lines are appended.

  • loop_var (str or set[str]) – Active loop variable name(s). Grows as inner loops are entered.

physika.utils.ast_utils.generate_class(name: str, class_def: dict[str, tuple[Any, ...] | list[ASTNode] | str | int | float | None]) str[source]

Generate an nn.Module subclass from a class AST entry.

Translates a Physika class into a Python class string with __init__ (wrapping tensor params as nn.Parameter), forward (the lambda body, with optional loop), and an optional loss method. Class parameter references in the forward/loss bodies are rewritten to self.param via replace_class_params.

Parameters:
  • name (str) – The class identifier (e.g. "OneLayerNet").

  • class_def (dict[str, ASTNode]) –

    A dict from unified_ast["classes"] with keys:

    • "class_params" — list of (name, type) pairs.

    • "lambda_params" — list of (name, type) pairs.

    • "body" — forward return expression AST.

    • "has_loop" (optional) — whether forward contains a loop.

    • "loop_var", "loop_body" (optional) — loop details.

    • "has_loss", "loss_body", "loss_params" (optional).

Returns:

A multi-line Python source string containing the complete nn.Module subclass definition.

Return type:

str

Examples

>>> from physika.utils.ast_utils import generate_class
>>> class_def = {
...     "class_params": [("w", "ℝ")],
...     "lambda_params": [("x", "ℝ")],
...     "body": ("mul", ("var", "w"), ("var", "x")),
...     "has_loop": False, "has_loss": False,
... }
>>> code = generate_class("Linear", class_def)
>>> "class Linear(nn.Module):" in code
True
physika.utils.ast_utils.generate_function(name: str, func_def: dict[str, Any]) str[source]

Generate a Python/PyTorch function definition from a function AST.

Translates a Physika function (params, body statements, return expression) into a valid Python function definition string.

If the function body contains a solve() call, local known-variable tracking is used to pass all in-scope variables as keyword arguments to solve.

Parameters:
  • name (str) – The function identifier (e.g. "sigma", "U").

  • func_def (dict[str, ASTNode]) – A dict from unified_ast["functions"] with keys "params" (list of (name, type) pairs), "body" (return expression AST), and optionally "statements" (list of body statement ASTs).

Returns:

A multi-line Python source string containing the complete function definition.

Return type:

str

Examples

>>> from physika.utils.ast_utils import generate_function
>>> func_def = {
...     "params": [("x", "ℝ")],
...     "body": ("call", "exp", [("var", "x")]),
...     "statements": [],
... }
>>> print(generate_function("f", func_def)) # noqa: E501
def f(x):
    return torch.exp(x if isinstance(x, torch.Tensor) else torch.tensor(float(x)))
physika.utils.ast_utils.generate_statement(stmt: ASTNode, grad_target_vars: set[str]) str | None[source]

Generate a PyTorch code string for a program-level statement.

Handles decl (variable declaration), assign (reassignment), expr (bare expression — wrapped in physika_print unless it is a side-effect call like simulate/animate), for_loop, and skips func_def/class_def (already emitted by from_ast_to_torch).

Variables whose names appear in grad_target_vars are initialised with requires_grad=True so that grad() can differentiate through them.

Parameters:
  • stmt (ASTNode) – An AST statement tuple (e.g. ("decl", name, type, expr, lineno)) or None.

  • grad_target_vars (set[str]) – Variable names used as differentiation targets in grad() calls. Collected by collect_grad_targets during the analysis pass.

Returns:

A Python source string for the statement, or None if the statement should be skipped (func_def, class_def, or None input).

Return type:

str or None

Examples

>>> from physika.utils.ast_utils import generate_statement
>>> generate_statement(("decl", "x", "ℝ", ("num", 3.0), 1), set())
'x = 3.0'
>>> generate_statement(("decl", "t", "ℝ", ("num", 0.0), 2), {"t"})
't = torch.tensor(0.0, requires_grad=True)'
>>> generate_statement(("expr", ("var", "x"), 0), set())
'physika_print(x)'
physika.utils.ast_utils.replace_class_params(code: str, class_params: list[tuple[str, tuple[Any, ...] | list[ASTNode] | str | int | float | None]]) str[source]

Replace class parameter references with self.param in generated code.

Rewrites bare parameter names inside the generated forward and loss method bodies. Applies regex substitutions for three contexts: function calls (f( -> self.f(), array indexing (W[ -> self.W[), and standalone references inside parenthesised expressions.

Parameters:
  • code (str) – The generated Python source string to transform.

  • class_params (list[tuple[str, ASTNode]]) – List of (name, type_spec) pairs from the class definition. Only the names are used; type specs are ignored.

Returns:

A new string with class parameter names prefixed by self. in the appropriate syntactic contexts.

Return type:

str

Examples

>>> from physika.utils.ast_utils import replace_class_params
>>> replace_class_params("(W @ x + b)", [("W", "ℝ"), ("b", "ℝ")])
'(self.W @ x + self.b)'

physika.utils.parser_utils

physika.utils.parser_utils.find_indexed_arrays(ast: tuple[Any, ...] | list[ASTNode] | str | int | float | None, loop_var: str) list[str][source]

Collect names of arrays indexed by a given loop variable.

Recursively walks an AST subtree looking for ("index", name, idx) nodes where idx resolves to loop_var. This function is called during parse to infer the iteration count of for loops: the generated code iterates range(len(arr)) where arr is the first array found.

The index expression is matched against three representations the parser may produce for the same loop variable:

  • ("var", loop_var) — standard variable reference.

  • bare loop_var string — legacy / simplified form.

  • ("imaginary",) — when loop_var is "i" (the lexer emits the IMAGINARY token for the identifier i).

Parameters:
  • ast (ASTNode) – The AST subtree to search (typically a loop body statement or a list of statements).

  • loop_var (str) – The loop variable name to look for in index positions (e.g. "i", "k").

Returns:

Array name strings indexed by loop_var, in encounter order (may contain duplicates).

Return type:

list[str]

Examples

>>> from physika.utils.parser_utils import find_indexed_arrays
>>> stmt = ("loop_assign", "total",
...         ("add", ("var", "total"),
...                 ("index", "arr", ("var", "i"))))
>>> find_indexed_arrays(stmt, "i")
['arr']
>>> stmt2 = ("add", ("index", "X", ("imaginary",)),
...                 ("index", "y", ("imaginary",)))
>>> find_indexed_arrays(stmt2, "i")
['X', 'y']

physika.utils.print_utils

physika.utils.print_utils.print_type_check_results(type_errors: list[str]) None[source]

Print type-checking results and exit on errors.

If type_errors is non-empty, prints each error prefixed with a cross mark, shows a summary count, and terminates the process with sys.exit(1). If no errors are present, prints a success message.

Parameters:

type_errors (list[str]) – A list of human-readable error messages returned by TypeChecker.run() (or the type_check wrapper).

Examples

>>> from physika.utils.print_utils import print_type_check_results
>>> print_type_check_results([])
  ✓ No type errors found
physika.utils.print_utils.print_unified_ast(unified_ast: dict[str, Any]) str[source]

Return a pretty-printed string of the unified AST.

Formats the three sections — "functions", "classes", and "program" — with indentation for easy reading. Used for debugging with the print_ast=True flag in build_unified_ast.

Parameters:

unified_ast (dict[str, Any]) – The unified AST dict produced by build_unified_ast(), with keys "functions", "classes", and "program".

Returns:

A multi-line, indented string representation of the entire unified AST.

Return type:

str

Examples

>>> from physika.utils.ast_utils import build_unified_ast
>>> ast = {"functions": {}, "classes": {},
... "program": [("expr", ("num", 1.0), 1)]}
>>> print(print_unified_ast(ast))
Functions:

Classes:

Program:
  (
    'expr',
    ('num', 1.0),
    1,
  )

physika.utils.type_checker_utils

physika.utils.type_checker_utils.broadcast_op(t1: TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, t2: TVar | TDim | TScalar | TTensor | TFunc | TInstance | None) TVar | TDim | TScalar | TTensor | TFunc | TInstance | None[source]

Return the result type of a broadcast operation.

Used at infer_expr for +, -, *, and / to determine the output type. The shape of the result follows PyTorch broadcast. Tensor/Tensor shape compatibility is checked via unify.

Parameters:
  • t1 (Optional[Type]) – Type of the left operand. t1 can be None when unknown.

  • t2 (Optional[Type]) – Type of the right operand. t2 can be None when unknown.

Returns:

  • t1 if t2 is None and viceversa.

  • TTensor operand if exactly one side is a tensor.

  • TScalar when both operands are scalars.

Return type:

Optional[Type]

Examples

>>> from physika.utils.type_checker_utils import broadcast_op
>>> from physika.utils.types import T_REAL, TTensor
>>> broadcast_op(T_REAL, T_REAL)

>>> broadcast_op(T_REAL, TTensor(((3, "invariant"),)))
ℝ[3]
physika.utils.type_checker_utils.dims_compatible(d1: int | str, d2: int | str) bool[source]

Check if two dimension values are compatible.

Two dimensions are compatible if is a symbolic string (e.g. “M”, “N”) or if they are equal integers.

Parameters:
  • d1 (int or str) – First dimension.

  • d2 (int or str) – Second dimension.

Returns:

True if the dimensions are compatible.

Return type:

bool

Examples

>>> from physika.utils.type_checker_utils import dims_compatible
>>> dims_compatible(3, 3)
True
>>> dims_compatible("M", 16)
True
>>> dims_compatible("M", "M")
True
>>> dims_compatible("M", "N")
True
>>> dims_compatible(3, 4)
False
physika.utils.type_checker_utils.from_typespec(ts: Any) TVar | TDim | TScalar | TTensor | TFunc | TInstance | None[source]

Convert a type value from AST to a valid Type value.

Called between the parser and the type checker to translate the annotation stored in the AST into the typed objects that will be used at the inference step. Returns None for any annotation that cannot be mapped.

Parameters:

ts (Any) – A parser type-spec: None, "ℝ"/"R", "ℕ"/"N", "ℂ", or "string", ("tensor", [(dim, variance), ...]), ("func_type", param_types, ret_type), ("instance", name).

Returns:

The corresponding Type, or None if the spec is unrecognised.

Return type:

Optional[Type]

Examples

>>> from physika.utils.type_checker_utils import from_typespec
>>> from physika.utils.types import T_REAL, TTensor
>>> from_typespec("ℝ")

>>> from_typespec(("tensor", [(3, "invariant"), (4, "invariant")]))
ℝ[3,4]
>>> from_typespec(None) is None
True
physika.utils.type_checker_utils.get_line_info(stmt: ASTExpr) int | None[source]

Extract the source line number from a statement AST node.

Each statement type stores the line number at a different index. This function knows the layout for decl, assign, expr, and for_loop.

Parameters:

stmt (ASTExpr) – A program-level statement tuple, or None.

Returns:

The source line number, or None if it cannot be determined.

Return type:

int or None

Examples

>>> from physika.utils.type_checker_utils import get_line_info
>>> get_line_info(("decl", "x", "ℝ", ("num", 3.0), 7))
7
>>> get_line_info(("expr", ("num", 1.0), 3))
3
>>> get_line_info(None) is None
True
physika.utils.type_checker_utils.get_tensor_shape(t: TypeSpec) List[int] | None[source]

Extract the dimension list from a TTensor, or None if not a tensor.

Parameters:

t (Optional[Type]) – Any Type.

Returns:

List of dimension entries [d0, d1, ...] for a TTensor, or None for scalars, functions, instances, and None input.

Return type:

Optional[list]

Examples

>>> from physika.utils.type_checker_utils import get_tensor_shape
>>> from physika.utils.types import TTensor, TDim, T_REAL
>>> get_tensor_shape(TTensor(((3, "invariant"),)))
[3]
>>> get_tensor_shape(TTensor(((2, "invariant"), (4, "invariant"))))
[2, 4]
>>> get_tensor_shape(TTensor(((TDim("n"), "invariant"), (3, "invariant"))))
[n, 3]
>>> get_tensor_shape(T_REAL) is None
True
>>> get_tensor_shape(None) is None
True
physika.utils.type_checker_utils.make_tensor(dims: list) TTensor[source]

Construct a TTensor from a list of dimension values.

Wwrapper used by helper functions to avoid repeating the (d, "invariant") tuple each time a tensor is contructed during type checker.

Parameters:

dims (list) – Dimension inputs. Each entry may be an int, a symbolic str, or a TDim variable.

Returns:

A tensor type with each dimension tagged as "invariant".

Return type:

TTensor

Examples

>>> from physika.utils.type_checker_utils import make_tensor
>>> make_tensor([2, 3])
ℝ[2,3]
physika.utils.type_checker_utils.make_tensor_type(shape: list[int] | None) TypeSpec[source]

Build a tensor type spec from a shape list.

Parameters:

shape (list[int] or None) – Dimension sizes. None or empty list produces "ℝ" (scalar).

Returns:

"ℝ" for scalars, or ("tensor", [(d, "invariant"), ...]) for tensors.

Return type:

TypeSpec

Examples

>>> from physika.utils.type_checker_utils import make_tensor_type
>>> make_tensor_type(None)
'ℝ'
>>> make_tensor_type([3])
('tensor', [(3, 'invariant')])
>>> make_tensor_type([2, 3])
('tensor', [(2, 'invariant'), (3, 'invariant')])
physika.utils.type_checker_utils.matmul_op(t1: TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, t2: TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, add_error: Callable) TVar | TDim | TScalar | TTensor | TFunc | TInstance | None[source]

Return the result type of t1 @ t2.

Valid rank combinations:

  • ℝ[n] @ ℝ[n]

  • ℝ[..., m, n] @ ℝ[..., n, p]ℝ[..., m, p]

Mixed-rank operands are rejected. Use an explicit 2D shape. For example, ℝ[1,n] @ ℝ[n,p] instead of ℝ[n] @ ℝ[n,p]. add_error is called for any shape error.

Parameters:
  • t1 (Optional[Type]) – Type of the left operand.

  • t2 (Optional[Type]) – Type of the right operand.

  • add_error (Callable) – Receives a human-readable error string on mismatch.

Returns:

The inferred result type, or None if an operand type is unknown or the rank combination is invalid.

Return type:

Optional[Type]

Examples

>>> from physika.utils.type_checker_utils import matmul_op
>>> from physika.utils.types import TTensor, T_REAL
>>> errors = []
>>> matmul_op(TTensor(((2,"invariant"),(3,"invariant"))), TTensor(((3,"invariant"),(4,"invariant"))), errors.append)
ℝ[2,4]
>>> matmul_op(TTensor(((3,"invariant"),)), TTensor(((3,"invariant"),)), errors.append)

>>> # batched: ℝ[5,2,3] @ ℝ[5,3,4] → ℝ[5,2,4]
>>> matmul_op(TTensor(((5,"invariant"),(2,"invariant"),(3,"invariant"))), TTensor(((5,"invariant"),(3,"invariant"),(4,"invariant"))), errors.append)
ℝ[5,2,4]
>>> errors  # no errors yet
[]
>>> matmul_op(TTensor(((3,"invariant"),)), TTensor(((4,"invariant"),)), errors.append)  # noqa: E501

>>> errors
['Matmul inner dimension mismatch: ℝ[3] @ ℝ[4]; different dims 3 ≠ 4']
physika.utils.type_checker_utils.occurs_in(var: TVar | TDim, t: TVar | TDim | TScalar | TTensor | TFunc | TInstance) bool[source]

Check whether variable var appears anywhere inside type t: Type to prevent creating infinite types.

Before binding α0 = some_type, the unification step calls occurs_in(α0, some_type). if it returns True, the binding would create a circular structure (e.g. α0 = ℝ[α0]) and a TypeError is raised instead.

Parameters:
  • var (Union[TVar, TDim]) – The variable to search for.

  • t (Type) – The type to search within.

Returns:

True if var appears in t, False otherwise.

Return type:

bool

Examples

>>> from physika.utils.type_checker_utils import occurs_in
>>> from physika.utils.types import TVar, TDim, TTensor, T_REAL
>>> occurs_in(TVar("α0"), TVar("α0"))
True
>>> occurs_in(TVar("α0"), T_REAL)
False
>>> occurs_in(TDim("δ0"), TTensor(((TDim("δ0"), "invariant"),)))
True
physika.utils.type_checker_utils.shapes_broadcast_compatible(s1: List[int] | None, s2: List[int] | None, allow_scalar_broadcast: bool = False) Tuple[List[int] | None, bool][source]

Check whether two shapes are broadcast-compatible.

Parameters:
  • s1 (list[int] or None) – Shape of the left operand (None means scalar).

  • s2 (list[int] or None) – Shape of the right operand (None means scalar).

  • allow_scalar_broadcast (bool, default False) – If True, a scalar (None) is compatible with any tensor shape, and the tensor shape is returned as the result.

Returns:

A (result_shape, ok) pair. ok is True if the shapes are compatible; result_shape is the broadcasted shape, or None if incompatible.

Return type:

tuple[list[int] | None, bool]

Examples

>>> from physika.utils.type_checker_utils import shapes_broadcast_compatible  # noqa: E501
>>> shapes_broadcast_compatible([3], [3])
([3], True)
>>> shapes_broadcast_compatible(None, [3], allow_scalar_broadcast=True)
([3], True)
>>> shapes_broadcast_compatible([2], [3])
(None, False)
physika.utils.type_checker_utils.statement_check(op: str, stmt: Any, infer_type: Callable[[...], Any], add_error: Callable[[str], None], type_env: dict[str, Any], check_statement: Callable[[Any], None]) None[source]

Check a single program-level statement for type errors.

Dispatches on the statement tag (op) and validates declared types against inferred types. Called by TypeChecker.check_statement.

Parameters:
  • op (str) – The statement tag: "decl", "assign", "expr", or "for_loop".

  • stmt (ASTExpr) – The full statement AST tuple.

  • infer_type (InferFn) – Type inference callback (TypeChecker.infer_type).

  • add_error (ErrorFn) – Callback to record a type error message.

  • type_env (dict[str, TypeSpec]) – Program-level variable type environment (mutated for decl and assign).

  • check_statement (Callable[[ASTExpr], None]) – Recursive callback for checking nested statements (e.g. for_loop bodies).

Examples

>>> from physika.utils.type_checker_utils import statement_check
>>> env = {}
>>> statement_check("decl", ("decl", "x", "ℝ", ("num", 3.0), 1),
...                 lambda e, le=None: "ℝ", lambda m: None, env,
...                 lambda s: None)
>>> env["x"]
'ℝ'
physika.utils.type_checker_utils.type_infer(op: str, expr: ASTExpr, type_env: dict[str, TypeSpec], local_env: dict[str, TypeSpec] | None, add_error: ErrorFn, infer_type: InferFn, func_env: dict[str, tuple[list[TypeSpec], TypeSpec | None]], class_env: dict[str, dict[str, Any]]) TypeSpec[source]

Infer the Physika type of an AST expression by its tag.

This is the main dispatch function called by TypeChecker.infer_type. It pattern-matches on the expression tag (op) and recursively infers operand types using infer_type (which is TypeChecker.infer_type itself, passed as a callback).

Handles: num, var, array, index, slice, add/sub, mul, div, matmul, pow, neg, call, call_index, string, imaginary.

Parameters:
  • op (str) – The expression tag (first element of the AST tuple).

  • expr (ASTExpr) – The full AST expression tuple.

  • type_env (dict[str, TypeSpec]) – Program-level variable type environment.

  • local_env (dict[str, TypeSpec] or None) – Local (function-scope) type environment.

  • add_error (ErrorFn) – Callback to record a type error message.

  • infer_type (InferFn) – Recursive type inference callback (TypeChecker.infer_type).

  • func_env (dict[str, tuple[list[TypeSpec], TypeSpec | None]]) – Registered function signatures {name: (param_types, return_type)}.

  • class_env (dict[str, dict[str, Any]]) – Registered class AST definitions.

Returns:

The inferred type, or None if it cannot be determined.

Return type:

TypeSpec

Examples

>>> from physika.utils.type_checker_utils import type_infer
>>> type_infer("num", ("num", 3.0), {}, {}, lambda m: None,
...            lambda e, le=None: "ℝ", {}, {})
'ℝ'
physika.utils.type_checker_utils.type_promotion(t1: TScalar, t2: TScalar) TScalar[source]

Promote two scalar numeric types to correct output type.

Parameters:
  • t1 (Type) – First scalar type.

  • t2 (Type) – Second scalar type.

Returns:

The promoted type with highest numeric priority.

Return type:

Type

Examples

>>> from physika.utils.type_checker_utils import type_promotion
>>> from physika.utils.types import T_REAL, T_COMPLEX, T_NAT
>>> type_promotion(T_REAL, T_COMPLEX)

>>> type_promotion(T_REAL, T_NAT)

>>> type_promotion(T_NAT, T_COMPLEX)

physika.utils.type_checker_utils.type_to_str(t: Any) str[source]

Convert a Physika type spec to a readable string.

Parameters:

t (TypeSpec) – A Physika type: "ℝ", "ℕ", "ℂ", ("tensor", [(dim, variance), ...]), ("func_type", input, output), or None.

Returns:

A readable representation, e.g. "ℝ", "ℝ[3]", "ℝ[2,3]", "(ℝ) ℝ", or "unknown" for None.

Return type:

str

Examples

>>> from physika.utils.type_checker_utils import type_to_str
>>> type_to_str("ℝ")
'ℝ'
>>> type_to_str(("tensor", [(3, "invariant")]))
'ℝ[3]'
>>> type_to_str(("tensor", [(2, "invariant"), (3, "invariant")]))
'ℝ[2,3]'
>>> type_to_str(None)
'unknown'
physika.utils.type_checker_utils.types_compatible(t1: Any, t2: Any) bool[source]

Check whether two Physika types are compatible.

Two types are compatible if either is None (unknown), they are equal, both are numeric scalars ("ℝ" / "ℕ"), or they are tensors with the same shape.

Parameters:
  • t1 (TypeSpec) – First type.

  • t2 (TypeSpec) – Second type.

Returns:

True if the types are compatible, False otherwise.

Return type:

bool

Examples

>>> from physika.utils.type_checker_utils import types_compatible
>>> types_compatible("ℝ", "ℝ")
True
>>> types_compatible("ℝ", "ℕ")
True
>>> types_compatible("ℝ", ("tensor", [(3, "invariant")]))
True
>>> types_compatible(None, "ℝ")
True
physika.utils.type_checker_utils.unify(t1: TVar | TDim | TScalar | TTensor | TFunc | TInstance, t2: TVar | TDim | TScalar | TTensor | TFunc | TInstance, s: Substitution) Substitution[source]

Return an extended Substitution that makes t1 equal to t2.

Unification step is used at type checker inference. Both types, t1 and t2, are resolved through s: Substitution so that known equalities are applied before comparing. The function then dispatches on the structural shape of the two types and either returns s unchanged (already equal), extends s with a new binding (one side is a variable), or recurses into sub-types (tensors, functions). A TypeError is raised for any mismatch.

Parameters:
  • t1 (Type) – First type to unify.

  • t2 (Type) – Second type to unify.

  • s (Substitution) – Current substitution accumulator. Both t1 and t2 are resolved through s before comparison.

Returns:

An extended substitution of s.

Return type:

Substitution

Raises:

TypeError – If t1 and t2 cannot be made equal. For example, scalar with tensor types, tensor types with rank mismatch, concrete dimension mismatch, or incompatible class instances.

Examples

>>> from physika.utils.type_checker_utils import unify
>>> from physika.utils.types import Substitution, TVar, T_REAL, T_NAT
>>> s = unify(TVar("α0"), T_REAL, Substitution()) # binds α0 to ℝ
>>> s
{'α0': ℝ}
>>> s.apply(TVar("α0"))

>>> unify(T_REAL, T_REAL, Substitution()) # no new bindings, already equal
{}
>>> unify(T_REAL, T_NAT, Substitution())  # ℕ ⊂ ℝ — no new bindings needed
{}
physika.utils.type_checker_utils.unify_dim(d1: Any, d2: Any, s: Substitution) Substitution[source]

Unify two tensor dimension entries and return an extended substitution.

Similar to unify but at dimension level, used when two TTensor types are unified. Dimension entries may be concrete integers (3) , symbolic strings ("n"), or unresolved variables (TVar / TDim).

Parameters:
  • d1 (Any) – First dimension entry: int, str, TVar, or TDim.

  • d2 (Any) – Second dimension entry: int, str, TVar, or TDim.

  • s (Substitution) – Current substitution accumulator; both entries are resolved through it before comparison.

Returns:

An extended substitution under which d1 and d2 are equal.

Return type:

Substitution

Raises:

TypeError – If both entries are concrete values (int or str) that differ.

Examples

>>> from physika.utils.type_checker_utils import unify_dim
>>> from physika.utils.types import Substitution, TDim
>>> s = unify_dim(TDim("δ0"), 4, Substitution())
>>> s
{'δ0': 4}
>>> s.apply_dim(TDim("δ0"))
4
>>> unify_dim(3, 3, Substitution())  # same concrete size, no new bindings
{}

physika.utils.infer_expr

class physika.utils.infer_expr.ExprContext(env: dict, s: Substitution, func_env: dict, class_env: dict, add_error: Callable)[source]

Bases: object

Data class represeting the context in which an expression is being inferred.

Passed to every expr_* handler so each handler receives the full typing environment.

env

Maps variable names to their current Type.

Type:

dict

s

The substitution dictionary accumulated so far. Handler functions thread s so each step registers any bindings made by previous steps.

Type:

Substitution

func_env

Maps user defined function names to (param_types, return_type).

Type:

dict

class_env

Maps class names to their definition dicts (class_params, return_type, …).

Type:

dict

add_error

Error callback.

Type:

Callable

Examples

>>> from physika.utils.infer_expr import ExprContext, T_REAL
>>> from physika.utils.types import Substitution
>>> errors = []
>>> ctx = ExprContext(env={"x": T_REAL}, s=Substitution(), func_env={}, class_env={}, add_error=errors.append)  # noqa: E501
>>> ctx.env
{'x': ℝ}
>>> ctx.s
{}
physika.utils.infer_expr.expr_add_sub(node: Any, ctx: ExprContext) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

Infer the result type of an addition or subtraction t1 +/− t2.

Both operand types are inferred with the substitution, so bindings made while inferring the left operand are visible when inferring the right. For two tensor operands their shapes are unified to catch mismatches. The result shape equals the unified shape. When one operand is scalar () and the other is a tensor, broadcasting applies and the tensor shape is returned.

Parameters:
  • node (ASTNode) – AST node of the form ("add", left_expr, right_expr) or ("sub", left_expr, right_expr).

  • ctx (ExprContext) – Current inference context. ctx.s is threaded through both operand inferences and updated with any new unification bindings. Shape mismatch errors are registered via ctx.add_error.

Returns:

(tensor_type, s) when either operand is a tensor, the tensor shape is the broadcast result (unified shape for tensor+tensor and tensor shape for tensor+scalar). (T_REAL, s) when both operands are scalars. (None, s) when both operand types could not be determined.

Return type:

tuple[Optional[Type], Substitution]

Examples

>>> from physika.utils.infer_expr import ExprContext, expr_add_sub, T_REAL, TTensor
>>> from physika.utils.types import Substitution
>>> ctx = ExprContext({"x": TTensor(((3, "invariant"),)), "y": TTensor(((3, "invariant"),))}, Substitution(), {}, {}, [].append)
>>> t, _= expr_add_sub(("add", ("var", "x"), ("var", "y")), ctx)  # ℝ[3] + ℝ[3] → ℝ[3]
>>> t
ℝ[3]
>>> t, _= expr_add_sub(("add", ("var", "x"), ("num", 1.0)), ctx)  # ℝ[3] + ℝ → ℝ[3]
>>> t
ℝ[3]
>>> t, _= expr_add_sub(("sub", ("num", 1.0), ("num", 2.0)), ctx)  # ℝ - ℝ → ℝ  # noqa: E501
>>> t

physika.utils.infer_expr.expr_array(node: Any, ctx: ExprContext) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

Infer the type of an array element [e0, e1, ..., en].

All elements are inferred and unified pairwise. A type error is reported if any two elements have incompatible types. An empty literal produces ℝ[0].

Parameters:
  • node (ASTNode) – AST node of the form ("array", elements) where elements is a list of AST expression nodes, one for each array element.

  • ctx (ExprContext) – Current inference context. ctx.s is threaded through each element inference so later elements see bindings from earlier ones. Type errors are registered via ctx.add_error.

Returns:

(TTensor(dims), updated_s) where dims[0] is the number of elements and any additional dims come from the element type (nested arrays). Returns (make_tensor([0]), ctx.s) for an empty literal.

Return type:

tuple[Optional[Type], Substitution]

Examples

>>> from physika.utils.infer_expr import ExprContext, expr_array
>>> from physika.utils.types import Substitution
>>> ctx = ExprContext({}, Substitution(), {}, {}, [].append)
>>> t, _= expr_array(("array", [("num", 1.0), ("num", 2.0), ("num", 3.0)]), ctx)
>>> t
ℝ[3]
>>> t, _= expr_array(("array", []), ctx)  # empty literal
>>> t
ℝ[0]
>>> nested = [("array", [("num", 1.0), ("num", 2.0)]), ("array", [("num", 3.0), ("num", 4.0)])]  # noqa: E501
>>> t, _= expr_array(("array", nested), ctx)  # ℝ[2,2]
>>> t
ℝ[2,2]
physika.utils.infer_expr.expr_call(node: Any, ctx: ExprContext) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

Infer the result type of a function call.

Resolution order:

  1. Built-in elementwise functions (exp, sin, cos, sqrt, abs, tanh, log, real, imag) — preserve the shape of their first argument.

  2. Built-in reduction (sum) → .

  3. grad(f, x) → same shape as x.

  4. User-defined functions in func_env — arity and argument types are checked; each argument is unified against its declared parameter type; the declared return type is returned.

Parameters:
  • node (ASTNode) – AST node of the form ("call", func_name, arg_list) where func_name is a str and arg_list is a list of expression nodes.

  • ctx (ExprContext) – Current inference context. ctx.s is threaded through all argument inferences and updated with unification bindings from parameter matching. Arity and type mismatch errors are reported via ctx.add_error.

Returns:

(result_type, s) where result_type depends on the solved path and type. (None, s) when the call target is unrecognised.

Return type:

tuple[Optional[Type], Substitution]

Examples

>>> from physika.utils.infer_expr import ExprContext, expr_call, T_REAL, TTensor  # noqa: E501
>>> from physika.utils.types import Substitution
>>> ctx = ExprContext({"x": TTensor(((3, "invariant"),))}, Substitution(), {}, {}, [].append)  # noqa: E501
>>> t, _= expr_call(("call", "sin", [("var", "x")]), ctx)  # element-wise
>>> t
ℝ[3]
>>> t, _= expr_call(("call", "sum", [("var", "x")]), ctx)  # ℝ
>>> t

>>> t, _= expr_call(("call", "grad", [("num", 1.0), ("var", "x")]), ctx)
>>> t
ℝ[3]
>>> func_env = {"f": ([TTensor(((3, "invariant"),))], TTensor(((3, "invariant"),)))}  # noqa: E501
>>> ctx2 = ExprContext({"x": TTensor(((3, "invariant"),))}, Substitution(), func_env, {}, [].append)  # noqa: E501
>>> t, _= expr_call(("call", "f", [("var", "x")]), ctx2)
>>> t
ℝ[3]
physika.utils.infer_expr.expr_chain_index(node: Any, ctx: ExprContext) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

Infer the type of a chained index expression A[i][k].

The inner expression (A[i]) is inferred first, yielding an intermediate tensor type. This handler then looks one more leading dimension from that result. A[i][k] is equivalent to A[i, k] for 2-D arrays.

Parameters:
  • node (ASTNode) – AST node of the form ("chain_index", inner_expr) where inner_expr is an expression node.

  • ctx (ExprContext) – Current inference context passed unchanged to the inner expression inference. The returned substitution reflects any new bindings produced while inferring inner_expr.

Returns:

  • tuple[Optional[Type], Substitution](None, s) when the inner expression itself failed to type-check. (T_REAL, s) when the inner expression is 0-D or 1-D (scalar result). (TTensor(inner_shape[1:]), s) when the inner expression has rank > 1.

  • Raises (via add_error)

  • ———————-

    • Inner expression typed as a scalar (T_REAL) — chaining [k] – onto a scalar is over-indexing.

Examples

>>> from physika.utils.infer_expr import ExprContext, expr_chain_index, TTensor
>>> from physika.utils.types import Substitution
>>> ctx = ExprContext({"A": TTensor(((3, "invariant"), (4, "invariant")))}, Substitution(), {}, {}, [].append)  # noqa: E501
>>> inner = ("index", "A", ("num", 0.0))  # A[0] → ℝ[4]
>>> t, _= expr_chain_index(("chain_index", inner), ctx)  # A[0][k] → ℝ
>>> t

physika.utils.infer_expr.expr_complex(node: Any, ctx: ExprContext) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

The type of a complex literal is always .

Parameters:
  • node (tuple) – AST node of the form ("complex", value) where value is an complex.

  • ctx (ExprContext) – Current inference context.

Returns:

Always (T_COMPLEX, ctx.s).

Return type:

tuple[Type, Substitution]

Examples

>>> from physika.utils.infer_expr import ExprContext, expr_complex
>>> from physika.utils.infer_expr import T_COMPLEX
>>> from physika.utils.types import Substitution
>>> ctx = ExprContext({}, Substitution(), {}, {}, [].append)
>>> t, _= expr_complex(("complex", 3j), ctx)
>>> t

physika.utils.infer_expr.expr_cond(node, ctx)[source]

Infer the type of a comparison condition expression.

Handles six comparison elements:
  • cond_eq

  • cond_neq

  • cond_lt

  • cond_gt

  • cond_leq

  • cond_geq

Condition expression nodes have the form:

("cond_gt", left_expr, right_expr)

Parameters:
  • node (tuple) – AST node of the form (op, left, right) where op is one of the six comparison tags.

  • ctx (ExprContext) – Current inference context.

Returns:

(t, s) where t is the inferred type of operand (T_REAL as a safe fallback). Reports an error when the two operand types differ so type checker can continue.

Return type:

tuple[Type | None, Substitution]

physika.utils.infer_expr.expr_div(node: Any, ctx: ExprContext) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

Infer the result type of division t1 / t2.

Valid cases:

  • tensor / scalar broadcasts and result has the shape of t1.

  • scalar / scalar result is .

  • tensor / tensor must match dimensions for element-wise operation, else a type error is reported.

Parameters:
  • node (ASTNode) – AST node of the form ("div", numerator_expr, denominator_expr).

  • ctx (ExprContext) – Current inference context. ctx.s is threaded through both operand inferences and updated with any new unification bindings. Shape mismatch errors are registered via ctx.add_error.

Returns:

(unified_tensor_type, s) for tensor / tensor. (tensor_type, s) for tensor / scalar (broadcast). (T_REAL, s) for scalar / scalar. (None, s) when the numerator type could not be determined.

Return type:

tuple[Optional[Type], Substitution]

Examples

>>> from physika.utils.infer_expr import ExprContext, expr_div, TTensor
>>> from physika.utils.types import Substitution
>>> ctx = ExprContext({"x": TTensor(((3, "invariant"),)), "y": TTensor(((3, "invariant"),))}, Substitution(), {}, {}, [].append)
>>> t, _= expr_div(("div", ("var", "x"), ("num", 2.0)), ctx)  # ℝ[3] / ℝ → ℝ[3]
>>> t
ℝ[3]
>>> t, _= expr_div(("div", ("num", 6.0), ("num", 2.0)), ctx)  # ℝ / ℝ → ℝ
>>> t

>>> t, _= expr_div(("div", ("var", "x"), ("var", "y")), ctx)  # ℝ[3] / ℝ[3] → ℝ[3]
>>> t
ℝ[3]
>>> errors = []
>>> ctx2 = ExprContext({"x": TTensor(((3, "invariant"),)), "z": TTensor(((2, "invariant"),))}, Substitution(), {}, {}, errors.append)  # noqa: E501
>>> t, _= expr_div(("div", ("var", "x"), ("var", "z")), ctx2)  # ℝ[3] / ℝ[2] → error
>>> errors
['Shape mismatch in div: ℝ[3] vs ℝ[2]']
physika.utils.infer_expr.expr_for_expr(node: Any, ctx: ExprContext, new_dim: Callable[[], TDim]) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

Infer the type of for-expression for i : ℕ(n) body.

The loop variable i is bound as inside the body. The result type has explicit ℕ(n) size as its leading dimension. When the body is itself a tensor (implicit for loops) the result is same rank. Nested for-exprs produce multi-dimensional tensors.

Parameters:
  • node (ASTNode) – AST node of the form ("for_expr", loop_var, size_expr, body_expr) where size_expr is typically ("num", n) literal, and body_expr is the body expression.

  • ctx (ExprContext) – Current inference context. The body is inferred with an extended environment that passes loop_var as . ctx.s is threaded through the size and body inferences.

  • new_dim (Callable[[], TDim]) – Fresh symbolic dimension variables, used when size_expr is not a numeric literal.

Returns:

(TTensor(((n, "invariant"),) + body_dims), s) when the body has a tensor type, prepends the outer ℕ(n) dimension. (TTensor(((n, "invariant"),)), s) when the body is scalar (). The leading dimension is a concrete int for literal sizes or a fresh TDim for dynamic sizes.

Return type:

tuple[Optional[Type], Substitution]

Examples

>>> from physika.utils.infer_expr import ExprContext, expr_for_expr
>>> from physika.utils.types import Substitution, new_dim
>>> ctx = ExprContext({}, Substitution(), {}, {}, [].append)
>>> t, _= expr_for_expr(("for_expr", "i", ("num", 3.0), ("imaginary")), ctx, new_dim)  #  ℝ[3]  # noqa: E501
>>> t
ℝ[3]
>>> inner_body = ("array", [("num", 1.0), ("num", 2.0)])  #  ℝ[2]
>>> t, _= expr_for_expr(("for_expr", "i", ("num", 4.0), inner_body), ctx, new_dim)  # ℝ[4,2]  # noqa: E501
>>> t
ℝ[4,2]
physika.utils.infer_expr.expr_for_expr_range(node: Any, ctx: ExprContext, new_dim: Callable[[], TDim]) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

Infer the type of a range for-expression for i : ℕ(start, end) body

Is similar to expr_for_expr but the size is derived from numeric start and end bounds. When bounds are non literal, a fresh symbolic dimension is introduced.

Parameters:
  • node (ASTNode) – AST node of the form ("for_expr_range", loop_var, start_expr, end_expr, body_expr) where start_expr and end_expr are num nodes (("num", value)).

  • ctx (ExprContext) – Current inference context. The body is inferred with an extended environment that passes loop_var as . ctx.s is threaded through the body inference.

  • new_dim (Callable[[], TDim]) – Fresh symbolic dimension variables, used when start_expr or end_expr is not a numeric literal.

Returns:

(TTensor(((end - start, "invariant"),) + body_dims), s) when both bounds are literals and the body has a tensor type. (TTensor(((end - start, "invariant"),)), s) when both bounds are literals and the body is scalar. (TTensor(((TDim, "invariant"), ...)), s) when either bound is dynamic, a fresh symbolic dimension replaces the outer size.

Return type:

tuple[Optional[Type], Substitution]

Examples

>>> from physika.utils.infer_expr import ExprContext, expr_for_expr_range
>>> from physika.utils.types import Substitution, new_dim
>>> ctx = ExprContext({}, Substitution(), {}, {}, [].append)
>>> t, _= expr_for_expr_range(("for_expr_range", "i", ("num", 0.0), ("num", 4.0), ("imaginary")), ctx, new_dim)
>>> t  # ℝ[4]
ℝ[4]
>>> inner_body = ("array", [("num", 0.0), ("num", 1.0), ("num", 2.0)])  # body produces ℝ[3]
>>> t, _= expr_for_expr_range(("for_expr_range", "k", ("num", 0.0), ("num", 2.0), inner_body), ctx, new_dim)  # noqa: E501
>>> t  # ℝ[2,3]
ℝ[2,3]
physika.utils.infer_expr.expr_imaginary(node: Any, ctx: ExprContext) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

Infer the type of the imaginary unit i.

Inside for i : Fin(n) body i is bound as a loop index (). But at the top level it is the imaginary unit .

Parameters:
  • node (tuple) – AST node ("imaginary",).

  • ctx (ExprContext) – Current inference context. When "i" is present in ctx.env the loop variable shadows the imaginary unit.

Returns:

(T_REAL, ctx.s) when "i" is a live loop variable; (T_COMPLEX, ctx.s) otherwise.

Return type:

tuple[Type, Substitution]

Examples

>>> from physika.utils.infer_expr import ExprContext, expr_imaginary, T_REAL, T_COMPLEX
>>> from physika.utils.types import Substitution
>>> ctx = ExprContext({}, Substitution(), {}, {}, [].append)
>>> t, _= expr_imaginary(("imaginary",), ctx)
>>> t

>>> ctx_loop = ExprContext({"i": T_REAL}, Substitution(), {}, {}, [].append)  # noqa: E501
>>> t, _= expr_imaginary(("imaginary",), ctx_loop)  # loop var shadows ℂ
>>> t

physika.utils.infer_expr.expr_index(node: Any, ctx: ExprContext) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

Infer the type of a 1D index expression arr[idx] from arr’s shape.

A indexed 1D array produces . A higher rank tensor produces a tensor of the remaining dimensions. The index expression is itself inferred and unified against the leading dimension to propagate any symbolic dim bindings. Errors are reported via ctx.add_error when:

  • arr is not in scope — returns (None, ctx.s).

  • arr is a scalar (cannot be indexed).

  • The index type is incompatible with the leading dimension size.

Parameters:
  • node (tuple) – AST node of the form ("index", arr_name, idx_expr) where idx_expr is any expression node whose type will be unified with the leading dimension.

  • ctx (ExprContext) – Current inference context. If arr_name is not in ctx.env, we get a None` result. ctx.s is updated with any new dim bindings produced by unifying the index type.

Returns:

(T_REAL, s) when the array is 1D (scalar element). (TTensor(shape[1:]), s) when the array has rank > 1 (row slice). (None, ctx.s) when arr_name is not in scope.

Return type:

tuple[Optional[Type], Substitution]

Examples

>>> from physika.utils.infer_expr import ExprContext, expr_index, TTensor
>>> from physika.utils.types import Substitution
>>> ctx = ExprContext({"v": TTensor(((3, "invariant"),))}, Substitution(), {}, {}, [].append)  # noqa: E501
>>> t, _= expr_index(("index", "v", ("num", 0.0)), ctx)  # ℝ[3][0] → ℝ
>>> t

>>> ctx2 = ExprContext({"A": TTensor(((3, "invariant"), (4, "invariant")))}, Substitution(), {}, {}, [].append)  # noqa: E501
>>> t, _= expr_index(("index", "A", ("num", 0.0)), ctx2)  # ℝ[3,4][0] → ℝ[4]  # noqa: E501
>>> t
ℝ[4]
physika.utils.infer_expr.expr_indexN(node: Any, ctx: ExprContext) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

Infer the type of a nD index expression arr[i0, i1, ...].

A generalisation of ‘’expr_index’’ to an arbitrary number of indices. Each index expression is inferred and unified against the corresponding leading dimension of arr. The visited dimensions are stripped from the front of the shape.

Fully indexing an ND tensor produces and partial indexing produces a lower rank tensor than the original.

Parameters:
  • node (Any) – AST node of the form ("indexN", arr_name, idx_exprs) idx_exprs is a list of expression nodes.

  • ctx (ExprContext) – Current inference context. ctx.env must contain arr_name. ctx.s is threaded through each index inference and updated with any new dimension bindings from unification.

Returns:

(TTensor(shape[n_idx:]), s) for partial indexing — when the number of indices is strictly less than the tensor rank, yielding a lower-rank tensor of the remaining dimensions. (T_REAL, s) when fully indexed (index count equals rank). (None, s) when arr_name is not in scope, resolves to a scalar, or is over-indexed (index count exceeds rank); an error is reported via ctx.add_error in the over-indexed case.

Return type:

tuple[Optional[Type], Substitution]

Examples

>>> from physika.utils.infer_expr import ExprContext, expr_indexN, TTensor
>>> from physika.utils.types import Substitution
>>> ctx = ExprContext({"T": TTensor(((2, "invariant"), (3, "invariant"), (4, "invariant")))}, Substitution(), {}, {}, [].append)
>>> t, _= expr_indexN(("indexN", "T", [("num", 0.0), ("num", 1.0)]), ctx)  # ℝ[2,3,4][0,1] → ℝ[4]  # noqa: E501
>>> t
ℝ[4]
>>> t, _= expr_indexN(("indexN", "T", [("num", 0.0), ("num", 1.0), ("num", 2.0)]), ctx)  # fully indexed → ℝ  # noqa: E501
>>> t

physika.utils.infer_expr.expr_matmul(node: Any, ctx: ExprContext) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

Infer the result type of matrix multiplication t1 @ t2.

matmul_op handles supported rank combinations. The inner dimensions must agree and a type error is reported otherwise.

Parameters:
  • node (ASTNode) – AST node of the form ("matmul", left_expr, right_expr).

  • ctx (ExprContext) – Current inference context. ctx.s is threaded through both operand inferences and updated with any new substitution bindings. Shape incompatibility errors are reported via ctx.add_error.

Returns:

(T_REAL, s) for a dot product (vector @ vector). (TTensor([m, p]), s) for a mat-mat product (ℝ[m,n] @ ℝ[n,p]). (None, s) when either operand type could not be determined or shapes are incompatible.

Return type:

tuple[Optional[Type], Substitution]

Examples

>>> from physika.utils.infer_expr import ExprContext, expr_matmul, TTensor
>>> from physika.utils.types import Substitution
>>> env = {"A": TTensor(((2, "invariant"), (3, "invariant"))), "B": TTensor(((3, "invariant"), (4, "invariant")))}  # noqa: E501
>>> ctx = ExprContext(env, Substitution(), {}, {}, [].append)
>>> t, _= expr_matmul(("matmul", ("var", "A"), ("var", "B")), ctx)  # ℝ[2,3] @ ℝ[3,4] → ℝ[2,4]
>>> t
ℝ[2,4]
>>> env2 = {"u": TTensor(((3, "invariant"),)), "v": TTensor(((3, "invariant"),))}  # noqa: E501
>>> ctx2 = ExprContext(env2, Substitution(), {}, {}, [].append)
>>> t, _= expr_matmul(("matmul", ("var", "u"), ("var", "v")), ctx2)  # ℝ[3] @ ℝ[3] → ℝ (dot)  # noqa: E501
>>> t

physika.utils.infer_expr.expr_mul(node: Any, ctx: ExprContext) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

Infer the type of multiplication t1 * t2.

As same as expr_add_sub, shapes must match for tensor operands, and follows broadcast rules.

Parameters:
  • node (ASTNode) – AST node of the form ("mul", left_expr, right_expr).

  • ctx (ExprContext) – Current inference context. ctx.s is threaded through both operand inferences and updated with any new unification bindings. Shape mismatch errors are registered via ctx.add_error.

Returns:

(TTensor, s) when either operand is a tensor. The tensor shape is the broadcast result. (T_REAL, s) when both operands are scalars. (None, s) when both operand types could not be determined.

Return type:

tuple[Optional[Type], Substitution]

Examples

>>> from physika.utils.infer_expr import ExprContext, expr_mul, T_REAL, TTensor
>>> from physika.utils.types import Substitution
>>> ctx = ExprContext({"x": TTensor(((3, "invariant"),))}, Substitution(), {}, {}, [].append)  # noqa: E501
>>> t, _=expr_mul(("mul", ("var", "x"), ("num", 2.0)), ctx)  # ℝ[3] * ℝ → ℝ[3]
>>> t
ℝ[3]
>>> t, _= expr_mul(("mul", ("num", 2.0), ("num", 3.0)), ctx)  # ℝ * ℝ → ℝ
>>> t

physika.utils.infer_expr.expr_neg(node: Any, ctx: ExprContext) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

Infer the result type of negation -expr.

Negation is shape-preserving and inferred through the single operand sub-expression.

Parameters:
  • node (ASTNode) – AST node of the form ("neg", operand_expr).

  • ctx (ExprContext) – Current inference context passed unchanged to the operand inference. The returned substitution reflects any new bindings produced while inferring the operand.

Returns:

Exactly the result of inferring the operand: (operand_type, s). (None, s) when the operand type could not be determined.

Return type:

tuple[Optional[Type], Substitution]

Examples

>>> from physika.utils.infer_expr import ExprContext, expr_neg, TTensor, T_REAL
>>> from physika.utils.types import Substitution
>>> ctx = ExprContext({"x": TTensor(((3, "invariant"),))}, Substitution(), {}, {}, [].append)  # noqa: E501
>>> t, _= expr_neg(("neg", ("var", "x")), ctx)  # -ℝ[3] → ℝ[3]
>>> t
ℝ[3]
>>> t, _= expr_neg(("neg", ("num", 1.0)), ctx)  # -ℝ → ℝ
>>> t

physika.utils.infer_expr.expr_num(node: Any, ctx: ExprContext) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

The type of a numeric literal is always .

Parameters:
  • node (tuple) – AST node of the form ("num", value) where value is an int or float.

  • ctx (ExprContext) – Current inference context.

Returns:

Always (T_REAL, ctx.s).

Return type:

tuple[Type, Substitution]

Examples

>>> from physika.utils.infer_expr import ExprContext, expr_num, T_REAL
>>> from physika.utils.types import Substitution
>>> ctx = ExprContext({}, Substitution(), {}, {}, [].append)
>>> t, _= expr_num(("num", 3.14), ctx)
>>> t

physika.utils.infer_expr.expr_pow(node: Any, ctx: ExprContext) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

Infer the result type of exponentiation t1 ** t2.

The result has the same shape as the base. The exponent type is inferred to accumulate substitutions but does not affect the output shape.

Parameters:
  • node (ASTNode) – AST node of the form ("pow", base_expr, exp_expr).

  • ctx (ExprContext) – Current inference context. Only the base expression is used to determine the result type; ctx.s is threaded through the base inference and the result has the base’s type after substitution.

Returns:

(base_type, s). Same type as the base expression after applying the accumulated substitution. (None, s) when the base type could not be determined.

Return type:

tuple[Optional[Type], Substitution]

Examples

>>> from physika.utils.infer_expr import ExprContext, expr_pow, TTensor
>>> from physika.utils.types import Substitution
>>> ctx = ExprContext({"x": TTensor(((3, "invariant"),))}, Substitution(), {}, {}, [].append)
>>> t, _= expr_pow(("pow", ("var", "x"), ("num", 2.0)), ctx)  # ℝ[3] ** ℝ → ℝ[3]  # noqa: E501
>>> t
ℝ[3]
>>> t, _= expr_pow(("pow", ("num", 2.0), ("num", 3.0)), ctx)  # ℝ ** ℝ → ℝ
>>> t

physika.utils.infer_expr.expr_slice(node: Any, ctx: ExprContext) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

Infer the type of a slice expression arr[start:end].

When both bounds are numeric literals the result length is computed as end start (end-exclusive). For higher-rank tensors only the leading dimension is sliced and remaining dims are preserved.

When either bound is a non-literal expression (e.g. a loop variable) a fresh symbolic dimension TDim("δN") is introduced for the sliced leading dimension so that the rank and trailing dims are still preserved.

When both bounds are literals, the following are reported as static semantic errors:

  • negative start or end

  • end < start

  • end == start

  • start >= leading dimension (start out of bounds)

  • end > leading dimension (end out of bounds)

Parameters:
  • node (ASTNode) – AST node of the form ("slice", arr_name, start_expr, end_expr) where start_expr and end_expr are expression nodes.

  • ctx (ExprContext) – Current inference context. ctx.env must contain arr_name; ctx.s is returned unchanged.

Returns:

(TTensor([end - start]), ctx.s) for a 1D array with literal bounds. (TTensor([end - start] + shape[1:]), ctx.s) for a higher-rank array with literal bounds. (TTensor([TDim("δN")] + shape[1:]), ctx.s) when either bound is dynamic — a fresh symbolic dimension replaces the sliced leading dim. (None, ctx.s) when arr_name is not in scope.

Return type:

tuple[Optional[Type], Substitution]

Examples

>>> from physika.utils.infer_expr import ExprContext, expr_slice, TTensor
>>> from physika.utils.types import Substitution
>>> ctx = ExprContext({"v": TTensor(((6, "invariant"),))}, Substitution(), {}, {}, [].append)
>>> t, _= expr_slice(("slice", "v", ("num", 0.0), ("num", 3.0)), ctx)  # v[0:3] → ℝ[3]
>>> t
ℝ[3]
>>> ctx2 = ExprContext({"A": TTensor(((3, "invariant"), (4, "invariant")))}, Substitution(), {}, {}, [].append)
>>> t, _= expr_slice(("slice", "A", ("num", 0.0), ("num", 2.0)), ctx2)  # A[0:2] → ℝ[2,4]  # noqa: E501
>>> t
ℝ[2,4]
physika.utils.infer_expr.expr_var(node: Any, ctx: ExprContext) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

Look up a variable in the current environment.

Returns (None, s) when the variable is not yet in scope.

Parameters:
  • node (tuple) – AST node ("var", name) where name is the variable name.

  • ctx (ExprContext) – Current inference context. ctx.env looks for name and ctx.s is applied to the result to expose any resolved unification bindings.

Returns:

(resolved_type, ctx.s) when name is in scope. (None, ctx.s) otherwise.

Return type:

tuple[Optional[Type], Substitution]

Examples

>>> from physika.utils.infer_expr import ExprContext, expr_var
>>> from physika.utils.types import Substitution, T_REAL, TTensor
>>> ctx = ExprContext({"x": TTensor(((3, "invariant"),))}, Substitution(), {}, {}, [].append)  # noqa: E501
>>> t, _= expr_var(("var", "x"), ctx)
>>> t
ℝ[3]
>>> t, _= expr_var(("var", "y"), ctx)  # not in scope
>>> t is None
True
physika.utils.infer_expr.infer_expr(node: tuple[Any, ...] | list[ASTNode] | str | int | float | None, env: dict, s: Substitution, func_env: dict, class_env: dict, add_error: Callable) Tuple[TVar | TDim | TScalar | TTensor | TFunc | TInstance | None, Substitution][source]

Infer the type of an expression AST node.

This is the main entry point for expression level type inference. The substitution s is threaded through every recursive call so that unification bindings made by sub-expressions are visible to the next ones. Dispatch is driven by the string tag in node[0] via EXPR_DISPATCH.

Parameters:
  • node (ASTNode) –

    The expression node to type-check. Accepted shapes:

    • None — missing/optional sub-expression.

    • int or float — bare numeric literal (from the parser).

    • ("num",    value) — explicit numeric literal node.

    • ("var",    name) — variable reference.

    • ("imaginary",) — the imaginary unit i.

    • ("array",  elems) — array literal [e0, e1, …].

    • ("index",  arr, idx) — 1-D subscript arr[idx].

    • ("indexN", arr, idxs) — N-D subscript arr[i,j,…].

    • ("chain_index", base, idx) — chained subscript A[i][k].

    • ("slice",  arr, start, end) — slice arr[start:end].

    • ("add",    l, r) — addition l + r.

    • ("sub",    l, r) — subtraction l - r.

  • env (dict) – Maps variable names (str) to their current inferred Type. Populated by the statement level inferencer before calling this function.

  • s (Substitution) – The substitution dictionary accumulated so far. Each call returns a substitution that the caller should thread forward.

  • func_env (dict) – Maps user defined function names to (param_types, return_type) pairs.

  • class_env (dict) – Maps class names to their definition dicts (class_params, return_type, etc), used for constructor call inference.

  • add_error (Callable) – Error reporting callback (errors.append).

Returns:

A pair (t, s2) where:

  • t is the inferred Type or None when the node cannot be typed.

  • s2 is the updated substitution that includes any new unification bindings produced while inferring node.

Return type:

tuple[Optional[Type], Substitution]

Examples

>>> from physika.utils.infer_expr import infer_expr, T_REAL
>>> from physika.utils.types import Substitution, TTensor
>>> errors = []
>>> # Numeric literal always infers ℝ
>>> t, _ = infer_expr(3.14, {}, Substitution(), {}, {}, errors.append)
>>> t

>>> # Variable reference resolved from env
>>> t, _ = infer_expr(("var", "x"), {"x": T_REAL}, Substitution(), {}, {}, errors.append)
>>> t

>>> # Array literal of two scalars is type ℝ[2]
>>> t, _ = infer_expr(("array", [("num", 1.0), ("num", 2.0)]), {}, Substitution(), {}, {}, errors.append)
>>> t
ℝ[2]
>>> # Unknown tag error recorded, returns None
>>> t, _ = infer_expr(("unknown", 42), {}, Substitution(), {}, {}, errors.append)  # noqa: E501
>>> t is None
True
>>> errors[-1]
'Unknown expression type: unknown'