This release is a pre-release and may not be stable for production use.
DistOPF
DistOPF provides an open-source, multi-phase, unbalanced, optimal power flow (OPF) tool for distribution systems to aid students and researchers. The tool aids users by providing:
-
Unbalanced multi-Phase OPF model generators usable with common Python solver packages such as CVXPY and SciPy;
-
A platform for creating and benchmarking new algorithms on a set of standard test systems;
-
An OpenDSS model importer allowing users to import power system models directly from the OpenDSS model format;
-
Model validation with OpenDSS;
-
Functions for visualizing results.
The tool is composed of four major parts, 1) model input system, 2) optimization model formulation, 3) OPF solver interface, and 4) solution output and visualization. Models are described using a set of CSV files that are read in as Pandas DataFrames. Buses are described in one CSV having columns for loads, base units, and voltage limits. Lines, switches, and transformers are described in a CSV having columns for each term in the upper diagonal impedance matrix. Regulators, capacitor banks, and generators each have their own CSV. To aid in model creation and validation, models can also be created using OpenDSS and converted to the tabular format. The tool provides classes and functions to make it easy to formulate and solve the power system for new users while being flexible for advanced users to create new models and algorithms. The tool has been used to solve a variety of problems including, conservation voltage reduction, power loss minimization, and generation curtailment minimization, where either generator real or reactive power injections are controlled.
Installation
pip install
pip install distopf
Optional Extras
| Extra | Install command | Purpose |
|---|---|---|
cim |
pip install distopf[cim] |
CIM XML file import support (requires pre-release packages cim-graph and gridappsd-python) |
Note: The
cimextra depends on pre-release packages. If you do not need CIM file support, a plainpip install distopfis sufficient. CIM functionality will raise an informativeImportErrorif those packages are not installed.
Developer Installation
To install the latest version from github:
- From the directory you want to keep your DistOPF files, run:
git clone https://github.com/nathantgray/distopf.git
- Create or activate the python environment you want to use.
- From the directory where the DistOPF package is stored, run:
pip install -e .
To also install the optional CIM extra in editable mode:
pip install -e ".[cim]"
This installs your local DistOPF package the python environment you activated. The -e option enables editable
mode, which allows you to directly edit the package and see changes immediately reflected in your environment
without reinstalling.
Getting Started
Using provided cases:
Unconstrained Power Flow
import distopf as opf
case = opf.create_case(opf.CASES_DIR / "csv" / "ieee123")
result = case.run_pf()
result.plot_network().show(renderer="browser")
DER Curtailment Minimization
import distopf as opf
case = opf.create_case(opf.CASES_DIR / "csv" / "ieee123_30der")
case.modify(v_max=1.05, v_min=0.95, gen_mult=10)
result = case.run_opf(objective="curtail_min", control_variable="P")
result.plot_network().show(renderer="browser")
Spatial ENAPP and ADMM solvers
The spatial solvers are available through the public Case.run_enapp() and Case.run_admm() methods. Supply an area topology as a JSON-safe dictionary keyed by area name. Each area must define up_areas, down_areas, and exactly one up_buses entry; bus names must match the selected case.
area_info = {
"area1": {
"up_areas": [],
"down_areas": ["area2"],
"up_buses": ["sourcebus"],
"down_buses": ["632"],
},
"area2": {
"up_areas": ["area1"],
"down_areas": [],
"up_buses": ["632"],
"down_buses": [],
},
}
case = opf.create_case(opf.CASES_DIR / "csv" / "ieee13")
result = case.run_enapp(
area_info=area_info,
objective="substation_power",
parallel=True,
max_iterations=25,
)
Set parallel=True to solve the independent area subproblems with multiprocessing. The coordination iterations themselves remain sequential: each iteration waits for all area solves before exchanging boundary messages. Use parallel=False for serial execution or when debugging.
Runs made through these methods are recorded by record_call(). Calling PowerFlowResult.save() writes run_config.json, including the distributed solver, topology, and parallel setting, so named-objective runs can be reproduced with replay():
result.save("results/ieee13-enapp")
replayed = opf.replay("results/ieee13-enapp/run_config.json")
Replay preserves the recorded parallel setting for ENAPP and ADMM. Parallel replay requires a normal multiprocessing-capable execution environment; arbitrary custom objective callables and solve/iteration callbacks remain non-replayable because they cannot be reconstructed from JSON.
Command-line interface
The installed distopf command provides human-readable and JSON output for saved runs, user-authored TOML scenarios, and result comparisons. Run distopf --help or any subcommand with --help for the complete option list.
distopf inspect results/ieee13-enapp/run_config.json
distopf validate results/ieee13-enapp/run_config.json
distopf run results/ieee13-enapp/run_config.json --output-dir results/replayed
Public commands are:
run CONFIG [--output-dir DIR] [--verbose] [--json]runs a JSONrun_config.jsonreplay artifact or a TOML scenario. Scenario analysis types arepf/power_flow,fbs,opf,enapp, andadmm.inspect CONFIG [--json]displays scenario or replay metadata without solving.validate CONFIG [--json]checks configuration structure, referenced paths, replayability, and case data.replay-exact-power-flow INPUT_PATH [OUTPUT_PATH] [--overwrite] [--json]replays saved OPF setpoints through the exact FBS power flow. The output defaults toINPUT_PATH/exact; existing output is preserved unless--overwriteis supplied.compare LEFT_FOLDER RIGHT_FOLDER [--output-dir DIR] [--nominal-voltage VALUE] [--json]compares common result CSV tables and writescomparison.jsonplus difference CSVs.--nominal-voltagedefaults to1.0and is used for voltage metrics.compare-exact FOLDER [RIGHT_FOLDER] [--batch] [--depth N] [--workers N] [--output-dir DIR] [--nominal-voltage VALUE] [--json]compares a result folder with itsexactFBS replay, creating that replay when needed. With--batch, it processes directories exactly--depthlevels belowFOLDER;RIGHT_FOLDERcannot be combined with--batch.
Add --json to any command for machine-readable output. Successful commands return 0; validation failures return 1; run and comparison runtime failures return 2.
Use distopf run --help for the full scenario format. A minimal scenario.toml is:
[case]
path = "../cases/csv/ieee13"
[analysis]
type = "opf"
objective = "loss"
TOML scenarios require only case.path and analysis.type; pf/power_flow, fbs, opf, enapp, and admm are supported. Case options go directly under [case], modifications under [case.modifications], and ENAPP/ADMM topology under [analysis.area_info.<area>]. The version field is optional and defaults to 1.
JSON run_config.json files generated by PowerFlowResult.save() remain supported as exact replay artifacts. Use --json on any subcommand for automation. inspect reads metadata without solving. validate checks the schema, referenced case directory, replayability, and case data. run executes either format and can persist result CSV files and metadata with --output-dir. Successful commands return exit code 0; validation failures return 1; run/runtime failures return 2.
Optimization Wrappers
DistOPF supports multiple optimization wrappers for solving OPF problems:
Pyomo Wrapper — LinDistFlow (default)
The default Pyomo wrapper uses the LinDistFlow model (formulation="lindist").
- Model: Linear approximation of power flow equations
- Solver: Pyomo with linear solvers
- Speed: Fast, suitable for real-time applications
- Accuracy: Good for systems with small voltage deviations
import distopf as opf
case = opf.create_case(opf.CASES_DIR / "csv" / "ieee123_30der")
result = case.run_opf(wrapper="pyomo", objective="loss") # formulation="lindist" is the default
Pyomo Wrapper — BranchFlow
The BranchFlow model type uses nonlinear power flow equations with IPOPT or MINLP solvers for higher accuracy.
Use formulation="branchflow".
- Model: Nonlinear power flow equations (exact)
- Solver: IPOPT (continuous) or MINLP using Gurobi if installed (discrete controls)
- Speed: Slower than linear, but more accurate
- Accuracy: Nonlinear power flow representation
Pyomo Wrapper — SOCP Relaxation
The SOCP formulation is a relaxation of the nonlinear BranchFlow model. It uses the same
Pyomo/IPOPT solve path, but replaces the two branch-current equality constraints with
inequality constraints. Use formulation="socp".
- Model: BranchFlow model with relaxed current constraints
- Solver: IPOPT for continuous problems; MINLP solvers for discrete controls
- Use case: Faster or more robust optimization when exact current equalities are not required
- Wrapper: Pyomo only; it is not available through the matrix wrappers
import distopf as opf
case = opf.create_case(opf.CASES_DIR / "csv" / "ieee13")
result = case.run_opf(
formulation="socp",
objective="loss",
solver="ipopt",
)
Continuous Optimization (IPOPT)
For continuous optimization without discrete controls:
import distopf as opf
case = opf.create_case(opf.CASES_DIR / "csv" / "ieee123_30der")
result = case.run_opf(
formulation="branchflow",
objective="loss",
solver="ipopt",
)
Discrete Controls (MINLP)
Enable regulator tap optimization and capacitor switching with MINLP solvers:
import distopf as opf
case = opf.create_case(opf.CASES_DIR / "csv" / "ieee123")
result = case.run_opf(
wrapper="pyomo",
formulation="branchflow",
objective="loss",
control_regulators=True, # Enable regulator tap control
control_capacitors=True, # Enable capacitor switching
initialize="fbs", # Recommended for discrete controls
solver="gurobi", # MINLP compatible solver
)
Matrix BESS Wrapper (Multi-Period with Batteries)
The matrix_bess wrapper supports multi-period (time-series) optimization with battery energy storage.
import distopf as opf
case = opf.create_case(opf.CASES_DIR / "csv" / "ieee123_bat")
result = case.run_opf(wrapper="matrix_bess", objective="loss")
Wrapper Comparison
| Feature | Matrix | Matrix BESS | Pyomo |
|---|---|---|---|
| Primary role | Single-period convex LinDistFlow OPF | Multi-period LinDistFlow OPF with batteries and schedules | LinDistFlow, BranchFlow, or SOCP-relaxed BranchFlow |
| Solver API | SciPy or CVXPY | CVXPY | Pyomo |
| Default solver path | CVXPY with CLARABEL | CVXPY with CLARABEL | IPOPT |
| Other supported solver choices | Depends on installed CVXPY/SciPy backends | Depends on installed CVXPY backend | MINLP solvers such as Bonmin/Couenne for supported discrete models |
The default Case.run_opf() wrapper is Pyomo unless a formulation selects another wrapper. Matrix is intended for fast single-period convex models; Matrix BESS is the time-series/battery backend. Pyomo is the backend for nonlinear BranchFlow, SOCP, Pyomo dual extraction, and its supported discrete formulations. Solver availability depends on the installed backend and model.
Solver Requirements
- IPOPT: Install via
conda install -c conda-forge ipopt. On Ubuntu,apt-get install coinor-libipopt-devonly installs headers and shared libraries; it does not provide theipoptexecutable that Pyomo'sSolverFactory("ipopt")expects.
Optimization Objectives
DistOPF supports multiple optimization objectives for distribution system OPF:
| Objective | String | Supported Wrappers | Description |
|---|---|---|---|
| Loss Minimization | "loss", "loss_min" |
Matrix, Matrix BESS, Pyomo | Minimize distribution losses (most common) |
| Substation Power | "substation", "substation_power" |
Pyomo | Minimize power imported from substation |
| Voltage Deviation | "voltage_deviation" |
Pyomo | Minimize deviations from nominal voltage (1.0 p.u.) |
| Generation Curtailment | "curtail", "curtail_min", "curtailment" |
Matrix, Matrix BESS, Pyomo | Minimize DER curtailment |
| Cost Minimization | "cost", "cost_min" |
Pyomo | Minimize energy procurement cost from the swing bus; requires price data and a time horizon |
Cost Minimization with Price Data
To use cost minimization, add an hourly price column to the case schedules (in $/MWh):
import pandas as pd
import distopf as opf
case = opf.create_case(opf.CASES_DIR / "csv" / "ieee123_30der", n_steps=24)
# Add hourly electricity prices
case.schedules['price'] = [38, 35, 34, 32, 30, 32, 38, 45, 52, 58, 62, 65,
68, 65, 62, 58, 55, 60, 75, 82, 78, 65, 50, 42]
# Run OPF minimizing energy cost
result = case.run_opf(objective="cost")
print(f"Total energy cost: ${result.objective_value:.2f}")
Result Fields
PowerFlowResult uses descriptive field names. Short aliases are also accepted for backward compatibility. Fields are optional: availability depends on the analysis, backend, case devices, and requested options.
Core fields include voltages/voltage_magnitudes, voltage_angles, active and reactive power flows, active and reactive generation, and active and reactive loads. FBS results provide voltage_angles, currents, and current_angles (angles are in degrees); these fields can also be present when another backend has computed them.
Battery and device fields include battery_active_power, battery_reactive_power, p_charge, p_discharge, and soc; regulator/capacitor-related fields include tap_ratios, reg_taps, z_caps, and u_caps, where supported by the selected model. capacitor_reactive_power is also available for capacitor outputs. Mixed-integer binary details may remain available through raw_result rather than the unified result fields.
| Field Name | Alias |
|---|---|
voltages |
— |
active_power_flows |
p_flows |
reactive_power_flows |
q_flows |
active_power_generation |
p_gens |
reactive_power_generation |
q_gens |
active_power_loads |
p_loads |
reactive_power_loads |
q_loads |
capacitor_reactive_power |
q_caps |
battery_active_power |
p_bats |
battery_reactive_power |
q_bats |
Plotting and Dual Variables
The public plotting methods return Plotly figures: plot_voltages(t=None), plot_power_flows(t=None), plot_gens(t=None), plot_batteries(), plot_schedules(), plot_network(v_min=0.95, v_max=1.05, show_phases="abc", show_reactive_power=False, t=None), plot_voltage_vs_distance(title=..., color_by="algorithm", include_secondary_phases=False), and plot_line_flow_vs_distance(power_type="active", title=..., color_by="algorithm", include_secondary_phases=False). Methods that require unavailable result data or a case reference raise RuntimeError.
When running with duals=True using the Pyomo wrapper, dual variables are accessible on the result object. The unified names are:
result = case.run_opf(wrapper="pyomo", objective="loss", duals=True)
result.dual_power_balance_p
result.dual_power_balance_q
result.dual_voltage_drop
result.dual_voltage_limits_lower
result.dual_voltage_limits_upper
Dual extraction is Pyomo-only; matrix and FBS results do not populate these fields.
Using a custom model.
Create CSVs formatted as shown below and store them in a single folder. The csv names must match exactly as shown. Column order is not important.
-your_model_directory
-branch_data.csv
-bus_data.csv
-gen_data.csv
-cap_data.csv
-reg_data.csv
-bat_data.csv
import distopf as opf
case = opf.create_case(
data_path="path/to/your_model_directory",
)
Or load them as dataframes
import distopf as opf
import pandas as pd
branch_data = pd.read_csv("path/to/your_model_directory/branch_data.csv", header=0)
bus_data = pd.read_csv("path/to/your_model_directory/bus_data.csv", header=0)
gen_data = pd.read_csv("path/to/your_model_directory/gen_data.csv", header=0)
cap_data = pd.read_csv("path/to/your_model_directory/cap_data.csv", header=0)
reg_data = pd.read_csv("path/to/your_model_directory/reg_data.csv", header=0)
bat_data = pd.read_csv("path/to/your_model_directory/bat_data.csv", header=0)
schedules = pd.read_csv("path/to/your_model_directory/schedules.csv", header=0) # Optional for multi-period cases
case = opf.Case(
branch_data=branch_data,
bus_data=bus_data,
gen_data=gen_data,
cap_data=cap_data,
reg_data=reg_data,
bat_data=bat_data,
schedules=schedules
)
Phase naming convention: Three-phase buses and lines use phases
a,b,c. Triplex (North American split-phase residential secondary) buses and lines use phasess1ands2, which are the two 120 V legs of a center-tapped single-phase transformer.s1s2refers to the 240 V line-to-line connection across both legs.
branch_data.csv
- fb: From bus id number
- tb: To bus id number
- r_aa, r_ab, r_ac, r_bb, r_bc, r_cc: upper-diagonal resistance matrix elements (p.u.) for 3-phase lines
- x_aa, x_ab, x_ac, x_bb, x_bc, x_cc: upper-diagonal reactance matrix elements (p.u.) for 3-phase lines
- r_s1s1, r_s1s2, r_s2s2: resistance matrix elements (p.u.) for triplex (split-phase) lines
- x_s1s1, x_s1s2, x_s2s2: reactance matrix elements (p.u.) for triplex (split-phase) lines
- primary_phase: primary-side phase for center-tap transformer branches (e.g. "a", "b", "c")
- s_a_max, s_b_max, s_c_max: per-phase apparent power limits (VA)
- type: overhead_line, switch, transformer, center_tap_xfmr, etc.
- name: other name of line
- status: (for switches) OPEN or CLOSED
- s_base: base VA
- v_ln_base: base line-to-neutral voltage
- z_base: base impedance
- phases: phases present on the line (e.g. "abc", "s1s2", "a")
bus_data.csv
- id: unique id for each bus (integer starting at 1)
- name: bus name
- pl_a, ql_a, pl_b, ql_b, pl_c, ql_c: active and reactive loads p.u.
- bus_type: SWING or PQ. SWING bus is voltage source
- v_a, v_b, v_c: voltage magnitude p.u. (input parameter for SWING bus. Other not used as input)
- v_ln_base: base line-to-neutral voltage (V)
- s_base: base power (VA)
- v_min, v_max: voltage magnitude limits (p.u.)
- cvr_p, cvr_q: conservation voltage reduction parameters; alternative to ZIP model for voltage dependant loads. (set to 0 for no voltage dependence)
- pl_s1, ql_s1, pl_s2, ql_s2, pl_s1s2, ql_s1s2: active and reactive loads for triplex (split-phase) buses (p.u.)
- primary_phase: primary-side phase for triplex buses (e.g. "a", "b", "c")
- phases: phases at bus (e.g. "abc", "a", "ab", "s1s2", etc.)
gen_data.csv
- id: bus id
- name: generator name
- p_a, p_b, p_c: active power output (p.u.)
- q_a, q_b, q_c: reactive power output (p.u.)
- s_base: base power (VA)
- s_a_max, s_b_max, s_c_max: rated maximum apparent power output per 3-phase (VA)
- p_s1, p_s2: active power output for triplex (split-phase) generators (p.u.)
- q_s1, q_s2: reactive power output for triplex generators (p.u.)
- s_s1_max, s_s2_max: rated maximum apparent power for triplex phases (VA)
- phases: generator phases (e.g. "abc", "s1s2") (this IS implemented)
- q_a_max, q_b_max, q_c_max: (not implemented) maximum reactive power output (p.u.)
- q_a_min, q_b_min, q_c_min: (not implemented) minimum reactive power output (p.u.)
cap_data.csv
- id: bus id
- name: capacitor name
- q_a, q_b, q_c: nominal reactive power (p.u.)
- phases: capacitor phases (abc string)
reg_data.csv
- fb: From bus id number
- tb: To bus id number
- name: regulator name
- tap_a, tap_b, tap_c: tap position (p.u.) -16 to +16; 0 is no tap change
Case and run_opf Options
create_case() / Case.__init__() parameters
| Parameter | Default | Description |
|---|---|---|
data_path |
required | Path to CSV directory, .dss file, or .xml CIM file |
start_step |
0 |
Starting time step for multi-period analysis |
n_steps |
1 |
Number of time steps (1 = single-period) |
delta_t |
1.0 |
Hours per time step (for battery energy calculations) |
ignore_schedule |
False |
If True, ignore schedule data; use multiplier 1.0 everywhere |
ignore_gen |
False |
If True, remove all generators |
ignore_bat |
False |
If True, remove all batteries |
ignore_cap |
False |
If True, remove all capacitors |
ignore_reg |
False |
If True, remove all regulators |
When constructing Case directly, pass DataFrames instead of data_path:
branch_data, bus_data, gen_data, cap_data, reg_data, bat_data, schedules.
case.modify() parameters
| Parameter | Description |
|---|---|
v_swing |
Override substation voltage (scalar or 3-element array, p.u.) |
v_min |
Override all bus voltage lower limits (p.u.) |
v_max |
Override all bus voltage upper limits (p.u.) |
gen_mult |
Scale all generator outputs and ratings |
load_mult |
Scale all loads |
cvr_p |
CVR factor for active power: cvr_p = (dP/P)/(dV/V). ZIP equivalent: 2kz + ki |
cvr_q |
CVR factor for reactive power: cvr_q = (dQ/Q)/(dV/V). ZIP equivalent: 2kz + ki |
OpenDSS Interface
You may also run using an OpenDSS model file as input.
import distopf as opf
case = opf.create_case(
data_path="path/to/your_model_directory/model.dss",
)
CIM XML Interface
To load a power system model from a CIM XML file, install the optional cim extra first:
pip install distopf[cim]
Then pass the path to your .xml file:
import distopf as opf
case = opf.create_case(data_path="path/to/model.xml")
If the cim extra is not installed, calling create_case with a .xml file will raise:
ImportError: CIM file support requires optional dependencies.
Install them with: pip install distopf[cim]
Citing this tool
Gray, Nathan T., Dubey, Anamika, Reiman, Andrew P., "DistOPF: Advanced Solutions for Distribution Optimal Power Flow Analysis - DistOPF v0.2 Documentation," (2025), https://doi.org/10.2172/2999990
@techreport{osti_2999990,
author = {Gray, Nathan T. and Dubey, Anamika and Reiman, Andrew P. and Sadnan, Rabayet},
title = {DistOPF: Advanced Solutions for Distribution Optimal Power Flow Analysis - DistOPF v0.2 Documentation},
institution = {Pacific Northwest National Laboratory (PNNL), Richland, WA (United States)},
doi = {10.2172/2999990},
url = {https://www.osti.gov/biblio/2999990},
place = {United States},
year = {2025},
month = {03}}
R. Sadnan, N. Gray, A. Bose, A. Dubey and K. P. Schneider, "Scaling Distributed Optimal Renewable Energy Coordination in Unbalanced Distribution Systems," in IEEE Transactions on Sustainable Energy, vol. 17, no. 1, pp. 3-15, Jan. 2026, doi: 10.1109/TSTE.2024.3492976.
@ARTICLE{10745555,
author={Sadnan, Rabayet and Gray, Nathan and Bose, Anjan and Dubey, Anamika and Schneider, Kevin P.},
journal={IEEE Transactions on Sustainable Energy},
title={Scaling Distributed Optimal Renewable Energy Coordination in Unbalanced Distribution Systems},
year={2026},
volume={17},
number={1},
pages={3-15},
doi={10.1109/TSTE.2024.3492976}}
Release files for distopf 1.1.0.dev5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| distopf-1.1.0.dev5.tar.gz | 21.1 MB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| distopf-1.1.0.dev5-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 41.9 MB
Release files / distopf-1.1.0.dev5.tar.gz
| Download URL | distopf-1.1.0.dev5.tar.gz |
|---|---|
| Size | 21.1 MB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
975057ee5a818c585e5d48a2adbb163d498a00e140c06f320d77eea7b3cd2c5e
|
|
BLAKE2b-256 checksum How to use checksums |
6b4259da296c8bfcd6597f256a1c51c0dca4e405877ee8b2d8e914773458e085
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / distopf-1.1.0.dev5-py3-none-any.whl
| Download URL | distopf-1.1.0.dev5-py3-none-any.whl |
|---|---|
| Size | 20.8 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8dcf33604bc5983defa831f1412f20ba4c23bd413c158d071dca4e2b39d84ee7
|
|
BLAKE2b-256 checksum How to use checksums |
dd936248c8f6bc133666e08e53e076bb06df3cce88eb2aa53a5cbab46a31a9c8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.18 {"installer":{"name":"uv","version":"0.12.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|