How Zippo Lighters Are Made

How Zippo Lighters Are Made

https://ift.tt/2JxWWMk

How Zippo Lighters Are Made

Link

BRANDMADE.TV takes us inside the Zippo lighter factory for a look at how they create their iconic windproof lighters. The process starts out with rolls of brass which are shaped to form each lighter’s case before it’s chromed. The interior is formed from steel, then brass, flint, a wick, and cotton are added to complete the assembly.

fun

via The Awesomer https://theawesomer.com

December 31, 2020 at 02:00PM

SQL Clause is coming to town

SQL Clause is coming to town

https://ift.tt/2WSmK90

Olya Kudriavtseva has an ugly christmas sweater:

“He’s making a table. He’s sorting it twice. SELECT * FROM contacts WHERE behavior = “nice”; SQL Clause is coming town! (buy here)

Katie Bauer observes:

I mean, except for the fact that sorting something twice is TERRIBLY optimized

So how bad is this? Let’s find out.

Some test data

We are defining a table santa, where we store peoples names (GDPR, EU Regulation 2016/679 applies!), their behavior (naughty or nice), their age, their location, and their wishlist items.

create table santa (
	id integer not null primary key auto_increment,
	name varchar(64) not null,
	loc point srid 0 not null,
	age integer not null,
	behavior enum('naughty', 'nice') not null,
	wish varchar(64) not null
)

We are also writing some code to generate data (to evade GDPR, we are using randomly generated test data):

for i in range(1, size):
	data = {
	    "id": i,
	    "name": "".join([chr(randint(97, 97 + 26)) for x in range(64)]),
	    "xloc": random()*360-180,
	    "yloc": random()*180-90,
	    "age": randint(1, 100),
	    "behavior": "naughty" if random() > nicelevel else "nice",
	    "wish": "".join([chr(randint(97, 97 + 26)) for x in range(64)]),
	}

	c.execute(sql, data)
	if i%1000 == 0:
	    print(f"{i=}")
	    db.commit()

db.commit()

The full data generator is available as santa.py. Note that the data generator there defines more indexes – see below.

In our example we generate one million rows, and assume a general niceness of 0.9 (90% of the children are nice). Also, all of our children have 64 characters long names, a single 64 characters long wish, a random age, and are equidistributed on a perfect sphere.

Our real planet is not a perfect sphere, and also not many people live in the Pacific Ocean. Also, not many children have 64 character names.

Sorting it twice

How do you even sort the data twice? Now, assuming we sort by name, we can run an increasingly deeply nested subquery:

kris@localhost [kris]> select count(*) from santa where behavior = 'nice';
+----------+
| count(*) |
+----------+
|   900216 |
+----------+
1 row in set (0.25 sec)

kris@localhost [kris]> explain select count(*) from santa where behavior = 'nice'\G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: santa
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 987876
     filtered: 50.00
        Extra: Using where
1 row in set, 1 warning (0.00 sec)

Note (Code 1003): /* select#1 */ select count(0) AS `count(*)` from `kris`.`santa` where (`kris`.`santa`.`behavior` = 'nice')

Out of 1 million children, we have around 900k nice children. No indexes can be used to resolve the query.

Let’s order by name, using a subquery:

kris@localhost [kris]> explain 
  -> select t.name from (
  ->   select * from santa where behavior = 'nice'
  -> ) as t order by name;
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+-----------------------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra                       |
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+-----------------------------+
|  1 | SIMPLE      | santa | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 987876 |    50.00 | Using where; Using filesort |
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+-----------------------------+
1 row in set, 1 warning (0.00 sec)

Note (Code 1003): /* select#1 */ select `kris`.`santa`.`name` AS `name` from `kris`.`santa` where (`kris`.`santa`.`behavior` = 'nice') order by `kris`.`santa`.`name`

We can already see that the MySQL 8 optimizer recognizes that this subquery can be merged with the inner query, and does this.

This can be done multiple times, but the optimizer handles this just fine:

kris@localhost [kris]> explain 
  -> select s.name from (
  ->   select t.name from (
  ->     select * from santa where behavior = 'nice'
  ->   ) as t order by name
  -> ) as s order by name;
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+-----------------------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra                       |
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+-----------------------------+
|  1 | SIMPLE      | santa | NULL       | ALL  | NULL          | NULL | NULL    | NULL | 987876 |    50.00 | Using where; Using filesort |
+----+-------------+-------+------------+------+---------------+------+---------+------+--------+----------+-----------------------------+
1 row in set, 1 warning (0.00 sec)

Note (Code 1003): /* select#1 */ select `kris`.`santa`.`name` AS `name` from `kris`.`santa` where (`kris`.`santa`.`behavior` = 'nice') order by `kris`.`santa`.`name`

We can see using filesort, so while we ask for the query result to be sorted by name twice, it is actually only sorted once.

No sorting at all

We can improve on this, using a covering index in appropriate order:

kris@localhost [kris]> alter table santa add index behavior_name (behavior, name);
Query OK, 0 rows affected (21.82 sec)
Records: 0  Duplicates: 0  Warnings: 0

Having done this, we now see that we lost the using filesort altogether:

kris@localhost [kris]> explain select s.name from ( select t.name from (select * from santa where behavior = 'nice') as t order by name ) as s order by name;
+----+-------------+-------+------------+------+---------------+---------------+---------+-------+--------+----------+--------------------------+
| id | select_type | table | partitions | type | possible_keys | key           | key_len | ref   | rows   | filtered | Extra                    |
+----+-------------+-------+------------+------+---------------+---------------+---------+-------+--------+----------+--------------------------+
|  1 | SIMPLE      | santa | NULL       | ref  | behavior_name | behavior_name | 1       | const | 493938 |   100.00 | Using where; Using index |
+----+-------------+-------+------------+------+---------------+---------------+---------+-------+--------+----------+--------------------------+
1 row in set, 1 warning (0.00 sec)

Note (Code 1003): /* select#1 */ select `kris`.`santa`.`name` AS `name` from `kris`.`santa` where (`kris`.`santa`.`behavior` = 'nice') order by `kris`.`santa`.`name`

The query is now annotated using index, which means that all data we ask for is present in the (covering) index behavior_name, and is stored in sort order. That means the data is physically stored and read in sort order and no actual sorting has to be done on read – despite us asking for sorting, twice.

Hidden ‘SELECT *’ and Index Condition Pushdown

In the example above, we have been asking for s.name and t.name only, and because the name is part of the index, using index is shown to indicate use of a covering index. We do not actually go to the table to generate the result set, we are using the index only.

Now, if we were to ask for t.* in the middle subquery, what will happen?

kris@localhost [kris]> explain
  -> select s.name from (
  ->   select * from (
  ->     select * from santa where behavior = 'nice'
  ->   ) as t order by name
  -> ) as s order by name;
+----+-------------+-------+------------+------+---------------+---------------+---------+-------+--------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key           | key_len | ref   | rows   | filtered | Extra                 |
+----+-------------+-------+------------+------+---------------+---------------+---------+-------+--------+----------+-----------------------+
|  1 | SIMPLE      | santa | NULL       | ref  | behavior_name | behavior_name | 1       | const | 493938 |   100.00 | Using index condition |
+----+-------------+-------+------------+------+---------------+---------------+---------+-------+--------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)

Note (Code 1003): /* select#1 */ select `kris`.`santa`.`name` AS `name` from `kris`.`santa` where (`kris`.`santa`.`behavior` = 'nice') order by `kris`.`santa`.`name`

In the Code 1003 Note we still see the exact same reconstituted query, but as can be seen in the plan annotastions, the internal handling changes – so the optimizer has not been working on this query at all times, but on some intermediary representation.

The ‘using index condition’ annotation points to Index Condition Pushdown Optimization being used. In our example, this is not good.

Worse than sorting: Selectivity

The column we select on is a column with a cardinality of 2: behavior can be either naughty or nice. That means, in an equidistribution, around half of the values are naughty, the other half is nice.

Data from disk is read in pages of 16 KB. If one row in a page matches, the entire page has to be read from disk. In our example, we have a row length of around 200 Byte, so we end up with 75-80 records per page. Half of them will be nice, so with an average of around 40 nice records per page, we will very likely have to read all pages from disk anyway.

