Skip to content

specl: CONDITIONAL/IF-THEN (parser and code) #198

Open

Description

@rburghol

@timcera

Overview

  • This enables nested IF-THEN-ELSE statements in hsp2.
  • Execution should follow an "as appears" in UCI or in hdf5 table (or CSV if applicable)
  • Tasks:
    • Parse complicated IF-THEN statement and store in h5/pandas friendly array
    • Add pointer to related IF-THEN in special ACTIONS parser (see specl: ACTIONS (parser and code) #105)
    • Enable UVNAME to facilitate evaluation of IF... (see specl: UVNAME (parser and code) #106 )
    • Develop data model for tokenized runtime object SpecialCondition in om_special_condition.py
    • Add runtime token for parent_ix and sibling_ix in SpecialAction and step_special_action() (see specl: ACTIONS (parser and code) #105)
    • Develop runtime execution step_special_condition() in om_special_condition.py

Examples

Data Model

Path and Column Definitions for hdf5

  • Path: /SPECACTIONS/conditions
  • cond_id: unique integer ID for this condition from parsing
  • parent_id: unique integer ID for containing condition if applicable
  • sibling_id: unique integer ID for most immediate prior conditions that share the same parent_id i.e. related ELSE statements (will be empty if not nested if/else) -- will only be the related statement that appears before the statement in the UCI
  • expression: string representation of conditional (normally as outlined in the UCI, but could be defined in hdf5 or CSV)

Example Table Entry

  • Shows a condition that has no parent, no siblings, just a stand-alone IF-THEN block of actions
cond_id parent_ix sibling_ix expression
0 -1 -1 'IF (precx1 > 6.000) THEN'

Code

UCI excerpt (see

Example 1: Adjust infiltration exp under high precip daily total.

***User-Defined Variable Quantity Line
  UVQUAN precx1 PERLND   1 PREC             3                 DY 14 SUM

*** ACTIONS
***optyp range dc ds yr  mo da hr mn d t   vari  s1 s2 s3 ac  value    tc ts num
  <****><-><--><><-><--><-><-><-><-><><>  <----><-><-><-><-><--------> <> <-><->

***Action Lines for INFEXP
IF (precx1 >  6.000) THEN
  PERLND  1                            3  INFEXP          =  8.0000000
ELSE
  PERLND  1                            3  INFEXP          =  2.0000000
END IF

Example 2:

***Action Lines for INFEXP
IF (precx1 >  6.000) THEN
  PERLND  1                            3  INFEXP          =  8.0000000
ELSE
  PERLND  1                            3  INFEXP          =  2.0000000
END IF

Operational model Design:

  • Each IF statement will be given it's own state_ix, and it's value will be 1/0 depending on the conditional evaluation at each time step (the step_model() method).
    • ix: Their state_ix value is their definitive active/inactive status
    • parent_ix: They will have an attribute that points to the state_ix of a parent conditional statement if it is nested inside another IF-THEN (this value will be -1 if they are not contained)
    • arg1_ix, arg2_ix, op: They will have attributes that define their input state_ix pointers, and their operator (an integer)
    • $V = AND(C_{self}, C_{p})$; where $C_{self}$ = conditional statement result and $C_{p}$ = parent conditional (if present)
  • Sub-conditionals ELSE/ELSE IF:
    • ix: Their state_ix value is their definitive active/inactive status
    • parent_ix: They will have an attribute that points to the state_ix of a parent conditional statement if it is nested inside another IF-THEN (this value will be -1 if they are not contained)
    • sibling_ix: The state_ix index of their prior sibling conditional. (assumes the activity state of the sibling aggregates any/all prior siblings state)
    • Their state_ix ACTIVE status will be the NOR of all previous sibling elements of their conditional tree and their own conditional evaluation.
    • $V = AND(C_{self}, C_{p}, NOR(C_{sibs})$; where $C_{self}$ = conditional statement result and $C_{p}$ = parent conditional (if present) and $C_{sibs}$ = siblings 1 to n value
    • Example:
      • IF an ELSE IF statement is preceded by another ELSE IF and an IF statement,
      • $V = AND(C_{self}, C_{p}, NOT(C_{sib} )$
      • $1 = AND(1, 1, NOT(0) )$
      • $0 = AND(1, 0, NOT(0) )$
      • $0 = AND(1, 1, NOT(1) )$
  • All conditional components will be evaluated during the beginning of the step() evaluation (so that changes of state are evaluated in the moment)
  • @njit code will check enabled state value and return if enabled == 0 (this should be quite fast, despite evaluating many times)

Activity

  1. rburghol commented on Jan 15, 2026

    @rburghol
    ContributorAuthor

    IF-THEN multiline parsing:

    • From Tim:
    words = line.split()
    ...
    words[0] == "IF" or f"{words[0]} {words[1]}" == "ELSE IF":
                        continuation_lines = [" ".join(line.strip().split())]
                        while True:
                            loop_words = continuation_lines[-1].split()
                            if loop_words[-1] == "THEN":
                                continuation_line = " ".join(continuation_lines)
                                break
                            continuation_lines.append(" ".join(line.next().strip().split()))
    

    Testing: Proof of concept.

    • Can we send an iter() list to a sub-function that then goes ahead and pops a couple of lines off in order to track a multi-line IF statement?
    • Answer: yes. The iterable object is incremented by the called function, and that is reflected by essentially skipping that line when it returns to the original loop.
    • Todo: use words method from above
      • words = line.split()
      • Allows us to ask words[0] == "IF" or f"{words[0]} {words[1]}" == "ELSE IF": as opener for a new conditional.
    def get_ifs(lines, line, line_end='THEN'):
        end_ln = len(line_end)
        while line[-end_ln:] != line_end:
            nline = next(lines).strip()
            line = line + " " + nline
        return(line)
    
    llines = ["IF h = 1 THEN", "IF k = 4", "AND z = 5", "AND i < 25 THEN", "v = 1", "ELSE IF b = 6 THEN", "ELSE IF v = 2 THEN"]
    lines = iter(llines)
    n = 0
    for line in lines:
        n = n + 1
        line = line.strip()
        if line[0:2] == 'IF':
            l = get_ifs(lines, line )
        else:
            l = line
        print(n, ":", l)
    
    

    Output:

    1 : IF h = 1 THEN
    2 : IF k = 4 AND z = 5 AND i < 25 THEN
    3 : v = 1
    4 : ELSE IF b = 6 THEN
    5 : ELSE IF v = 2 THEN
    
    
  2. timcera commented on Jul 8, 2026

    @timcera
    Contributor

    I think resolved by #225, by implementing parsing function developed by @rburghol that accumulates conditions into a "conditions" table and each record in the "ACTIONS" table points to it's matching condition in the "conditions" table.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions