# SpaceGass API > Complete documentation for Large Language Models --- ## Document: API Versioning How API versioning works in the SpaceGass API URL: /docs/versioning # API Versioning The SpaceGass API uses URL segment versioning. The version number is part of the URL path. ## URL Format ``` http://your-host/api/v{version}/{resource} ``` All current endpoints use **v1**: ``` http://localhost:34560/api/v1/job http://localhost:34560/api/v1/job/structure/nodes http://localhost:34560/api/v1/job/structure/members ``` ## SDK Clients The generated SDK clients have the version baked into the base URL, so you don't need to manage it: ```csharp title="C#" // The base URL includes /api/v1 — just navigate from the client var nodes = await client.Job.Structure.Nodes.GetAsync(); ``` ```python title="Python" # Same in Python — the base URL handles versioning nodes = await client.job.structure.nodes.get() ``` ## Package Versioning SDK and package versions mirror the SPACE GASS Desktop version to eliminate ambiguity: | SPACE GASS Desktop | NuGet Package | PyPI Package | |---|---|---| | v15.0.2 | `SpaceGassApi 15.0.2` | `space-gass-api==15.0.2` | | v15.1.0 | `SpaceGassApi 15.1.0` | `space-gass-api==15.1.0` | --- ## Document: Using the SDK A plain-English reference for the REST verbs and async patterns used by the SPACE GASS SDK URL: /docs/using-the-sdk # Using the SDK > **Want to generate your own client?** These SDKs are generated from the > OpenAPI spec at > **https://api.spacegass.com/docs/api/1/schema.json** — feed that file > to [Kiota](https://learn.microsoft.com/en-us/openapi/kiota/overview), > OpenAPI Generator, or an LLM to produce a typed client in any language. If you are new to web APIs, the SDK calls and HTTP examples in this documentation can feel jargon-heavy. This page is a quick reference for the terms you will see repeatedly — what they actually mean, and how they map to engineering terms you already know. ## The REST verbs The API uses standard HTTP verbs. Each one has a consistent meaning: | Verb | What it means in plain English | SDK call ends in… | | -------- | -------------------------------------------------------------------- | ----------------- | | `GET` | **Read** something — fetch a value without changing anything | `GetAsync()` | | `POST` | **Create** a new thing — add a node, member, load case, etc. | `PostAsync(...)` | | `PATCH` | **Update** an existing thing in place — change one or two properties | `PatchAsync(...)` | | `PUT` | **Replace** an existing thing wholesale — replace its entire state | `PutAsync(...)` | | `DELETE` | **Remove** an existing thing | `DeleteAsync()` | So `client.Job.Structure.Nodes.PostAsync(...)` reads as "post (create) a new node", and `client.Job.Structure.Nodes[5].DeleteAsync()` reads as "delete the node with Id 5". ## The `Async` suffix and `await` Every SDK call ends in `Async` (in C#) or is awaited (in Python): ```csharp title="C#" var node = await client.Job.Structure.Nodes.PostAsync(new NodeCreate { ... }); ``` ```python title="Python" node = await client.job.structure.nodes.post(NodeCreate(...)) ``` The `Async` / `await` machinery is there because the API is reached over the network. Your code does not freeze the program while it waits for a response — it lets the rest of your application keep working until the result comes back. For day-to-day use you can read `await foo` as "do `foo` and give me the result"; the rest is plumbing. In C#, the `Async` suffix is a naming convention — every method that returns a `Task` is suffixed `Async` so it is obvious from the method name that you must `await` it. ## Request and response bodies Most `POST`, `PATCH`, and `PUT` calls take a **body** — the data you are sending up. The SDK exposes this as a strongly-typed object: - `NodeCreate` — the shape of the body when creating a node - `MemberDistributedLoadCreate` — the body when creating a distributed load - `SectionUpdate` — the body when patching a section's properties A `*Create` model has all the fields you can set on creation; a `*Update` model is the same shape but every field is optional, so you only need to set the ones you want to change. ## Path parameters and query parameters When you see `/job/structure/nodes/{id}`, the `{id}` is a **path parameter** — replaced with the actual value you want, e.g. `5`. The SDK exposes this as a typed indexer: ```csharp title="C#" await client.Job.Structure.Nodes[5].GetAsync(); ``` ```python title="Python" await client.job.structure.nodes.by_id(5).get() ``` Anything after a `?` in a URL is a **query parameter** — used for filtering, pagination, and similar options. The SDK exposes them as typed properties on a configuration object: ```csharp title="C#" await client.Job.Query.Analysis.Static.NodeReactions.GetAsync(config => { config.QueryParameters.LoadCases = "1,3-7"; config.QueryParameters.Nodes = "10-12"; }); ``` ```python title="Python" await client.job.query.analysis.static.node_reactions.get( load_cases="1,3-7", nodes="10-12") ``` ```bash title="curl" curl "http://localhost:34560/api/v1/job/query/analysis/static/node-reactions?loadCases=1,3-7&nodes=10-12" ``` See the [Filtering & Querying guide](/guides/filtering-and-querying) for the full SG list-string filter syntax. ## Status codes Every response carries an HTTP status code. The ones you will see in practice: | Code | Meaning | | ---- | ----------------------------------------------------------------------------- | | 200 | OK — your read worked | | 201 | Created — your new entity was saved | | 204 | No Content — your delete or update worked, no data to return | | 400 | Bad Request — the body you sent was invalid; check the `detail` field | | 404 | Not Found — the entity (or file) does not exist | | 409 | Conflict — the operation clashes with current state (e.g. file already open) | | 500 | Server Error — the API ran into trouble; please report it via [Support](/support) | The SDK clients turn non-2xx responses into typed exceptions (`ApiException` in C#, `APIError` in Python). See [Error Handling](/guides/error-handling) for the patterns. --- ## Document: Support Get help with the SPACE GASS API URL: /docs/support # Support Need help with the SPACE GASS API? Reach the team directly: **Email:** [support@spacegass.com](mailto:support@spacegass.com) When reporting an issue, please include: - Your SPACE GASS Desktop version (visible at the top of the SPACE GASS window, or via `GET /service/info`) - The endpoint you were calling and the request body, if applicable - The full error response (status code, `title`, `detail`, and `traceId`) - A short description of what you expected to happen --- ## Document: Quick Start Get started with the SpaceGass API in 5 minutes URL: /docs/quick-start # Quick Start This guide walks you through connecting to the API, opening a built-in sample project, and reading nodes — using the SDK clients or plain HTTP. No project file of your own is required. ### Prerequisites - [SPACE GASS](https://www.spacegass.com/) **14.5 or later** installed on your machine - SPACE GASS has been opened at least once to initialise the required data files - One of: - **C#** — [.NET 8 SDK](https://dotnet.microsoft.com/download) (any editor works; [Visual Studio Code](https://code.visualstudio.com/) + the C# Dev Kit extension is a free option), or - **Python** — [Python 3.10 or newer](https://www.python.org/downloads/) (any editor works; VS Code + the Python extension is a free option), or - **Just an HTTP client** — `curl` is built into Windows 10+; [Postman](https://www.postman.com/) and [Insomnia](https://insomnia.rest/) also work. 1. **Start the Service** The API runs as a local server. The easiest way to start it is to double-click the **SPACE GASS API** shortcut under the SPACE GASS Windows application folder. Alternatively, open a terminal and run: ```bash "C:\Program Files\SPACE GASS 14.5\SpaceGassApi.exe" ``` By default the service starts on `http://localhost:34560`. To use a different port, pass the `--port` flag: ```bash "C:\Program Files\SPACE GASS 14.5\SpaceGassApi.exe" --port=5025 ``` Leave the terminal or shortcut window open — the service runs until you close it. 1. **Explore the API** Once the service is running, open your browser and navigate to: ``` http://localhost:34560/swagger ``` The interactive Swagger UI lets you browse every endpoint, see request and response schemas, and make live API calls directly from your browser — no SDK or code required. This is the quickest way to get familiar with the API. When you are ready to integrate the API into your own code, continue with the steps below. 1. **Create a Project Folder and Install the SDK** The API includes generated SDK clients for C# and Python. These handle serialisation and give you typed methods for every endpoint. If you prefer raw HTTP, you can skip this step and use the **curl** tab in every snippet below. ```bash title="C#" # In a fresh folder: dotnet new console -n SpaceGassQuickStart cd SpaceGassQuickStart dotnet add package SpaceGassApi ``` ```bash title="Python" # In a fresh folder: python -m venv .venv .venv\Scripts\activate # macOS/Linux: source .venv/bin/activate pip install space-gass-api ``` ```bash title="curl" # No installation needed — curl is built into Windows 10+ and most # macOS/Linux installations. Postman or Insomnia work too. ``` 1. **Create the Client** The SDK provides a `CreateClient()` factory that configures everything for you — it connects to the local API with no additional setup required. See [Authentication](/authentication) for details on custom URLs and future API key support. ```csharp title="C#" using SpaceGassApi; using SpaceGassApi.Models; var client = SpaceGassApiClient.CreateClient("http://localhost:34560"); ``` ```python title="Python" from space_gass_api import SpaceGassApiClient import space_gass_api.models as models client = SpaceGassApiClient.create_client("http://localhost:34560") ``` ```bash title="curl" # No client setup needed — just make HTTP requests directly. curl http://localhost:34560/api/v1/service/info ``` 1. **Open a Sample Project** `Job.OpenSample` loads one of the built-in SPACE GASS samples as a new unsaved job — perfect for getting going without your own `.sg` file. List available samples with `GET /file/samples` (or [browse them in Swagger](http://localhost:34560/swagger#/File/get_file_samples)); here we'll open `Portal Frame.SG`, which ships with every install. When you have your own file, use `Job.Open` with `FilePath` instead. See [File Handling](/guides/file-handling) for the full set of open/save/close patterns. ```csharp title="C#" await client.Job.OpenSample.PostAsync( new OpenSampleRequest { FileName = "Portal Frame.SG" }); ``` ```python title="Python" await client.job.open_sample.post( models.OpenSampleRequest(file_name="Portal Frame.SG")) ``` ```bash title="curl" curl -X POST http://localhost:34560/api/v1/job/open-sample \ -H "Content-Type: application/json" \ -d '{"fileName": "Portal Frame.SG"}' ``` 1. **Get All Nodes** ```csharp title="C#" var nodes = await client.Job.Structure.Nodes.GetAsync(); foreach (var node in nodes) { Console.WriteLine($"Node {node.Id}: ({node.X}, {node.Y}, {node.Z})"); } ``` ```python title="Python" nodes = await client.job.structure.nodes.get() for node in nodes: print(f"Node {node.id}: ({node.x}, {node.y}, {node.z})") ``` ```bash title="curl" curl http://localhost:34560/api/v1/job/structure/nodes ``` 1. **Close the Project** ```csharp title="C#" await client.Job.Close.PostAsync(); ``` ```python title="Python" await client.job.close.post() ``` ```bash title="curl" curl -X POST http://localhost:34560/api/v1/job/close ``` ## Putting It All Together Drop this complete program into the project you created in Step 3 and run it. It opens the `Portal Frame.SG` sample, lists every node, and closes cleanly. ```csharp title="C# (Program.cs)" using SpaceGassApi; using SpaceGassApi.Models; var client = SpaceGassApiClient.CreateClient("http://localhost:34560"); try { await client.Job.OpenSample.PostAsync( new OpenSampleRequest { FileName = "Portal Frame.SG" }); var nodes = await client.Job.Structure.Nodes.GetAsync(); foreach (var node in nodes!) { Console.WriteLine($"Node {node.Id}: ({node.X}, {node.Y}, {node.Z})"); } } finally { await client.Job.Close.PostAsync(); } ``` ```python title="Python (main.py)" import asyncio from space_gass_api import SpaceGassApiClient import space_gass_api.models as models async def main(): client = SpaceGassApiClient.create_client("http://localhost:34560") try: await client.job.open_sample.post( models.OpenSampleRequest(file_name="Portal Frame.SG")) nodes = await client.job.structure.nodes.get() for node in nodes: print(f"Node {node.id}: ({node.x}, {node.y}, {node.z})") finally: await client.job.close.post() if __name__ == "__main__": asyncio.run(main()) ``` ```bash title="curl" # Open the sample, list nodes, close the job. Run from a terminal — # bash on Linux/macOS or Git Bash / WSL on Windows. curl -X POST http://localhost:34560/api/v1/job/open-sample \ -H "Content-Type: application/json" \ -d '{"fileName": "Portal Frame.SG"}' curl http://localhost:34560/api/v1/job/structure/nodes curl -X POST http://localhost:34560/api/v1/job/close ``` ## Run this example locally The same code is in the repo as [`Example.QuickStart`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/csharp/examples/Example.QuickStart) (C#) and [`quick_start`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/python/examples/quick_start) (Python). Clone the repo and run it directly — no copy-paste: ```bash title="C#" git clone https://github.com/SpaceGass/space-gass-api.git cd space-gass-api/sdks/csharp/examples/Example.QuickStart dotnet run ``` ```bash title="Python" git clone https://github.com/SpaceGass/space-gass-api.git cd space-gass-api/sdks/python/examples/quick_start pip install space-gass-api # once per environment python quick_start.py ``` Make sure the SPACE GASS API service is running first (step 1 above). ## Next Steps - [Simple Beam Model](/examples/simple-beam) — Full walkthrough building a model, running analysis, and querying results - [File Handling](/guides/file-handling) — Open, save, force-open, and file status checks - [API Reference](/api) — Browse all endpoints interactively --- ## Document: Overview Start here — programmatic access to SPACE GASS. Machine-readable OpenAPI spec: https://api.spacegass.com/docs/api/1/schema.json URL: /docs/overview # Overview The SPACE GASS API provides programmatic access to SPACE GASS structural analysis data. Use it to open job files, read and edit structural entities, run analyses, and query results. The API is a **headless service** — it runs as a local server on your machine without any program UI. There is no interaction with the SPACE GASS desktop interface. All operations are performed through HTTP requests to the local server. ## What You Can Do - **Open and manage job files** — Create, open, save, and close SPACE GASS `.sg` project files - **Read, write, and edit structural data** — Nodes, members, sections, materials, load cases, load categories, and load case groups. Create or update many entities at once using bulk operations. - **Run analyses** — Start linear, non-linear, buckling, and dynamic frequency analyses, and monitor progress in real time - **Query analysis results** — Retrieve node displacements, reactions, member forces, buckling load factors, and more ## Prerequisites To get access to the API, follow the steps in the [Quick Start](/quick-start). You will need: - [SPACE GASS](https://www.spacegass.com/) **14.5 or later** installed on your machine - SPACE GASS has been opened at least once to initialize the required data files ## Licensing API access is included with Pro and above subscriptions, or available as a yearly add-on for perpetual license holders. See [Licensing](/licensing) for details or visit [spacegass.com](https://www.spacegass.com/) for pricing. ## Authentication No authentication is required. The API runs locally and is accessed over plain HTTP. See [Authentication](/authentication) for details. ## SDK Clients Generated SDK clients are available for: - **C#** — `SpaceGassApi` (NuGet package) - **Python** — `space-gass-api` (pip installable) Both are generated from the OpenAPI specification using [Microsoft Kiota](https://learn.microsoft.com/en-us/openapi/kiota/overview). The machine-readable spec is published at [`api/1/schema.json`](https://api.spacegass.com/docs/api/1/schema.json) if you want to generate a client for another language or feed it to an LLM. ## Using with AI coding agents Building scripts with an AI assistant (Claude Code, Cursor, Copilot, ChatGPT)? Point it at these single, clean, machine-readable entry points instead of letting it crawl the source repo: - **Full docs bundle for LLMs:** [`llms-full.txt`](https://api.spacegass.com/docs/llms-full.txt) - **OpenAPI spec (JSON):** [`api/1/schema.json`](https://api.spacegass.com/docs/api/1/schema.json) - **Repo as an MCP server** (search docs, examples, and code with zero setup): [`gitmcp.io/SpaceGass/space-gass-api`](https://gitmcp.io/SpaceGass/space-gass-api) Or paste this primer straight into your AI assistant to point it at everything in one step: ```text You have access to the SPACE GASS structural-analysis API — a LOCAL, no-auth service at http://localhost:34560 (base path /api/v1). Load these first: - Full docs bundle: https://api.spacegass.com/docs/llms-full.txt - OpenAPI spec: https://api.spacegass.com/docs/api/1/schema.json - Repo via GitMCP: https://gitmcp.io/SpaceGass/space-gass-api SDKs (Kiota, fluent builder chains, async): - Python: pip install space-gass-api -> from space_gass_api import SpaceGassApiClient - C#: dotnet add package SpaceGassApi Both expose SpaceGassApiClient.CreateClient() (no auth, base URL auto-set). Use raw.githubusercontent.com for repo files, never /blob/ URLs. ``` ## Next Steps - [Quick Start](/quick-start) — Get up and running in 5 minutes - [Simple Beam Model](/examples/simple-beam) — Full walkthrough building a model, running analysis, and querying results - [API Reference](/api) — Browse all endpoints interactively --- ## Document: Licensing SPACE GASS license requirements for API access URL: /docs/licensing # Licensing The SPACE GASS API is licensed as an **add-on** to your existing SPACE GASS license. API access is bundled into **Pro and above** subscriptions, or available as a **yearly add-on** for perpetual license holders. ## How License Seats Are Consumed ### API License When the API service is started, an **API license seat** is consumed and held for the duration of that service process. The seat is released when the service is stopped. ### SPACE GASS License When you open a job through the API, a **standard SPACE GASS license seat** is consumed — the same as opening a job in the desktop application. The seat is released when you close the job. ### Module Licenses While working with an open job, additional module licenses (e.g. plates, master-slave constraints) are consumed as needed. They are released when you close the job. ## API Modes: ReadWrite and ReadOnly The API can run in two modes that control how license seats are consumed: ### ReadWrite (default) In **readwrite** mode the API holds a full SPACE GASS license seat and can open jobs, create/modify entities, run analyses, and save files — every endpoint is available. ### ReadOnly In **readonly** mode the API releases the SPACE GASS license seat, freeing it for use by the desktop application or another API instance. While in readonly mode, only read operations are available — you can query structure, loads, and results, but you cannot create, modify, or delete entities, run analyses, or save files. ### Switching Modes Use the `POST /service/access-mode` endpoint to transition between modes: ```csharp title="C#" // Switch to readonly — releases the SPACE GASS license seat await client.Service.AccessMode.PostAsync(new AccessModeUpdate { AccessMode = AccessMode.ReadOnly }); // Switch back to readwrite — re-acquires the license seat await client.Service.AccessMode.PostAsync(new AccessModeUpdate { AccessMode = AccessMode.ReadWrite }); ``` ```python title="Python" # Switch to readonly — releases the SPACE GASS license seat await client.service.access_mode.post(models.AccessModeUpdate(access_mode=models.AccessMode.ReadOnly)) # Switch back to readwrite — re-acquires the license seat await client.service.access_mode.post(models.AccessModeUpdate(access_mode=models.AccessMode.ReadWrite)) ``` ```bash title="curl" # Switch to readonly curl -X POST http://localhost:34560/api/v1/service/access-mode \ -H "Content-Type: application/json" \ -d '{"accessMode": "ReadOnly"}' # Switch back to readwrite curl -X POST http://localhost:34560/api/v1/service/access-mode \ -H "Content-Type: application/json" \ -d '{"accessMode": "ReadWrite"}' ``` If a job is open when you switch to readonly, the transition is **queued** (HTTP 202) and commits when the job closes. Check the current mode and any pending transition via `GET /service/access-mode` (the `accessMode` and `pendingAccessMode` fields). If the license seat cannot be re-acquired when switching back to readwrite (e.g. all seats are in use), the API returns `409 Conflict` and stays in readonly. ## More Information For full details on SPACE GASS licensing and pricing, visit [spacegass.com](https://www.spacegass.com/). --- ## Document: Concepts The mental model for working with the SPACE GASS API — the active job, the entity model, units, and common pitfalls URL: /docs/concepts # Concepts This page is the conceptual map of the API. It does not show code — the [Quick Start](/quick-start) and the [Examples](/examples/simple-beam) cover that. Read this once and the calls in those examples will make sense. ## The Active Job The API is **stateful** with respect to a single active job. At any moment the service is in one of two states: - **No job open** — most endpoints under `/job/...` will return `409 Conflict` until you open one. - **One job open** — every `/job/...` call acts on that job. There is no way to open two jobs simultaneously. You move between states with three endpoints: | Endpoint | When to use | |---|---| | `POST /job/new` | Start fresh, in-memory only — no file on disk yet. | | `POST /job/open` | Load an existing `.sg` file from disk into the active job. | | `POST /job/open-sample` | Load one of the built-in sample projects (`Portal Frame.SG`, etc.) — useful for getting started without a file of your own. List them at `GET /file/samples`. | | `POST /job/save` | Persist the current job (use `Save As` semantics by passing a `filePath`). | | `POST /job/close` | Close the active job and return to the no-job-open state. | The recommended pattern in your code is **`open` → work → `close`** in a `try / finally` so the job is always cleaned up, even if a step in the middle throws. The [Quick Start](/quick-start) and the [Simple Beam tutorial](/examples/simple-beam) both follow this pattern. ### Why this matters If you call `GET /job/structure/nodes` (or any other entity endpoint) without an open job, you'll see a `409 Conflict` with a `ErrorResponse` body explaining the active-job requirement. This is the most common error a new user hits. The fix is always: open a job first. ## The Entity Model A SPACE GASS job is built from a small number of entity types. Most of the API surface is CRUD over these: | Entity | Path prefix | What it represents | |---|---|---| | **Node** | `/job/structure/nodes` | A point in 3D space (x, y, z). Identified by an `Id`. | | **Member** | `/job/structure/members` | A 1D line element between two nodes. References a section + material. | | **Plate** | `/job/structure/plates` | A 2D shell element on three or four nodes. References a section + material. | | **Section** | `/job/structure/sections` | A cross-section (user-defined or pulled from a library). | | **Material** | `/job/structure/materials` | A material (user-defined or pulled from a library). | | **Node restraint** | `/job/structure/node-restraints` | A support condition (fixed, pinned, spring, etc.) attached to a node. Top-level entity, not a sub-resource of nodes. | | **Node constraint** | `/job/structure/node-constraints` | A master/slave kinematic relationship between nodes. | | **Member offset** | `/job/structure/member-offsets` | Rigid end zones at each end of a member. Top-level entity. | | **Load case** | `/job/loads/load-cases` | A grouping of loads (primary, e.g. dead, live, wind). | | **Combination case** | `/job/loads/combination-load-cases` | A weighted combination of primary cases (e.g. ULS, SLS). | | **Loads** | `/job/loads/...` | Node loads, member distributed loads, member concentrated loads, plate pressure loads, prestress loads, thermal loads, self-weight loads, etc. — each its own collection. | ### Collection vs single-item endpoints Every entity follows a uniform shape: - `GET /…/` returns the whole collection (often supports filtering and pagination — see [Filtering & Querying](/guides/filtering-and-querying)). - `POST /…/` creates one entity from a `Create` body. The response includes the `Id` SPACE GASS allocated. - `POST /…//bulk` creates many at once with a list body — see [Bulk Operations](/guides/bulk-operations). - `GET /…//{id}` reads one entity. - `PATCH /…//{id}` partially updates one entity from an `Update` body — only fields included in the request change. - `DELETE /…//{id}` removes one entity. The SDK's typed methods mirror this 1:1. ### Ids are allocated by SPACE GASS For most entities you don't pick the `Id` — POST returns the saved object with the `Id` populated, and you hold onto that for any later calls that reference it. Two entity families are an exception, where you supply a meaningful integer key directly: - **Load cases** and **combination cases** — you provide the `Id` in the create body so they have stable user-visible numbers. The numeric Id IS the user-meaningful name. - **Self-weight loads** — keyed by the load case Id (one per case). ## Units The API is unit-agnostic. Numeric values are interpreted according to whatever unit set the active job is configured with. Read or change the active set via `GET /job/units` and `PATCH /job/units`. Most model field descriptions in the [API Reference](/api) tell you which unit family applies (Length, Force, Section Properties, etc.). If you create a model entirely from the API (no opened `.sg` file), units default to SPACE GASS's standard SI set. Check first if you're unsure. ## Analysis Runs Running an analysis is asynchronous. The pattern is: 1. **Configure** with `PATCH /job/settings/...` (e.g. solver, optimisation). 2. **Start** with `POST /job/analysis/static/run-linear` (or the nonlinear / buckling / dynamic counterpart). This returns a `runId` and an initial status of `Running` — it does **not** wait. 3. **Poll** with `GET /job/analysis/runs/{runId}` until `status` is `Completed`, `Failed`, or `Cancelled`. The response includes a `progress` block (current step, percentage, load-case status) you can render. 4. **Query** results via the static / dynamic / buckling endpoints under `/job/query/analysis/...`. Each result endpoint returns one row per (case, entity) combination. If a case asked for hasn't been analysed (or any of its constituent primaries, for combinations), it appears in `Warnings.LoadCasesNotAnalyzed` in the result envelope — check this before reading `Results` so a missing run doesn't silently look like zero rows. See [Running Analysis](/guides/running-analysis) for the full pattern with both languages, including `WaitForCompletion` helpers. ## Common Pitfalls | Symptom | Cause | Fix | |---|---|---| | `409 Conflict` on every endpoint | No active job. | Open one with `POST /job/new`, `POST /job/open`, or `POST /job/open-sample`. | | `404 Not Found` on a node / member you just created | Wrong Id, or the entity was created in a previous job that has since been closed. | The Id is returned on POST — capture and reuse it within the same active job. | | Result `Warnings.LoadCasesNotAnalyzed` is non-empty | The combination's primary cases (or the case itself) haven't been analysed. | Run the relevant analysis before querying, or filter to only analysed cases. | | `409 Conflict` on creating a restraint / constraint / offset | One already exists for that node / member. They are entity-style: at most one per parent. | `DELETE` the existing one first, or `PATCH` to update it. | | Numbers come back wrong by a factor of 1000 | Unit mismatch — your code assumed kN, model is in N (or vice versa). | Check / set `GET /job/units` before sending or interpreting numeric data. | ## Next Steps - [Quick Start](/quick-start) — open the `Portal Frame.SG` sample and read its nodes in five minutes. - [Using the SDK](/using-the-sdk) — REST verbs and async patterns translated for engineers. - [Simple Beam Model](/examples/simple-beam) — the full build-to-result walkthrough. --- ## Document: Authentication Authentication for the SpaceGass API URL: /docs/authentication # Authentication The SPACE GASS API currently requires **no authentication**. The API runs as a local service on your machine and is accessed over plain HTTP — no API keys, tokens, or credentials are needed. ## Default Connection The SDK clients connect to the local API with no setup: ```csharp title="C#" using SpaceGassApi; var client = SpaceGassApiClient.CreateClient("http://localhost:34560"); // Connects to http://localhost:34560/api/v1 ``` ```python title="Python" from space_gass_api import SpaceGassApiClient client = SpaceGassApiClient.create_client("http://localhost:34560") # Connects to http://localhost:34560/api/v1 ``` ```bash title="curl" curl http://localhost:34560/api/v1/service/info ``` ## Custom Base URL If the API is running on a different port, pass the URL explicitly: ```csharp title="C#" var client = SpaceGassApiClient.CreateClient("http://localhost:8080"); ``` ```python title="Python" client = SpaceGassApiClient.create_client("http://localhost:8080") ``` ## Future Authentication API key authentication may be introduced in a future release. --- ## Document: Service Automation Start, wait for, use, and stop the SPACE GASS API service from your own code URL: /docs/guides/service-automation # Service Automation The SPACE GASS API runs as a local HTTP service launched from the SpaceGassApi executable installed with SPACE GASS. For interactive use you can double-click the shortcut and leave it running, but for scripts and batch jobs you usually want your code to manage the service itself — start it on demand, wait until it is ready, run your work, and shut it down cleanly when done. The SDK does not ship a service-lifecycle helper. The pattern is short enough to keep in your own project; the boilerplate below is a copy-paste starting point for both C# and Python. ## The Lifecycle A typical script does four things: 1. **Probe** — try `Service.Info` to see if the service is already running. If yes, reuse it (don't kill someone else's instance). 2. **Start** — if not running, launch `SpaceGassApi.exe` as a child process. 3. **Wait** — poll `Service.Info` until it responds, or fail with a timeout if the service never comes up. 4. **Stop** — when your script is finished, terminate the child process if (and only if) you started it. Skip cleanup if the service was already running before your script ran. Wrap steps 2–4 in a `try / finally` (C#) or context manager (Python) so the service still shuts down if your code throws. ## Boilerplate The example below probes for an existing service, starts one if needed, waits up to 30 seconds for it to become ready, fetches `Service.Info` to confirm, then shuts the service down. ```csharp title="C#" using System.Diagnostics; using SpaceGassApi; const string ServiceExePath = @"C:\Program Files\SPACE GASS 14.5\SpaceGassApi.exe"; var client = SpaceGassApiClient.CreateClient("http://localhost:34560"); Process? serviceProcess = null; try { // 1. Probe — reuse an already-running service if there is one if (!await IsServiceReadyAsync(client)) { // 2. Start Console.WriteLine("Starting the SPACE GASS API service..."); serviceProcess = Process.Start(new ProcessStartInfo { FileName = ServiceExePath, UseShellExecute = false, CreateNoWindow = true, }); // 3. Wait — poll until ready or fail with a timeout await WaitForServiceReadyAsync(client, TimeSpan.FromSeconds(30)); Console.WriteLine("Service is ready."); } else { Console.WriteLine("Service was already running — reusing it."); } // Do your work here var info = await client.Service.Info.GetAsync(); Console.WriteLine($"Connected to SPACE GASS {info?.SpaceGassVersion} at {info?.ApiPath}"); } finally { // 4. Stop — only if we started it if (serviceProcess is not null && !serviceProcess.HasExited) { Console.WriteLine("Stopping the service..."); serviceProcess.Kill(entireProcessTree: true); serviceProcess.WaitForExit(); } } static async Task IsServiceReadyAsync(SpaceGassApiClient c) { try { await c.Service.Info.GetAsync(); return true; } catch { return false; } } static async Task WaitForServiceReadyAsync(SpaceGassApiClient c, TimeSpan timeout) { var deadline = DateTime.UtcNow + timeout; while (DateTime.UtcNow < deadline) { if (await IsServiceReadyAsync(c)) return; await Task.Delay(500); } throw new TimeoutException("SPACE GASS API service did not become ready in time."); } ``` ```python title="Python" import asyncio import subprocess import time from space_gass_api import SpaceGassApiClient SERVICE_EXE = r"C:\Program Files\SPACE GASS 14.5\SpaceGassApi.exe" async def is_service_ready(client) -> bool: try: await client.service.info.get() return True except Exception: return False async def wait_for_service_ready(client, timeout: float = 30.0) -> None: deadline = time.monotonic() + timeout while time.monotonic() < deadline: if await is_service_ready(client): return await asyncio.sleep(0.5) raise TimeoutError("SPACE GASS API service did not become ready in time.") async def main() -> None: client = SpaceGassApiClient.create_client("http://localhost:34560") process: subprocess.Popen | None = None try: # 1. Probe — reuse an already-running service if there is one if not await is_service_ready(client): # 2. Start print("Starting the SPACE GASS API service...") process = subprocess.Popen( [SERVICE_EXE], creationflags=subprocess.CREATE_NO_WINDOW, ) # 3. Wait await wait_for_service_ready(client) print("Service is ready.") else: print("Service was already running — reusing it.") # Do your work here info = await client.service.info.get() print(f"Connected to SPACE GASS {info.space_gass_version} at {info.api_path}") finally: # 4. Stop — only if we started it if process is not None and process.poll() is None: print("Stopping the service...") process.terminate() try: process.wait(timeout=5) except subprocess.TimeoutExpired: process.kill() if __name__ == "__main__": asyncio.run(main()) ``` ## Custom Ports To run the service on a non-default port — useful if `34560` is in use, or if you want to run multiple instances side by side — pass `--port` to the executable and the matching base URL to `CreateClient`: ```csharp title="C#" const int Port = 35000; var client = SpaceGassApiClient.CreateClient($"http://localhost:{Port}/api/v1"); serviceProcess = Process.Start(new ProcessStartInfo { FileName = ServiceExePath, Arguments = $"--port={Port}", UseShellExecute = false, CreateNoWindow = true, }); ``` ```python title="Python" PORT = 35000 client = SpaceGassApiClient.create_client(f"http://localhost:{PORT}/api/v1") process = subprocess.Popen( [SERVICE_EXE, f"--port={PORT}"], creationflags=subprocess.CREATE_NO_WINDOW, ) ``` ## Handling Ctrl+C The `try / finally` (C#) and context manager (Python) above handle exceptions correctly, but a hard interrupt (Ctrl+C) skips them by default. Wire a signal handler so the service still shuts down on abort: ```csharp title="C#" Console.CancelKeyPress += (_, e) => { if (serviceProcess is not null && !serviceProcess.HasExited) serviceProcess.Kill(entireProcessTree: true); }; ``` ```python title="Python" import signal def _shutdown(*_): if process is not None and process.poll() is None: process.terminate() signal.signal(signal.SIGINT, _shutdown) signal.signal(signal.SIGTERM, _shutdown) ``` ## Tips - **Locate the executable** — the install path varies by SPACE GASS version. Read it from configuration or an environment variable rather than hard-coding `C:\Program Files\SPACE GASS 14.5\` so the same script keeps working after an upgrade. - **One service at a time on the default port** — if `34560` is already bound (perhaps by an interactive instance the user launched), starting another on the same port will fail. Either pick a different port or accept the existing one (the boilerplate above does the latter). - **Don't kill a service you didn't start** — the `finally` block guards on `serviceProcess != null`, so a script that found an already-running service leaves it running. Useful when developing interactively in the same session as a long-lived service. - **Surface service stdout** — for debugging, redirect the child process's `StandardOutput` / `StandardError` to a log file. The boilerplate uses `CreateNoWindow = true` so the service runs invisibly; flip that off (or omit it) if you want the console to show service messages. ## Run this example locally The complete program with error handling and Ctrl+C wiring is in the repo as [`Example.ServiceAutomation`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/csharp/examples/Example.ServiceAutomation) (C#) and [`service_automation`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/python/examples/service_automation) (Python). Clone the repo and run it directly — no copy-paste: ```bash title="C#" git clone https://github.com/SpaceGass/space-gass-api.git cd space-gass-api/sdks/csharp/examples/Example.ServiceAutomation dotnet run ``` ```bash title="Python" git clone https://github.com/SpaceGass/space-gass-api.git cd space-gass-api/sdks/python/examples/service_automation pip install space-gass-api # once per environment python service_automation.py ``` Edit the `ServiceExePath` / `SERVICE_EXE` constant first to match the SPACE GASS install path on your machine. --- ## Document: Running Analysis How to run analyses, poll for progress, and handle async workflows URL: /docs/guides/running-analysis # Running Analysis Analysis in the SPACE GASS API is **asynchronous** — when you start an analysis, the API returns immediately with a run ID. You then poll for progress until the run completes. This guide covers the common patterns from simple scripting to advanced multi-run monitoring. ## How It Works 1. **Start** an analysis — returns an `AnalysisRun` with a `runId` and initial status 2. **Poll** the run status using the `runId` at regular intervals 3. **Check** for a terminal status: `Completed`, `Failed`, or `Cancelled` 4. **Query** results once completed ## Simple Script — Wait for Completion Most scripts just need to run an analysis and wait. Here is a simple helper that polls until the analysis finishes, then returns the final status: ```csharp title="C#" using SpaceGassApi; using SpaceGassApi.Models; var client = SpaceGassApiClient.CreateClient("http://localhost:34560"); // Open a project and run linear static analysis await client.Job.Open.PostAsync( new OpenJobRequest { FilePath = @"C:\Projects\MyStructure.sg" }); var run = await client.Job.Analysis.Static.RunLinear.PostAsync( new StaticSettingsUpdate()); // Poll until complete var result = await WaitForCompletion(client, run.RunId.Value); Console.WriteLine($"Analysis {result.Status} in {result.ElapsedTime}"); if (result.Status == AnalysisRunStatus.Completed) { // Query results... var reactions = await client.Job.Query.Analysis.Static.NodeReactions.GetAsync(); Console.WriteLine($"Got {reactions.Results.Count} reactions"); } await client.Job.Close.PostAsync(); // --- Helper: poll until terminal state --- static async Task WaitForCompletion( SpaceGassApiClient client, Guid runId, int pollMs = 500) { while (true) { await Task.Delay(pollMs); var status = await client.Job.Analysis.Runs[runId].GetAsync(); if (status.Status is AnalysisRunStatus.Completed or AnalysisRunStatus.Failed or AnalysisRunStatus.Cancelled) { return status; } } } ``` ```python title="Python" import asyncio from space_gass_api import SpaceGassApiClient import space_gass_api.models as models client = SpaceGassApiClient.create_client("http://localhost:34560") # Open a project and run linear static analysis await client.job.open.post( models.OpenJobRequest(file_path="C:\\Projects\\MyStructure.sg")) run = await client.job.analysis.static.run_linear.post( models.StaticSettingsUpdate()) # Poll until complete async def wait_for_completion(client, run_id, poll_interval=0.5): while True: await asyncio.sleep(poll_interval) status = await client.job.analysis.runs.by_run_id(str(run_id)).get() if status.status in ( models.AnalysisRunStatus.Completed, models.AnalysisRunStatus.Failed, models.AnalysisRunStatus.Cancelled, ): return status result = await wait_for_completion(client, run.run_id) print(f"Analysis {result.status} in {result.elapsed_time}") if result.status == models.AnalysisRunStatus.Completed: # Query results... reactions = await client.job.query.analysis.static.node_reactions.get() print(f"Got {len(reactions.results)} reactions") await client.job.close.post() ``` The `WaitForCompletion` helper turns the async polling into a simple blocking call — your script just waits until the analysis is done before moving on. This is the pattern you will use most often in scripts. ## Pre-Check with Analysis Info Before running an analysis, you can call the **Info** endpoint to see which load cases already have results — and which still need to be analysed. This avoids re-running cases that are already complete, which saves time on large models. `Analysis.Static.Info` (and the equivalent `Buckling.Info` and `DynamicFrequency.Info`) returns: | Field | Description | |---|---| | `HasResults` | `true` if any case has stored results for this analysis type | | `LoadCases` | Per-case breakdown — each entry has a `LoadCase` Id and `HasResults` flag | | `NotAnalyzed` | SG list-format string of case Ids that have **not** been analysed — ready to pass straight into a run request | ### Check which cases have been analysed ```csharp title="C#" var info = await client.Job.Analysis.Static.Info.GetAsync(); Console.WriteLine($"Has any results: {info.HasResults}"); foreach (var c in info.LoadCases!) { Console.WriteLine($" LC {c.LoadCase}: {(c.HasResults == true ? "analysed" : "not analysed")}"); } if (info.NotAnalyzed != null) Console.WriteLine($"Not analysed: {info.NotAnalyzed}"); ``` ```python title="Python" info = await client.job.analysis.static.info.get() print(f"Has any results: {info.has_results}") for c in info.load_cases: status = "analysed" if c.has_results else "not analysed" print(f" LC {c.load_case}: {status}") if info.not_analyzed: print(f"Not analysed: {info.not_analyzed}") ``` ```bash title="curl" curl http://localhost:34560/api/v1/job/analysis/static/info ``` ### Filter to a specific range Pass a `loadCases` filter to check a subset. The `NotAnalyzed` field returns only the intersection of your filter with the not-analysed set — you can feed it directly into the run request. ```csharp title="C#" // Check cases 1 through 10 var info = await client.Job.Analysis.Static.Info.GetAsync( config => config.QueryParameters.LoadCases = "1-10"); if (info.NotAnalyzed != null) { Console.WriteLine($"Cases 1-10 not yet analysed: {info.NotAnalyzed}"); // Run only the missing cases var run = await client.Job.Analysis.Static.RunLinear.PostAsync( new StaticSettingsUpdate { LoadCases = info.NotAnalyzed }); var result = await WaitForCompletion(client, run.RunId.Value); Console.WriteLine($"Analysis {result.Status}"); } else { Console.WriteLine("Cases 1-10 are all analysed — nothing to run."); } ``` ```python title="Python" # Check cases 1 through 10 info = await client.job.analysis.static.info.get(load_cases="1-10") if info.not_analyzed: print(f"Cases 1-10 not yet analysed: {info.not_analyzed}") # Run only the missing cases run = await client.job.analysis.static.run_linear.post( models.StaticSettingsUpdate(load_cases=info.not_analyzed)) result = await wait_for_completion(client, run.run_id) print(f"Analysis {result.status}") else: print("Cases 1-10 are all analysed — nothing to run.") ``` ```bash title="curl" # Check cases 1-10 curl "http://localhost:34560/api/v1/job/analysis/static/info?loadCases=1-10" # If notAnalyzed is "4,7-10", run only those curl -X POST http://localhost:34560/api/v1/job/analysis/static/run-linear \ -H "Content-Type: application/json" \ -d '{"loadCases": "4,7-10"}' ``` This pattern is especially useful in automation workflows where a model is re-analysed incrementally — new load cases are added and only the missing ones need to be run, leaving existing results intact. ## Running Multiple Analyses in Sequence The API processes analysis runs in a queue. You can start multiple analyses and they will execute in order. Poll the last run ID to know when all analyses are complete: ```csharp title="C#" // Queue three analysis types var staticRun = await client.Job.Analysis.Static.RunLinear.PostAsync( new StaticSettingsUpdate()); Console.WriteLine($"Queued linear static: {staticRun.RunId}"); var bucklingRun = await client.Job.Analysis.Buckling.Run.PostAsync( new BucklingSettingsUpdate()); Console.WriteLine($"Queued buckling: {bucklingRun.RunId}"); var dynamicRun = await client.Job.Analysis.DynamicFrequency.Run.PostAsync( new DynamicFrequencySettingsUpdate()); Console.WriteLine($"Queued dynamic frequency: {dynamicRun.RunId}"); // Wait for each in order var staticResult = await WaitForCompletion(client, staticRun.RunId.Value); Console.WriteLine($"Static: {staticResult.Status} ({staticResult.ElapsedTime})"); var bucklingResult = await WaitForCompletion(client, bucklingRun.RunId.Value); Console.WriteLine($"Buckling: {bucklingResult.Status} ({bucklingResult.ElapsedTime})"); var dynamicResult = await WaitForCompletion(client, dynamicRun.RunId.Value); Console.WriteLine($"Dynamic: {dynamicResult.Status} ({dynamicResult.ElapsedTime})"); ``` ```python title="Python" # Queue three analysis types static_run = await client.job.analysis.static.run_linear.post( StaticSettingsUpdate()) print(f"Queued linear static: {static_run.run_id}") buckling_run = await client.job.analysis.buckling.run.post( BucklingSettingsUpdate()) print(f"Queued buckling: {buckling_run.run_id}") dynamic_run = await client.job.analysis.dynamic_frequency.run.post( DynamicFrequencySettingsUpdate()) print(f"Queued dynamic frequency: {dynamic_run.run_id}") # Wait for each in order static_result = await wait_for_completion(client, static_run.run_id) print(f"Static: {static_result.status} ({static_result.elapsed_time})") buckling_result = await wait_for_completion(client, buckling_run.run_id) print(f"Buckling: {buckling_result.status} ({buckling_result.elapsed_time})") dynamic_result = await wait_for_completion(client, dynamic_run.run_id) print(f"Dynamic: {dynamic_result.status} ({dynamic_result.elapsed_time})") ``` ## Monitoring Progress For longer analyses you may want to display progress as it runs. The `AnalysisRun.Progress` object provides step labels, iteration percentages, and load case status: ```csharp title="C#" var lastStep = -1; while (true) { await Task.Delay(500); var status = await client.Job.Analysis.Runs[runId].GetAsync(); if (status.Progress != null) { var p = status.Progress; var stepInfo = $"Step {p.CurrentStep}/{p.TotalSteps}"; // Print step label when it changes if (p.CurrentStep != lastStep && p.StepLabels != null) { var idx = p.CurrentStep ?? 0; if (idx < p.StepLabels.Count && !string.IsNullOrEmpty(p.StepLabels[idx])) Console.WriteLine($" [{stepInfo}] {p.StepLabels[idx]}"); lastStep = p.CurrentStep ?? -1; } // Overwrite current line with live progress Console.Write($"\r {stepInfo} | {p.IterationPercentage}%"); if (p.LoadCaseStatus != null) Console.Write($" | {p.LoadCaseStatus}"); Console.Write("".PadRight(20)); } if (status.Status is AnalysisRunStatus.Completed or AnalysisRunStatus.Failed or AnalysisRunStatus.Cancelled) { Console.WriteLine(); Console.WriteLine($"Analysis {status.Status} in {status.ElapsedTime}"); if (status.Warnings?.Count > 0) Console.WriteLine($" Warnings: {status.Warnings.Count}"); if (status.ErrorMessage != null) Console.WriteLine($" Error: {status.ErrorMessage}"); break; } } ``` ```python title="Python" last_step = -1 while True: await asyncio.sleep(0.5) status = await client.job.analysis.runs.by_run_id(str(run_id)).get() if status.progress is not None: p = status.progress step_info = f"Step {p.current_step}/{p.total_steps}" # Print step label when it changes if p.current_step != last_step and p.step_labels: idx = p.current_step or 0 if idx < len(p.step_labels) and p.step_labels[idx]: print(f" [{step_info}] {p.step_labels[idx]}") last_step = p.current_step if p.current_step is not None else -1 # Overwrite current line with live progress line = f"\r {step_info} | {p.iteration_percentage}%" if p.load_case_status: line += f" | {p.load_case_status}" print(f"{line:<60}", end="", flush=True) if status.status in ( AnalysisRunStatus.Completed, AnalysisRunStatus.Failed, AnalysisRunStatus.Cancelled, ): print() print(f"Analysis {status.status} in {status.elapsed_time}") if status.warnings: print(f" Warnings: {len(status.warnings)}") if status.error_message: print(f" Error: {status.error_message}") break ``` ## Cancelling a Run You can cancel a running or queued analysis by calling `DELETE` on the run: ```csharp title="C#" await client.Job.Analysis.Runs[runId].DeleteAsync(); ``` ```python title="Python" await client.job.analysis.runs.by_run_id(str(run_id)).delete() ``` ```bash title="curl" curl -X DELETE http://localhost:34560/api/v1/job/analysis/runs/{runId} ``` The run transitions to `Cancelling` and then `Cancelled` once the solver has stopped. ## Analysis Run Lifecycle | Status | Description | |---|---| | `Queued` | Run is waiting in the queue | | `Running` | Analysis is actively executing | | `Cancelling` | Cancellation requested, waiting for solver to stop | | `Completed` | Analysis finished successfully — results are available | | `Failed` | Analysis encountered an error — check `errorMessage` | | `Cancelled` | Analysis was cancelled before completion | ## Advanced: Real-Time Monitoring Application For a full example of an interactive monitoring application that tracks multiple concurrent runs with live progress bars, elapsed timers, and error/warning display, see the [AnalysisMonitor WPF example](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/csharp/examples/Example.AnalysisMonitor) in the repository. --- ## Document: Filtering & Querying How to filter, query, and paginate API results URL: /docs/guides/filtering-and-querying # Filtering & Querying Most API endpoints support query parameters for filtering, and all query result endpoints support pagination. This guide covers the common patterns. ## Filtering Entities Structure endpoints like nodes, members, and sections support query parameters to narrow down results. ```csharp title="C#" // Get nodes within a coordinate range var filtered = await client.Job.Structure.Nodes.GetAsync( config => { config.QueryParameters.MinX = 0; config.QueryParameters.MaxX = 100; config.QueryParameters.Limit = 50; }); ``` ```python title="Python" filtered = await client.job.structure.nodes.get( min_x=0, max_x=100, limit=50) ``` ```bash title="curl" curl "http://localhost:34560/api/v1/job/structure/nodes?MinX=0&MaxX=100&Limit=50" ``` ## Getting a Single Entity Access any entity by its Id using the indexer (C#) or `by_id` (Python): ```csharp title="C#" var node = await client.Job.Structure.Nodes[1].GetAsync(); Console.WriteLine($"Node {node.Id}: ({node.X}, {node.Y}, {node.Z})"); ``` ```python title="Python" node = await client.job.structure.nodes.by_id(1).get() print(f"Node {node.id}: ({node.x}, {node.y}, {node.z})") ``` ```bash title="curl" curl http://localhost:34560/api/v1/job/structure/nodes/1 ``` ## Querying Analysis Results After running an analysis, query endpoints return results in a flat, efficient format. All query endpoints support optional filters for load case and entity key. ### Node Reactions ```csharp title="C#" var result = await client.Job.Query.Analysis.Static.NodeReactions.GetAsync(); foreach (var r in result.Results) { Console.WriteLine($"Case {r.LoadCase}, Node {r.Node}: " + $"Fx={r.Fx}, Fy={r.Fy}, Fz={r.Fz}"); } ``` ```python title="Python" result = await client.job.query.analysis.static.node_reactions.get() for r in result.results: print(f"Case {r.load_case}, Node {r.node}: " f"Fx={r.fx}, Fy={r.fy}, Fz={r.fz}") ``` ```bash title="curl" curl http://localhost:34560/api/v1/job/query/analysis/static/node-reactions ``` ### Filtering by Load Case and Node Filter values use **SG list format** — a string of comma-separated Ids and dash-separated ranges (e.g. `"1,3-7,10"`). Pass an empty string or omit the parameter to return all results. ```csharp title="C#" var result = await client.Job.Query.Analysis.Static.NodeDisplacements .GetAsync(config => { config.QueryParameters.LoadCases = "1,3"; config.QueryParameters.Nodes = "10-12"; }); ``` ```python title="Python" result = await client.job.query.analysis.static.node_displacements.get( load_cases="1,3", nodes="10-12") ``` ```bash title="curl" curl "http://localhost:34560/api/v1/job/query/analysis/static/node-displacements?loadCases=1,3&nodes=10-12" ``` ### Member Intermediate Forces Grouped results like member forces use a columnar layout — the parent object contains the case and member keys, with force values as parallel arrays indexed by station. ```csharp title="C#" var result = await client.Job.Query.Analysis.Static.MemberIntermediateForces .GetAsync(config => { config.QueryParameters.Members = "1"; }); foreach (var m in result.Results) { Console.WriteLine($"Case {m.LoadCase}, Member {m.Member}:"); for (int i = 0; i < m.Station.Length; i++) { Console.WriteLine($" Station {m.Station[i]}: " + $"Fx={m.Fx[i]}, Fy={m.Fy[i]}, Mz={m.Mz[i]}"); } } ``` ```python title="Python" result = await client.job.query.analysis.static.member_intermediate_forces.get( members="1") for m in result.results: print(f"Case {m.load_case}, Member {m.member}:") for i in range(len(m.station)): print(f" Station {m.station[i]}: " f"Fx={m.fx[i]}, Fy={m.fy[i]}, Mz={m.mz[i]}") ``` ```bash title="curl" curl "http://localhost:34560/api/v1/job/query/analysis/static/member-intermediate-forces?members=1" ``` ### Buckling Load Factors ```csharp title="C#" var result = await client.Job.Query.Analysis.Buckling.LoadFactors.GetAsync(); foreach (var b in result.Results) { Console.WriteLine($"Case {b.LoadCase}, Mode {b.Mode}: " + $"Factor={b.LoadFactor}, Tolerance={b.Tolerance}"); } ``` ```python title="Python" result = await client.job.query.analysis.buckling.load_factors.get() for b in result.results: print(f"Case {b.load_case}, Mode {b.mode}: " f"Factor={b.load_factor}, Tolerance={b.tolerance}") ``` ```bash title="curl" curl http://localhost:34560/api/v1/job/query/analysis/buckling/load-factors ``` ### Natural Frequencies ```csharp title="C#" var result = await client.Job.Query.Analysis.Dynamic.NaturalFrequencies .GetAsync(); foreach (var f in result.Results) { Console.WriteLine($"Case {f.LoadCase}, Mode {f.Mode}: " + $"{f.NaturalFrequency} Hz (period {f.NaturalPeriod}s)"); } ``` ```python title="Python" result = await client.job.query.analysis.dynamic.natural_frequencies.get() for f in result.results: print(f"Case {f.load_case}, Mode {f.mode}: " f"{f.natural_frequency} Hz (period {f.natural_period}s)") ``` ```bash title="curl" curl http://localhost:34560/api/v1/job/query/analysis/dynamic/natural-frequencies ``` ### Steel Design Check Summary ```csharp title="C#" var result = await client.Job.Query.Design.SteelMember.CheckSummary.GetAsync(); foreach (var s in result.Results) { Console.WriteLine($"Member {s.Member}: {s.Section} — " + $"{s.Failure} (factor {s.LoadFactor}, case {s.CriticalCase})"); } ``` ```python title="Python" result = await client.job.query.design.steel_member.check_summary.get() for s in result.results: print(f"Member {s.member}: {s.section} — " f"{s.failure} (factor {s.load_factor}, case {s.critical_case})") ``` ```bash title="curl" curl http://localhost:34560/api/v1/job/query/design/steel-member/check-summary ``` ## Pagination All query endpoints support `offset` and `limit` for pagination. By default all results are returned. Use pagination for large result sets. ```csharp title="C#" // Get the first 100 node displacements var page1 = await client.Job.Query.Analysis.Static.NodeDisplacements .GetAsync(config => { config.QueryParameters.Offset = 0; config.QueryParameters.Limit = 100; }); // Get the next 100 var page2 = await client.Job.Query.Analysis.Static.NodeDisplacements .GetAsync(config => { config.QueryParameters.Offset = 100; config.QueryParameters.Limit = 100; }); ``` ```python title="Python" page1 = await client.job.query.analysis.static.node_displacements.get( offset=0, limit=100) page2 = await client.job.query.analysis.static.node_displacements.get( offset=100, limit=100) ``` ```bash title="curl" # Page 1 curl "http://localhost:34560/api/v1/job/query/analysis/static/node-displacements?offset=0&limit=100" # Page 2 curl "http://localhost:34560/api/v1/job/query/analysis/static/node-displacements?offset=100&limit=100" ``` --- ## Document: File Handling Working with SPACE GASS job files through the API URL: /docs/guides/file-handling # File Handling The API provides endpoints for managing SPACE GASS `.sg` project files — opening, saving, closing, and checking file status before operations. ## Opening a File ```csharp title="C#" using SpaceGassApi.Models; await client.Job.Open.PostAsync( new OpenJobRequest { FilePath = @"C:\Projects\MyStructure.sg" }); ``` ```python title="Python" from space_gass_api.models.open_job_request import OpenJobRequest await client.job.open.post( OpenJobRequest(file_path="C:\\Projects\\MyStructure.sg")) ``` ```bash title="curl" curl -X POST http://localhost:34560/api/v1/job/open \ -H "Content-Type: application/json" \ -d '{"filePath": "C:\\Projects\\MyStructure.sg"}' ``` ## Checking File Status Before opening a file, you can check its status to determine if it is locked by another process or has unsaved changes: ```csharp title="C#" var status = await client.File.Status.GetAsync(config => config.QueryParameters.FilePath = @"C:\Projects\MyStructure.sg"); // status contains CanOpenSafely, Description, RecommendedAction ``` ```python title="Python" from kiota_abstractions.base_request_configuration import RequestConfiguration from space_gass_api.file.status.status_request_builder import StatusRequestBuilder query_params = StatusRequestBuilder.StatusRequestBuilderGetQueryParameters( file_path=r"C:\Projects\MyStructure.sg") status = await client.file.status.get( request_configuration=RequestConfiguration(query_parameters=query_params)) ``` ```bash title="curl" curl "http://localhost:34560/api/v1/file/status?filePath=C:%5CProjects%5CMyStructure.sg" ``` ## Saving The save endpoint handles both saving to the current path and saving to a new path. Pass a `filePath` to save as a new file, or omit it to save in place. ```csharp title="C#" // Save to current file await client.Job.Save.PostAsync(new SaveJobRequest()); // Save to a new file path await client.Job.Save.PostAsync( new SaveJobRequest { FilePath = @"C:\Projects\MyStructure_Copy.sg" }); ``` ```python title="Python" from space_gass_api.models.save_job_request import SaveJobRequest # Save to current file await client.job.save.post(SaveJobRequest()) # Save to a new file path await client.job.save.post( SaveJobRequest(file_path="C:\\Projects\\MyStructure_Copy.sg")) ``` ```bash title="curl" # Save to current file curl -X POST http://localhost:34560/api/v1/job/save \ -H "Content-Type: application/json" # Save to a new file path curl -X POST http://localhost:34560/api/v1/job/save \ -H "Content-Type: application/json" \ -d '{"filePath": "C:\\Projects\\MyStructure_Copy.sg"}' ``` ## Creating a New Job ```csharp title="C#" await client.Job.New.PostAsync(); ``` ```python title="Python" await client.job.new.post() ``` ```bash title="curl" curl -X POST http://localhost:34560/api/v1/job/new ``` ## Creating a New Job From a Template Upload a `.sgbase` template file to seed a new job. The template data is loaded but the new job has no file path — use `Job.Save` with a `FilePath` (see [Saving](#saving)) to write it to disk. The body is a `multipart/form-data` upload with one part named `template`. Both SDKs expose a `NewFromTemplateRequest` you construct from a file path. ```csharp title="C#" using SpaceGassApi; await client.Job.NewFromTemplate.PostAsync( new NewFromTemplateRequest(@"C:\Templates\my-template.sgbase")); ``` ```python title="Python" from space_gass_api import NewFromTemplateRequest await client.job.new_from_template.post( NewFromTemplateRequest(r"C:\Templates\my-template.sgbase")) ``` ```bash title="curl" curl -X POST http://localhost:34560/api/v1/job/new-from-template \ -F "template=@C:/Templates/my-template.sgbase" ``` ## Closing ```csharp title="C#" await client.Job.Close.PostAsync(); ``` ```python title="Python" await client.job.close.post() ``` ```bash title="curl" curl -X POST http://localhost:34560/api/v1/job/close ``` --- ## Document: Error Handling How the SpaceGass API reports and handles errors URL: /docs/guides/error-handling # Error Handling Every error response from the API uses a standard `ErrorResponse` envelope. All error responses include a JSON body with diagnostic information. ## Error Response Format ```json { "title": "Bad Request", "status": 400, "detail": "Node with Id 1 already exists", "errorCode": "DUPLICATE_ENTITY", "errors": [ { "field": "id", "message": "Node with Id 1 already exists", "code": "DUPLICATE_ENTITY" } ], "source": "Application", "timestamp": "2026-05-18T10:30:00+10:00" } ``` | Field | Type | Description | |-------|------|-------------| | `title` | `string` | Short human-readable summary | | `status` | `int` | HTTP status code | | `detail` | `string` | Human-readable explanation specific to this occurrence | | `errorCode` | `string` | Machine-readable error code for programmatic handling | | `errors` | `ValidationError[]` | One entry per failed validation rule (field, message, code) | | `source` | `string` | Where the error originated (e.g. `"Application"`, `"Infrastructure"`) | | `timestamp` | `string` | ISO 8601 timestamp when the error occurred | | `instance` | `string` | URI reference for the specific occurrence | | `extensions` | `object` | Additional context-specific data | ## HTTP Status Codes | Status | Meaning | |--------|---------| | `400` | Bad request — validation failure or invalid input | | `404` | Not found — resource does not exist | | `409` | Conflict — operation conflicts with current state (e.g., file locked) | | `500` | Server error — unexpected internal failure | ## Handling Errors in SDK Clients The SDK raises a typed `ErrorResponse` exception that carries the fields from the JSON body — `Status`, `Title`, `Detail`, `ErrorCode`, `Errors`, and more. Catch it directly so you can inspect the structured error rather than relying on the generic exception message: ```csharp title="C#" using SpaceGassApi.Models; try { var node = await client.Job.Structure.Nodes[999].GetAsync(); } catch (ErrorResponse err) when (err.Status == 404) { Console.WriteLine($"Node not found: {err.Detail}"); } catch (ErrorResponse err) { Console.WriteLine($"API error {err.Status} ({err.Title}): {err.Detail}"); if (err.ErrorCode != null) Console.WriteLine($" Code: {err.ErrorCode}"); foreach (var ve in err.Errors ?? []) Console.WriteLine($" [{ve.Field}] {ve.Message}"); } ``` ```python title="Python" from space_gass_api.models.error_response import ErrorResponse try: node = await client.job.structure.nodes.by_id(999).get() except ErrorResponse as err: if err.status == 404: print(f"Node not found: {err.detail}") else: print(f"API error {err.status} ({err.title}): {err.detail}") if err.error_code: print(f" Code: {err.error_code}") for ve in err.errors or []: print(f" [{ve.field}] {ve.message}") ``` ```bash title="curl" # curl shows HTTP status in verbose mode curl -v http://localhost:34560/api/v1/job/structure/nodes/999 # Response (404): # {"title":"Not Found","status":404,"detail":"Node 999 not found","errorCode":"NOT_FOUND",...} ``` `ErrorResponse` inherits from Kiota's base `ApiException` (C#) / `APIError` (Python), so you can still catch the broader type if you need to handle transport errors (network drops, timeouts) the same way. --- ## Document: Bulk Operations How bulk endpoints work, how they differ from single-item calls, and when to use them URL: /docs/guides/bulk-operations # Bulk Operations Most collection endpoints — `Job.Structure.Nodes`, `Job.Structure.Members`, the load primitives, and so on — expose two shapes: - **Single**: `Nodes.PostAsync(NodeCreate body)` — one entity per call - **Bulk**: `Nodes.Bulk.PostAsync(List body)` — many entities per call ## Differences | | Single | Bulk | | --- | --- | --- | | **Round trips** | One per item | One per batch | | **Validation** | Each item is validated as the call arrives | The full batch is validated up front, before any items are created | | **Failure response** | Throws on the first failing item | Returns a `*BulkResult` with `Succeeded` and `Errors` lists | | **Partial commit on validation failure** | Items submitted before the failure are already created | Nothing is created unless every item validates | | **Partial success** | Not applicable | Opt-in via `?continueOnError=true` | ## Example: creating 100 nodes Bulk endpoints accept a `List` and return an `XBulkResult` with the created entities under `Succeeded` and any failures under `Errors`. `BulkError.Index` is the position in the input list, so you can match an error back to the body you sent. ```csharp title="C#" var bodies = Enumerable.Range(0, 100) .Select(i => new NodeCreate { X = i % 10, Y = 0, Z = i / 10 }) .ToList(); var result = await client.Job.Structure.Nodes.Bulk.PostAsync(bodies); foreach (var node in result!.Succeeded!) { Console.WriteLine($" Node {node.Id}: ({node.X}, {node.Y}, {node.Z})"); } foreach (var error in result.Errors!) { Console.WriteLine($" [{error.Index}] {error.Error}"); } ``` ```python title="Python" bodies = [ NodeCreate(x=i % 10, y=0, z=i // 10) for i in range(100) ] result = await client.job.structure.nodes.bulk.post(bodies) for node in result.succeeded or []: print(f" Node {node.id}: ({node.x}, {node.y}, {node.z})") for error in result.errors or []: print(f" [{error.index}] {error.error}") ``` The same pattern applies to `Job.Structure.Members`, `Job.Structure.Sections`, and the load primitives — every collection endpoint has a `.Bulk` counterpart. ## Partial success with `continueOnError` A bulk POST is all-or-nothing by default — if any item fails validation, nothing is created and the call returns errors. Pass `continueOnError=true` to commit every item that validates and report the rest in `Errors`: ```csharp title="C#" var result = await client.Job.Structure.Nodes.Bulk.PostAsync( bodies, config => config.QueryParameters.ContinueOnError = true); Console.WriteLine($"Succeeded: {result!.Succeeded!.Count}"); Console.WriteLine($"Failed: {result.Errors!.Count}"); ``` ```python title="Python" from kiota_abstractions.base_request_configuration import RequestConfiguration from space_gass_api.job.structure.nodes.bulk.bulk_request_builder import BulkRequestBuilder params = BulkRequestBuilder.BulkRequestBuilderPostQueryParameters(continue_on_error=True) result = await client.job.structure.nodes.bulk.post( bodies, request_configuration=RequestConfiguration(query_parameters=params)) print(f"Succeeded: {len(result.succeeded or [])}") print(f"Failed: {len(result.errors or [])}") ``` ## When to use bulk - Creating or updating more than one entity in a single script or automated job. - Importing or replicating a model from another source. - Anywhere you'd otherwise loop a single-item call. --- ## Document: Switch to readonly mode Release the SPACE GASS license seat so other users or instances can use it while you query results URL: /docs/examples/switch-to-readonly # Switch to readonly mode The API runs in **readwrite** mode by default, holding a full SPACE GASS license seat. If you only need to query results, switch to **readonly** mode to release the seat — freeing it for the desktop application or another API instance. Readonly mode must be set **before** opening a job. If a job is already open, you must close it first — the mode switch is queued until the job closes. ## Switch to readonly, open, query, close The snippet below switches to readonly, opens a sample project, queries node reactions from a previous analysis, then closes the job. Because the mode was set before opening, the API never acquires a SPACE GASS license seat. ```csharp title="C#" using SpaceGassApi; using SpaceGassApi.Models; var client = SpaceGassApiClient.CreateClient("http://localhost:34560"); // Switch to readonly before opening — no SPACE GASS seat consumed await client.Service.AccessMode.PostAsync( new AccessModeUpdate { AccessMode = AccessMode.ReadOnly }); try { await client.Job.OpenSample.PostAsync( new OpenSampleRequest { FileName = "Portal Frame.SG" }); // Query existing analysis results (read-only operations only) var reactions = await client.Job.Query.Analysis.Static.NodeReactions.GetAsync(); foreach (var r in reactions!.Results!) { Console.WriteLine($"Node {r.Node}, LC {r.LoadCase}: Fy={r.Fy:F2} kN"); } } finally { await client.Job.Close.PostAsync(); // Restore readwrite when finished await client.Service.AccessMode.PostAsync( new AccessModeUpdate { AccessMode = AccessMode.ReadWrite }); } ``` ```python title="Python" from space_gass_api import SpaceGassApiClient import space_gass_api.models as models client = SpaceGassApiClient.create_client("http://localhost:34560") # Switch to readonly before opening — no SPACE GASS seat consumed await client.service.access_mode.post( models.AccessModeUpdate(access_mode=models.AccessMode.ReadOnly)) try: await client.job.open_sample.post( models.OpenSampleRequest(file_name="Portal Frame.SG")) # Query existing analysis results (read-only operations only) reactions = await client.job.query.analysis.static.node_reactions.get() for r in reactions.results: print(f"Node {r.node}, LC {r.load_case}: Fy={r.fy:.2f} kN") finally: await client.job.close.post() # Restore readwrite when finished await client.service.access_mode.post( models.AccessModeUpdate(access_mode=models.AccessMode.ReadWrite)) ``` ```bash title="curl" # Switch to readonly curl -X POST http://localhost:34560/api/v1/service/access-mode \ -H "Content-Type: application/json" \ -d '{"accessMode": "ReadOnly"}' # Open a sample (read-only — no SPACE GASS seat consumed) curl -X POST http://localhost:34560/api/v1/job/open-sample \ -H "Content-Type: application/json" \ -d '{"fileName": "Portal Frame.SG"}' # Query reactions curl http://localhost:34560/api/v1/job/query/analysis/static/node-reactions # Close and restore readwrite curl -X POST http://localhost:34560/api/v1/job/close curl -X POST http://localhost:34560/api/v1/service/access-mode \ -H "Content-Type: application/json" \ -d '{"accessMode": "ReadWrite"}' ``` ### What if a job is already open? If you call `POST /service/access-mode` with `"ReadOnly"` while a job is open, the API returns **202 Accepted** — the transition is queued and commits when the job closes. Close the job first if you need the mode to take effect immediately. If all license seats are in use when switching back to readwrite, the API returns **409 Conflict** and stays in readonly. ### Check the current mode Use `GET /service/access-mode` (the `accessMode` and `pendingAccessMode` fields) to see the active mode and any pending transition. ## See also - [Licensing](/licensing) — full explanation of readwrite and readonly modes - [Concepts — The Active Job](/concepts#the-active-job) --- ## Document: Simple Beam Model Build a simple beam model, run analysis, and query results URL: /docs/examples/simple-beam # Simple Beam Model This walkthrough demonstrates a complete SPACE GASS API workflow from start to finish: create a project, build a simply-supported beam with section, material, and loads, run a linear static analysis, and retrieve the design results. ## What We Will Build A simply-supported steel beam with self-weight, a dead load and a live load, and ULS / SLS combinations to AS/NZS 1170: - **Node 1** at (0, 0, 0) — fixed support - **Node 2** at (6, 0, 0) — pinned support - **Member 1** between the nodes, using a library section + library material - **Three primary load cases** — self-weight, dead, live - **Two combination cases** — ULS strength, SLS deflection - A linear static analysis run, then queries for the maximum ULS bending moment and the SLS midspan deflection ## Project Setup Before you can run this example you need a code editor and the SPACE GASS SDK for your chosen language. If you already have a working .NET or Python environment, skip ahead to [Step 1](#step-1--create-the-client-and-a-new-project). We recommend [Visual Studio Code](https://code.visualstudio.com/) — it is free, runs on Windows, macOS, and Linux, and has first-class support for both C# and Python. If you already use Visual Studio, JetBrains Rider, or PyCharm, those work too — the steps below translate directly. ### C# 1. Install the [.NET 8 SDK](https://dotnet.microsoft.com/download) and [Visual Studio Code](https://code.visualstudio.com/). 2. In VS Code, install the [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) extension. 3. Pick a folder for your project (anywhere on disk), open it in VS Code via **File → Open Folder...**, then open the integrated terminal with **View → Terminal**. In the terminal, run: ```bash dotnet new console dotnet add package SpaceGassApi ``` 4. Open `Program.cs` (created by the first command) and paste the example code from the steps below. Run it with `dotnet run`. ### Python 1. Install [Python 3.10 or newer](https://www.python.org/downloads/) and [Visual Studio Code](https://code.visualstudio.com/). 2. In VS Code, install the [Python extension](https://marketplace.visualstudio.com/items?itemName=ms-python.python). 3. Pick a folder for your project (anywhere on disk), open it in VS Code via **File → Open Folder...**, then open the integrated terminal with **View → Terminal**. In the terminal, run: ```bash python -m venv .venv .venv\Scripts\activate pip install space-gass-api ``` On macOS or Linux replace the second line with `source .venv/bin/activate`. 4. Create a new file called `main.py` (**File → New File**, save with that name in the project folder) and paste the example code from the steps below. Run it with `python main.py`. ## Step 1 — Create the Client and a New Project `SpaceGassApiClient` is your handle to the running API. Every endpoint follows the same pattern — `GetAsync` to read, `PostAsync` to create, `PatchAsync` to update, `DeleteAsync` to remove. See the [Using the SDK guide](/using-the-sdk) if these are new. The API only has one project open at a time. After `Job.New` (or `Job.Open` on an existing `.sg` file) the new project becomes the **active** job — every later call in this walkthrough acts on it until you `Job.Close` it in Step 16. ```csharp title="C#" using SpaceGassApi; using SpaceGassApi.Models; var client = SpaceGassApiClient.CreateClient("http://localhost:34560"); // Create a new blank project await client.Job.New.PostAsync(); ``` ```python title="Python" from space_gass_api import SpaceGassApiClient import space_gass_api.models as models client = SpaceGassApiClient.create_client("http://localhost:34560") # Create a new blank project await client.job.new.post() ``` ## Step 2 — Create Nodes Create the two endpoints of the beam — Node 1 at the origin and Node 2 six metres along X. POSTing a `NodeCreate` to `Job.Structure.Nodes` returns the saved node including the `Id` SPACE GASS allocated; hold onto each Id, every later call that references this node uses it. ```csharp title="C#" var node1 = await client.Job.Structure.Nodes.PostAsync( new NodeCreate { X = 0.0, Y = 0.0, Z = 0.0 }); var node2 = await client.Job.Structure.Nodes.PostAsync( new NodeCreate { X = 6.0, Y = 0.0, Z = 0.0 }); Console.WriteLine($"Node {node1.Id}: ({node1.X}, {node1.Y}, {node1.Z})"); Console.WriteLine($"Node {node2.Id}: ({node2.X}, {node2.Y}, {node2.Z})"); ``` ```python title="Python" node1 = await client.job.structure.nodes.post( models.NodeCreate(x=0.0, y=0.0, z=0.0)) node2 = await client.job.structure.nodes.post( models.NodeCreate(x=6.0, y=0.0, z=0.0)) print(f"Node {node1.id}: ({node1.x}, {node1.y}, {node1.z})") print(f"Node {node2.id}: ({node2.x}, {node2.y}, {node2.z})") ``` ## Step 3 — Apply Restraints Restraints are top-level entities — POST a `NodeRestraintCreate` (with `Node` set on the body) to `Job.Structure.NodeRestraints`. The 6-character `RestraintCode` maps to the TX, TY, TZ, RX, RY, RZ DOFs. Each character is one of: | Code | Meaning | | --- | --- | | `F` | **Fixed** — prevents movement | | `R` | **Released** — allows movement | | `S` | Spring (movement governed by a spring stiffness) | | `V` | Variable spring (stiffness-vs-deflection table) | | `P` | Plastic (upper force / moment limit on the reaction) | | `N` | Friction (limit proportional to the normal-axis reaction) | For our beam, Node 1 is fully fixed (`FFFFFF` — all six DOFs prevented from moving) and Node 2 is pinned (`FFFRRR` — translations fixed, rotations released). ```csharp title="C#" // Node 1: Fully fixed support (all 6 DOFs Fixed) await client.Job.Structure.NodeRestraints.PostAsync( new NodeRestraintCreate { Node = node1.Id, RestraintCode = "FFFFFF" }); // Node 2: Pinned support (translations Fixed, rotations Released) await client.Job.Structure.NodeRestraints.PostAsync( new NodeRestraintCreate { Node = node2.Id, RestraintCode = "FFFRRR" }); ``` ```python title="Python" # Node 1: Fully fixed support (all 6 DOFs Fixed) await client.job.structure.node_restraints.post( models.NodeRestraintCreate(node=node1.id, restraint_code="FFFFFF")) # Node 2: Pinned support (translations Fixed, rotations Released) await client.job.structure.node_restraints.post( models.NodeRestraintCreate(node=node2.id, restraint_code="FFFRRR")) ``` ## Step 4 — Add a Material Add the material before the member so its `Id` is available to assign. POSTing a `MaterialLibraryCreate` to `Job.Structure.Materials.Library` pulls a standard material from a SPACE GASS library — `Library` is the library file name installed with SPACE GASS and `Name` is the material designation in that library. (Pass a `MaterialUserCreate` to `Job.Structure.Materials` directly if you need a fully user-defined material instead.) ```csharp title="C#" var steel = await client.Job.Structure.Materials.Library.PostAsync( new MaterialLibraryCreate { Library = "Aust", Name = "STEEL", }); Console.WriteLine($"Material {steel.Id}: {steel.Name}"); ``` ```python title="Python" steel = await client.job.structure.materials.library.post( models.MaterialLibraryCreate( library="Aust", name="STEEL", )) print(f"Material {steel.id}: {steel.name}") ``` ## Step 5 — Add a Section Like the material, create the section before the member so its `Id` is available. POSTing a `SectionLibraryCreate` to `Job.Structure.Sections.Library` pulls a standard profile from a SPACE GASS library — `Library` is the library file name installed with SPACE GASS and `Name` is the section designation in that library. ```csharp title="C#" var section = await client.Job.Structure.Sections.Library.PostAsync( new SectionLibraryCreate { Library = "Aust300", Name = "360 UB 44.7", Mark = "B1", }); Console.WriteLine($"Section {section.Id}: {section.Name}"); ``` ```python title="Python" section = await client.job.structure.sections.library.post( models.SectionLibraryCreate( library="Aust300", name="360 UB 44.7", mark="B1", )) print(f"Section {section.id}: {section.name}") ``` ## Step 6 — Create the Member `MemberCreate` connects the two nodes with a single member and assigns the section and material we just made. `NodeA` / `NodeB` take the Ids from Step 2; `Section` and `Material` take the Ids returned by Steps 4 and 5. ```csharp title="C#" var member = await client.Job.Structure.Members.PostAsync( new MemberCreate { NodeA = node1.Id, NodeB = node2.Id, Section = section.Id, Material = steel.Id, }); Console.WriteLine($"Member {member.Id}: Node {member.NodeA} → Node {member.NodeB}"); ``` ```python title="Python" member = await client.job.structure.members.post( models.MemberCreate( node_a=node1.id, node_b=node2.id, section=section.id, material=steel.id, )) print(f"Member {member.id}: Node {member.node_a} → Node {member.node_b}") ``` ## Step 7 — Create Primary Load Cases Create three primary cases up front — self-weight, dead, and live — so their Ids are available when we attach loads in Steps 8–10. POSTing a `LoadCaseCreate` to `Job.Loads.LoadCases` creates a primary case; combination cases come later in Step 11. ```csharp title="C#" var selfWeightCase = await client.Job.Loads.LoadCases.PostAsync( new LoadCaseCreate { Id = 1, Title = "Self-weight" }); var deadCase = await client.Job.Loads.LoadCases.PostAsync( new LoadCaseCreate { Id = 2, Title = "Dead Load" }); var liveCase = await client.Job.Loads.LoadCases.PostAsync( new LoadCaseCreate { Id = 3, Title = "Live Load" }); Console.WriteLine($"Load cases: SW={selfWeightCase.Id}, G={deadCase.Id}, Q={liveCase.Id}"); ``` ```python title="Python" self_weight_case = await client.job.loads.load_cases.post( models.LoadCaseCreate(id=1, title="Self-weight")) dead_case = await client.job.loads.load_cases.post( models.LoadCaseCreate(id=2, title="Dead Load")) live_case = await client.job.loads.load_cases.post( models.LoadCaseCreate(id=3, title="Live Load")) print(f"Load cases: SW={self_weight_case.id}, G={dead_case.id}, Q={live_case.id}") ``` ## Step 8 — Apply the Self-Weight Load Attach a self-weight load to the self-weight case. POST a `SelfWeightLoadCreate` to `Job.Loads.SelfWeightLoads` with `Case` set on the body — the load case must already exist, and one self-weight load per case is allowed. Acceleration is expressed in **G** (multiples of gravity), so set `AccelerationY = -1.0` for one G of gravity in the negative-Y direction. ```csharp title="C#" await client.Job.Loads.SelfWeightLoads.PostAsync( new SelfWeightLoadCreate { LoadCase = selfWeightCase.Id, AccelerationX = 0.0, AccelerationY = -1.0, // 1 G downward AccelerationZ = 0.0, }); ``` ```python title="Python" await client.job.loads.self_weight_loads.post( models.SelfWeightLoadCreate( load_case=self_weight_case.id, acceleration_x=0.0, acceleration_y=-1.0, # 1 G downward acceleration_z=0.0, )) ``` ## Step 9 — Add a Member Distributed Load to the Dead Case Apply a uniform 2 kN/m downward across the full 6 m span on the dead case. `MemberDistributedLoadCreate` takes the target `LoadCase` and `Member` Ids, start / finish positions along the member (in length units), and start / finish force intensity per unit length on each axis. ```csharp title="C#" await client.Job.Loads.MemberDistributedLoads.PostAsync( new MemberDistributedLoadCreate { LoadCase = deadCase.Id, Member = member.Id, PositionUnits = LoadPositionUnits.Percent, StartPosition = 0.0, FinishPosition = 100.0, FyStart = -2.0, // kN/m downward FyFinish = -2.0, }); ``` ```python title="Python" await client.job.loads.member_distributed_loads.post( models.MemberDistributedLoadCreate( load_case=dead_case.id, member=member.id, position_units=models.LoadPositionUnits.Percent, start_position=0.0, finish_position=100.0, fy_start=-2.0, # kN/m downward fy_finish=-2.0, )) ``` ## Step 10 — Add a Member Distributed Load to the Live Case Same shape as Step 9 — swap the `LoadCase` to the live case and bump the intensity to 5 kN/m. ```csharp title="C#" await client.Job.Loads.MemberDistributedLoads.PostAsync( new MemberDistributedLoadCreate { LoadCase = liveCase.Id, Member = member.Id, PositionUnits = LoadPositionUnits.Percent, StartPosition = 0.0, FinishPosition = 100.0, FyStart = -5.0, // kN/m downward FyFinish = -5.0, }); ``` ```python title="Python" await client.job.loads.member_distributed_loads.post( models.MemberDistributedLoadCreate( load_case=live_case.id, member=member.id, position_units=models.LoadPositionUnits.Percent, start_position=0.0, finish_position=100.0, fy_start=-5.0, # kN/m downward fy_finish=-5.0, )) ``` ## Step 11 — Define the ULS and SLS Combinations A combination case is created in a single call via `Job.Loads.CombinationLoadCases`, with the title, Id, and the list of items (primary case + factor pairs) supplied inline on `CombinationLoadCaseCreate`. Below we define ULS (`1.2G + 1.5Q`) and SLS short-term (`1.0G + 0.7Q`) combinations to AS/NZS 1170. ```csharp title="C#" // ULS: 1.2 G + 1.5 Q (self-weight + dead are both G) var ulsCase = await client.Job.Loads.CombinationLoadCases.PostAsync( new CombinationLoadCaseCreate { Id = 10, Title = "ULS - Strength", CombinationItems = new List { new() { LoadCase = selfWeightCase.Id, MultiplyingFactor = 1.2 }, new() { LoadCase = deadCase.Id, MultiplyingFactor = 1.2 }, new() { LoadCase = liveCase.Id, MultiplyingFactor = 1.5 }, }, }); // SLS short-term: 1.0 G + 0.7 Q var slsCase = await client.Job.Loads.CombinationLoadCases.PostAsync( new CombinationLoadCaseCreate { Id = 20, Title = "SLS - Short-term Deflection", CombinationItems = new List { new() { LoadCase = selfWeightCase.Id, MultiplyingFactor = 1.0 }, new() { LoadCase = deadCase.Id, MultiplyingFactor = 1.0 }, new() { LoadCase = liveCase.Id, MultiplyingFactor = 0.7 }, }, }); ``` ```python title="Python" # ULS: 1.2 G + 1.5 Q (self-weight + dead are both G) uls_case = await client.job.loads.combination_load_cases.post( models.CombinationLoadCaseCreate( id=10, title="ULS - Strength", combination_items=[ models.CombinationLoadCaseItem(load_case=self_weight_case.id, multiplying_factor=1.2), models.CombinationLoadCaseItem(load_case=dead_case.id, multiplying_factor=1.2), models.CombinationLoadCaseItem(load_case=live_case.id, multiplying_factor=1.5), ], )) # SLS short-term: 1.0 G + 0.7 Q sls_case = await client.job.loads.combination_load_cases.post( models.CombinationLoadCaseCreate( id=20, title="SLS - Short-term Deflection", combination_items=[ models.CombinationLoadCaseItem(load_case=self_weight_case.id, multiplying_factor=1.0), models.CombinationLoadCaseItem(load_case=dead_case.id, multiplying_factor=1.0), models.CombinationLoadCaseItem(load_case=live_case.id, multiplying_factor=0.7), ], )) ``` ## Step 12 — Save the Initial Model Persist the project to disk before kicking off the analysis. If something fails downstream, you can open the saved `.sg` in SPACE GASS and inspect every entity, load, and combination to confirm the model is set up the way you expect. The `Save` response is a `JobStatus` whose `State.File` carries the file's `Path`, `Name`, and `Source` — useful both as a sanity check and for any tooling that needs to know where the file landed. ```csharp title="C#" var initialSave = await client.Job.Save.PostAsync( new SaveJobRequest { FilePath = @"C:\Projects\SimpleBeam.sg" }); var jobFile = initialSave?.State?.File; Console.WriteLine($" Path: {jobFile?.Path}"); Console.WriteLine($" Name: {jobFile?.Name}"); Console.WriteLine($" Source: {jobFile?.Source}"); Console.WriteLine($" IsNew: {initialSave?.State?.IsNew}"); Console.WriteLine($" IsOpen: {initialSave?.State?.IsOpen}"); ``` ```python title="Python" initial_save = await client.job.save.post( models.SaveJobRequest(file_path="C:\\Projects\\SimpleBeam.sg")) job_file = initial_save.state.file if initial_save and initial_save.state else None print(f" Path: {job_file.path if job_file else None}") print(f" Name: {job_file.name if job_file else None}") print(f" Source: {job_file.source if job_file else None}") print(f" IsNew: {initial_save.state.is_new if initial_save and initial_save.state else None}") print(f" IsOpen: {initial_save.state.is_open if initial_save and initial_save.state else None}") ``` ## Step 13 — Run a Linear Static Analysis Analyses are **asynchronous** in SPACE GASS — when you start one, the API returns immediately with a run Id and your code keeps going. Before you can query results you need to wait until the run reaches a terminal status (`Completed`, `Failed`, or `Cancelled`). The simplest way is a poll loop, shown below. For longer analyses where you want to display progress, see the [Running Analysis guide](/guides/running-analysis). ```csharp title="C#" // Start a linear static analysis. Pass an empty StaticSettingsUpdate // to use the project's current settings as-is. var run = await client.Job.Analysis.Static.RunLinear.PostAsync( new StaticSettingsUpdate()); Console.WriteLine($"Run {run.RunId} queued; waiting for completion..."); // Poll until the run reaches a terminal state AnalysisRun finalRun; while (true) { await Task.Delay(500); finalRun = await client.Job.Analysis.Runs[run.RunId.Value].GetAsync(); if (finalRun.Status is AnalysisRunStatus.Completed or AnalysisRunStatus.Failed or AnalysisRunStatus.Cancelled) { break; } } Console.WriteLine($"Analysis {finalRun.Status} in {finalRun.ElapsedTime}"); if (finalRun.Status != AnalysisRunStatus.Completed) { throw new Exception($"Analysis did not complete: {finalRun.ErrorMessage}"); } ``` ```python title="Python" import asyncio # Start a linear static analysis. Pass an empty models.StaticSettingsUpdate to # use the project's current settings as-is. run = await client.job.analysis.static.run_linear.post( models.StaticSettingsUpdate()) print(f"Run {run.run_id} queued; waiting for completion...") # Poll until the run reaches a terminal state while True: await asyncio.sleep(0.5) final_run = await client.job.analysis.runs.by_run_id(str(run.run_id)).get() if final_run.status in ( models.AnalysisRunStatus.Completed, models.AnalysisRunStatus.Failed, models.AnalysisRunStatus.Cancelled, ): break print(f"Analysis {final_run.status} in {final_run.elapsed_time}") if final_run.status != models.AnalysisRunStatus.Completed: raise RuntimeError(f"Analysis did not complete: {final_run.error_message}") ``` ## Step 14 — Query Reactions Read the support reactions under the ULS combination. By default the query returns every result for every case; the `LoadCases` query parameter filters to the load case(s) you want. The result also carries a `Warnings` object. If a requested case (or any of its constituent primaries, for combinations) has not been analysed, its Id appears in `Warnings.LoadCasesNotAnalyzed` — check this before reading `Results` so a missing run doesn't silently return zero rows. ```csharp title="C#" var reactions = await client.Job.Query.Analysis.Static.NodeReactions.GetAsync( config => config.QueryParameters.LoadCases = $"{ulsCase.Id}"); if (reactions.Warnings?.LoadCasesNotAnalyzed is { Length: > 0 } missing) { throw new Exception($"Cases not analysed: {missing}. Run the analysis first."); } foreach (var r in reactions.Results) { Console.WriteLine( $"Node {r.Node}, LC {r.LoadCase}: " + $"Fx={r.Fx:F2}, Fy={r.Fy:F2}, Fz={r.Fz:F2}"); } ``` ```python title="Python" reactions = await client.job.query.analysis.static.node_reactions.get( load_cases=str(uls_case.id)) if reactions.warnings and reactions.warnings.load_cases_not_analyzed: raise RuntimeError( f"Cases not analysed: {reactions.warnings.load_cases_not_analyzed}. " "Run the analysis first.") for r in reactions.results: print(f"Node {r.node}, LC {r.load_case}: " f"Fx={r.fx:.2f}, Fy={r.fy:.2f}, Fz={r.fz:.2f}") ``` ## Step 15 — Get the Maximum ULS Bending Moment Read the peak bending moment along the beam under the ULS combination. The result is one row per (case, member) combination; force values are returned as parallel arrays indexed by station, so element `[i]` of every array describes the same point along the member: ```text MemberIntermediateForce { Case: int // load case Id Member: int // member Id Station: int[] // station index per sample Location: float[] // distance along the member per sample Fx, Fy, Fz: float[] // force per sample Mx, My, Mz: float[] // moment per sample } ``` Filter by ULS case + the member Id and take the maximum of `|Mz|`. ```csharp title="C#" var ulsForces = await client.Job.Query.Analysis.Static.MemberIntermediateForces .GetAsync(config => { config.QueryParameters.LoadCases = $"{ulsCase.Id}"; config.QueryParameters.Members = $"{member.Id}"; }); var beamForces = ulsForces.Results.First(); var maxMz = beamForces.Mz.Max(v => Math.Abs(v ?? 0.0)); Console.WriteLine($"Max ULS bending moment on Member {member.Id}: {maxMz:F2} kNm"); ``` ```python title="Python" uls_forces = await client.job.query.analysis.static.member_intermediate_forces.get( load_cases=str(uls_case.id), members=str(member.id)) beam_forces = uls_forces.results[0] max_mz = max(abs(v) for v in beam_forces.mz if v is not None) print(f"Max ULS bending moment on Member {member.id}: {max_mz:.2f} kNm") ``` ## Step 16 — Get the SLS Midspan Deflection Read the maximum vertical deflection along the beam under the SLS combination. `MemberIntermediateDisplacement` follows the same columnar shape as Step 14 — one row per (case, member), translations as parallel arrays indexed by station: ```text MemberIntermediateDisplacement { Case: int // load case Id Member: int // member Id Station: int[] // station index per sample Location: float[] // distance along the member per sample TxLocal, TyLocal, TzLocal: float[] // translation in member-local axes TxGlobal, TyGlobal, TzGlobal: float[] // translation in global axes } ``` `TyGlobal` is the global-Y translation at each station; filter by SLS case + the member Id and take the maximum of `|TyGlobal|`. ```csharp title="C#" var slsDisplacements = await client.Job.Query.Analysis.Static.MemberIntermediateDisplacements .GetAsync(config => { config.QueryParameters.LoadCases = $"{slsCase.Id}"; config.QueryParameters.Members = $"{member.Id}"; }); var beamDisplacements = slsDisplacements.Results.First(); var maxDeflection = beamDisplacements.TyGlobal.Max(v => Math.Abs(v ?? 0.0)); Console.WriteLine($"Max SLS midspan deflection on Member {member.Id}: {maxDeflection * 1000:F2} mm"); ``` ```python title="Python" sls_displacements = await client.job.query.analysis.static.member_intermediate_displacements.get( load_cases=str(sls_case.id), members=str(member.id)) beam_displacements = sls_displacements.results[0] max_deflection = max(abs(v) for v in beam_displacements.ty_global if v is not None) print(f"Max SLS midspan deflection on Member {member.id}: {max_deflection * 1000:.2f} mm") ``` ## Step 17 — Save and Close Save persists the project to disk so you can re-open it later from SPACE GASS. Close ends the active job and releases the file. Run `Job.Close` from a `finally` (C#) or `finally`-equivalent (Python) block so the active job is always cleaned up — even if a step above threw — leaving the service ready for the next run. ```csharp title="C#" try { // ...all the steps above... await client.Job.Save.PostAsync( new SaveJobRequest { FilePath = @"C:\Projects\SimpleBeam.sg" }); } finally { // Always close the active job so the next run starts clean. try { await client.Job.Close.PostAsync(); } catch (Exception closeEx) { Console.Error.WriteLine($"Warning: failed to close job: {closeEx.Message}"); } } ``` ```python title="Python" try: # ...all the steps above... await client.job.save.post( models.SaveJobRequest(file_path="C:\\Projects\\SimpleBeam.sg")) finally: # Always close the active job so the next run starts clean. try: await client.job.close.post() except Exception as close_ex: print(f"Warning: failed to close job: {close_ex}", file=sys.stderr) ``` ## Run this example locally The complete program with error handling is in the repo as [`Example.CreateSimpleBeam`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/csharp/examples/Example.CreateSimpleBeam) (C#) and [`create_simple_beam`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/python/examples/create_simple_beam) (Python). Clone the repo and run it directly — no copy-paste: ```bash title="C#" git clone https://github.com/SpaceGass/space-gass-api.git cd space-gass-api/sdks/csharp/examples/Example.CreateSimpleBeam dotnet run ``` ```bash title="Python" git clone https://github.com/SpaceGass/space-gass-api.git cd space-gass-api/sdks/python/examples/create_simple_beam pip install space-gass-api # once per environment python create_simple_beam.py ``` The example saves the model to `~/Desktop/SpaceGass Examples/SimpleBeam.sg` so you can open it in SPACE GASS Desktop and verify the geometry / results visually. --- ## Document: Save and close cleanly Persist the active job to disk, then release it with a try / finally so the close always runs URL: /docs/examples/save-and-close # Save and close cleanly `Job.Save` writes the active job to a `.sg` file. The `filePath` works like Save As — pass any path you want; the file is created (or overwritten) at that location. Wrap the work in `try / finally` so `Job.Close` runs even if a step in the middle throws, otherwise the service is left holding an active job and the next `Open` will return `409`. The snippet below opens the `Portal Frame.SG` sample, saves it as a new file on the desktop, and closes the job — useful as a template for any "open something, save it as something else" workflow. ```csharp title="C#" using SpaceGassApi; using SpaceGassApi.Models; var savePath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "PortalFrameCopy.sg"); var client = SpaceGassApiClient.CreateClient("http://localhost:34560"); try { await client.Job.OpenSample.PostAsync( new OpenSampleRequest { FileName = "Portal Frame.SG" }); // ...modify the model however you like... await client.Job.Save.PostAsync( new SaveJobRequest { FilePath = savePath }); Console.WriteLine($"Saved to {savePath}"); } finally { await client.Job.Close.PostAsync(); } ``` ```python title="Python" import os from space_gass_api import SpaceGassApiClient import space_gass_api.models as models save_path = os.path.join( os.path.expanduser("~/Desktop"), "PortalFrameCopy.sg", ) client = SpaceGassApiClient.create_client("http://localhost:34560") try: await client.job.open_sample.post( models.OpenSampleRequest(file_name="Portal Frame.SG")) # ...modify the model however you like... await client.job.save.post( models.SaveJobRequest(file_path=save_path)) print(f"Saved to {save_path}") finally: await client.job.close.post() ``` ```bash title="curl" # Open the sample curl -X POST http://localhost:34560/api/v1/job/open-sample \ -H "Content-Type: application/json" \ -d '{"fileName": "Portal Frame.SG"}' # ...modify the model however you like... curl -X POST http://localhost:34560/api/v1/job/save \ -H "Content-Type: application/json" \ -d '{"filePath": "C:\\Users\\you\\Desktop\\PortalFrameCopy.sg"}' curl -X POST http://localhost:34560/api/v1/job/close ``` If you want to save to the path the job was originally opened from, omit `filePath` — the API saves over the original. (Sample projects opened with `Job.OpenSample` are unsaved and have no original path, so you must supply one.) ## See also - [File Handling](/guides/file-handling) — open, save, force-open, status, and the full set of file lifecycle endpoints. - [Concepts — The Active Job](/concepts#the-active-job) --- ## Document: Run a linear static analysis Start a linear static run, poll for completion, and read the result status URL: /docs/examples/run-linear-static-analysis # Run a linear static analysis Analysis is asynchronous — `POST /run-linear` starts it and returns a `runId` immediately, then you poll `GET /runs/{runId}` until the status reaches a terminal state (`Completed`, `Failed`, or `Cancelled`). The snippet below opens your project, runs analysis with the current settings, polls every half-second, and prints a one-line summary on completion. The empty `StaticSettingsUpdate()` body means "use whatever settings the job already has" — pass non-null fields to override, including a selection of the load cases and combinations you would like to run. ```csharp title="C#" using SpaceGassApi; using SpaceGassApi.Models; var client = SpaceGassApiClient.CreateClient("http://localhost:34560"); try { await client.Job.OpenSample.PostAsync( new OpenSampleRequest { FileName = "Portal Frame.SG" }); var run = await client.Job.Analysis.Static.RunLinear.PostAsync( new StaticSettingsUpdate()); while (true) { await Task.Delay(500); var status = await client.Job.Analysis.Runs[run!.RunId!.Value].GetAsync(); if (status!.Status is AnalysisRunStatus.Completed or AnalysisRunStatus.Failed or AnalysisRunStatus.Cancelled) { Console.WriteLine($"{status.Status} in {status.ElapsedTime}"); break; } } } finally { await client.Job.Close.PostAsync(); } ``` ```python title="Python" import asyncio from space_gass_api import SpaceGassApiClient import space_gass_api.models as models client = SpaceGassApiClient.create_client("http://localhost:34560") try: await client.job.open_sample.post( models.OpenSampleRequest(file_name="Portal Frame.SG")) run = await client.job.analysis.static.run_linear.post( models.StaticSettingsUpdate()) while True: await asyncio.sleep(0.5) status = await client.job.analysis.runs.by_run_id(str(run.run_id)).get() if status.status in ( models.AnalysisRunStatus.Completed, models.AnalysisRunStatus.Failed, models.AnalysisRunStatus.Cancelled, ): print(f"{status.status} in {status.elapsed_time}") break finally: await client.job.close.post() ``` ## Run this example locally A richer version with progress polling, a reusable `WaitForCompletion` helper, and a result summary is in the repo as [`Example.RunAnalysis`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/csharp/examples/Example.RunAnalysis) (C#) and [`run_analysis`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/python/examples/run_analysis) (Python). Clone and run: ```bash title="C#" git clone https://github.com/SpaceGass/space-gass-api.git cd space-gass-api/sdks/csharp/examples/Example.RunAnalysis dotnet run ``` ```bash title="Python" git clone https://github.com/SpaceGass/space-gass-api.git cd space-gass-api/sdks/python/examples/run_analysis pip install space-gass-api # once per environment python run_analysis.py ``` Edit the example's `project_file_path` constant first to point at a `.sg` file with structure and loads defined. ## See also - [Running Analysis](/guides/running-analysis) — the full pattern with a reusable `WaitForCompletion` helper and richer progress display. - [Concepts — Analysis Runs](/concepts#analysis-runs) --- ## Document: Read reactions for restrained nodes only Filter nodes to those with restraints, then read reactions for just those nodes URL: /docs/examples/reactions-for-restrained-nodes # Read reactions for restrained nodes only Common after an analysis: you only care about reactions at supports, not internal nodes. Two-step: filter nodes to `NodeType=Restrained` to get the support node Ids, then pass them as the `Nodes` filter on the reactions endpoint. `Nodes` accepts SG list format — comma-separated Ids and dash ranges (`"1,3-7,10"`). Build it with `string.Join(",", ids)` (C#) or `",".join(...)` (Python). The snippet below opens the `Portal Frame.SG` sample, runs a linear static analysis to populate reactions, queries restrained nodes, then prints their reactions. `try / finally` closes the job even if a step in the middle throws. ```csharp title="C#" using SpaceGassApi; using SpaceGassApi.Models; var client = SpaceGassApiClient.CreateClient("http://localhost:34560"); try { await client.Job.OpenSample.PostAsync( new OpenSampleRequest { FileName = "Portal Frame.SG" }); // Run analysis so reactions are available. var run = await client.Job.Analysis.Static.RunLinear.PostAsync( new StaticSettingsUpdate()); while (true) { await Task.Delay(500); var s = await client.Job.Analysis.Runs[run!.RunId!.Value].GetAsync(); if (s!.Status is AnalysisRunStatus.Completed or AnalysisRunStatus.Failed or AnalysisRunStatus.Cancelled) break; } // 1. Get the restrained nodes var restrained = await client.Job.Structure.Nodes.GetAsync( config => config.QueryParameters.NodeType = NodeTypeFilter.Restrained); var nodeFilter = string.Join(",", restrained!.Where(n => n.Id != null).Select(n => n.Id!.Value)); // 2. Read reactions filtered to those nodes var result = await client.Job.Query.Analysis.Static.NodeReactions.GetAsync( config => config.QueryParameters.Nodes = nodeFilter); foreach (var r in result!.Results!) { Console.WriteLine( $"Node {r.Node}, Case {r.LoadCase}: " + $"Fx={r.Fx:F2}, Fy={r.Fy:F2}, Fz={r.Fz:F2}"); } } finally { await client.Job.Close.PostAsync(); } ``` ```python title="Python" import asyncio from space_gass_api import SpaceGassApiClient import space_gass_api.models as models client = SpaceGassApiClient.create_client("http://localhost:34560") try: await client.job.open_sample.post( models.OpenSampleRequest(file_name="Portal Frame.SG")) # Run analysis so reactions are available. run = await client.job.analysis.static.run_linear.post( models.StaticSettingsUpdate()) while True: await asyncio.sleep(0.5) s = await client.job.analysis.runs.by_run_id(str(run.run_id)).get() if s.status in ( models.AnalysisRunStatus.Completed, models.AnalysisRunStatus.Failed, models.AnalysisRunStatus.Cancelled, ): break # 1. Get the restrained nodes restrained = await client.job.structure.nodes.get( node_type=models.NodeTypeFilter.Restrained) node_filter = ",".join(str(n.id) for n in restrained if n.id is not None) # 2. Read reactions filtered to those nodes result = await client.job.query.analysis.static.node_reactions.get( nodes=node_filter) for r in result.results: print(f"Node {r.node}, Case {r.load_case}: " f"Fx={r.fx:.2f}, Fy={r.fy:.2f}, Fz={r.fz:.2f}") finally: await client.job.close.post() ``` ```bash title="curl" # 1. Restrained nodes curl "http://localhost:34560/api/v1/job/structure/nodes?nodeType=Restrained" # 2. Reactions for the returned Ids (substitute below) curl "http://localhost:34560/api/v1/job/query/analysis/static/node-reactions?nodes=1,2,3" ``` ## Run this example locally The same code (against your own .sg file rather than the sample) is in the repo as [`Example.QueryRestrainedNodes`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/csharp/examples/Example.QueryRestrainedNodes) (C#) and [`query_restrained_nodes`](https://github.com/SpaceGass/space-gass-api/tree/main/sdks/python/examples/query_restrained_nodes) (Python). Clone and run: ```bash title="C#" git clone https://github.com/SpaceGass/space-gass-api.git cd space-gass-api/sdks/csharp/examples/Example.QueryRestrainedNodes dotnet run ``` ```bash title="Python" git clone https://github.com/SpaceGass/space-gass-api.git cd space-gass-api/sdks/python/examples/query_restrained_nodes pip install space-gass-api # once per environment python query_restrained_nodes.py ``` Edit the example's `PROJECT_FILE_PATH` constant first to point at an analysed `.sg` file. ## See also - [Filtering & Querying](/guides/filtering-and-querying) — full SG-list filter syntax and pagination. --- ## Document: Open your own .sg file Load a SPACE GASS project from disk into the active job URL: /docs/examples/open-your-own-file # Open your own .sg file The [Quick Start](/quick-start) uses `Job.OpenSample` to load a built-in sample so you don't need a project file. Once you have your own `.sg` file, switch to `Job.Open` — same shape, but you supply the path. ## Check the file status first Before opening, call `File.Status` to find out whether the file is in a safe state. The response includes `CanOpenSafely` (a simple boolean), a human-readable `Description`, and a `RecommendedAction` string you can show to the user if something is wrong. ```csharp title="C#" using SpaceGassApi; using SpaceGassApi.Models; var client = SpaceGassApiClient.CreateClient("http://localhost:34560"); var status = await client.File.Status.GetAsync( config => config.QueryParameters.FilePath = @"C:\Projects\MyStructure.sg"); Console.WriteLine($"Status: {status!.Status}"); Console.WriteLine($"Description: {status.Description}"); Console.WriteLine($"Can open safely: {status.CanOpenSafely}"); if (status.CanOpenSafely != true) { Console.WriteLine($"Recommended action: {status.RecommendedAction}"); } ``` ```python title="Python" from space_gass_api import SpaceGassApiClient import space_gass_api.models as models client = SpaceGassApiClient.create_client("http://localhost:34560") status = await client.file.status.get( file_path=r"C:\Projects\MyStructure.sg") print(f"Status: {status.status}") print(f"Description: {status.description}") print(f"Can open safely: {status.can_open_safely}") if not status.can_open_safely: print(f"Recommended action: {status.recommended_action}") ``` ```bash title="curl" curl "http://localhost:34560/api/v1/file/status?filePath=C%3A%5CProjects%5CMyStructure.sg" ``` ## Open the file `Job.Open` returns `409 Conflict` if a job is already open — close it first with `Job.Close`. If the file has unsaved temporary files from a previous crashed session, set `ForceOption` on the request body to either `OpenPreviousSaved` (discard the unsaved changes) or `OpenUnsavedMostRecent` (recover them). ```csharp title="C#" using SpaceGassApi; using SpaceGassApi.Models; var client = SpaceGassApiClient.CreateClient("http://localhost:34560"); try { await client.Job.Open.PostAsync( new OpenJobRequest { FilePath = @"C:\Projects\MyStructure.sg" }); // ...do work on the open job... } finally { await client.Job.Close.PostAsync(); } ``` ```python title="Python" from space_gass_api import SpaceGassApiClient import space_gass_api.models as models client = SpaceGassApiClient.create_client("http://localhost:34560") try: await client.job.open.post( models.OpenJobRequest(file_path=r"C:\Projects\MyStructure.sg")) # ...do work on the open job... finally: await client.job.close.post() ``` ```bash title="curl" curl -X POST http://localhost:34560/api/v1/job/open \ -H "Content-Type: application/json" \ -d '{"filePath": "C:\\Projects\\MyStructure.sg"}' # ...calls against the open job... curl -X POST http://localhost:34560/api/v1/job/close ``` ## See also - [Concepts — The Active Job](/concepts#the-active-job) - [File Handling](/guides/file-handling) — full set of open / save / status patterns. --- ## Document: Filter analysis results by load case Read reactions or member forces for one specific case (or set of cases) using SG list format URL: /docs/examples/filter-results-by-case # Filter analysis results by load case Every result endpoint accepts a `LoadCases` query parameter to restrict the response to a subset of analysed cases. The format is SG's list syntax — comma-separated Ids and dash ranges in a single string: | Filter | Meaning | |---|---| | `"1"` | Just case 1 | | `"1,3,7"` | Cases 1, 3, and 7 | | `"1,3-7,10"` | Case 1, 3 through 7 inclusive, and 10 | | `""` or omit | All analysed cases | The same format applies to `Nodes`, `Members`, `Modes`, and any other list filter. The snippet below opens the `Portal Frame.SG` sample, runs a linear static analysis, then reads node reactions filtered to load case `1`. Adjust the case Id to match a case that exists in your model. `try / finally` closes the job at the end even if a step in the middle throws. ```csharp title="C#" using SpaceGassApi; using SpaceGassApi.Models; var client = SpaceGassApiClient.CreateClient("http://localhost:34560"); try { await client.Job.OpenSample.PostAsync( new OpenSampleRequest { FileName = "Portal Frame.SG" }); var run = await client.Job.Analysis.Static.RunLinear.PostAsync( new StaticSettingsUpdate()); while (true) { await Task.Delay(500); var s = await client.Job.Analysis.Runs[run!.RunId!.Value].GetAsync(); if (s!.Status is AnalysisRunStatus.Completed or AnalysisRunStatus.Failed or AnalysisRunStatus.Cancelled) break; } var reactions = await client.Job.Query.Analysis.Static.NodeReactions.GetAsync( config => config.QueryParameters.LoadCases = "1"); if (reactions!.Warnings?.LoadCasesNotAnalyzed is { Length: > 0 } missing) { throw new Exception($"Cases not analysed: {missing}. Run the analysis first."); } foreach (var r in reactions.Results!) { Console.WriteLine($"Node {r.Node}: Fx={r.Fx:F2}, Fy={r.Fy:F2}, Fz={r.Fz:F2}"); } } finally { await client.Job.Close.PostAsync(); } ``` ```python title="Python" import asyncio from space_gass_api import SpaceGassApiClient import space_gass_api.models as models client = SpaceGassApiClient.create_client("http://localhost:34560") try: await client.job.open_sample.post( models.OpenSampleRequest(file_name="Portal Frame.SG")) run = await client.job.analysis.static.run_linear.post( models.StaticSettingsUpdate()) while True: await asyncio.sleep(0.5) s = await client.job.analysis.runs.by_run_id(str(run.run_id)).get() if s.status in ( models.AnalysisRunStatus.Completed, models.AnalysisRunStatus.Failed, models.AnalysisRunStatus.Cancelled, ): break reactions = await client.job.query.analysis.static.node_reactions.get( load_cases="1") if reactions.warnings and reactions.warnings.load_cases_not_analyzed: raise RuntimeError( f"Cases not analysed: {reactions.warnings.load_cases_not_analyzed}. " "Run the analysis first.") for r in reactions.results: print(f"Node {r.node}: Fx={r.fx:.2f}, Fy={r.fy:.2f}, Fz={r.fz:.2f}") finally: await client.job.close.post() ``` ```bash title="curl" # Single case curl "http://localhost:34560/api/v1/job/query/analysis/static/node-reactions?loadCases=10" # Multiple cases via SG list curl "http://localhost:34560/api/v1/job/query/analysis/static/node-reactions?loadCases=1,3-7,10" ``` ### Always check `Warnings.LoadCasesNotAnalyzed` If you ask for a case that hasn't been analysed (or, for a combination, any of its primary cases), it appears in `Warnings.LoadCasesNotAnalyzed` and silently returns no rows for that case. Check the warning before reading `Results` so a missing run doesn't look like clean data. ## See also - [Filtering & Querying](/guides/filtering-and-querying) - [Concepts — Analysis Runs](/concepts#analysis-runs)