Using the index will not decrease the amount of data read from disk at all. In fact we will have to read the index pages on top of the data pages, so using an index on a low cardinality column has the potential of making the situation slightly worse than even a full table scan.

Generally speaking, defining an index on a low cardinality column is usually not helpful – if there are 10 or fewer values, benchmark carefully and decide, or just don’t define an index.

In our case, the index is not even equidistributed, but biased to 90% nice. We end up with mostly nice records, so we can guarantee that all data pages will be read for the SQL SELECT * FROM santa WHERE behavior = "nice", and the index usage will not be contributing in any positive way.

We could try to improve the query by adding conditions to make it more selective. For example, we could ask for people close to our current position, using an RTREE index such as this:

kris@localhost [kris]> ALTER TABLE santa ADD SPATIAL INDEX (loc);
...
kris@localhost [kris]> set @rect = 'polygon((10 10, 10 20, 20 20, 20 10, 10 10 ))';
kris@localhost [kris]> select * from santa where mbrcovers(st_geomfromtext(@rect), loc);
...
1535 rows in set (3.53 sec)

The ALTER defines a spatial index (an RTREE), which can help to speed up coordinate queries.

The SET defines a coordinate rectangle around our current position (supposedly 15/15).

We then use the mbrcovers() function to find all points loc that are covered by the @rect. It seems to be somewhat complicated to get MySQL to actually use the index, but I have not been investigating deeply.

If we added an ORDER BY name here, we would see using filesort again, because data is retrieved in RTREE order, if the index loc is used, but we want output in name order.

Conclusion

  • The Santa query is inefficient, but likely sorting twice is not the root cause for that.
    • The optimizer will be able to merge the multiple sorts and be able to deliver the result with one or no sorting, depending on our index construction.
    • The optimizer is not using the reconstituted query shown in the warning to plan the execution, and that is weird.
  • Selectivity matters, especially for indices on low cardinality columns.
    • Asking for all nice behaviors on a naughty/nice column is usually not benefitting from index usage.
    • Additional indexable conditions that improve selectivity can help, a lot.

technology

via Planet MySQL https://ift.tt/2iO8Ob8

December 30, 2020 at 06:56AM

How Golf Balls Are Made

How Golf Balls Are Made

https://ift.tt/2L2Pfya

How Golf Balls Are Made

Link

Tens of millions of golf balls are made every year. In this clip from Golf Town, they take us inside one of Titleist’s factories to see how they make their Pro V1 and Pro V1x golf balls. The process starts with a rubber sheet, which is formed and smoothed, then encased in a dimpled urethane covering before painting and packaging.

fun

via The Awesomer https://theawesomer.com

December 30, 2020 at 08:00AM

OK, gun nuts, this one’s for you!

OK, gun nuts, this one’s for you!

https://ift.tt/38L7Yq5

 

Ian McCollum of Forgotten Weapons has just released his latest video, in which he examines the firearms used in the original Star Wars movie, released in 1977.  They were based on real firearms, but embellished with add-on components and props to look more like science fiction weapons.

I found the presence of an OEG (occluded eye gunsight) particularly interesting, because this was originally developed in South Africa (a few years after Star Wars came out).  I used one of the first models to be produced there, and found it intriguing.  Basically, one doesn’t look through the sight at all:  it’s a solid object that can’t be seen through.  One keeps both eyes open, so that with one eye one sees the target, and with the other the red dot image in the otherwise blank sight.  One’s brain superimposes the dot on the target, making it relatively easy to hit what one’s aiming at.

I must admit, though, I prefer today’s red dot sights, where I can see the target through the sight.

Peter

non critical

via Bayou Renaissance Man https://ift.tt/1ctARFa

December 30, 2020 at 01:05PM

Add All the Ports to Your Laptop with $34 off Vava’s 12-in-1 Docking Station

Add All the Ports to Your Laptop with $34 off Vava’s 12-in-1 Docking Station

https://ift.tt/2EONRMO


Best Tech DealsBest Tech DealsThe best tech deals from around the web, updated daily.

Vava 12-in-1 USB-C Docking Station | $66 | Amazon | Clip coupon + code: KINJA1228

