Skip to content

ps.bundle

ps.bundle.BundleRefusal(reason: str, message: str, *, file: str, schema: str | None = None, sheet: str | None = None, column: str | None = None, columns: list[str] | None = None, row: Any = None, given: str | None = None, expected: str | None = None) -> None

A typed refusal at the bundle door: its code, its class and where in the bundle it is.

except ps.bundle.BundleRefusal as error: catches a refused bundle; error.reason is its code (bundle.*) and error.code the same.

Every field is data a program can act on: the reason code, its class (kind: one of unit, column, key, vocabulary, shape, reference, range, grid, recipe and schedule), the schema id, the sheet, the column (and columns, every column a unit refusal names at once), the row key, and what was given and what was expected. The message is for a person and names the problem, the expected value and what was sent; the fields are for the program that answers it. It is a ValueError and a ps.PetroScriptError.

ps.bundle.Recipe(*, recipe_version: Literal['0.1'], units: Literal['METRIC', 'FIELD'], title: Optional[Annotated[str, 'title']] = None, start: Annotated[str, 'date'], history, forecast, phases: list[str] | None = None, selected_deck: Optional[Annotated[str, 'line']] = None, grid=None, sheets: list[str], schedule, options=None, outputs=None, ensemble=None) -> None

recipe.json, the one file that says what a bundle is.

