Wolf_Param & Project — Parameter management

  • ``Wolf_Param`` — hierarchical parameter container (groups → keys → typed values)

  • ``key_Param`` / ``Type_Param`` — enums for parameter structure and typing

  • ``Wolf2D_Project`` — project files linking simulations, vectors, and palettes

Wolf_Param inherits from wx.Frame but works headlessly when no wx.App is running (or when init_GUI=False). All examples below run without GUI.

Internal structure

Each parameter is a dict keyed by key_Param enum values:

{
    key_Param.NAME:    "dt",
    key_Param.TYPE:    Type_Param.Float,
    key_Param.VALUE:   "0.5",            # always stored as str
    key_Param.COMMENT: "Time step [s]",
    key_Param.ADDED_JSON:   None,
    key_Param.ENUM_CHOICES: None,
}

Parameters are organized in groups. Wolf_Param.myparams is a dict[str, dict]:

myparams = {
    "simulation": { "dt": {...}, "duration": {...} },
    "mesh":       { "dx": {...}, "nx": {...} },
}

Available types (Type_Param): Integer, Float, Double, Logical, String, File, Directory, Color, Enum, Empty, …

[1]:
from wolfhece.PyParams import Wolf_Param, Type_Param, key_Param

Creating parameters from scratch (headless)

[2]:
# Create an empty Wolf_Param (no file, no GUI)
p = Wolf_Param(to_read=False, init_GUI=False, title='My simulation')

# Add a group with typed parameters
p.addparam('simulation', 'duration', '3600', Type_Param.Float, 'Simulation duration [s]')
p.addparam('simulation', 'dt',       '0.5',  Type_Param.Float, 'Time step [s]')
p.addparam('simulation', 'nsteps',   '100',  Type_Param.Integer, 'Output steps')
p.addparam('simulation', 'verbose',  '1',    Type_Param.Logical, 'Verbose output')

# Add another group
p.addparam('mesh', 'dx', '1.0', Type_Param.Float, 'Cell size X [m]')
p.addparam('mesh', 'dy', '1.0', Type_Param.Float, 'Cell size Y [m]')
p.addparam('mesh', 'nx', '200', Type_Param.Integer, 'Number of cells X')
p.addparam('mesh', 'ny', '150', Type_Param.Integer, 'Number of cells Y')

# List groups
print("Groups:", p.get_group_keys())
print("Params in 'simulation':", p.get_param_keys('simulation'))
print("Params in 'mesh':", p.get_param_keys('mesh'))
Groups: ['simulation', 'mesh']
Params in 'simulation': ['duration', 'dt', 'nsteps', 'verbose']
Params in 'mesh': ['dx', 'dy', 'nx', 'ny']

Reading and modifying parameters

Values are accessed via __getitem__ with a (group, key) tuple. The return is automatically typed (int, float, bool, str) according to Type_Param.

[14]:
# --- Read values (auto-typed) ---
dt = p[('simulation', 'dt')]
print(f"dt = {dt}  (type: {type(dt).__name__})")

nsteps = p[('simulation', 'nsteps')]
print(f"nsteps = {nsteps}  (type: {type(nsteps).__name__})")

verbose = p[('simulation', 'verbose')]
print(f"verbose = {verbose}  (type: {type(verbose).__name__})")

# --- Alternative: get_param with default fallback ---
manning = p.get_param('physics', 'manning', default_value=0.035)
print(f"manning = {manning}  (from default, group doesn't exist yet)")

# --- Modify a value ---
p[('simulation', 'dt')] = 0.25
print(f"\ndt after modification: {p[('simulation', 'dt')]}")

