Metadata-Version: 2.4
Name: libzrod
Version: 0.1.29
Summary: Sucker Rod Design Library
Keywords: zrod,srod,qrod,rodstar,rod pump,rod design,rod lift,sucker rod,gibbs,everitt jennings,petroleum,oil well,pumpjack,wave equation
Author-Email: Walter Phillips <dev@zrod.io>
License-Expression: LicenseRef-Commercial
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: C
Classifier: Programming Language :: C++
Classifier: Programming Language :: Python
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering :: Mathematics
Classifier: Topic :: Scientific/Engineering :: Physics
Classifier: Topic :: Scientific/Engineering :: Information Analysis
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: MacOS
Project-URL: Homepage, https://zrod.io
Project-URL: Terms of Service, https://zrod.io/terms
Project-URL: Privacy Policy, https://zrod.io/privacy
Requires-Python: >=3.9
Requires-Dist: numpy
Description-Content-Type: text/markdown

# libzrod

### Sucker Rod Design Library (Wave Equation / Gibbs / Everitt-Jennings)

A wave equation library for sucker rod pump design and analysis, written in C/C++ and wrapped in Python.

## Overview

libzrod solves the 1D damped wave equation to calculate dynamometer cards for both **diagnostic** (measured card in, pump card out) and **predictive** (parameters in, surface + pump cards out) analysis of sucker rod pumping systems. The core engine is cross-platform C++, exposed to Python via ctypes.

**Key capabilities:**
- Diagnostic analysis: transform a measured surface dynamometer card into a downhole pump card
- Predictive analysis: generate surface and pump cards from well parameters (rod string, pump, fluid, geometry)
- Rod loading analysis: max/min stress per rod section with Modified Goodman limits
- Pump velocity and chamber pressure visualization
- Multi-format file I/O (.dyn, .rsdx/.rsvx, .inp6/.inp6e, .qrd, .zrod, .DAT)
- Cloud design management (save, fetch, share via AWS)

## Installation

```bash
pip install libzrod
```

**Requirements:** Python 3.9 - 3.13, numpy. Supported platforms: Windows (x86_64), macOS (x86_64/ARM), Linux (x86_64).

## Authorization

An account is required to use the library:

