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_Paraminherits fromwx.Framebut works headlessly when nowx.Appis running (or wheninit_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 |
|---|---|
|
Active values — what the simulation actually uses |
|
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:
|
Stored in |
|---|---|
|
Both active and default |
|
Active only |
|
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
A reference parameter (e.g.
nb_sectionsin groupConfig) holds the current count.A template group named with
$n$(e.g.Section$n$) defines the parameters each instance should contain.When
update_incremental_groups_params()is called (or when the GUI refreshes), the template is expanded intoSection1,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 |
|
Add parameter |
|
Read value (typed) |
|
Set value |
|
Read with default |
|
List groups |
|
List params in group |
|
Check existence |
|
Count params |
|
Get type / comment |
|
Save to file |
|
Load from file |
|
Load from string |
|
Deep copy |
|
Compare / diff |
|
Active vs Default |
|
Add to active only |
|
Add to default only |
|
Read from default |
|
Cleanup active=default |
|
Incrementable groups |
|
Declare template group |
|
Expand instances |
|
Query inc. group count |
|
See also:
Factory methods — quick object creation patterns
WolfArray tutorial — arrays use
Wolf_Paramfor simulation configs