Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion grammar/BaseModelica.g4
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ primary
| STRING
| 'false'
| 'true'
| ('der' | 'initial' | 'pure') functionCallArgs
| ('der' | 'initial' | 'pure' | 'Clock') functionCallArgs
| componentReference functionCallArgs?
| '(' outputExpressionList ')' arraySubscripts?
| '[' expressionList (';' expressionList)* ']'
Expand Down
2 changes: 1 addition & 1 deletion src/antlr_generated/BaseModelica.interp

Large diffs are not rendered by default.

620 changes: 310 additions & 310 deletions src/antlr_generated/BaseModelicaParser.py

Large diffs are not rendered by default.

109 changes: 93 additions & 16 deletions src/antlr_parser.jl
Original file line number Diff line number Diff line change
Expand Up @@ -250,13 +250,13 @@ end
function visit_shortClassSpecifier(visitor::ASTBuilderVisitor, ctx::Py)
# For now, return a placeholder - can be expanded later
name = get_text(ctx.IDENT())
return BaseModelicaLongClass(name, "", BaseModelicaComposition([], [], [], [], nothing))
return BaseModelicaLongClass(name, "", BaseModelicaComposition([], [], [], [], nothing, []))
end

function visit_derClassSpecifier(visitor::ASTBuilderVisitor, ctx::Py)
# For now, return a placeholder - can be expanded later
name = get_text(ctx.IDENT(0))
return BaseModelicaLongClass(name, "", BaseModelicaComposition([], [], [], [], nothing))
return BaseModelicaLongClass(name, "", BaseModelicaComposition([], [], [], [], nothing, []))
end

function visit_globalConstant(visitor::ASTBuilderVisitor, ctx::Py)
Expand Down Expand Up @@ -342,7 +342,91 @@ function visit_composition(visitor::ASTBuilderVisitor, ctx::Py)
end
end

return BaseModelicaComposition(components, equations, initial_equations, parameter_equations, annotation)
# Get clock base partitions
base_partitions = []
partition_ctxs = ctx.basePartition()
if !is_null(partition_ctxs)
if pyconvert(Bool, pybuiltins.hasattr(partition_ctxs, "__iter__"))
for partition_ctx in partition_ctxs
partition = visit_basePartition(visitor, partition_ctx)
if !isnothing(partition)
push!(base_partitions, partition)
end
end
else
partition = visit_basePartition(visitor, partition_ctxs)
if !isnothing(partition)
push!(base_partitions, partition)
end
end
end

return BaseModelicaComposition(components, equations, initial_equations, parameter_equations, annotation, base_partitions)
end

function visit_basePartition(visitor::ASTBuilderVisitor, ctx::Py)
# basePartition: 'partition' stringComment (annotationComment ';')? (clockClause ';')* subPartition*
clock_clauses = []
clock_clause_ctxs = ctx.clockClause()
if !is_null(clock_clause_ctxs)
if pyconvert(Bool, pybuiltins.hasattr(clock_clause_ctxs, "__iter__"))
for cc_ctx in clock_clause_ctxs
push!(clock_clauses, visit_clockClause(visitor, cc_ctx))
end
else
push!(clock_clauses, visit_clockClause(visitor, clock_clause_ctxs))
end
end

sub_partitions = []
sub_partition_ctxs = ctx.subPartition()
if !is_null(sub_partition_ctxs)
if pyconvert(Bool, pybuiltins.hasattr(sub_partition_ctxs, "__iter__"))
for sp_ctx in sub_partition_ctxs
push!(sub_partitions, visit_subPartition(visitor, sp_ctx))
end
else
push!(sub_partitions, visit_subPartition(visitor, sub_partition_ctxs))
end
end

return BaseModelicaBasePartition(clock_clauses, sub_partitions)
end

function visit_clockClause(visitor::ASTBuilderVisitor, ctx::Py)
# clockClause: decoration? 'Clock' IDENT '=' expression comment
name = get_text(ctx.IDENT())
expr = visit_expression(visitor, ctx.expression())
return BaseModelicaClockClause(name, expr)
end