Strict: every key has its type, a required key missing, a key it does not know at any depth, a wrong type, a bad date and a word outside a key’s vocabulary each refuse by name with the dotted path (bundle.recipe.*) before the assembler reads a key. The door keeps the recipe as the JSON it read; recipe_schema() is this model, read.

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object’s (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list. For example: dict(one=1, two=2)

ps.bundle.SheetSchema(kind: str, version: str, columns: tuple[Column, ...] | list[Column], doc: str = '') -> None

A registered sheet: its id, its columns, and nothing a deck would not need.

kind : str The sheet’s name (wells, history_panels): the stem of its file under tables/. version : str The schema’s version (0.1); a recipe or a file names the schema kind@version. columns : tuple The sheet’s columns, in the order it states them: each one’s name, dtype, units per unit system, its key and nullable flags and its vocabulary. Given as a list, they are held as a tuple. doc : str What the sheet states and how the door reads it.

ps.bundle.assemble(bundle, *, member: int | None = None) -> Model

Assemble the bundle into a ps.Model: every sheet and grid file as statements.

With no member the base case, exactly as the sheets state it. With member=i (1..N, N the recipe’s ensemble.members) the i-th member of the ensemble: the base case with that member’s draws from the uncertainty sheet applied (a multiplier over its target’s cells, kv_kh as PERMZ = ratio x PERMX, a contact shift added to the contacts row, an operations draw written into the producer control rows it targets: the THP floor, its start month, the uptime, the retest interval), deterministically, the same member for the same seed; ps.bundle.member_draws states every member’s numbers.

The grid files are read into the model (a corner-point geometry, the property and region arrays, each stamped with its file and checksum) and render inline, so the sealed deck is one self-contained file. The PVT and SCAL sheets become the tables of the family the recipe’s phases name, gas-water or oil-gas-water; contacts becomes EQUIL, one record per region, each stamped sheet:<id> row=<region>; wells and completions become the wells, a well’s head cell the (i, j) of its earliest completion by date and layer, each cell declared at the start as the sheet has it over the start’s month and each later change of its state a COMPDAT under the DATES of the month it holds from; faults becomes FAULTS and MULTFLT; the schedule sheets enter as dated statements, with those dated COMPDAT records among them and a producer’s THP limit through the lift_tables table its row names; tubing is read through the door and its wells checked against the wells sheet, and nothing renders from it (a deck carries a lift table, never tubing); the observables are the field totals and pressure and each well’s totals. Nothing is defaulted from a seed.

bundle : Bundle, str or Path An opened bundle, or the path ps.bundle.open takes. member : int, optional The ensemble member to assemble, 1 to the recipe’s ensemble.members; None assembles the base case.

Model The model the sheets state, in the recipe’s unit system.

BundleRefusal A sheet the model needs that the recipe does not name (bundle.assemble.sheet_unnamed) or whose file is gone (bundle.assemble.sheet_missing), a region file naming a region no sheet states (bundle.assemble.region_unstated), a well with no completion (bundle.assemble.well.no_completion), a table family that contradicts the phases (bundle.assemble.phases_mismatch), a phase set this version does not spell, or a PVT sheet stating any region but a lone region 1 (bundle.assemble.fluids.unsupported), a grid file this version does not read (bundle.assemble.grid.unsupported), a property array the model needs that the recipe does not name (bundle.assemble.grid.property_unnamed), a member outside the ensemble (bundle.recipe.ensemble), and a model the model layer refuses, a contradiction (bundle.model.invalid, its first row’s code as column) or an input (bundle.model.input). A path given is opened first, with ps.bundle.open’s refusals, and each sheet read refuses as Bundle.sheet does.

ps.bundle.check_fluids(path)

Ask the PVT sheets what the fluids weigh where the model starts, and report it.

The report states the family, how it was decided, and per equilibration region the datum pressure, the saturated solution ratio each table carries there and the derived phase densities (water from its compressibility branch, the hydrocarbons from each table’s saturated curve read linearly between its nodes), with the formula and the unit words beside every number. Nothing is extrapolated: a datum pressure outside a table’s nodes gives no number, the reason and a warnings line. Each region with oil states saturation_pressure, the datum pressure the door saturates it at, and an oil region whose gas-oil contact lies above its active depth span (read off the grid, as the assembler reads it) is a warnings line: no gas cap is in the grid, and a bubble point below the datum is not stated. A report accepts nothing and refuses nothing about the numbers it derives; a hydrocarbon heavier than its water at the datum is a warning here and refuses through assemble (bundle.pvt.density_order), where the two numbers decide the whole initial state. Every warnings line is also an issues row (severity, code, message) a program can act on.

path : str, Path or Bundle The workspace or its sidecars directory, or a bundle already opened: a caller that holds one for assemble hands it in, and nothing is opened twice.

dict The report: the family, each region’s numbers with their formulas and unit words, warnings (lines) and issues (rows of severity, code and message).

BundleRefusal A sheet the report reads that the recipe does not name, or whose file is gone, refuses as it does through assemble (bundle.assemble.sheet_unnamed, bundle.assemble.sheet_missing), a set of phases no family spells refuses bundle.assemble.fluids.unsupported, and a formation-volume factor at or below 0 on any row of a table it reads, which every density is divided by, refuses bundle.model.invalid with the model layer’s rule id. A path given is opened first, with ps.bundle.open’s refusals.

ps.bundle.check_grid(path, *, geometry: str = 'sidecars/grid/GRID.GRDECL', properties: tuple[str, ...] | list[str] = (), regions: tuple[str, ...] | list[str] = (), contacts=None)

Check a grid alone, with no sheets and no recipe, and report what it measures.

It runs the checks the assembler runs before it reads a sheet (the geometry reads as corner-point, every active cell has thickness and a positive depth, every named array holds one value per cell, at least one cell is active) with the same typed refusals, and when they pass returns a report of raw measurements: dims, active, active_per_layer, thickness min and max over the active cells, the active depth span, the footprint (x and y extent, active area in km2), the median cell size, how well laterally adjacent cells agree at the corners they share, and warnings (more than half of the shared corners disagreeing usually means a ZCORN written in the wrong order: a warning, never a refusal, since faults are real). A quiet report accepts nothing about the geometry. layout states how a property array is laid out (one value per cell of the whole grid, I fastest, then J, then K), and properties carries, per array named in properties=, its mean over the active cells along K (by_layer), along J (by_row) and along I (by_column), eight equal-count bins where an axis runs past 64: an index inside the grid proves nothing about placement, and an array written transposed or in the wrong order reads without error and shows here as the gradient the engineer put in one axis appearing in another.

A bundle already opened is checked with its own recipe’s grid keys, but for a key the call states (a geometry equal to the default and an empty properties or regions read as not stated: the recipe’s key stands; pass the path to check without the recipe). Every warnings line is also an issues row (severity, code, message) a program can act on.

path : str, Path or Bundle The workspace or its sidecars directory, or a bundle already opened. geometry : str, optional The geometry’s GRDECL file, as the recipe’s grid.geometry key names it. Left out, sidecars/grid/GRID.GRDECL, or an opened bundle’s key where its recipe states one. properties : list of str, optional The property arrays to read and measure (["PORO", "PERMX"]), as grid.properties. Left out, none, or an opened bundle’s key where its recipe states one. regions : list of str, optional The region arrays to read (["EQLNUM"]), as grid.regions. Left out, none, or an opened bundle’s key where its recipe states one. contacts : str or Path, optional A written contacts sheet (sidecars/tables/contacts.parquet, or a path): it is validated from its own stamp, a datum or contact the bundle reads as an elevation is a warnings line (bundle.contacts.outside_grid: Flow runs it as written), and the report gains contacts: each stated contact and datum against the active depth span, of its own region when regions names EQLNUM, above, inside or below.

dict The report: the measurements above by name, warnings (lines) and issues (rows of severity, code and message).

BundleRefusal bundle.sheet.missing when contacts is not a regular file, and bundle.schema.mismatch when it is a sheet of another kind (the file is validated as a sheet from its own stamp first, with a sheet’s refusals). The grid’s checks refuse with bundle.assemble.grid.unsupported (the geometry does not read as corner-point, or a named array’s file is absent, unreadable, defaulted or of the wrong size), bundle.grid.depth (an active cell at or above zero depth), bundle.grid.degenerate (an active cell with no thickness) and bundle.grid.inactive (no cell is active).

ps.bundle.export_schemas()

Return the sheet registry as the one JSON the bundle’s readers take (SCHEMAS.json).

ps.bundle.export_schemas() is every registered sheet’s schema: its kind, doc, key and columns (each one’s name, dtype, units per unit system, nullable and key flags and vocabulary).

dict recipe_version, unit_systems, schemas (each schema by its id, kind@version), vocabularies and recipe_fields; json.dumps-able.

ps.bundle.family_of(bundle) -> str

Return the fluid family the assembler reads off the recipe’s phases and the PVT sheets named.

It is gas_water (the phases gas and water), three_phase (oil, gas and water, the recipe naming no wet-gas pvt_pvtg sheet, so the gas is the dry-gas pvt_pvdg sheet) or gas_condensate (oil, gas and water with the wet-gas pvt_pvtg sheet: the oil is the condensate the gas carries, and the deck produces gas, water and condensate).

bundle : Bundle, str or Path An opened bundle, or the path ps.bundle.open takes.

str "gas_water", "three_phase" or "gas_condensate".

BundleRefusal bundle.assemble.fluids.unsupported for any other set of phases; a path given is opened first, with ps.bundle.open’s refusals.

ps.bundle.member_draws(bundle)

Return every member’s draws as one polars frame: the numbers its deck was rendered from.

The draw is a Latin hypercube over each uncertainty row’s box: the members are dealt the N strata by a permutation seeded by (seed, row), and each sits at a point of its stratum seeded by (seed, row, member), so twenty members cover the box where twenty independent draws may not, the same seed gives the same numbers, and the population is the row’s box from low to high, uniform in log10 for a log-uniform row.

bundle : Bundle, str or Path An opened bundle, or the path ps.bundle.open takes.

polars.DataFrame Columns member, parameter, target, value and units: one row per member and uncertainty row, members first, each member’s rows in the sheet’s (parameter, target) order; a value is in its row’s units.

BundleRefusal bundle.recipe.ensemble_missing when the recipe draws no ensemble. A path given is opened first, with ps.bundle.open’s refusals, and the uncertainty sheet is read as Bundle.sheet reads it, with its refusals.

ps.bundle.open(path) -> Bundle

Open the bundle at path (the workspace, or its sidecars directory).

A bundle is a model stated as sheets: recipe.json says what it is, each sheet is a parquet file under tables/ and the grid files sit under grid/. ps.bundle.open("workspace") opens the bundle there; ps.bundle.assemble builds its ps.Model.

path : str or Path The folder holding recipe.json, or the folder whose sidecars directory holds it.

Bundle The opened bundle: its recipe checked, each sheet read and checked when it is first asked for (bundle.sheet("wells")).

BundleRefusal bundle.recipe.missing when neither path nor its sidecars directory holds a recipe.json, and bundle.recipe.unreadable when that file is not JSON. The recipe is checked before the bundle opens, each fault by its code: its shape under bundle.recipe.* (a key missing, unknown or of the wrong type, a date malformed or out of order, a word outside its key’s vocabulary), a sheet with no registered schema (bundle.sheet.unknown) or whose tables/<kind>.parquet is absent (bundle.sheet.missing), a sheet needing a phase the recipe’s phases do not state (bundle.recipe.phase_disagrees), a grid file it names that is absent (bundle.grid.missing), and an ensemble of fewer than two members or whose uncertainty names no uncertainty sheet the recipe lists (bundle.recipe.ensemble).

ps.bundle.params_keys_doc() -> str

Return the params keys each schedule event class renders, as one text.

Each event class with the numeric params keys it renders (each with its units twin, bhp_limit beside bhp_limit_units), then the producer’s rate targets with the control mode each selects and the unit words a rate or a pressure takes.

str One paragraph: <event class>: <keys> for each class, joined by semicolons, then the producer’s targets and the unit words.

ps.bundle.recipe_schema()

Return the recipe as a table a caller reads before writing one (RECIPE.json).

It holds every key with its type, whether it is required, one line of meaning and an example, plus one minimal recipe that assembles. ps.bundle.recipe_schema() lists every recipe key with its type and meaning.

dict recipe_version, keys (one row per key: path, type, required, doc, example) and example, a whole recipe; json.dumps-able.

PetroScript is licensed under the Apache License 2.0. © 2026 Blauweiss EDV LLC. PetroScript by Blauweiss AI.For AI agents: llms.txt