Code Map¶
This page maps the code of SolarFields: its packages, how they depend on each other, and the main call paths of a calculation. It was derived from the knowledge graph of the repository (GitNexus, October 2026). The physical models, the design decisions and the limitations are described in Architecture; this page doesn't repeat them.
Overview¶
SolarFields computes the steady state of a solar thermal collector field at one operating point: the flow distribution down to every riser of every collector, the pressures and temperatures of the whole field network, the heat output and the pressure drops.
A calculation runs in four stages:
- Input. A YAML configuration (or Python objects) describes the field: collectors, strings, sub-fields, pipes, valves, the fluid, the operating conditions and the inlet.
io/loaders.pyreads it strictly and builds the components. - Network. Every field format becomes one
FieldNetwork: nodes and edges, where an edge is a pipe, a valve or a string. - Solution.
FieldSolvercouples the string solver (collector physics, riser flows) with the own network solver (node-pressure Newton method) in an outer loop, and carries the temperatures along the flow, including the heat losses of the pipes. - Results. A
FieldResultmirrors the component hierarchy: field, sub-fields, strings, collectors, pipes and valves, nodes.
Architecture Diagram¶
flowchart TD
subgraph Input["Input"]
YAML["Config files<br/>configs/*.yaml, examples/*.yaml"]
API["Python API"]
end
subgraph IO["io"]
LOAD["loaders.py<br/>load_config: strict keys and names"]
REN["renamed.py<br/>renamed and replaced keys"]
end
subgraph COMP["components"]
COLL["Collector, CollectorString, Pipe"]
FIELD["CollectorField<br/>to_network, add_to_network"]
NET["FieldNetwork<br/>nodes and edges<br/>from_subfields, BranchPipe"]
end
subgraph SOLV["solvers"]
FS["FieldSolver<br/>outer loop"]
SS["StringSolver"]
PP["PandapipesSolver<br/>reference, optional"]
RA["result_assembly.py<br/>FieldResult"]
end
subgraph CORE["core: numerics and physics"]
NN["network_newton.py<br/>node-pressure Newton, temperatures"]
DB["dominant_branch.py<br/>riser flows"]
TH["thermal.py<br/>ISO 9806 collector model"]
HL["heat_loss.py<br/>pipe heat losses"]
FR["friction.py<br/>Churchill friction, K values"]
FL["fluid_loader.py, fluid_properties.py<br/>temperature-dependent properties"]
OC["operating_conditions.py"]
end
subgraph OUT["Output"]
RES["results<br/>FieldResult, SubfieldResult,<br/>StringResult, PipeResult"]
PLOT["plotting"]
end
YAML --> LOAD
REN --> LOAD
LOAD --> COLL
LOAD --> FIELD
LOAD --> NET
LOAD --> OC
API --> COLL
API --> FIELD
FIELD --> NET
NET --> FS
NET --> PP
FS --> SS
FS --> NN
PP --> SS
PP --> NN
SS --> DB
SS --> TH
SS --> HL
NN --> HL
NN --> FR
DB --> FR
SS --> FL
NN --> FL
FS --> RA
PP --> RA
RA --> RES
RES --> PLOT
PandapipesSolver solves the same networks with pandapipes and serves as reference for tests and benchmarks; pandapipes is an optional dependency, imported on first use.
Functional Areas¶
| Package | Role | Main modules |
|---|---|---|
io |
Reading configurations: every section is checked, unknown keys and undefined names are errors; renamed keys raise an error that names the replacement. | loaders.py (load_config, load_collector, load_string, load_field), renamed.py |
components |
The field model: collectors (harp, meander), strings of collectors in series, sub-fields with headers, and the network of the whole field. | collector.py, collector_string.py, pipe.py, field.py (CollectorField, FieldPiping), network.py (FieldNetwork and its from_subfields, BranchPipe, NetworkEdge), pipe_systems.py (which values of buried_pipes and pipes_in_air the pipes take, #74) |
solvers |
The solution of a string and of a field, and the assembly of the results. | string_solver.py (StringSolver), field_solver.py (FieldSolver, solve_field_from_yaml), result_assembly.py (the FieldResult, with each string's share of the flow, flow_shares), pandapipes_solver.py and network_builder.py (reference) |
core |
Numerics and physics, mostly compiled with numba: network solution, riser flow distribution, collector thermal model, pipe friction and heat losses, fluid properties, operating conditions. | network_newton.py, dominant_branch.py, thermal.py, heat_loss.py, friction.py, fluid_loader.py, fluid_properties.py, fluid_registry.py, header_design.py (header sizes for velocity limits), pipe_materials.py and pipe_catalogue.py (pipe materials and sizes), operating_conditions.py, constants.py, units.py, yaml_files.py (reading YAML files), quantities.py (units of the inputs), input_checks.py (ranges of the inputs), frozen.py (read-only lists and mappings of the frozen components) |
results |
Result dataclasses that mirror the component hierarchy. | field_result.py, subfield_result.py, headers_result.py, string_result.py, collector_result.py, pipe_result.py, beyond_data.py |
plotting |
Figures of string and field results, with the limits of VelocityLimits (highest velocities, lowest riser Reynolds number). |
strings.py, field.py, composite.py, common.py (VelocityLimits) |
data |
Fluid property coefficients and tables (generated offline from CoolProp). | fluid_coefficients/, fluid_metadata.yaml, tables/ |
Outside the package: configs/ holds the component libraries (collectors, pipes), examples/ the worked examples and the benchmarks, scripts/ the generation of the fluid tables and of the API reference pages, and tests/ the unit and integration tests.
Key Execution Flows¶
The traces below come from the knowledge graph; each step calls the next.
1. Field from a configuration file¶
solve_field_from_yaml → FieldSolver.solve → solve_flows → residuals → _edge_characteristics → pressure_drop_slope → churchill_friction_slope → churchill_friction
solve_field_from_yaml loads the configuration with load_config, which builds the FieldNetwork for every field format (one sub-field, several sub-fields with branch pipes, or a graph). FieldSolver.solve then runs the outer loop:
- Each string enters the network through a characteristic Δp(ṁ), fitted to string solves.
solve_flows(core/network_newton.py) solves the network for the flows of all edges and the pressures of all nodes. Each Newton step evaluates the pressure drop of every edge and its slope (_edge_characteristics), with the Churchill friction factor and the K values (core/friction.py).check_string_flowsrejects a string with reverse or no flow (both field solvers, #71).propagate_temperaturescarries the temperatures along the flow, solves every string at its flow and inlet temperature, applies the heat losses of the pipes (core/heat_loss.py) and mixes the flows where they join by enthalpy (mix_temperature, #71). The given pressure is taken at the field's height (pressure_at_field_height), with the density of the column at the last pass's outlet temperature on the safe side (column_temperature).- The loop stops when every string's pressure drop matches the network and the temperatures no longer change;
result_assembly.field_resultbuilds theFieldResult, andcheck_field_result(core/fluid_limits.py) checks it against the temperature limits, for a field below the point of the given pressure its outlet at that height too (places_above_field).
2. Minor losses in the network solution¶
solve_field → FieldSolver.solve → solve_flows → residuals → _edge_characteristics → pressure_drop_slope → pressure_drop_k_factor
The same Newton step, for the K values of fittings, valves and T-junctions: each loss is evaluated with its own K value and diameter, so that pipe friction and minor losses stay separate in the results.
3. String with pipe heat losses¶
solve_string → StringSolver.solve → solve_mass_flow → _pipe_result → Pipe.outlet_temperature → pipe_outlet_temperature
StringSolver solves the collectors of a string in series: the ISO 9806 thermal model of every riser strip of each collector (solve_thermal_risers in core/thermal.py; one model per collector with a single flow path or with riser_temperatures off), the riser flow distribution inside it (flow 4), and the pipes between and around the collectors with the exact solution of their heat losses (core/heat_loss.py). The temperature loop repeats until the collector and riser temperatures no longer change; each pass solves the riser flows with the fluid properties at the riser temperatures of the pass before (StringSolver._riser_pass). A collector or riser whose thermal model returns no number (no solution, or the iteration with a8 didn't converge) raises CollectorEquationError right there (StringSolver._equation_error, #70).
4. Riser flow distribution of a collector¶
riser_flows → _solve_riser_flows → _jacobian → pressure_drop_slope → pressure_drop_pipe → reynolds_number
The dominant-branch method (core/dominant_branch.py) solves the flows of all risers of a harp or meander collector at once. Its Jacobian uses the exact slope of every loss term (pipe friction with the local slope of the Churchill friction factor, T-junction and bend losses), so that each step is a Newton step. The elements of a collector's riser paths (harp_elements) are built once per collector; every element has the fluid properties at its own temperature (element_temperatures, get_fluid_props_within_limits_array). The paths count as balanced when they agree within 1 % of tol_dp and within 0.01 % of the collector's pressure drop (RISER_TOL_RELATIVE, #70). dominant_branch_harp is the same solve with one density and viscosity for all elements.
5. Fluid properties¶
main (string benchmark) → run_single_benchmark → StringSolver.solve → solve_mass_flow → _pipe_result → get_fluid_props → load_shipped_coefficients → load_metadata
Every element evaluates the density, viscosity and heat capacity of the fluid at its own temperature, and for water at its own absolute pressure. core/fluid_loader.py loads the fitted formulas of the fluid and concentration from data/ once and caches them; core/fluid_properties.py evaluates them (compiled with numba). scripts/generate_fluid_tables.py fits the formulas to CoolProp's data; CoolProp isn't needed at run time. core/fluid_limits.py gives the limits, freezing below and boiling at the local pressure above: the solvers take the properties at a limit during their iterations (get_fluid_props_within_limits) and check their converged results (check_string_result, check_field_result). A converged result beyond these limits raises TemperatureOutOfRangeError, and so does a thermal oil below the range of its vapour pressure curve (about 0.5 bar) or a temperature or pressure that isn't a number (#69). The same check lists the places beyond the fluid's data, the range its formulas are fitted to (data_limits), in the result's beyond_fluid_data; with SolverParams.allow_fluid_extrapolation off, it raises there too (#69).
Where to Start¶
- A new configuration key:
io/loaders.py(the known keys of a section come from the dataclass fields where the section maps onto one), the component incomponents/, a test intests/unit/test_config_keys.py. - A change of the physics:
core/and the description indocs/architecture.md, in the same commit. - A new result value:
results/andsolvers/result_assembly.py, so that both field solvers give it.