# --- Get the full parameter dict ---
dt_dict = p.get_param_dict('simulation', 'dt')
print(f"\nFull dict for 'dt': { {k.value: v for k, v in dt_dict.items()} }")
dt = 0.25  (type: float)
nsteps = 100  (type: int)
verbose = 1  (type: str)
manning = 0.035  (from default, group doesn't exist yet)

dt after modification: 0.25

Full dict for 'dt': {'name': 'dt', 'type': <Type_Param.Float: 'Float'>, 'value': 0.25, 'comment': 'Time step [s]', 'added_json': None, 'enum_choices': None}

Iterating over all groups and parameters

[15]:
for group in p.get_group_keys():
    nb_active, nb_default = p.get_nb_params(group)
    print(f"\n[{group}]  ({nb_active} active params)")
    for key in p.get_param_keys(group):
        val = p[(group, key)]
        typ = p.get_type(group, key)
        comment = p.get_help(group, key)[0]
        print(f"  {key:15s} = {str(val):10s}  ({typ.value:8s})  # {comment}")

[simulation]  (5 active params)
  duration        = 3600.0      (Float   )  # Simulation duration [s]
  dt              = 0.25        (Float   )  # Time step [s]
  nsteps          = 100         (Integer )  # Output steps
  verbose         = 1           (Logical )  # Verbose output
  solver          = Solver.EXPLICIT  (Enum    )  # Numerical solver

[mesh]  (4 active params)
  dx              = 1.0         (Float   )  # Cell size X [m]
  dy              = 1.0         (Float   )  # Cell size Y [m]
  nx              = 200         (Integer )  # Number of cells X
  ny              = 150         (Integer )  # Number of cells Y

Saving and loading parameter files

The file format is tab-separated: group_name: on its own line, then key\tvalue\tcomment (Type) for each parameter.

[16]:
import tempfile
from pathlib import Path

tmpdir = Path(tempfile.mkdtemp())
param_file = tmpdir / 'simulation.param'

# Save to file
p.Save(str(param_file))
print("Saved file content:")
print(param_file.read_text())

# Reload from file
p2 = Wolf_Param(filename=str(param_file), to_read=True, init_GUI=False)
print(f"\nReloaded groups: {p2.get_group_keys()}")
print(f"dt = {p2[('simulation', 'dt')]}")
Saved file content:
 simulation:
duration        3600
dt      0.25
nsteps  100
verbose 1
solver  Solver.EXPLICIT
 mesh:
dx      1.0
dy      1.0
nx      200
ny      150


Reloaded groups: ['simulation', 'mesh']
dt = 0.25

Active vs Default values

Wolf_Param maintains two parallel dictionaries:

Dict

Role

myparams

Active values — what the simulation actually uses

myparams_default

Default values — reference / factory settings

When reading a .param file, the code also looks for a .param.default companion file. If none exists, the current values are copied as defaults.

The whichdict argument of addparam() controls where a parameter is stored:

whichdict

Stored in

'' or 'All'

Both active and default

'Active'

Active only

'Default'

Default only

Reading with p[(group, key)] (i.e. get_param) checks active first, then falls back to default. This means you can delete a param from active and the default value will transparently take over.

[17]:
# --- Add params to different dicts ---
pd = Wolf_Param(to_read=False, init_GUI=False, title='Active vs Default demo')

# By default (whichdict=''), params go to BOTH active and default
pd.addparam('physics', 'gravity', '9.81', Type_Param.Float, 'Gravitational acceleration')
pd.addparam('physics', 'manning', '0.035', Type_Param.Float, 'Manning coefficient')

print("Active groups:", pd.get_group_keys())
print("Default groups:", pd.get_default_group_keys())

# Read from active (primary)
print(f"\ngravity (active) = {pd[('physics', 'gravity')]}")

# Modify only the active value
pd[('physics', 'manning')] = 0.05
print(f"manning active  = {pd[('physics', 'manning')]}")
print(f"manning default = {pd._get_param_def('physics', 'manning')}")

# --- Check membership ---
print(f"\nis_in_active('physics', 'manning')?  {pd.is_in_active('physics', 'manning')}")
print(f"is_in_default('physics', 'manning')? {pd.is_in_default('physics', 'manning')}")

# --- Count active vs default ---
nb_a, nb_d = pd.get_nb_params('physics')
print(f"\nphysics: {nb_a} active params, {nb_d} default params")

# --- Add a param only to Active ---
pd.addparam('physics', 'rho', '1000', Type_Param.Float, 'Water density',
            whichdict='Active')
print(f"\nrho in active?  {pd.is_in_active('physics', 'rho')}")
print(f"rho in default? {pd.is_in_default('physics', 'rho')}")

# --- compare_active_to_default: remove active entries that match default ---
# Reset manning to its default value first
pd[('physics', 'manning')] = 0.035
pd.compare_active_to_default(remove_from_active_if_same=True)
# manning is now removed from active (same as default), but still readable via fallback
print(f"\nmanning after cleanup = {pd[('physics', 'manning')]}  (falls back to default)")
Active groups: ['physics']
Default groups: ['physics']

gravity (active) = 9.81
manning active  = 0.05
manning default = 0.035

is_in_active('physics', 'manning')?  True
is_in_default('physics', 'manning')? True

physics: 2 active params, 2 default params

rho in active?  True
rho in default? False

manning after cleanup = 0.035  (falls back to default)

Incrementable groups

Some simulations need a variable number of parameter groups — e.g. one group per cross-section, per boundary condition, or per material layer. Wolf_Param handles this with incrementable groups.

How it works

  1. A reference parameter (e.g. nb_sections in group Config) holds the current count.

  2. A template group named with $n$ (e.g. Section$n$) defines the parameters each instance should contain.

  3. When update_incremental_groups_params() is called (or when the GUI refreshes), the template is expanded into Section1, Section2, … up to the current count. Extra groups are saved internally and restored if the count is later increased.

Declaring syntax

The full form embeds everything in the group name:

"Section$n(Config, nb_sections, 1, 10)$"
│         │  │          │        │   │
│         │  │          │        │   └── max instances
│         │  │          │        └── min instances
│         │  │          └── reference parameter name
│         │  └── reference group name
│         └── placeholder for the index
└── base group name

Or use add_IncGroup() explicitly and then add params with whichdict='IncGroup'.

[18]:
# --- Incrementable groups: compact syntax ---
pi = Wolf_Param(to_read=False, init_GUI=False, title='Incrementable demo')

# Reference parameter: how many sections do we have?
pi.addparam('Config', 'nb_sections', '3', Type_Param.Integer, 'Number of sections')

# Template group — the $n(...)$ syntax auto-registers the incrementable group
pi.addparam('Section$n(Config, nb_sections, 1, 10)$', 'name',
            'default', Type_Param.String, 'Section name')
pi.addparam('Section$n(Config, nb_sections, 1, 10)$', 'manning',
            '0.035', Type_Param.Float, 'Manning coefficient')
pi.addparam('Section$n(Config, nb_sections, 1, 10)$', 'width',
            '5.0', Type_Param.Float, 'Section width [m]')

print(f"Incrementable groups registered: {pi.get_nb_inc_groups()}")

# Expand: creates Section1, Section2, Section3 from the template
pi.update_incremental_groups_params()

print(f"\nActive groups after expansion: {pi.get_group_keys()}")
for g in pi.get_group_keys():
    if g.startswith('Section'):
        keys = pi.get_param_keys(g)
        print(f"  [{g}] params: {keys}")

# Modify one instance
pi[('Section2', 'manning')] = 0.05
print(f"\nSection2 manning = {pi[('Section2', 'manning')]}")
print(f"Section3 manning = {pi[('Section3', 'manning')]}  (still default)")

# --- Change the count: increase to 5 ---
pi[('Config', 'nb_sections')] = 5
pi.update_incremental_groups_params()
print(f"\nAfter increasing to 5: {pi.get_group_keys()}")

# Section2's modified value is preserved
print(f"Section2 manning = {pi[('Section2', 'manning')]}  (preserved)")

# --- Decrease back to 2: extra groups are saved internally ---
pi[('Config', 'nb_sections')] = 2
pi.update_incremental_groups_params()
print(f"\nAfter decreasing to 2: {pi.get_group_keys()}")

# Increase again: saved values come back
pi[('Config', 'nb_sections')] = 5
pi.update_incremental_groups_params()
print(f"Section2 manning after restore = {pi[('Section2', 'manning')]}  (restored)")
WARNING:root:WARNING : group is incrementable. -- You must use 'IncGroup' for whichdict
WARNING:root:WARNING : group is incrementable. -- You must use 'IncGroup' for whichdict
WARNING:root:WARNING : group is incrementable. -- You must use 'IncGroup' for whichdict
Incrementable groups registered: 1

Active groups after expansion: ['Config', 'Section1', 'Section2', 'Section3']
  [Section1] params: ['name', 'manning', 'width']
  [Section2] params: ['name', 'manning', 'width']
  [Section3] params: ['name', 'manning', 'width']

Section2 manning = 0.05
Section3 manning = 0.035  (still default)

After increasing to 5: ['Config', 'Section1', 'Section2', 'Section3', 'Section4', 'Section5']
Section2 manning = 0.05  (preserved)

After decreasing to 2: ['Config', 'Section1', 'Section2']
Section2 manning after restore = 0.05  (restored)

Building from a raw string

fill_from_strings parses parameter text directly — useful for tests or embedding default configurations in code.

[6]:
raw = """\
physics:
manning\t0.035\tManning coefficient (Float)
gravity\t9.81\tGravitational acceleration (Float)
boundary:
upstream_Q\t120.0\tUpstream discharge [m3/s] (Float)
downstream_h\t2.5\tDownstream water level [m] (Float)
"""

p3 = Wolf_Param(to_read=False, init_GUI=False)
p3.fill_from_strings(raw)

print(f"Groups: {p3.get_group_keys()}")
print(f"manning = {p3[('physics', 'manning')]}  (type: {type(p3[('physics', 'manning')]).__name__})")
print(f"upstream_Q = {p3[('boundary', 'upstream_Q')]}")
Groups: ['physics', 'boundary']
manning = 0.035  (type: str)
upstream_Q = 120.0

Copying and comparing

[19]:
# Deep copy (data only, no GUI)
p_copy = p.copy()
p_copy[('simulation', 'dt')] = 1.0

print(f"Original dt = {p[('simulation', 'dt')]}")
print(f"Copy dt     = {p_copy[('simulation', 'dt')]}")

# Compare two param sets
same = p.is_like(p_copy)
print(f"\nIdentical? {same}")

# Show differences
diffs = p.diff(p_copy)
if diffs:
    for k, (val_a, val_b) in diffs.items():
        if isinstance(k, tuple):
            group, key = k
            print(f"  [{group}] {key}: {val_a} -> {val_b}")
        else:
            print(f"  {k}: {val_a} -> {val_b}")
WARNING:root:ERROR : cannot find the following file :
Original dt = 0.25
Copy dt     = 1.0

Identical? False
  [simulation] dt: {<key_Param.NAME: 'name'>: 'dt', <key_Param.TYPE: 'type'>: <Type_Param.Float: 'Float'>, <key_Param.VALUE: 'value'>: 0.25, <key_Param.COMMENT: 'comment'>: 'Time step [s]', <key_Param.ADDED_JSON: 'added_json'>: None, <key_Param.ENUM_CHOICES: 'enum_choices'>: None} -> {<key_Param.NAME: 'name'>: 'dt', <key_Param.TYPE: 'type'>: <Type_Param.Float: 'Float'>, <key_Param.VALUE: 'value'>: 1.0, <key_Param.COMMENT: 'comment'>: 'Time step [s]', <key_Param.ADDED_JSON: 'added_json'>: None, <key_Param.ENUM_CHOICES: 'enum_choices'>: None}

Checking existence and using Enum parameters

[13]:
# --- Existence checks ---
print(f"'simulation' group exists? {p.is_in_active('simulation')}")
print(f"'simulation/dt' exists?    {p.is_in_active('simulation', 'dt')}")
print(f"'physics' group exists?    {p.is_in_active('physics')}")

# --- Enum-typed parameter (dropdown in GUI) ---
from enum import Enum

class Solver(Enum):
    EXPLICIT = 'explicit'
    IMPLICIT = 'implicit'
    SEMIIMPLICIT = 'semi-implicit'

# Value must be the Enum member itself (not .value),
# because __getitem__ checks isinstance(value, enum_choices).
p.addparam('simulation', 'solver', Solver.EXPLICIT,
           Type_Param.Enum, 'Numerical solver',
           enum_choices=Solver)

print(f"\nsolver = {p[('simulation', 'solver')]}")
'simulation' group exists? True
'simulation/dt' exists?    True
'physics' group exists?    False

solver = Solver.EXPLICIT

Wolf2D_Project — linking simulations, vectors, and palettes

Wolf2D_Project is a specialized Project that organizes:

  • Simulations (WOLF2D CPU/GPU) with their file paths

  • Vectors (polygons for zone-based analysis)

  • Palettes (color maps for visualization)

  • Links between simulations ↔ vectors, simulations ↔ palettes

Project file format

wolf2d:
SimName_A    .\Q25\sim_A\simul
SimName_B    .\Q25\sim_B\simul
vector:
zone1        ..\Vectors\zone1.vec
vector_array_link:
SimName_A    zone1
SimName_B    zone1
palette:
Q25          q25.pal
palette-array:
SimName_A    Q25
SimName_B    Q25

Creating a project in the GUI

from wolfhece.multiprojects import Wolf2D_Project

proj = Wolf2D_Project(
    wdir='path/to/project/',
    filename='my_project.param',
    toShow=True   # opens the parameter editor
)

# Load all linked simulations
proj.load_all()

# Get a specific simulation result
res = proj['wolf2d', 'SimName_A']

Summary

Task

Code

Create empty

Wolf_Param(to_read=False, init_GUI=False)

Add parameter

p.addparam(group, key, value, Type_Param.Float, comment)

Read value (typed)

p[('group', 'key')]

Set value

p[('group', 'key')] = val

Read with default

p.get_param('group', 'key', default_value=0)

List groups

p.get_group_keys() / get_default_group_keys()

List params in group

p.get_param_keys('group')

Check existence

p.is_in_active(g, k) / p.is_in_default(g, k)

Count params

p.get_nb_params('group')(nb_active, nb_default)

Get type / comment

p.get_type(g, k), p.get_help(g, k)

Save to file

p.Save('path.param')

Load from file

Wolf_Param(filename=f, init_GUI=False)

Load from string

p.fill_from_strings(text)

Deep copy

p.copy()

Compare / diff

p.is_like(other), p.diff(other)

Active vs Default

Add to active only

p.addparam(..., whichdict='Active')

Add to default only

p.addparam(..., whichdict='Default')

Read from default

p._get_param_def('group', 'key')

Cleanup active=default

p.compare_active_to_default(remove_from_active_if_same=True)

Incrementable groups

Declare template group

p.addparam('Grp$n(ref_g, ref_p, min, max)$', ...)

Expand instances

p.update_incremental_groups_params()

Query inc. group count

p.get_nb_inc_groups()

See also: