{"id":19416508,"url":"https://github.com/brandondube/pctl","last_synced_at":"2026-03-27T04:51:18.629Z","repository":{"id":38381487,"uuid":"226439818","full_name":"brandondube/pctl","owner":"brandondube","description":"Industrial control systems in Go","archived":false,"fork":false,"pushed_at":"2023-09-10T20:16:37.000Z","size":76,"stargazers_count":37,"open_issues_count":1,"forks_count":1,"subscribers_count":5,"default_branch":"master","last_synced_at":"2024-08-11T21:07:48.946Z","etag":null,"topics":["biquad","biquads","control-shaping","control-systems","high-pass-filter","low-pass-filter","pid","single-pole-filters","state-space"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/brandondube.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":"ROADMAP.md","authors":null}},"created_at":"2019-12-07T01:43:37.000Z","updated_at":"2024-07-25T13:30:41.000Z","dependencies_parsed_at":"2024-01-12T02:14:09.580Z","dependency_job_id":null,"html_url":"https://github.com/brandondube/pctl","commit_stats":{"total_commits":45,"total_committers":3,"mean_commits":15.0,"dds":0.06666666666666665,"last_synced_commit":"ebfaac83726af311eb25a6f8e6f0adcf98cde75c"},"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brandondube%2Fpctl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brandondube%2Fpctl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brandondube%2Fpctl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/brandondube%2Fpctl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/brandondube","download_url":"https://codeload.github.com/brandondube/pctl/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":223954851,"owners_count":17231187,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["biquad","biquads","control-shaping","control-systems","high-pass-filter","low-pass-filter","pid","single-pole-filters","state-space"],"created_at":"2024-11-10T12:48:28.873Z","updated_at":"2026-03-27T04:51:13.496Z","avatar_url":"https://github.com/brandondube.png","language":"Go","funding_links":[],"categories":["Embedded Systems"],"sub_categories":["Instrumentation and control with sensors and actuators"],"readme":"# pctl\n\npctl, \"process control\" is a package for industrial control in Go.\n\nIt contains an implementation of the classic PID controller with integral\nanti-windup, as well as many filter types that can be used for loop shaping:\n- single pole low pass\n- single pole high pass\n- Biquads\n- State-Space filters with an arbitrary number of states\n- FIR filters with an arbitrary number of taps\n\nThe package declares the top-level `Cascade` function, which takes a sequence of\ninterfaces that are met by all types in the package to facilitate SOS and other\n\"fluent\" designs.  Use of Cascade will be somewhat slower or less efficient than\nmanually writing a chain of function calls due to the virtualization implied by\ninterfaces.\n\nIts types are not concurrent safe, and use double precision, which is low cost\non most software platforms.  Tinygo may perform relatively worse, although it\nshould not matter much.  The implementations of each type in this repository are\nrelatively optimized, easily able to function at up to MHz on even a raspberry\npi.\n\nFor Biquads, design methods are included to synthesize common filter types from\ncorner frequencies, etc, in applications where detailed analysis of the transfer\nfunctions or plant response are not required.\n\n## Usage\n\n### Biquad filter on measurement with PID controller\n\n```go\n// Biquad, 1k sample rate, 50Hz corner freq, maximally flat in band\n// 6 = gain; unused for LPF; see NewBiquad interface\n// or bring your own a0, a1, a2, b1, b2 coefs\ninputFilter := pctl.NewBiquadLowPass(1000, 50, math.Sqrt(2), 6)\ncontroller := pctl.PID{P: 1, I: 0.5, Setpt: 50, DT: 1e-3}\nfor {\n    input := getInput()\n    controlCommand := pctl.Cascade(input, inputFilter, controller)\n    applyControl(controlCommand)\n}\n```\n\n### State-Space filtering the error signal for control shaping\n\n```go\n// State-space second order lowpass filter,\n// 900Hz sample rate, 2Hz corner freq, -6dB/octave\nA := [][]float64{\n    {2, -1},\n    {1, 0},\n}\nB := []float64{5e-5, 0}\nC := []float64{4, 0.02}\nD := 5e-5\nsetpt := pctl.Setpoint(50)\n// FB = feedback\nFBFilter := pctl.NewStateSpaceFilter(A, B, C, D, nil)\nfor {\n    input := getInput()\n    controlCommand := pctl.Cascade(input, setpt, FBFilter)\n    applyControl(controlCommand)\n}\n```\n\n### Shaped controller response, control setpoint change stability\n\n\nThe previous examples lack prefilters on the setpoint, so the system can be\ndestabilized by large setpoint changes.  A prefilter can be added that operates\non `*setpt` to remedy this.\n\nOpening or closing the control loop independent of measurement is also not\npossible.  The latter can be achieved by simply adding one line:\n\n```go\nfor {\n    // ...\n    if controlLoopClosed {\n        applyControl(process)\n    }\n}\n```\n\nManipulating of this variable is outside the scope of pctl.  It could be e.g. a\nstruct member, or simply a pointer to a bool that is dereferenced at the if.\nThe \"size\" of the solution can scale with the \"size\" of the processor and\nproblem.\n\n\n## Performance\n\nSee `pctl_test.go` for a benchmark suite.  The FIR filter in the benchmark has\n32 taps.\n\n### Mac M1 Pro\n\nM1 Pro Boost frequency = 3.2GHz; 1 clock ~=0.3125 ns.\n\n```sh\nname           time/op\nPIDLoop-10     3.50ns ± 1%\nLPF-10         4.52ns ± 2%\nHPF-10         4.49ns ± 2%\nBiquad-10      4.89ns ± 1%\nStateSpace-10  12.5ns ± 3%\nSetpoint-10    0.32ns ± 1%\nFIRFilter-10   11.8ns ± 1%\n```\nA reasonable average is the Biquad filter, 15.6 clocks.\n\n### Intel i7-9700k\n\nThis CPU boosts to 4.6GHz during the benchmark; 1 clock ~=0.217 ns.\n```sh\nname          time/op\nPIDLoop-8     1.99ns ± 2%\nLPF-8         3.74ns ± 1%\nHPF-8         2.80ns ± 1%\nBiquad-8      3.65ns ± 1%\nStateSpace-8  9.72ns ± 1%\nSetpoint-8    0.21ns ± 3%\nFIRFilter-8   8.66ns ± 2%\n```\n\nThe Biquad filter takes 16.8 clocks.  Broadly comparable to the ARM64 M1.\n\n### AMD 7950X (Windows)\n\nThis CPU boosts to 5.3GHz during the benchmark; 1 clock ~= 0.189 ns.  cTDP 105w\neco mode is enabled.\n```sh\nname            time/op\nPIDLoop-8       3.444n ± 0%\nLPF-8           4.317n ± 0%\nHPF-8           4.312n ± 3%\nBiquad-8        4.694n ± 3%\nStateSpace-8    10.90n ± 1%\nSetpoint-8       0.29n ± 1%\nFIRFilter-8     10.88n ± 0%\n```\n\nDespite having a considerably higher clockspeed, this CPU takes more time to\nperform the functions within pctl.\n\n### AMD 7950X (WSL)\n\n\n```sh\nname             time/op\nPIDLoop-32       2.168n ± 1%\nLPF-32           3.190n ± 0%\nHPF-32           2.654n ± 0%\nBiquad-32        3.191n ± 0%\nStateSpace-32    7.301n ± 1%\nSetpoint-32     0.1801n ± 1%\nFIRFilter-32     6.943n ± 2%\n```\n\nPerformance is ~50% higher in Windows subsystem for Linux / Ubuntu.\n\n### AMD V1500B Embedded (virtualized)\n\nThis benchmark is run virtualized on a Synology NAS, with two vCPUs and 2GB of\nRAM.  The clock speed is 2.2GHz.\n\n```sh\nname             time/op\nPIDLoop-32       5.037n ± 1%\nLPF-32           8.506n ± 0%\nHPF-32           7.068n ± 0%\nBiquad-32        8.261n ± 0%\nStateSpace-32    24.76n ± 1%\nSetpoint-32      0.487n ± 1%\nFIRFilter-32     19.00n ± 2%\n```\n\n## Design\n\nSeveral designs have been iterated in this repository.  An early design used\nchannels to communicate, which took about 500ns per update.  This was less\ncomposable than methods/functions.\n\nAn intermediate design maintained clocks inside each control element.  This was\nless performant, but more importantly could not be used in a simulation capacity\nrunning at any speed other than real time.  Explicitly including dT (fielded as\nDT) in the structs allows these controllers to be used in simulation studies as\nwell.  The nearly 10x increase in performance and better friendliness to tinygo\nplatforms are also nice benefits.\n\nThe current design has been released as v1 (guaranteed stable) and is unlikely\nto change for marginal improvements in favor of API stability.\n\n## Expansion\n\nThis library is dependency-free outside stdlib/math and easily portable to tiny\nplatforms, even if a float32 type-change would be required (this is as simply as\nctrl+F).  Future additions shall not disturb that property.  LQR/LQG, Kalman\nfiltering, etc, may be implemented here if the the implementations do not\nrequire a dependency on e.g. Gonum.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrandondube%2Fpctl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbrandondube%2Fpctl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbrandondube%2Fpctl/lists"}