function visit_subPartition(visitor::ASTBuilderVisitor, ctx::Py)
# subPartition: 'subpartition' '(' argumentList ')' stringComment (annotationComment ';')?
# ('equation' (equation ';')* | 'algorithm' (statement ';')*)*
clock_args = []
if !is_null(ctx.argumentList())
clock_args = visit_argumentList(visitor, ctx.argumentList())
end

equations = []
eq_ctxs = ctx.equation()
if !is_null(eq_ctxs)
if pyconvert(Bool, pybuiltins.hasattr(eq_ctxs, "__iter__"))
for eq_ctx in eq_ctxs
eq = visit_equation(visitor, eq_ctx)
if !isnothing(eq)
push!(equations, eq)
end
end
else
eq = visit_equation(visitor, eq_ctxs)
if !isnothing(eq)
push!(equations, eq)
end
end
end

return BaseModelicaSubPartition(clock_args, equations)
end

function visit_genericElement(visitor::ASTBuilderVisitor, ctx::Py)
Expand Down Expand Up @@ -1022,20 +1106,13 @@ function visit_primary(visitor::ASTBuilderVisitor, ctx::Py)
elseif get_text(ctx) == "end"
return BaseModelicaIdentifier("end")
elseif !is_null(ctx.functionCallArgs()) && is_null(ctx.componentReference())
# Check for der, initial, or pure function calls
# These appear as: ('der' | 'initial' | 'pure') functionCallArgs
# Check for der, initial, pure, Clock function calls (keywords that can also be function names)
# These appear as: ('der' | 'initial' | 'pure' | 'Clock') functionCallArgs
full_text = get_text(ctx)
if startswith(full_text, "der") || startswith(full_text, "initial") || startswith(full_text, "pure")
# Determine which function it is
func_name_str = if startswith(full_text, "der")
"der"
elseif startswith(full_text, "initial")
"initial"
else
"pure"
end

func_name = BaseModelicaIdentifier(func_name_str)
keyword_funcs = ("der", "initial", "pure", "Clock")
matched = findfirst(kw -> startswith(full_text, kw), keyword_funcs)
if !isnothing(matched)
func_name = BaseModelicaIdentifier(keyword_funcs[matched])
args = visit_functionCallArgs(visitor, ctx.functionCallArgs())
return BaseModelicaFunctionCall(func_name, args)
end
Expand Down
5 changes: 4 additions & 1 deletion src/ast.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@
BaseModelicaAnyEquation(equation, description)
BaseModelicaAnnotation(annotation_content)
BaseModelicaForIndex(ident, expression)
BaseModelicaComposition(components, equations, initial_equations, parameter_equations, annotation)
BaseModelicaComposition(components, equations, initial_equations, parameter_equations, annotation, base_partitions)
BaseModelicaClockClause(name, expression)
BaseModelicaSubPartition(clock_args, equations)
BaseModelicaBasePartition(clock_clauses, sub_partitions)
BaseModelicaLongClass(name, description, composition)
BaseModelicaModification(class_modifications, expr)
BaseModelicaClassModificationArg(name, modification)
Expand Down
131 changes: 105 additions & 26 deletions src/evaluator.jl
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,10 @@ function eval_AST(when_eq::BaseModelicaWhenEquation)
end
end

# Pair shorthand: [condition ~ 0] => affect_eqs
# Fires on positive zero-crossing (condition becomes true) → Modelica edge semantics
push!(callbacks, [crossing ~ 0] => affects)
# Only fire on positive zero-crossing (condition becomes true) → Modelica edge semantics
push!(callbacks, ModelingToolkit.SymbolicContinuousCallback(
[crossing ~ 0], affects; affect_neg = nothing
))
end
return callbacks
end
Expand Down Expand Up @@ -282,9 +283,54 @@ function eval_AST(component::BaseModelicaComponentClause)
end
end