1. Create an account at [zrod.io](https://zrod.io)
2. Email for manual authorization of your trial account (account creation is not monitored)
3. Note: The beta library includes time restrictions

Or see the [Colab Example](https://colab.research.google.com/drive/1kIxbrbpOUuc1pFZcnCw43Jhnt-wc3XLd?usp=sharing) for a quick demo.

## Quick Start

This self-contained example runs a full diagnostic + predictive analysis using inline card data (no external files needed). Save it as `test_libzrod.py` and run with `python test_libzrod.py`.

```python
import libzrod
from libzrod import zrod, TaperBase_t, TubingBase_t, WaveParams_t, PuApi_t

# ── Create engine and login ──────────────────────────────────────────
myzrod = zrod()
print(f"libzrod library version: v{myzrod.GetZrodVersionString()}")
print(f"libzrod package version: v{libzrod.__version__}")

if(not myzrod.Login("demo", "password1")):
    print("Login failed!")
    exit(1)
print("Login OK\n")

result = myzrod.SetUpscaledDynoPointCount(4000) #this is for the smoothed/curve-fitted measured surface card
result = myzrod.SetFourierCoeffCountPos(23) #these are for the curve fitting of a measured dyno card
result = myzrod.SetFourierCoeffCountLoad(50)

# ── Well parameters ──────────────────────────────────────────────────
waveParams = myzrod.GetWaveParams()
waveParams.WellDepth = 5900
waveParams.diagSpm = 8.0
waveParams.predSpm = 8.0
waveParams.diagDampUp = 0.2
waveParams.diagDampDn = 0.2
waveParams.predDampUp = 0.2
waveParams.predDampDn = 0.2
waveParams.diagPumpPlungerDiameter = 1.5
waveParams.predPumpPlungerDiameter = 1.5
waveParams.predFluidSG = 1.0
waveParams.predFluidLevel = 2000
waveParams.predCasingPressure = 93
waveParams.predTubingPressure = 200.2
waveParams.predFo = 4000
waveParams.usePumpingUnitForPosition = True
myzrod.SetWaveParams(waveParams)

# ── Rod string (3 tapers) ───────────────────────────────────────────
tarr = (TaperBase_t * 3)()
tarr[0] = TaperBase_t(id=b"", L=3200, D=0.875, W=2.224, E=30500000, R=492)
tarr[1] = TaperBase_t(id=b"", L=2400, D=0.750, W=1.630, E=30500000, R=492)
tarr[2] = TaperBase_t(id=b"", L= 300, D=1.500, W=6.500, E=30500000, R=492)
myzrod.xSetDiagTapers(tarr, 1.0)
myzrod.xSetPredTapers(tarr, 1.0)

# ── Tubing ───────────────────────────────────────────────────────────
tuarr = (TubingBase_t * 1)()
tuarr[0] = TubingBase_t(L=5900, innerDiameter=2.441, outerDiameter=2.875,
                         weight=6.5, W=2.904, E=30500000, R=490)
myzrod.xSetDiagTubings(tuarr)
myzrod.xSetPredTubings(tuarr)

# ── Pumping unit geometry ────────────────────────────────────────────
puapi = PuApi_t()
puapi.Type = ord("C")
puapi.Rotate = 1
puapi.A = 158.375
puapi.P = 122.499992
puapi.C = 100
puapi.I = 110
puapi.K = 164.639999
puapi.R = 43
puapi.CBE = 450.0
puapi.Torque = 456000
puapi.Structure = 21300
puapi.MaxStroke = 144.0
puapi.S = 144.0
myzrod.SetPuApi(puapi)

# ── Load a measured surface card (inline data) ───────────────────────
surface_x = [
    687.0, 642.2, 596.5, 550.4, 504.4, 459.0, 414.3, 370.5, 327.7, 286.1,
    246.0, 207.9, 172.2, 139.0, 108.5, 80.8, 56.0, 34.8, 17.8, 5.9,
    0.1, 0.4, 6.8, 18.5, 34.4, 53.9, 76.1, 100.9, 128.2, 157.8,
    189.8, 223.8, 259.7, 297.3, 336.5, 377.1, 419.1, 462.2, 506.4, 551.2,
    596.6, 642.4, 688.4, 734.5, 780.5, 826.4, 872.0, 917.0, 961.2, 1004.4,
    1046.5, 1087.3, 1126.5, 1164.2, 1200.0, 1233.8, 1265.4, 1294.8, 1321.6, 1345.9,
    1367.7, 1386.8, 1403.4, 1417.4, 1428.9, 1437.8, 1444.2, 1448.2, 1449.9, 1449.4,
    1446.7, 1442.0, 1435.3, 1426.7, 1416.3, 1404.2, 1390.4, 1375.1, 1358.2, 1339.7,
    1319.8, 1298.5, 1275.8, 1251.7, 1226.1, 1199.2, 1170.9, 1141.2, 1110.2, 1077.9,
    1044.1, 1009.0, 972.5, 934.7, 895.8, 855.9, 815.1, 773.5, 730.8, 687.0,
]
surface_x = [x / 10.0 for x in surface_x]  # convert to inches

surface_y = [
    10583, 10409, 10157, 9891, 9681, 9575, 9576, 9653, 9778, 9956,
    10230, 10641, 11186, 11799, 12374, 12827, 13139, 13359, 13555, 13756,
    13927, 14009, 13982, 13917, 13957, 14243, 14815, 15585, 16380, 17054,
    17575, 18039, 18592, 19305, 20096, 20749, 21024, 20791, 20107, 19183,
    18275, 17545, 17007, 16557, 16076, 15529, 15001, 14650, 14606, 14884,
    15369, 15875, 16241, 16412, 16447, 16456, 16517, 16613, 16642, 16488,
    16099, 15538, 14952, 14494, 14242, 14160, 14143, 14084, 13945, 13766,
    13615, 13522, 13446, 13295, 12996, 12547, 12030, 11557, 11196, 10923,
    10643, 10250, 9700, 9046, 8409, 7918, 7651, 7615, 7766, 8045,
    8402, 8802, 9219, 9619, 9975, 10265, 10479, 10612, 10651, 10583,
]

myzrod.LoadDyn(8.1, surface_x, surface_y)

# ── Validate before running ──────────────────────────────────────────
issues = myzrod.ValidateDesign()
if(issues):
    print("Validation issues:")
    for issue in issues:
        severity = "WARN" if(issue["severity"] == 1) else "ERROR"
        print(f"  [{severity}] {issue['code']}: {issue['message']}")
    print()

# ── Run the wave equation solver ─────────────────────────────────────
print("Running design...")
if(not myzrod.RunDesign()):
    print("RunDesign() FAILED")
    exit(1)
print("RunDesign() OK\n")

# ── Fetch results ────────────────────────────────────────────────────
results = myzrod.GetWaveResults()

print("=== Diagnostic Results ===")
print(f"  Rod weight in air:   {results.diag.Rwa:.0f} lbs")
print(f"  Rod weight in fluid: {results.diag.Rwf:.0f} lbs")
print(f"  Fo (SKr):            {results.diag.FoSKr:.0f} lbs")
print(f"  Kr:                  {results.diag.Kr:.1f} lb/in")
print(f"  N/No:                {results.diag.NNo:.3f}")
print(f"  Stroke length:       {results.diag.SL:.2f} in")
print(f"  Pump stroke (net):   {results.diag.PumpStrokeNet:.2f} in")
print(f"  Production (100%):   {results.diag.BblPerDay100:.1f} bbl/day")
print(f"  Solver time:         {results.LastExecutionTimeMs} ms")
print()

print("=== Predictive Results ===")
print(f"  Rod weight in air:   {results.pred.Rwa:.0f} lbs")
print(f"  Rod weight in fluid: {results.pred.Rwf:.0f} lbs")
print(f"  Fo (SKr):            {results.pred.FoSKr:.0f} lbs")
print(f"  Kr:                  {results.pred.Kr:.1f} lb/in")
print(f"  N/No:                {results.pred.NNo:.3f}")
print(f"  Stroke length:       {results.pred.SL:.2f} in")
print(f"  Pump stroke (net):   {results.pred.PumpStrokeNet:.2f} in")
print(f"  Production (100%):   {results.pred.BblPerDay100:.1f} bbl/day")
print()

# ── Fetch dyno card arrays ───────────────────────────────────────────
(surfX, surfY) = myzrod.GetMeasuredDyno()
(pumpX, pumpY) = myzrod.GetMeasuredPump()
(upscX, upscY) = myzrod.GetUpscaledDyno()
(upscPX, upscPY) = myzrod.GetUpscaledPump()
(predDynX, predDynY) = myzrod.GetPredDyno()
(predPmpX, predPmpY) = myzrod.GetPredPump()

print("=== Card Point Counts ===")
print(f"  Measured surface:  {len(surfX)} pts")
print(f"  Measured pump:     {len(pumpX)} pts")
print(f"  Upscaled surface:  {len(upscX)} pts")
print(f"  Upscaled pump:     {len(upscPX)} pts")
print(f"  Predicted surface: {len(predDynX)} pts")
print(f"  Predicted pump:    {len(predPmpX)} pts")
print()

# ── Rod loading ──────────────────────────────────────────────────────
(depth, lbsMax, lbsMin, pctMax, pctMin) = myzrod.xGetUpscaledRodLoading()
print("=== Rod Loading (Upscaled) ===")
for i in range(len(depth)):
    print(f"  Depth {depth[i]:7.0f} ft  |  Max {lbsMax[i]:8.0f} lbs ({pctMax[i]:5.1f}%)"
          f"  |  Min {lbsMin[i]:8.0f} lbs ({pctMin[i]:5.1f}%)")
print()

# ── Optional: plot if matplotlib is available ────────────────────────
try:
    import matplotlib.pyplot as plt

    fig, axes = plt.subplots(1, 2, figsize=(12, 5))

    # Surface cards
    axes[0].set_title("Surface Dynamometer Cards")
    axes[0].plot(surfX, surfY, ".", ms=2, color="gray", label="Measured")
    axes[0].plot(upscX, upscY, color="blue", label="Upscaled")
    axes[0].plot(predDynX, predDynY, color="red", label="Predicted")
    axes[0].set_xlabel("Position (in)")
    axes[0].set_ylabel("Load (lbs)")
    axes[0].legend()

    # Pump cards
    axes[1].set_title("Pump Cards")
    axes[1].plot(pumpX, pumpY, ".", ms=2, color="gray", label="Measured")
    axes[1].plot(upscPX, upscPY, color="green", label="Upscaled")
    axes[1].plot(predPmpX, predPmpY, color="lime", label="Predicted")
    axes[1].set_xlabel("Position (in)")
    axes[1].set_ylabel("Load (lbs)")
    axes[1].legend()

    plt.tight_layout()
    plt.savefig("test_libzrod_output.png", dpi=150)
    print("Plot saved to test_libzrod_output.png")
    plt.show()
except ImportError:
    print("matplotlib not installed — skipping plot (pip install matplotlib)")

myzrod.Logout()
print("\nDone.")
```

## API Reference

### Initialization & Auth

| Method | Description |
|--------|-------------|
| `zrod()` | Create engine instance (singleton) |
| `Login(username, password) -> bool` | Authenticate with zrod.io |
| `Logout() -> bool` | End session |
| `SetClientVersion(platform, major, minor, rev, build)` | Set client identifier |
| `GetZrodVersion() -> np.ndarray` | Get C++ library version [major, minor, rev, build] |
| `GetZrodVersionString() -> str` | Get version as "M.m.r.b" string |

### Configuration

| Method | Description |
|--------|-------------|
| `GetWaveParams() -> WaveParams_t` | Get current wave parameters |
| `SetWaveParams(params) -> bool` | Set wave parameters |
| `GetWaveSettings() -> WaveSettings_t` | Get advanced solver settings |
| `SetWaveSettings(settings) -> bool` | Set advanced solver settings |
| `xSetDiagTapers(tapers, fluidSG) -> bool` | Set diagnostic rod string (infers count) |
| `xSetPredTapers(tapers, fluidSG) -> bool` | Set predictive rod string (infers count) |
| `SetDiagTapers(tapers, count, fluidSG) -> bool` | Set diagnostic rod string (manual count) |
| `SetPredTapers(tapers, count, fluidSG) -> bool` | Set predictive rod string (manual count) |
| `xSetDiagTubings(tubings) -> bool` | Set diagnostic tubing string (infers count) |
| `xSetPredTubings(tubings) -> bool` | Set predictive tubing string (infers count) |
| `SetDiagTubings(tubings, count) -> bool` | Set diagnostic tubing string (manual count) |
| `SetPredTubings(tubings, count) -> bool` | Set predictive tubing string (manual count) |
| `xSetCasings(casings) -> bool` | Set casing string (infers count) |
| `SetCasings(casings, count) -> bool` | Set casing string (manual count) |
| `SetDiagApiTaperByNumber(num, pumpDia, pumpDepth) -> bool` | Set diagnostic taper from API rod number |
| `SetPredApiTaperByNumber(num, pumpDia, pumpDepth) -> bool` | Set predictive taper from API rod number |
| `SetPuApi(puApi) -> bool` | Set pumping unit geometry |
| `SetPuInfo(puInfo) -> bool` | Set pumping unit info (with designation) |
| `SetPuByName(name) -> bool` | Set pumping unit by database name |
| `SetFourierCoeffCountPos(count)` | Set Fourier coefficients for position curve fitting |
| `SetFourierCoeffCountLoad(count)` | Set Fourier coefficients for load curve fitting |
| `SetUpscaledDynoPointCount(count)` | Set upscaled output resolution |

### Calculation

| Method | Description |
|--------|-------------|
| `RunDesign() -> bool` | Execute wave equation solver (diagnostic + predictive) |
| `CalculateFo() -> float` | Calculate fluid load from pump/pressure parameters (lbs) |
| `CalculateKrToDepth(depth) -> float` | Calculate rod spring constant to a given depth |
| `ValidateDesign() -> list[dict]` | Validate current design, returns list of issues |

### Results — Diagnostic

| Method | Returns | Description |
|--------|---------|-------------|
| `GetMeasuredDyno()` | `(pos, load)` | Measured surface card |
| `GetMeasuredPump()` | `(pos, load)` | Calculated downhole pump card |
| `GetMeasuredPumpColors()` | `np.ndarray[int32]` | Pump valve state colors per point |
| `GetMeasuredPumpVelocity()` | `np.ndarray` | Plunger velocity (in/sec) |
| `GetMeasuredPumpVelocityNormalized()` | `np.ndarray` | Plunger velocity (0-100 scale) |
| `GetMeasuredPumpChamberPressure()` | `np.ndarray` | Chamber pressure (psi) |
| `GetMeasuredPumpChamberPressureNormalized()` | `np.ndarray` | Chamber pressure (0-100 scale) |
| `GetUpscaledDyno()` | `(pos, load)` | Curve-fitted surface card |
| `GetUpscaledPump()` | `(pos, load)` | Pump card from curve-fitted input |
| `GetUpscaledPumpColors()` | `np.ndarray[int32]` | Valve state colors |
| `GetUpscaledPumpVelocity()` | `np.ndarray` | Plunger velocity |
| `GetUpscaledPumpVelocityNormalized()` | `np.ndarray` | Plunger velocity (0-100) |
| `GetUpscaledPumpChamberPressure()` | `np.ndarray` | Chamber pressure |
| `GetUpscaledPumpChamberPressureNormalized()` | `np.ndarray` | Chamber pressure (0-100) |

### Results — Predictive

| Method | Returns | Description |
|--------|---------|-------------|
| `GetPredDyno()` | `(pos, load)` | Predicted surface card |
| `GetPredPump()` | `(pos, load)` | Predicted pump card |
| `GetPredPumpColors()` | `np.ndarray[int32]` | Valve state colors |
| `GetPredPumpVelocity()` | `np.ndarray` | Plunger velocity |
| `GetPredPumpVelocityNormalized()` | `np.ndarray` | Plunger velocity (0-100) |
| `GetPredPumpChamberPressure()` | `np.ndarray` | Chamber pressure |
| `GetPredPumpChamberPressureNormalized()` | `np.ndarray` | Chamber pressure (0-100) |

### Results — Rod Loading

| Method | Returns | Description |
|--------|---------|-------------|
| `xGetMeasuredRodLoading()` | `(depth, lbsMax, lbsMin, pctMax, pctMin)` | Measured rod loading per section |
| `xGetUpscaledRodLoading()` | `(depth, lbsMax, lbsMin, pctMax, pctMin)` | Upscaled rod loading |
| `xGetPredRodLoading()` | `(depth, lbsMax, lbsMin, pctMax, pctMin)` | Predicted rod loading |

### Results — Other

| Method | Returns | Description |
|--------|---------|-------------|
| `GetWaveResults()` | `WaveResults_t` | Numeric results (Fo, Kr, pump stroke, production, etc.) |
| `GetWaveParamsReadOnly()` | `WaveParamsReadOnly_t` | Computed parameters (point counts, DT, buoyancy) |
| `GetIntermediateCard(nodeindex, dynotype)` | `(pos, load)` | FEA node card (dynotype: 1=meas, 2=upsc, 3=pred) |
| `GetIntermediateTimeSlice(timestep, dynotype)` | `(pos, load)` | All nodes at one timestep |
| `GetPermissibleLoads(destX, destY, count, clipmin, clipmax)` | `bool` | Modified Goodman envelope |

### File I/O

| Method | Description |
|--------|-------------|
| `LoadDyn(spm, pos_array, load_array) -> bool` | Load measured card from arrays |
| `LoadDynFromFile(filepath) -> bool` | Load .dyn file |
| `LoadDynFromFileContents(text) -> bool` | Load .dyn from string |
| `LoadDynFromDAT(filepath) -> bool` | Load .DAT format (Python-only parser) |
| `ParseDesignFile(filepath) -> bool` | Parse design file (.zrod, .rsdx, .inp6, etc.) |
| `ParseDesignFileContents(contents, ftype) -> bool` | Parse from string |
| `WriteDesignFile(filepath) -> bool` | Write .zrod design file |
| `WriteDesignFileWithTemplate(filepath, templatepath) -> bool` | Write with template |

### Design Management (Cloud)

> **Note:** Cloud design management is a work in progress and subject to change.

| Method | Description |
|--------|-------------|
| `FetchDesigns(fromserver) -> int` | Fetch design list (True=server, False=cached). Returns count. |
| `GetDesigns() -> list[DesignInfo]` | Get list of DesignInfo objects |
| `GetDesign(designId) -> bool` | Load a design by ID |
| `SaveCurrentDesign(overwrite) -> bool` | Save current design to cloud |
| `SubmitDesign(email, nonblocking) -> bool` | Submit design for review |
| `DeleteDesign(designId) -> bool` | Delete a cloud design |
| `SetDesignTitle(title) -> bool` | Set design title |
| `GetDesignTitle() -> str` | Get design title |
| `SetComment(comment) -> bool` | Set design comment |
| `GetComment() -> str` | Get design comment |
| `GetDesignID() -> str` | Get current design ID (compact UUID) |

### Metadata & Debug

| Method | Description |
|--------|-------------|
| `GetPuInfo() -> PuInfo_t` | Get pumping unit info |
| `GetCasingCount() -> int` | Get casing section count |
| `GetCasings() -> list[CasingBase_t]` | Get casing sections |
| `GetDeviationSurveyCount() -> int` | Get deviation survey point count |
| `GetDeviationSurvey() -> list[DeviationSurveyPoint_t]` | Get deviation survey |
| `PrintZrodObj()` | Print internal state to console |
| `DebugZrodObj(filepath)` | Dump debug info to file (if available) |

## Data Structures

### TaperBase_t — Rod Section

| Field | Type | Unit | Description |
|-------|------|------|-------------|
| `id` | `bytes[32]` | — | Rod grade identifier (e.g. `b"D"`) |
| `L` | `float` | feet | Section length |
| `D` | `float` | inches | Rod body diameter |
| `W` | `float` | lb/ft | Weight per foot |
| `T` | `float` | psi | Minimum tensile strength |
| `E` | `float` | psi | Young's modulus (typically 30,500,000) |
| `R` | `float` | lb/ft^3 | Density (typically 490 for steel) |
| `RL` | `float` | feet | Individual rod length for UI (default 25) |
| `K` | `float` | — | Modified Goodman K constant (default 2.8) |
| `M` | `float` | — | Modified Goodman M constant (default 0.375) |

### TubingBase_t — Tubing Section

| Field | Type | Unit | Description |
|-------|------|------|-------------|
| `L` | `float` | feet | Section length |
| `innerDiameter` | `float` | inches | Tubing ID |
| `outerDiameter` | `float` | inches | Tubing OD |
| `weight` | `float` | lb/ft | Weight per foot |
| `W` | `float` | lb/ft | Rod-in-tubing weight factor |
| `T` | `float` | psi | Tensile strength |
| `E` | `float` | psi | Young's modulus |
| `R` | `float` | lb/ft^3 | Density |

### WaveParams_t — Core Parameters

Key fields (see CLAUDE.md for full list):

| Field | Type | Unit | Description |
|-------|------|------|-------------|
| `WellDepth` | `float` | feet | Total well depth |
| `diagSpm` / `predSpm` | `float` | SPM | Strokes per minute |
| `diagDampUp` / `diagDampDn` | `float` | — | Damping factors (0.0–1.0, typical 0.05) |
| `predDampUp` / `predDampDn` | `float` | — | Predictive damping factors |
| `diagFluidSG` / `predFluidSG` | `float` | — | Fluid specific gravity |
| `diagPumpPlungerDiameter` | `float` | inches | Plunger diameter |
| `diagPumpDepth` / `predPumpDepth` | `float` | feet | Pump setting depth |
| `predFo` | `float` | lbs | Target fluid load (or use `CalculateFo()`) |
| `predFillage` | `float` | % | Pump fillage (0–100) |
| `predCompression` | `float` | — | Gas compression ratio |
| `usePumpingUnitForPosition` | `bool` | — | Use PU geometry for position (vs measured) |

## Naming Conventions

**`x`-prefix methods** (e.g. `xSetDiagTapers`, `xGetMeasuredRodLoading`) are Pythonic convenience wrappers:
- `xSet*` infers the array count from `len()` and passes `ctypes.byref()` automatically
- `xGet*` allocates numpy output arrays automatically and returns them

The non-`x` versions require you to pass count and pre-allocated arrays manually.

## Workflow Patterns

### Diagnostic + Predictive Analysis

```
1. zrod() → Login()
2. GetWaveParams() → set fields → SetWaveParams()
3. xSetDiagTapers() + xSetPredTapers()
4. xSetDiagTubings() + xSetPredTubings()
5. LoadDyn() or LoadDynFromDAT() or ParseDesignFile()
6. RunDesign()
7. GetMeasuredDyno(), GetMeasuredPump(), GetPredDyno(), GetPredPump()
8. GetWaveResults() for numeric output
```

### Predictive-Only (No Measured Card)

```
1. zrod() → Login()
2. GetWaveParams() → set fields, usePumpingUnitForPosition=True → SetWaveParams()
3. SetPuApi() or SetPuByName()
4. xSetPredTapers(), xSetPredTubings()
5. CalculateFo() or set predFo manually
6. RunDesign()
7. GetPredDyno(), GetPredPump()
```

### File Round-Trip

```
1. ParseDesignFile("input.zrod")    # loads all parameters + measured card
2. RunDesign()
3. WriteDesignFile("output.zrod")   # saves current state
```

## Constants

```python
# Prediction algorithm types (for WaveSettings_t.predAlgorithmType)
# Note: Integer values are subject to change — use the PRED_ALGORITHM_* names.
PRED_ALGORITHM_STANDARD = 0       # Default
PRED_ALGORITHM_OLD = 1            # Legacy algorithm
PRED_ALGORITHM_EXPERIMENTAL = 2   # Experimental
PRED_ALGORITHM_LEAKAGE = 3       # With leakage modeling
```

## Known Limitations

The following C API functions are **not yet wrapped** in the Python interface:

| C Function | Purpose |
|------------|---------|
| `SetDeviationSurvey()` | Set wellbore deviation survey |
| `SetDeviationSurveyWithMIA()` | Set deviation survey (interleaved format) |
| `SetSaveIntermediateCards()` | Enable intermediate FEA node card output |
| `SetStatusCallback()` | Async status callbacks |
| `GetDiagTaperCount/GetPredTaperCount` | Query rod section counts |
| `GetDiagTapers/GetPredTapers` | Retrieve rod sections |
| `GetDiagTubingCount/GetPredTubingCount` | Query tubing section counts |
| `GetDiagTubings/GetPredTubings` | Retrieve tubing sections |

## Platform Support

| Platform | Architecture | Python Versions |
|----------|-------------|-----------------|
| Windows | x86_64 | 3.9 - 3.13 |
| macOS | x86_64, ARM64 | 3.9 - 3.13 |
| Linux | x86_64 | 3.9 - 3.13 |

## Mobile Apps

Beta access available for iOS and Android demo apps. Contact for access.

## Web App

The [ZROD web app](https://zrod.io) demonstrates the full feature set of the library, including interactive dynamometer card visualization, rod loading analysis, pump velocity display, multi-format file import/export, and cloud design management.

## Troubleshooting

**`RunDesign()` returns False**: Call `ValidateDesign()` to get a list of issues with codes and messages. Common causes: missing rod string, missing measured card (when `usePumpingUnitForPosition=False`), zero SPM, or invalid pump parameters.

**Singleton warning**: `zrod` is a singleton. Creating a second instance returns the same object with a warning. Use `del myzrod` before creating a new one if needed.

**Platform binary not found**: The library ships platform-specific binaries. Ensure your Python version and platform match a supported wheel.

## Contact & Licensing

For licensing inquiries, reach out via [LinkedIn](https://www.linkedin.com/in/wansco/).

## Legal

- [Terms of Service](https://zrod.io/terms)
- [Privacy Policy](https://zrod.io/privacy)

---

*Help improve this document. Contact me with feedback or ambiguities.
