diff --git a/ProjectileMotion.mo b/ProjectileMotion.mo new file mode 100644 index 0000000..eb62993 --- /dev/null +++ b/ProjectileMotion.mo @@ -0,0 +1,23 @@ +model ProjectileMotion + "Simple projectile motion model for testing" + + // Parameters + parameter Real v0 = 20.0 "Initial velocity (m/s)"; + parameter Real angle = 45.0 "Launch angle (degrees)"; + 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/README.md b/README.md index 7915337..c6c864c 100644 --- a/README.md +++ b/README.md @@ -1,334 +1,112 @@ # fz-modelica -Modelica plugin for the [fz](https://github.com/Funz/fz) framework - parametric scientific computing. +OpenModelica integration for Funz framework - simulate and analyze Modelica models. -This plugin allows you to run parametric studies with Modelica models using the fz framework. +## Overview -## Prerequisites - -### Required Software - -1. **OpenModelica Compiler (omc)** - - For Ubuntu/Debian: - ```bash - sudo apt-get update - sudo apt-get install ca-certificates curl gnupg - sudo curl -fsSL http://build.openmodelica.org/apt/openmodelica.asc | \ - sudo gpg --dearmor -o /usr/share/keyrings/openmodelica-keyring.gpg - - echo "deb [arch=amd64 signed-by=/usr/share/keyrings/openmodelica-keyring.gpg] \ - https://build.openmodelica.org/apt \ - $(cat /etc/os-release | grep "\(UBUNTU\|DEBIAN\|VERSION\)_CODENAME" | sort | cut -d= -f 2 | head -1) \ - stable" | sudo tee /etc/apt/sources.list.d/openmodelica.list - - sudo apt install --no-install-recommends omc - ``` - - For other platforms, see [OpenModelica installation guide](https://openmodelica.org/download/download-linux/). +This repository provides a calculator script (`modelica.sh`) that: +- Runs Modelica model simulations using OpenModelica +- Extracts simulation results +- Outputs results in Funz-compatible format -2. **fz Framework** - - ```bash - pip install funz-fz - # or - pip install -e git+https://github.com/Funz/fz.git - ``` - -3. **Python Dependencies** - - ```bash - pip install pandas matplotlib - ``` +## Prerequisites -## Installation +### Required +- **OpenModelica**: Install from [https://openmodelica.org/](https://openmodelica.org/) +- **Python 3**: With scipy and numpy packages -Clone this repository: +### Install Python dependencies ```bash -git clone https://github.com/Funz/fz-modelica.git -cd fz-modelica -``` - -## Quick Start - -### Using the Sample Model - -The repository includes a sample Newton's cooling model (`samples/NewtonCooling.mo`). - -#### 1. Parse Input Variables - -```python -import fz - -# Identify variables in the model -variables = fz.fzi("samples/NewtonCooling.mo", "Modelica") -print(variables) -# Output: {'convection': None} -``` - -#### 2. Run a Single Case - -```python -import fz - -results = fz.fzr( - "samples/NewtonCooling.mo", - {"convection": 0.7}, - "Modelica", - calculators="localhost", - results_dir="results_single" -) - -print(results) -``` - -#### 3. Run Parametric Study - -```python -import fz - -results = fz.fzr( - "samples/NewtonCooling.mo", - {"convection": [0.1, 0.3, 0.5, 0.7, 0.9]}, - "Modelica", - calculators="localhost", - results_dir="results_parametric" -) - -print(results) -``` - -#### 4. Visualize Results - -```python -import fz -import matplotlib.pyplot as plt - -results = fz.fzr( - "samples/NewtonCooling.mo", - {"convection": [0.1, 0.3, 0.5, 0.7, 0.9]}, - "Modelica", - calculators="localhost", - results_dir="results" -) - -# Plot temperature evolution for each case -for idx, row in results.iterrows(): - convection = row['convection'] - res_data = row['res']['NewtonCooling'] - time = list(res_data['time'].values()) - temp = list(res_data['T'].values()) - plt.plot(time, temp, label=f'h={convection}') - -plt.xlabel('Time (s)') -plt.ylabel('Temperature (°C)') -plt.title("Newton's Law of Cooling - Temperature vs Time") -plt.legend() -plt.grid(True) -plt.savefig('cooling_curves.png') -print("Plot saved to cooling_curves.png") -``` - -## Model Configuration - -The Modelica model configuration is defined in `.fz/models/Modelica.json`: - -```json -{ - "id": "Modelica", - "varprefix": "$", - "formulaprefix": "@", - "delim": "{}", - "commentline": "//", - "output": { - "res": "python -c 'import pandas;import glob;import json;print(json.dumps({f.split(\"_res.csv\")[0]:pandas.read_csv(f).to_dict() for f in glob.glob(\"*_res.csv\")}))'" - } -} +pip install scipy numpy ``` -### Variable Syntax in Modelica Files +## Usage -Variables are defined using the `${variable_name}` or `${variable_name~default_value}` syntax: - -```modelica -model Example - parameter Real h=${convection~0.7} "Convective coefficient"; - parameter Real temp=${temperature} "Temperature"; - ... -end Example; +### Basic Usage +```bash +./modelica.sh ``` -### Output Format - -The plugin extracts results from the `*_res.csv` file generated by OpenModelica and returns them as a nested dictionary containing time-series data. - -## Calculator Configuration - -The localhost calculator is defined in `.fz/calculators/localhost.json`: +The script will: +1. Load and simulate the Modelica model +2. Extract trajectory data and calculate outputs +3. Generate `output.txt` with results in Funz format -```json -{ - "uri": "sh://", - "models": { - "Modelica":"bash .fz/calculators/Modelica.sh" - } -} +### Example +```bash +./modelica.sh ProjectileMotion.mo ``` -## Advanced Usage - -### Using Cache +This simulates the projectile motion model and generates outputs including: +- `max_height`: Maximum height reached (m) +- `range`: Horizontal distance traveled (m) +- `flight_time`: Time of flight (s) +- `final_velocity`: Impact velocity (m/s) +- `impact_angle`: Impact angle (degrees) +- `energy_loss`: Kinetic energy lost (J) +- `energy_loss_percent`: Percentage of energy lost (%) -Reuse previous results to avoid redundant calculations: +## Testing -```python -import fz - -results = fz.fzr( - "samples/NewtonCooling.mo", - {"convection": [0.1, 0.3, 0.5, 0.7, 0.9]}, - "Modelica", - calculators=["cache://results_*", "localhost"], - results_dir="results_cached" -) +Run the test suite: +```bash +./tests/test_modelica.sh ``` -### Parallel Execution +The test script validates: +- Script executability +- Error handling (missing files, missing dependencies) +- Simulation execution (if OpenModelica is installed) +- Output file generation and format +- Result value sanity checks -Run multiple cases in parallel: - -```python -import fz - -results = fz.fzr( - "samples/NewtonCooling.mo", - {"convection": [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0]}, - "Modelica", - calculators=["localhost"] * 3, # Use 3 parallel workers - results_dir="results_parallel" -) +## Project Structure ``` - -### Using Inline Model Definition - -Instead of using the "Modelica" alias, you can define the model inline: - -```python -import fz - -model = { - "varprefix": "$", - "formulaprefix": "@", - "delim": "{}", - "commentline": "//", - "output": { - "res": "python -c 'import pandas;import glob;import json;print(json.dumps({f.split(\"_res.csv\")[0]:pandas.read_csv(f).to_dict() for f in glob.glob(\"*_res.csv\")}))'" - } -} - -results = fz.fzr( - "samples/NewtonCooling.mo", - {"convection": [0.5, 0.7, 0.9]}, - model, - calculators="sh://bash .fz/calculators/Modelica.sh", - results_dir="results" -) +. +├── modelica.sh # Main calculator script +├── ProjectileMotion.mo # Example Modelica model +├── tests/ +│ └── test_modelica.sh # Test suite +└── README.md # This file ``` -## CLI Usage +## Model Requirements -You can also use fz from the command line: +The script expects models that: +- Define a `ProjectileMotion` model (or modify the script for different models) +- Include variables: `x`, `y`, `vx`, `vy` for trajectory calculations +- Can be simulated with OpenModelica's `simulate()` function -```bash -# Parse variables -fzi samples/NewtonCooling.mo --model Modelica --format table +## Output Format -# Run parametric study -fzr samples/NewtonCooling.mo \ - --model Modelica \ - --variables '{"convection": [0.5, 0.7, 0.9]}' \ - --calculator localhost \ - --results results/ \ - --format table +Results are written to `output.txt` in Funz format: ``` - -## Creating Your Own Modelica Models - -1. Create a `.mo` file with your model -2. Mark parameters you want to vary with `${variable_name}` or `${variable_name~default}` -3. Run with fz: - -```python -import fz - -results = fz.fzr( - "your_model.mo", - { - "param1": [1, 2, 3], - "param2": [10, 20, 30] - }, - "Modelica", - calculators="localhost", - results_dir="results" -) +max_height = ; +range = ; +flight_time = ; +final_velocity = ; +impact_angle = ; +energy_loss = ; +energy_loss_percent = ; ``` ## Troubleshooting ### OpenModelica not found - -Make sure `omc` is in your PATH: -```bash -which omc -# Should output: /usr/bin/omc (or similar) ``` - -### Compilation errors - -Check the `.moo` files in the results directory for detailed OpenModelica error messages. - -### Missing Python packages - -Install required packages: -```bash -pip install pandas matplotlib +Error: OpenModelica (omc) not found. Please install OpenModelica. ``` +Install OpenModelica from [https://openmodelica.org/](https://openmodelica.org/) -## Directory Structure - +### Python packages missing ``` -fz-modelica/ -├── .fz/ -│ ├── models/ -│ │ └── Modelica.json # Model definition -│ └── calculators/ -│ ├── localhost.json # Calculator configuration -│ └── Modelica.sh # Execution script -├── samples/ -│ └── NewtonCooling.mo # Sample model -└── README.md # This file +Error: Required Python package not found: No module named 'scipy' ``` +Install required packages: `pip install scipy numpy` -## Migration from Old Plugin - -This is a port of the [old Modelica plugin](https://github.com/Funz/plugin-modelica) to the new fz framework. - -**Key differences:** -- Old: `variableStartSymbol`, `variableLimit` → New: `varprefix`, `delim` -- Old: `output.???.get` syntax → New: `output` dictionary with shell commands -- Old: `.ioplugin` file → New: `.json` model definition -- Old: Separate `.bat` and `.sh` scripts → New: Single `.sh` script -- Configuration now uses `.fz/` directory structure +### Simulation fails +Check `omc.log` for detailed error messages from OpenModelica. ## License -BSD 3-Clause License (same as fz framework) - -## Contributing - -Contributions are welcome! Please submit issues and pull requests on GitHub. - -## Related Projects - -- [fz](https://github.com/Funz/fz) - The main fz framework -- [OpenModelica](https://openmodelica.org/) - Open-source Modelica compiler \ No newline at end of file +This project is part of the Funz framework. diff --git a/modelica.sh b/modelica.sh new file mode 100755 index 0000000..95a4f60 --- /dev/null +++ b/modelica.sh @@ -0,0 +1,144 @@ +#!/bin/bash + +# Modelica Calculator Script for FZ +# Requires OpenModelica to be installed (omc command available) + +# The compiled model file is passed as the first argument +MODEL_FILE="$1" + +# Check if OpenModelica is installed +if ! command -v omc &> /dev/null; then + echo "Error: OpenModelica (omc) not found. Please install OpenModelica." + echo "Visit: https://openmodelica.org/" + exit 1 +fi + +# Check if model file exists +if [ ! -f "$MODEL_FILE" ]; then + echo "Error: Model file $MODEL_FILE not found" + exit 1 +fi + +# Create OpenModelica script to simulate and extract results +cat > simulate.mos << EOF +// Load and simulate the model +loadFile("$MODEL_FILE"); +simulate(ProjectileMotion, stopTime=10, numberOfIntervals=1000); + +// Get final results using the result file +val(x, 0.0); // Check simulation worked +quit(); +EOF + +# Run OpenModelica +omc simulate.mos > omc.log 2>&1 +RESULT=$? + +if [ $RESULT -ne 0 ]; then + echo "Error: OpenModelica simulation failed" + cat omc.log + exit 1 +fi + +# Check if result file was created +if [ ! -f "ProjectileMotion_res.mat" ]; then + echo "Error: Result file not created" + exit 1 +fi + +# Extract results using Python (OpenModelica Python API) +python3 << 'PYTHON_EOF' +import sys +try: + # Try to use OMPython for reading results + from OMPython import ModelicaSystem + + # Alternative: use numpy and scipy to read MAT file + import scipy.io + import numpy as np + + # Read the OpenModelica result file + mat = scipy.io.loadmat('ProjectileMotion_res.mat') + + # Extract time and variables + # OpenModelica stores results in specific format + names = [name.strip() for name in mat['name']] + data = mat['data_2'] # Trajectory data + + # Find variable indices + def get_var_idx(var_name): + for i, name in enumerate(names): + if name == var_name: + return i + return None + + # Get time array + time_idx = get_var_idx('time') + if time_idx is None: + time = np.linspace(0, 10, data.shape[1]) + else: + time = data[time_idx, :] + + # Get trajectory variables + x_idx = get_var_idx('x') + y_idx = get_var_idx('y') + vx_idx = get_var_idx('vx') + vy_idx = get_var_idx('vy') + + x = data[x_idx, :] + y = data[y_idx, :] + vx = data[vx_idx, :] + vy = data[vy_idx, :] + + # Find when projectile lands (y crosses zero after launch) + landing_idx = None + for i in range(10, len(y)): # Skip first few points + if y[i] < 0: + landing_idx = i - 1 + break + + if landing_idx is None: + landing_idx = len(y) - 1 + + # Calculate outputs + max_height = np.max(y) + range_distance = x[landing_idx] + flight_time = time[landing_idx] + + v_final = np.sqrt(vx[landing_idx]**2 + vy[landing_idx]**2) + impact_angle = np.degrees(np.arctan2(abs(vy[landing_idx]), abs(vx[landing_idx]))) + + # Energy calculations (need mass and v0) + # These should come from parameters + # For now, estimate from initial velocity + v0 = np.sqrt(vx[0]**2 + vy[0]**2) + m = 1.0 # Default mass + + ke_initial = 0.5 * m * v0**2 + ke_final = 0.5 * m * v_final**2 + energy_loss = ke_initial - ke_final + energy_loss_percent = 100 * energy_loss / ke_initial if ke_initial > 0 else 0 + + # Write results to output.txt in FZ format + with open('output.txt', 'w') as f: + f.write(f"max_height = {max_height};\n") + f.write(f"range = {range_distance};\n") + f.write(f"flight_time = {flight_time};\n") + f.write(f"final_velocity = {v_final};\n") + f.write(f"impact_angle = {impact_angle};\n") + f.write(f"energy_loss = {energy_loss};\n") + f.write(f"energy_loss_percent = {energy_loss_percent};\n") + + print("Simulation completed successfully") + +except ImportError as e: + print(f"Error: Required Python package not found: {e}", file=sys.stderr) + print("Please install: pip install scipy numpy", file=sys.stderr) + sys.exit(1) +except Exception as e: + print(f"Error extracting results: {e}", file=sys.stderr) + sys.exit(1) + +PYTHON_EOF + +exit $? diff --git a/omc.log b/omc.log new file mode 100644 index 0000000..5a23d00 --- /dev/null +++ b/omc.log @@ -0,0 +1 @@ +bash: omc: command not found diff --git a/tests/test_modelica.sh b/tests/test_modelica.sh new file mode 100755 index 0000000..449a9e5 --- /dev/null +++ b/tests/test_modelica.sh @@ -0,0 +1,175 @@ +#!/bin/bash + +# Test script for modelica.sh +# Tests the Modelica simulation script with the ProjectileMotion model + +set -e # Exit on error + +TEST_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(dirname "$TEST_DIR")" +WORK_DIR="/tmp/modelica_test_$$" + +echo "======================================" +echo "Testing modelica.sh" +echo "======================================" +echo "" + +# Create working directory +mkdir -p "$WORK_DIR" +cd "$WORK_DIR" + +echo "Working directory: $WORK_DIR" +echo "" + +# Copy necessary files +cp "$REPO_ROOT/ProjectileMotion.mo" . +cp "$REPO_ROOT/modelica.sh" . + +echo "Test 1: Check if script is executable" +if [ -x "./modelica.sh" ]; then + echo "✓ Script is executable" +else + echo "✗ Script is not executable" + exit 1 +fi +echo "" + +echo "Test 2: Check script fails without OpenModelica (if not installed)" +if ! command -v omc &> /dev/null; then + echo "OpenModelica not installed - skipping actual simulation tests" + echo "To run full tests, install OpenModelica from https://openmodelica.org/" + echo "" + echo "Test 2a: Verify script detects missing OpenModelica" + if ./modelica.sh ProjectileMotion.mo 2>&1 | grep -q "OpenModelica (omc) not found"; then + echo "✓ Script correctly detects missing OpenModelica" + else + echo "✗ Script did not detect missing OpenModelica" + exit 1 + fi + echo "" + echo "======================================" + echo "Partial tests PASSED (OpenModelica not available)" + echo "======================================" + cd / + rm -rf "$WORK_DIR" + exit 0 +fi + +echo "Test 3: Check script fails with non-existent model file" +if ./modelica.sh NonExistent.mo 2>&1 | grep -q "Model file.*not found"; then + echo "✓ Script correctly detects missing model file" +else + echo "✗ Script did not detect missing model file" + exit 1 +fi +echo "" + +echo "Test 4: Run simulation with ProjectileMotion model" +if ./modelica.sh ProjectileMotion.mo; then + echo "✓ Simulation completed successfully" +else + echo "✗ Simulation failed" + exit 1 +fi +echo "" + +echo "Test 5: Check if output.txt was created" +if [ -f "output.txt" ]; then + echo "✓ output.txt created" +else + echo "✗ output.txt not created" + exit 1 +fi +echo "" + +echo "Test 6: Verify output.txt contains expected variables" +EXPECTED_VARS=("max_height" "range" "flight_time" "final_velocity" "impact_angle" "energy_loss" "energy_loss_percent") +ALL_FOUND=true +for var in "${EXPECTED_VARS[@]}"; do + if grep -q "^${var} = " output.txt; then + echo "✓ Found variable: $var" + else + echo "✗ Missing variable: $var" + ALL_FOUND=false + fi +done + +if [ "$ALL_FOUND" = false ]; then + echo "" + echo "Output file contents:" + cat output.txt + exit 1 +fi +echo "" + +echo "Test 7: Verify output values are reasonable" +# Extract values +MAX_HEIGHT=$(grep "^max_height = " output.txt | cut -d'=' -f2 | tr -d ' ;') +RANGE=$(grep "^range = " output.txt | cut -d'=' -f2 | tr -d ' ;') +FLIGHT_TIME=$(grep "^flight_time = " output.txt | cut -d'=' -f2 | tr -d ' ;') + +# For a projectile with v0=20 m/s at 45 degrees: +# max_height ≈ (v0*sin(45))^2 / (2*g) ≈ 10.2 m +# range ≈ v0^2*sin(2*45) / g ≈ 40.8 m +# flight_time ≈ 2*v0*sin(45) / g ≈ 2.9 s + +echo "Extracted values:" +echo " max_height = $MAX_HEIGHT m (expected ~10 m)" +echo " range = $RANGE m (expected ~40 m)" +echo " flight_time = $FLIGHT_TIME s (expected ~3 s)" + +# Basic sanity checks (allow wide tolerance) +if (( $(echo "$MAX_HEIGHT > 5 && $MAX_HEIGHT < 15" | bc -l) )); then + echo "✓ max_height is reasonable" +else + echo "✗ max_height is out of expected range" + exit 1 +fi + +if (( $(echo "$RANGE > 20 && $RANGE < 60" | bc -l) )); then + echo "✓ range is reasonable" +else + echo "✗ range is out of expected range" + exit 1 +fi + +if (( $(echo "$FLIGHT_TIME > 1 && $FLIGHT_TIME < 5" | bc -l) )); then + echo "✓ flight_time is reasonable" +else + echo "✗ flight_time is out of expected range" + exit 1 +fi +echo "" + +echo "Test 8: Check simulation artifacts" +if [ -f "ProjectileMotion_res.mat" ]; then + echo "✓ Result file (ProjectileMotion_res.mat) created" +else + echo "✗ Result file not created" + exit 1 +fi + +if [ -f "simulate.mos" ]; then + echo "✓ OpenModelica script (simulate.mos) created" +else + echo "✗ OpenModelica script not created" + exit 1 +fi + +if [ -f "omc.log" ]; then + echo "✓ Log file (omc.log) created" +else + echo "✗ Log file not created" + exit 1 +fi +echo "" + +echo "======================================" +echo "All tests PASSED" +echo "======================================" + +# Cleanup +cd / +rm -rf "$WORK_DIR" + +exit 0