function eval_clocked_rhs(expr_ast, k)
# Recursively evaluate a clocked RHS, mapping previous(x) → x(k-1).
if expr_ast isa BaseModelicaFunctionCall
func_name = expr_ast.func_name
fname = if func_name isa BaseModelicaIdentifier
func_name.name
elseif func_name isa BaseModelicaComponentReference
func_name.ref_list[1].name
else
""
end
if fname == "previous"
arg = eval_AST(expr_ast.args.args[1])
return arg(k - 1)
else
return eval_AST(expr_ast)
end
elseif expr_ast isa BaseModelicaSum
return eval_clocked_rhs(expr_ast.left, k) + eval_clocked_rhs(expr_ast.right, k)
elseif expr_ast isa BaseModelicaMinus
return eval_clocked_rhs(expr_ast.left, k) - eval_clocked_rhs(expr_ast.right, k)
elseif expr_ast isa BaseModelicaProd
return eval_clocked_rhs(expr_ast.left, k) * eval_clocked_rhs(expr_ast.right, k)
elseif expr_ast isa BaseModelicaDivide
return eval_clocked_rhs(expr_ast.left, k) / eval_clocked_rhs(expr_ast.right, k)
elseif expr_ast isa BaseModelicaUnaryMinus
return -eval_clocked_rhs(expr_ast.operand, k)
else
return eval_AST(expr_ast)
end
end

function eval_clocked_equation(eq_ast, k)
# Evaluate a simple equation inside a clock partition.
# LHS variable becomes var(k); RHS evaluates with previous(x) → x(k-1).
inner = eq_ast isa BaseModelicaAnyEquation ? eq_ast.equation : eq_ast
if inner isa BaseModelicaSimpleEquation
lhs_var = eval_AST(inner.lhs)
rhs = eval_clocked_rhs(inner.rhs, k)
return lhs_var(k) ~ rhs
end
return nothing
end

function eval_AST(model::BaseModelicaModel)
empty!(variable_map)
empty!(parameter_val_map)
empty!(clock_map)

class_specifier = model.long_class_specifier
model_name = class_specifier.name
Expand Down Expand Up @@ -399,22 +445,20 @@ function eval_AST(model::BaseModelicaModel)
end

# Split equations into when-equations (→ continuous callbacks) and regular equations.
# The Julia parser wraps all equation types in BaseModelicaSimpleEquation via |>,
# with the inner type in .lhs. The ANTLR parser produces bare types without that
# wrapper, so guard with hasproperty before touching .equation.
when_equation_list = [
eq for eq in equations if
hasproperty(eq, :equation) &&
eq.equation isa BaseModelicaSimpleEquation &&
eq.equation.lhs isa BaseModelicaWhenEquation
]
regular_equations = [
eq for eq in equations if !(
hasproperty(eq, :equation) &&
eq.equation isa BaseModelicaSimpleEquation &&
eq.equation.lhs isa BaseModelicaWhenEquation
)
]
# Julia parser: wrapped as BaseModelicaAnyEquation { equation: BaseModelicaSimpleEquation { lhs: BaseModelicaWhenEquation } }
# ANTLR parser: bare BaseModelicaWhenEquation (visit_whenEquation returns directly)
when_equation_list = []
regular_equations = []
for eq in equations
if eq isa BaseModelicaWhenEquation ||
(hasproperty(eq, :equation) &&
eq.equation isa BaseModelicaSimpleEquation &&
eq.equation.lhs isa BaseModelicaWhenEquation)
push!(when_equation_list, eq)
else
push!(regular_equations, eq)
end
end

# Flatten regular equations - some (like if-equations) return lists
eqs_raw = [eval_AST(eq) for eq in regular_equations]
Expand All @@ -435,7 +479,8 @@ function eval_AST(model::BaseModelicaModel)
# Build continuous callbacks from when-equations
when_callbacks = []
for eq in when_equation_list
append!(when_callbacks, eval_AST(eq.equation.lhs))
when_eq = eq isa BaseModelicaWhenEquation ? eq : eq.equation.lhs
append!(when_callbacks, eval_AST(when_eq))
end

