Tutorials › Pharmacometric Workflows and Reproducible Modeling › Pinning Software Environments for Reproducibility

Project Structure, Version Control and Documentation · Tutorial 59 of 399

Pinning Software Environments for Reproducibility

Learn to record a usable, appropriately specific software environment without confusing portable requirements with settings tied to one machine.

Intermediate 11 min read

What You'll Learn

  • Distinguish an environment manifest from general setup documentation.
  • Explain why recording only direct dependencies can leave an environment underspecified.
  • Choose between exact version pins, compatible ranges, and documented external requirements.
  • Separate portable software requirements from local paths, credentials, and machine settings.
  • Recognize platform and native-library limits on what a manifest can reproduce.
  • Identify common signs that a recorded environment specification is incomplete or stale.

Why Pin the Environment?

A pharmacometric project depends on more than its model code and input data. It also depends on the software that interprets that code: the language runtime, package versions, and sometimes external programs, system libraries, or compilers. If those dependencies change, an analysis may behave differently or stop running even though the project files have not changed.

An environment specification makes those requirements visible and repeatable. It helps a collaborator create a compatible software setup and helps the project team understand what versions were intended. It does not, by itself, prove that an analysis produces identical outputs on every machine. Pinning the environment is one part of reproducible work, not a substitute for controlling other relevant inputs or recording how a particular run was performed.

Definition: An environment specification records the software and dependency requirements needed to use a project. A manifest is a machine-readable form of that specification, often including resolved package versions and their sources. Setup documentation explains requirements or installation steps in human-readable form. Neither should be confused with a record of a particular run or with machine-specific configuration such as a local installation path.

The right level of detail depends on what the project needs. A broad statement such as “requires R and several packages” may be easy to maintain but too vague to recreate a compatible setup. At the other extreme, copying every detail of one workstation into a shared manifest can make the project unnecessarily tied to that machine. The aim is to pin or describe portable requirements precisely, while documenting local details separately.

What a Useful Specification Records

Begin with the software components that can affect whether the project runs as intended. Depending on the workflow, these can include the language or runtime version; direct packages; resolved transitive packages; and external programs that the project invokes. If a dependency comes from a particular repository, source, or release channel, record enough information to identify it. Where the ecosystem supports a lockfile, use it to capture the resolved environment rather than relying only on a short list of top-level packages.

A direct dependency is a package the project uses directly. A transitive dependency is a package required by another dependency. For example, a project may call package A, while package A requires package B. The project’s code does not call B, but B is still needed to install or use A. A direct-dependency list can therefore be accurate and still fail to describe the complete environment.

A useful specification generally makes clear which components are exact pins, which are compatible ranges, and which are external requirements that must be installed by another route. For an external licensed program, setup documentation may need to state the required product and version, how an authorized user obtains it, and any relevant platform requirement. Do not put credentials, license keys, or private download links in a public repository.

Why Dependency Closure Matters

A resolver installs dependencies according to the requirements declared by packages and the sources available to it. The result is a dependency closure: the direct dependencies plus the transitive dependencies needed to satisfy them. If a specification records only direct dependencies, the resolver may select different transitive versions at different times. A lockfile is useful because it can record one resolved set, but its coverage and portability depend on the package ecosystem and the information it stores.

The logic can be stated precisely. Let \(D\) be the project’s direct dependencies, and let \(R(d)\) be the dependencies required by package \(d\). A complete package environment must include \(D\), the requirements of each member of \(D\), and the requirements of those added packages, continuing until no new requirements are introduced. If \(C_0=D\) and \(C_{k+1}=C_k \cup \bigcup_{d \in C_k} R(d)\), then the closure is reached when \(C_{k+1}=C_k\). A manifest that records only \(D\) does not necessarily specify this resolved closure.

This explains why writing down a few package names is not equivalent to pinning an environment. It also explains why an exact lockfile is not always enough: the runtime, operating system, native libraries, external executables, or installation source may still matter. State the scope of the specification instead of implying that it captures every condition on every machine.