Many laptops these days sacrifice extensive ports in the favor of being as thin and light as possible, which has its obvious benefits and drawbacks. That’s great for easy portability, but can sometimes be a drag when you need to plug in a device or if you want to make your laptop the center of a more robust home office setup.

There are all sorts of USB-C hubs available, but Vava’s 12-in-1 Docking Station is one of the most port-packed options we’ve seen at an affordable price. Simply plug it into a USB-C port and you’ll add two USB 3.0 ports, two USB 2.0 ports, a USB-C PD port, SD and microSD card readers, an Ethernet port, a 3.5mm headphone jack, DC in port, and two HDMI ports. Those HDMI ports enable dual-monitor 4K/60fps action with compatible laptops, letting you turn your slim notebook into a beast of a home PC.

The Vava 12-in-1 Docking Station usually runs $100, but right now when you clip the Amazon coupon and input the exclusive promo code KINJA1228, you’ll drop it down to just $66. If you need a more robust hub like this, it’s a bargain.

G/O Media may get a commission

This deal was originally published in October 2020 by Andrew Hayward and was updated with new information on 12/28/2020.


Tech

via Lifehacker https://lifehacker.com

December 28, 2020 at 01:48PM

Python Dash: How to Build a Beautiful Dashboard in 3 Steps

Python Dash: How to Build a Beautiful Dashboard in 3 Steps

https://ift.tt/2Kx8Arx

Data visualization is an important toolkit for a data scientist. Building beautiful dashboards is an important skill to acquire if you plan to show your insights to a C-Level executive. In this blog post you will get an introduction to a visualization framework in Python. You will learn how to build a dashboard from fetching the data to creating interactive widgets using Dash – a visualization framework in Python.

Introduction to Dash

The dash framework can be divided into two components

  1. Layouts: Layout is the UI element of your dashboard. You can use components like Button, Table, Radio buttons and define them in your layout.
  2. Callbacks: Callbacks provide the functionality to add reactivity to your dashboard. It works by using a decorator function to define the input and output entities. 

In the next section you will learn how to build a simple dashboard to visualize the marathon performance from 1991 to 2018. 

Importing the libraries

Let us first import all the import libraries

import dash
import dash_core_components as dcc
import dash_html_components as html
import dash_split_pane
import plotly.express as px
import pandas as pd
from datetime import datetime

We are importing the pandas library to load the data and the dash library to build the dashboard. 

The plotly express library is built on top of ploty to provide some easy-to-use functionalities for data visualization.

First we will begin by downloading the data. The data can be accessed on Kaggle using the following link   

Step 1: Initializing a Dash App

We start by initializing a dash app and using the command run_server to start the server.

external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css']
 
app = dash.Dash(__name__, external_stylesheets=external_stylesheets)
 
 
if __name__ == '__main__':
   app.run_server(debug=True)

Step 2: Building the Layout

We will start by dividing our UI layer into two parts  – the left pane will show the settings window which will include an option to select the year. The right pane will include a graphical window displaying a bar plot.

app.layout = html.Div(children=[
   html.H1(children='World Marathon Analysis',
    style={'textAlign': 'center'}),
  
   dash_split_pane.DashSplitPane(
   children=[
  
   html.Div(children=[
        html.H1(children='Settings', style={'textAlign': 'center'}),
           ], style={'margin-left': '50%', 'verticalAlign': 'middle'}),
   html.Div(children=[
        html.H1(children='Graph View', style={'textAlign': 'center'}),
            ])
   ],
   id="splitter",
   split="vertical",
   size=1000,
)
  
])

We construct two div elements- one for the left pane and the other for the right pane. To align the header elements to the center we use the style tag and using standard CSS syntax to position the HTML elements.

If you now start the server and go to your browser on localhost:8050, you will see the following window.

Step 3: Creating the Dropdown Widget and the Graphical Window

Once we have the basic layout setup we can continue with the remaining parts.

Loading the Data  

We begin by loading the data using the pandas library

def convert_to_time(time_in_some_format):
   time_obj =  datetime.strptime(time_in_some_format, '%H:%M:%S').time()
   return time_obj
 
def get_data():
  df = pd.read_csv('world_marathon_majors.csv', engine="python")
  df['time'] = df['time'].apply(convert_to_time)
  return df

We create two functions to load the data and convert the time value to datetime object values.

