API Reference
physika.features.classes
- class physika.features.classes.ClassFeature[source]
Bases:
ELFPhysika classes implemented as an ELF subclass.
ClassFeatureinjects rules viaREGISTRYat lexer, parser, type checker, and code generator.Lexer rules Adds two new tokens,
CLASSreserved keyword ("class") andDOTtoken (".") for field and method accessParser 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_enventries so the type checker can resolve field types, method calls and constructor calls.Forward rules Three code-generation handlers were defined.
class_defemits a completenn.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_tensorobjects. Parameters used inside a forward method are wrapped innn.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_defemits a completenn.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_tensorobjects. Parameters used inside a forward method are wrapped innn.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,
CLASSreserved keyword ("class") andDOTtoken (".") 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_ruleshandler 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.fieldby 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_callinfersobj.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]) –
Nonewhen 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.Moduleclass.- 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_exprfor 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.Modulesubclass 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:
ELFDifferentiable 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_emitemitsname = <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_emitemits 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
TILDEtoken ("~") for stochastic sampling syntax and includesPHYSIKAreserved keyword so thatphysika.seed(n)parses. Also, includes greek letters aliases for mapping with torch distributions:𝒩→Normal𝒰→UniformΓ→Gammaℬ→Beta
- Returns:
Dictionary with
tokens(["TILDE", "PHYSIKA"]) andtoken_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_exprto 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_exprto 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_argsare distribution parameters related to sampling like mean (μ) and standard deviation (σ) for Normal distribution.shape_argsare 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_exprto 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_exprto 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’srsample()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 differentiablelog_probterm in the loss is needed so that the gradient is computed.
- Score Function Estimator (
- 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_exprused 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_exprto 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_dimension_list_multi(p)[source]
dimension_list : dimension_spec COMMA dimension_list
- 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_range(p)[source]
factor : FOR ID COLON TYPE LPAREN func_expr COMMA func_expr RPAREN ARROW func_expr
- 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_func_body_stmt_decl(p)[source]
func_body_stmt : ID COLON type_spec EQUALS func_expr 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_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_for_expr(p)[source]
func_factor : FOR ID COLON TYPE LPAREN func_expr RPAREN 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_step_slice(p)[source]
func_factor : ID LBRACKET NUMBER COLON COLON NUMBER RBRACKET
- 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_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_loop_index_list_multi(p)[source]
loop_index_list : loop_index_list COMMA func_expr
- physika.parser.p_multi_index_list_extend(p)[source]
multi_index_list : multi_index_list COMMA func_expr
- 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_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_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_statemet_equation_decl(p)[source]
statement : ID COLON EQUATION WALRUS func_expr EQUALS func_expr NEWLINE
physika.type_checker
- class physika.type_checker.TypeChecker(unified_ast: dict)[source]
Bases:
objectType 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. Whenunifydetermines thatαNmust equal some typeT, it extends the substitution with the bindingαN → T. Callings.apply(t)replaces every bound type variable intwith its mapped type, following chains of bindings until a concrete type is reached.Unification (
unify(t1, t2, s)) uses the accumulated version ofsthat makess.apply(t1) == s.apply(t2). If either side is a freeTVar, a new binding is added tos. If both sides are concrete types of the same constructor (arrays, matrices, tensors asTTensortypes), 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:
Signature registration: All function and class signatures are stored in
func_envandclass_envbefore any body is examined. Class constructors are stored infunc_envas(field_types, TInstance(name)).Body checking (
check_function,check_class): For eachdefandclass,infer_stmtswalks statements in order, threadingsthrough 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.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.errorsas 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:
Register all function and class signatures.
Check function bodies
Check class bodies.
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:
Analysis pass — walks the AST to determine which
runtime.pyhelpers (solve,train,evaluate,compute_grad,simulate,animate, etc) are referenced, and collects variables used asgrad()differentiation targets.Code-generation pass — uses
generate_function,generate_class, andgenerate_statement(fromutils.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
importstatements, function definitions,nn.Moduleclass definitions, and program-level statements. Variables that appear asgrad()targets are initialised withrequires_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
funcatn_pointsevenly-spaced time values betweentime_minandtime_max, numerically differentiates to obtain velocity, and displays an interactive animation.If PyVista is installed an interactive 3-D scene is used (
SPACEto pause,Qto quit). Otherwise falls back to a matplotlibFuncAnimation.The last two (or three) positional
argsare interpreted as(time_min, time_max)or(time_min, time_max, n_points); everything before them is forwarded as extra arguments tofunc.- Parameters:
func (callable) – A callable
(*extra_args, t) -> scalarwheretis the time parameter andextra_argsare 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_pointsdefaults to200when 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
for a computed output tensorf(x)whose gradient is requested.x (float or torch.Tensor) – The scalar point at which to differentiate
f. Whenfis a tensor, x must be a leaf withrequires_grad=Truethat was used when the output was computed.
- Returns:
Detached scalar
df/dxevaluated 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’sloss()method (if defined) or MSE, and returns the average.- Parameters:
model (nn.Module) – The Physika
nn.Moduleto 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), andnn.Modulesubclasses.
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
modelfornstepsdiscrete time-steps starting from initial statex0with step sizedt: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.Modulewhoseforwardmaps 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
intinternally).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"whereexpris a linear combination of unknowns and known variables. Builds the coefficient matrix A and constant vector b, then solvesAx = busingtorch.linalg.solve.Unknowns are automatically detected: any variable on the right-hand side that is not in
known_varsand 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
modelso the original is not mutated, enables gradients on all parameters, then runsepochsof SGD. For each epoch every sample(X[i], y[i])is passed through the model; if the model defines aloss(pred, target[, input])method it is used, otherwise MSE(pred - y_i)**2is the default.Training progress is printed every
epochs // 10epochs and on the final epoch.- Parameters:
model (nn.Module) – The Physika
nn.Moduleto 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
intinternally).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
sintotorch.sin), and complex numbers.This is the core of the string-codegen path used by
generate_function,generate_class, andgenerate_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 oftorch.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 offrom_ast_to_torchto 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:
Trueif a matching call node exists anywhere in the subtree,Falseotherwise.- 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:
Trueif a("call", "solve", ...)node exists anywhere in the subtree,Falseotherwise.- 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:
Trueif a("symbol_decl", ...)`or`("function_decl", ...)node exists anywhere in the subtree,Falseotherwise.- 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 thesymbol_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 thatgenerate_statementcan initialise those variables withrequires_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, orbody_ifAST 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, orbody_ifAST tuples to emit.Noneentries 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. PassNone(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 nestedfor_loop/for_loop_rangenodes. 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_looporfor_loop_rangeAST 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_stmtAST nodes.Recurse for nested
loop_for_range,loop_if, andloop_if_elsenodes, extendingloop_varwith each new inner variable.ast_to_torch_exprresolves the imaginary-unit tokenito the correct Python name instead oftorch.tensor(1j).- Parameters:
loop_body (list[ASTNode]) –
func_loop_stmtnodes.Noneentries are skipped. Supported tags: -loop_assign-loop_pluseq-loop_index_pluseq-loop_for_range-loop_if-loop_if_elseindent_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.Modulesubclass from a class AST entry.Translates a Physika class into a Python class string with
__init__(wrapping tensor params asnn.Parameter),forward(the lambda body, with optional loop), and an optionallossmethod. Class parameter references in the forward/loss bodies are rewritten toself.paramviareplace_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.Modulesubclass 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 tosolve.- 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 inphysika_printunless it is a side-effect call likesimulate/animate),for_loop, and skipsfunc_def/class_def(already emitted byfrom_ast_to_torch).Variables whose names appear in grad_target_vars are initialised with
requires_grad=Trueso thatgrad()can differentiate through them.- Parameters:
stmt (ASTNode) – An AST statement tuple (e.g.
("decl", name, type, expr, lineno)) orNone.grad_target_vars (set[str]) – Variable names used as differentiation targets in
grad()calls. Collected bycollect_grad_targetsduring the analysis pass.
- Returns:
A Python source string for the statement, or
Noneif the statement should be skipped (func_def,class_def, orNoneinput).- 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.paramin generated code.Rewrites bare parameter names inside the generated
forwardandlossmethod 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 offorloops: the generated code iteratesrange(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_varstring — legacy / simplified form.("imaginary",)— when loop_var is"i"(the lexer emits theIMAGINARYtoken for the identifieri).
- 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 thetype_checkwrapper).
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 theprint_ast=Trueflag inbuild_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_exprfor+,-,*, and/to determine the output type. The shape of the result follows PyTorch broadcast. Tensor/Tensor shape compatibility is checked viaunify.- Parameters:
t1 (Optional[Type]) – Type of the left operand.
t1can beNonewhen unknown.t2 (Optional[Type]) – Type of the right operand.
t2can beNonewhen unknown.
- Returns:
t1ift2isNoneand viceversa.TTensoroperand if exactly one side is a tensor.TScalarwhen 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:
Trueif 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
Typevalue.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
Nonefor 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, orNoneif 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, andfor_loop.- Parameters:
stmt (ASTExpr) – A program-level statement tuple, or
None.- Returns:
The source line number, or
Noneif 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, orNoneif not a tensor.- Parameters:
t (Optional[Type]) – Any
Type.- Returns:
List of dimension entries
[d0, d1, ...]for aTTensor, orNonefor scalars, functions, instances, andNoneinput.- 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
TTensorfrom 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 symbolicstr, or aTDimvariable.- 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.
Noneor 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_erroris 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
Noneif 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
varappears anywhere inside typet: Typeto prevent creating infinite types.Before binding
α0 = some_type, the unification step callsoccurs_in(α0, some_type). if it returnsTrue, the binding would create a circular structure (e.g.α0 = ℝ[α0]) and aTypeErroris raised instead.- Parameters:
var (Union[TVar, TDim]) – The variable to search for.
t (Type) – The type to search within.
- Returns:
Trueifvarappears int,Falseotherwise.- 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 (
Nonemeans scalar).s2 (list[int] or None) – Shape of the right operand (
Nonemeans 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.okisTrueif the shapes are compatible;result_shapeis the broadcasted shape, orNoneif 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
declandassign).check_statement (Callable[[ASTExpr], None]) – Recursive callback for checking nested statements (e.g.
for_loopbodies).
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 isTypeChecker.infer_typeitself, 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
Noneif 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), orNone.- Returns:
A readable representation, e.g.
"ℝ","ℝ[3]","ℝ[2,3]","(ℝ) → ℝ", or"unknown"forNone.- 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:
Trueif the types are compatible,Falseotherwise.- 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
Substitutionthat makest1equal tot2.Unification step is used at type checker inference. Both types,
t1andt2, are resolved throughs: Substitutionso that known equalities are applied before comparing. The function then dispatches on the structural shape of the two types and either returnssunchanged (already equal), extendsswith a new binding (one side is a variable), or recurses into sub-types (tensors, functions). ATypeErroris raised for any mismatch.- Parameters:
t1 (Type) – First type to unify.
t2 (Type) – Second type to unify.
s (Substitution) – Current substitution accumulator. Both
t1andt2are resolved throughsbefore comparison.
- Returns:
An extended substitution of
s.- Return type:
Substitution
- Raises:
TypeError – If
t1andt2cannot 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
unifybut at dimension level, used when twoTTensortypes 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, orTDim.d2 (Any) – Second dimension entry:
int,str,TVar, orTDim.s (Substitution) – Current substitution accumulator; both entries are resolved through it before comparison.
- Returns:
An extended substitution under which
d1andd2are 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:
objectData 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
sso 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.sis threaded through both operand inferences and updated with any new unification bindings. Shape mismatch errors are registered viactx.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.sis threaded through each element inference so later elements see bindings from earlier ones. Type errors are registered viactx.add_error.
- Returns:
(TTensor(dims), updated_s)wheredims[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:
Built-in elementwise functions (
exp,sin,cos,sqrt,abs,tanh,log,real,imag) — preserve the shape of their first argument.Built-in reduction (
sum) →ℝ.grad(f, x)→ same shape asx.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 astrand arg_list is a list of expression nodes.ctx (ExprContext) – Current inference context.
ctx.sis threaded through all argument inferences and updated with unification bindings from parameter matching. Arity and type mismatch errors are reported viactx.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 toA[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 ancomplex.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_eqcond_neqcond_ltcond_gtcond_leqcond_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_REALas 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.sis threaded through both operand inferences and updated with any new unification bindings. Shape mismatch errors are registered viactx.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
iis 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.sis 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 concreteintfor literal sizes or a freshTDimfor 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) → bodyIs similar to
expr_for_exprbut the size is derived from numericstartandendbounds. 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.sis 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)bodyiis 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 inctx.envthe 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]fromarr’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 viactx.add_errorwhen:arris not in scope — returns(None, ctx.s).arris 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.sis 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)whenarr_nameis 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.envmust contain arr_name.ctx.sis 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)whenarr_nameis not in scope, resolves to a scalar, or is over-indexed (index count exceeds rank); an error is reported viactx.add_errorin 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_ophandles 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.sis threaded through both operand inferences and updated with any new substitution bindings. Shape incompatibility errors are reported viactx.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.sis threaded through both operand inferences and updated with any new unification bindings. Shape mismatch errors are registered viactx.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 anintorfloat.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.sis 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.envmust contain arr_name;ctx.sis 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)whenarr_nameis 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.envlooks for name andctx.sis 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]viaEXPR_DISPATCH.- Parameters:
node (ASTNode) –
The expression node to type-check. Accepted shapes:
None— missing/optional sub-expression.intorfloat— bare numeric literal (from the parser).("num", value)— explicit numeric literal node.("var", name)— variable reference.("imaginary",)— the imaginary uniti.("array", elems)— array literal[e0, e1, …].("index", arr, idx)— 1-D subscriptarr[idx].("indexN", arr, idxs)— N-D subscriptarr[i,j,…].("chain_index", base, idx)— chained subscriptA[i][k].("slice", arr, start, end)— slicearr[start:end].("add", l, r)— additionl + r.("sub", l, r)— subtractionl - r.
env (dict) – Maps variable names (
str) to their current inferredType. 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:tis the inferredTypeorNonewhen the node cannot be typed.s2is 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'