Worked Example: Recording a Resolved R Environment

Suppose a hypothetical project uses R 4.3.2 directly and calls dplyr 1.1.4 and ggplot2 3.5.0. A setup note that says “install R and the tidyverse packages” does not identify those versions and may not install the same package set later.

Specification decision. The project records the R version and uses its package-management lockfile to capture the resolved package versions and their sources. The two direct packages are dplyr and ggplot2; the lockfile also records their resolved dependencies as supported by the project’s package-management tools.

What this achieves. A collaborator can use the recorded runtime and lockfile as a specific target, rather than asking a resolver to choose any currently compatible package versions. The specification is more informative than the two direct package names alone because it also addresses the transitive package set.

What it does not establish. The R version and package lock do not, by themselves, guarantee identical behavior on every operating system. A package may rely on compiled code or system libraries, and the lockfile’s source and platform support matter. The project should describe known platform requirements where relevant and avoid claiming that package pins alone guarantee bit-for-bit identical results.

Choosing Exact Pins or Compatible Ranges

An exact pin names one version, such as version 1.1.4. A compatible range permits multiple versions that satisfy a declared constraint. Exact pins are useful when the goal is to recreate a known resolved set. Ranges can be useful when a package is intended to work across a tested span of versions, or when a project deliberately allows routine updates. A range is not a record of which version was actually used.

These choices are not interchangeable. A project can declare acceptable ranges for direct dependencies while maintaining a lockfile for a particular working environment. In that arrangement, the ranges express intended compatibility, and the lockfile records one resolved set. The project should say which role each file serves; otherwise, contributors may mistake a broad requirement for an exact reproduction target.

Worked Example: Detecting Incompatible Requirements

Consider a hypothetical package ecosystem. The project directly requires package A and package B. Package A requires shared-library versions greater than or equal to 2 and less than 3. Package B requires shared-library versions greater than or equal to 3 and less than 4.

Check for a common version. The first requirement is \(2 \leq v < 3\), while the second is \(3 \leq v < 4\). To satisfy both, a version would have to meet both inequalities. But no value can be both less than 3 and greater than or equal to 3. The requirements have an empty intersection.

Conclusion. A resolver cannot produce an environment satisfying both declared requirements as written. Recording an exact version of the shared library cannot solve the conflict: any chosen version violates at least one constraint. The project must instead use compatible versions of A and B, change the dependency choices, or document a supported alternative. Forcing an installation that ignores the declared constraints may produce a setup that installs but is unsupported or fails at runtime.

This is one reason a lockfile should represent a successfully resolved environment, not just a list of preferred package versions. A version list that contradicts dependency constraints is not a coherent specification.

Portable Requirements and Local Installation Details

A portable requirement describes what another user needs, not where one particular user happens to have installed it. “Requires software X, version 7.5, on a supported operating system” is a portable requirement. “Executable is at /opt/lab/tools/x” is a local path. The latter may work on one workstation and fail elsewhere, so it belongs in local configuration or setup guidance rather than as a universal project requirement.

The same distinction applies to environment variables, personal library directories, temporary folders, license configuration, and credentials. A project may need a setting to locate an external tool, but its shared configuration should expose a portable setting or a documented placeholder rather than hard-code one machine’s value. Keep private or site-specific details out of shared files, and explain how a user supplies them locally.

Setup documentation is especially useful when an important requirement cannot be represented fully by a package lockfile. It can identify a required external program, explain how to obtain it through authorized channels, note platform constraints, and distinguish required steps from optional conveniences. Documentation should complement a manifest, not obscure version requirements that could be captured precisely in a machine-readable file.

Worked Example: Documenting an External Modeling Tool

Suppose a hypothetical project calls an externally installed, licensed modeling program named ModelTool, version 7.5. The program is not installed through the project’s language package manager. One analyst has it at /opt/lab/modeltool/bin/modeltool; a collaborator uses a different operating system and installation directory.

