diff --git a/.fz/calculators/Model.sh b/.fz/calculators/Model.sh new file mode 100755 index 0000000..02fa528 --- /dev/null +++ b/.fz/calculators/Model.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# Model calculator script (mock implementation) +# Compatible with fz framework +# +# This is a template script that demonstrates the structure of a calculator. +# Replace this with actual calls to your simulation code. + +# if directory as input, cd into it +if [ -d "$1" ]; then + cd "$1" + # Find the first input file (not .out or .msg) + input=$(ls | grep -v '\.out$' | grep -v '\.msg$' | grep -v '\.sh$' | head -n 1) + if [ -z "$input" ]; then + echo "No input file found in directory. Exiting." + exit 1 + fi + shift +# if $1 is a file, use it +elif [ -f "$1" ]; then + input="$1" + shift +else + echo "Usage: $0 " + exit 2 +fi + +PID_FILE=$PWD/PID +echo $$ >> $PID_FILE + +# Mock calculation: extract the value from the input file and write to output +# This simulates a real calculation that produces an output file +# Replace this section with actual calls to your simulation code + +echo "Running mock calculation on $input..." + +# Extract variable value from input (looking for "value = X" pattern) +value=$(grep -E '^value\s*=' "$input" | sed 's/.*=\s*//' | tr -d '[:space:]') + +if [ -n "$value" ]; then + # Mock computation: just return the value (or compute something from it) + result=$value + echo "Input value: $value" + echo "Result: $result" + echo "$result" > output.txt + echo "Calculation completed successfully." +else + echo "Warning: No value found in input file" + echo "" > output.txt +fi + +if [ -f "$PID_FILE" ]; then + rm -f "$PID_FILE" +fi diff --git a/.fz/calculators/localhost_Model.json b/.fz/calculators/localhost_Model.json new file mode 100644 index 0000000..bc4357d --- /dev/null +++ b/.fz/calculators/localhost_Model.json @@ -0,0 +1,6 @@ +{ + "uri": "sh://", + "models": { + "Model": "bash .fz/calculators/Model.sh" + } +} diff --git a/.fz/models/Model.json b/.fz/models/Model.json new file mode 100644 index 0000000..f664a3d --- /dev/null +++ b/.fz/models/Model.json @@ -0,0 +1,10 @@ +{ + "id": "Model", + "varprefix": "$", + "formulaprefix": "@", + "delim": "{}", + "commentline": "#", + "output": { + "result": "cat output.txt 2>/dev/null || echo ''" + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1b6fd5b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,122 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +permissions: + contents: read + +jobs: + test: + name: Test Plugin Structure + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install fz framework + run: | + pip install git+https://github.com/Funz/fz.git + + - name: Validate JSON files + run: | + echo "Validating model files..." + for f in .fz/models/*.json; do + echo " Checking $f" + python -m json.tool "$f" > /dev/null + done + echo "Validating calculator config files..." + for f in .fz/calculators/*.json; do + echo " Checking $f" + python -m json.tool "$f" > /dev/null + done + + - name: Check shell script syntax + run: | + echo "Checking shell scripts..." + for f in .fz/calculators/*.sh; do + echo " Checking $f" + bash -n "$f" + done + + - name: Test fzi (parse variables) + run: | + python -c " + import fz + variables = fz.fzi('examples/Model/input.txt', 'Model') + print(f'Variables found: {list(variables.keys())}') + assert 'x' in variables, 'Variable x not found' + print('✓ fzi test passed') + " + + - name: Test fzc (compile input) + run: | + python -c " + import fz + import tempfile + import os + with tempfile.TemporaryDirectory() as tmpdir: + fz.fzc('examples/Model/input.txt', {'x': 3.14}, 'Model', output_dir=tmpdir) + compiled = os.path.join(tmpdir, 'x=3.14', 'input.txt') + assert os.path.exists(compiled), 'Compiled file not created' + with open(compiled) as f: + content = f.read() + assert '3.14' in content, 'Variable not substituted' + print('✓ fzc test passed') + " + + - name: Run plugin tests + run: | + python tests/test_plugin.py + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check shell scripts are executable + run: | + for f in .fz/calculators/*.sh; do + if [ ! -x "$f" ]; then + echo "Error: $f is not executable" + exit 1 + fi + done + + docs: + name: Check Documentation + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check required documentation files + run: | + for f in README.md LICENSE; do + if [ ! -f "$f" ]; then + echo "Error: Missing $f" + exit 1 + fi + echo "✓ $f exists" + done + + - name: Check example files + run: | + if [ ! -f "examples/Model/input.txt" ]; then + echo "Error: Missing example input file" + exit 1 + fi + echo "✓ Example files present" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..187e788 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Results and temporary files +results/ +*.out +*.msg +*.html +PID + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +venv/ +ENV/ +.venv + +# fz temporary files +.fz/tmp/ + +# OS +.DS_Store +Thumbs.db diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c96bc41 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2025, Funz Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 8f660d7..2d25061 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,178 @@ # fz-Model -Generic template repository for fz model plugin + +A [Funz](https://github.com/Funz/fz) plugin template repository for creating custom model plugins. + +This repository serves as a template and starting point for creating new Funz model plugins. It provides a working example with a mock calculator that you can customize for your specific simulation code. + +## Features + +### Input Syntax +- **Variable syntax**: `${variable_name}` +- **Formula syntax**: `@{formula}` +- **Comment character**: `#` + +### Supported Output Variables + +The template model (`Model`) extracts these output variables: +- `result`: The main result value from the calculation + +## Installation + +This plugin requires the [Funz/fz](https://github.com/Funz/fz) framework. + +```bash +pip install git+https://github.com/Funz/fz.git +``` + +## Usage + +### With fz Python API + +```python +import fz + +# Example: Run calculation with varying x value +results = fz.fzr( + input_path="examples/Model/input.txt", + input_variables={ + "x": [1.0, 2.0, 3.0, 4.0, 5.0] + }, + model="Model", + calculators="localhost_Model", + results_dir="my_results" +) + +print(results[['x', 'result']]) +``` + +### Directory Structure + +``` +your_project/ +├── examples/ +│ └── Model/ +│ └── input.txt # Example input file +├── .fz/ +│ ├── models/ +│ │ └── Model.json # Model configuration +│ └── calculators/ +│ ├── Model.sh # Calculator script (mock) +│ └── localhost_Model.json +├── tests/ +│ └── test_plugin.py # Test suite +└── results/ # Generated by fz +``` + +## Example Input File + +``` +# Example input file for Model plugin +# Variables are defined using ${variable_name} syntax + +value = ${x} +``` + +In this example: +- `${x}` is a variable parameter that will be substituted by fz +- Lines starting with `#` are comments + +## Creating Your Own Plugin + +To create a custom plugin based on this template: + +1. **Clone this repository** as a starting point +2. **Rename the model**: + - Rename `.fz/models/Model.json` to `.fz/models/YourModel.json` + - Update the `id` field in the JSON file +3. **Customize the calculator script**: + - Rename `.fz/calculators/Model.sh` to `.fz/calculators/YourModel.sh` + - Implement actual calls to your simulation code +4. **Update output parsing**: + - Edit the `output` section in your model JSON + - Add shell commands that extract values from your output files +5. **Update calculator configuration**: + - Edit `.fz/calculators/localhost_Model.json` + - Update model mappings to match your script names +6. **Add examples**: + - Create example input files for your simulation code + +### Model Configuration + +The model JSON file defines variable syntax and output parsing: + +```json +{ + "id": "Model", + "varprefix": "$", + "formulaprefix": "@", + "delim": "{}", + "commentline": "#", + "output": { + "result": "cat output.txt 2>/dev/null || echo ''" + } +} +``` + +Fields: +- `id`: Unique identifier for the model +- `varprefix`: Character prefix for variables (e.g., `$` for `${x}`) +- `formulaprefix`: Character prefix for formulas +- `delim`: Delimiter characters around variable names +- `commentline`: Character(s) that start a comment line +- `output`: Mapping of output variable names to shell commands that extract their values + +### Calculator Configuration + +The calculator JSON files define how to execute your code: + +```json +{ + "uri": "sh://", + "models": { + "Model": "bash .fz/calculators/Model.sh" + } +} +``` + +- `uri`: Execution method (`sh://` for local shell, `ssh://` for remote) +- `models`: Mapping of model names to execution commands + +## Remote Execution + +To run calculations on a remote server: + +```python +results = fz.fzr( + input_path="input.txt", + input_variables={"x": [1.0, 2.0, 3.0]}, + model="Model", + calculators="ssh://user@server.com/bash /path/to/calculators/Model.sh", + results_dir="remote_results" +) +``` + +## Troubleshooting + +### Calculator script not found +Ensure the calculator script is executable: +```bash +chmod +x .fz/calculators/Model.sh +``` + +### Output variable not found +Check the `output` section in your model JSON file. The shell commands must correctly parse your output files. + +## Running Tests + +```bash +python tests/test_plugin.py +``` + +## License + +BSD 3-Clause License. See [LICENSE](LICENSE) file. + +## Related Links + +- [Funz/fz](https://github.com/Funz/fz) - Main framework +- [Funz/fz-Scale](https://github.com/Funz/fz-Scale) - Example plugin for SCALE code diff --git a/examples/Model/input.txt b/examples/Model/input.txt new file mode 100644 index 0000000..aa74b94 --- /dev/null +++ b/examples/Model/input.txt @@ -0,0 +1,4 @@ +# Example input file for Model plugin +# Variables are defined using ${variable_name} syntax + +value = ${x} diff --git a/tests/test_plugin.py b/tests/test_plugin.py new file mode 100644 index 0000000..712a894 --- /dev/null +++ b/tests/test_plugin.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +""" +Basic test suite for fz-Model plugin. + +These tests verify the plugin structure. +They test: +- Model file validity +- Variable parsing +- Input file compilation +- Calculator configuration +""" + +import os +import json +import tempfile +import sys + + +def test_model_files(): + """Test that all model JSON files are valid and have required fields.""" + print("Testing model files...") + + models = [ + ".fz/models/Model.json" + ] + + required_fields = ["id", "varprefix", "delim", "commentline", "output"] + + for model_file in models: + print(f" Checking {model_file}...", end=" ") + + # Check file exists + assert os.path.exists(model_file), f"File not found: {model_file}" + + # Load and validate JSON + with open(model_file, 'r') as f: + model = json.load(f) + + # Check required fields + for field in required_fields: + assert field in model, f"Missing field '{field}' in {model_file}" + + # Check output section has at least one variable + assert len(model["output"]) > 0, f"No output variables in {model_file}" + + print("✓") + + print(" All model files valid!\n") + + +def test_calculator_files(): + """Test that calculator JSON files are valid.""" + print("Testing calculator configuration files...") + + calculators = [ + ".fz/calculators/localhost_Model.json" + ] + + for calc_file in calculators: + print(f" Checking {calc_file}...", end=" ") + + # Check file exists + assert os.path.exists(calc_file), f"File not found: {calc_file}" + + # Load and validate JSON + with open(calc_file, 'r') as f: + calc = json.load(f) + + # Check required fields + assert "uri" in calc, f"Missing 'uri' field in {calc_file}" + assert "models" in calc, f"Missing 'models' field in {calc_file}" + + print("✓") + + print(" All calculator files valid!\n") + + +def test_calculator_scripts(): + """Test that calculator shell scripts exist and are executable.""" + print("Testing calculator shell scripts...") + + scripts = [ + ".fz/calculators/Model.sh" + ] + + for script_file in scripts: + print(f" Checking {script_file}...", end=" ") + + # Check file exists + assert os.path.exists(script_file), f"File not found: {script_file}" + + # Check if executable + assert os.access(script_file, os.X_OK), f"Script not executable: {script_file}" + + print("✓") + + print(" All calculator scripts valid!\n") + + +def test_example_files(): + """Test that example files exist.""" + print("Testing example files...") + + examples = [ + "examples/Model/input.txt" + ] + + for example_file in examples: + print(f" Checking {example_file}...", end=" ") + assert os.path.exists(example_file), f"File not found: {example_file}" + print("✓") + + print(" All example files present!\n") + + +def test_with_fz(): + """Test integration with fz framework (if available).""" + print("Testing fz framework integration...") + + try: + import fz + print(" fz module found ✓") + + # Test parsing input file + print(" Testing fz.fzi() on input.txt...", end=" ") + variables = fz.fzi("examples/Model/input.txt", "Model") + assert "x" in variables, "Variable 'x' not found in parsed input" + print("✓") + + # Test compiling input file + print(" Testing fz.fzc() compilation...", end=" ") + with tempfile.TemporaryDirectory() as tmpdir: + fz.fzc( + "examples/Model/input.txt", + {"x": 3.14}, + "Model", + output_dir=tmpdir + ) + + # Check compiled file exists + compiled_file = os.path.join(tmpdir, "x=3.14", "input.txt") + assert os.path.exists(compiled_file), "Compiled file not created" + + # Check variable was substituted + with open(compiled_file, 'r') as f: + content = f.read() + assert "3.14" in content, "Variable not substituted" + assert "${x}" not in content, "Variable marker still present" + print("✓") + + print(" fz integration tests passed!\n") + + except ImportError: + print(" fz module not installed - skipping integration tests") + print(" (Install with: pip install git+https://github.com/Funz/fz.git)\n") + + +def main(): + """Run all tests.""" + print("=" * 70) + print("fz-Model Plugin Test Suite") + print("=" * 70) + print() + + # Change to repository root if needed + if not os.path.exists(".fz"): + if os.path.exists("../fz-Model/.fz"): + os.chdir("../fz-Model") + else: + print("Error: Could not find .fz directory") + print("Please run this script from the fz-Model repository root") + return 1 + + try: + test_model_files() + test_calculator_files() + test_calculator_scripts() + test_example_files() + test_with_fz() + + print("=" * 70) + print("All tests passed! ✓") + print("=" * 70) + return 0 + + except AssertionError as e: + print(f"\n✗ Test failed: {e}") + return 1 + except Exception as e: + print(f"\n✗ Unexpected error: {e}") + import traceback + traceback.print_exc() + return 1 + + +if __name__ == "__main__": + sys.exit(main())