The table below shows the first five rows of the dataset.

Every row consists of

  1. The Year the marathon took place
  2. The winner of the marathon in that year
  3. The gender of the winner
  4. The country the winner represents
  5. The time to finish the race
  6. The country in which the marathon took place.

Extending the Layout

The next step is to extend our layout layer by adding the dropdown widget and the graphical window.

app.layout = html.Div(children=[
   html.H1(children='World Marathon Analysis',
    style={'textAlign': 'center'}),
  
   dash_split_pane.DashSplitPane(
   children=[
  
   html.Div(children=[
        html.H1(children='Settings', style={'textAlign': 'center'}),
        dcc.Dropdown(id='dropdown-menu', options=[{'label':x, 'value': x} for x in range(df['year'].min(), df['year'].max()+1)],
         value=df['year'].max(),
         style={'width': '220px','font-size': '90%','height': '40px',}
        )
    ], style={'margin-left': '50%', 'verticalAlign': 'middle'}),
   html.Div(children=[
        html.H1(children='Graph View', style={'textAlign': 'center'}),
        dcc.Graph( id='input-graph',figure=get_default_data())
    ]) 
   ],
   id="splitter",
   split="vertical",
   size=1000,
)
])

We give the dropdown widget a unique id called dropdown-menu and the graphical window is given an id input-graph. 

Callbacks

Callbacks are used to enable communication between two widgets. 

We define a function called update_output_div which takes the year value whenever the dropdown menu is changed. On every change in the dropdown value the function update_output_div is executed and a bar plot is drawn to indicate the top countries which won the race.

@app.callback(
   dash.dependencies.Output('input-graph', 'figure'),
   [dash.dependencies.Input('dropdown-menu', 'value')]
)
def update_output_div(value):
   test_sample = df[df['year'] == value]
   test_sample = test_sample.groupby('country')['time'].min().reset_index()
   tt = test_sample.sort_values(by=['time'])
   fig = px.bar(tt, y='country', x='time', orientation='h', hover_data=["time"], )
   return fig

Live Demo

Let us now see the dashboard in action.

In this blog post you learned how to build a simple dashboard in Python. You can extend the above dashboard to include more widgets and displaying more graphs for further analysis.

The post Python Dash: How to Build a Beautiful Dashboard in 3 Steps first appeared on Finxter.

Python

via Finxter https://ift.tt/2HRc2LV

December 28, 2020 at 10:59AM

2-Acre Vertical Farm Run By AI and Robots Out-Produces 720-Acre Flat Farm

2-Acre Vertical Farm Run By AI and Robots Out-Produces 720-Acre Flat Farm

https://ift.tt/3rxeZDx

schwit1 quotes Intelligent Living: Plenty is an ag-tech startup in San Francisco, co-founded by Nate Storey, that is reinventing farms and farming. Storey, who is also the company’s chief science officer, says the future of farms is vertical and indoors because that way, the food can grow anywhere in the world, year-round; and the future of farms employ robots and AI to continually improve the quality of growth for fruits, vegetables, and herbs. Plenty does all these things and uses 95% less water and 99% less land because of it. Plenty’s climate-controlled indoor farm has rows of plants growing vertically, hung from the ceiling. There are sun-mimicking LED lights shining on them, robots that move them around, and artificial intelligence (AI) managing all the variables of water, temperature, and light, and continually learning and optimizing how to grow bigger, faster, better crops. These futuristic features ensure every plant grows perfectly year-round. The conditions are so good that the farm produces 400 times more food per acre than an outdoor flat farm. Another perk of vertical farming is locally produced food. The fruits and vegetables aren’t grown 1,000 miles away or more from a city; instead, at a warehouse nearby. Meaning, many transportation miles are eliminated, which is useful for reducing millions of tons of yearly CO2 emissions and prices for consumers. Imported fruits and vegetables are more expensive, so society’s most impoverished are at an extreme nutritional disadvantage. Vertical farms could solve this problem.


Read more of this story at Slashdot.

geeky

via Slashdot https://slashdot.org/

December 27, 2020 at 07:28PM

A question from my childhood answered

A question from my childhood answered

https://ift.tt/3nPQIGt

