FZ - Parametric Scientific Computing Framework
A powerful Python package for parametric simulations and computational experiments. FZ wraps your simulation codes to automatically run parametric studies, manage input/output files, handle parallel execution, and collect results in structured DataFrames.
Table of Contents
- Features
- Installation
- Quick Start
- CLI Usage
- Core Functions
- Model Definition
- Calculator Types
- Advanced Features
- Complete Examples
- Writing Custom Algorithms for fzd
- Configuration
- Interrupt Handling
- Breaking Changes
- Development
- Troubleshooting
- Performance Tips
- Documentation
- Support
Features
Core Capabilities
- 🔄 Parametric Studies: Factorial designs (dict with Cartesian product) or non-factorial designs (DataFrame with specific cases)
- ⚡ Parallel Execution: Run multiple cases concurrently across multiple calculators with automatic load balancing
- 💾 Smart Caching: Reuse previous calculation results based on input file hashes to avoid redundant computations
- 🔁 Retry Mechanism: Automatically retry failed calculations with alternative calculators
- 🌐 Remote Execution: Execute calculations on remote servers via SSH with automatic file transfer
- 📊 DataFrame I/O: Input and output using pandas DataFrames with automatic type casting and variable extraction
- 🛑 Interrupt Handling: Gracefully stop long-running calculations with Ctrl+C while preserving partial results
- 🔍 Formula Evaluation: Support for calculated parameters using Python or R expressions
- 📁 Directory Management: Automatic organization of inputs, outputs, and logs for each case
- 🎯 Adaptive Algorithms: Iterative design of experiments with intelligent sampling strategies (fzd)
- ⚠️ Error Reporting: Protocol-specific error classification with descriptive messages recorded in results
- 🖥️ Cross-Platform: Works on Linux, macOS, and Windows (MSYS2/Git Bash) with configurable shell paths
Six Core Functions
fzi- Parse Input files to identify variablesfzc- Compile input files by substituting variable valuesfzo- Parse Output files from calculationsfzr- Run complete parametric calculations end-to-endfzd- Run iterative Design of experiments with adaptive algorithmsfzl- List and validate installed models and calculators
Installation
Using pip
pip install funz-fz
Using pipx (recommended for CLI tools)
pipx install funz-fz
pipx installs the package in an isolated environment while making the CLI commands (fz, fzi, fzc, fzo, fzr, fzl, fzd) available globally.
From Source
git clone https://github.com/Funz/fz.git
cd fz
pip install -e .
Or straight from GitHub via pip:
pip install --break-system-packages --upgrade --force-reinstall "git+https://github.com/Funz/fz.git"
- '--upgrade --force-reinstall' option to force update of possible previous installation
- '--break-system-packages' option to enable user-wide installation (ie. not in dedicated venv)
Dependencies
# Optional dependencies:
# for SSH support
pip install paramiko
# for DataFrame support (recommended)
pip install pandas
# for fzd (design of experiments) - REQUIRED
pip install pandas
# for R interpreter support
pip install funz-fz[r]
# OR
pip install rpy2
# Note: Requires R installed with system libraries - see examples/r_interpreter_example.md
# for optimization algorithms (scipy-based algorithms in examples/)
pip install scipy numpy
Quick Start
Here's a complete example for a simple parametric study:
1. Create an Input Template
Create input.txt:
# input file for Perfect Gaz Pressure, with variables n_mol, T_celsius, V_L
n_mol=$n_mol
T_kelvin=@{$T_celsius + 273.15}
#@ def L_to_m3(L):
#@ return(L / 1000)
V_m3=@{L_to_m3($V_L)}
Or using R for formulas (assuming R interpreter is set up: fz.set_interpreter("R")):
# input file for Perfect Gaz Pressure, with variables n_mol, T_celsius, V_L
n_mol=$n_mol
T_kelvin=@{$T_celsius + 273.15}
#@ L_to_m3 <- function(L) {
#@ return (L / 1000)
#@ }
V_m3=@{L_to_m3($V_L)}
2. Create a Calculation Script
Create PerfectGazPressure.sh:
#!/bin/bash
# read input file
source $1
sleep 5 # simulate a calculation time
echo 'pressure = '`echo "scale=4;$n_mol*8.314*$T_kelvin/$V_m3" | bc` > output.txt
echo 'Done'
Make it executable:
chmod +x PerfectGazPressure.sh
3. Run Parametric Study
Create run_study.py:
import fz
# Define the model
model = {
"varprefix": "$",
"formulaprefix": "@",
"delim": "{}",
"commentline": "#",
"output": {
"pressure": "grep 'pressure = ' output.txt | awk '{print $3}'"
# or, shell-free (portable, no bash/FZ_SHELL_PATH needed):
# "pressure": "python://grep(r'pressure = (\\S+)', 'output.txt')"
# or, for JSON results, using jq (requires the jq executable):
# "pressure": "jq://.pressure output.json"
# or, for YAML/JSON/XML/TOML results, using yq (requires yq):
# "pressure": "yq://.pressure output.yaml"
# or, for XML results, using XPath (requires xmllint):
# "pressure": "xpath://'//pressure/text()' output.xml"
}
}
# Define parameter values
input_variables = {
"T_celsius": [10, 20, 30, 40], # 4 temperatures
"V_L": [1, 2, 5], # 3 volumes
"n_mol": 1.0 # fixed amount
}
# Run all combinations (4 × 3 = 12 cases)
results = fz.fzr(
"input.txt",
input_variables,
model,
calculators="sh://bash PerfectGazPressure.sh",
results_dir="results"
)
# Display results
print(results)
print(f"\nCompleted {len(results)} calculations")
Run it:
python run_study.py
Expected output:
T_celsius V_L n_mol pressure status calculator error command
0 10 1.0 1.0 235358.1200 done sh:// None bash...
1 10 2.0 1.0 117679.0600 done sh:// None bash...
2 10 5.0 1.0 47071.6240 done sh:// None bash...
3 20 1.0 1.0 243730.2200 done sh:// None bash...
...
Completed 12 calculations
CLI Usage
FZ provides command-line tools for quick operations without writing Python scripts. All four core functions are available as CLI commands.
Installation of CLI Tools
The CLI commands are automatically installed when you install the fz package:
pip install -e .
Available commands:
fz- Main entry point (general configuration, plugins management, logging, ...)fzi- Parse input variablesfzc- Compile input filesfzo- Read output filesfzr- Run parametric calculationsfzl- List and validate installed models and calculatorsfzd- Run design of experiments with adaptive algorithms
fzi - Parse Input Variables
Identify variables in input files:
# Parse a single file
fzi input.txt --model perfectgas
# Parse a directory
fzi input_dir/ --model mymodel
# Output formats
fzi input.txt --model perfectgas --format json
fzi input.txt --model perfectgas --format table
fzi input.txt --model perfectgas --format csv
Example:
$ fzi input.txt --model perfectgas --format table
┌──────────────┬───────┐
│ Variable │ Value │
├──────────────┼───────┤
│ T_celsius │ None │
│ V_L │ None │
│ n_mol │ None │
└──────────────┴───────┘
With inline model definition:
fzi input.txt \
--varprefix '$' \
--delim '{}' \
--format json
Output (JSON):
{
"T_celsius": null,
"V_L": null,
"n_mol": null
}
fzc - Compile Input Files
Substitute variables and create compiled input files:
# Basic usage
fzc input.txt \
--model perfectgas \
--variables '{"T_celsius": 25, "V_L": 10, "n_mol": 1}' \
--output compiled/
# Grid of values (creates subdirectories)
fzc input.txt \
--model perfectgas \
--variables '{"T_celsius": [10, 20, 30], "V_L": [1, 2], "n_mol": 1}' \
--output compiled_grid/
Directory structure created:
compiled_grid/
├── T_celsius=10,V_L=1/
│ └── input.txt
├── T_celsius=10,V_L=2/
│ └── input.txt
├── T_celsius=20,V_L=1/
│ └── input.txt
...
Using formula evaluation:
# Input file with formulas
cat > input.txt << 'EOF'
Temperature: $T_celsius C
#@ T_kelvin = $T_celsius + 273.15
Calculated T: @{T_kelvin} K
EOF
# Compile with formula evaluation
fzc input.txt \
--varprefix '$' \
--formulaprefix '@' \
--delim '{}' \
--commentline '#' \
--variables '{"T_celsius": 25}' \
--output compiled/
fzo - Read Output Files
Parse calculation results:
# Read single directory
fzo results/case1/ --model perfectgas --format table
# Read directory with subdirectories
fzo results/ --model perfectgas --format json
# Different output formats
fzo results/ --model perfectgas --format csv > results.csv
fzo results/ --model perfectgas --format html > results.html
fzo results/ --model perfectgas --format markdown
Example output:
$ fzo results/ --model perfectgas --format table
┌─────────────────────────┬──────────┬────────────┬──────┬───────┐
│ path │ pressure │ T_celsius │ V_L │ n_mol │
├─────────────────────────┼──────────┼────────────┼──────┼───────┤
│ T_celsius=10,V_L=1 │ 235358.1 │ 10 │ 1.0 │ 1.0 │
│ T_celsius=10,V_L=2 │ 117679.1 │ 10 │ 2.0 │ 1.0 │
│ T_celsius=20,V_L=1 │ 243730.2 │ 20 │ 1.0 │ 1.0 │
└─────────────────────────┴──────────┴────────────┴──────┴───────┘
With inline model definition:
fzo results/ \
--output-cmd pressure="grep 'pressure = ' output.txt | awk '{print \$3}'" \
--output-cmd temperature="cat temp.txt" \
--format json
fzl - List and Validate Models/Calculators
List installed models and calculators with optional validation:
# List all models and calculators
fzl
# List with validation checks
fzl --check
# Filter by pattern
fzl --models "perfect*" --calculators "ssh*"
# Different output formats
fzl --format json
fzl --format table
fzl --format markdown # default
Example output:
$ fzl --check --format table
=== MODELS ===
Model: perfectgas ✓
Path: /home/user/project/.fz/models/perfectgas.json
Supported Calculators: 2
- local
- ssh_cluster
Model: navier-stokes ✗
Path: /home/user/.fz/models/navier-stokes.json
Error: Missing required field 'output'
Supported Calculators: 0
=== CALCULATORS ===
Calculator: local ✓
Path: /home/user/project/.fz/calculators/local.json
URI: sh://
Models: 1
- perfectgas
Calculator: ssh_cluster ✓
Path: /home/user/.fz/calculators/ssh_cluster.json
URI: ssh://user@cluster.edu
Models: 2
- perfectgas
- navier-stokes
fzr - Run Parametric Calculations
Execute complete parametric studies from the command line:
# Basic usage
fzr input.txt \
--model perfectgas \
--variables '{"T_celsius": [10, 20, 30], "V_L": [1, 2], "n_mol": 1}' \
--calculator "sh://bash PerfectGazPressure.sh" \
--results results/
# Multiple calculators for parallel execution
fzr input.txt \
--model perfectgas \
--variables '{"param": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}' \
--calculator "sh://bash calc.sh" \
--calculator "sh://bash calc.sh" \
--calculator "sh://bash calc.sh" \
--results results/ \
--format table
Using cache:
# First run
fzr input.txt \
--model perfectgas \
--variables '{"T_celsius": [10, 20, 30], "V_L": [1, 2]}' \
--calculator "sh://bash PerfectGazPressure.sh" \
--results run1/
# Resume with cache (only runs missing cases)
fzr input.txt \
--model perfectgas \
--variables '{"T_celsius": [10, 20, 30, 40], "V_L": [1, 2, 3]}' \
--calculator "cache://run1" \
--calculator "sh://bash PerfectGazPressure.sh" \
--results run2/ \
--format table
Remote SSH execution:
fzr input.txt \
--model mymodel \
--variables '{"mesh_size": [100, 200, 400]}' \
--calculator "ssh://user@cluster.edu/bash /path/to/submit.sh" \
--results hpc_results/ \
--format json
Output formats:
# Table (default)
fzr input.txt --model perfectgas --variables '{"x": [1, 2, 3]}' --calculator "sh://calc.sh"
# JSON
fzr ... --format json
# CSV
fzr ... --format csv > results.csv
# Markdown
fzr ... --format markdown
# HTML
fzr ... --format html > results.html
fzd - Design of Experiments
Run iterative design of experiments with adaptive algorithms:
# Basic usage with random sampling
fzd --input_dir input/ \
--model perfectgas \
--input_vars '{"x": "[-2;2]", "y": "[-2;2]"}' \
--output_expression "result" \
--algorithm examples/algorithms/randomsampling.py \
--options '{"nvalues": 20, "seed": 42}'
# With multiple calculators for parallel evaluation
fzd --input_dir input/ \
--model perfectgas \
--input_vars '{"x": "[-2;2]", "y": "[-2;2]"}' \
--output_expression "result" \
--algorithm examples/algorithms/bfgs.py \
--calculators '["sh://bash calc.sh", "sh://bash calc.sh"]' \
--options '{"max_iter": 20, "tol": 1e-4}' \
--results_dir optimization_results/
Algorithm options from file:
# Store options in a JSON file
cat > algo_config.json << 'EOF'
{"nvalues": 50, "seed": 42}
EOF
fzd -i input/ -m perfectgas \
-v '{"x": "[-2;2]", "y": "[-2;2]"}' \
-e "result" \
-a examples/algorithms/randomsampling.py \
-o algo_config.json
Also available as subcommand: fz design ...
fz install / uninstall
Install models or algorithms from GitHub or local zip files:
# Install a model (to .fz/models/ in current project)
fz install model perfectgas
fz install model https://github.com/user/model-repo.git
# Install globally (to ~/.fz/models/)
fz install model perfectgas --global
# Install an algorithm
fz install algorithm https://github.com/user/algo-repo.git
# Uninstall
fz uninstall model perfectgas
fz uninstall algorithm myalgo
CLI Options Reference
Common Options (all commands)
--help, -h Show help message
--version Show version
--model MODEL Model alias or inline definition
--varprefix PREFIX Variable prefix (default: $)
--delim DELIMITERS Formula delimiters (default: {})
--formulaprefix PREFIX Formula prefix (default: @)
--commentline CHAR Comment character (default: #)
--format FORMAT Output format: json, table, csv, markdown, html
Exit Codes and Output Streams
CLI commands are script-friendly: results are printed to stdout, while log messages
(FZ_LOG_LEVEL), the progress bar, and error messages go to stderr. The progress bar
is automatically disabled when stderr is not a terminal (piped/redirected output or CI).
# stdout contains only the JSON results; logs and progress stay on stderr
fzr input.txt --model perfectgas --variables '{"x": [1, 2]}' \
--calculator "sh://bash calc.sh" --format json > results.json 2> run.log
Exit codes: 0 on success, non-zero on errors (invalid arguments, missing files, ...).
fzr also exits with 1 when no case succeeded; partial success exits 0 with
per-case details in the status column.
Argument Formats
FZ CLI commands support three flexible formats for specifying models, calculators, and variables:
1. Inline JSON - Direct JSON string:
fzr input.txt \
--model '{"varprefix": "$", "output": {"result": "cat output.txt"}}' \
--variables '{"temp": [10, 20, 30], "pressure": 1}' \
--calculator "sh://bash calc.sh"
2. JSON File - Path to JSON file:
# Create model file
cat > mymodel.json << 'EOF'
{
"varprefix": "$",
"formulaprefix": "@",
"delim": "{}",
"output": {
"result": "cat output.txt"
}
}
EOF
# Use file path
fzr input.txt --model mymodel.json --variables vars.json --calculator "sh://calc.sh"
3. Alias - Named configuration from .fz/ directory:
# Uses .fz/models/perfectgas.json
fzr input.txt --model perfectgas --calculator local
Automatic Detection:
- FZ automatically detects which format you're using
- Tries formats in order: Alias → JSON File → Inline JSON
- Provides helpful error messages if parsing fails
- Works for
--model,--calculator, and--variablesarguments
Format Detection Logic:
# Examples of automatic detection
"perfectgas" # → Alias (no .json, no braces)
"model.json" # → File (ends with .json)
'{"varprefix": "$"}' # → Inline JSON (starts with {)
Mixing Formats:
# Mix different formats in the same command
fzr input.txt \
--model perfectgas \ # Alias
--variables '{"temp": [10, 20, 30]}' \ # Inline JSON
--calculator cluster # Alias
Model Definition Options
Instead of using --model alias, you can define the model inline:
fzr input.txt \
--varprefix '$' \
--formulaprefix '@' \
--delim '{}' \
--commentline '#' \
--output-cmd pressure="grep 'pressure' output.txt | awk '{print \$2}'" \
--output-cmd temp="cat temperature.txt" \
--variables '{"x": 10}' \
--calculator "sh://bash calc.sh"
fzr-Specific Options
--calculator URI Calculator URI (can be specified multiple times)
--results DIR Results directory (default: results)
--case_naming SCHEME Case directory naming: path (default), hash, or index
Complete CLI Examples
Example 1: Quick Variable Discovery
# Check what variables are in your input files
$ fzi simulation_template.txt --varprefix '$' --format table
┌──────────────┬───────┐
│ Variable │ Value │
├──────────────┼───────┤
│ mesh_size │ None │
│ timestep │ None │
│ iterations │ None │
└──────────────┴───────┘
Example 2: Quick Compilation Test
# Test variable substitution
$ fzc simulation_template.txt \
--varprefix '$' \
--variables '{"mesh_size": 100, "timestep": 0.01, "iterations": 1000}' \
--output test_compiled/
$ cat test_compiled/simulation_template.txt
# Compiled with mesh_size=100
mesh_size=100
timestep=0.01
iterations=1000
Example 3: Parse Existing Results
# Extract results from previous calculations
$ fzo old_results/ \
--output-cmd energy="grep 'Total Energy' log.txt | awk '{print \$3}'" \
--output-cmd time="grep 'CPU Time' log.txt | awk '{print \$3}'" \
--format csv > analysis.csv
Example 4: End-to-End Parametric Study
#!/bin/bash
# run_study.sh - Complete parametric study from CLI
# 1. Parse input to verify variables
echo "Step 1: Parsing input variables..."
fzi input.txt --model perfectgas --format table
# 2. Run parametric study
echo -e "\nStep 2: Running calculations..."
fzr input.txt \
--model perfectgas \
--variables '{
"T_celsius": [10, 20, 30, 40, 50],
"V_L": [1, 2, 5, 10],
"n_mol": 1
}' \
--calculator "sh://bash PerfectGazPressure.sh" \
--calculator "sh://bash PerfectGazPressure.sh" \
--results results/ \
--format table
# 3. Export results to CSV
echo -e "\nStep 3: Exporting results..."
fzo results/ --model perfectgas --format csv > results.csv
echo "Results saved to results.csv"
Example 5: Using Model and Calculator Aliases
First, create model and calculator configurations:
# Create model alias
mkdir -p .fz/models
cat > .fz/models/perfectgas.json << 'EOF'
{
"varprefix": "$",
"formulaprefix": "@",
"delim": "{}",
"commentline": "#",
"output": {
"pressure": "grep 'pressure = ' output.txt | awk '{print $3}'"
},
"id": "perfectgas"
}
EOF
# Create calculator alias
mkdir -p .fz/calculators
cat > .fz/calculators/local.json << 'EOF'
{
"uri": "sh://",
"models": {
"perfectgas": "bash PerfectGazPressure.sh"
}
}
EOF
# Now run with short aliases
fzr input.txt \
--model perfectgas \
--variables '{"T_celsius": [10, 20, 30], "V_L": [1, 2]}' \
--calculator local \
--results results/ \
--format table
Example 6: Interrupt and Resume
# Start long-running calculation
fzr input.txt \
--model mymodel \
--variables '{"param": [1..100]}' \
--calculator "sh://bash slow_calc.sh" \
--results run1/
# Press Ctrl+C after some cases complete...
# ⚠️ Interrupt received (Ctrl+C). Gracefully shutting down...
# ⚠️ Execution was interrupted. Partial results may be available.
# Resume from cache
fzr input.txt \
--model mymodel \
--variables '{"param": [1..100]}' \
--calculator "cache://run1" \
--calculator "sh://bash slow_calc.sh" \
--results run1_resumed/ \
--format table
# Only runs the remaining cases
fzd - Run Design of Experiments
Run iterative design of experiments with adaptive algorithms:
# Basic usage with Monte Carlo algorithm
fzd --input_dir input/ \
--model perfectgas \
--input_vars '{"T_celsius": "[10;50]", "V_L": "[1;10]", "n_mol": "1"}' \
--calculators "sh://bash PerfectGazPressure.sh" \
--output_expression "pressure" \
--algorithm examples/algorithms/montecarlo_uniform.py \
--options '{"batch_sample_size": 20, "max_iterations": 10}' \
--results_dir fzd_results/
# With optimization algorithm (BFGS)
fzd --input_dir input/ \
--model perfectgas \
--input_vars '{"T_celsius": "[10;50]", "V_L": "[1;10]", "n_mol": "1"}' \
--calculators "sh://bash calc.sh" \
--output_expression "pressure" \
--algorithm examples/algorithms/bfgs.py \
--options '{"minimize": true, "max_iterations": 50}' \
--results_dir optimization_results/
# Fixed and variable inputs (V_L fixed at 5.0, T_celsius explored by algorithm)
fzd --input_dir input/ \
--model perfectgas \
--input_vars '{"T_celsius": "[10;50]", "V_L": "5.0", "n_mol": "1"}' \
--calculators "sh://bash calc.sh" \
--output_expression "pressure" \
--algorithm examples/algorithms/brent.py \
--results_dir brent_results/
Short form: fzd -i input/ -m perfectgas -v '...' -e "pressure" -a algo.py
Also available as subcommand: fz design --input_dir input/ ...
Key Differences from fzr:
--input_varsuses"[min;max]"for ranges (algorithm decides values) or"value"(string) for fixed- Requires
--algorithmwith algorithm name (randomsampling,brent,bfgs, ...) or path to.pyfile - Algorithm options via
--options(JSON dict or file) - Results directory via
--results_dir(default:results_fzd); if it already exists it is renamed with a timestamp and its cached results are still reused - Duplicate design points within a batch are automatically deduplicated and results reused
Environment Variables for CLI
# Set logging level
export FZ_LOG_LEVEL=DEBUG
fzr input.txt --model perfectgas ...
# Set maximum parallel workers
export FZ_MAX_WORKERS=4
fzr input.txt --model perfectgas --calculator "sh://calc.sh" ...
# Set retry attempts
export FZ_MAX_RETRIES=3
fzr input.txt --model perfectgas ...
# SSH configuration
export FZ_SSH_AUTO_ACCEPT_HOSTKEYS=1 # Use with caution
export FZ_SSH_KEEPALIVE=300
fzr input.txt --calculator "ssh://user@host/bash calc.sh" ...
# Shell path for binary resolution (Windows)
export FZ_SHELL_PATH="C:\msys64\usr\bin;C:\msys64\mingw64\bin"
fzr input.txt --model perfectgas ...
Core Functions
fzi - Parse Input Variables
Identify all variables in an input file or directory:
import fz
model = {
"varprefix": "$",
"delim": "{}"
}
# Parse single file
variables = fz.fzi("input.txt", model)
# Returns: {'T_celsius': None, 'V_L': None, 'n_mol': None}
# Parse directory (scans all files)
variables = fz.fzi("input_dir/", model)
Returns: Dictionary with variable names as keys (values are None)
fzc - Compile Input Files
Substitute variable values and evaluate formulas:
import fz
model = {
"varprefix": "$",
"formulaprefix": "@",
"delim": "{}",
"commentline": "#"
}
input_variables = {
"T_celsius": 25,
"V_L": 10,
"n_mol": 2
}
# Compile single file
fz.fzc(
"input.txt",
input_variables,
model,
output_dir="compiled"
)
# Compile with multiple value sets (creates subdirectories)
fz.fzc(
"input.txt",
{
"T_celsius": [20, 30], # 2 values
"V_L": [5, 10], # 2 values
"n_mol": 1 # fixed
},
model,
output_dir="compiled_grid"
)
# Creates: compiled_grid/T_celsius=20,V_L=5/, T_celsius=20,V_L=10/, etc.
Parameters:
input_path: Path to input file or directoryinput_variables: Dictionary of variable values (scalar or list). Optional (defaultNone) when the input files declare no variables (non-parametric dataset) — omit it and passmodelas a keyword argument:fz.fzc(input_path, model=model). If the input files do declare variables and it's omitted,fzcraises aValueErrornaming them.model: Model definition (dict or alias name)output_dir: Output directory path
fzo - Read Output Files
Parse calculation results from output directory:
import fz
model = {
"output": {
"pressure": "grep 'Pressure:' output.txt | awk '{print $2}'",
"temperature": "grep 'Temperature:' output.txt | awk '{print $2}'"
}
}
# Read from single directory
output = fz.fzo("results/case1", model)
# Returns: DataFrame with 1 row
# Read from directory with subdirectories
output = fz.fzo("results/*", model)
# Returns: DataFrame with 1 row per subdirectory
Automatic Path Parsing: If subdirectory names follow the pattern key1=val1,key2=val2,..., variables are automatically extracted as columns:
# Directory structure:
# results/
# ├── T_celsius=20,V_L=1/output.txt
# ├── T_celsius=20,V_L=2/output.txt
# └── T_celsius=30,V_L=1/output.txt
output = fz.fzo("results/*", model)
print(output)
# path pressure T_celsius V_L
# 0 T_celsius=20,V_L=1 2437.30 20.0 1.0
# 1 T_celsius=20,V_L=2 1218.65 20.0 2.0
# 2 T_celsius=30,V_L=1 2520.74 30.0 1.0
If subdirectories were instead named with case_naming="hash" or "index" (see below),
fzo recovers the variable columns from cases.csv, a single manifest fzr writes
at the results root mapping each case directory to its variables (falling back to
each case's own info.txt, which always has input.<var>=<value> lines, if the
manifest is missing or incomplete).
fzr - Run Parametric Calculations
Execute complete parametric study with automatic parallelization:
import fz
model = {
"varprefix": "$",
"output": {
"result": "cat output.txt"
}
}
results = fz.fzr(
input_path="input.txt",
input_variables={
"temperature": [100, 200, 300],
"pressure": [1, 10, 100],
"concentration": 0.5
},
model=model,
calculators=["sh://bash calculate.sh"],
results_dir="results"
)
# Results DataFrame includes:
# - All variable columns
# - All output columns
# - Metadata: status, calculator, error, command
print(results)
Parameters:
-
input_path: Input file or directory path -
input_variables: Variable values - dict (factorial) or DataFrame (non-factorial). Optional (defaultNone) when the input files declare no variables (non-parametric dataset) — omit it and passmodelas a keyword argument:fz.fzr(input_path, model=model, calculators=calculators). If the input files do declare variables and it's omitted,fzrraises aValueErrornaming them. -
model: Model definition (dict or alias) -
calculators: Calculator URI(s) - string or list -
results_dir: Results directory path -
case_naming: How each case's result/temp subdirectory is named (default"path"):"path":var1=val1,var2=val2,...- human-readable, but can exceed filesystem filename length limits (~255 chars) with many variables"hash": short content hash of the variable combination - always short and stable"index":case_<i>- shortest, order-dependent
With
"hash"/"index", a singlecases.csvmanifest is written at the results root mapping each case directory to its variables, andfzo()reads it back when the directory name isn't akey=val,...pattern (falling back to each case's owninfo.txtif the manifest is missing or incomplete). Defaults to theFZ_CASE_NAMINGenv var, or"path". -
input_static: Files identical across every case (a shared weather CSV, a large reference dataset) that are never templated and never duplicated per case — seedoc/core-functions.md("fzr" →input_static) for the full write-up. If a large variable-free file is left ininput_pathinstead,fzr()logs a one-time warning suggestinginput_static(threshold:FZ_STATIC_CANDIDATE_MIN_SIZE, default 1 MiB,0disables it).
Returns: pandas DataFrame with all results
fzd - Run Design of Experiments
Execute iterative design of experiments with adaptive algorithms:
import fz
model = {
"varprefix": "$",
"output": {
"result": "grep 'Result:' output.txt | awk '{print $2}'"
}
}
# Run Monte Carlo sampling
results = fz.fzd(
input_path="input.txt",
input_variables={
"x": "[0;10]", # Range: algorithm decides values
"y": "[-5;5]", # Range: algorithm decides values
"z": "2.5" # Fixed value
},
model=model,
output_expression="result",
algorithm="examples/algorithms/montecarlo_uniform.py",
calculators=["sh://bash calculate.sh"],
algorithm_options={"batch_sample_size": 10, "max_iterations": 20},
analysis_dir="results_fzd"
)
# Results include:
# - results['XY']: DataFrame with all input/output values
# - results['analysis']: Processed analysis (HTML, plots, metrics, etc.)
# - results['iterations']: Number of iterations completed
# - results['total_evaluations']: Total function evaluations
# - results['summary']: Summary text
print(results['XY']) # All sampled points and outputs
print(results['summary']) # Algorithm completion summary
Algorithm Examples:
examples/algorithms/montecarlo_uniform.py- Uniform random samplingexamples/algorithms/randomsampling.py- Simple random samplingexamples/algorithms/bfgs.py- BFGS optimization (requires scipy)examples/algorithms/brent.py- Brent's 1D optimization (requires scipy)
Parameters:
input_path: Input file or directory pathinput_variables: Dict where"[min;max]"entries are varied by the algorithm and plain"value"entries are fixed at that value for every evaluationmodel: Model definition (dict or alias)output_expression: Expression to evaluate from outputs (e.g.,"pressure"or"out1 + out2 * 2")algorithm: Algorithm name (randomsampling,brent,bfgs, ...) or path to.pyfilecalculators: Calculator URI(s) - string or listalgorithm_options: Dict, JSON string, or JSON file path with algorithm-specific optionsanalysis_dir: Analysis results directory (default:"analysis"); if it already exists it is renamed with a timestamp and its cached results are still reused automatically
Returns: Dict with:
XY: pandas DataFrame with all input and output valuesanalysis: Processed analysis results (HTML files, plots, metrics)algorithm: Algorithm pathiterations: Number of iterations completedtotal_evaluations: Total number of function evaluationssummary: Human-readable summary text
Automatic behaviors:
- Batch deduplication: duplicate points proposed by the algorithm in the same iteration are evaluated only once; results are re-mapped to all occurrences
- Cross-iteration caching: results from previous iterations are automatically reused — a point evaluated in iteration 2 is never re-run in iteration 5
- Re-run resume: if
analysis_diralready exists it is renamed with a timestamp; all its iteration subdirectories are still consulted as cache, so a re-run with different options benefits from all prior computations
Vector-valued outputs as objectives: a case's model output can be a
vector (list) — see the "Vector (array) Outputs" section. fzd's algorithms
always need a single scalar objective, so output_expression is where
such a vector gets reduced: on top of abs()/min()/max()/sqrt()/...
and indexing/slicing (series[-1]), the reduction functions sum(),
len(), sorted(), mean(), median(), stdev(), variance() are
available, e.g. output_expression="mean(T_series)". Two different vector
outputs can be combined too: plain + concatenates them before reducing
(mean(a + b)), and zip() combines them element-wise, e.g.
output_expression="sqrt(sum((x - y) ** 2 for x, y in zip(sim, ref)) / len(sim))"
for an RMSE between a simulated and a reference series. Referencing a
vector-valued output without reducing it raises a clear error naming the
output and suggesting a fix; that point is simply reported as a failed
evaluation, it does not stop the run. See examples/fzd_example.md,
"Vector-valued outputs as objectives".
Input Variables: Factorial vs Non-Factorial Designs
FZ supports two types of parametric study designs through different input_variables formats:
Factorial Design (Dict)
Use a dict to create a full factorial design (Cartesian product of all variable values):
# Dict with lists creates ALL combinations (factorial)
input_variables = {
"temp": [100, 200, 300], # 3 values
"pressure": [1.0, 2.0] # 2 values
}
# Creates 6 cases: 3 × 2 = 6
# (100,1.0), (100,2.0), (200,1.0), (200,2.0), (300,1.0), (300,2.0)
results = fz.fzr(input_file, input_variables, model, calculators)
Use factorial design when:
- You want to explore all possible combinations
- Variables are independent
- You need a complete design space exploration
Non-Factorial Design (DataFrame)
Use a pandas DataFrame to specify exactly which cases to run (non-factorial):
import pandas as pd
# DataFrame: each row is ONE case (non-factorial)
input_variables = pd.DataFrame({
"temp": [100, 200, 100, 300],
"pressure": [1.0, 1.0, 2.0, 1.5]
})
# Creates 4 cases ONLY:
# (100,1.0), (200,1.0), (100,2.0), (300,1.5)
# Note: (100,2.0) is included but (200,2.0) is not
results = fz.fzr(input_file, input_variables, model, calculators)
Use non-factorial design when:
- You have specific combinations to test
- Variables are coupled or have constraints
- You want to import a design from another tool
- You need an irregular or optimized sampling pattern
Examples of non-factorial patterns:
# Latin Hypercube Sampling
import pandas as pd
from scipy.stats import qmc
sampler = qmc.LatinHypercube(d=2)
sample = sampler.random(n=10)
input_variables = pd.DataFrame({
"x": sample[:, 0] * 100, # Scale to [0, 100]
"y": sample[:, 1] * 10 # Scale to [0, 10]
})
# Constraint-based design (only valid combinations)
input_variables = pd.DataFrame({
"rpm": [1000, 1500, 2000, 2500],
"load": [10, 20, 40, 50] # load increases with rpm
})
# Imported from design of experiments tool
input_variables = pd.read_csv("doe_design.csv")
Model Definition
A model defines how to parse inputs and extract outputs:
model = {
# Input parsing
"varprefix": "$", # Variable marker (e.g., $temp)
"formulaprefix": "@", # Formula marker (e.g., @pressure)
"delim": "{}", # Formula delimiters
"commentline": "#", # Comment character
# Optional: formula interpreter
"interpreter": "python", # "python" (default) or "R"
# Output extraction (shell commands)
"output": {
"pressure": "grep 'P =' out.txt | awk '{print $3}'",
"temperature": "cat temp.txt",
"energy": "python extract.py"
},
# Optional: model identifier
"id": "perfectgas"
}
Model Aliases
Store reusable models in .fz/models/:
.fz/models/perfectgas.json:
{
"varprefix": "$",
"formulaprefix": "@",
"delim": "{}",
"commentline": "#",
"output": {
"pressure": "grep 'pressure = ' output.txt | awk '{print $3}'"
},
"id": "perfectgas"
}
Use by name:
results = fz.fzr("input.txt", input_variables, "perfectgas")
Formula Evaluation
Formulas in input files are evaluated during compilation using Python or R interpreters.
Python Interpreter (Default)
# Input template with formulas
Temperature: $T_celsius C
Volume: $V_L L
# Context (available in all formulas)
#@import math
#@R = 8.314
#@def celsius_to_kelvin(t):
#@ return t + 273.15
# Calculated value
#@T_kelvin = celsius_to_kelvin($T_celsius)
#@pressure = $n_mol * R * T_kelvin / ($V_L / 1000)
Result: @{pressure} Pa
Circumference: @{2 * math.pi * $radius}
R Interpreter
For statistical computing, you can use R for formula evaluation:
from fz import fzi
from fz.config import set_interpreter
# Set interpreter to R
set_interpreter("R")
# Or specify in model
model = {"interpreter": "R", "formulaprefix": "@", "delim": "{}", "commentline": "#"}
R template example:
# Input template with R formulas
Sample size: $n
Mean: $mu
SD: $sigma
# R context (available in all formulas)
#@samples <- rnorm($n, mean=$mu, sd=$sigma)
Mean (sample): @{mean(samples)}
SD (sample): @{sd(samples)}
Median: @{median(samples)}
Installation requirements: R must be installed along with system libraries. See examples/r_interpreter_example.md for detailed installation instructions.
# Install with R support
pip install funz-fz[r]
Key differences:
- Python requires
import mathformath.pi, R haspibuilt-in - R excels at statistical functions:
mean(),sd(),median(),rnorm(), etc. - R uses
<-for assignment in context lines - R is vectorized by default
Variable Default Values
Variables can specify default values using the ${var~default} syntax:
# Configuration template
Host: ${host~localhost}
Port: ${port~8080}
Debug: ${debug~false}
Workers: ${workers~4}
Behavior:
- If variable is provided in
input_variables, its value is used - If variable is NOT provided but has default, default is used (with warning)
- If variable is NOT provided and has NO default, it remains unchanged
Example:
from fz.interpreter import replace_variables_in_content
content = "Server: ${host~localhost}:${port~8080}"
input_variables = {"host": "example.com"} # port not provided
result = replace_variables_in_content(content, input_variables)
# Result: "Server: example.com:8080"
# Warning: Variable 'port' not found in input_variables, using default value: '8080'
Use cases:
- Configuration templates with sensible defaults
- Environment-specific deployments
- Optional parameters in parametric studies
See examples/variable_substitution.md for comprehensive documentation.
Old Funz Syntax Compatibility
For backward compatibility with legacy Java Funz users, FZ supports the old ?var variable syntax:
# Legacy Funz syntax (still supported)
Temperature: ?T_celsius
Pressure: ?P_bar
# Equivalent modern FZ syntax
Temperature: $T_celsius
Pressure: $P_bar
Behavior:
?variableis automatically converted to$variable- No configuration needed - works transparently
- Useful for migrating existing Funz projects to Python
- Can mix both syntaxes in the same file
Example:
import fz
# Input file with old Funz syntax
content = """
n_mol=?n_mol
T_celsius=?T_celsius
"""
model = {"varprefix": "$"} # Standard FZ configuration
# Works automatically - ?n_mol treated as $n_mol
variables = fz.fzi("input.txt", model)
# Returns: {'n_mol': None, 'T_celsius': None}
See examples/java_funz_syntax_example.py for more examples.
Features:
- Python or R expression evaluation
- Multi-line function definitions
- Variable substitution in formulas
- Default values for variables
- Nested formula evaluation
Calculator Types
Local Shell Execution
Execute calculations locally:
# Basic shell command
calculators = "sh://bash script.sh"
# With multiple arguments
calculators = "sh://python calculate.py --verbose"
# Multiple calculators (tries in order, parallel execution)
calculators = [
"sh://bash method1.sh",
"sh://bash method2.sh",
"sh://python method3.py"
]
How it works:
- Input files copied to temporary directory
- Command executed in that directory with input files as arguments
- Outputs parsed from result directory
- Temporary files cleaned up (preserved in DEBUG mode)
SSH Remote Execution
Execute calculations on remote servers:
# SSH with password
calculators = "ssh://user:password@server.com:22/bash /absolutepath/to/calc.sh"
# SSH with key-based auth (recommended)
calculators = "ssh://user@server.com/bash /absolutepath/to/calc.sh"
# SSH with custom port
calculators = "ssh://user@server.com:2222/bash /absolutepath/to/calc.sh"
Features:
- Automatic file transfer (SFTP)
- Remote execution with timeout
- Result retrieval
- SSH key-based or password authentication
- Host key verification
Security:
- Interactive host key acceptance
- Warning for password-based auth
- Environment variable for auto-accepting host keys:
FZ_SSH_AUTO_ACCEPT_HOSTKEYS=1
SLURM Workload Manager
Execute calculations on SLURM clusters (local or remote):
# Local SLURM execution
calculators = "slurm://:compute/bash script.sh"
# Remote SLURM execution via SSH
calculators = "slurm://user@cluster.edu:gpu/bash script.sh"
# With custom SSH port
calculators = "slurm://user@cluster.edu:2222:gpu/bash script.sh"
# Multiple partitions for parallel execution
calculators = [
"slurm://user@hpc.edu:compute/bash calc.sh",
"slurm://user@hpc.edu:gpu/bash calc.sh"
]
URI Format: slurm://[user@host[:port]]:partition/script
Note: For local execution, the partition must be prefixed with a colon (:partition), e.g., slurm://:compute/script.sh
How it works:
- Local execution: Uses
srun --partition=<partition> <script>directly - Remote execution: Connects via SSH, transfers files, runs
srunon remote cluster - Automatically handles SLURM partition scheduling
- Supports interrupt handling (Ctrl+C terminates SLURM jobs)
Features:
- Local or remote SLURM execution
- Automatic file transfer for remote execution (via SFTP)
- SLURM partition specification
- Timeout and interrupt handling
- Compatible with all SLURM schedulers
Requirements:
- Local: SLURM installed (
sruncommand available) - Remote: SSH access to SLURM cluster +
paramikolibrary
Funz Server Execution
Execute calculations using the Funz server protocol (compatible with legacy Java Funz servers):
# Connect to local Funz server
calculators = "funz://:5555/R"
# Connect to remote Funz server
calculators = "funz://server.example.com:5555/Python"
# Multiple Funz servers for parallel execution
calculators = [
"funz://:5555/R",
"funz://:5556/R",
"funz://:5557/R"
]
Features:
- Compatible with legacy Java Funz calculator servers
- Automatic file upload to server
- Remote execution with the Funz protocol
- Result download and extraction
- Support for interrupt handling
- UDP discovery for automatic server detection
UDP Discovery:
FZ supports automatic Funz server discovery via UDP broadcast:
from fz import discover_funz_servers
# Listen on UDP port 19001 for 10s and collect every distinct calculator seen
servers = discover_funz_servers(udp_port=19001, listen_duration=10)
# Returns list of discovered servers:
# [
# {'host': '192.168.1.100', 'tcp_port': 5555, 'name': 'calc1',
# 'os': 'Linux 6.1', 'activity': 'idle', 'idle': True, 'codes': ['R']},
# {'host': '192.168.1.101', 'tcp_port': 5555, 'name': 'calc2',
# 'os': 'Linux 6.1', 'activity': 'idle', 'idle': True, 'codes': ['Python']},
# ...
# ]
# Use idle servers offering "R"
calculators = [
f"funz://{s['host']}:{s['tcp_port']}/R"
for s in servers if s["idle"] and "R" in s["codes"]
]
results = fz.fzr("input.txt", input_variables, model, calculators=calculators)
Discovery Protocol:
- Broadcasts UDP message on port 19001
- Servers respond with their host, port, and supported codes
- Useful for dynamic calculator allocation in cluster environments
- See
doc/funz-protocol.mdfor detailed protocol documentation
Protocol:
- Text-based TCP socket communication
- Calculator reservation with authentication
- Automatic cleanup and unreservation
URI Format: funz://[host]:<port>/<code>
host: Server hostname (default: localhost)port: Server port (required)code: Calculator code/model name (e.g., "R", "Python", "Modelica")
Example:
import fz
model = {
"output": {
"pressure": "grep 'pressure = ' output.txt | awk '{print $3}'"
}
}
results = fz.fzr(
"input.txt",
{"temp": [100, 200, 300]},
model,
calculators="funz://:5555/R"
)
Cache Calculator
Reuse previous calculation results:
# Check single cache directory
calculators = "cache://previous_results"
# Check multiple cache locations
calculators = [
"cache://run1",
"cache://run2/results",
"sh://bash calculate.sh" # Fallback to actual calculation
]
# Use glob patterns
calculators = "cache://archive/*/results"
Cache Matching:
- Based on MD5 hash of input files (
.fz_hash) - Validates outputs are not None
- Falls through to next calculator on miss
- No recalculation if cache hit
Calculator Aliases
Store calculator configurations in .fz/calculators/:
.fz/calculators/cluster.json:
{
"uri": "ssh://user@cluster.university.edu",
"models": {
"perfectgas": "bash /home/user/codes/perfectgas/run.sh",
"navier-stokes": "bash /home/user/codes/cfd/run.sh"
}
}
Use by name:
results = fz.fzr("input.txt", input_variables, "perfectgas", calculators="cluster")
Calculator-Model Compatibility
FZ automatically validates that calculators support the specified model to prevent incompatible combinations:
# .fz/calculators/cluster.json
{
"uri": "ssh://user@cluster.edu",
"models": {
"perfectgas": "bash /path/to/perfectgas.sh",
"cfd": "bash /path/to/cfd.sh"
}
}
Validation:
# This works - perfectgas is supported
results = fz.fzr("input.txt", input_variables, "perfectgas", calculators="cluster")
# This fails with clear error - unsupported_model not in calculator's models
results = fz.fzr("input.txt", input_variables, "unsupported_model", calculators="cluster")
# Error: Calculator 'cluster' does not support model 'unsupported_model'
# Supported models: perfectgas, cfd
Automatic Resolution:
- Model and calculator aliases are resolved from
.fz/directories - Compatibility check happens before execution
- Clear error messages indicate which models are supported
- Prevents wasted computation on incompatible setups
Direct URIs (No Validation):
When using direct calculator URIs (not aliases), no validation occurs:
# No validation - you're responsible for compatibility
results = fz.fzr(
"input.txt",
input_variables,
"anymodel",
calculators="sh://bash any_script.sh"
)
Best Practice:
- Use calculator aliases for complex setups
- Document model compatibility in calculator JSON files
- Use
fzl --checkto validate configurations
Advanced Features
Parallel Execution
FZ automatically parallelizes when you have multiple cases and calculators:
# Sequential: 1 calculator, 10 cases → runs one at a time
results = fz.fzr(
"input.txt",
{"temp": list(range(10))},
model,
calculators="sh://bash calc.sh"
)
# Parallel: 3 calculators, 10 cases → 3 concurrent
results = fz.fzr(
"input.txt",
{"temp": list(range(10))},
model,
calculators=[
"sh://bash calc.sh",
"sh://bash calc.sh",
"sh://bash calc.sh"
]
)
# Control parallelism with environment variable
import os
os.environ['FZ_MAX_WORKERS'] = '4'
# Or use duplicate calculator URIs
calculators = ["sh://bash calc.sh"] * 4 # 4 parallel workers
Load Balancing:
- Round-robin distribution of cases to calculators
- Thread-safe calculator locking
- Automatic retry on failures
- Progress tracking with ETA
Retry Mechanism
Automatic retry on calculation failures:
import os
os.environ['FZ_MAX_RETRIES'] = '3' # Try each case up to 3 times
results = fz.fzr(
"input.txt",
input_variables,
model,
calculators=[
"sh://unreliable_calc.sh", # Might fail
"sh://backup_calc.sh" # Backup method
]
)
Retry Strategy:
- Try first available calculator
- On failure, try next calculator
- Repeat up to
FZ_MAX_RETRIEStimes - Report all attempts in logs
Caching Strategy
Intelligent result reuse:
# First run
results1 = fz.fzr(
"input.txt",
{"temp": [10, 20, 30]},
model,
calculators="sh://expensive_calc.sh",
results_dir="run1"
)
# Add more cases - reuse previous results
results2 = fz.fzr(
"input.txt",
{"temp": [10, 20, 30, 40, 50]}, # 2 new cases
model,
calculators=[
"cache://run1", # Check cache first
"sh://expensive_calc.sh" # Only run new cases
],
results_dir="run2"
)
# Only runs calculations for temp=40 and temp=50
Output Type Casting
Automatic type conversion:
model = {
"output": {
"scalar_int": "echo 42",
"scalar_float": "echo 3.14159",
"array": "echo '[1, 2, 3, 4, 5]'",
"single_array": "echo '[42]'", # → 42 (simplified)
"json_object": "echo '{\"key\": \"value\"}'",
"string": "echo 'hello world'"
}
}
results = fz.fzo("output_dir", model)
# Values automatically cast to int, float, list, dict, or str
Casting Rules:
- Try JSON parsing
- Try Python literal evaluation
- Try numeric conversion (int/float)
- Keep as string
- Single-element arrays → scalar
Vector (array) Outputs
An output entry does not have to be a single number: python://, jq://,
yq:// and plain shell commands can all return a full list (a time series, a
per-node profile, a spectrum...), which lands in the results DataFrame as one
Python list per case — fzr/fzo never flatten, truncate or pad vector
outputs, and cases are free to produce vectors of different lengths:
model = {
"output": {
# whole JSON array file -> plain Python list
"T_series": "python://json_file('series.json')",
# every regex match -> list
"T_series_grep": "python://grep(r'T=(\S+)', 'log.txt', all=True)",
# jq/yq filter selecting an array
"T_series_jq": "jq://.temperatures results.json",
# xpath:// returns a list too when the expression matches more
# than one XML node (e.g. several <value> siblings)
"T_series_xpath": "xpath://'//value/text()' output.xml",
}
}
Note the single-element-array simplification above (rule 5) only applies to
the legacy plain-shell-command form; it does not apply to python://,
jq://, yq:// or xpath:// outputs, so a length-1 vector stays a
one-element list with those forms. See examples/vector_outputs_example.md
and doc/model-definition.md ("output" → "Vector / array outputs") for more.
Progress Callbacks
Monitor execution progress in real-time with custom callback functions:
import fz
model = {
"varprefix": "$",
"output": {"result": "cat output.txt"}
}
# Define callback function
def progress_callback(event_type, case_info):
"""
Called during execution for each case event.
Args:
event_type: One of "case_start", "case_complete", "case_failed"
case_info: Dict with case details (case_name, calculator, etc.)
"""
if event_type == "case_start":
print(f"⏳ Starting: {case_info['case_name']}")
elif event_type == "case_complete":
print(f"✅ Completed: {case_info['case_name']}")
elif event_type == "case_failed":
print(f"❌ Failed: {case_info['case_name']} - {case_info.get('error', 'Unknown error')}")
# Run with callback
results = fz.fzr(
"input.txt",
{"param": [1, 2, 3, 4, 5]},
model,
calculators="sh://bash calc.sh",
results_dir="results",
callbacks=[progress_callback]
)
Use Cases:
- Custom progress bars
- Real-time logging and monitoring
- Integration with external monitoring systems
- UI updates for long-running calculations
- Performance profiling
Multiple Callbacks:
def logger_callback(event_type, case_info):
# Log to file
with open("execution.log", "a") as f:
f.write(f"{event_type}: {case_info}\n")
def metrics_callback(event_type, case_info):
# Send to monitoring system
send_to_prometheus(event_type, case_info)
results = fz.fzr(..., callbacks=[logger_callback, metrics_callback])
Complete Examples
Interactive Jupyter Notebooks
Explore fz features hands-on with these notebooks — open directly in Google Colab, no local install needed:
Example 1: Perfect Gas Pressure Study
Input file (input.txt):
# input file for Perfect Gaz Pressure, with variables n_mol, T_celsius, V_L
n_mol=$n_mol
T_kelvin=@{$T_celsius + 273.15}
#@ def L_to_m3(L):
#@ return(L / 1000)
V_m3=@{L_to_m3($V_L)}
Calculation script (PerfectGazPressure.sh):
#!/bin/bash
# read input file
source $1
sleep 5 # simulate a calculation time
echo 'pressure = '`echo "scale=4;$n_mol*8.314*$T_kelvin/$V_m3" | bc` > output.txt
echo 'Done'
Python script (run_perfectgas.py):
import fz
import matplotlib.pyplot as plt
# Define model
model = {
"varprefix": "$",
"formulaprefix": "@",
"delim": "{}",
"commentline": "#",
"output": {
"pressure": "grep 'pressure = ' output.txt | awk '{print $3}'"
}
}
# Parametric study
results = fz.fzr(
"input.txt",
{
"n_mol": [1, 2, 3],
"T_celsius": [10, 20, 30],
"V_L": [5, 10]
},
model,
calculators="sh://bash PerfectGazPressure.sh",
results_dir="perfectgas_results"
)
print(results)
# Plot results: pressure vs temperature for different volumes
for volume in results['V_L'].unique():
for n in results['n_mol'].unique():
data = results[(results['V_L'] == volume) & (results['n_mol'] == n)]
plt.plot(data['T_celsius'], data['pressure'],
marker='o', label=f'n={n} mol, V={volume} L')
plt.xlabel('Temperature (°C)')
plt.ylabel('Pressure (Pa)')
plt.title('Ideal Gas: Pressure vs Temperature')
plt.legend()
plt.grid(True)
plt.savefig('perfectgas_results.png')
print("Plot saved to perfectgas_results.png")
Example 2: Remote HPC Calculation
import fz
model = {
"varprefix": "$",
"output": {
"energy": "grep 'Total Energy' output.log | awk '{print $4}'",
"time": "grep 'CPU time' output.log | awk '{print $4}'"
}
}
# Run on HPC cluster
results = fz.fzr(
"simulation_input/",
{
"mesh_size": [100, 200, 400, 800],
"timestep": [0.001, 0.01, 0.1],
"iterations": 1000
},
model,
calculators=[
"cache://previous_runs/*", # Check cache first
"ssh://user@hpc.university.edu/sbatch /path/to/submit.sh"
],
results_dir="hpc_results"
)
# Analyze convergence
import pandas as pd
summary = results.groupby('mesh_size').agg({
'energy': ['mean', 'std'],
'time': 'sum'
})
print(summary)
Example 3: Multi-Calculator with Failover
import fz
model = {
"varprefix": "$",
"output": {"result": "cat result.txt"}
}
results = fz.fzr(
"input.txt",
{"param": list(range(100))},
model,
calculators=[
"cache://previous_results", # 1. Check cache
"sh://bash fast_but_unstable.sh", # 2. Try fast method
"sh://bash robust_method.sh", # 3. Fallback to robust
"ssh://user@server/bash remote.sh" # 4. Last resort: remote
],
results_dir="results"
)
# Check which calculator was used for each case
print(results[['param', 'calculator', 'status']].head(10))
Example 4: Design of Experiments with Adaptive Sampling
import fz
import matplotlib.pyplot as plt
# Input template with perfect gas law
# (same as Example 1, but using fzd for adaptive design)
model = {
"varprefix": "$",
"formulaprefix": "@",
"delim": "{}",
"commentline": "#",
"output": {
"pressure": "grep 'pressure = ' output.txt | awk '{print $3}'"
}
}
# Run Monte Carlo sampling to explore pressure distribution
results = fz.fzd(
input_path="input.txt",
input_variables={
"T_celsius": "[10;50]", # Range: 10 to 50°C
"V_L": "[1;10]", # Range: 1 to 10 L
"n_mol": "1.0" # Fixed: 1 mole
},
model=model,
output_expression="pressure",
algorithm="examples/algorithms/montecarlo_uniform.py",
calculators=["sh://bash PerfectGazPressure.sh"],
algorithm_options={
"batch_sample_size": 20, # 20 samples per iteration
"max_iterations": 10 # 10 iterations
},
analysis_dir="monte_carlo_results"
)
# Results DataFrame has all sampled points
print(f"Total evaluations: {results['total_evaluations']}")
print(f"Iterations: {results['iterations']}")
print(results['summary'])
# Access the data
df = results['XY']
print(df.head())
# Plot the sampled points
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 5))
# Scatter plot: Temperature vs Volume colored by Pressure
scatter = ax1.scatter(df['T_celsius'], df['V_L'], c=df['pressure'],
cmap='viridis', s=50, alpha=0.6)
ax1.set_xlabel('Temperature (°C)')
ax1.set_ylabel('Volume (L)')
ax1.set_title('Sampled Design Space')
plt.colorbar(scatter, ax=ax1, label='Pressure (Pa)')
# Histogram of pressure values
ax2.hist(df['pressure'], bins=20, edgecolor='black')
ax2.set_xlabel('Pressure (Pa)')
ax2.set_ylabel('Frequency')
ax2.set_title('Pressure Distribution')
plt.tight_layout()
plt.savefig('monte_carlo_analysis.png')
print("Analysis plot saved to monte_carlo_analysis.png")
Example 5: Optimization with BFGS
import fz
# Find temperature and volume that minimize pressure
model = {
"varprefix": "$",
"formulaprefix": "@",
"delim": "{}",
"commentline": "#",
"output": {
"pressure": "grep 'pressure = ' output.txt | awk '{print $3}'"
}
}
results = fz.fzd(
input_path="input.txt",
input_variables={
"T_celsius": "[10;50]", # Search range
"V_L": "[1;10]", # Search range
"n_mol": "1.0" # Fixed
},
model=model,
output_expression="pressure",
algorithm="examples/algorithms/bfgs.py",
calculators=["sh://bash PerfectGazPressure.sh"],
algorithm_options={
"minimize": True, # Minimize pressure
"max_iterations": 50
},
analysis_dir="optimization_results"
)
# Get optimal point
df = results['XY']
optimal_idx = df['pressure'].idxmin()
optimal = df.loc[optimal_idx]
print(f"Optimal temperature: {optimal['T_celsius']:.2f}°C")
print(f"Optimal volume: {optimal['V_L']:.2f} L")
print(f"Minimum pressure: {optimal['pressure']:.2f} Pa")
print(f"Total evaluations: {results['total_evaluations']}")
# Plot optimization path
import matplotlib.pyplot as plt
plt.figure(figsize=(10, 6))
plt.scatter(df['T_celsius'], df['V_L'], c=df['pressure'],
cmap='coolwarm', s=100, edgecolor='black')
plt.plot(df['T_celsius'], df['V_L'], 'k--', alpha=0.3, label='Optimization path')
plt.scatter(optimal['T_celsius'], optimal['V_L'],
color='red', s=300, marker='*',
edgecolor='black', label='Optimum')
plt.xlabel('Temperature (°C)')
plt.ylabel('Volume (L)')
plt.title('BFGS Optimization Path')
plt.colorbar(label='Pressure (Pa)')
plt.legend()
plt.savefig('optimization_path.png')
print("Optimization path saved to optimization_path.png")
Writing Custom Algorithms for fzd
FZ provides an extensible framework for implementing adaptive algorithms. Each algorithm is a Python class with specific methods.
Algorithm Interface
Create a Python file with a class implementing these methods:
class MyAlgorithm:
"""Custom algorithm for design of experiments"""
def __init__(self, **options):
"""
Initialize algorithm with options passed from algorithm_options.
Args:
**options: Algorithm-specific parameters (e.g., batch_size, max_iter)
"""
self.batch_size = options.get('batch_size', 10)
self.max_iterations = options.get('max_iterations', 100)
self.iteration = 0
def get_initial_design(self, input_vars, output_vars):
"""
Return initial design points to evaluate.
Args:
input_vars: Dict[str, tuple] - {var_name: (min, max)}
e.g., {"x": (0.0, 10.0), "y": (-5.0, 5.0)}
output_vars: List[str] - Output variable names
Returns:
List[Dict[str, float]] - Initial points to evaluate
e.g., [{"x": 0.5, "y": 0.0}, {"x": 7.5, "y": 2.3}]
"""
# Generate initial sample points
import random
points = []
for _ in range(self.batch_size):
point = {
var: random.uniform(bounds[0], bounds[1])
for var, bounds in input_vars.items()
}
points.append(point)
return points
def get_next_design(self, previous_input_vars, previous_output_values):
"""
Return next design points based on previous results.
Args:
previous_input_vars: List[Dict[str, float]] - All previous input combinations
previous_output_values: List[float] - Corresponding outputs (may contain None)
Returns:
List[Dict[str, float]] - Next points to evaluate
Empty list [] signals algorithm is finished
"""
self.iteration += 1
# Stop if max iterations reached
if self.iteration >= self.max_iterations:
return [] # Empty list = finished
# Generate next batch based on results
# ... your adaptive logic here ...
return next_points
def get_analysis(self, input_vars, output_values):
"""
Return final analysis results.
Args:
input_vars: List[Dict[str, float]] - All evaluated inputs
output_values: List[float] - All outputs (may contain None)
Returns:
Dict with analysis information (can include 'text', 'data', etc.)
"""
# Filter out failed evaluations (None values)
valid_results = [(x, y) for x, y in zip(input_vars, output_values) if y is not None]
return {
'text': f"Algorithm completed: {len(valid_results)} successful evaluations",
'data': {'mean': sum(y for _, y in valid_results) / len(valid_results)}
}
def get_analysis_tmp(self, input_vars, output_values):
"""
[OPTIONAL] Return intermediate results at each iteration.
Args:
input_vars: List[Dict[str, float]] - All inputs so far
output_values: List[float] - All outputs so far
Returns:
Dict with intermediate analysis information
"""
valid_count = sum(1 for y in output_values if y is not None)
return {
'text': f"Iteration {self.iteration}: {valid_count} valid samples"
}
Algorithm Examples
1. Monte Carlo Sampling
See examples/algorithms/montecarlo_uniform.py:
import fz
results = fz.fzd(
input_path="input.txt",
input_variables={"x": "[0;10]", "y": "[0;5]"},
model="mymodel",
output_expression="result",
algorithm="examples/algorithms/montecarlo_uniform.py",
calculators=["sh://bash calc.sh"],
algorithm_options={"batch_sample_size": 20, "max_iterations": 10}
)
2. BFGS Optimization
See examples/algorithms/bfgs.py (requires scipy):
results = fz.fzd(
input_path="input.txt",
input_variables={"x": "[0;10]", "y": "[0;5]"},
model="mymodel",
output_expression="energy",
algorithm="examples/algorithms/bfgs.py",
calculators=["sh://bash calc.sh"],
algorithm_options={"minimize": True, "max_iterations": 50}
)
3. Brent's Method (1D Optimization)
See examples/algorithms/brent.py (requires scipy):
results = fz.fzd(
input_path="input.txt",
input_variables={"temperature": "[0;100]"}, # Single variable
model="mymodel",
output_expression="efficiency",
algorithm="examples/algorithms/brent.py",
calculators=["sh://bash calc.sh"],
algorithm_options={"minimize": False} # Maximize efficiency
)
Algorithm Features
Content Format Detection
Algorithms can return analysis results in multiple formats:
def get_analysis(self, input_vars, output_values):
# Return HTML
return {
'text': '<html><body><h1>Results</h1><p>Mean: 42.5</p></body></html>',
'data': {'mean': 42.5}
}
# Saved to: analysis_<iteration>.html
# Return JSON
return {
'text': '{"mean": 42.5, "std": 3.2}',
'data': {}
}
# Saved to: analysis_<iteration>.json
# Return Markdown
return {
'text': '# Results\n\n**Mean**: 42.5\n**Std**: 3.2',
'data': {}
}
# Saved to: analysis_<iteration>.md
# Return key-value format
return {
'text': 'mean=42.5\nstd=3.2\nsamples=100',
'data': {}
}
# Saved to: analysis_<iteration>.txt
See docs/FZD_CONTENT_FORMATS.md for detailed format documentation.
Dependency Management
Specify required packages using __require__:
__require__ = ["numpy", "scipy", "matplotlib"]
class MyAlgorithm:
def __init__(self, **options):
import numpy as np
import scipy.optimize
# ...
FZ will check dependencies at load time and warn if packages are missing.
Configuration
Environment Variables
# Logging level (DEBUG, INFO, WARNING, ERROR)
export FZ_LOG_LEVEL=INFO
# Maximum retry attempts per case
export FZ_MAX_RETRIES=5
# Thread pool size for parallel execution
export FZ_MAX_WORKERS=8
# SSH keepalive interval (seconds)
export FZ_SSH_KEEPALIVE=300
# Auto-accept SSH host keys (use with caution!)
export FZ_SSH_AUTO_ACCEPT_HOSTKEYS=0
# Default formula interpreter (python or R)
export FZ_INTERPRETER=python
# Custom shell binary search path (overrides system PATH)
# Windows example: SET FZ_SHELL_PATH=C:\msys64\usr\bin;C:\Program Files\Git\usr\bin
# Linux/macOS example: export FZ_SHELL_PATH=/opt/custom/bin:/usr/local/bin
export FZ_SHELL_PATH=/usr/local/bin:/usr/bin
# Run timeout in seconds (default: 3600 = 1 hour)
export FZ_RUN_TIMEOUT=1800
# Case directory naming scheme: "path" (var=val,... subdirs, default), "hash"
# (short content hash, avoids filesystem filename length limits with many
# variables), or "index" (case_<i>)
export FZ_CASE_NAMING=path
# Minimum size (bytes) for a variable-free input_path file to trigger a
# one-time warning suggesting input_static instead (default: 1048576 = 1 MiB;
# 0 disables the warning)
export FZ_STATIC_CANDIDATE_MIN_SIZE=1048576
Shell Path Configuration (FZ_SHELL_PATH)
The FZ_SHELL_PATH environment variable allows you to specify custom locations for shell binaries (grep, awk, sed, etc.) used in model output expressions and calculator commands. This is particularly important on Windows where Unix-like tools may be installed in non-standard locations.
Why use FZ_SHELL_PATH?
- Windows compatibility: Locate tools in MSYS2, Git Bash, Cygwin, or WSL
- Custom installations: Use specific versions of tools from custom directories
- Priority control: Override system PATH to ensure correct tool versions
- Performance: Cached binary paths for faster resolution
Usage examples:
# Windows with MSYS2 (use semicolon separator)
SET FZ_SHELL_PATH=C:\msys64\usr\bin;C:\msys64\mingw64\bin
# Windows with Git Bash
SET FZ_SHELL_PATH=C:\Program Files\Git\usr\bin;C:\Program Files\Git\bin
# Linux/macOS (use colon separator)
export FZ_SHELL_PATH=/opt/homebrew/bin:/usr/local/bin
# Priority: FZ_SHELL_PATH paths are checked BEFORE system PATH
How it works:
- Commands in model
outputdictionaries are parsed for binary names (grep, awk, etc.) - Binary names are resolved to absolute paths using FZ_SHELL_PATH
- Commands in
sh://calculators are similarly resolved - Windows: Automatically tries both
commandandcommand.exe - Resolved paths are cached for performance
Example in model:
model = {
"output": {
"pressure": "grep 'pressure' output.txt | awk '{print $2}'"
}
}
# With FZ_SHELL_PATH=C:\msys64\usr\bin, executes:
# C:\msys64\usr\bin\grep.exe 'pressure' output.txt | C:\msys64\usr\bin\awk.exe '{print $2}'
See doc/shell-path.md and examples/shell_path_example.md for detailed documentation.
Timeout Configuration
FZ provides flexible timeout settings for controlling calculation execution time:
1. Environment Variable (Global Default)
# Set default timeout for all calculations (in seconds)
export FZ_RUN_TIMEOUT=1800 # 30 minutes (default: 3600 seconds = 1 hour)
2. Model Configuration (Per-Model)
model = {
"varprefix": "$",
"output": {"result": "cat output.txt"},
"timeout": 1800 # 30 minutes for this model, regardless of FZ_RUN_TIMEOUT
}
results = fz.fzr("input.txt", input_variables, model, calculators="sh://calc.sh")
A model timeout of None/null or 0 disables the timeout entirely for that
model (the calculation may run indefinitely):
model = {"timeout": None, "output": {"result": "cat output.txt"}}
3. fzr()/fzc() timeout= Argument (Per-Call)
# Overrides both the model's timeout and FZ_RUN_TIMEOUT for this call only
results = fz.fzr("input.txt", input_variables, model, calculators="sh://calc.sh", timeout=7200)
Priority Order (highest to lowest)
timeout=argument passed tofzr()/fzc()- Model configuration (
model["timeout"]) - Environment variable (
FZ_RUN_TIMEOUT, default 3600 seconds = 1 hour)
Timeout Behavior:
- Calculation terminates after timeout expires
- Marked as "failed" with timeout error
- Retry mechanism may attempt with next calculator
- Partial results preserved for debugging
Python Configuration
from fz import get_config
# Get current config
config = get_config()
print(f"Max retries: {config.max_retries}")
print(f"Max workers: {config.max_workers}")
# Modify configuration
config.max_retries = 10
config.max_workers = 4
Directory Structure
FZ uses the following directory structure:
your_project/
├── input.txt # Your input template
├── calculate.sh # Your calculation script
├── run_study.py # Your Python script
├── .fz/ # FZ configuration (optional)
│ ├── models/ # Model aliases
│ │ └── mymodel.json
│ ├── calculators/ # Calculator aliases
│ │ └── mycluster.json
│ ├── algorithms/ # Algorithm plugins
│ │ ├── myalgo.py
│ │ └── myalgo.R
│ └── tmp/ # Temporary files (auto-created)
│ └── fz_temp_*/ # Per-run temp directories
└── results/ # Results directory
├── case1/ # One directory per case
│ ├── input.txt # Compiled input
│ ├── output.txt # Calculation output
│ ├── log.txt # Execution metadata
│ ├── out.txt # Standard output
│ ├── err.txt # Standard error
│ └── .fz_hash # File checksums (for caching)
└── case2/
└── ...
Installing Plugins
FZ supports installing models and algorithms as plugins from GitHub repositories, local zip files, or URLs.
Installing Algorithm Plugins
Algorithm plugins enable design of experiments and optimization workflows. Install algorithms from GitHub repositories in the fz-<algorithm> format:
From GitHub Repository Name
# Install from Funz organization (convention: fz-<algorithm>)
fz install algorithm montecarlo
# This installs from: https://github.com/Funz/fz-montecarlo
# Python API
import fz
# Install locally (.fz/algorithms/)
fz.install_algorithm("montecarlo")
# Install globally (~/.fz/algorithms/)
fz.install_algorithm("montecarlo", global_install=True)
From GitHub URL
# Install from full URL
fz install algorithm https://github.com/YourOrg/fz-custom-algo
fz.install_algorithm("https://github.com/YourOrg/fz-custom-algo")
From Local Zip File
# Install from downloaded zip
fz install algorithm ./fz-myalgo.zip
fz.install_algorithm("./fz-myalgo.zip")
Using Installed Algorithms
Once installed, algorithms can be referenced by name:
import fz
# Use installed algorithm plugin
results = fz.fzd(
input_path="input.txt",
input_variables={"x": "[0;10]", "y": "[-5;5]"},
model="mymodel",
output_expression="result",
algorithm="montecarlo", # Plugin name (no path or extension)
calculators=["sh://bash calc.sh"],
algorithm_options={"batch_sample_size": 20}
)
Installing Model Plugins
Model plugins define input parsing and output extraction patterns. Install models from GitHub:
From GitHub Repository Name
# Install from Funz organization (convention: fz-<model>)
fz install model moret
# This installs from: https://github.com/Funz/fz-moret
# Python API
import fz
# Install locally (.fz/models/)
fz.install("moret")
# Install globally (~/.fz/models/)
fz.install("moret", global_install=True)
From GitHub URL or Local Zip
fz install model https://github.com/Funz/fz-moret
fz install model ./fz-moret.zip
Listing Installed Plugins
# List installed algorithms
fz list algorithms
# List only global algorithms
fz list algorithms --global
# List installed models
fz list models
# List only global models
fz list models --global
# Python API
import fz
# List algorithms
algorithms = fz.list_algorithms()
for name, info in algorithms.items():
print(f"{name} ({info['type']}) - {info['file']}")
# List models
models = fz.list_models()
for name, model in models.items():
print(f"{name}: {model.get('id', 'N/A')}")
Uninstalling Plugins
# Uninstall algorithm
fz uninstall algorithm montecarlo
# Uninstall from global location
fz uninstall algorithm montecarlo --global
# Uninstall model
fz uninstall model moret
# Python API
import fz
# Uninstall algorithm
fz.uninstall_algorithm("montecarlo")
# Uninstall model
fz.uninstall("moret")
Plugin Priority
When the same plugin exists in multiple locations, FZ uses the following priority:
- Project-level (
.fz/algorithms/or.fz/models/) - Highest priority - Global (
~/.fz/algorithms/or~/.fz/models/) - Fallback
This allows project-specific customization while maintaining a personal library of reusable plugins.
Creating Algorithm Plugins
To create your own algorithm plugin repository (for sharing or distribution):
-
Create repository named
fz-<algorithm>(e.g.,fz-montecarlo) -
Add algorithm file as
<algorithm>.pyor<algorithm>.Rin repository root or.fz/algorithms/:
# montecarlo.py
class MonteCarlo:
def __init__(self, **options):
self.n_samples = options.get("n_samples", 100)
def get_initial_design(self, input_vars, output_vars):
import random
samples = []
for _ in range(self.n_samples):
sample = {}
for var, (min_val, max_val) in input_vars.items():
sample[var] = random.uniform(min_val, max_val)
samples.append(sample)
return samples
def get_next_design(self, X, Y):
return [] # One-shot sampling
def get_analysis(self, X, Y):
valid_Y = [y for y in Y if y is not None]
mean = sum(valid_Y) / len(valid_Y) if valid_Y else 0
return {"text": f"Mean: {mean:.2f}", "data": {"mean": mean}}
-
Push to GitHub and share repository URL
-
Install using
fz install algorithm <name>orfz install algorithm <url>
See examples/algorithms/PLUGIN_SYSTEM.md for complete documentation on the algorithm plugin system.
Interrupt Handling
FZ supports graceful interrupt handling for long-running calculations:
How to Interrupt
Press Ctrl+C during execution:
python run_study.py
# ... calculations running ...
# Press Ctrl+C
⚠️ Interrupt received (Ctrl+C). Gracefully shutting down...
⚠️ Press Ctrl+C again to force quit (not recommended)
What Happens
-
First Ctrl+C:
- Currently running calculations complete
- No new calculations start
- Partial results are saved
- Resources are cleaned up
- Signal handlers restored
-
Second Ctrl+C (not recommended):
- Immediate termination
- May leave resources in inconsistent state
Resuming After Interrupt
Use caching to resume from where you left off:
# First run (interrupted after 50/100 cases)
results1 = fz.fzr(
"input.txt",
{"param": list(range(100))},
model,
calculators="sh://bash calc.sh",
results_dir="results"
)
print(f"Completed {len(results1)} cases before interrupt")
# Resume using cache
results2 = fz.fzr(
"input.txt",
{"param": list(range(100))},
model,
calculators=[
"cache://results", # Reuse completed cases
"sh://bash calc.sh" # Run remaining cases
],
results_dir="results_resumed"
)
print(f"Total completed: {len(results2)} cases")
Example with Interrupt Handling
import fz
import signal
import sys
model = {
"varprefix": "$",
"output": {"result": "cat output.txt"}
}
def main():
try:
results = fz.fzr(
"input.txt",
{"param": list(range(1000))}, # Many cases
model,
calculators="sh://bash slow_calculation.sh",
results_dir="results"
)
print(f"\n✅ Completed {len(results)} calculations")
return results
except KeyboardInterrupt:
# This should rarely happen (graceful shutdown handles it)
print("\n❌ Forcefully terminated")
sys.exit(1)
if __name__ == "__main__":
main()
Output File Structure
Each case creates a directory with complete execution metadata:
log.txt - Execution Metadata
Command: bash calculate.sh input.txt
Exit code: 0
Time start: 2024-03-15T10:30:45.123456
Time end: 2024-03-15T10:32:12.654321
Execution time: 87.531 seconds
User: john_doe
Hostname: compute-01
Operating system: Linux
Platform: Linux-5.15.0-x86_64
Working directory: /tmp/fz_temp_abc123/case1
Original directory: /home/john/project
.fz_hash - Input File Checksums
a1b2c3d4e5f6... input.txt
f6e5d4c3b2a1... config.dat
Used for cache matching.
Breaking Changes
Version 0.9.1
fzr Directory Structure Change
Previous behavior (< 0.9.1):
fzrcreated subdirectories only when multiple values were provided for variables- Single values resulted in flat directory structure
New behavior (>= 0.9.1):
fzrcreates subdirectories inresults_diras long as anyinput_variableis provided- No subdirectories only when
input_variables={}(empty dict) - More consistent and predictable behavior
Example:
import fz
model = {"output": {"result": "cat output.txt"}}
# Single value - OLD: flat directory, NEW: subdirectory
results = fz.fzr(
"input.txt",
{"temp": 25}, # Single value
model,
calculators="sh://bash calc.sh",
results_dir="results"
)
# OLD (< 0.9.1):
# results/input.txt, results/output.txt, ...
#
# NEW (>= 0.9.1):
# results/temp=25/input.txt, results/temp=25/output.txt, ...
# Only flat when explicitly empty
results = fz.fzr(
"input.txt",
{}, # Empty - no variables
model,
calculators="sh://bash calc.sh",
results_dir="results"
)
# Both versions: results/input.txt, results/output.txt, ... (flat)
Migration:
- Update scripts expecting flat directory structure for single-value cases
- Use path parsing from
fzoto handle subdirectory names - Benefits: Better organization, consistent with parametric study expectations
Development
Running Tests
# Install development dependencies
pip install -e .[dev]
# Run all tests
python -m pytest tests/ -v
# Run specific test file
python -m pytest tests/test_examples_perfectgaz.py -v
# Run with debug output
FZ_LOG_LEVEL=DEBUG python -m pytest tests/test_parallel.py -v
# Run tests matching pattern
python -m pytest tests/ -k "parallel" -v
# Test interrupt handling
python -m pytest tests/test_interrupt_handling.py -v
# Run examples
python example_usage.py
python example_interrupt.py # Interactive interrupt demo
Project Structure
fz/
├── fz/ # Main package
│ ├── __init__.py # Public API exports
│ ├── core.py # Core functions (fzi, fzc, fzo, fzr, fzd)
│ ├── interpreter.py # Variable parsing, formula evaluation
│ ├── runners.py # Calculation execution (sh, ssh, cache)
│ ├── helpers.py # Parallel execution, retry logic
│ ├── io.py # File I/O, caching, hashing
│ ├── algorithms.py # Algorithm framework for fzd
│ ├── shell.py # Shell utilities, binary path resolution
│ ├── logging.py # Logging configuration
│ ├── cli.py # Command-line interface
│ └── config.py # Configuration management
├── examples/ # Example files
│ └── algorithms/ # Example algorithms for fzd
│ ├── montecarlo_uniform.py # Monte Carlo sampling
│ ├── randomsampling.py # Simple random sampling
│ ├── bfgs.py # BFGS optimization
│ └── brent.py # Brent's 1D optimization
├── tests/ # Test suite
│ ├── test_parallel.py # Parallel execution tests
│ ├── test_interrupt_handling.py # Interrupt handling tests
│ ├── test_fzd.py # Design of experiments tests
│ ├── test_examples_*.py # Example-based tests
│ └── ...
├── docs/ # Documentation
│ └── FZD_CONTENT_FORMATS.md # fzd content format documentation
├── README.md # This file
└── setup.py # Package configuration
Testing Your Own Models
Create a test following this pattern:
import fz
import tempfile
from pathlib import Path
def test_my_model():
# Create input
with tempfile.TemporaryDirectory() as tmpdir:
input_file = Path(tmpdir) / "input.txt"
input_file.write_text("Parameter: $param\n")
# Create calculator script
calc_script = Path(tmpdir) / "calc.sh"
calc_script.write_text("""#!/bin/bash
source $1
echo "result=$param" > output.txt
""")
calc_script.chmod(0o755)
# Define model
model = {
"varprefix": "$",
"output": {
"result": "grep 'result=' output.txt | cut -d= -f2"
}
}
# Run test
results = fz.fzr(
str(input_file),
{"param": [1, 2, 3]},
model,
calculators=f"sh://bash {calc_script}",
results_dir=str(Path(tmpdir) / "results")
)
# Verify
assert len(results) == 3
assert list(results['result']) == [1, 2, 3]
assert all(results['status'] == 'done')
print("✅ Test passed!")
if __name__ == "__main__":
test_my_model()
Troubleshooting
Common Issues
Problem: Calculations fail with "command not found"
# Solution: Use absolute paths in calculator URIs
calculators = "sh://bash /full/path/to/script.sh"
Problem: SSH calculations hang
# Solution: Increase timeout or check SSH connectivity
calculators = "ssh://user@host/bash script.sh"
# Test manually: ssh user@host "bash script.sh"
Problem: Cache not working
# Solution: Check .fz_hash files exist in cache directories
# Enable debug logging to see cache matching process
import os
os.environ['FZ_LOG_LEVEL'] = 'DEBUG'
Problem: Out of memory with many parallel cases
# Solution: Limit parallel workers
export FZ_MAX_WORKERS=2
Windows / Cross-Platform
Problem: Shell commands fail on Windows
# Solution: Install MSYS2 and set FZ_SHELL_PATH to point to its binaries
SET FZ_SHELL_PATH=C:\msys64\usr\bin;C:\msys64\mingw64\bin
# See examples/shell_path_example.md for details
Problem: Line ending issues on Windows
# Solution: Write input files with Unix line endings (newline='\n')
# FZ templates and shell scripts expect LF, not CRLF
with open("input.txt", "w", newline='\n') as f:
f.write(content)
Problem: chmod has no effect on Windows
# This is expected — Windows does not support Unix file permissions.
# Shell scripts run via sh:// do not need chmod on Windows.
Debug Mode
Enable detailed logging:
import os
os.environ['FZ_LOG_LEVEL'] = 'DEBUG'
results = fz.fzr(...) # Will show detailed execution logs
Debug output includes:
- Calculator selection and locking
- File operations
- Command execution
- Cache matching
- Thread pool management
- Temporary directory preservation
Performance Tips
- Use caching: Reuse previous results when possible
- Limit parallelism: Don't exceed your CPU/memory limits
- Optimize calculators: Fast calculators first in the list
- Batch similar cases: Group cases that use the same calculator
- Use SSH keepalive: For long-running remote calculations
- Clean old results: Remove old result directories to save disk space
License
BSD 3-Clause License. See LICENSE file for details.
Contributing
Contributions welcome! Please:
- Fork the repository
- Create a feature branch
- Add tests for new features
- Ensure all tests pass
- Submit a pull request
Citation
If you use FZ in your research, please cite:
@software{fz,
title = {FZ: Parametric Scientific Computing Framework},
designers = {[Yann Richet]},
authors = {[Claude Sonnet, Yann Richet]},
year = {2025},
url = {https://github.com/Funz/fz}
}
Using fz with AI Coding Agents
An Agent Skill is bundled in skills/fz/ to teach
AI coding agents (Claude Code, and other agents supporting the skills format) the fz
workflow: parameterizing input files, defining models, choosing calculators, and running
parametric studies.
Easiest (Claude Code plugin) — install straight from Claude Code, no shell needed:
/plugin marketplace add Funz/fz
/plugin install fz@funz
The skill then loads automatically in every project, and updates with the marketplace.
The plugin also adds four slash commands — /fz:wrap, /fz:run, /fz:design,
/fz:install — that pre-frame the corresponding workflow.
Manual install — copy (or symlink) the skill into your project or user skills directory:
# Project-level (recommended): available to anyone working in the project
mkdir -p .claude/skills
cp -r /path/to/fz/skills/fz .claude/skills/
# Or user-level: available in all your projects
mkdir -p ~/.claude/skills
cp -r /path/to/fz/skills/fz ~/.claude/skills/
Then just ask the agent things like "wrap my simulation code with fz and run a parameter study over mesh_size and timestep" — the skill is loaded automatically when relevant.
The skill contains:
- skills/fz/SKILL.md - Step-by-step workflow for wrapping a simulation code
- skills/fz/reference.md - Condensed API/CLI reference, JSON schemas, environment variables
- skills/fz/algorithm-wrapper.md - Interface for writing custom fzd algorithms
See skills/howto.md for a complete walkthrough with example prompts (parametric studies, SSH execution, cache reuse, optimization, headless usage).
Documentation
Main Documentation
- README.md (this file) - Complete user guide with examples
- NEWS.md - Release notes and changelog (version 0.9.1 and later)
- doc/funz-protocol.md - Funz protocol and UDP discovery documentation
- doc/shell-path.md - FZ_SHELL_PATH configuration details
Context Documentation
Modular documentation in the doc/ directory:
- doc/INDEX.md - Documentation overview and navigation
- doc/overview.md - High-level FZ concepts and design
- doc/core-functions.md - API reference for fzi, fzc, fzo, fzr, fzl, fzd
- doc/installing-models.md - Installing models and algorithms (
fz install) - doc/calculators.md - Calculator types, URIs, and configuration
- doc/model-definition.md - Model structure, aliases, and output parsing
- doc/formulas-and-interpreters.md - Formula evaluation (Python/R)
- doc/syntax-guide.md - Input template syntax reference
- doc/parallel-and-caching.md - Performance optimization strategies
- doc/quick-examples.md - Common usage patterns and snippets
Examples
Practical examples in the examples/ directory:
- examples/examples.md - Overview of all examples
- examples/fzd_example.md - Iterative design of experiments (fzd) examples
- examples/dataframe_input.md - DataFrame input for non-factorial designs
- examples/vector_outputs_example.md - Vector/array-valued outputs with fzr and fzo
- examples/algorithm_options_example.md - Algorithm options format guide
- examples/r_interpreter_example.md - R interpreter setup and usage
- examples/shell_path_example.md - FZ_SHELL_PATH configuration examples
- examples/java_funz_syntax_example.py - Legacy Funz syntax compatibility
- examples/fzi_formulas_example.py - Formula evaluation examples
- examples/fzi_static_objects_example.py - Static object handling
Test Examples
Working examples in test files:
tests/test_examples_*.py- Comprehensive integration teststests/test_parallel.py- Parallel execution examplestests/test_interrupt_handling.py- Interrupt handling demonstrationstests/test_funz_protocol.py- Funz server protocol examplestests/test_slurm_runner.py- SLURM workload manager examples
Support
- Issues: https://github.com/Funz/fz/issues
- Documentation: https://fz.github.io
- Repository: https://github.com/Funz/fz
Release files for funz-fz 1.2
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| funz_fz-1.2.tar.gz | 429.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| funz_fz-1.2-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 606.8 kB
Release files / funz_fz-1.2.tar.gz
| Download URL | funz_fz-1.2.tar.gz |
|---|---|
| Size | 429.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4bcc272fc8c28300d3424568d844a6f5c32e996ffb733e450886872ed0edcc7b
|
|
BLAKE2b-256 checksum How to use checksums |
069402ac6bf6c71b43a1f4911b368b8cf84c33ad1cce3705df61e61e7b144bb6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 4, 2026.
Transparency logRelease files / funz_fz-1.2-py3-none-any.whl
| Download URL | funz_fz-1.2-py3-none-any.whl |
|---|---|
| Size | 177.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1de2970e237eb068347398a6ecaaf312eae6f455f36a085a8604a337b4131e61
|
|
BLAKE2b-256 checksum How to use checksums |
a51fe61c35e18fdfa21fe600367619227abc7b20ff20a4f1c61fb80e0ac6e147
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 4, 2026.
Transparency log