diff --git a/.fz/calculators/localhost.json b/.fz/calculators/localhost_Modelica.json similarity index 100% rename from .fz/calculators/localhost.json rename to .fz/calculators/localhost_Modelica.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..ef8b6b9 --- /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/Modelica/NewtonCooling.mo', 'Modelica') + print(f'Variables found: {list(variables.keys())}') + assert 'convection' in variables, 'Variable convection 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/Modelica/NewtonCooling.mo', {'convection': 0.5}, 'Modelica', output_dir=tmpdir) + compiled = os.path.join(tmpdir, 'convection=0.5', 'NewtonCooling.mo') + assert os.path.exists(compiled), 'Compiled file not created' + with open(compiled) as f: + content = f.read() + assert '0.5' 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/Modelica/NewtonCooling.mo" ]; then + echo "Error: Missing example input file" + exit 1 + fi + echo "✓ Example files present" diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml deleted file mode 100644 index 409d4a7..0000000 --- a/.github/workflows/verify.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Verify Plugin Structure - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main, develop ] - -permissions: - contents: read - -jobs: - verify: - 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.9' - - - name: Run verification script - run: | - python3 tests/verify_installation.py - - - name: Validate JSON files - run: | - echo "Validating JSON files..." - python3 -m json.tool .fz/models/Modelica.json > /dev/null - python3 -m json.tool .fz/calculators/localhost.json > /dev/null - echo "✓ All JSON files are valid" - - - name: Check shell script syntax - run: | - echo "Checking shell script syntax..." - bash -n .fz/calculators/Modelica.sh - echo "✓ Shell script syntax is valid" diff --git a/.gitignore b/.gitignore index e812728..cd7be7d 100644 --- a/.gitignore +++ b/.gitignore @@ -44,6 +44,10 @@ results/ results_*/ output/ .fz/tmp/ +*.out +*.msg +*.html +PID # IDE .vscode/ diff --git a/README.md b/README.md index 7360ca7..c38a044 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,73 @@ # fz-modelica -OpenModelica plugin for Funz framework - simulate and analyze Modelica models through parametric studies. +A [Funz](https://github.com/Funz/fz) plugin for OpenModelica - simulate and analyze Modelica models through parametric studies. -## Overview +## Features -This plugin integrates OpenModelica with the Funz framework, enabling: -- Parametric studies of Modelica models -- Batch simulations with different parameter sets -- Result extraction and analysis -- Optimization and design of experiments using Funz capabilities +### Input Syntax +- **Variable syntax**: `${variable_name~default_value}` +- **Formula syntax**: `@{formula}` +- **Comment character**: `//` -## Prerequisites +### Supported Output Variables -### Required -- **Funz framework**: `pip install funz-fz` -- **OpenModelica**: Install from [https://openmodelica.org/](https://openmodelica.org/) (see [INSTALL_OPENMODELICA.md](INSTALL_OPENMODELICA.md)) -- **Python 3.7+**: With pandas package +The Modelica model extracts simulation results: +- `res`: JSON dictionary containing all simulation output CSV data -### Install Dependencies -```bash -# Core dependencies -pip install funz-fz pandas +## Installation + +This plugin requires the [Funz/fz](https://github.com/Funz/fz) framework and OpenModelica. -# For running example notebooks -pip install jupyter matplotlib scipy +```bash +pip install git+https://github.com/Funz/fz.git ``` -See [INSTALL_OPENMODELICA.md](INSTALL_OPENMODELICA.md) for detailed OpenModelica installation instructions for different platforms. +See [INSTALL_OPENMODELICA.md](INSTALL_OPENMODELICA.md) for detailed OpenModelica installation instructions. -## Quick Start +## Usage -### 1. Verify Installation -```bash -python tests/verify_installation.py +### With fz Python API + +```python +import fz + +# Example: Run simulation with varying parameters +results = fz.fzr( + input_path="examples/Modelica/NewtonCooling.mo", + input_variables={ + "convection": [0.5, 0.7, 0.9] + }, + model="Modelica", + calculators="localhost_Modelica", + results_dir="my_results" +) + +print(results[['convection', 'res']]) ``` -### 2. Explore Example Notebooks +### Directory Structure -The `examples/` directory contains comprehensive Jupyter notebooks demonstrating the plugin: +``` +your_project/ +├── examples/ +│ └── Modelica/ +│ ├── NewtonCooling.mo # Example Modelica model +│ └── ProjectileMotion.mo # Example physics model +├── .fz/ +│ ├── models/ +│ │ └── Modelica.json # Model configuration +│ └── calculators/ +│ ├── Modelica.sh # Calculator script +│ └── localhost_Modelica.json +├── tests/ +│ └── test_plugin.py # Test suite +├── *.ipynb # Jupyter notebook examples +└── results/ # Generated by fz +``` + +## Example Notebooks + +Comprehensive Jupyter notebooks demonstrating the plugin: **📓 01_NewtonCooling_Parametric.ipynb** - Introduction to Funz-Modelica workflow @@ -57,86 +87,97 @@ The `examples/` directory contains comprehensive Jupyter notebooks demonstrating - Target hitting optimization - Result caching for efficiency -### 3. Run the Notebooks +### Run the Notebooks ```bash +# Install dependencies +pip install jupyter matplotlib scipy pandas + # Start Jupyter jupyter notebook - -# Or use JupyterLab -jupyter lab - -# Navigate to examples/ and open any notebook -``` - -## Project Structure -``` -. -├── .fz/ # Funz plugin configuration -│ ├── models/ -│ │ └── Modelica.json # Model definition -│ └── calculators/ -│ ├── localhost.json # Calculator configuration -│ └── Modelica.sh # Execution script -├── examples/ # Jupyter notebook examples -│ ├── 01_NewtonCooling_Parametric.ipynb # Intro to parametric studies -│ ├── 02_ProjectileMotion_Parametric.ipynb # Physics simulations -│ └── 03_ProjectileMotion_Advanced.ipynb # Advanced features & optimization -├── tests/ -│ └── verify_installation.py # Installation verification -└── Documentation files - ├── README.md # This file - ├── STRUCTURE.md # Repository structure details - ├── MIGRATION.md # Migration from old plugin - ├── INSTALL_OPENMODELICA.md # OpenModelica setup - └── CONTRIBUTING.md # Contribution guidelines ``` -## Creating Parametric Models - -Add variable syntax to your Modelica model parameters: +## Example Input File ```modelica -model MyModel - parameter Real param1=${var1~default_value}; +model NewtonCooling "An example of Newton's law of cooling" + parameter Real T_inf=25 "Ambient temperature"; + parameter Real T0=90 "Initial temperature"; + parameter Real h=${convection~0.7} "Convective cooling coefficient"; // ... rest of model -end MyModel; +end NewtonCooling; ``` -Then run with Funz: - -```python -import fz - -results = fz.fzr( - "MyModel.mo", - {"var1": [0.5, 0.7, 0.9]}, # Test multiple values - "Modelica", - calculators="localhost" -) +In this example: +- `${convection~0.7}` is a variable parameter with default value 0.7 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. **Customize the calculator script**: + - Edit `.fz/calculators/Modelica.sh` for your specific needs + - Implement actual calls to your simulation code +3. **Update output parsing**: + - Edit the `output` section in your model JSON + - Add shell commands that extract values from your output files +4. **Add examples**: + - Create example input files for your simulation code + +### Model Configuration + +The model JSON file defines variable syntax and output parsing: + +```json +{ + "id": "Modelica", + "varprefix": "$", + "formulaprefix": "@", + "delim": "{}", + "commentline": "//", + "output": { + "res": "python -c '...'" + } +} ``` -## Testing - -Verify the plugin structure: -```bash -python tests/verify_installation.py +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": { + "Modelica": "bash .fz/calculators/Modelica.sh" + } +} ``` -This checks: -- Directory structure -- Configuration files -- Example notebooks -- Plugin executability +- `uri`: Execution method (`sh://` for local shell, `ssh://` for remote) +- `models`: Mapping of model names to execution commands -### Running Example Notebooks +## Remote Execution -The notebooks demonstrate complete workflows and can be run interactively: -```bash -# Install Jupyter if needed -pip install jupyter matplotlib scipy +To run calculations on a remote server: -# Launch Jupyter -jupyter notebook examples/ +```python +results = fz.fzr( + input_path="model.mo", + input_variables={"x": [1.0, 2.0, 3.0]}, + model="Modelica", + calculators="ssh://user@server.com/bash /path/to/calculators/Modelica.sh", + results_dir="remote_results" +) ``` ## Troubleshooting @@ -147,23 +188,28 @@ Error: omc command not found ``` Install OpenModelica from [https://openmodelica.org/](https://openmodelica.org/) or see [INSTALL_OPENMODELICA.md](INSTALL_OPENMODELICA.md) -### Funz not installed -``` -Error: No module named 'fz' +### Calculator script not found +Ensure the calculator script is executable: +```bash +chmod +x .fz/calculators/Modelica.sh ``` -Install Funz: `pip install funz-fz` -### Simulation fails -Check the temporary directory for `*.moo` log files from OpenModelica. +### Output variable not found +Check the `output` section in your model JSON file. The shell commands must correctly parse your output files. -## Documentation +## Running Tests -- [README.md](README.md) - This file (quick start) -- [STRUCTURE.md](STRUCTURE.md) - Complete repository structure -- [MIGRATION.md](MIGRATION.md) - Migration guide from old plugin -- [INSTALL_OPENMODELICA.md](INSTALL_OPENMODELICA.md) - OpenModelica installation -- [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution guidelines +```bash +python tests/test_plugin.py +``` ## License -This project is part of the Funz framework. See [LICENSE](LICENSE) for details. +BSD 3-Clause License. See [LICENSE](LICENSE) file. + +## Related Links + +- [Funz/fz](https://github.com/Funz/fz) - Main framework +- [Funz/fz-Model](https://github.com/Funz/fz-Model) - Template repository +- [INSTALL_OPENMODELICA.md](INSTALL_OPENMODELICA.md) - OpenModelica installation +- [CONTRIBUTING.md](CONTRIBUTING.md) - Contribution guidelines diff --git a/examples/Modelica/NewtonCooling.mo b/examples/Modelica/NewtonCooling.mo new file mode 100644 index 0000000..62b6fe3 --- /dev/null +++ b/examples/Modelica/NewtonCooling.mo @@ -0,0 +1,14 @@ +// @ref http://book.xogeny.com/behavior/equations/physical/ +model NewtonCooling "An example of Newton's law of cooling" + parameter Real T_inf=25 "Ambient temperature"; + parameter Real T0=90 "Initial temperature"; + parameter Real h=${convection~0.7} "Convective cooling coefficient"; + parameter Real A=1.0 "Surface area"; + parameter Real m=0.1 "Mass of thermal capacitance"; + parameter Real c_p=1.2 "Specific heat"; + Real T "Temperature"; +initial equation + T = T0 "Specify initial value for T"; +equation + m*c_p*der(T) = h*A*(T_inf-T) "Newton's law of cooling"; +end NewtonCooling; diff --git a/examples/Modelica/ProjectileMotion.mo b/examples/Modelica/ProjectileMotion.mo new file mode 100644 index 0000000..8dfd408 --- /dev/null +++ b/examples/Modelica/ProjectileMotion.mo @@ -0,0 +1,25 @@ +model ProjectileMotion + "Parametric projectile motion" + + // Parametric variables (can be varied by Funz) + parameter Real v0 = ${velocity~20.0} "Initial velocity (m/s)"; + parameter Real angle = ${launch_angle~45.0} "Launch angle (degrees)"; + + // Fixed parameters + parameter Real g = 9.81 "Gravitational acceleration (m/s^2)"; + parameter Real m = 1.0 "Mass (kg)"; + + // State variables + Real x(start = 0.0) "Horizontal position (m)"; + Real y(start = 0.0) "Vertical position (m)"; + Real vx(start = v0 * cos(angle * 3.14159265359 / 180.0)) "Horizontal velocity (m/s)"; + Real vy(start = v0 * sin(angle * 3.14159265359 / 180.0)) "Vertical velocity (m/s)"; + +equation + // Equations of motion + der(x) = vx; + der(y) = vy; + der(vx) = 0; // No horizontal acceleration + der(vy) = -g; // Gravitational acceleration downward + +end ProjectileMotion; diff --git a/tests/test_plugin.py b/tests/test_plugin.py new file mode 100644 index 0000000..7df0bba --- /dev/null +++ b/tests/test_plugin.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +""" +Basic test suite for fz-modelica 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/Modelica.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_Modelica.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/Modelica.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/Modelica/NewtonCooling.mo", + "examples/Modelica/ProjectileMotion.mo" + ] + + 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 NewtonCooling.mo...", end=" ") + variables = fz.fzi("examples/Modelica/NewtonCooling.mo", "Modelica") + assert "convection" in variables, "Variable 'convection' not found in parsed input" + print("✓") + + # Test compiling input file + print(" Testing fz.fzc() compilation...", end=" ") + with tempfile.TemporaryDirectory() as tmpdir: + fz.fzc( + "examples/Modelica/NewtonCooling.mo", + {"convection": 0.5}, + "Modelica", + output_dir=tmpdir + ) + + # Check compiled file exists (it may be in a subdirectory) + compiled_file = None + for root, dirs, files in os.walk(tmpdir): + if "NewtonCooling.mo" in files: + compiled_file = os.path.join(root, "NewtonCooling.mo") + break + assert compiled_file is not None, "Compiled file not created" + + # Check variables were substituted + with open(compiled_file, 'r') as f: + content = f.read() + assert "0.5" in content, "Variable convection not substituted" + assert "${convection" not in content, "Variable marker ${convection} 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-modelica Plugin Test Suite") + print("=" * 70) + print() + + # Change to repository root if needed + if not os.path.exists(".fz"): + if os.path.exists("../fz-modelica/.fz"): + os.chdir("../fz-modelica") + else: + print("Error: Could not find .fz directory") + print("Please run this script from the fz-modelica 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()) diff --git a/tests/verify_installation.py b/tests/verify_installation.py deleted file mode 100755 index 7114158..0000000 --- a/tests/verify_installation.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/env python3 -""" -Basic verification script for fz-modelica plugin. -Checks that all required files and configurations are present. -""" - -import os -import sys -import json -from pathlib import Path - - -def check_file_exists(filepath, description): - """Check if a file exists and report status.""" - if os.path.exists(filepath): - print(f"✓ {description}: {filepath}") - return True - else: - print(f"✗ {description}: {filepath} NOT FOUND") - return False - - -def check_json_valid(filepath, description): - """Check if a JSON file is valid.""" - try: - with open(filepath, 'r') as f: - json.load(f) - print(f"✓ {description} is valid JSON") - return True - except Exception as e: - print(f"✗ {description} JSON parsing error: {e}") - return False - - -def check_executable(filepath, description): - """Check if a file is executable.""" - if os.access(filepath, os.X_OK): - print(f"✓ {description} is executable") - return True - else: - print(f"✗ {description} is NOT executable") - return False - - -def main(): - """Main verification function.""" - print("=" * 60) - print("fz-modelica Plugin Verification") - print("=" * 60) - print() - - # Get repository root - repo_root = Path(__file__).parent.parent - os.chdir(repo_root) - - all_checks_passed = True - - # Check directory structure - print("Checking directory structure...") - dirs = [ - ".fz/models", - ".fz/calculators", - "examples", - "tests" - ] - for d in dirs: - all_checks_passed &= check_file_exists(d, f"Directory {d}") - print() - - # Check model configuration - print("Checking model configuration...") - model_file = ".fz/models/Modelica.json" - all_checks_passed &= check_file_exists(model_file, "Model configuration") - if os.path.exists(model_file): - all_checks_passed &= check_json_valid(model_file, "Model configuration") - print() - - # Check calculator configuration - print("Checking calculator configuration...") - calc_config = ".fz/calculators/localhost.json" - all_checks_passed &= check_file_exists(calc_config, "Calculator configuration") - if os.path.exists(calc_config): - all_checks_passed &= check_json_valid(calc_config, "Calculator configuration") - print() - - # Check calculator script - print("Checking calculator script...") - calc_script = ".fz/calculators/Modelica.sh" - all_checks_passed &= check_file_exists(calc_script, "Calculator script") - if os.path.exists(calc_script): - all_checks_passed &= check_executable(calc_script, "Calculator script") - print() - - # Check example notebooks - print("Checking example notebooks...") - notebooks = [ - "examples/01_NewtonCooling_Parametric.ipynb", - "examples/02_ProjectileMotion_Parametric.ipynb", - "examples/03_ProjectileMotion_Advanced.ipynb" - ] - for nb in notebooks: - all_checks_passed &= check_file_exists(nb, f"Notebook {os.path.basename(nb)}") - print() - - # Check documentation - print("Checking documentation...") - docs = [ - "README.md", - "MIGRATION.md", - "INSTALL_OPENMODELICA.md", - "CONTRIBUTING.md", - "LICENSE" - ] - for doc in docs: - all_checks_passed &= check_file_exists(doc, f"Documentation {doc}") - print() - - # Verify model configuration content - print("Verifying model configuration content...") - try: - with open(model_file, 'r') as f: - model_config = json.load(f) - - required_keys = ['id', 'varprefix', 'delim', 'output'] - for key in required_keys: - if key in model_config: - print(f"✓ Model config has '{key}': {model_config[key] if key != 'output' else '...'}") - else: - print(f"✗ Model config missing '{key}'") - all_checks_passed = False - except Exception as e: - print(f"✗ Error reading model config: {e}") - all_checks_passed = False - print() - - # Final result - print("=" * 60) - if all_checks_passed: - print("✓ ALL CHECKS PASSED") - print("=" * 60) - print() - print("Plugin is properly configured!") - print("Next steps:") - print(" 1. Install OpenModelica (see INSTALL_OPENMODELICA.md)") - print(" 2. Install dependencies: pip install funz-fz pandas jupyter matplotlib scipy") - print(" 3. Run notebooks: jupyter notebook examples/") - return 0 - else: - print("✗ SOME CHECKS FAILED") - print("=" * 60) - print() - print("Please fix the issues above before using the plugin.") - return 1 - - -if __name__ == "__main__": - sys.exit(main())