Metadata-Version: 2.4
Name: YggSimLib
Version: 1.11
Summary: A library for interfacing with the kspice API for the Yggdrasil project
Author: Håkon Enerstvedt
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: networkx==3.4.2
Dynamic: author
Dynamic: description
Dynamic: description-content-type
Dynamic: license-file
Dynamic: requires-dist
Dynamic: requires-python
Dynamic: summary

# YggSimLib

YggSimLib is a Python framework for orchestrating and automating simulation workflows in the Yggdrasil Engineering Simulator. It provides a lightweight abstraction on top of the k-Spice API, including simulator initialization, sequence execution, dependency management, and simplified tag/property access.

## Features

- Simple simulator initialization through the `YggLCS` class
- Programmatic or GUI-based selection of timelines and model files
- Generic tag/property access through `get()` and `set()`
- Flexible step-based sequencing using `Step` and `Sequence`
- Parallel branches within a sequence via step-level fork/join
- Dependency-managed orchestration using `Admin`
- Parallel sequence execution support
- Inhibit logic and transition conditions
- Configurable step timeouts, including raise or jump-to-step behavior
- Standard-library `logging`-based verbose execution trace
- Direct access to the underlying k-Spice Timeline object
- Minimal abstraction layer with native k-Spice compatibility

---

## Installation

```bash
pip install YggSimLib
```

---

## Requirements

- Python 3.12+
- Yggdrasil Engineering Simulator
- k-Spice Python bindings
- networkx

---

## Quick Start

### GUI Mode

When no arguments are supplied, YggSimLib opens dialogs that let you select:

- Model directory
- Timeline
- Model file
- Parameter file
- Initial condition file

```python
from YggSimLib import YggLCS

sim = YggLCS()
```

---

### Scripted Mode

The simulator can also be initialized directly from code without any dialogs.

```python
from YggSimLib import YggLCS

sim = YggLCS(
    model=r"C:\K-Spice-Projects\Hugin A",
    tl="S24 and S38 steady state",
    mpc=[
        "S24 and S38 steady state",
        "S24 and S38 steady state",
        "S24 and S38 shut down, warm TEG"
    ]
)
```

#### Constructor Arguments

| Argument | Description |
|-----------|-------------|
| `model` | Model directory path |
| `tl` | Timeline name |
| `mpc` | List containing model, parameter, and initial condition names |
| `run` | Automatically start simulation after initialization |

Example:

```python
sim = YggLCS(
    model=r"C:\K-Spice-Projects\Hugin A",
    tl="S24 and S38 steady state",
    mpc=[
        "S24 and S38 steady state",
        "S24 and S38 steady state",
        "S24 and S38 shut down, warm TEG"
    ],
    run=True
)
```

---

## Reading and Writing Values

### Read a Property

```python
pressure = sim.get(
    "D-38PT4225",
    "MeasuredValue",
    unit="barg"
)

print(pressure)
```

### Write a Property

```python
sim.set(
    "D-38PA002A_m",
    "LocalInput",
    True
)
```

### Direct Timeline Access

The active k-Spice Timeline object is available through:

```python
timeline = sim.get_timeline()
```

or

```python
timeline = sim.timeline
```

This gives access to the full k-Spice API:

```python
value = sim.timeline.get_value(
    "ProcessModel",
    "D-38PT4225:MeasuredValue"
)

sim.timeline.set_value(
    "ProcessModel",
    "D-38PA002A_m:LocalInput",
    True
)
```

---

## Creating a Step

A step consists of:

- Actions
- Transition conditions
- Minimum and maximum time limits (`tmin` / `tmax`)
- Next step logic
- Optional timeout behavior (raise an error, or jump to a fallback step)
- Optional fork/join to spawn parallel branches

```python
step = Step({
    "number": 10,
    "actions": [
        lambda: sim.set(
            "D-38PA002A_m",
            "LocalInput",
            True
        )
    ],
    "transitions": [
        lambda: sim.get(
            "D-38PA002A_m",
            "MachineState"
        ) == 1
    ],
    "tmax": 30,
    "next": lambda: "S020"
})
```

#### Step Configuration Keys

| Key | Description | Default |
|-----|--------------|---------|
| `number` | Unique step identifier; also used to order steps within a sequence | *required* |
| `actions` | List of zero-argument callables executed once when the step starts | `[]` |
| `transitions` | List of zero-argument callables that must all return `True` for the step to complete | `[]` |
| `tmin` | Minimum time (seconds) the step must remain active before transitions are honored | `0` |
| `tmax` | Maximum time (seconds) the step may remain active before timing out | infinite |
| `next` | Step target (or callable returning one) to move to once transitions succeed; `None` ends the branch | `None` |
| `timeout_action` | `"raise"` to raise an exception on timeout, or `"goto"` to jump to `timeout_next` | `"raise"` |
| `timeout_next` | Step target (or callable) to jump to on timeout, when `timeout_action="goto"` | `None` |
| `fork` | List of step targets (or callables) to spawn as parallel branches once this step completes. Requires `join` | `[]` |
| `join` | Step target (or callable) where forked branches rejoin once all of them complete | `None` |

##### Timeout example (jump to a fallback step instead of raising)

```python
step = Step({
    "number": 15,
    "transitions": [
        lambda: sim.get("D-38PT4225", "MeasuredValue") > 10
    ],
    "tmax": 60,
    "timeout_action": "goto",
    "timeout_next": lambda: "S015_FALLBACK",
    "next": lambda: "S020"
})
```

---

## Creating a Sequence