Portable specification. The project setup documentation states that ModelTool version 7.5 is required, identifies the supported platforms, and directs authorized users to the approved installation process. It explains that the executable location must be supplied through a local configuration setting.

Local detail. The analyst’s actual executable path is stored in a machine-specific, untracked configuration file. It is not presented as the project’s universal path, and license credentials are not committed to version control.

Reasoning. The product and version describe a requirement shared by project users; the installation path describes one installation. Separating them preserves the portable requirement without making the project depend on a directory that exists only on one machine. If the path is an input required by the project, document how to set it, but do not mislabel its local value as a pinned software version.

Comparison, Diagnostics, and Limits

ApproachWhat it communicatesMain limitation
Informal setup noteGeneral software requirements and human-readable instructionsMay omit exact resolved versions or transitive dependencies
Version rangesWhich versions are intended to be compatibleDifferent installs may resolve to different versions within the ranges
Lockfile or pinned manifestA specific resolved package set, within the file’s supported scopeMay not capture runtimes, system libraries, external tools, or all platform differences
Container or virtual machine specificationA more complete software image or system setup, depending on its contentsCan be platform-dependent and still relies on external inputs, licenses, or infrastructure

No approach automatically solves every reproducibility problem. A container can package more of the operating environment than a package lockfile, but it may still rely on a compatible host, external data, licensed software, or configuration supplied at runtime. A setup note can make an external requirement clear, but cannot force a resolver to install the same package versions. Choose the tool that matches the dependency and document its scope.

Look for warning signs when reviewing an environment specification: package names without versions where exact versions are needed; a lockfile that omits the runtime; direct dependencies recorded without resolved dependencies; references to untracked local paths; conflicting version constraints; or instructions that point to unavailable sources. Also check whether the recorded versions agree with the project’s stated requirements. These are signs to clarify or update the specification, not proof that every analysis result is wrong.

A further limitation is that an environment can be pinned while other influential conditions remain uncontrolled. Data, configuration, code revisions, and run-specific settings may affect results too. Environment pinning answers “what software was required?” It does not answer every question about the inputs or circumstances of a particular analysis.

Common Mistakes

  • Recording only direct dependencies. The reasoning “the project calls these packages, so these names are enough” overlooks the packages they require. Record the resolved dependency set where the tooling supports it.
  • Treating a version range as an exact record. A constraint permits a set of versions; it does not say which one a particular installation selected. Use a lockfile when a resolved target is needed.
  • Committing a local path as though it were portable. A path that works on one workstation may not exist for another user. Document the requirement and provide a local configuration mechanism instead.
  • Assuming a lockfile fixes the entire machine. A lockfile may not capture operating-system libraries, compilers, runtimes, or external tools. State relevant requirements separately.
  • Claiming exact reproducibility from version pins alone. Pins constrain software versions; they do not establish that every other input and execution condition is identical.
Key takeaway: A good environment specification records the software requirements at the level needed for the project, including resolved dependencies where possible. Keep portable requirements in shared manifests or setup documentation, keep machine-specific installation details local, and describe the limits of what the specification captures.

Check Your Understanding

Answer each question, then compare your reasoning with the solutions below.

  1. A project lists two direct packages but no versions or lockfile. What important information may be missing?
  2. A requirement allows versions 2.1 through 2.8. Does that identify one exact installed version? Explain.
  3. A collaborator’s software executable is installed at a different path from the analyst’s. Which information should be shared, and which should be local?
  4. Why can a complete package lockfile still fail to guarantee identical behavior across operating systems?

Solutions. (1) The list may omit the selected versions and transitive dependencies, and may not identify the runtime or sources needed to resolve them. (2) No. The range permits multiple versions; a particular installation may select one of them, so an exact resolved record is needed to identify that selection. (3) Share the required software name and version, relevant platform conditions, and instructions for supplying its location. Keep each user’s actual executable path in local configuration. (4) Operating systems can differ in native libraries, compilers, platform-specific package builds, and other requirements that a package lockfile may not record.

Clinical Biostats