#vars_and_pars = merge(Dict(vars .=> vars), Dict(pars .=> pars))
Expand Down Expand Up @@ -469,14 +514,51 @@ function eval_AST(model::BaseModelicaModel)
!isnothing(idx) && (vars[idx] = variable_map[name])
end

real_eqs = Equation[declaration_eqs..., eqs...]
# Process clock partitions: register clocks then evaluate clocked equations.
clocked_eqs = []
for partition in composition.base_partitions
for cc in partition.clock_clauses
clock_name = Symbol(cc.name)
clock_map[clock_name] = eval_AST(cc.expression)
end
for sp in partition.sub_partitions
clock_sym = nothing
for arg in sp.clock_args
arg_name = arg.name isa BaseModelicaIdentifier ? arg.name.name : arg.name
if arg_name == "clock"
if !isnothing(arg.modification) &&
!isnothing(arg.modification.expr) &&
!isempty(arg.modification.expr)
ref = arg.modification.expr[end]
ref_name = ref isa BaseModelicaComponentReference ?
Symbol(ref.ref_list[1].name) : Symbol(ref.name)
clock_sym = clock_map[ref_name]
end
end
end

if isnothing(clock_sym)
error("Could not resolve clock for subpartition")
end

k = ModelingToolkit.ShiftIndex(clock_sym)
for eq in sp.equations
ceq = eval_clocked_equation(eq, k)
if !isnothing(ceq)
push!(clocked_eqs, ceq)
end
end
end
end

real_eqs = Equation[declaration_eqs..., eqs..., clocked_eqs...]

# Variables that only appear in when-equation bodies need D(v) ~ 0 hold dynamics
# so they are proper ODE states between events (constant until an event fires).
if !isempty(when_callbacks)
continuous_syms = Set(Iterators.flatten(Symbolics.get_variables.(real_eqs)))
seen = Set()
for cb in when_callbacks, affect_eq in last(cb)
for cb in when_callbacks, affect_eq in cb.affect.affect
var_sym = Symbolics.unwrap(affect_eq.lhs)
if var_sym ∉ seen
push!(seen, var_sym)
Expand All @@ -488,10 +570,7 @@ function eval_AST(model::BaseModelicaModel)
end
end

# Use only() to extract the runtime-typed Pair from Vector{Any} for single events;
# pass the vector directly for multiple events.
events = length(when_callbacks) == 1 ? only(when_callbacks) : when_callbacks
@named sys = System(real_eqs, t; continuous_events = events)
@named sys = System(real_eqs, t; continuous_events = when_callbacks)
return mtkcompile(sys)
end

Expand Down
2 changes: 1 addition & 1 deletion src/julia_parser.jl
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ function BaseModelicaComposition(input_list)
annotation = input
end
end
return BaseModelicaComposition(components, equations, initial_equations, parameter_equations, annotation)
return BaseModelicaComposition(components, equations, initial_equations, parameter_equations, annotation, [])
end

function BaseModelicaPackage(input_list)
Expand Down
9 changes: 9 additions & 0 deletions src/maps.jl
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,12 @@ function_map = Dict(
# ── Assertion and termination (Spec 8.3.7-8.3.8) ─────────────────────────
:assert => x -> nothing,
:terminate => x -> nothing,

# -- Clocked Variables --
:pre => x -> ModelingToolkit.Pre(x...),
:sample => x -> ModelingToolkit.Sample(x...),
:hold => x -> ModelingToolkit.Hold(x...),
:Clock => x -> ModelingToolkit.Clock(x...),
)

# holds variables, populated by evaluating component_clause
Expand All @@ -109,3 +115,6 @@ variable_map = Dict()
parameter_val_map = Dict()

initial_value_map = Dict()

# holds named clocks for clocked partitions, populated during partition evaluation
clock_map = Dict{Symbol, Any}()
Loading
Loading