I saw this posted at Wirecutter’s.

I grew up with the audiobooks of James Herriot’s All Creatures Great and Small series, narrated by Christopher Timothy, who played James Herriot MRCVS in the BBC series (which I also love).

A few times n the book series, James mentions having to put down an animal with a humane killer.  I never knew that was a specific device until I saw this video.

I found this extremely interesting because it answered a question from my childhood that I never knew I had.

guns

via https://gunfreezone.net

December 26, 2020 at 06:25AM

How Gift Wrapping Paper is Made: Rotary Screen Printing

How Gift Wrapping Paper is Made: Rotary Screen Printing

https://ift.tt/2KvoT85

If you thought gift wrapping paper was wasteful as an end product, wait ’til you see the production method. In rotary screen printing, each color requires its own copper cylinder:

Source

Here’s a more in-depth look at how the process works:

I’ll stick with newspaper.

fun

via Core77 https://ift.tt/1KCdCI5

December 23, 2020 at 12:49PM

CMMG Releases Shortest and Most Compact BANSHEE .22LR AR-15

CMMG Releases Shortest and Most Compact BANSHEE .22LR AR-15

https://ift.tt/34CMB96

CMMG Releases Shortest and Most Compact BANSHEE .22LR AR-15
CMMG Releases Shortest and Most Compact BANSHEE .22LR AR-15

U.S.A.-(AmmoLand.com)- CMMG is proud to introduce the shortest and most compact BANSHEE to date. This new line-up is chambered in .22LR and features a capped lower receiver with no buffer tube (receiver extension).

This ultra-compact BANSHEE is made possible by CMMG’s new .22LR End Cap – which is a new way to transform your .22LR AR15 build.

The .22LR End Cap is the perfect accessory that shortens your .22LR AR15 by replacing the need for a receiver extension and buffer assembly. The .22LR End Cap is compatible with all CMMG .22LR AR Conversion Kits, as well as any AR15 that uses a dedicated CMMG .22LR bolt carrier group and barrel.

The .22LR End Cap is available in two variations: standard, with a smooth exterior and CMMG logo (.22LR End Cap LOGO), and QD (quick-detach), which has an attachment point machined into the exterior for attaching a QD sling. Installing the .22LR End Cap is made easy by securing the .22LR End Cap on the back of the lower receiver with a 3/8” hex wrench, in lieu of the buffer tube.

The .22LR End Cap with CMMG logo can be purchased separately for $24.95 and the QD End Cap for $29.95.

.22LR End Cap with CMMG logo (left) QD End Cap (Right)
.22LR End Cap with CMMG logo (left) QD End Cap (Right)

BANSHEE lower groups and complete BANSHEE .22LR pistols are offered with the .22 LR End Cap preinstalled: the BANSHEE 100 Series comes with the .22 LR End Cap LOGO and the BANSHEE 200 and 300 Series come with the QD End Cap. MSRP on the complete BANSHEE pistols range from $799.95 to $1,024.95.

For more information on the .22LR End Cap and all BANSHEE models, please visit www.CMMGinc.com.

CMMG Guarantee:

All CMMG products are covered under the CMMG Lifetime Quality Guarantee. Conditioned on being a Limited Warranty of use, maintenance, and cleaning of the product in accordance with CMMG, Inc.’s instructions to be free of defects in material and workmanship. CMMG will repair, replace or substitute part(s) as determined in the sole and absolute discretion of CMMG Inc at no charge to the purchaser or provider. Complete limited warranty information can be found at CMMGinc.com/tech-support


About CMMG:

CMMG began in central Missouri in 2002 and quickly developed into a full-time business because of its group of knowledgeable and passionate firearms enthusiasts committed to quality and service. Its reputation was built on attention to detail, cutting edge innovation and the superior craftsmanship that comes from sourcing all their own parts. By offering high quality AR rifles, parts and accessories, CMMG’s commitment to top-quality products and professional service is as deep today as it was when it began.

For more information, visit CMMGinc.comCMMG logo

The post CMMG Releases Shortest and Most Compact BANSHEE .22LR AR-15 appeared first on AmmoLand.com.

guns

via AmmoLand.com https://ift.tt/2okaFKE

December 23, 2020 at 01:07PM