```python
steps = {
    "S010": step1,
    "S020": step2
}

sequence = Sequence(
    "Pump Startup",
    steps,
    sim
)

sequence.add_steps(steps.values())

sequence.start(verbose=True)
```

### Sequence Features

- Ordered execution
- Conditional transitions
- Timeouts (raise or jump to a fallback step)
- Parallel branches within a sequence via fork/join
- Inhibit conditions
- Verbose execution logging via the standard `logging` module

---

## Fork and Join (Parallel Branches within a Sequence)

A step can fork into several branches that run concurrently and rejoin at
a shared join step once all of them complete. Only one fork can be
pending at a time per sequence — nested or overlapping forks are not
supported.

```python
S010 = Step({
    "number": 10,
    "actions": [],
    "transitions": [lambda: True],
    "tmax": 5,
    "fork": ["S020A", "S020B"],
    "join": "S030"
})

S020A = Step({
    "number": 20,
    "actions": [lambda: sim.set("Pump1", "Start", True)],
    "transitions": [lambda: sim.get("Pump1", "Running") == 1],
    "tmax": 30,
    "next": None
})

S020B = Step({
    "number": 21,
    "actions": [lambda: sim.set("Pump2", "Start", True)],
    "transitions": [lambda: sim.get("Pump2", "Running") == 1],
    "tmax": 30,
    "next": None
})

S030 = Step({
    "number": 30,
    "actions": [],
    "transitions": [lambda: True],
    "tmax": 5,
    "next": None
})

steps = {
    "S010": S010,
    "S020A": S020A,
    "S020B": S020B,
    "S030": S030
}

seq = Sequence("Parallel Pump Startup", steps, sim)
seq.add_steps(steps.values())
seq.start()
```

---

## Example: Multi-Step Sequence

```python
S010 = Step({
    "number": 10,
    "actions": [
        lambda: sim.set(
            "D-38PA002A_m",
            "LocalInput",
            True
        )
    ],
    "transitions": [
        lambda: sim.get(
            "D-38PA002A_m",
            "MachineState"
        ) == 1
    ],
    "tmax": 30,
    "next": lambda: "S020"
})

S020 = Step({
    "number": 20,
    "actions": [],
    "transitions": [
        lambda: sim.get(
            "D-38PT4225",
            "MeasuredValue"
        ) > 10
    ],
    "tmax": 60,
    "next": None
})

steps = {
    "S010": S010,
    "S020": S020
}

seq = Sequence(
    "Pump Startup",
    steps,
    sim
)

seq.add_steps(steps.values())
seq.start()
```

---

## Parallel Sequence Execution

Multiple sequences can be coordinated through the `Admin` class.

```python
admin = Admin(
    "Startup Controller",
    [seq1, seq2, seq3],
    edges,
    sim
)

admin.start()
```

### Dependency Graph

Dependencies are defined as directed edges:

```python
edges = [
    ("START", "WaterWash"),
    ("WaterWash", "TEGStartup"),
    ("TEGStartup", "END")
]
```

Sequences whose dependencies are satisfied can execute in parallel.

---

## Working with Simulator Time

Sequences evaluate transitions against simulator time.

For fully automated execution it may be useful to advance timeline time in a separate thread:

```python
import threading
import time

stop_event = threading.Event()

def advance_time():
    while not stop_event.is_set():
        sim.timeline.run_steps(1)
        time.sleep(0.1)

clock = threading.Thread(target=advance_time)
clock.start()

try:
    sequence.start(verbose=True)
finally:
    stop_event.set()
    clock.join()
```

---

## Logging / Verbose Mode

`Sequence.start()` and `Sequence.process_branch_once()` accept a
`verbose` argument that controls trace output through Python's standard
`logging` module (logger name `"YggSimLib"`):

- `verbose=False` (default): sequence start/finish banners, inhibit
  warnings, and timeout fallback notices are still shown (`INFO` level
  and above).
- `verbose=True`: adds a step-by-step `DEBUG`-level trace of action and
  transition execution.

```python
sequence.start(verbose=True)
```

To customize formatting or redirect output (e.g. to a file), configure
the `"YggSimLib"` logger yourself before calling `start()`:

```python
import logging

logging.getLogger("YggSimLib").addHandler(logging.FileHandler("run.log"))
```

---

## Main Classes

### YggLCS

Simulator wrapper responsible for:

- Project loading
- Timeline activation
- Model loading
- Property access

Methods:

```python
get_timeline()
get(tag, prop, unit=None)
set(tag, prop, value, unit=None)
run()
pause()
close_project()
```

### Step

Represents an individual sequence step: its actions, transition
conditions, `tmin`/`tmax` timing window, timeout behavior
(`timeout_action`/`timeout_next`), and optional fork/join targets for
spawning parallel branches. See [Creating a Step](#creating-a-step) for
the full set of configuration keys.

### Sequence

Executes a collection of steps as a cooperative state machine, including
ordered execution, inhibit conditions, timeouts, and fork/join
parallel branches. See [Logging / Verbose Mode](#logging--verbose-mode)
for trace output options.

### Admin

Coordinates multiple sequences using a dependency graph, running
independent sequences in parallel threads as their dependencies are
satisfied.

---

## Design Philosophy

YggSimLib intentionally stays close to the underlying k-Spice API.

The library focuses on:

- Simplified simulator initialization
- Readable sequence definitions
- Reusable startup and shutdown procedures
- Dependency management
- Minimal abstraction overhead


---

## Author

Built by Håkon Enerstvedt.

YggSimLib is designed to simplify automation, testing, startup procedures, and workflow orchestration within the Yggdrasil Engineering Simulator ecosystem.
